1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
|
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(C)opyright 2025, Leafscale, LLC - https://www.leafscale.com
Project: Zygaena
Filename: remove.reef
Authors: Chris Tusa <chris.tusa@leafscale.com>
License: <see LICENSE file included with this source code>
Description: Remove command - uninstall packages
******************************************************************************/
module commands.remove
import sys.args
import sys.process
import io.file
import io.dir
import io.path
import core.str
import core.config
import core.database
import core.pkgdb
import core.package
import core.port
import util.prompt
import util.mtree
import util.checksum
import types
import util.color
import fs.stat
import fs.ops
import util.priv
import exitcodes as ec
import core.result as res
export
fn execute(opts: types.GlobalOptions): int
end export
fn execute(opts: types.GlobalOptions): int
let argc = args.count()
// Package arguments should be after the command
if argc <= opts.cmd_index + 1
print_usage()
return ec.EXIT_USAGE()
end if
// Load configuration and apply command-line overrides
let base_cfg = config.load()
let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix, opts.no_chroot)
let root_path = config.get_root(cfg)
// Show root if set (verbose mode)
if opts.verbose and str.length(root_path) > 0
color.print_info("Using root: " + root_path)
end if
// Collect packages to remove
mut packages: [string] = new [string](256)
mut pkg_count = 0
mut i = opts.cmd_index + 1
while i < argc and pkg_count < 256
let pkg_name = args.get(i)
// Skip flags and their values
if pkg_name == "--root" or pkg_name == "--prefix"
i = i + 2 // Skip flag and its value
continue
elif pkg_name == "--no-chroot-scripts"
i = i + 1 // valueless flag: skip only itself
continue
end if
if str.starts_with(pkg_name, "-")
i = i + 1
continue
end if
// Check if installed (use rooted version if root is set)
let installed = check_installed(pkg_name, root_path)
if installed
packages[pkg_count] = pkg_name
pkg_count = pkg_count + 1
else
color.print_warning(pkg_name + " is not installed")
end if
i = i + 1
end while
if pkg_count == 0
color.print_info("No packages to remove")
return ec.EXIT_SUCCESS()
end if
// Check for reverse dependencies (packages that depend on these)
mut dependents: [string] = new [string](256)
mut dependent_count = 0
i = 0
while i < pkg_count
let pkg_name = packages[i]
let count = get_reverse_dependencies(pkg_name, dependents, dependent_count, 256)
dependent_count = count
i = i + 1
end while
// Warn if there are dependents
if dependent_count > 0
color.print_warning("The following installed packages depend on packages being removed:")
i = 0
while i < dependent_count
println(" - " + dependents[i])
i = i + 1
end while
// Require --force to proceed
if not opts.force
color.print_error("Use --force to remove packages with dependents")
return ec.EXIT_DEPS_CONFLICT()
end if
color.print_warning("Proceeding with removal (--force specified)")
end if
// Ask for confirmation (unless -y flag)
if not prompt.confirm_remove(packages, pkg_count, dependents, dependent_count, opts)
color.print_info("Removal cancelled")
return ec.EXIT_CANCELLED()
end if
// Dry-run: show what would be done and exit
if opts.dry_run
color.print_info("Dry run — no changes made")
return ec.EXIT_SUCCESS()
end if
// Remove packages
mut all_success = true
i = 0
while i < pkg_count
if not remove_package(packages[i], root_path, cfg)
color.print_error("Failed to remove: " + packages[i])
all_success = false
end if
i = i + 1
end while
if all_success
return ec.EXIT_SUCCESS()
else
return ec.EXIT_REMOVE_FAILED()
end if
end execute
// Check if package is installed, using rooted check if root is set
fn check_installed(name: string, root_path: string): bool
if str.length(root_path) > 0
return database.is_installed_rooted(name, root_path)
end if
return database.is_installed(name)
end check_installed
proc print_usage()
println("Usage: coral remove <package...>")
println("")
println("Remove one or more installed packages.")
println("")
println("Arguments:")
println(" <package> Name of installed package to remove")
println("")
println("Options:")
println(" -y, --yes Don't ask for confirmation")
println(" -f, --force Remove even if other packages depend on it")
println("")
println("Examples:")
println(" coral remove vim")
println(" coral remove vim nano htop")
println(" coral remove --force libfoo")
end print_usage
fn remove_package(pkg_name: string, root_path: string, cfg: config.Config): bool
// Check if package is installed (use rooted version if root is set)
if not check_installed(pkg_name, root_path)
color.print_error(pkg_name + " is not installed")
return false
end if
color.print_action("Removing " + pkg_name + "...")
let install_root = get_actual_root(root_path)
// Resolve script execution context; abort early if confinement is required but impossible.
// Defense-in-depth: main.reef already bars non-root for all non-query commands, so this abort
// is currently unreachable via the CLI. Kept so confinement fails safely if that global gate is
// ever relaxed.
if package.script_wants_chroot(install_root, config.get_chroot_scripts(cfg)) and not priv.is_root()
color.print_error("--root " + install_root + " runs maintainer scripts confined to the target via chroot, which requires root privileges.")
color.print_error("Re-run as root, or pass --no-chroot-scripts to run them on the host (unconfined).")
return false
end if
let script_ctx = package.resolve_script_ctx(install_root, config.get_chroot_scripts(cfg), priv.is_root())
// Get package info (use rooted version if root is set)
let pkg = get_installed_pkg(pkg_name, root_path)
color.print_info("Package: " + pkg.info.name + " " + pkg.info.version)
// Get file list from database (use rooted version if root is set)
mut files: [string] = new [string](4096)
let file_count = get_files_list(pkg_name, files, 4096, root_path)
if file_count == 0
color.print_warning("No files recorded for package")
else
color.print_info("Removing " + int_to_str(file_count) + " files...")
end if
// Run pre-remove script if present (abort on failure)
let scripts_dir = database.get_scripts_dir(pkg_name)
if database.has_stored_scripts(pkg_name)
color.print_info("Running pre-remove script...")
if not package.run_stored_pre_remove(script_ctx, scripts_dir, pkg.info.version)
color.print_error("Pre-remove script failed, aborting removal of " + pkg_name)
return false
end if
end if
// Load config file list and manifest checksums for config protection
mut config_files: [string] = new [string](256)
let config_count = database.get_config_files(pkg_name, config_files, 256)
mut manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
mut manifest_count = 0
let manifest_path = database.get_manifest_path(pkg_name)
if file.fileExists(manifest_path)
let manifest_content = res.unwrap_or(file.readFile(manifest_path), "")
if str.length(manifest_content) > 0
manifest_count = mtree.parse_manifest(manifest_content, manifest_entries, 8192)
end if
end if
// Remove files (with proper root path, respecting config protection)
let actual_root = get_actual_root(root_path)
if not remove_files(files, file_count, actual_root, config_files, config_count, manifest_entries, manifest_count)
color.print_warning("Some files could not be removed")
// Continue anyway - unregister from database
end if
// Run post-remove script if present (warn on failure, don't abort)
if database.has_stored_scripts(pkg_name)
color.print_info("Running post-remove script...")
if not package.run_stored_post_remove(script_ctx, scripts_dir, pkg.info.version)
color.print_warning("Post-remove script failed")
end if
end if
// Unregister from database (use rooted version if root is set)
color.print_info("Unregistering package...")
if not unregister_pkg(pkg_name, root_path)
color.print_error("Failed to unregister package from database")
return false
end if
// Update package database indexes
if not pkgdb.rebuild_indexes_rooted(root_path)
color.print_warning("Failed to update package indexes")
end if
color.print_success("Removed " + pkg_name)
return true
end remove_package
// Get installed package info, using rooted check if root is set
fn get_installed_pkg(name: string, root_path: string): types.InstalledPackage
if str.length(root_path) > 0
return database.get_installed_rooted(name, root_path)
end if
return database.get_installed(name)
end get_installed_pkg
// Get files list, using rooted check if root is set
fn get_files_list(name: string, files: [string], max_count: int, root_path: string): int
if str.length(root_path) > 0
return database.get_files_rooted(name, files, max_count, root_path)
end if
return database.get_files(name, files, max_count)
end get_files_list
// Unregister package, using rooted version if root is set
fn unregister_pkg(name: string, root_path: string): bool
if str.length(root_path) > 0
return database.unregister_package_rooted(name, root_path)
end if
return database.unregister_package(name)
end unregister_pkg
// Get actual root path (returns "/" if empty)
fn get_actual_root(root_path: string): string
if str.length(root_path) == 0
return "/"
end if
return root_path
end get_actual_root
// Remove files from system, respecting config file protection
fn remove_files(files: [string], file_count: int, root_path: string, config_files: [string], config_count: int, manifest_entries: [mtree.ManifestEntry], manifest_count: int): bool
mut all_success = true
// Remove files in reverse order
mut i = file_count - 1
while i >= 0
let rel_path = files[i]
let full_path = path.join_path(root_path, rel_path)
if stat.is_symlink(full_path) or stat.exists(full_path)
// Check if this is a config file that the user has modified
if config_count > 0 and package.is_config_file(rel_path, config_files, config_count)
// Look up the package's sha256 from the manifest
let pkg_sha = find_manifest_sha256(rel_path, manifest_entries, manifest_count)
if str.length(pkg_sha) > 0
let installed_sha = checksum.sha256_file(full_path)
if not str.equals(installed_sha, pkg_sha)
// User has modified this config — preserve it
color.print_info("Preserving modified config: " + rel_path)
i = i - 1
continue
end if
end if
end if
if not res.is_ok(ops.remove_file(full_path))
color.print_warning("Failed to remove: " + full_path)
all_success = false
end if
end if
i = i - 1
end while
// Try to remove empty parent directories
i = file_count - 1
while i >= 0
let rel_path = files[i]
let full_path = path.join_path(root_path, rel_path)
let parent = path.dirname(full_path)
if str.length(parent) > str.length(root_path) and dir.dir_exists(parent)
dir.remove_dir(parent)
end if
i = i - 1
end while
return all_success
end remove_files
// Look up the sha256 for a given relative path in the manifest entries
fn find_manifest_sha256(rel_path: string, entries: [mtree.ManifestEntry], count: int): string
mut i = 0
while i < count
let epath = mtree.entry_path(entries[i])
// Compare with and without ./ prefix
mut entry_rel = epath
if str.starts_with(epath, "./")
entry_rel = str.substring(epath, 2, str.length(epath) - 2)
end if
if str.equals(entry_rel, rel_path)
return mtree.entry_sha256(entries[i])
end if
i = i + 1
end while
return ""
end find_manifest_sha256
// Get packages that depend on the given package
// Returns new total count (existing + new dependents)
fn get_reverse_dependencies(pkg_name: string, dependents: [string], current_count: int, max_count: int): int
mut count = current_count
// List all installed packages
mut installed: [string] = new [string](512)
let installed_count = database.list_installed(installed, 512)
mut i = 0
while i < installed_count and count < max_count
let other_pkg = installed[i]
// Skip the package being removed and already listed dependents
if str.equals(other_pkg, pkg_name)
i = i + 1
continue
end if
// Check if already in dependents list
if array_contains(dependents, count, other_pkg)
i = i + 1
continue
end if
// Try to find this package's port to get its dependencies
let port_result = port.find(other_pkg)
if port_result.success
let p = port_result.port
// Check runtime dependencies
if depends_on(p.deps.runtime, p.deps.runtime_count, pkg_name)
dependents[count] = other_pkg
count = count + 1
end if
end if
i = i + 1
end while
return count
end get_reverse_dependencies
// Check if a dependency list contains the given package
fn depends_on(deps: [string], dep_count: int, pkg_name: string): bool
mut i = 0
while i < dep_count
let dep = deps[i]
// Handle alternative dependencies (format: base:opt1,opt2)
let colon_pos = str.index_of_char(dep, ':')
mut base_dep = dep
if colon_pos > 0
base_dep = str.substring(dep, 0, colon_pos)
end if
if str.equals(base_dep, pkg_name)
return true
end if
i = i + 1
end while
return false
end depends_on
// Helper to check if array contains a string
fn array_contains(arr: [string], count: int, s: string): bool
mut i = 0
while i < count
if str.equals(arr[i], s)
return true
end if
i = i + 1
end while
return false
end array_contains
// Helper: convert int to string
fn int_to_str(n: int): string
if n == 0
return "0"
end if
mut negative = false
mut value = n
if n < 0
negative = true
value = 0 - n
end if
mut result = ""
while value > 0
let digit = value % 10
result = str.concat(str.substring("0123456789", digit, 1), result)
value = value / 10
end while
if negative
result = str.concat("-", result)
end if
return result
end int_to_str
end module
|