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
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
|
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(C)opyright 2025, Leafscale, LLC - https://www.leafscale.com
Project: Zygaena
Filename: install.reef
Authors: Chris Tusa <chris.tusa@leafscale.com>
License: <see LICENSE file included with this source code>
Description: Install command - install packages from archives
******************************************************************************/
module commands.install
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.groups
import core.repository
import core.signing
import core.resolver
import core.port
import core.portindex
import util.checksum
import util.mtree
import util.http as uhttp
import util.prompt
import util.version as ver
import types
import util.color
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 start 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)
uhttp.enable_proxy(config.get_use_proxy(cfg))
// Auto-refresh port index if configured
if config.get_auto_refresh(cfg)
portindex.rebuild_port_index()
end if
// Show root/prefix if set (verbose mode)
if opts.verbose
let root = config.get_root(cfg)
let prefix = config.get_prefix(cfg)
if str.length(root) > 0
color.print_info("Using root: " + root)
end if
if str.length(prefix) > 0
color.print_info("Using prefix: " + prefix)
end if
end if
// Collect all packages to install (expanding groups)
mut packages: [string] = new [string](256)
mut pkg_count = 0
mut direct_files: [string] = new [string](16)
mut direct_count = 0
// Process each package argument (starting after the command)
mut i = opts.cmd_index + 1
while i < argc and pkg_count < 256
let pkg_arg = args.get(i)
// Skip flags and their values
if pkg_arg == "--root" or pkg_arg == "--prefix"
i = i + 2 // Skip flag and its value
continue
elif pkg_arg == "--no-chroot-scripts"
i = i + 1 // valueless flag: skip only itself
continue
end if
if str.starts_with(pkg_arg, "-")
i = i + 1
continue
end if
// Check if this is a direct file path to a .pkg.tar.xz
if str.ends_with(pkg_arg, ".pkg.tar.xz") and file.fileExists(pkg_arg)
direct_files[direct_count] = pkg_arg
direct_count = direct_count + 1
i = i + 1
continue
end if
// Check if this is a group reference
if groups.is_group(pkg_arg)
// Expand group to member packages
mut group_members: [string] = new [string](128)
let member_count = groups.expand_group(pkg_arg, group_members, 128)
if member_count == 0
color.print_error("Group not found or empty: " + pkg_arg)
i = i + 1
continue
end if
color.print_info("Expanding group " + pkg_arg + " (" + int_to_str(member_count) + " packages)")
// Add all group members to packages list
mut j = 0
while j < member_count and pkg_count < 256
packages[pkg_count] = group_members[j]
pkg_count = pkg_count + 1
j = j + 1
end while
else
// Regular package
packages[pkg_count] = pkg_arg
pkg_count = pkg_count + 1
end if
i = i + 1
end while
// Handle direct file installs — bypass resolver, install immediately
if direct_count > 0
let root = get_root(cfg)
let root_for_db = config.get_root(cfg)
if str.length(root_for_db) > 0
if not database.init_db_rooted(root_for_db)
color.print_error("Failed to initialize database in " + root_for_db)
return ec.EXIT_DB_ERROR()
end if
else
if not database.init_db()
color.print_error("Failed to initialize package database")
return ec.EXIT_DB_ERROR()
end if
end if
mut di = 0
while di < direct_count
if not install_package(direct_files[di], cfg, root_for_db, opts)
color.print_error("Failed to install: " + direct_files[di])
return ec.EXIT_INSTALL_FAILED()
end if
di = di + 1
end while
if pkg_count == 0
return ec.EXIT_SUCCESS()
end if
end if
// Resolve dependencies for all packages
// Use rooted resolver if installing to alternate root
mut install_order: [string] = new [string](512)
let root_for_resolve = config.get_root(cfg)
mut install_count = 0
if str.length(root_for_resolve) > 0
install_count = resolver.get_install_order_rooted(packages, pkg_count, install_order, 512, root_for_resolve)
else
install_count = resolver.get_install_order(packages, pkg_count, install_order, 512)
end if
// When force/reinstall is set, add requested packages that the resolver
// skipped (because they're already installed) back into the install order
if opts.force and install_count < pkg_count
mut fi = 0
while fi < pkg_count
let pkg = packages[fi]
if not array_contains(install_order, install_count, pkg)
install_order[install_count] = pkg
install_count = install_count + 1
end if
fi = fi + 1
end while
end if
if install_count == 0 and pkg_count > 0
color.print_warning("No packages to install (all already installed or not found)")
return ec.EXIT_SUCCESS()
end if
// Separate new installs from already-installed (for display)
mut to_install: [string] = new [string](512)
mut install_idx = 0
mut dep_idx = 0
while dep_idx < install_count
to_install[install_idx] = install_order[dep_idx]
install_idx = install_idx + 1
dep_idx = dep_idx + 1
end while
// Ask for confirmation (unless -y flag)
mut empty_upgrade: [string] = new [string](1)
if not prompt.confirm_install(to_install, install_idx, empty_upgrade, 0, opts)
color.print_info("Installation 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
// Get root path for installation
let root = get_root(cfg)
// Initialize database in the target root
let root_for_db = config.get_root(cfg)
if str.length(root_for_db) > 0
if not database.init_db_rooted(root_for_db)
color.print_error("Failed to initialize database in " + root_for_db)
return ec.EXIT_DB_ERROR()
end if
else
if not database.init_db()
color.print_error("Failed to initialize package database")
return ec.EXIT_DB_ERROR()
end if
end if
// Install all packages in resolved order (dependencies first)
i = 0
while i < install_count
let pkg_arg = install_order[i]
// Install this package
if not install_package(pkg_arg, cfg, root_for_db, opts)
color.print_error("Failed to install: " + pkg_arg)
color.print_error("Aborting installation - dependency failed")
return ec.EXIT_INSTALL_FAILED()
end if
i = i + 1
end while
return ec.EXIT_SUCCESS()
end execute
// 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
proc print_usage()
println("Usage: coral install <package...>")
println("")
println("Install one or more packages.")
println("")
println("Arguments:")
println(" <package> Package name or path to .pkg.tar.xz file")
println("")
println("Options:")
println(" -y, --yes Don't ask for confirmation")
println(" --force Force reinstall if already installed")
println("")
println("Examples:")
println(" coral install vim")
println(" coral install /path/to/package.pkg.tar.xz")
println(" coral install vim nano htop")
end print_usage
fn install_package(pkg_arg: string, cfg: config.Config, root_path: string, opts: types.GlobalOptions): bool
// Determine if this is a file path or package name
let pkg_path = resolve_package_path(pkg_arg, cfg)
if str.length(pkg_path) == 0
color.print_error("Package not found: " + pkg_arg)
return false
end if
// Verify signature if required (for local packages not already verified by resolve_package_path)
let require_signed = config.get_require_signed_packages(cfg)
if require_signed
let sig_path = pkg_path + ".sig"
if file.fileExists(sig_path)
color.print_info("Verifying package signature...")
let verify_result = signing.verify_package(pkg_path, sig_path)
if not verify_result.success or not verify_result.valid
color.print_error("Signature verification failed: " + verify_result.error)
return false
end if
color.print_success("Signature verified (key: " + verify_result.keyid + ")")
else
color.print_error("Package signature required but not found: " + sig_path)
return false
end if
end if
color.print_action("Installing " + pkg_arg + "...")
// Create extraction directory
let extract_dir = create_extract_dir(cfg)
if str.length(extract_dir) == 0
color.print_error("Failed to create extraction directory")
return false
end if
// Extract package
color.print_info("Extracting package...")
let result = package.extract_package(pkg_path, extract_dir)
if not result.success
color.print_error(result.error)
cleanup_extract_dir(extract_dir)
return false
end if
// Read package info
let info = package.read_pkginfo(extract_dir)
if str.length(info.name) == 0
color.print_error("Failed to read package info")
cleanup_extract_dir(extract_dir)
return false
end if
color.print_info("Package: " + info.name + " " + info.version)
// Check for conflicts with installed packages
if info.conflicts_count > 0
mut ci = 0
while ci < info.conflicts_count
let conflict = info.conflicts[ci]
if check_installed(conflict, root_path)
color.print_error("Package " + info.name + " conflicts with installed package: " + conflict)
if not opts.force
color.print_error("Use --force to override conflict check")
cleanup_extract_dir(extract_dir)
return false
end if
color.print_warning("Proceeding despite conflict (--force specified)")
end if
ci = ci + 1
end while
end if
// Check if already installed (in target root)
let already_installed = check_installed(info.name, root_path)
mut is_upgrade = false
mut old_version = ""
if already_installed
// Get old package info to check version
let old_pkg = get_old_installed(info.name, root_path)
old_version = old_pkg.info.version
if not str.equals(old_version, info.version)
// Different version — upgrade
is_upgrade = true
color.print_info("Upgrading " + info.name + " " + old_version + " -> " + info.version)
else
// Same version — skip or reinstall with --force
if not opts.force
color.print_warning(info.name + " " + info.version + " is already installed (use --force to reinstall)")
cleanup_extract_dir(extract_dir)
return true
end if
color.print_info("Reinstalling " + info.name + "...")
end if
end if
let install_root = get_root(cfg)
// 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).")
cleanup_extract_dir(extract_dir)
return false
end if
let script_ctx = package.resolve_script_ctx(install_root, config.get_chroot_scripts(cfg), priv.is_root())
if is_upgrade
// Upgrade flow: pre-upgrade → remove old files → install new files → post-upgrade
// Run pre-upgrade from stored scripts (old package)
let old_scripts = database.get_scripts_dir(info.name)
if database.has_stored_scripts(info.name)
color.print_info("Running pre-upgrade script...")
if not package.run_stored_pre_upgrade(script_ctx, old_scripts, old_version, info.version)
color.print_error("Pre-upgrade script failed, aborting")
cleanup_extract_dir(extract_dir)
return false
end if
end if
// Remove old files
color.print_info("Removing old version files...")
mut old_files: [string] = new [string](4096)
let old_file_count = get_old_files(info.name, old_files, 4096, root_path)
if old_file_count > 0
package.remove_files(old_files, old_file_count, install_root)
end if
// Load old manifest for config file checksum protection
mut old_manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192)
let old_manifest_count = load_old_manifest(info.name, root_path, old_manifest_entries, 8192)
// Install new files (with upgrade config protection)
color.print_info("Installing files...")
if old_manifest_count > 0
if not package.install_files_upgrade(extract_dir, install_root, old_manifest_entries, old_manifest_count)
color.print_error("Failed to install files")
cleanup_extract_dir(extract_dir)
return false
end if
else
if not package.install_files(extract_dir, install_root)
color.print_error("Failed to install files")
cleanup_extract_dir(extract_dir)
return false
end if
end if
// Run post-upgrade from new package's scripts
if package.has_scripts(extract_dir)
color.print_info("Running post-upgrade script...")
if not package.run_post_upgrade(script_ctx, extract_dir, old_version, info.version)
color.print_warning("Post-upgrade script failed")
end if
end if
else
// Fresh install flow
// Run pre-install script (abort on failure)
if package.has_scripts(extract_dir)
color.print_info("Running pre-install script...")
if not package.run_pre_install(script_ctx, extract_dir, info.version)
color.print_error("Pre-install script failed")
cleanup_extract_dir(extract_dir)
return false
end if
end if
// Install files to target root
color.print_info("Installing files...")
if not package.install_files(extract_dir, install_root)
color.print_error("Failed to install files")
cleanup_extract_dir(extract_dir)
return false
end if
// Run post-install script (warn on failure, don't abort)
if package.has_scripts(extract_dir)
color.print_info("Running post-install script...")
if not package.run_post_install(script_ctx, extract_dir, info.version)
color.print_warning("Post-install script failed")
end if
end if
end if
// Register in database (use rooted version if root is set)
color.print_info("Registering package...")
if not register_installed_fast_rooted(info, extract_dir, root_path)
color.print_error("Failed to register package in database")
// Files are installed but not registered - warn but don't fail
end if
// Store scripts for later use (removal)
if package.has_scripts(extract_dir)
if not store_scripts_rooted(info.name, extract_dir, root_path)
color.print_warning("Failed to store scripts for removal")
end if
end if
// Store config files list for upgrade handling
if not store_config_files_rooted(info.name, extract_dir, root_path)
color.print_warning("Failed to store config files list")
end if
// Update package database indexes
if not pkgdb.rebuild_indexes_rooted(root_path)
color.print_warning("Failed to update package indexes")
end if
// Cleanup
cleanup_extract_dir(extract_dir)
color.print_success("Installed " + info.name + " " + info.version)
return true
end install_package
// 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
// Register package using rooted version if root is set
fn register_installed_fast_rooted(info: types.PackageInfo, extract_dir: string, root_path: string): bool
mut files: [string] = new [string](1)
mut pkg = new_installed_pkg(info, files, 0)
if str.length(root_path) > 0
return database.register_from_extract_dir_rooted(pkg, extract_dir, root_path)
end if
return database.register_from_extract_dir(pkg, extract_dir)
end register_installed_fast_rooted
// Store scripts using rooted version if root is set
fn store_scripts_rooted(name: string, extract_dir: string, root_path: string): bool
if str.length(root_path) > 0
return database.store_scripts_rooted(name, extract_dir, root_path)
end if
return database.store_scripts(name, extract_dir)
end store_scripts_rooted
// Store config files using rooted version if root is set
fn store_config_files_rooted(name: string, extract_dir: string, root_path: string): bool
if str.length(root_path) > 0
return database.store_config_files_rooted(name, extract_dir, root_path)
end if
return database.store_config_files(name, extract_dir)
end store_config_files_rooted
// Find package file - either direct path, repository, or search in packages directory
fn resolve_package_path(pkg_arg: string, cfg: config.Config): string
// If it's a file path that exists, use it directly
if file.fileExists(pkg_arg)
return pkg_arg
end if
// Search in local packages cache first
let packages_dir = config.get_packages_dir(cfg)
// Find all matching packages and select highest version
let entries = res.unwrap_or(dir.list_dir(packages_dir), new [string](0))
let entry_count = entries.length()
// Collect all matching package files
mut best_file = ""
mut best_version = ""
mut best_release = 0
mut i = 0
while i < entry_count
let entry = entries[i]
// Check if entry starts with package name and has correct suffix
if str.starts_with(entry, pkg_arg + "-") and str.ends_with(entry, ".pkg.tar.xz")
// Extract version from filename
let entry_version = ver.extract_version_from_filename(entry, pkg_arg)
let entry_release = ver.extract_release_from_filename(entry)
// Compare with current best
if str.length(best_file) == 0
// First match
best_file = entry
best_version = entry_version
best_release = entry_release
else
// Compare versions
let cmp = ver.compare_versions(entry_version, best_version)
if cmp > 0
// Entry has higher version
best_file = entry
best_version = entry_version
best_release = entry_release
elif cmp == 0 and entry_release > best_release
// Same version but higher release
best_file = entry
best_version = entry_version
best_release = entry_release
end if
end if
end if
i = i + 1
end while
// Return the best matching package
if str.length(best_file) > 0
return path.join_path(packages_dir, best_file)
end if
// Also try with .pkg.tar.xz appended
let direct_path = path.join_path(packages_dir, pkg_arg + ".pkg.tar.xz")
if file.fileExists(direct_path)
return direct_path
end if
// Try to build from source if configured
if config.get_fallback_to_source(cfg)
let port_result = port.find(pkg_arg)
if port_result.success
color.print_info("No binary package found for " + pkg_arg + ", building from source...")
let build_cmd = "coral build -y " + pkg_arg
let build_pid = process.process_spawn_shell(build_cmd)
if build_pid > 0
let build_exit = process.process_wait(build_pid)
if build_exit == 0
// Build succeeded, look for the package it produced
let p = port_result.port
let built_pkg = p.info.name + "-" + p.info.version + "-" + int_to_str(p.info.release) + ".pkg.tar.xz"
let built_path = path.join_path(packages_dir, built_pkg)
if file.fileExists(built_path)
return built_path
end if
end if
end if
color.print_warning("Source build failed for " + pkg_arg + ", trying repositories...")
end if
end if
// Try to find in repositories and download
let remote_result = repository.find_remote_package(pkg_arg)
if remote_result.success
let remote_pkg = remote_result.pkg
color.print_info("Found " + pkg_arg + " " + remote_pkg.version + " in repository: " + remote_pkg.repo_name)
// Download the package
let downloaded_path = repository.download_package(remote_pkg)
if str.length(downloaded_path) > 0
// Verify checksum if available
if str.length(remote_pkg.sha256) > 0
color.print_info("Verifying checksum...")
if not checksum.verify_checksum(downloaded_path, remote_pkg.sha256)
color.print_error("Checksum verification failed!")
return ""
end if
color.print_success("Checksum verified")
end if
// Verify signature if required or available
let require_signed = config.get_require_signed_packages(cfg)
if require_signed or str.length(remote_pkg.signature) > 0
let sig_path = downloaded_path + ".sig"
// Download signature file if we don't have it
if not file.fileExists(sig_path) and str.length(remote_pkg.signature) > 0
color.print_info("Downloading signature...")
let sig_url = get_sig_url(remote_pkg)
if not download_signature(sig_url, sig_path)
if require_signed
color.print_error("Failed to download package signature")
return ""
else
color.print_warning("Could not download signature, skipping verification")
end if
end if
end if
// Verify signature if we have it
if file.fileExists(sig_path)
color.print_info("Verifying signature...")
let verify_result = signing.verify_package(downloaded_path, sig_path)
if verify_result.success and verify_result.valid
color.print_success("Signature verified (key: " + verify_result.keyid + ")")
else
if require_signed
color.print_error("Signature verification failed: " + verify_result.error)
return ""
else
color.print_warning("Signature verification failed: " + verify_result.error)
end if
end if
elif require_signed
color.print_error("Package signature required but not available")
return ""
end if
end if
return downloaded_path
end if
end if
return ""
end resolve_package_path
// Create a temporary extraction directory
fn create_extract_dir(cfg: config.Config): string
let work_dir = config.get_work_dir(cfg)
let pid = process.getpid()
let extract_dir = path.join_path(work_dir, "install_" + int_to_str(pid))
if dir.dir_exists(extract_dir)
// Clean existing
let cmd = "rm -rf \"" + extract_dir + "\""
let rm_pid = process.process_spawn_shell(cmd)
if rm_pid > 0
process.process_wait(rm_pid)
end if
end if
if not res.is_ok(dir.create_dir_all(extract_dir))
return ""
end if
return extract_dir
end create_extract_dir
// Cleanup extraction directory
fn cleanup_extract_dir(dir_path: string): bool
if str.length(dir_path) == 0
return true
end if
let cmd = "rm -rf \"" + dir_path + "\""
let pid = process.process_spawn_shell(cmd)
if pid < 0
return false
end if
process.process_wait(pid)
return true
end cleanup_extract_dir
// Get installation root from config (can be overridden with --root)
fn get_root(cfg: config.Config): string
let root = config.get_root(cfg)
if str.length(root) == 0
return "/"
end if
return root
end get_root
// Check for file conflicts with other installed packages
// TODO: Implement efficient conflict detection using a file database
fn check_conflicts(files: [string], file_count: int, pkg_name: string): bool
// Skip conflict checking for performance - file overwrites are handled
// by the manifest-driven installer and dependencies are managed separately
return true
end check_conflicts
// Helper to create InstalledPackage
fn new_installed_pkg(info: types.PackageInfo, files: [string], file_count: int): types.InstalledPackage
mut pkg = types.new_installed_package()
pkg.info = info
pkg.install_date = get_date_string()
pkg.install_reason = "explicit"
pkg.files = files
pkg.files_count = file_count
return pkg
end new_installed_pkg
// Get current date as string (simplified)
fn get_date_string(): string
// Use date command for simplicity
// TODO: Use proper time functions when available
return "2025-01-24"
end get_date_string
// Get installed package info (root-aware)
fn get_old_installed(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_old_installed
// Get old file list for a package (root-aware)
fn get_old_files(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_old_files
// Load old manifest from database for upgrade config protection
fn load_old_manifest(name: string, root_path: string, entries: [mtree.ManifestEntry], max_count: int): int
let manifest_path = database.get_manifest_path(name)
if not file.fileExists(manifest_path)
return 0
end if
let content = res.unwrap_or(file.readFile(manifest_path), "")
if str.length(content) == 0
return 0
end if
return mtree.parse_manifest(content, entries, max_count)
end load_old_manifest
// 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
// Build signature URL from remote package info
fn get_sig_url(pkg: repository.RemotePackage): string
mut base_url = pkg.repo_url
if not str.ends_with(base_url, "/")
base_url = base_url + "/"
end if
// If signature field contains a full URL, use it
if str.starts_with(pkg.signature, "http")
return pkg.signature
end if
// Otherwise construct from filename
return base_url + "packages/" + pkg.filename + ".sig"
end get_sig_url
// Download a signature file
fn download_signature(url: string, dest: string): bool
return uhttp.download(url, dest)
end download_signature
end module
|