380 Commits

Author SHA1 Message Date
Red Bear OS bf43c0a34f installer: commit collision-tracker WIP to satisfy clean-fork build gate
Pre-existing uncommitted work (materialized by the build's local-over-WIP
preflight): CollisionTracker detects config [[files]] (Layer 1) being
silently overwritten by package staging (Layer 2) — the D-Bus regression
class. Committed as-is so the fork has a clean HEAD for pkgar fingerprints;
no functional change intended by this commit beyond tracking it.
2026-07-18 16:07:53 +09:00
Red Bear OS 460d9530a5 config: warn on file-path override across included configs
Replace silent files.extend() with explicit override detection: when a
later-included config redefines a [[files]] path, warn on the console and
replace in place (last definition wins, no duplicate left). Makes the
previously-invisible, order-dependent config override visible.
2026-07-16 11:16:01 +09:00
Red Bear OS 1e9dcdee98 rb: reapply Red Bear patches on upstream 05bf2eb 2026-07-12 16:53:48 +03:00
Jeremy Soller 05bf2eb429 Merge branch 'syscall-update' into 'master'
Update redox-syscall

See merge request redox-os/installer!84
2026-07-09 10:47:24 -06:00
Wildan M d96c9f82fe Update redox-syscall 2026-07-09 22:24:03 +07:00
Jeremy Soller d195096aef Merge branch 'gui-fix' into 'master'
Fix build GUI and add CI check

See merge request redox-os/installer!83
2026-07-04 07:01:26 -06:00
Jeremy Soller e249630e51 Merge branch 'linux' into 'master'
gui: Implement sudo for linux

See merge request redox-os/installer!82
2026-07-03 06:06:30 -06:00
Wildan M 5b2f2ecd53 Fix build GUI and add CI check 2026-07-03 14:59:57 +07:00
Wildan M 3b4fd843a4 Update res config 2026-07-02 22:23:35 +07:00
Wildan M 227606752d Use builtin bytes format string 2026-06-16 14:48:13 +07:00
Wildan M 7166bee750 Implement sudo for linux 2026-06-16 14:02:40 +07:00
Wildan M fcdc88ded7 gui: Update libscosmic 2026-06-16 13:37:43 +07:00
Jeremy Soller 142b2d639e Merge branch 'fix/os-release-build-metadata' into 'master'
Added cookbook commit hash for /etc/os-release

Closes redox#1813

See merge request redox-os/installer!81
2026-05-27 05:22:01 -06:00
Frank Li e1b20c59c4 Added cookbook commit hash for /etc/os-release 2026-05-27 05:22:01 -06:00
Jeremy Soller 847dca49c8 Merge branch 'add-files-append' into 'master'
Add append support for filesystem files

Closes redox#1700

See merge request redox-os/installer!80
2026-05-21 06:44:21 -06:00
Ziwen Li(Frank) be3e5097bc Add append support for filesystem files 2026-05-21 12:23:49 +12:00
Jeremy Soller 948bfdcce9 Merge branch 'installer-merge' into 'master'
Installer merge, update winit

See merge request redox-os/installer!79
2026-04-18 06:57:31 -06:00
Wildan M c867ae1528 Update winit and installer 2026-04-18 05:05:35 +07:00
Wildan M 6ec7747b71 Merge commit '88143858c780c144f1fc08c5adbb696e94979a92' into installer-merge 2026-04-18 04:54:21 +07:00
Wildan M 88143858c7 Move to gui subdirectory 2026-04-18 04:51:00 +07:00
Jeremy Soller bac755f440 Merge branch 'update-deps' into 'main'
Update dependencies, Add a hint to run with sudo

See merge request redox-os/installer-gui!2
2026-04-11 06:40:53 -06:00
Wildan M aab8c5f5ea Add a hint to run with sudo 2026-04-11 07:00:57 +07:00
Wildan M 4ee2101d01 Update dependencies 2026-04-11 06:59:51 +07:00
Jeremy Soller 1c2534e44c Merge branch 'bump-deps' into 'master'
Update cargo versions

See merge request redox-os/installer!78
2026-03-30 10:59:09 -06:00
Wildan M a9d6a925e8 Update cargo versions 2026-03-30 21:44:17 +07:00
Jeremy Soller 0ace1223e4 Merge branch 'wrap-inst-feature' into 'master'
Wrap more deps behind installer feature

See merge request redox-os/installer!77
2026-03-19 08:25:52 -06:00
Wildan M 49f8f74125 Wrap more deps behind installer feature 2026-03-19 17:15:05 +07:00
Jeremy Soller ed6013c295 Merge branch 'bump-argon' into 'master'
Bump argon deps

See merge request redox-os/installer!76
2026-03-17 05:46:41 -06:00
Wildan M e79c1be43e Bump argon deps 2026-03-16 21:27:44 +07:00
Jeremy Soller b5b9bfd825 Merge branch 'bump-ver' into 'master'
Bump redox_syscall and version

See merge request redox-os/installer!75
2026-03-15 13:23:48 -06:00
Wildan M 8c2b0f9de7 Bump redox_syscall and version 2026-03-15 18:44:08 +07:00
Jeremy Soller 9800eac87c Merge branch 'misc_changes2' into 'master'
Make arg_parser non-optional

See merge request redox-os/installer!74
2026-03-11 14:42:10 -06:00
bjorn3 e61506c50f Make arg_parser non-optional
It is used by a Debug impl in the config code.
2026-03-11 21:01:48 +01:00
Jeremy Soller b29021c352 Merge branch 'misc_changes' into 'master'
Misc changes

See merge request redox-os/installer!73
2026-03-11 14:00:12 -06:00
bjorn3 474ff08431 Fix bootloader location 2026-03-11 20:53:46 +01:00
bjorn3 f691afabab Fix usage message 2026-03-11 20:31:03 +01:00
bjorn3 f74e3fad19 Make home directories 700 instead of 777 2026-03-11 20:12:40 +01:00
Jeremy Soller 3ef2cac56d Merge branch 'new-pkg' into 'master'
Update pkg to use new API

See merge request redox-os/installer!72
2026-03-08 13:25:42 -06:00
Wildan M aa9c978f90 Resolve ring dependency 2026-03-08 21:04:32 +07:00
Wildan M a6fd457652 Update pkg to use new API 2026-03-08 20:53:33 +07:00
Jeremy Soller 7806f87c56 Merge branch 'move_bootloader_to_user' into 'master'
Bootloader is now stored in /usr/lib/boot

See merge request redox-os/installer!71
2026-03-07 08:36:04 -07:00
bjorn3 70b8cb0e37 Bootloader is now stored in /usr/lib/boot 2026-03-07 16:30:35 +01:00
Jeremy Soller 397974cb68 Workaround for existing symlinks by overwriting them 2026-03-04 11:18:14 -07:00
Jeremy Soller 8765c51696 Log target of symlink 2026-03-04 10:53:22 -07:00
Jeremy Soller 2d3b2ed38a Improve some error reporting 2026-03-04 10:49:09 -07:00
Jeremy Soller 1840ceac85 Merge branch 'fix-redox-err' into 'master'
Fix redox compilation error

See merge request redox-os/installer!70
2026-02-14 12:31:56 -07:00
Wildan M 59d0872197 Fix redox compilation error 2026-02-15 01:47:13 +07:00
Jeremy Soller 7f918e544e Merge branch 'skel-copy' into 'master'
Copy /etc/skel to users directory

See merge request redox-os/installer!69
2026-02-14 06:41:32 -07:00
Wildan M 730eb9a606 Copy /etc/skel to users directory 2026-02-14 18:13:27 +07:00
Jeremy Soller a790f1c228 Merge branch 'local-err-help' into 'master'
Make missing file clear when locally installing

See merge request redox-os/installer!68
2026-02-10 05:59:32 -07:00
Wildan M b33f3c1a80 Make missing file clear when locally installing 2026-02-10 01:35:44 +07:00
Jeremy Soller b0f90da7b8 Merge branch 'cli-enh' into 'master'
Some improvement as a standalone CLI

See merge request redox-os/installer!67
2026-01-30 20:28:01 -07:00
Wildan M c4ed26e3d4 Don't confirm password if empty 2026-01-31 05:10:03 +07:00
Wildan M 753256cfaf Strip fallocate from CI test 2026-01-31 05:02:08 +07:00
Wildan M cfc7938ecc Shorten remote packages log 2026-01-31 04:53:30 +07:00
Wildan M 15de2b4272 Add help hint, remove repo_binary 2026-01-31 04:17:54 +07:00
Wildan M fed265252d Unify password prompt code 2026-01-31 04:17:11 +07:00
Wildan M d40619d66c Implement Display for Config 2026-01-31 04:15:10 +07:00
Jeremy Soller 9dd913f9da Update dependencies 2026-01-22 10:42:08 -07:00
Jeremy Soller 64f46052db 0.2.41 - update dependencies 2026-01-22 10:39:26 -07:00
Jeremy Soller eff771e640 0.2.40: downgrade libredox to 0.1.11 for redox_syscall 0.6 compatibility 2026-01-02 20:00:13 -07:00
Jeremy Soller 397749f1f5 0.2.39: update redox_syscall and redoxfs 2026-01-02 19:42:55 -07:00
Jeremy Soller 753c8af0e5 Merge branch 'expose-fuse-opt' into 'master'
Make FUSE feature can be opted out

See merge request redox-os/installer!66
2026-01-01 08:15:36 -07:00
Wildan M 846e8446e0 Make FUSE feature can be opted out 2026-01-01 14:46:54 +07:00
Jeremy Soller 385696d84a Merge branch 'update-redoxfs' into 'master'
Bump redoxfs

See merge request redox-os/installer!64
2025-12-16 11:08:07 -07:00
Wildan M ca9748f1ba Bump redoxfs 2025-12-16 22:51:05 +07:00
Jeremy Soller 7e697cfb3a Merge branch 'bump-ver' into 'master'
Expose custom mount API and bump version

See merge request redox-os/installer!63
2025-12-15 15:54:52 -07:00
Wildan M 86d2af6ca0 Expose custom mount API and bump version 2025-12-16 05:27:09 +07:00
Jeremy Soller 8ba8d6e15b Merge branch 'no-mount' into 'master'
Add no_mount option and tidy up flags

See merge request redox-os/installer!62
2025-12-15 12:38:41 -07:00
Wildan M 3dba5827f2 Try fix CI 2025-12-16 01:55:54 +07:00
Wildan M 433aa99660 Add no_mount option and tidy up flags 2025-12-16 01:41:55 +07:00
Jeremy Soller 990d9d343e Merge branch 'bump-pkg' into 'master'
Update pkg version

See merge request redox-os/installer!61
2025-12-09 08:48:25 -07:00
Wildan M 8bc7c9d88d Update pkg version 2025-12-09 10:22:36 -05:00
Jeremy Soller 4fcd97e8da Merge branch 'split-cargo-features' into 'master'
Split installer code to cargo feature

See merge request redox-os/installer!60
2025-11-22 11:31:56 -07:00
Wildan M 7a85862733 Split installer code to cargo feature 2025-11-22 09:15:25 -08:00
Wildan M 7308cfe259 Move installer code 2025-11-22 09:15:24 -08:00
Jeremy Soller 58f161e019 Fix compilation on Redox 2025-11-20 08:58:51 -07:00
Jeremy Soller 693d9470f9 Merge branch 'always-local' into 'master'
Always Install binaries locally if cookbook set

See merge request redox-os/installer!59
2025-11-20 06:48:09 -07:00
Jeremy Soller 2a910e7058 Merge branch 'tidy-up' into 'master'
Remove list_packages

See merge request redox-os/installer!58
2025-11-20 06:46:28 -07:00
Wildan M 5fad110e5e Always Install binaries locally if cookbook set 2025-11-20 01:50:47 -08:00
Wildan M 09531dfdc8 Fix fmt 2025-11-20 01:24:41 -08:00
Wildan M e5233171ae Remove list_packages 2025-11-20 01:22:02 -08:00
Jeremy Soller 8281316190 Merge branch 'pkgar-local-path' into 'master'
Fix local package head install path

See merge request redox-os/installer!57
2025-11-12 06:58:47 -07:00
Wildan M 9594e87937 Fix local package head install path 2025-11-12 20:55:33 +07:00
Jeremy Soller bb9fa4ec7c Merge branch 'i586_target' into 'master'
Allow installing for i586-unknown-redox

See merge request redox-os/installer!56
2025-11-01 09:34:59 -06:00
bjorn3 f30179a889 Allow installing for i586-unknown-redox 2025-11-01 16:33:21 +01:00
Jeremy Soller 97dd77918e Merge branch 'update-pkg' into 'master'
Bump pkgar and rand

See merge request redox-os/installer!54
2025-10-01 13:31:25 -06:00
Wildan M 8e6a1c3aa4 Bump pkgar and rand 2025-10-01 21:41:02 +07:00
Jeremy Soller c35c9a88a9 Merge branch 'fix-deps' into 'master'
Fix local dependency install

See merge request redox-os/installer!53
2025-09-30 19:57:16 -06:00
Wildan M 43c1cd987f Fix local dependency install 2025-09-30 19:19:01 +07:00
Jeremy Soller 727c703bad Merge branch 'deterministic-source' into 'master'
Make install order predictable

See merge request redox-os/installer!52
2025-09-29 07:50:48 -06:00
Wildan M 0fa085c0f2 Make install order predictable 2025-09-29 20:29:43 +07:00
Jeremy Soller d558ba4211 Update redox_installer and add fast install mode 2025-09-26 15:54:38 -06:00
Jeremy Soller 69de3d62a1 Fix build on non-Redox 2025-09-26 15:52:31 -06:00
Jeremy Soller c540ed8f9d 0.2.37 - add support for fast install 2025-09-26 15:49:04 -06:00
Jeremy Soller 7023dc0eb4 0.2.36 2025-09-26 15:47:29 -06:00
Jeremy Soller 039ded92a4 Merge branch 'overridable-config' into 'master'
Allow overwriting package files

See merge request redox-os/installer!51
2025-09-26 14:25:40 -06:00
Wildan M e74c9279cf Apply postconfig configuration 2025-09-26 17:27:27 +07:00
Wildan M ee5a94dbe4 Tidy up file config init 2025-09-26 17:26:09 +07:00
Wildan Mubarok b425b12cf5 Allow overwriting package files 2025-09-26 08:32:28 +00:00
Jeremy Soller 957f5acf6a Update dependencies and port to libcosmic 2025-09-07 21:51:56 -06:00
Jeremy Soller 432abd9249 0.2.35: relax serde requirement 2025-09-07 21:24:59 -06:00
Jeremy Soller 01a3adb662 Merge branch 'opt-pkg' into 'master'
Handle metapackages for local install

See merge request redox-os/installer!50
2025-07-27 10:31:27 -06:00
Wildan M 81e8bc09f8 Handle metapackages for local install 2025-07-27 22:49:37 +07:00
Jeremy Soller 5758ba854a Merge branch 'add-ci' into 'master'
Add CI Tests

See merge request redox-os/installer!49
2025-07-09 12:42:59 -06:00
Wildan Mubarok 64b047195d Add CI Tests 2025-07-09 12:42:59 -06:00
Jeremy Soller fd0976f4a2 Update to new scheme format 2025-07-02 14:06:13 -06:00
Jeremy Soller a80429ac27 Fix compilation with new pkgar 2025-07-02 13:54:30 -06:00
Jeremy Soller 8f21b64a3d Update to redoxfs 0.7 2025-07-02 13:45:59 -06:00
Jeremy Soller 2730f1a70c 0.2.34: update to redoxfs 0.7 2025-07-02 13:44:45 -06:00
Jeremy Soller d82dd8998d Merge branch 'clippy_lints' into 'master'
apply some simple clippy lints

See merge request redox-os/installer!48
2025-07-01 08:31:00 -06:00
auronandace 721d49adfb apply some simple clippy lints 2025-06-30 17:11:48 +01:00
Jeremy Soller a86aec193d Merge branch 'ignore-package' into 'master'
Add option to ignore packages

See merge request redox-os/installer!47
2025-06-27 07:09:14 -06:00
Wildan Mubarok 55a0bd1643 Add option to ignore packages 2025-06-27 08:25:59 +00:00
Jeremy Soller 2c3afb4956 0.2.33: fix compilation with new pkgar 2025-06-12 12:07:52 -06:00
Jeremy Soller 2c8c6bf30f update dependencies 2025-06-12 11:56:24 -06:00
Jeremy Soller 0db88e3cb4 Merge branch 'write_bootloader' into 'master'
Add flag to write bootloader to chosen location

See merge request redox-os/installer!46
2025-06-12 10:52:00 -06:00
bjorn3 7ce72b7299 Add flag to write bootloader to chosen location
The build system needs the bootloader separately for netboot support.
2025-06-01 20:53:01 +02:00
Jeremy Soller f91f928a81 Update some dependencies 2025-04-26 20:53:43 -06:00
Jeremy Soller 180cf57506 Support privilege escalation 2025-04-26 20:43:03 -06:00
Jeremy Soller 9e888c06fa 0.2.32: update redoxfs 2025-04-20 18:55:44 -06:00
Jeremy Soller f181694a61 Update ring patch 2025-04-11 20:48:10 -06:00
Jeremy Soller a4032c27b7 0.2.31: update pkgar 2025-03-27 15:47:03 -06:00
Jeremy Soller fcc2dfb1c8 Merge branch 'skip_partitions' into 'master'
Add support for only creating redoxfs partition installer

See merge request redox-os/installer!45
2025-03-27 13:49:21 +00:00
4lDO2 d85eceac71 Add support for only creating redoxfs partition installer. 2025-03-27 14:27:40 +01:00
Jeremy Soller 3c35457b60 Switch from failure to anyhow 2025-03-25 15:51:35 -06:00
Jeremy Soller 10c0f0e688 Update redox_installer 2025-03-25 15:47:32 -06:00
Jeremy Soller a78377da19 0.2.30: update redoxfs 2025-03-25 15:46:05 -06:00
Jeremy Soller 2bf4865794 installer_tui: fix disk detection 2025-03-25 15:40:01 -06:00
Jeremy Soller d53435420b Merge branch 'repo-error-messages' into 'master'
Update to get better error messages

See merge request redox-os/installer!44
2024-12-06 21:57:56 +00:00
Ron Williams 3a084e1c27 Update to get better error messages 2024-12-06 11:43:32 -08:00
Jeremy Soller e17550b6bd Merge branch 'master' into 'master'
fix: install runtime dependencies of locally built packages

See merge request redox-os/installer!43
2024-12-06 17:36:23 +00:00
Anhad Singh 183e18be31 fix: install runtime dependencies of locally built packages
This is the same behavior as remote prebuilt packages and what the
package manager already does. It seems to be only unimplemented for
locally built packages.

Signed-off-by: Anhad Singh <andypython@protonmail.com>
2024-12-07 01:38:16 +11:00
Jeremy Soller 729c5631bc Merge branch 'source-recipe' into 'master'
Accept "source" as an alternative to "recipe"

See merge request redox-os/installer!42
2024-11-21 15:07:55 +00:00
Ron Williams 47282ba9dd Accept "source" as an alternative to "recipe" 2024-11-21 02:17:25 -08:00
Jeremy Soller 7c359f1622 Update dependencies 2024-11-06 06:13:23 -07:00
Jeremy Soller 37cc477d5c 0.2.29: update redoxfs 2024-10-29 08:55:39 -06:00
Jeremy Soller dc7d923e5a 0.2.28 2024-10-20 14:09:50 -06:00
Jeremy Soller 71ac2646bb Merge branch 'master' of https://gitlab.redox-os.org/redox-os/installer 2024-10-20 14:09:21 -06:00
Jeremy Soller 17db563677 0.2.27 - update pkgar 2024-10-20 14:08:47 -06:00
Jeremy Soller cf83106610 Merge branch 'fix-hostname-space' into 'master'
fix: Invalid space in `/etc/hostname`

See merge request redox-os/installer!41
2024-10-20 12:33:04 +00:00
Josh Megnauth 0ee8e95f62 fix: Invalid space in /etc/hostname
Spaces and newlines are invalid in hostnames. However, the base config inadvertently creates an `/etc/hostname` with a newline after "redox".

This can cause programs to fail.
2024-10-20 02:23:04 -04:00
Jeremy Soller b3e90be70b Link to ring issue requiring patch 2024-10-18 14:11:24 -06:00
Jeremy Soller 20fa660797 Patch ring to fix i686 build 2024-10-18 14:09:52 -06:00
Jeremy Soller eebccba932 0.2.26: Update to use pkgutils rewrite 2024-10-18 12:20:09 -06:00
Jeremy Soller 4b9e9e17ab 0.2.25 2024-10-16 13:26:17 -06:00
Jeremy Soller 17263316d9 Merge branch 'deps' into 'master'
Bump dependencies

See merge request redox-os/installer!40
2024-10-16 19:25:00 +00:00
Andrey Turkin 5c8cdd8950 Bump dependencies 2024-10-16 22:13:50 +03:00
Jeremy Soller 5c52a2e80e Merge branch 'riscv' into 'master'
Risc-v support

See merge request redox-os/installer!39
2024-10-16 18:02:45 +00:00
Andrey Turkin 7ccaccee4e Risc-v support 2024-10-16 20:26:51 +03:00
Jeremy Soller afc225c8d6 Merge branch 'ext' into 'master'
Modernize code and add support for local binary packages

See merge request redox-os/installer!38
2024-10-06 15:32:39 +00:00
4lDO2 098fd8bbc9 Support local binary packages. 2024-10-06 17:12:08 +02:00
4lDO2 66d66a537c Switch from failure to anyhow. 2024-10-06 13:08:17 +02:00
4lDO2 bfe70c409f 2021 edition. 2024-10-06 13:02:44 +02:00
4lDO2 cb707c3ce4 Update edition and rustfmt. 2024-10-06 13:02:16 +02:00
Jeremy Soller 9c3c5228df 0.2.24 2024-09-19 20:45:09 -06:00
Jeremy Soller caebc0d6f7 Add XDG user dirs configuration 2024-09-18 09:42:05 -06:00
Jeremy Soller 087810a252 Merge branch 'no_legacy_scheme' into 'master'
Remove all usages of the legacy scheme syntax

See merge request redox-os/installer!37
2024-08-18 22:16:53 +00:00
bjorn3 34a832b43e Remove all usages of the legacy scheme syntax 2024-08-18 21:44:10 +02:00
Jeremy Soller d352203794 Merge branch 'package-lister' into 'master'
Add list_packages binary

See merge request redox-os/installer!36
2024-08-17 23:58:19 +00:00
Ron Williams ad54d088d5 Add list_packages binary 2024-08-16 23:26:25 -07:00
Jeremy Soller 2aebb119ed Install config before package files to ensure usr symlinks are properly handled 2024-06-24 21:05:20 -06:00
Jeremy Soller a5534b8699 Install config before package files to ensure usr symlinks are properly handled 2024-06-24 21:04:31 -06:00
Jeremy Soller a4f12da337 Update installer 2024-06-24 20:06:53 -06:00
Jeremy Soller e409c11e67 0.2.23: fix install from inside Redox 2024-06-24 20:05:38 -06:00
Jeremy Soller 6895d9a9aa 0.2.22: pin cc to prevent ring build failure 2024-06-24 11:28:03 -06:00
Jeremy Soller 31d4816fcb 0.2.21 2024-06-24 11:21:14 -06:00
Jeremy Soller 8d7a3a5588 Update iced, prepare to use libcosmic 2024-06-15 18:56:38 -06:00
Jeremy Soller fe5d3d11a5 Update dependencies 2024-05-27 09:38:48 -06:00
Jeremy Soller 2058101f61 Update dependencies 2024-05-27 09:37:39 -06:00
Jeremy Soller 69d4db511a 0.2.20 2024-04-22 15:28:20 -06:00
Jeremy Soller 52281ea9b5 0.2.19 2024-04-19 13:33:16 -06:00
Jeremy Soller ab5edf2225 Update redoxfs 2024-04-01 13:33:04 -06:00
Jeremy Soller 7b9f9f663d Update redoxfs 2024-04-01 13:31:54 -06:00
Jeremy Soller 7bcc07f357 Merge branch 'redox-users' into 'master'
Update Cargo.lock for redox_users

See merge request redox-os/installer!35
2024-04-01 13:25:03 +00:00
Ron Williams 5b7c64d587 Update Cargo.lock for redox_users 2024-03-31 22:11:37 -07:00
Jeremy Soller 248990b5ef Merge branch 'group' into 'master'
Add missing password field in /etc/group

See merge request redox-os/installer!34
2024-03-25 19:56:03 +00:00
Piotr Wójcik 2acf506d16 Add missing password field in /etc/group 2024-03-25 19:34:22 +01:00
Jeremy Soller 43aed214f4 Update dependencies 2024-03-23 09:06:32 -06:00
Jeremy Soller f725079176 0.2.18: Update redoxfs 2024-03-23 08:57:20 -06:00
Jeremy Soller 319b338063 Update dependencies 2024-02-15 16:09:48 -07:00
Jeremy Soller 1c30f4a3c1 0.2.17: update redoxfs 2024-02-15 16:04:43 -07:00
Jeremy Soller 17219dab9f 0.2.16 2024-02-07 20:00:51 -07:00
Jeremy Soller 6dfcee04ad Add output-config parameter to installer for getting merged config 2024-02-07 19:44:05 -07:00
Jeremy Soller dbfdd417f1 Remove file: prefix from /etc/passwd 2024-01-18 13:43:20 -07:00
Jeremy Soller 51f6efbaad Merge branch 'fix_config_serialization' into 'master'
Update the toml crate

See merge request redox-os/installer!33
2024-01-10 20:50:04 +00:00
bjorn3 3d417f7742 Update the toml crate
This fixes serialization of the config when using specifying that
packages should be downloaded as binary.
2024-01-10 21:48:05 +01:00
Jeremy Soller a258044dcc Merge branch 'limit_progressbar_rate' into 'master'
Update pkgutils

See merge request redox-os/installer!32
2024-01-10 15:55:29 +00:00
bjorn3 9a1463dd11 Update pkgutils 2024-01-10 16:08:06 +01:00
Jeremy Soller 7a7790e9bd Merge branch 'installer_groups' into 'master'
Move /etc/group generation to the installer

See merge request redox-os/installer!30
2024-01-06 16:28:32 +00:00
bjorn3 09da116ccd Move /etc/group generation to the installer
This ensures that the gid's for the auto-generated user groups stay in
sync with the gid in /etc/passwd. It also makes it easier to evolve the
format of /etc/group in the future.
2024-01-06 17:24:48 +01:00
Jeremy Soller 6a4802d556 Add flag to print filesystem size 2024-01-04 12:18:41 -07:00
Jeremy Soller 352407371c Make general config optional 2024-01-04 11:58:21 -07:00
Jeremy Soller 6f40e32d49 Make general.prompt optional 2024-01-04 11:54:48 -07:00
Jeremy Soller b84146edd5 Merge branch 'config_include' into 'master'
Support including other config files from a config file

See merge request redox-os/installer!29
2024-01-04 15:23:21 +00:00
bjorn3 adfee95484 Support including other config files from a config file
This will allow significantly reducing the amount of duplication
between all the different configs used by Redox OS.
2024-01-04 15:29:15 +01:00
bjorn3 1f4bbece4a Introduce Config::from_file for parsing the config toml 2024-01-04 14:54:13 +01:00
bjorn3 5cdec46800 Reserialize filesystem.toml instead of copying the original config
This will be necessary to handle including other configs from the config
to use as these configs will not be copied.
2024-01-04 14:53:05 +01:00
bjorn3 c9e091be67 Remove mention of /games
This is no longer used since redox-os/redox!1397
2024-01-03 21:58:52 +01:00
Jeremy Soller aa1a982479 Merge branch 'symlinks' into 'master'
Create symlinks before installing packages

See merge request redox-os/installer!27
2024-01-03 19:17:52 +00:00
bjorn3 b4f2f01d73 Create symlinks before installing packages
This allows redirecting the location packages are installed using
symlinks. This makes it much easier to change the filesystem hierarchy
by not having to change every recipe in the cookbook.
2023-12-29 15:16:56 +01:00
Jeremy Soller a04a083cd3 Update redox_installer 2023-12-13 14:12:28 -07:00
Jeremy Soller 38f6aa70bd 0.2.15 2023-12-13 14:11:29 -07:00
Jeremy Soller 39f4b1c8b6 Make bootloader install directory per-process 2023-12-13 14:11:09 -07:00
Jeremy Soller ba2fc38a3f Remove temporary mount directory 2023-12-13 14:10:56 -07:00
Jeremy Soller c36431035f Fix for new redox_installer 2023-12-12 13:18:54 -07:00
Jeremy Soller 213d655886 Update dependencies 2023-12-12 13:14:04 -07:00
Jeremy Soller f241649bba 0.2.14 2023-12-12 13:10:15 -07:00
Jeremy Soller 4821c55242 Update redoxfs 2023-12-12 13:10:01 -07:00
Jeremy Soller cf9e2c9719 Use per-process mount path 2023-12-12 11:11:17 -07:00
Jeremy Soller 59e0f9e7ae Update redoxfs 2023-12-12 11:10:53 -07:00
Jeremy Soller e0c909536c Merge branch 'ivan/efi_size' into 'master'
add option for variable size of EFI system partition

See merge request redox-os/installer!26
2023-12-07 15:34:54 +00:00
Ivan Tan cbdea61a61 add option for variable size of EFI system partition 2023-11-18 02:50:06 +00:00
Jeremy Soller c5335d4aae Merge branch 'rw_van_231024' into 'master'
Allow package spec "binary" or "recipe" to override REPO_BINARY=0 or REPO_BINARY=1

See merge request redox-os/installer!25
2023-11-04 18:33:52 +00:00
Ron Williams 2b34b31956 Allow package spec "binary" or "recipe" to override REPO_BINARY=0 or REPO_BINARY=1 2023-11-04 18:33:52 +00:00
Jeremy Soller c5372c6107 Update installer 2023-09-07 19:55:37 -06:00
Jeremy Soller d61b30de3f 0.2.13: update redoxfs and redox_syscall 2023-09-07 16:22:13 -06:00
Jeremy Soller 970cbfaef6 Update redoxfs and uuid 2023-09-07 16:12:50 -06:00
Jeremy Soller 7a85c43974 Merge branch 'master' into 'master'
config: misc changes for GPU drivers

See merge request redox-os/installer!23
2023-07-07 14:09:05 +00:00
Anhad Singh 7b35a76e49 config: misc changes for GPU drivers
Signed-off-by: Anhad Singh <andypythonappdeveloper@gmail.com>
2023-07-07 17:55:10 +10:00
Jeremy Soller db992de466 Merge branch 'update-cargo-lock' into 'main'
Update Cargo.lock

See merge request redox-os/installer-gui!1
2023-04-12 17:13:03 +00:00
Will Angenent e2abdaf935 Update Cargo.lock 2023-04-08 20:59:00 +01:00
Jeremy Soller 363166ad1e Merge branch 'update-pkgar' into 'master'
Update pkgar to 0.1.10

See merge request redox-os/installer!22
2023-04-05 21:32:32 +00:00
Will Angenent 3f306765ad Update pkgar to 0.1.10 2023-04-05 22:30:51 +01:00
Jeremy Soller 20c0bdf60f Merge branch 'update-to-use-redoxfs-unmount' into 'master'
Update installer to use redoxfs unmount function

See merge request redox-os/installer!21
2023-04-03 17:16:27 +00:00
Will Angenent 46e14ea06f Update installer to use redoxfs unmount function
This fixes completing the image build on gentoo and MacOS.
2023-04-01 20:57:43 +01:00
Jeremy Soller 7e0f635d0e Merge branch 'update-cargo-lock' into 'master'
Update Cargo.toml

See merge request redox-os/installer!20
2023-03-21 20:16:11 +00:00
Will Angenent 46e39bc109 Update Cargo.toml 2023-03-21 10:45:21 +00:00
Jeremy Soller b04fcab423 Merge branch 'rw_van_230309' into 'master'
fix mixed build in a new clone

See merge request redox-os/installer!19
2023-03-10 14:13:48 +00:00
Ron Williams b23b2f6e5d fix mixed build in a new clone 2023-03-09 21:17:13 -08:00
Jeremy Soller c4a7020e93 Merge branch 'rw_van_230307' into 'master'
Add ability to read package = recipe from filesystem config

See merge request redox-os/installer!18
2023-03-09 02:24:13 +00:00
Ron Williams 1411257784 add ability to read package = recipe from filesystem config 2023-03-08 14:18:45 -08:00
Jeremy Soller cfd556f3b3 Merge branch 'fix_warnings' into 'master'
Remove unused imports and function

See merge request redox-os/installer!17
2023-02-24 22:01:22 +00:00
andrewdavidmackenzie 257020852d Remove unused imports and function 2023-02-24 22:48:17 +01:00
Jeremy Soller a4ff8931fe Update libc crate 2023-02-11 14:43:14 -07:00
Jeremy Soller c91bea9452 Update libc crate 2023-02-11 14:42:31 -07:00
Jeremy Soller 6386447784 Add success page and properly exit 2023-01-12 09:31:40 -07:00
Jeremy Soller 41a656af92 Ignore live disks and partitions in TUI 2023-01-12 08:50:04 -07:00
Jeremy Soller 68e5718788 Ensure worker thread exits when out of commands 2023-01-12 08:26:54 -07:00
Jeremy Soller 28e3544f4a Filter out unusable drives 2023-01-11 21:06:13 -07:00
Jeremy Soller fce55b54db Message after unmount 2023-01-11 18:10:30 -07:00
Jeremy Soller 9a068db3f1 Implement installation 2023-01-11 14:40:32 -07:00
Jeremy Soller fadb0a1c1d 0.2.12 2023-01-11 14:11:55 -07:00
Jeremy Soller be3a797644 Initial commit 2023-01-11 21:05:55 +00:00
Jeremy Soller f710fa79db Recursively set permissions on home directories 2022-11-21 14:55:08 -07:00
Jeremy Soller b724e714d3 Move EFI partition to second megabyte 2022-09-21 14:42:45 -06:00
Jeremy Soller c6d7cf16e1 Fix EFI partition alignment 2022-09-21 13:54:14 -06:00
Jeremy Soller e24f5ef8e3 Only use cookbook argument if not doing list-packages 2022-09-12 10:14:51 -06:00
Jeremy Soller fa836008c0 Rely on packages being built prior to installer running 2022-09-12 07:32:24 -06:00
Jeremy Soller 5e8e596d40 Fix seek not changing in last commit 2022-09-07 10:48:39 -06:00
Jeremy Soller 7b24f868b8 Optimization for block aligned I/O in disk wrapper 2022-09-07 10:46:24 -06:00
Jeremy Soller 0b4f836f2a Use same logic for read and write in disk wrapper 2022-09-07 10:40:41 -06:00
Jeremy Soller 8d2d4cd62d Add disk wrapper to ensure block aligned I/O 2022-09-07 10:29:15 -06:00
Jeremy Soller 9666c62036 Fix copying EFI partition message 2022-09-07 09:55:02 -06:00
Jeremy Soller f103b5c4b9 Fixup for last commit 2022-09-07 09:48:12 -06:00
Jeremy Soller 4b1df136a6 Write BIOS and EFI partitions in memory 2022-09-07 09:45:27 -06:00
Jeremy Soller ddf59d8c7d Print debug messages while doing partitioning 2022-09-07 09:02:06 -06:00
Jeremy Soller c3e679d5f1 Fix 32-bit x86 target name 2022-09-05 10:51:13 -06:00
Jeremy Soller ad11b2adb2 Ensure config and cookbook key are installed 2022-09-05 09:55:15 -06:00
Jeremy Soller e8e4687c05 Pass live argument from CLI to library 2022-09-05 09:10:46 -06:00
Jeremy Soller 1ecb9b49fc Support fetching live bootloader 2022-09-05 09:07:40 -06:00
Jeremy Soller f1a806b288 0.2.11 - allow installer to create full disk installs supporting both BIOS and EFI 2022-09-05 08:58:53 -06:00
Jeremy Soller 4e7f7d8413 Update to use new boot directory format 2022-08-26 17:56:22 -06:00
Jeremy Soller be5eefd44f Update Cargo.lock 2022-07-26 17:26:26 -06:00
Jeremy Soller 2b1107c856 Also copy initfs 2022-07-22 10:30:44 -06:00
Jeremy Soller 2418286a29 Improve installer size formatting 2022-04-26 08:22:58 -06:00
4lDO2 0ed65d7d3d Use crates.io version of syscall 2022-03-27 12:25:09 +02:00
4lDO2 f76771491c Update syscall 2022-03-27 11:25:01 +02:00
Jeremy Soller f8ec4d28bc Ensure pkg folder exists 2022-03-23 15:04:53 -06:00
Jeremy Soller e666636ab7 Create pkgar_head files when installing 2022-03-23 13:53:10 -06:00
Jeremy Soller 900f77e7f2 Use pkgar_head files for installer_tui 2022-03-23 13:52:33 -06:00
Jeremy Soller adebf727dc Update pkgar and lock file 2022-03-23 13:52:16 -06:00
Jeremy Soller 9ea9a6ecac 0.2.10: update pkgar 2022-03-17 16:35:15 -06:00
Jeremy Soller 3c03576518 0.2.9: add tests and make it possible to install to disk 2022-03-17 08:50:01 -06:00
Jeremy Soller 0c731043ac Use fs::copy to ensure metadata is copied 2022-03-16 14:13:41 -06:00
Jeremy Soller 2c54a9b868 installer_tui: support password encryption 2022-03-15 13:50:48 -06:00
Jeremy Soller b35e132153 Update Cargo.lock 2022-03-11 09:14:45 -07:00
Jeremy Soller 41eba65942 Merge branch 'redoxfs-0.5.0' into 'master'
Redoxfs 0.5.0

See merge request redox-os/installer!16
2022-03-11 16:14:20 +00:00
Jeremy Soller 1634877666 0.2.8 2022-03-11 09:14:07 -07:00
Jeremy Soller ff2fdeca84 Switch to crates.io version of RedoxFS 0.5 2022-03-11 09:13:33 -07:00
Jeremy Soller 621a4dee2c Update to RedoxFS 0.5.0 2022-02-28 12:24:00 -07:00
Jeremy Soller 84856f459d Update patches 2021-08-10 16:08:02 -06:00
Jeremy Soller 7f0a57108d Fix size formatting in installer TUI 2021-08-02 17:44:52 -06:00
4lDO2 70a2dc7532 Update Cargo.lock, again. 2021-06-18 09:23:19 +02:00
4lDO2 224bb047dc Update dependencies. 2021-06-17 18:10:32 +02:00
Jeremy Soller 150d65c31b Update syscall 2020-08-02 15:26:52 -06:00
Jeremy Soller af5f64b807 Remove pbr patch 2020-05-28 11:56:13 -06:00
Jeremy Soller 03320a2025 Remove libc and tar patch 2020-05-26 10:27:19 -06:00
Jeremy Soller 03f996c70f 0.2.7 - add patches for libc, pbr, ring, and tar-rs 2020-05-21 13:47:57 -06:00
Jeremy Soller c22b1cf71b Add cookbook pkgar support 2020-03-13 20:04:43 -06:00
Jeremy Soller 6a49914f0f 0.2.6 - update dependencies 2020-01-20 10:04:24 -07:00
Jeremy Soller 3a1a257f0f Update redoxfs 2020-01-20 09:58:10 -07:00
Jeremy Soller 1fccae679d Add default-run attribute 2019-11-29 10:15:45 -07:00
Jeremy Soller 7c5236fe35 0.2.5 - update dependencies and prepare for publishing 2019-11-29 10:09:27 -07:00
Jeremy Soller 3b0f59cbb0 Copy include, lib, and pkg 2019-09-22 10:58:55 -06:00
Jeremy Soller cd9e026705 Fix issue of trying to run read_dir on disk/live: 2019-09-22 10:06:25 -06:00
Jeremy Soller 9ff577792b Disable creation of liner context - it causes installer_tui to lock 2019-09-22 09:27:27 -06:00
Jeremy Soller fbfc4edf6f Add message when installer is successful 2019-09-19 21:22:05 -06:00
Jeremy Soller 505dec6069 Optimized file copy 2019-09-19 19:39:53 -06:00
Jeremy Soller 89dcc2c060 Add unmounting message 2019-09-15 13:31:59 -06:00
Jeremy Soller 7ef2567da6 Copy locally installed package files 2019-09-15 13:14:46 -06:00
Jeremy Soller 5762840f79 Installer TUI (WIP) 2019-09-08 20:47:05 -06:00
Jeremy Soller 39d68618ca Switch to using rust-argon2 crate, which is more actively maintained 2019-09-08 08:59:50 -06:00
Jeremy Soller b75b1c2f86 Revert libc to 0.2.54 to support older Redox 2019-06-10 17:37:05 -06:00
Jeremy Soller acb7bced58 0.2.4 - allow empty configuration sections and update dependencies 2019-06-10 17:31:44 -06:00
Jeremy Soller f30987b1e5 0.2.3: Update dependencies 2019-05-12 08:34:49 -06:00
Jeremy Soller 713359252a 0.2.2 - fix cargo doc 2019-05-12 08:15:18 -06:00
Jeremy Soller f794ab2a66 Add metadata 2019-05-12 08:02:59 -06:00
Jeremy Soller 40addc7020 Bump version and update dependencies 2019-05-12 08:01:30 -06:00
Jeremy Soller 8e88231ede Update for newer rust 2019-04-07 10:36:45 -06:00
Jeremy Soller a262db6f25 Merge branch 'get-target-from-env' into 'master'
Get TARGET from the environment

See merge request redox-os/installer!11
2019-03-02 21:07:12 +00:00
Robin Randhawa 78e9f77950 Get TARGET from the environment
This enables flexible cross-installation of packages as opposed to only
compiling for the x86_64-unknown-redox target.

This also permits native builds.
2019-03-01 21:02:10 +00:00
Jeremy Soller 03fc74e21f Update installer depends 2018-12-10 14:23:50 -07:00
Jeremy Soller ed25899b0e Update Cargo.lock 2018-10-14 19:58:26 -06:00
Jeremy Soller 49b7a5f427 Fix types when compiling for Redox 2018-08-19 12:33:51 -06:00
Jeremy Soller 7f38ffac1f Use forked libc crate, update lock file 2018-08-19 11:39:22 -06:00
Jeremy Soller 33fa0af521 Fix uid and gid unwrap 2018-08-17 19:59:22 -06:00
Jeremy Soller 91c303df7d Do not set uid or gid if not asked to 2018-08-17 19:34:29 -06:00
Jeremy Soller 123350ae60 Merge remote-tracking branch 'origin/v0.3' 2018-08-17 17:16:16 -06:00
Jeremy Soller be2e09472a Update lock file 2018-08-17 17:13:05 -06:00
MggMuggins 03cae0ca37 Implement shadowfile 2018-08-17 17:26:11 -05:00
Jeremy Soller 8e39b2ea32 Merge branch 'mggmuggins/shadowfile' into 'master'
Use shadowfile for user creation

See merge request redox-os/installer!7
2018-08-17 12:24:04 +00:00
MggMuggins 66b24d1116 Support Directories 2018-07-28 10:41:39 -05:00
MggMuggins e58815f89c Rudimentary support for permissions
I sorta OOP'ed this, combining the logic of what needs to happen for
each config object with those objects. More things are coming, this is a
very WIP commit.
2018-07-27 10:55:11 -05:00
MggMuggins ce6e96bd20 Major Refactor; Create passwd hashes
Maybe some of the refactoring things I did don't make much sense, but I
did them. The goal was to get rid of those macros (because macro
definitions are really hard to read). I replaced two of them with very
similar functions, and rearranged the `install` function.

The installer now generates password hashes from the password entered by
the user or listed in the toml file. This value is NO LONGER the
password hash. The example configs have been changed to reflect this.
2018-07-26 14:58:14 -05:00
MggMuggins 80add18dc9 Functional Partial rewrite 2018-07-26 12:43:57 -05:00
MggMuggins 1359501d3f Use shadowfile for user creation 2018-07-25 15:20:15 -05:00
Jeremy Soller a1afbb144f Update links to gitlab 2018-06-12 12:30:44 -06:00
Jeremy Soller 999db45b0e Update redoxfs 2017-11-19 16:24:25 -07:00
Jeremy Soller f6c5fd94a7 Merge pull request #6 from chebykinn/redox_users
Migrate to redox_users
2017-11-11 12:54:12 -07:00
Ivan Chebykin 681fd3d0ef Migrated to redox_users 2017-11-11 22:12:55 +03:00
Jeremy Soller 62412ef548 Create LICENSE 2017-10-20 20:08:54 -06:00
Jeremy Soller 43e506c447 Update Cargo.lock 2017-10-11 20:54:45 -06:00
Jeremy Soller ac759a17bd Update Cargo.lock 2017-10-04 20:29:41 -06:00
Jeremy Soller a01dc9f37a Switch to arg_parser 2017-09-27 20:53:13 -06:00
Jeremy Soller a82a577180 Change format of installer arguments 2017-09-26 20:52:54 -06:00
Jeremy Soller a7b16dcb97 Update configs, update dependencies 2017-09-26 20:00:50 -06:00
Jeremy Soller 093ae7de55 Fix method of adding scheme 2017-08-23 20:29:27 -06:00
Jeremy Soller 25d6a0fec4 Add scheme to paths 2017-08-23 20:16:45 -06:00
Jeremy Soller 7ad7827497 Merge pull request #5 from ids1024/symlink
Support adding symlinks in configuration
2017-08-21 15:07:56 -06:00
Ian Douglas Scott a4adb124d0 Support adding symlinks in configuration
I'm not sure if this is the best syntax...
2017-08-21 13:24:36 -07:00
Jeremy Soller 5cfe357821 Update Cargo.lock and Cargo.toml 2017-08-19 14:51:08 -06:00
Jeremy Soller a76c8df867 Update Cargo.lock 2017-08-02 21:14:10 -06:00
Jeremy Soller a9a1f0e76d Update Cargo.lock 2017-08-02 19:55:53 -06:00
Jeremy Soller ed468af4c3 Update Cargo.lock 2017-07-29 08:27:31 -06:00
Jeremy Soller 3efca19473 Update userutils 2017-07-26 20:35:27 -06:00
Jeremy Soller c0bc46b954 Update Cargo.lock 2017-07-26 08:25:05 -06:00
Jeremy Soller 2c0e14cadf Add Cargo.lock file 2017-07-26 08:02:37 -06:00
Jeremy Soller d73dd18f07 Add Cargo.lock 2017-07-26 07:54:39 -06:00
Jeremy Soller 15d37cea36 Merge pull request #4 from ids1024/list
--list-packages argument
2017-07-10 21:30:42 -06:00
Ian Douglas Scott e47108a70e --list-packages argument 2017-07-10 20:04:06 -07:00
Jeremy Soller d6f2936a53 Remove redoxfs submodule 2017-06-29 18:13:10 -06:00
Jeremy Soller f99fac30ea Update redoxfs 2017-06-23 17:59:16 -06:00
Jeremy Soller af1dd3b9fd Use .tar.gz for packages 2017-06-18 15:14:51 -06:00
Jeremy Soller 31a60eb8a3 Update for new location of script 2017-05-11 20:52:42 -06:00
Jeremy Soller 318e7bb89f Merge commit '8d6a4d7' 2017-05-10 21:38:17 -06:00
Jeremy Soller 8d6a4d7071 Add RedoxFS as submodule. Do not create passwd if no users are provided 2017-05-10 21:38:05 -06:00
Jeremy Soller 7e9f15e54f Print message when installing from cookbook 2017-05-08 21:20:15 -06:00
Jeremy Soller c2bae24835 Merge pull request #3 from ids1024/package
Update for pkgutils refactoring
2017-04-26 06:24:09 -06:00
Ian Douglas Scott 160a793017 Update for pkgutils refactoring 2017-04-25 21:57:04 -07:00
Jeremy Soller 7c7b30c16e Merge pull request #2 from ids1024/pkgutils_cookbook
Use install methods from pkgutils, and allow installing directly from cookbook with --cookbook=path
2017-04-09 12:59:47 -06:00
Ian Douglas Scott bb94edcb15 Support building directly from cookbook with --cookbook= 2017-04-09 10:11:49 -07:00
Ian Douglas Scott e2aa3c3032 Move package installing logic to function 2017-04-09 09:19:24 -07:00
Ian Douglas Scott bb56cee678 Let pkgutils handle downloading and extracting package 2017-04-09 09:06:07 -07:00
Jeremy Soller 4e9d22ada4 Fix installer paths 2017-04-07 19:57:00 -06:00
Jeremy Soller cfdb2a4968 Log creation of parent dirs 2017-01-09 22:29:03 -07:00
Jeremy Soller be092f3754 Create parent directories 2017-01-09 22:16:47 -07:00
Jeremy Soller eb2c0aafd4 Fix package loop 2017-01-09 22:11:11 -07:00
Jeremy Soller 5303301d3d Download packages from repo 2017-01-09 19:25:58 -07:00
Jeremy Soller 9125d089fb Add package extraction 2017-01-09 19:03:54 -07:00
Jeremy Soller aeb9f89ec1 Fix prompt macro, add ability to insert files in config 2017-01-09 16:05:27 -07:00
Jeremy Soller 7d5baf798e Macros 2017-01-09 15:41:40 -07:00
Jeremy Soller a56ec333ae Add sysroot config 2017-01-09 15:23:30 -07:00
Jeremy Soller dd0a541bfe Fix comment 2017-01-09 14:50:22 -07:00
Jeremy Soller a5773c3504 Add prompt example 2017-01-09 14:49:09 -07:00
Jeremy Soller e4db84e1d7 Add prompt example 2017-01-09 14:48:29 -07:00
Jeremy Soller 542ca6cb3d Fix example 2017-01-09 14:46:56 -07:00
Jeremy Soller 961983e788 Cleanup prompt macros 2017-01-09 14:46:21 -07:00
Jeremy Soller f666d06485 User configuration 2017-01-09 14:35:33 -07:00
Jeremy Soller 1abfd39b8d Initial commit 2017-01-09 10:12:49 -07:00
34 changed files with 14257 additions and 1210 deletions
+4 -2
View File
@@ -1,2 +1,4 @@
/target
/Cargo.lock
pkg
sysroot
/target/
/test.bin
+34
View File
@@ -0,0 +1,34 @@
stages:
- lint
- test
workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
fmt:
image: "rust:latest"
stage: lint
script:
- rustup component add rustfmt
- cargo fmt -- --check
- cd gui && cargo fmt -- --check
cargo-test:
image: "rust:latest"
stage: test
script:
- apt update && apt install -y fuse3 libfuse3-dev
- cargo build --locked
- cargo test
- ./target/debug/redox_installer -c res/test.toml test.bin --no-mount
gui-build:
stage: test
script:
- apt update && apt install -y fuse3 libfuse3-dev
- cd gui
- redoxer build
- cargo build
+3
View File
@@ -0,0 +1,3 @@
# format_bytes_inner was intentionally inlined as nested fn inner()
# inside format_bytes() in lib.rs — RB refactoring, not a bug.
src/installer.rs:format_bytes_inner
Generated
+2684
View File
File diff suppressed because it is too large Load Diff
+67 -29
View File
@@ -1,38 +1,76 @@
[package]
name = "libredox"
authors = ["4lDO2 <4lDO2@protonmail.com>", "vasilito <adminpupkin@gmail.com>"]
version = "0.1.18+rb0.3.1"
edition = "2021"
name = "redox_installer"
version = "0.2.42+rb0.3.1"
description = "A Redox filesystem builder"
license = "MIT"
description = "Redox stable ABI"
repository = "https://gitea.redbearos.org/vasilito/RedBear-OS"
exclude = ["target"]
authors = ["Jeremy Soller <jackpot51@gmail.com>", "vasilito <adminpupkin@gmail.com>"]
repository = "https://gitlab.redox-os.org/redox-os/installer"
default-run = "redox_installer"
edition = "2021"
[features]
default = ["base", "call", "std", "protocol", "redox_syscall"]
base = ["libc"]
call = ["base"]
std = ["base"]
protocol = ["plain", "bitflags"]
mkns = ["ioslice"]
# The `redox_syscall` feature activates the optional `redox_syscall`
# dep below. The `#[cfg(feature = "redox_syscall")]` attributes in
# src/lib.rs gate the `From` impls between `libredox::error::Error`
# and `syscall::Error`; without the feature, those impls are absent
# and `?` propagation between the two types fails. Mirroring
# upstream `libredox 0.1.18`'s feature structure.
redox_syscall = ["dep:redox_syscall"]
[[bin]]
name = "redox_installer"
path = "src/bin/installer.rs"
required-features = ["installer"]
[[bin]]
name = "redox_installer_tui"
path = "src/bin/installer_tui.rs"
required-features = ["installer"]
[lib]
name = "redox_installer"
path = "src/lib.rs"
[dependencies]
bitflags = { version = "2", optional = true }
libc = { version = "0.2", optional = true }
# The redox_syscall package has [lib] name = "syscall", so
# in Rust code it is imported as `syscall`, NOT `redox_syscall`.
# This dep is optional and gated by the `redox_syscall` feature
# above. Matching upstream `libredox 0.1.18` structure.
anyhow = "1"
arg_parser = "0.1.0"
fatfs = { version = "0.3.0", optional = true }
fscommon = { version = "0.1.1", optional = true }
gpt = { version = "3.0.0", optional = true }
libc = { version = "0.2.70", optional = true }
pkgar = { version = "0.2.2", optional = true }
pkgar-core = { version = "0.2.2", optional = true }
pkgar-keys = { version = "0.2.2", optional = true }
rand = { version = "0.9", optional = true }
redox-pkg = { version = "0.3.1", features = ["indicatif"], optional = true }
redox_syscall = { path = "../syscall", optional = true }
ioslice = { version = "0.6", optional = true }
plain = { version = "0.2", optional = true }
redoxfs = { path = "../redoxfs", optional = true, default-features = false, features = ["std", "log"] }
rust-argon2 = { version = "3", optional = true }
serde = "1"
serde_derive = "1.0"
termion = { version = "4", optional = true }
toml = "0.8"
uuid = { version = "1.4", features = ["v4"], optional = true }
[target.'cfg(target_os = "redox")'.dependencies]
libredox = { path = "../libredox", optional = true }
ring = { version = "=0.17.8", optional = true }
[features]
default = ["installer", "fuse"]
installer = [
"fatfs",
"fscommon",
"gpt",
"libc",
"libredox",
"pkgar",
"pkgar-core",
"pkgar-keys",
"rand",
"redox-pkg",
"redox_syscall",
"redoxfs",
"ring",
"rust-argon2",
"termion",
"uuid",
]
fuse = ["redoxfs/fuse"]
[patch.crates-io]
# https://github.com/briansmith/ring/issues/1999
ring = { git = "https://gitlab.redox-os.org/redox-os/ring.git", branch = "redox-0.17.8" }
redox_syscall = { path = "../syscall" }
libredox = { path = "../libredox" }
-44
View File
@@ -1,44 +0,0 @@
[package]
name = "libredox"
authors = ["4lDO2 <4lDO2@protonmail.com>"]
# Red Bear OS Phase J: version is 0.1.18 upstream. The
# redox_syscall dep is now required (not optional) because
# the local fork's acpi module (added in this commit) re-
# exports AcPiVerb from redox_syscall, and downstream recipes
# that don't enable the redox_syscall feature get an
# "unresolved import" error. Making the dep non-optional
# also matches the upstream 0.1.18 Cargo.toml pattern where
# the redox_syscall dep is unconditional.
version = "0.1.18"
edition = "2021"
license = "MIT"
description = "Redox stable ABI"
# Red Bear OS fork lives at the canonical outer repo
# (gitea.redbearos.org/vasilito/RedBear-OS).
repository = "https://gitea.redbearos.org/vasilito/RedBear-OS"
exclude = ["target"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["base", "call", "std", "protocol"]
base = ["libc"]
call = ["base"]
std = ["base"]
protocol = ["plain", "bitflags", "redox_syscall"]
mkns = ["ioslice"]
[dependencies]
bitflags = { version = "2", optional = true }
libc = { version = "0.2", optional = true }
# Phase J: path override to the local fork (../syscall
# relative to the libredox fork's local/sources/libredox/
# path). This gives libredox access to the EnterS2Idle /
# ExitS2Idle AcpiVerb variants. Cargo's [patch.crates-io]
# in the workspace's outer Cargo.toml (in base/ and kernel/)
# is what wires this path through to the actual
# redox_syscall crate; this path entry is the libredox-
# side patch override for the same crate.
redox_syscall = { path = "../syscall", version = "0.8" }
ioslice = { version = "0.6", optional = true }
plain = { version = "0.2", optional = true }
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 4lDO2
Copyright (c) 2017 Redox OS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+50
View File
@@ -0,0 +1,50 @@
# Redox OS installer
The Redox installer will allow you to produce a Redox OS image. You will
be able to specify:
- Output device (raw image, ISO, QEMU, VirtualBox, drive)
- Filesystem
- Included packages
- Method of installation (from source, from binary)
- User accounts
You will be prompted to install dependencies, based on your OS and method of
installation. The easiest method is to install from binaries.
## Usage
It is recommended to compile with `cargo`, in release mode:
```bash
cargo build --release
```
By default, you will be prompted to supply configuration options. You can
use the scripted mode by supplying a configuration file:
```bash
cargo run --release -- config/example.toml
```
An example configuration can be found in [config/example.toml](./config/example.toml).
Unsuplied configuration will use the default. You can use the `general.prompt`
setting to prompt when configuration is not set. Multiple configurations can
be specified, they will be built in order.
## Embedding
The installer can also be used inside of other crates, as a library:
```toml
# Cargo.toml
[dependencies]
redox_installer = "0.1"
```
```rust
// src/main.rs
extern crate redox_installer;
fn main() {
let mut config = redox_installer::Config::default();
...
redox_installer::install(config);
}
```
+174
View File
@@ -0,0 +1,174 @@
# This is the default configuration file
# General settings
[general]
# Do not prompt if settings are not defined
prompt = false
# Package settings
[packages]
#acid = {}
#autoconf = {}
#automake = {}
#bash = {}
#binutils = {}
#ca-certificates = {}
#cargo = {}
#contain = {}
coreutils = {}
#curl = {}
#dash = {}
#diffutils = {}
drivers = {}
extrautils = {}
findutils = {}
#games = {}
#gawk = {}
#gcc = {}
#git = {}
#gnu-binutils = {}
#gnu-make = {}
#installer = {}
ion = {}
#lua = {}
#nasm = {}
netstack = {}
netutils = {}
#newlib = {}
#openssl = {}
orbdata = {}
orbital = {}
orbterm = {}
orbutils = {}
pastel = {}
#patch = {}
#pixelcannon = {}
pkgutils = {}
ptyd = {}
#python = {}
randd = {}
#redoxfs = {}
#rust = {}
#rustual-boy = {}
#sed = {}
smith = {}
sodium = {}
userutils = {}
uutils = {}
#xz = {}
# User settings
[users.root]
password = "password"
uid = 0
gid = 0
name = "root"
home = "/root"
[users.user]
# Password is unset
password = ""
[groups.sudo]
gid = 1
members = ["user"]
[[files]]
path = "/etc/init.d/00_base"
data = """
pcid /etc/pcid/filesystem.toml
randd
ptyd
"""
[[files]]
path = "/etc/init.d/10_net"
data = """
ethernetd
ipd
icmpd
tcpd
udpd
dhcpd -b
"""
[[files]]
path = "/etc/init.d/20_orbital"
data = """
orbital orblogin launcher
"""
[[files]]
path = "/etc/init.d/30_console"
data = """
getty display/vesa:2
getty debug: -J
"""
[[files]]
path = "/etc/net/dns"
data = """
208.67.222.222
"""
[[files]]
path = "/etc/net/ip"
data = """
10.0.2.15
"""
[[files]]
path = "/etc/net/ip_router"
data = """
10.0.2.2
"""
[[files]]
path = "/etc/net/ip_subnet"
data = """
255.255.255.0
"""
[[files]]
path = "/etc/net/mac"
data = """
54-52-00-ab-cd-ef
"""
[[files]]
path = "/etc/pkg.d/50_redox"
data = "https://static.redox-os.org/pkg"
[[files]]
path = "/etc/hostname"
data = "redox"
[[files]]
path = "/etc/issue"
data = """
########## Redox OS ##########
# Login with the following: #
# `user` #
# `root`:`password` #
##############################
"""
[[files]]
path = "/etc/motd"
data = """
Welcome to Redox OS!
"""
[[files]]
path = "/usr"
data = "/"
symlink = true
[[files]]
path = "/tmp"
data = ""
directory = true
# 0o1777
mode = 1023
+24
View File
@@ -0,0 +1,24 @@
# This is the default configuration file
# General settings
[general]
# Do not prompt if settings are not defined
prompt = false
# Package settings
[packages]
binutils = {}
coreutils = {}
extrautils = {}
ion = {}
netutils = {}
pkgutils = {}
userutils = {}
# User settings
[users.root]
password = "password"
uid = 0
gid = 0
name = "root"
home = "/root"
+1
View File
@@ -0,0 +1 @@
/target/
+7016
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "redox_installer_gui"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
pkgar = "0.2"
pkgar-core = "0.2"
pkgar-keys = "0.2"
redox_installer = { path = ".." }
redox_syscall = "0.9"
toml = "0.8"
# TODO: remove after pkgutils update
futures-channel = "0.3.32"
[target.'cfg(target_os = "redox")'.dependencies]
libredox = "0.1"
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic.git"
# use the same rev with other cosmic app
rev = "bb10afd849f1ad172be6b78eeec347d548aa4fd7"
default-features = false
features = ["winit"]
[patch.crates-io]
ring = { git = "https://gitlab.redox-os.org/redox-os/ring.git", branch = "redox-0.17.8" }
+92
View File
@@ -0,0 +1,92 @@
# installer_gui
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://gitlab.redox-os.org/redox-os/installer_gui.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](https://gitlab.redox-os.org/redox-os/installer_gui/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
+634
View File
@@ -0,0 +1,634 @@
use anyhow::format_err;
use cosmic::{
app::{self, Task},
iced::{
self, executor,
futures::sink::SinkExt,
widget::{row, text_input},
window, Alignment, Size, Subscription,
},
widget::{button, column, progress_bar, radio, space, text},
Application, ApplicationExt, Core, Element,
};
use futures_channel::mpsc;
use pkgar::{ext::EntryExt, PackageHead};
use pkgar_core::PackageSrc;
use pkgar_keys::PublicKeyFile;
use redox_installer::{try_fast_install, with_redoxfs_mount, with_whole_disk, Config, DiskOption};
use std::{
ffi::OsStr,
fs,
io::{self, Read, Write},
os::unix::fs::{symlink, MetadataExt, OpenOptionsExt},
path::Path,
sync::Arc,
};
mod sys;
pub use sys::*;
fn main() -> iced::Result {
let mut settings = app::Settings::default();
settings = settings.size(Size::new(608.0, 416.0));
settings = settings.exit_on_close(false);
app::run::<Window>(settings, ())
}
fn copy_file(src: &Path, dest: &Path, buf: &mut [u8]) -> anyhow::Result<()> {
if let Some(parent) = dest.parent() {
// Parent may be a symlink
if !parent.is_symlink() {
match fs::create_dir_all(&parent) {
Ok(()) => (),
Err(err) => {
return Err(format_err!(
"failed to create directory {}: {}",
parent.display(),
err
));
}
}
}
}
let metadata = match fs::symlink_metadata(&src) {
Ok(ok) => ok,
Err(err) => {
return Err(format_err!(
"failed to read metadata of {}: {}",
src.display(),
err
));
}
};
if metadata.file_type().is_symlink() {
let real_src = match fs::read_link(&src) {
Ok(ok) => ok,
Err(err) => {
return Err(format_err!(
"failed to read link {}: {}",
src.display(),
err
));
}
};
match symlink(&real_src, &dest) {
Ok(()) => (),
Err(err) => {
return Err(format_err!(
"failed to copy link {} ({}) to {}: {}",
src.display(),
real_src.display(),
dest.display(),
err
));
}
}
} else {
let mut src_file = match fs::File::open(&src) {
Ok(ok) => ok,
Err(err) => {
return Err(format_err!(
"failed to open file {}: {}",
src.display(),
err
));
}
};
let mut dest_file = match fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(metadata.mode())
.open(&dest)
{
Ok(ok) => ok,
Err(err) => {
return Err(format_err!(
"failed to create file {}: {}",
dest.display(),
err
));
}
};
loop {
let count = match src_file.read(buf) {
Ok(ok) => ok,
Err(err) => {
return Err(format_err!(
"failed to read file {}: {}",
src.display(),
err
));
}
};
if count == 0 {
break;
}
match dest_file.write_all(&buf[..count]) {
Ok(()) => (),
Err(err) => {
return Err(format_err!(
"failed to write file {}: {}",
dest.display(),
err
));
}
}
}
}
Ok(())
}
fn package_files(
root_path: &Path,
config: &mut Config,
files: &mut Vec<String>,
) -> Result<(), anyhow::Error> {
//TODO: Remove packages from config where all files are located (and have valid shasum?)
config.packages.clear();
let pkey_path = "pkg/id_ed25519.pub.toml";
let pkey = PublicKeyFile::open(&root_path.join(pkey_path))?.pkey;
files.push(pkey_path.to_string());
for item_res in fs::read_dir(&root_path.join("pkg"))? {
let item = item_res?;
let pkg_path = item.path();
if pkg_path.extension() == Some(OsStr::new("pkgar_head")) {
let mut pkg = PackageHead::new(&pkg_path, &root_path, &pkey)?;
for entry in pkg.read_entries()? {
files.push(entry.check_path()?.to_str().unwrap().to_string());
}
files.push(
pkg_path
.strip_prefix(root_path)
.unwrap()
.to_str()
.unwrap()
.to_string(),
);
}
}
Ok(())
}
fn install<F: FnMut(Message)>(disk_path: String, password_opt: Option<String>, mut f: F) {
let start = std::time::Instant::now();
let mut progress = 0;
macro_rules! message {
($($arg:tt)*) => {{
eprintln!($($arg)*);
f(Message::Install(
progress,
format!($($arg)*)
));
}}
}
let root_path = Path::new("/scheme/file/");
message!("Loading bootloader");
let bootloader_bios = {
let path = root_path.join("boot").join("bootloader.bios");
if path.exists() {
match fs::read(&path) {
Ok(ok) => ok,
Err(err) => {
f(Message::Error(format!(
"{}: failed to read: {}",
path.display(),
err
)));
return;
}
}
} else {
Vec::new()
}
};
message!("Loading bootloader.efi");
let bootloader_efi = {
let path = root_path.join("boot").join("bootloader.efi");
if path.exists() {
match fs::read(&path) {
Ok(ok) => ok,
Err(err) => {
f(Message::Error(format!(
"{}: failed to read: {}",
path.display(),
err
)));
return;
}
}
} else {
Vec::new()
}
};
message!("Formatting disk");
let disk_option = DiskOption {
bootloader_bios: &bootloader_bios,
bootloader_efi: &bootloader_efi,
password_opt: password_opt.as_ref().map(|x| x.as_bytes()),
efi_partition_size: None,
skip_partitions: false,
};
let res = with_whole_disk(&disk_path, &disk_option, |mut fs| -> anyhow::Result<()> {
// Fast install method via filesystem clone
let mut last_progress = 0;
if try_fast_install(&mut fs, |used, used_old| {
progress = ((used * 100) / used_old) as usize;
if progress != last_progress {
message!(
"{}%: {} MB/{} MB",
progress,
used / 1000 / 1000,
used_old / 1000 / 1000
);
last_progress = progress;
}
})? {
progress = 100;
message!("Finished installing using fast mode");
return Ok(());
}
with_redoxfs_mount(fs, None, |mount_path: &Path| -> anyhow::Result<()> {
message!("Loading filesystem.toml");
let mut config: Config = {
let path = root_path.join("filesystem.toml");
match fs::read_to_string(&path) {
Ok(config_data) => match toml::from_str(&config_data) {
Ok(config) => config,
Err(err) => {
return Err(format_err!(
"{}: failed to decode: {}",
path.display(),
err
));
}
},
Err(err) => {
return Err(format_err!("{}: failed to read: {}", path.display(), err));
}
}
};
// Copy filesystem.toml, which is not packaged
let mut files = vec!["filesystem.toml".to_string()];
// Copy files from locally installed packages
message!("Loading package files");
if let Err(err) = package_files(&root_path, &mut config, &mut files) {
return Err(format_err!("failed to read package files: {}", err));
}
// Sort and remove duplicates
files.sort();
files.dedup();
// Perform config install (after packages have been converted to files)
message!("Configuring system");
let cookbook: Option<&'static str> = None;
redox_installer::install_dir(config, mount_path, cookbook)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
// Install files
let mut buf = vec![0; 4096 * 1024];
for (i, name) in files.iter().enumerate() {
progress = (i * 100) / files.len();
message!("Copy {} [{}/{}]", name, i, files.len());
let src = root_path.join(name);
let dest = mount_path.join(name);
copy_file(&src, &dest, &mut buf)?;
}
progress = 100;
message!("Finished installing, unmounting filesystem");
Ok(())
})
});
match res {
Ok(()) => {
f(Message::Success(format!(
"Finished installing in {:?}, ready to reboot",
start.elapsed()
)));
}
Err(err) => {
f(Message::Error(format!("Failed to install: {}", err)));
}
}
}
#[derive(Debug)]
enum Page {
Sudo(String),
Disk(Option<usize>),
Install(usize, String),
Success(String),
Error(String),
}
#[derive(Clone, Debug)]
struct Worker {
command_sender: std::sync::mpsc::Sender<(String, Option<String>)>,
join_handle: Arc<std::thread::JoinHandle<()>>,
}
#[derive(Clone, Debug)]
enum Message {
None,
Worker(Worker),
SudoInput(String),
SudoSubmit,
DiskChoose(usize),
DiskConfirm(usize),
Install(usize, String),
Success(String),
Exit,
Error(String),
}
struct Window {
core: Core,
page: Page,
disk_paths: Vec<(String, bool, u64)>,
worker_opt: Option<Worker>,
}
enum State {
Ready,
Waiting(mpsc::UnboundedReceiver<Message>),
Finished,
}
impl Window {
fn worker_stream() -> impl iced::futures::Stream<Item = Message> {
iced::stream::channel(100, |mut output: mpsc::Sender<Message>| async move {
let mut state = State::Ready;
loop {
let (message, new_state) = match state {
State::Ready => {
let (command_sender, command_receiver) = std::sync::mpsc::channel();
let (message_sender, message_receiver) = mpsc::unbounded();
//TODO: kill worker thread?
let join_handle = std::thread::spawn(move || {
while let Ok((disk_path, password_opt)) = command_receiver.recv() {
println!("Installing to {:?}", disk_path);
install(disk_path, password_opt, |message| {
message_sender.unbounded_send(message).unwrap();
});
}
});
let worker = Worker {
command_sender,
join_handle: std::sync::Arc::new(join_handle),
};
(Message::Worker(worker), State::Waiting(message_receiver))
}
State::Waiting(mut message_receiver) => {
use iced::futures::StreamExt;
match message_receiver.next().await {
Some(message) => (message, State::Waiting(message_receiver)),
None => (Message::None, State::Finished),
}
}
State::Finished => iced::futures::future::pending().await,
};
output.send(message).await.unwrap();
state = new_state;
}
})
}
}
impl Application for Window {
type Executor = executor::Default;
type Flags = ();
type Message = Message;
const APP_ID: &'static str = "org.redox-os.InstallerGui";
fn init(core: Core, _flags: ()) -> (Self, Task<Message>) {
//TODO: load in background
let (page, disk_paths) = match disk_paths() {
Ok(disk_paths) => (Page::Disk(None), disk_paths),
Err(err) => (Page::Error(err), Vec::new()),
};
let mut app = Self {
core,
page,
disk_paths,
worker_opt: None,
};
let task = app.set_window_title("Redox OS Installer".to_string());
(app, task)
}
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::None => {}
Message::Worker(worker) => {
self.worker_opt = Some(worker);
}
Message::SudoInput(password) => {
self.page = Page::Sudo(password);
}
Message::SudoSubmit => {
#[cfg(target_os = "redox")]
if let Page::Sudo(password) = &self.page {
match ask_root(&password) {
Ok(()) => {
self.page = Page::Disk(None);
}
Err(err) => {
eprintln!("{err}");
}
}
}
#[cfg(target_os = "linux")]
match ask_root() {
Ok(()) => {
if let Some(window_id) = self.core.main_window_id() {
return window::close(window_id);
}
}
Err(err) => {
eprintln!("{err}");
}
}
}
Message::DiskChoose(disk_i) => {
self.page = Page::Disk(Some(disk_i));
}
Message::DiskConfirm(disk_i) => match self.disk_paths.get(disk_i) {
Some((disk_path, _is_partition, _disk_size)) => match &self.worker_opt {
Some(worker) => match worker.command_sender.send((disk_path.clone(), None)) {
Ok(()) => self.page = Page::Install(0, format!("Starting install...")),
Err(err) => {
self.page = Page::Error(format!("failed to send command: {}", err));
}
},
None => {
self.page = Page::Error(format!("command sender not found"));
}
},
None => {
self.page = Page::Error(format!("invalid disk number {} chosen", disk_i));
}
},
Message::Install(progress, description) => {
self.page = Page::Install(progress, description);
}
Message::Success(description) => {
self.page = Page::Success(description);
}
Message::Error(err) => {
self.page = Page::Error(err);
}
Message::Exit => {
if let Some(worker) = self.worker_opt.take() {
drop(worker.command_sender);
let join_handle = Arc::try_unwrap(worker.join_handle).unwrap();
join_handle.join().unwrap();
}
if let Some(window_id) = self.core.main_window_id() {
return window::close(window_id);
}
}
}
Task::none()
}
fn view(&self) -> Element<'_, Message> {
let mut widgets = Vec::new();
match &self.page {
Page::Sudo(password) => {
widgets.push(text("Enter your password:").into());
widgets.push(
text_input("", password)
.on_input(Message::SudoInput)
.secure(true)
.on_submit(Message::SudoSubmit)
.into(),
);
}
Page::Disk(disk_i_opt) => {
if !self.disk_paths.is_empty() {
if is_root() {
widgets.push(text("Choose a drive:").size(24).into());
for (disk_i, (disk_path, is_partition, disk_size)) in
self.disk_paths.iter().enumerate()
{
if !is_partition {
widgets.push(
row![
radio(
text(disk_path),
disk_i,
*disk_i_opt,
Message::DiskChoose
),
space::horizontal(),
text(redox_installer::format_bytes(*disk_size)),
]
.into(),
);
}
}
if let Some(disk_i) = *disk_i_opt {
widgets.push(space::vertical().into());
widgets.push(
row![
space::horizontal(),
button::destructive("Confirm")
.on_press(Message::DiskConfirm(disk_i)),
]
.into(),
);
}
} else {
#[cfg(target_os = "linux")]
let page = Message::SudoSubmit;
#[cfg(target_os = "redox")]
let page = Message::SudoInput(String::new());
widgets.push(space::vertical().into());
widgets.push(
row![
text("Ask superuser permission to install into drives"),
space::horizontal(),
button::suggested("Ask root access").on_press(page),
]
.into(),
);
}
} else {
widgets.push(text("No drives found").into());
// TODO: expose disk.pci-*-*nvme/* */ scheme to user
widgets.push(text("(try to rerun with sudo)").into());
}
}
Page::Install(progress, description) => {
widgets.push(text("Installation progress:").size(24).into());
widgets.push(progress_bar::determinate_linear(*progress as f32 / 100.).into());
widgets.push(text(description).into());
}
Page::Success(description) => {
widgets.push(text("Installation complete!").size(24).into());
widgets.push(text(description).into());
widgets.push(space::vertical().into());
widgets.push(
row![
space::horizontal(),
button::standard("Exit").on_press(Message::Exit),
]
.into(),
);
}
Page::Error(err) => {
widgets.push(text(format!("{}", err)).into());
}
};
column::with_children(widgets)
.spacing(8)
.padding(24)
.align_x(Alignment::Start)
.into()
}
fn subscription(&self) -> Subscription<Message> {
Subscription::run(Self::worker_stream)
}
}
+12
View File
@@ -0,0 +1,12 @@
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "redox")]
mod redox;
#[cfg(target_os = "linux")]
pub use linux::*;
#[cfg(target_os = "redox")]
pub use redox::*;
#[cfg(not(any(target_os = "linux", target_os = "redox")))]
compile_error!("Platform is not ported");
+76
View File
@@ -0,0 +1,76 @@
use std::{fs, os::unix::process::CommandExt, process::Command};
pub fn ask_root() -> Result<(), String> {
let Ok(exe_path) = std::env::current_exe() else {
return Err(format!("Could not determine current_exe"));
};
let mut cmd = Command::new("pkexec");
cmd.arg("env");
for (key, value) in std::env::vars() {
if [
"DISPLAY",
"WAYLAND_DISPLAY",
"XAUTHORITY",
"XDG_RUNTIME_DIR",
"DBUS_SESSION_BUS_ADDRESS",
]
.contains(&key.as_str())
{
cmd.arg(format!("{}={}", key, value));
}
}
cmd.arg(exe_path);
// will never return unless fail
let e = cmd.exec();
return Err(format!("Failed to escalate: {e}"));
}
pub fn is_root() -> bool {
let euid = unsafe { libc::geteuid() };
euid == 0
}
pub fn disk_paths() -> Result<Vec<(String, bool, u64)>, String> {
let mut paths = Vec::new();
let entries = match fs::read_dir("/sys/class/block") {
Ok(entries) => entries,
Err(err) => return Err(format!("failed to read /sys/class/block: {}", err)),
};
for entry_res in entries {
if let Ok(entry) = entry_res {
let file_name = entry.file_name();
let name = file_name.to_string_lossy();
if name.starts_with("loop")
|| name.starts_with("ram")
|| name.starts_with("sr")
|| name.starts_with("zram")
{
continue;
}
let path = entry.path();
let is_partition = path.join("partition").exists();
let size_path = path.join("size");
let Ok(size_str) = fs::read_to_string(&size_path) else {
continue;
};
let Ok(sectors) = size_str.trim().parse::<u64>() else {
continue;
};
// /sys/class/block/*/size is 512 (cmiiw)
let size_bytes = sectors * 512;
if size_bytes == 0 {
continue;
}
paths.push((format!("/dev/{}", name), is_partition, size_bytes));
continue;
}
}
paths.sort_by(|a, b| a.0.cmp(&b.0));
Ok(paths)
}
+95
View File
@@ -0,0 +1,95 @@
use std::fs;
pub fn ask_root(password: &str) -> Result<(), String> {
let file = libredox::Fd::open("/scheme/sudo", libredox::flag::O_CLOEXEC, 0)
.map_err(|err| err.to_string())?;
file.write(password.as_bytes())
.map_err(|err| err.to_string())?;
// FIXME move to libredox
unsafe extern "C" {
safe fn redox_cur_procfd_v0() -> usize;
}
// Elevate privileges of our own process with help from the sudo daemon
file.call_wo(
&libredox::call::dup(redox_cur_procfd_v0(), &[])
.map_err(|err| err.to_string())?
.to_ne_bytes(),
syscall::CallFlags::FD,
&[],
)
.map_err(|err| err.to_string())?;
Ok(())
}
pub fn is_root() -> bool {
let euid = libredox::call::geteuid().unwrap();
euid == 0
}
pub fn disk_paths() -> Result<Vec<(String, bool, u64)>, String> {
let mut schemes = Vec::new();
match fs::read_dir("/scheme/") {
Ok(entries) => {
for entry_res in entries {
if let Ok(entry) = entry_res {
let path = entry.path();
if let Ok(path_str) = path.into_os_string().into_string() {
let scheme = path_str.trim_start_matches("/scheme/").trim_matches('/');
if scheme.starts_with("disk") {
if scheme == "disk/live" {
// Skip live disks
continue;
}
schemes.push(format!("/scheme/{}", scheme));
}
}
}
}
}
Err(err) => {
return Err(format!("failed to list schemes: {}", err));
}
}
let mut paths = Vec::new();
for scheme in schemes {
let is_dir = fs::metadata(&scheme).map(|x| x.is_dir()).unwrap_or(false);
if !is_dir {
continue;
}
match fs::read_dir(&scheme) {
Ok(entries) => {
for entry_res in entries {
let Ok(entry) = entry_res else {
continue;
};
let Ok(file_name) = entry.file_name().into_string() else {
continue;
};
let Ok(path) = entry.path().into_os_string().into_string() else {
continue;
};
let Ok(metadata) = entry.metadata() else {
continue;
};
let is_partition = file_name.contains('p');
let size = metadata.len();
if size == 0 {
continue;
}
paths.push((path, is_partition, size));
}
}
Err(err) => {
return Err(format!("failed to list {:?}: {}", scheme, err));
}
}
}
Ok(paths)
}
+372
View File
@@ -0,0 +1,372 @@
# Automatically generated by update.sh
include = []
[general]
prompt = false
filesystem_size = 196
[packages.base]
[packages.bootloader]
[packages.ca-certificates]
[packages.coreutils]
[packages.extrautils]
[packages.ion]
[packages.kernel]
[packages.kibi]
[packages.libgcc]
[packages.libstdcxx]
[packages.netdb]
[packages.netutils]
[packages.pkgutils]
[packages.relibc]
[packages.userutils]
[packages.uutils]
[packages.uutils-procps]
[[files]]
path = "/etc/login_schemes.toml"
data = """
[user_schemes.root]
schemes = ["*"]
[user_schemes.user]
schemes = [
# Kernel schemes
"debug",
"event",
"memory",
"pipe",
"serio",
"irq",
"time",
"sys",
# Base schemes
"rand",
"null",
"zero",
"log",
# Network schemes
"ip",
"icmp",
"tcp",
"udp",
# IPC schemes
"shm",
"chan",
"uds_stream",
"uds_dgram",
# File schemes
"file",
# Display schemes
"display.vesa",
"display*",
# Other schemes
"proc",
"pty",
"sudo",
"audio",
"orbital",
]
"""
symlink = false
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/etc/hostname"
data = "redox"
symlink = false
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/etc/net/dns"
data = """
9.9.9.9
"""
symlink = false
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/etc/net/ip"
data = """
10.0.2.15
"""
symlink = false
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/etc/net/ip_router"
data = """
10.0.2.2
"""
symlink = false
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/etc/net/ip_subnet"
data = """
255.255.255.0
"""
symlink = false
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/usr/lib/os-release"
data = """
PRETTY_NAME="Redox OS 0.9.0"
NAME="Redox OS"
VERSION_ID="0.9.0"
VERSION="0.9.0"
ID="redox-os"
HOME_URL="https://redox-os.org/"
DOCUMENTATION_URL="https://redox-os.org/docs/"
SUPPORT_URL="https://redox-os.org/community/"
"""
symlink = false
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/etc/os-release"
data = "../usr/lib/os-release"
symlink = true
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/etc/pkg.d/50_redox"
data = "https://static.redox-os.org/pkg"
symlink = false
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/usr"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/usr/bin"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/bin"
data = "usr/bin"
symlink = true
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/usr/include"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/include"
data = "usr/include"
symlink = true
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/usr/lib"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/lib"
data = "usr/lib"
symlink = true
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/usr/libexec"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/usr/share"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/share"
data = "usr/share"
symlink = true
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/ui"
data = "usr/share/ui"
symlink = true
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/usr/share/ui/fonts"
data = "/usr/share/fonts"
symlink = true
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/usr/share/ui/icons"
data = "/usr/share/icons"
symlink = true
directory = false
recursive_chown = false
postinstall = false
[[files]]
path = "/var"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/var/cache"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/var/lib"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/var/lock"
data = ""
symlink = false
directory = true
mode = 1023
recursive_chown = false
postinstall = false
[[files]]
path = "/var/log"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/var/run"
data = ""
symlink = false
directory = true
mode = 493
recursive_chown = false
postinstall = false
[[files]]
path = "/var/tmp"
data = ""
symlink = false
directory = true
mode = 1023
recursive_chown = false
postinstall = false
[[files]]
path = "/usr/lib/init.d/30_console"
data = """
requires_weak 10_net.target
inputd -A 2
nowait getty 2
nowait getty /scheme/debug/no-preserve -J
"""
symlink = false
directory = false
recursive_chown = false
postinstall = false
[users.root]
password = "password"
uid = 0
gid = 0
shell = "/usr/bin/ion"
[users.user]
password = ""
shell = "/usr/bin/ion"
[groups.sudo]
gid = 1
members = ["user"]
Executable
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e
RES_PATH="$(dirname "$0")"
if [ -d "$1" ]
then
REDOX_PATH="$1"
else
echo "$0 [path to redox repository]" >&2
exit 1
fi
set -x
# Update res/test.toml from the redoxer.toml template
"${REDOX_PATH}/build/fstools/bin/redox_installer" \
--config="${REDOX_PATH}/config/minimal.toml" \
--output-config="${RES_PATH}/test.toml"
sed -i '1s/^/# Automatically generated by update.sh\n\n/' "${RES_PATH}/test.toml"
+220
View File
@@ -0,0 +1,220 @@
extern crate arg_parser;
extern crate redox_installer;
extern crate serde;
extern crate toml;
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use std::{env, fs, process};
use arg_parser::ArgParser;
use pkg::net_backend::DownloadBackend;
use redox_installer::{Config, PackageConfig};
const HELP_STR: &str = r#"
redox_installer - Redox Installer.
Refer to link below for filesystem config reference:
https://doc.redox-os.org/book/configuration-settings.html
Using redox_installer as an installer:
redox_installer <diskpath.img> [--config=file.toml] [--write-bootloader=file.img] [--live] [--no-mount] [--skip-partition]
<diskpath.img> Disk file to write
--config Path to filesystem config TOML
--write-bootloader Path to write UEFI bootloader to in addition to the embedded ESP
--skip-partition Skip writing GPT partition tables
Use this only if you plan to use other partition tool
--live Use bootloader configured for live disk
--no-mount Use RedoxFS AR instead of FUSE to write files
--cookbook Use local Redox OS build system rather than downloading packages
--config-name Name of the filesystem configuration used for os-release VARIANT
Using redox_installer as a configuration parser:
redox_installer --config=file.toml [--list-packages|--filesystem-size|--output-config path]
--list-packages List packages will be installed
--filesystem-size Output filesystem size in MB
--output-config Path to write the parsed config as another TOML
"#;
fn os_release_quote(value: &str) -> String {
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{}\"", escaped)
}
fn build_id_from_repo_toml(repo_toml: &str) -> Option<String> {
toml::from_str::<toml::Value>(repo_toml)
.ok()?
.get("build_id")?
.as_str()
.and_then(|value| {
if value.is_empty() {
None
} else {
Some(value.to_string())
}
})
}
fn local_repo_build_id(cookbook: &str) -> Option<String> {
let repo_toml = Path::new(cookbook)
.join("repo")
.join(redox_installer::get_target())
.join("repo.toml");
let repo_toml = fs::read_to_string(repo_toml).ok()?;
build_id_from_repo_toml(&repo_toml)
}
fn remote_repo_build_id() -> Option<String> {
let callback = Rc::new(RefCell::new(pkg::callback::SilentCallback::new()));
let download_backend = pkg::net_backend::DefaultNetBackend::new().ok()?;
let mut repo = pkg::RepoManager::new(callback, Box::new(download_backend));
repo.add_remote(
"https://static.redox-os.org/pkg",
&redox_installer::get_target(),
)
.ok()?;
let package = pkg::PackageName::new("repo").ok()?;
let (repo_toml, _) = repo.get_package_toml(&package).ok()?;
build_id_from_repo_toml(&repo_toml)
}
fn append_os_release_metadata(
config: &mut Config,
config_name: Option<&str>,
build_id: Option<String>,
) {
let mut data = String::new();
if let Some(config_name) = config_name.filter(|value| !value.is_empty()) {
data.push_str("VARIANT=");
data.push_str(&os_release_quote(config_name));
data.push('\n');
}
if let Some(build_id) = build_id.filter(|value| !value.is_empty()) {
data.push_str("BUILD_ID=");
data.push_str(&os_release_quote(&build_id));
data.push('\n');
}
if !data.is_empty() {
config.files.push(redox_installer::FileConfig {
path: "/usr/lib/os-release".to_string(),
data,
append: true,
..Default::default()
});
}
}
fn main() {
let mut parser = ArgParser::new(4)
.add_opt("b", "cookbook")
.add_opt("", "config-name")
.add_opt("c", "config")
.add_opt("o", "output-config")
.add_opt("", "write-bootloader")
.add_flag(&["skip-partition"])
.add_flag(&["filesystem-size"])
.add_flag(&["r", "repo-binary"]) // TODO: Remove
.add_flag(&["l", "list-packages"])
.add_flag(&["live"])
.add_flag(&["no-mount"]);
parser.parse(env::args());
let skip_partition = parser.found("skip-partition");
let mut config = if let Some(path) = parser.get_opt("config") {
match Config::from_file(Path::new(&path)) {
Ok(config) => config,
Err(err) => {
eprintln!("installer: {err}");
process::exit(1);
}
}
} else {
redox_installer::Config::default()
};
// Get toml of merged config
let merged_toml = toml::to_string_pretty(&config).unwrap();
// Just output merged config and exit
if let Some(path) = parser.get_opt("output-config") {
fs::write(path, merged_toml).unwrap();
return;
}
// Add filesystem.toml to config
config.files.push(redox_installer::FileConfig {
path: "filesystem.toml".to_string(),
data: merged_toml,
..Default::default()
});
if skip_partition {
config.general.skip_partitions = Some(true);
}
if parser.found("filesystem-size") {
println!("{}", config.general.filesystem_size.unwrap_or(0));
} else if parser.found("list-packages") {
// List the packages that should be fetched or built by the cookbook
for (packagename, package) in &config.packages {
match package {
PackageConfig::Build(rule) if rule == "ignore" => {
// skip this package
}
_ => {
println!("{}", packagename);
}
}
}
} else {
let cookbook = if let Some(path) = parser.get_opt("cookbook") {
if !Path::new(&path).is_dir() {
eprintln!("installer: {}: cookbook not found", path);
process::exit(1);
}
Some(path)
} else {
None
};
let build_id = if let Some(cookbook) = cookbook.as_deref() {
local_repo_build_id(cookbook)
} else {
remote_repo_build_id()
};
append_os_release_metadata(
&mut config,
parser.get_opt("config-name").as_deref(),
build_id,
);
if cookbook.is_some() {
config.general.cookbook = cookbook;
}
if parser.found("live") {
config.general.live_disk = Some(true);
}
if parser.found("no-mount") {
config.general.no_mount = Some(true);
}
let write_bootloader = parser.get_opt("write-bootloader");
if write_bootloader.is_some() {
config.general.write_bootloader = write_bootloader;
}
if let Some(path) = parser.args.first() {
if let Err(err) = redox_installer::install(config, path) {
eprintln!("installer: failed to install: {:?}", err);
process::exit(1);
}
} else {
eprint!("{}", HELP_STR);
process::exit(1);
}
}
}
+373
View File
@@ -0,0 +1,373 @@
use anyhow::{anyhow, bail, Result};
use pkgar::{ext::EntryExt, PackageHead};
use pkgar_core::PackageSrc;
use pkgar_keys::PublicKeyFile;
use redox_installer::{try_fast_install, with_redoxfs_mount, with_whole_disk, Config, DiskOption};
use std::{
ffi::OsStr,
fs,
io::{self, Read, Write},
os::unix::fs::{symlink, MetadataExt, OpenOptionsExt},
path::{Path, PathBuf},
process,
};
// TODO: This is not the TUI a regular user would expect it does
// 1. Linux: Implement disk listing, use "dd" to write into whole disk
// 2. Allow partitioning to allow dual boot, possibly an integration with systemd-boot/grub
// 3. Prompt everything (disk password, users, preconfigured packages, import from existing img)
#[cfg(not(target_os = "redox"))]
fn disk_paths(_paths: &mut Vec<(PathBuf, u64)>) {}
#[cfg(target_os = "redox")]
fn disk_paths(paths: &mut Vec<(PathBuf, u64)>) {
let mut schemes = Vec::new();
match fs::read_dir("/scheme") {
Ok(entries) => {
for entry_res in entries {
if let Ok(entry) = entry_res {
if let Ok(file_name) = entry.file_name().into_string() {
if file_name.starts_with("disk") {
schemes.push(entry.path());
}
}
}
}
}
Err(err) => {
eprintln!("redox_installer_tui: failed to list schemes: {}", err);
}
}
for scheme in schemes {
if scheme.is_dir() {
match fs::read_dir(&scheme) {
Ok(entries) => {
for entry_res in entries {
if let Ok(entry) = entry_res {
if let Ok(file_name) = entry.file_name().into_string() {
if file_name.contains('p') {
// Skip partitions
continue;
}
if let Ok(metadata) = entry.metadata() {
let size = metadata.len();
if size > 0 {
paths.push((entry.path(), size));
}
}
}
}
}
}
Err(err) => {
eprintln!(
"redox_installer_tui: failed to list '{}': {}",
scheme.display(),
err
);
}
}
}
}
}
fn copy_file(src: &Path, dest: &Path, buf: &mut [u8]) -> Result<()> {
if let Some(parent) = dest.parent() {
// Parent may be a symlink
if !parent.is_symlink() {
match fs::create_dir_all(&parent) {
Ok(()) => (),
Err(err) => {
bail!("failed to create directory {}: {}", parent.display(), err);
}
}
}
}
let metadata = match fs::symlink_metadata(&src) {
Ok(ok) => ok,
Err(err) => {
bail!("failed to read metadata of {}: {}", src.display(), err);
}
};
if metadata.file_type().is_symlink() {
let real_src = match fs::read_link(&src) {
Ok(ok) => ok,
Err(err) => {
bail!("failed to read link {}: {}", src.display(), err);
}
};
match symlink(&real_src, &dest) {
Ok(()) => (),
Err(err) => {
bail!(
"failed to copy link {} ({}) to {}: {}",
src.display(),
real_src.display(),
dest.display(),
err
);
}
}
} else {
let mut src_file = match fs::File::open(&src) {
Ok(ok) => ok,
Err(err) => {
bail!("failed to open file {}: {}", src.display(), err);
}
};
let mut dest_file = match fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(metadata.mode())
.open(&dest)
{
Ok(ok) => ok,
Err(err) => {
bail!("failed to create file {}: {}", dest.display(), err);
}
};
loop {
let count = match src_file.read(buf) {
Ok(ok) => ok,
Err(err) => {
bail!("failed to read file {}: {}", src.display(), err);
}
};
if count == 0 {
break;
}
match dest_file.write_all(&buf[..count]) {
Ok(()) => (),
Err(err) => {
bail!("failed to write file {}: {}", dest.display(), err);
}
}
}
}
Ok(())
}
fn package_files(
root_path: &Path,
config: &mut Config,
files: &mut Vec<String>,
) -> Result<(), anyhow::Error> {
//TODO: Remove packages from config where all files are located (and have valid shasum?)
config.packages.clear();
let pkey_path = "pkg/id_ed25519.pub.toml";
let pkey = PublicKeyFile::open(&root_path.join(pkey_path))?.pkey;
files.push(pkey_path.to_string());
for item_res in fs::read_dir(&root_path.join("pkg"))? {
let item = item_res?;
let pkg_path = item.path();
if pkg_path.extension() == Some(OsStr::new("pkgar_head")) {
let mut pkg = PackageHead::new(&pkg_path, &root_path, &pkey)?;
for entry in pkg.read_entries()? {
files.push(entry.check_path()?.to_str().unwrap().to_string());
}
files.push(
pkg_path
.strip_prefix(root_path)
.unwrap()
.to_str()
.unwrap()
.to_string(),
);
}
}
Ok(())
}
fn choose_disk() -> PathBuf {
let mut paths = Vec::new();
disk_paths(&mut paths);
loop {
for (i, (path, size)) in paths.iter().enumerate() {
eprintln!(
"\x1B[1m{}\x1B[0m: {}: {}",
i + 1,
path.display(),
redox_installer::format_bytes(*size)
);
}
if paths.is_empty() {
eprintln!("redox_installer_tui: no RedoxFS partition found");
eprintln!("redox_installer_tui: this tool is used to overwrite unmounted RedoxFS disk in Redox OS");
process::exit(1);
} else {
eprint!("Select a drive from 1 to {}: ", paths.len());
let mut line = String::new();
match io::stdin().read_line(&mut line) {
Ok(0) => {
eprintln!("redox_installer_tui: failed to read line: end of input");
process::exit(1);
}
Ok(_) => (),
Err(err) => {
eprintln!("redox_installer_tui: failed to read line: {}", err);
process::exit(1);
}
}
match line.trim().parse::<usize>() {
Ok(i) => {
if i >= 1 && i <= paths.len() {
break paths[i - 1].0.clone();
} else {
eprintln!("{} not from 1 to {}", i, paths.len());
}
}
Err(err) => {
eprintln!("invalid input: {}", err);
}
}
}
}
}
fn main() {
let root_path = Path::new("/");
let disk_path = choose_disk();
let Ok(password_opt) = redox_installer::prompt_password(
"redox_installer_tui: redoxfs password (empty for none)",
"redox_installer_tui: confirm password",
) else {
process::exit(1);
};
let instant = std::time::Instant::now();
let bootloader_bios = {
let path = root_path.join("usr/lib/boot/bootloader.bios");
if path.exists() {
match fs::read(&path) {
Ok(ok) => ok,
Err(err) => {
eprintln!(
"redox_installer_tui: {}: failed to read: {}",
path.display(),
err
);
process::exit(1);
}
}
} else {
Vec::new()
}
};
let bootloader_efi = {
let path = root_path.join("usr/lib/boot/bootloader.efi");
if path.exists() {
match fs::read(&path) {
Ok(ok) => ok,
Err(err) => {
eprintln!(
"redox_installer_tui: {}: failed to read: {}",
path.display(),
err
);
process::exit(1);
}
}
} else {
Vec::new()
}
};
let disk_option = DiskOption {
bootloader_bios: &bootloader_bios,
bootloader_efi: &bootloader_efi,
password_opt: password_opt.as_ref().map(|x| x.as_bytes()),
efi_partition_size: None,
skip_partitions: false, // TODO?
};
let res = with_whole_disk(&disk_path, &disk_option, |mut fs| {
// Fast install method via filesystem clone
let mut last_percent = 0;
if try_fast_install(&mut fs, move |used, used_old| {
let percent = (used * 100) / used_old;
if percent != last_percent {
eprint!(
"\r{}%: {} MB/{} MB",
percent,
used / 1000 / 1000,
used_old / 1000 / 1000
);
last_percent = percent;
}
})? {
eprintln!("\rfinished installing using fast mode");
return Ok(());
}
// Slow install method via file copy
with_redoxfs_mount(fs, None, |mount_path| {
let mut config: Config = Config::from_file(&root_path.join("filesystem.toml"))?;
// Copy filesystem.toml, which is not packaged
let mut files = vec!["filesystem.toml".to_string()];
// Copy files from locally installed packages
package_files(&root_path, &mut config, &mut files)
// TODO: implement Error trait
.map_err(|err| anyhow!("failed to read package files: {err}"))?;
// Perform config install (after packages have been converted to files)
eprintln!("configuring system");
let cookbook: Option<&'static str> = None;
redox_installer::install_dir(config, mount_path, cookbook)
.map_err(|err| io::Error::other(err))?;
// Sort and remove duplicates
files.sort();
files.dedup();
// Install files
let mut buf = vec![0; 4096 * 1024];
for (i, name) in files.iter().enumerate() {
eprintln!("copy {} [{}/{}]", name, i, files.len());
let src = root_path.join(name);
let dest = mount_path.join(name);
copy_file(&src, &dest, &mut buf)?;
}
eprintln!("finished installing, unmounting filesystem");
Ok(())
})
});
match res {
Ok(()) => {
eprintln!(
"redox_installer_tui: installed successfully in {:?}",
instant.elapsed()
);
process::exit(0);
}
Err(err) => {
eprintln!("redox_installer_tui: failed to install: {:?}", err);
process::exit(1);
}
}
}
+418
View File
@@ -0,0 +1,418 @@
//! Runtime file-collision detection for `install_dir()`.
//!
//! The installer writes files in four layers (see AGENTS.md § "Installer File
//! Layering"):
//!
//! ```text
//! Layer 1: Config pre-install [[files]] (postinstall = false)
//! Layer 2: Package staging (install_packages())
//! Layer 3: Config post-install [[files]] (postinstall = true)
//! Layer 4: User/group creation (passwd, shadow, group)
//! ```
//!
//! Last writer wins. Layer 2 silently overwriting Layer 1 is the D-Bus
//! regression class: a config `[[files]]` entry writes to a path, a package
//! stages the same path during `install_packages()`, the package wins, the
//! operator's customization is gone — and the build succeeds. This module
//! makes that class of bug visible at install time.
//!
//! ## Behavior
//!
//! - Every file write (regular file or symlink) is recorded with its layer
//! and source.
//! - When a later layer overwrites a path written by an earlier layer, a
//! `[COLLISION-WARN]` line is emitted to stderr. The build-time
//! `scripts/validate-collision-log.sh` greps logs for that marker.
//! - Layer 3 (`ConfigPostInstall`) is the *intentional override* layer — its
//! whole purpose is to overwrite package defaults. Collisions from Layer 3
//! are suppressed by design and never warn.
//! - Known-safe override path pairs (e.g. `/etc/init.d/` over
//! `/usr/lib/init.d/`, mirroring the init system's `config_for_dirs()`
//! priority) are also suppressed.
//! - Strict mode (`REDBEAR_INSTALLER_STRICT_COLLISIONS=1`) promotes
//! collisions to hard errors via `anyhow::bail!`. Default is warn-only.
//!
//! ## What is NOT tracked
//!
//! Directory creation. `fs::create_dir_all` is idempotent; two layers
//! creating the same directory is a no-op, not a collision. Only regular
//! files and symlinks are recorded.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{bail, Result};
/// Installer file-write layers, in write order. The numeric value matches
/// the layer numbering in AGENTS.md § "Installer File Layering" and is used
/// in user-facing warning text.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Layer {
/// `[[files]]` entries with `postinstall = false`. Written first.
ConfigPreInstall = 1,
/// Package staging via `install_packages()`. Layer 2 overwriting
/// Layer 1 is the D-Bus regression class (silent loss of config).
PackageStaging = 2,
/// `[[files]]` entries with `postinstall = true`. Intentional override
/// layer — collisions from this layer never warn by design.
ConfigPostInstall = 3,
/// Files the installer itself writes at the end of `install_dir()`
/// (`/etc/passwd`, `/etc/shadow`, `/etc/group`).
InstallerBuiltin = 4,
}
impl Layer {
/// Numeric layer identifier as documented in AGENTS.md. Stable across
/// releases — used in `[COLLISION-WARN]` output.
pub fn as_u8(self) -> u8 {
self as u8
}
}
/// Where a tracked file came from.
#[derive(Debug, Clone)]
pub enum ProvenanceSource {
/// `[[files]]` entry. The string is the config-visible path
/// (leading-slash form, e.g. `"/etc/passwd"`).
ConfigFile(String),
/// Package staging. The string is a package identifier, or `"staged"`
/// when the writing package cannot be identified (the common case —
/// `install_packages()` does not report per-file provenance).
Package(String),
/// Installer-builtin file (`/etc/passwd`, `/etc/shadow`, `/etc/group`).
InstallerBuiltin,
}
/// Provenance record for one file path.
#[derive(Debug, Clone)]
pub struct FileProvenance {
/// What wrote the file.
pub source: ProvenanceSource,
/// Which install layer wrote it.
pub layer: Layer,
}
/// One collision event recorded by the tracker.
#[derive(Debug, Clone)]
pub struct Collision {
/// Absolute filesystem path that was overwritten.
pub path: PathBuf,
/// Provenance of the earlier (overwritten) write.
pub previous: FileProvenance,
/// Provenance of the later (overwriting) write.
pub current: FileProvenance,
}
/// Marker emitted on stderr for every collision warning. The build-time
/// `scripts/validate-collision-log.sh` script greps logs for this token.
pub const WARN_MARKER: &str = "[COLLISION-WARN]";
/// Marker emitted on stderr when strict mode promotes a collision to an
/// error. Paired with `WARN_MARKER` in the same message so log-grepping
/// for either token catches every collision.
pub const ERROR_MARKER: &str = "[COLLISION-ERROR]";
/// Environment variable name that opts into strict mode (promotes
/// collisions to hard errors). Default is warn-only.
pub const STRICT_ENV_VAR: &str = "REDBEAR_INSTALLER_STRICT_COLLISIONS";
/// Pairs of `(override_dir, package_default_dir)`. When the current write
/// is under `override_dir` and the previous write was under
/// `package_default_dir`, the collision is considered intentional and is
/// suppressed. These mirror the init system's `config_for_dirs()` priority
/// directories documented in AGENTS.md § "Init Service File Ownership".
const KNOWN_SAFE_OVERRIDE_PAIRS: &[(&str, &str)] = &[
// Config overrides at /etc/init.d/ intentionally replace package
// defaults at /usr/lib/init.d/ for the same filename. The init
// system's config_for_dirs() BTreeMap gives /etc/init.d/ priority.
("/etc/init.d", "/usr/lib/init.d"),
// Likewise for environment.d — config overrides at /etc/ replace
// package defaults at /usr/lib/.
("/etc/environment.d", "/usr/lib/environment.d"),
];
/// Returns true if `(previous_config_path, current_config_path)` matches a
/// known-safe override pair: previous under a package default dir, current
/// under the matching config override dir. Both arguments are absolute
/// config-visible paths (leading `/`).
fn is_known_safe_override(previous_config_path: &str, current_config_path: &str) -> bool {
for &(override_dir, default_dir) in KNOWN_SAFE_OVERRIDE_PAIRS {
let curr_is_override =
current_config_path == override_dir || is_dir_member(current_config_path, override_dir);
let prev_is_default =
previous_config_path == default_dir || is_dir_member(previous_config_path, default_dir);
if curr_is_override && prev_is_default {
return true;
}
}
false
}
/// True if `child` is a file path strictly inside `parent` (not `parent`
/// itself). Both arguments are absolute config-visible paths with no
/// trailing slash. Uses byte comparison to stay I/O-free.
fn is_dir_member(child: &str, parent: &str) -> bool {
// We expect paths like "/etc/init.d/foo" inside "/etc/init.d".
let parent_with_slash = format!("{parent}/");
child.starts_with(&parent_with_slash)
}
/// File-write provenance tracker for `install_dir()`.
///
/// Construct with [`CollisionTracker::new`] to pick up strict mode from the
/// environment, or with [`CollisionTracker::new_with_strict`] for tests and
/// explicit control. Call [`CollisionTracker::record`] before each file
/// write, and [`CollisionTracker::scan_package_staging`] once after
/// `install_packages()` returns.
pub struct CollisionTracker {
/// Map from absolute filesystem path to the most recent provenance.
files: HashMap<PathBuf, FileProvenance>,
/// Map from absolute filesystem path back to the leading-`/` config
/// path, when known. Lets the known-safe override check compare
/// previous vs. current config-visible paths even when both resolves
/// land at the same physical file (e.g. via a symlinked /etc/init.d).
config_paths: HashMap<PathBuf, String>,
strict: bool,
collisions: Vec<Collision>,
}
impl CollisionTracker {
/// Construct a tracker, reading strict mode from the
/// `REDBEAR_INSTALLER_STRICT_COLLISIONS` environment variable.
pub fn new() -> Self {
let strict = std::env::var(STRICT_ENV_VAR)
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
Self::new_with_strict(strict)
}
/// Construct a tracker with explicit strict-mode control.
///
/// Tests should prefer this over [`Self::new`] to avoid env-var races
/// under parallel test execution.
pub fn new_with_strict(strict: bool) -> Self {
Self {
files: HashMap::new(),
config_paths: HashMap::new(),
strict,
collisions: Vec::new(),
}
}
/// Number of distinct file paths currently tracked.
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.files.len()
}
/// Whether the tracker has observed any file writes.
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
/// Number of collisions recorded (warnings emitted, or errors raised in
/// strict mode).
pub fn collision_count(&self) -> usize {
self.collisions.len()
}
/// Snapshot of recorded collisions (for test inspection and reporting).
pub fn collisions(&self) -> &[Collision] {
&self.collisions
}
/// Whether strict mode is enabled on this tracker.
#[allow(dead_code)]
pub fn is_strict(&self) -> bool {
self.strict
}
/// Record a file write at `abs_path` (absolute filesystem path) with
/// the given provenance.
///
/// `config_path` is the leading-`/` config-visible path when known
/// (used for the known-safe override check), or `None` for paths whose
/// config-visible form is not meaningful.
///
/// # Collisions
///
/// A collision is when the same `abs_path` was previously written by a
/// *different* layer. Collisions emit `[COLLISION-WARN]` on stderr and
/// are recorded. In strict mode they additionally return `Err`.
///
/// # Suppression
///
/// Warnings are suppressed when either:
/// - the current write is from `Layer::ConfigPostInstall` (the
/// intentional override layer), or
/// - the previous config path and the current config path form a
/// known-safe override pair (e.g. `/etc/init.d/X` over
/// `/usr/lib/init.d/X`).
///
/// Suppressed collisions are still recorded in the `files` map (the
/// latest provenance wins) but do not emit a warning and do not
/// increment `collision_count()`.
pub fn record(
&mut self,
abs_path: PathBuf,
provenance: FileProvenance,
config_path: Option<&str>,
) -> Result<()> {
// Capture previous state BEFORE any mutation. The known-safe
// override check compares the previous config_path against the
// current one; if we let the insert below clobber the map first,
// we'd compare the new path against itself.
let previous_provenance = self.files.get(&abs_path).cloned();
let previous_config_path = self.config_paths.get(&abs_path).cloned();
if let Some(cp) = config_path {
self.config_paths.insert(abs_path.clone(), cp.to_string());
}
let Some(previous) = previous_provenance else {
self.files.insert(abs_path, provenance);
return Ok(());
};
// Same-layer re-write is not a collision. Config merges already
// deduplicate same-path entries; package staging may legitimately
// touch a file twice (e.g. an inner archive extraction).
if previous.layer == provenance.layer {
self.files.insert(abs_path, provenance);
return Ok(());
}
// Layer 3 is the intentional override layer by design. Its whole
// purpose is to overwrite package defaults — collisions from it
// must not warn or the override mechanism would be unusable.
let is_layer3_override = provenance.layer == Layer::ConfigPostInstall;
// Known-safe override path pairs (e.g. /etc/init.d over
// /usr/lib/init.d). Only meaningful when both layers recorded a
// config-visible path for the same abs_path.
let is_known_safe = match (previous_config_path.as_deref(), config_path) {
(Some(prev_cp), Some(curr_cp)) => is_known_safe_override(prev_cp, curr_cp),
_ => false,
};
if is_layer3_override || is_known_safe {
self.files.insert(abs_path, provenance);
return Ok(());
}
// Real collision — warn (and bail in strict mode).
let collision = Collision {
path: abs_path.clone(),
previous: previous.clone(),
current: provenance.clone(),
};
self.collisions.push(collision.clone());
emit_warning(&collision);
self.files.insert(abs_path, provenance);
if self.strict {
bail!(
"{ERROR_MARKER} strict mode ({STRICT_ENV_VAR}=1): collision at {} \
— {} layer {} overwritten by {} layer {}. Aborting install. \
Fix the conflicting paths, or unset {STRICT_ENV_VAR} to demote \
this to a warning.",
collision.path.display(),
source_label(&collision.previous.source),
collision.previous.layer.as_u8(),
source_label(&collision.current.source),
collision.current.layer.as_u8(),
);
}
Ok(())
}
/// Walk `dest` after `install_packages()` returns, recording every file
/// the package staging wrote.
///
/// Files already tracked from Layer 1 trigger the Layer 2 overwrites
/// Layer 1 collision (the D-Bus regression class). Files not previously
/// tracked are recorded with `Layer::PackageStaging` provenance.
///
/// Directories are not tracked — `fs::create_dir_all` is idempotent and
/// directory paths are shared infrastructure. Only regular files and
/// symlinks are recorded.
///
/// I/O errors during the walk are tolerated (treated as "no file here")
/// so a partially-populated tree does not abort the install. The
/// collision decision is made from what we *can* read.
pub fn scan_package_staging(&mut self, dest: &Path) -> Result<()> {
if !dest.exists() {
return Ok(());
}
walk_for_files(dest, dest, self)
}
}
impl Default for CollisionTracker {
fn default() -> Self {
Self::new()
}
}
/// Recursive walker that records every file (regular or symlink) under
/// `root` as a Layer 2 (PackageStaging) write.
fn walk_for_files(root: &Path, current: &Path, tracker: &mut CollisionTracker) -> Result<()> {
let read_dir = match std::fs::read_dir(current) {
Ok(rd) => rd,
// Tolerate unreadable subdirs — they contribute no files.
Err(_) => return Ok(()),
};
for entry in read_dir {
let Ok(entry) = entry else {
continue;
};
let Ok(file_type) = entry.file_type() else {
continue;
};
let abs_path = entry.path();
if file_type.is_symlink() || file_type.is_file() {
let provenance = FileProvenance {
source: ProvenanceSource::Package("staged".to_string()),
layer: Layer::PackageStaging,
};
// For package-staged files the config-visible path is the path
// under `root` (with a leading `/`). This matches how a config
// `[[files]]` entry would address the same file.
let config_visible = match abs_path.strip_prefix(root) {
Ok(rel) => format!("/{}", rel.display()),
Err(_) => abs_path.to_string_lossy().into_owned(),
};
tracker.record(abs_path, provenance, Some(&config_visible))?;
} else if file_type.is_dir() {
walk_for_files(root, &abs_path, tracker)?;
}
}
Ok(())
}
/// Emit a single `[COLLISION-WARN]` line on stderr. The build's
/// `validate-collision-log.sh` greps for this token after the install
/// finishes.
fn emit_warning(c: &Collision) {
eprintln!(
"{WARN_MARKER} {} layer {} overwritten by layer {} ({} -> {}). \
Set {STRICT_ENV_VAR}=1 to promote this to an error.",
c.path.display(),
c.previous.layer.as_u8(),
c.current.layer.as_u8(),
source_label(&c.previous.source),
source_label(&c.current.source),
);
}
/// Short human-readable label for a provenance source, used in warning and
/// error text.
fn source_label(s: &ProvenanceSource) -> &'static str {
match s {
ProvenanceSource::ConfigFile(_) => "config",
ProvenanceSource::Package(_) => "package",
ProvenanceSource::InstallerBuiltin => "installer",
}
}
+88
View File
@@ -0,0 +1,88 @@
use std::fmt::Display;
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct FileConfig {
pub path: String,
pub data: String,
#[serde(default)]
pub symlink: bool,
#[serde(default)]
pub directory: bool,
pub mode: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>,
#[serde(default)]
pub recursive_chown: bool,
#[serde(default)]
pub postinstall: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub append: bool,
}
impl FileConfig {
pub fn new_file(path: String, data: String) -> FileConfig {
FileConfig {
path,
data,
..Default::default()
}
}
pub fn new_directory(path: String) -> FileConfig {
FileConfig {
path,
data: String::new(),
directory: true,
..Default::default()
}
}
pub fn with_mod(&mut self, mode: u32, uid: u32, gid: u32) -> &mut FileConfig {
self.mode = Some(mode);
self.uid = Some(uid);
self.gid = Some(gid);
self
}
pub fn with_recursive_mod(&mut self, mode: u32, uid: u32, gid: u32) -> &mut FileConfig {
self.with_mod(mode, uid, gid);
self.recursive_chown = true;
self
}
}
impl Display for FileConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path)?;
if self.symlink {
write!(f, " -> {}", self.data)?;
} else if self.directory {
write!(f, " type=dir")?;
if self.recursive_chown {
write!(f, " chown=yes")?;
}
} else {
write!(f, " size={}", crate::format_bytes(self.data.len() as u64))?;
if self.postinstall {
write!(f, "!")?;
}
if self.append {
write!(f, " append=yes")?;
}
}
if let Some(uid) = self.uid {
write!(f, " uid={}", uid)?;
}
if let Some(uid) = self.uid {
write!(f, " gid={}", uid)?;
}
if let Some(mode) = self.mode {
write!(f, " mode={:3o}", mode)?;
}
Ok(())
}
}
+98
View File
@@ -0,0 +1,98 @@
use anyhow::{Context, Result};
use libc::{gid_t, uid_t};
use std::ffi::{CString, OsStr};
use std::fs::{self, File};
use std::io::{Error, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{symlink, PermissionsExt};
use std::path::Path;
fn chown<P: AsRef<Path>>(path: P, uid: uid_t, gid: gid_t, recursive: bool) -> Result<()> {
let path = path.as_ref();
let c_path = CString::new(path.as_os_str().as_bytes()).unwrap();
if unsafe { libc::chown(c_path.as_ptr(), uid, gid) } != 0 {
return Err(Error::last_os_error().into());
}
if recursive && path.is_dir() {
for entry_res in fs::read_dir(path)? {
let entry = entry_res?;
chown(entry.path(), uid, gid, recursive)?;
}
}
Ok(())
}
// TODO: Rewrite impls
impl crate::FileConfig {
pub(crate) fn create<P: AsRef<Path>>(&self, prefix: P) -> Result<()> {
let path = self.path.trim_start_matches('/');
let target_file = prefix.as_ref().join(path);
if self.directory {
println!("Create directory {}", target_file.display());
fs::create_dir_all(&target_file)
.with_context(|| format!("failed to create directory {}", target_file.display()))?;
self.apply_perms(&target_file)?;
return Ok(());
} else if let Some(parent) = target_file.parent() {
println!("Create file parent {}", parent.display());
fs::create_dir_all(parent)
.with_context(|| format!("failed to create file parent {}", parent.display()))?;
}
if self.symlink {
println!("Create symlink {} to {}", target_file.display(), self.data);
if target_file.is_symlink() {
fs::remove_file(&target_file).with_context(|| {
format!("failed to remove old symlink {}", target_file.display())
})?;
}
symlink(&OsStr::new(&self.data), &target_file).with_context(|| {
format!(
"failed to create symlink {} to {}",
target_file.display(),
self.data
)
})?;
Ok(())
} else {
let action = if self.append { "Append" } else { "Create" };
println!("{action} file {}", target_file.display());
let mut file = if self.append {
fs::OpenOptions::new()
.create(true)
.append(true)
.open(&target_file)
.with_context(|| format!("failed to append file {}", target_file.display()))?
} else {
File::create(&target_file)
.with_context(|| format!("failed to create file {}", target_file.display()))?
};
file.write_all(self.data.as_bytes())?;
self.apply_perms(target_file)
}
}
fn apply_perms<P: AsRef<Path>>(&self, target: P) -> Result<()> {
let path = target.as_ref();
let mode = self
.mode
.unwrap_or_else(|| if self.directory { 0o0755 } else { 0o0644 });
let uid = self.uid.unwrap_or(!0);
let gid = self.gid.unwrap_or(!0);
// chmod
fs::set_permissions(path, fs::Permissions::from_mode(mode))
.with_context(|| format!("failed to set permissions on {}", path.display()))?;
// chown
chown(path, uid, gid, self.recursive_chown)
.with_context(|| format!("failed to chown {}", path.display()))
}
}
+42
View File
@@ -0,0 +1,42 @@
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct GeneralConfig {
/// Specify a path where cookbook exists, all packages will be installed locally
pub cookbook: Option<String>,
/// Allow prompts for missing information such as user password
pub prompt: Option<bool>,
/// Total filesystem size in MB
pub filesystem_size: Option<u32>,
/// EFI partition size in MB, default to 2MB
pub efi_partition_size: Option<u32>,
/// Skip disk partitioning, assume whole disk is a partition
pub skip_partitions: Option<bool>,
/// Set a plain text password to encrypt the disk
pub encrypt_disk: Option<String>,
/// Use live disk for bootloader config, default is false
pub live_disk: Option<bool>,
/// If set, write bootloader disk into this path
pub write_bootloader: Option<String>,
/// Use AR to write files instead of FUSE-based mount
/// (bypasses FUSE, but slower and requires namespaced context such as "podman unshare")
pub no_mount: Option<bool>,
}
impl GeneralConfig {
/// Merge two config, "other" is more dominant
pub(super) fn merge(&mut self, other: GeneralConfig) {
if let Some(cookbook) = other.cookbook {
self.cookbook = Some(cookbook);
}
self.filesystem_size = other.filesystem_size.or(self.filesystem_size);
self.efi_partition_size = other.efi_partition_size.or(self.efi_partition_size);
self.skip_partitions = other.skip_partitions.or(self.skip_partitions);
if let Some(encrypt_disk) = other.encrypt_disk {
self.encrypt_disk = Some(encrypt_disk);
}
self.live_disk = other.live_disk.or(self.live_disk);
if let Some(write_bootloader) = other.write_bootloader {
self.write_bootloader = Some(write_bootloader);
}
self.no_mount = other.no_mount.or(self.no_mount);
}
}
+154
View File
@@ -0,0 +1,154 @@
use std::collections::BTreeMap;
use std::fmt::Display;
use std::fs;
use std::mem;
use std::path::{Path, PathBuf};
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use crate::PackageConfig;
pub mod file;
#[cfg(feature = "installer")]
pub mod file_impl;
pub mod general;
pub mod package;
pub mod user;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Config {
#[serde(default)]
pub include: Vec<PathBuf>,
#[serde(default)]
pub general: general::GeneralConfig,
#[serde(default)]
pub packages: BTreeMap<String, package::PackageConfig>,
#[serde(default)]
pub files: Vec<file::FileConfig>,
#[serde(default)]
pub users: BTreeMap<String, user::UserConfig>,
#[serde(default)]
pub groups: BTreeMap<String, user::GroupConfig>,
}
impl Config {
/// Load installer config from a TOML path
pub fn from_file(path: &Path) -> Result<Self> {
let mut config: Config = match fs::read_to_string(&path) {
Ok(config_data) => match toml::from_str(&config_data) {
Ok(config) => config,
Err(err) => {
bail!("failed to decode '{}': {}", path.display(), err);
}
},
Err(err) => {
bail!("failed to read '{}': {}", path.display(), err);
}
};
let config_dir = path.parent().unwrap();
let mut configs = mem::take(&mut config.include)
.into_iter()
.map(|path| {
Config::from_file(&config_dir.join(&path))
.with_context(|| format!("Importing from {}", path.display()))
})
.collect::<Result<Vec<Config>>>()?;
configs.push(config); // Put ourself last to ensure that it overwrites anything else.
config = configs.remove(0);
for other_config in configs {
config.merge(other_config);
}
Ok(config)
}
/// Load hardcoded install config to fetch bootloaders
pub fn bootloader_config() -> Self {
let mut bootloader_config = Config::default();
// TODO: This is unused
bootloader_config.files.push(file::FileConfig {
path: "/etc/pkg.d/50_redox".to_string(),
data: "https://static.redox-os.org/pkg".to_string(),
..Default::default()
});
bootloader_config
.packages
.insert("bootloader".to_string(), PackageConfig::default());
bootloader_config
}
pub fn merge(&mut self, other: Config) {
assert!(self.include.is_empty());
assert!(other.include.is_empty());
let Config {
include: _,
general: other_general,
packages: other_packages,
files: other_files,
users: other_users,
groups: other_groups,
} = other;
self.general.merge(other_general);
for (package, package_config) in other_packages {
self.packages.insert(package, package_config);
}
// File entries from later-included configs override earlier ones for
// the same path. Previously this was `self.files.extend(other_files)`,
// which silently accumulated duplicate paths and left the winner to
// whichever entry the installer wrote last — an invisible, order-
// dependent override that is a frequent source of "my config edit did
// nothing" confusion (e.g. an init service redefined by a legacy
// overlay config). Make the override explicit and visible: warn on the
// console and replace in place so the last definition deterministically
// wins with no duplicate left in the list.
for file in other_files {
if let Some(existing) = self.files.iter_mut().find(|f| f.path == file.path) {
eprintln!(
"config: WARNING: file '{}' redefined by a later-included config \
(override — last definition wins)",
file.path
);
*existing = file;
} else {
self.files.push(file);
}
}
for (user, user_config) in other_users {
self.users.insert(user, user_config);
}
for (group, group_config) in other_groups {
self.groups.insert(group, group_config);
}
}
}
impl Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "files:")?;
for file in &self.files {
writeln!(f, "- {}", file)?;
}
writeln!(f, "users:")?;
for (name, user) in &self.users {
writeln!(f, "- {}:{}", name, user)?;
}
write!(f, "packages: ")?;
for name in self.packages.keys() {
write!(f, " {}", name)?;
}
writeln!(f, "")?;
Ok(())
}
}
+19
View File
@@ -0,0 +1,19 @@
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PackageConfig {
Empty,
Build(String),
// TODO: Sum type
Spec {
version: Option<String>,
git: Option<String>,
path: Option<String>,
},
}
impl Default for PackageConfig {
fn default() -> Self {
Self::Empty
}
}
+43
View File
@@ -0,0 +1,43 @@
use std::fmt::Display;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct UserConfig {
pub password: Option<String>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub name: Option<String>,
pub home: Option<String>,
pub shell: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct GroupConfig {
pub gid: Option<u32>,
// FIXME move this to the UserConfig struct as extra_groups
pub members: Vec<String>,
}
impl Display for UserConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(uid) = &self.uid {
write!(f, " uid={}", uid)?;
}
if let Some(gid) = &self.gid {
write!(f, " gid={}", gid)?;
}
if let Some(name) = &self.name {
write!(f, " name={}", name)?;
}
if let Some(home) = &self.home {
write!(f, " home={}", home)?;
}
if let Some(shell) = &self.shell {
write!(f, " shell={}", shell)?;
}
if self.password.as_ref().is_some_and(|s| !s.is_empty()) {
write!(f, " password=yes")?;
}
Ok(())
}
}
+127
View File
@@ -0,0 +1,127 @@
use std::{
cmp,
convert::TryInto,
fs::{File, OpenOptions},
io::{Read, Result, Seek, SeekFrom, Write},
path::Path,
};
#[derive(Debug)]
pub struct DiskWrapper {
disk: File,
size: u64,
block: Box<[u8]>,
seek: u64,
}
enum Buffer<'a> {
Read(&'a mut [u8]),
Write(&'a [u8]),
}
impl DiskWrapper {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let disk = OpenOptions::new().read(true).write(true).open(path)?;
let metadata = disk.metadata()?;
let size = metadata.len();
// TODO: get real block size: disk_metadata.blksize() works on disks but not image files
let block_size = 512;
let block = vec![0u8; block_size].into_boxed_slice();
Ok(Self {
disk,
size,
block,
seek: 0,
})
}
pub fn block_size(&self) -> usize {
self.block.len()
}
pub fn size(&self) -> u64 {
self.size
}
fn io<'a>(&mut self, buf: &mut Buffer<'a>) -> Result<usize> {
let buf_len = match buf {
Buffer::Read(read) => read.len(),
Buffer::Write(write) => write.len(),
};
let block_len: u64 = self.block.len().try_into().unwrap();
// Do aligned I/O quickly
if self.seek % block_len == 0 && buf_len as u64 % block_len == 0 {
self.disk.seek(SeekFrom::Start(self.seek))?;
match buf {
Buffer::Read(read) => self.disk.read_exact(read)?,
Buffer::Write(write) => self.disk.write_all(write)?,
}
self.seek = self.seek.checked_add(buf_len.try_into().unwrap()).unwrap();
return Ok(buf_len);
}
let mut i = 0;
while i < buf_len {
let block = self.seek / block_len;
let offset: usize = (self.seek % block_len).try_into().unwrap();
let remaining = buf_len.checked_sub(i).unwrap();
let len = cmp::min(remaining, self.block.len().checked_sub(offset).unwrap());
self.disk
.seek(SeekFrom::Start(block.checked_mul(block_len).unwrap()))?;
self.disk.read_exact(&mut self.block)?;
match buf {
Buffer::Read(read) => {
read[i..i.checked_add(len).unwrap()]
.copy_from_slice(&self.block[offset..offset.checked_add(len).unwrap()]);
}
Buffer::Write(write) => {
self.block[offset..offset.checked_add(len).unwrap()]
.copy_from_slice(&write[i..i.checked_add(len).unwrap()]);
self.disk
.seek(SeekFrom::Start(block.checked_mul(block_len).unwrap()))?;
self.disk.write_all(&mut self.block)?;
}
}
i = i.checked_add(len).unwrap();
self.seek = self.seek.checked_add(len.try_into().unwrap()).unwrap();
}
Ok(i)
}
}
impl Read for DiskWrapper {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
self.io(&mut Buffer::Read(buf))
}
}
impl Seek for DiskWrapper {
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
let current: i64 = self.seek.try_into().unwrap();
let end: i64 = self.size.try_into().unwrap();
self.seek = match pos {
SeekFrom::Start(offset) => cmp::min(self.size, offset),
SeekFrom::End(offset) => cmp::max(0, cmp::min(end, end.wrapping_add(offset))) as u64,
SeekFrom::Current(offset) => {
cmp::max(0, cmp::min(end, current.wrapping_add(offset))) as u64
}
};
Ok(self.seek)
}
}
impl Write for DiskWrapper {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
self.io(&mut Buffer::Write(buf))
}
fn flush(&mut self) -> Result<()> {
self.disk.flush()
}
}
+918
View File
@@ -0,0 +1,918 @@
use anyhow::Context;
use anyhow::{bail, Result};
use pkg::Library;
use rand::{rngs::OsRng, TryRngCore};
use redoxfs::{unmount_path, Disk, DiskIo, FileSystem, BLOCK_SIZE};
use termion::input::TermRead;
use crate::collision::{CollisionTracker, FileProvenance, Layer, ProvenanceSource};
use crate::config::file::FileConfig;
use crate::config::package::PackageConfig;
use crate::config::Config;
use crate::disk_wrapper::DiskWrapper;
use std::{
cell::RefCell,
collections::BTreeMap,
env,
fmt::Write as WriteFmt,
fs,
io::{self, Seek, SeekFrom, Write},
path::{Path, PathBuf},
process,
rc::Rc,
sync::mpsc::channel,
thread,
time::{SystemTime, UNIX_EPOCH},
};
pub struct DiskOption<'a> {
pub bootloader_bios: &'a [u8],
pub bootloader_efi: &'a [u8],
pub password_opt: Option<&'a [u8]>,
pub efi_partition_size: Option<u32>, //MiB
pub skip_partitions: bool,
}
pub fn get_target() -> String {
// TODO: Configurable from filesystem config?
env::var("TARGET").unwrap_or(
option_env!("TARGET").map_or("x86_64-unknown-redox".to_string(), |x| x.to_string()),
)
}
/// Converts a password to a serialized argon2rs hash, understandable
/// by redox_users. If the password is blank, the hash is blank.
fn hash_password(password: &str) -> Result<String> {
if !password.is_empty() {
let salt = format!("{:X}", OsRng.try_next_u64()?);
let config = argon2::Config::default();
let hash = argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &config)?;
Ok(hash)
} else {
Ok("".into())
}
}
fn syscall_error(err: syscall::Error) -> io::Error {
io::Error::from_raw_os_error(err.errno)
}
/// Returns a password collected from the user (plaintext)
pub fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result<Option<String>> {
let stdin = io::stdin();
let mut stdin = stdin.lock();
let stdout = io::stdout();
let mut stdout = stdout.lock();
for i in 0..3 {
print!("{}", prompt);
let mut password = stdin.read_passwd(&mut stdout)?;
if let Some(password) = password.as_mut() {
*password = password.trim().to_string();
}
password.take_if(|s| s.is_empty());
if password.is_none() {
return Ok(None);
}
print!("\n{}", confirm_prompt);
let confirm_password = stdin.read_passwd(&mut stdout)?;
// Note: Actually comparing two Option<String> values
if confirm_password == password {
return Ok(password);
} else if i < 2 {
eprintln!("passwords do not match, please try again");
}
}
bail!("passwords do not match, giving up");
}
fn install_packages(config: &Config, dest: &Path, cookbook: Option<&str>) -> anyhow::Result<()> {
let target = &get_target();
let packages: Vec<&String> = config
.packages
.iter()
.filter_map(|(packagename, package)| match package {
PackageConfig::Build(rule) if rule == "ignore" => None,
_ => Some(packagename),
})
.collect();
let mut library = if let Some(cookbook) = cookbook {
let callback = pkg::callback::PlainCallback::new();
let repo = Path::new(cookbook).join("repo");
let pubkey = Path::new(cookbook).join("build");
Library::new_local(
repo,
pubkey,
dest.to_path_buf(),
target,
Rc::new(RefCell::new(callback)),
)
} else {
let callback = pkg::callback::IndicatifCallback::new();
Library::new_remote(
&vec!["https://static.redox-os.org/pkg"],
dest,
target,
Rc::new(RefCell::new(callback)),
)
}?;
let packages = pkg::PackageName::from_list(packages)?;
library.install(packages)?;
library.apply()?;
Ok(())
}
pub fn install_dir(
config: Config,
output_dir: impl AsRef<Path>,
cookbook: Option<&str>,
) -> Result<()> {
let output_dir = output_dir.as_ref();
let output_dir = output_dir.to_owned();
let mut tracker = CollisionTracker::new();
// Layer 1: config pre-install [[files]] (postinstall = false).
for file in &config.files {
if !file.postinstall {
record_config_file(&mut tracker, &output_dir, file, Layer::ConfigPreInstall)?;
file.create(&output_dir)?;
}
}
// Layer 2: package staging. install_packages() is opaque to per-file
// provenance, so we walk output_dir afterwards to register what it wrote
// and detect the Layer 2 overwrites Layer 1 collision (D-Bus regression
// class — config edits silently lost).
install_packages(&config, &output_dir, cookbook)?;
tracker.scan_package_staging(&output_dir)?;
// Layer 3: config post-install [[files]] (postinstall = true). This is
// the intentional override layer — CollisionTracker suppresses its
// collisions by design.
for file in &config.files {
if file.postinstall {
record_config_file(&mut tracker, &output_dir, file, Layer::ConfigPostInstall)?;
file.create(&output_dir)?;
}
}
let mut passwd = String::new();
let mut shadow = String::new();
let mut next_uid = 1000;
let mut next_gid = 1000;
let mut groups = vec![];
for (username, user) in config.users {
// plaintext
let password = if let Some(password) = user.password {
password
} else if config.general.prompt.unwrap_or(true) {
prompt_password(
&format!("{}: enter password: ", username),
&format!("{}: confirm password: ", username),
)?
.unwrap_or_default()
} else {
String::new()
};
let uid = user.uid.unwrap_or(next_uid);
if uid >= next_uid {
next_uid = uid + 1;
}
let gid = user.gid.unwrap_or(next_gid);
if gid >= next_gid {
next_gid = gid + 1;
}
let name = user.name.unwrap_or(username.clone());
let home = user.home.unwrap_or(format!("/home/{}", username));
let shell = user.shell.unwrap_or("/bin/ion".into());
println!("Adding user {username}:");
if password.is_empty() {
println!("\tPassword: unset");
} else {
println!("\tPassword: set");
}
println!("\tUID: {uid}");
println!("\tGID: {gid}");
println!("\tName: {name}");
println!("\tHome: {home}");
println!("\tShell: {shell}");
FileConfig::new_directory(home.clone())
.with_recursive_mod(0o700, uid, gid)
.create(&output_dir)?;
if uid >= 1000 {
prepare_user_home(&output_dir, uid, gid, &home)?;
}
let password = hash_password(&password)?;
passwd.push_str(&format!("{username};{uid};{gid};{name};{home};{shell}\n",));
shadow.push_str(&format!("{username};{password}\n"));
groups.push((username.clone(), gid, vec![username]));
}
for (group, group_config) in config.groups {
// FIXME this assumes there is no overlap between auto-created groups for users
// and explicitly specified groups.
let gid = group_config.gid.unwrap_or(next_gid);
if gid >= next_gid {
next_gid = gid + 1;
}
groups.push((group, gid, group_config.members));
}
// Layer 4: installer-builtin files (/etc/passwd, /etc/shadow, /etc/group).
// A collision here means a package or config also wrote one of these
// core files — almost always a real misconfiguration worth surfacing.
if !passwd.is_empty() {
record_installer_builtin(&mut tracker, &output_dir, "/etc/passwd")?;
FileConfig::new_file("/etc/passwd".to_string(), passwd).create(&output_dir)?;
}
if !shadow.is_empty() {
record_installer_builtin(&mut tracker, &output_dir, "/etc/shadow")?;
FileConfig::new_file("/etc/shadow".to_string(), shadow)
.with_mod(0o0600, 0, 0)
.create(&output_dir)?;
}
if !groups.is_empty() {
let mut groups_data = String::new();
for (name, gid, members) in groups {
use std::fmt::Write;
writeln!(groups_data, "{name};x;{gid};{}", members.join(","))?;
println!("Adding group {name}:");
println!("\tGID: {gid}");
println!("\tMembers: {}", members.join(", "));
}
record_installer_builtin(&mut tracker, &output_dir, "/etc/group")?;
FileConfig::new_file("/etc/group".to_string(), groups_data)
.with_mod(0o0600, 0, 0)
.create(&output_dir)?;
}
Ok(())
}
// CollisionTracker wiring helpers. Each records the provenance of one file
// write that is about to happen; the actual write (`FileConfig::create`)
// is performed by the caller. Directory creations are not recorded (see
// collision.rs module docs).
fn record_config_file(
tracker: &mut CollisionTracker,
output_dir: &Path,
file: &FileConfig,
layer: Layer,
) -> Result<()> {
if file.directory {
return Ok(());
}
let abs_path = output_dir.join(file.path.trim_start_matches('/'));
tracker.record(
abs_path,
FileProvenance {
source: ProvenanceSource::ConfigFile(file.path.clone()),
layer,
},
Some(&file.path),
)
}
fn record_installer_builtin(
tracker: &mut CollisionTracker,
output_dir: &Path,
config_path: &str,
) -> Result<()> {
tracker.record(
output_dir.join(config_path.trim_start_matches('/')),
FileProvenance {
source: ProvenanceSource::InstallerBuiltin,
layer: Layer::InstallerBuiltin,
},
Some(config_path),
)
}
fn prepare_user_home(
output_dir: &PathBuf,
uid: u32,
gid: u32,
home: &String,
) -> Result<(), anyhow::Error> {
for xdg_folder in &[
"Desktop",
"Documents",
"Downloads",
"Music",
"Pictures",
"Public",
"Templates",
"Videos",
".config",
".local",
".local/share",
".local/share/Trash",
".local/share/Trash/info",
] {
FileConfig::new_directory(format!("{}/{}", home, xdg_folder))
.with_mod(0o0700, uid, gid)
.create(output_dir)?;
}
FileConfig::new_file(
format!("{}/.config/user-dirs.dirs", home),
r#"# Produced by redox installer
XDG_DESKTOP_DIR="$HOME/Desktop"
XDG_DOCUMENTS_DIR="$HOME/Documents"
XDG_DOWNLOAD_DIR="$HOME/Downloads"
XDG_MUSIC_DIR="$HOME/Music"
XDG_PICTURES_DIR="$HOME/Pictures"
XDG_PUBLICSHARE_DIR="$HOME/Public"
XDG_TEMPLATES_DIR="$HOME/Templates"
XDG_VIDEOS_DIR="$HOME/Videos"
"#
.to_string(),
)
.with_mod(0o0600, uid, gid)
.create(output_dir)?;
let skel_dir = output_dir.join("etc/skel");
if skel_dir.is_dir() {
copy_dir_all(&skel_dir, home.clone(), output_dir, uid, gid)?;
}
Ok(())
}
fn copy_dir_all(
src: impl AsRef<Path>,
dst: String,
output_dir: &Path,
uid: u32,
gid: u32,
) -> anyhow::Result<()> {
if !Path::new(dst.as_str()).is_dir() {
FileConfig::new_directory(dst.clone())
.with_mod(0o0700, uid, gid)
.create(&output_dir)?;
}
for entry in fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let dst_path = format!("{}/{}", dst, entry.file_name().display());
if file_type.is_dir() {
copy_dir_all(entry.path(), dst_path, output_dir, uid, gid)?;
} else if file_type.is_file() {
FileConfig::new_file(
dst_path,
fs::read_to_string(entry.path())
.with_context(|| format!("Reading {}", entry.path().display()))?,
)
.with_mod(0o0600, uid, gid)
.create(&output_dir)?;
} else if file_type.is_symlink() {
// TODO
}
}
Ok(())
}
pub fn with_redoxfs<D, T, F>(disk: D, password_opt: Option<&[u8]>, callback: F) -> Result<T>
where
D: Disk + Send + 'static,
F: FnOnce(FileSystem<D>) -> Result<T>,
{
let ctime = SystemTime::now().duration_since(UNIX_EPOCH)?;
let fs = FileSystem::create(disk, password_opt, ctime.as_secs(), ctime.subsec_nanos())
.map_err(syscall_error)?;
callback(fs)
}
fn decide_mount_path(mount_path: Option<&Path>) -> PathBuf {
let mount_path = mount_path.map(|p| p.to_path_buf()).unwrap_or_else(|| {
PathBuf::from(if cfg!(target_os = "redox") {
format!("file.redox_installer_{}", process::id())
} else {
format!("/tmp/redox_installer_{}", process::id())
})
});
mount_path
}
pub fn with_redoxfs_mount<D, T, F>(
fs: FileSystem<D>,
mount_path: Option<&Path>,
callback: F,
) -> Result<T>
where
D: Disk + Send + 'static,
F: FnOnce(&Path) -> Result<T>,
{
let mount_path = decide_mount_path(mount_path);
if cfg!(not(target_os = "redox")) && !mount_path.exists() {
fs::create_dir(&mount_path)?;
}
let (tx, rx) = channel();
let join_handle = {
let mount_path = mount_path.clone();
thread::spawn(move || {
let res = redoxfs::mount(fs, &mount_path, |real_path| {
tx.send(Ok(real_path.to_owned())).unwrap();
});
match res {
Ok(()) => (),
Err(err) => {
tx.send(Err(err)).unwrap();
}
};
})
};
let res = match rx.recv() {
Ok(ok) => match ok {
Ok(real_path) => callback(&real_path),
Err(err) => return Err(err.into()),
},
Err(_) => {
return Err(io::Error::new(
io::ErrorKind::NotConnected,
"redoxfs thread did not send a result",
)
.into())
}
};
unmount_path(&mount_path.as_os_str().to_str().unwrap())?;
join_handle.join().unwrap();
if cfg!(not(target_os = "redox")) {
fs::remove_dir_all(&mount_path)?;
}
res
}
pub fn with_redoxfs_ar<D, T, F>(
mut fs: FileSystem<D>,
mount_path: Option<&Path>,
callback: F,
) -> Result<T>
where
D: Disk + Send + 'static,
F: FnOnce(&Path) -> Result<T>,
{
let mount_path = decide_mount_path(mount_path);
let res = callback(Path::new(&mount_path));
if res.is_ok() {
let _end_block = fs
.tx(|tx| {
// Archive_at root node
redoxfs::archive_at(tx, Path::new(&mount_path), redoxfs::TreePtr::root())
.map_err(|err| syscall::Error::new(err.raw_os_error().unwrap()))?;
// Squash alloc log
tx.sync(true)?;
let end_block = tx.header.size() / BLOCK_SIZE;
/* TODO: Cut off any free blocks at the end of the filesystem
let mut end_changed = true;
while end_changed {
end_changed = false;
let allocator = fs.allocator();
let levels = allocator.levels();
for level in 0..levels.len() {
let level_size = 1 << level;
for &block in levels[level].iter() {
if block < end_block && block + level_size >= end_block {
end_block = block;
end_changed = true;
}
}
}
}
*/
// Update header
tx.header.size = (end_block * BLOCK_SIZE).into();
tx.header_changed = true;
tx.sync(false)?;
Ok(end_block)
})
.map_err(syscall_error)?;
// let size = (fs.block + end_block) * BLOCK_SIZE;
// fs.disk.file.set_len(size)?;
}
fs::remove_dir_all(&mount_path)?;
res
}
pub fn fetch_bootloaders(
config: &Config,
cookbook: Option<&str>,
live: bool,
) -> Result<(Vec<u8>, Vec<u8>)> {
let bootloader_dir =
PathBuf::from(format!("/tmp/redox_installer_bootloader_{}", process::id()));
if bootloader_dir.exists() {
fs::remove_dir_all(&bootloader_dir)?;
}
fs::create_dir(&bootloader_dir)?;
let mut bootloader_config = Config::bootloader_config();
bootloader_config.general = config.general.clone();
install_packages(&bootloader_config, &bootloader_dir, cookbook)?;
let boot_dir = bootloader_dir.join("usr/lib/boot");
let bios_path = boot_dir.join(if live {
"bootloader-live.bios"
} else {
"bootloader.bios"
});
let efi_path = boot_dir.join(if live {
"bootloader-live.efi"
} else {
"bootloader.efi"
});
let bios_data = if bios_path.exists() {
fs::read(bios_path)?
} else {
Vec::new()
};
let efi_data = if efi_path.exists() {
fs::read(efi_path)?
} else {
Vec::new()
};
fs::remove_dir_all(&bootloader_dir)?;
Ok((bios_data, efi_data))
}
//TODO: make bootloaders use Option, dynamically create BIOS and EFI partitions
pub fn with_whole_disk<P, F, T>(disk_path: P, disk_option: &DiskOption, callback: F) -> Result<T>
where
P: AsRef<Path>,
F: FnOnce(FileSystem<DiskIo<fscommon::StreamSlice<DiskWrapper>>>) -> Result<T>,
{
let target = get_target();
let bootloader_efi_name = match target.as_str() {
"aarch64-unknown-redox" => "BOOTAA64.EFI",
"i586-unknown-redox" | "i686-unknown-redox" => "BOOTIA32.EFI",
"x86_64-unknown-redox" => "BOOTX64.EFI",
"riscv64gc-unknown-redox" => "BOOTRISCV64.EFI",
_ => {
bail!("target '{target}' not supported");
}
};
// Open disk and read metadata
eprintln!("Opening disk {}", disk_path.as_ref().display());
let mut disk_file = DiskWrapper::open(disk_path.as_ref())?;
let disk_size = disk_file.size();
let block_size = disk_file.block_size() as u64;
if disk_option.skip_partitions {
return with_redoxfs(
DiskIo(fscommon::StreamSlice::new(
disk_file,
0,
disk_size.next_multiple_of(block_size),
)?),
disk_option.password_opt,
callback,
);
}
let gpt_block_size = match block_size {
512 => gpt::disk::LogicalBlockSize::Lb512,
_ => {
// TODO: support (and test) other block sizes
bail!("block size {block_size} not supported");
}
};
// Calculate partition offsets
let gpt_reserved = 34 * 512; // GPT always reserves 34 512-byte sectors
let mibi = 1024 * 1024;
// First megabyte of the disk is reserved for BIOS partition, wich includes GPT tables
let bios_start = gpt_reserved / block_size;
let bios_end = (mibi / block_size) - 1;
// Second megabyte of the disk is reserved for EFI partition
let efi_start = bios_end + 1;
let efi_size = if let Some(size) = disk_option.efi_partition_size {
size as u64
} else {
1
};
let efi_end = efi_start + (efi_size * mibi / block_size) - 1;
// The rest of the disk is RedoxFS, reserving the GPT table mirror at the end of disk
let redoxfs_start = efi_end + 1;
let redoxfs_end = ((((disk_size - gpt_reserved) / mibi) * mibi) / block_size) - 1;
// Format and install BIOS partition
{
// Write BIOS bootloader to disk
eprintln!(
"Write bootloader with size {:#x}",
disk_option.bootloader_bios.len()
);
disk_file.seek(SeekFrom::Start(0))?;
disk_file.write_all(&disk_option.bootloader_bios)?;
// Replace MBR tables with protective MBR
// TODO: div_ceil
let mbr_blocks = ((disk_size + block_size - 1) / block_size) - 1;
eprintln!("Writing protective MBR with disk blocks {mbr_blocks:#x}");
gpt::mbr::ProtectiveMBR::with_lb_size(mbr_blocks as u32)
.update_conservative(&mut disk_file)?;
// Open disk, mark it as not initialized
let mut gpt_disk = gpt::GptConfig::new()
.initialized(false)
.writable(true)
.logical_block_size(gpt_block_size)
.create_from_device(Box::new(&mut disk_file), None)?;
// Add BIOS boot partition
let mut partitions = BTreeMap::new();
let mut partition_id = 1;
partitions.insert(
partition_id,
gpt::partition::Partition {
part_type_guid: gpt::partition_types::BIOS,
part_guid: uuid::Uuid::new_v4(),
first_lba: bios_start,
last_lba: bios_end,
flags: 0, // TODO
name: "BIOS".to_string(),
},
);
partition_id += 1;
// Add EFI boot partition
partitions.insert(
partition_id,
gpt::partition::Partition {
part_type_guid: gpt::partition_types::EFI,
part_guid: uuid::Uuid::new_v4(),
first_lba: efi_start,
last_lba: efi_end,
flags: 0, // TODO
name: "EFI".to_string(),
},
);
partition_id += 1;
// Add RedoxFS partition
partitions.insert(
partition_id,
gpt::partition::Partition {
//TODO: Use REDOX_REDOXFS type (needs GPT crate changes)
part_type_guid: gpt::partition_types::LINUX_FS,
part_guid: uuid::Uuid::new_v4(),
first_lba: redoxfs_start,
last_lba: redoxfs_end,
flags: 0,
name: "REDOX".to_string(),
},
);
eprintln!("Writing GPT tables: {partitions:#?}");
// Initialize GPT table
gpt_disk.update_partitions(partitions)?;
// Write partition layout, returning disk file
gpt_disk.write()?;
}
// Format and install EFI partition
{
let disk_efi_start = efi_start * block_size;
let disk_efi_end = (efi_end + 1) * block_size;
let mut disk_efi =
fscommon::StreamSlice::new(&mut disk_file, disk_efi_start, disk_efi_end)?;
eprintln!(
"Formatting EFI partition with size {:#x}",
disk_efi_end - disk_efi_start
);
fatfs::format_volume(&mut disk_efi, fatfs::FormatVolumeOptions::new())?;
eprintln!("Opening EFI partition");
let fs = fatfs::FileSystem::new(&mut disk_efi, fatfs::FsOptions::new())?;
eprintln!("Creating EFI directory");
let root_dir = fs.root_dir();
root_dir.create_dir("EFI")?;
eprintln!("Creating EFI/BOOT directory");
let efi_dir = root_dir.open_dir("EFI")?;
efi_dir.create_dir("BOOT")?;
eprintln!(
"Writing EFI/BOOT/{} file with size {:#x}",
bootloader_efi_name,
disk_option.bootloader_efi.len()
);
let boot_dir = efi_dir.open_dir("BOOT")?;
let mut file = boot_dir.create_file(bootloader_efi_name)?;
file.truncate()?;
file.write_all(&disk_option.bootloader_efi)?;
}
// Format and install RedoxFS partition
eprintln!(
"Installing to RedoxFS partition with size {:#x}",
(redoxfs_end - redoxfs_start) * block_size
);
let disk_redoxfs = DiskIo(fscommon::StreamSlice::new(
disk_file,
redoxfs_start * block_size,
(redoxfs_end + 1) * block_size,
)?);
with_redoxfs(disk_redoxfs, disk_option.password_opt, callback)
}
#[cfg(not(target_os = "redox"))]
pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
_fs: &mut redoxfs::FileSystem<D>,
_progress: F,
) -> Result<bool> {
Ok(false)
}
/// Try fast install using live disk memory
#[cfg(target_os = "redox")]
pub fn try_fast_install<D: redoxfs::Disk, F: FnMut(u64, u64)>(
fs: &mut redoxfs::FileSystem<D>,
mut progress: F,
) -> Result<bool> {
use libredox::{call::MmapArgs, flag};
use std::os::fd::AsRawFd;
use syscall::PAGE_SIZE;
let phys = env::var("DISK_LIVE_ADDR")
.ok()
.and_then(|x| usize::from_str_radix(&x, 16).ok())
.unwrap_or(0);
let size = env::var("DISK_LIVE_SIZE")
.ok()
.and_then(|x| usize::from_str_radix(&x, 16).ok())
.unwrap_or(0);
if phys == 0 || size == 0 {
return Ok(false);
}
let start = (phys / PAGE_SIZE) * PAGE_SIZE;
let end = phys
.checked_add(size)
.context("phys + size overflow")?
.next_multiple_of(PAGE_SIZE);
let size = end - start;
let original = unsafe {
//TODO: unmap this memory
let file = fs::File::open("/scheme/memory/physical")?;
let base = libredox::call::mmap(MmapArgs {
fd: file.as_raw_fd() as usize,
addr: core::ptr::null_mut(),
offset: start as u64,
length: size,
prot: flag::PROT_READ,
flags: flag::MAP_SHARED,
})
.map_err(|err| anyhow::anyhow!("failed to mmap livedisk: {}", err))?;
std::slice::from_raw_parts(base as *const u8, size)
};
struct DiskLive {
original: &'static [u8],
}
impl redoxfs::Disk for DiskLive {
unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> syscall::Result<usize> {
let offset = (block * redoxfs::BLOCK_SIZE) as usize;
if offset + buffer.len() > self.original.len() {
return Err(syscall::Error::new(syscall::EINVAL));
}
buffer.copy_from_slice(&self.original[offset..offset + buffer.len()]);
Ok(buffer.len())
}
unsafe fn write_at(&mut self, _block: u64, _buffer: &[u8]) -> syscall::Result<usize> {
Err(syscall::Error::new(syscall::EINVAL))
}
fn size(&mut self) -> syscall::Result<u64> {
Ok(self.original.len() as u64)
}
}
let mut fs_old = redoxfs::FileSystem::open(DiskLive { original }, None, None, false)?;
let size_old = fs_old.header.size();
let free_old = fs_old.allocator().free() * redoxfs::BLOCK_SIZE;
let used_old = size_old - free_old;
redoxfs::clone(&mut fs_old, fs, move |used| {
progress(used, used_old);
})?;
Ok(true)
}
fn install_inner(config: Config, output: &Path) -> Result<()> {
println!("Installing to {}:\n{}", output.display(), config);
let cookbook = config.general.cookbook.clone();
let cookbook = cookbook.as_ref().map(|p| p.as_str());
if output.is_dir() {
install_dir(config, output, cookbook)
} else {
if !output.is_file() {
let fs_size = config.general.filesystem_size.unwrap_or(0) as u64;
// arbitrary size approximately fit just for initfs
if fs_size < 32 {
bail!("Refusing to create image disk less than 32 MB");
}
eprintln!(
"Creating a new file to {} with size {} MB",
output.display(),
fs_size
);
let file = fs::File::create(output)?;
file.set_len(fs_size * 1024 * 1024)?;
}
let live = config.general.live_disk.unwrap_or(false);
let password_opt = config.general.encrypt_disk.clone();
let password_opt = password_opt.as_ref().map(|p| p.as_bytes());
let (bootloader_bios, bootloader_efi) = fetch_bootloaders(&config, cookbook, live)?;
if let Some(write_bootloader) = &config.general.write_bootloader {
std::fs::write(write_bootloader, &bootloader_efi)?;
}
let disk_option = DiskOption {
bootloader_bios: &bootloader_bios,
bootloader_efi: &bootloader_efi,
password_opt: password_opt,
efi_partition_size: config.general.efi_partition_size,
skip_partitions: config.general.skip_partitions.unwrap_or(false),
};
with_whole_disk(output, &disk_option, move |fs| {
if config.general.no_mount.unwrap_or(false) {
with_redoxfs_ar(fs, None, move |mount_path| {
install_dir(config, mount_path, cookbook)
})
} else {
with_redoxfs_mount(fs, None, move |mount_path| {
install_dir(config, mount_path, cookbook)
})
}
})
}
}
/// Install RedoxFS into a new disk file, or a sysroot directory.
/// This function assumes all interactive prompts resolved by the caller.
pub fn install(config: Config, output: impl AsRef<Path>) -> Result<()> {
install_inner(config, output.as_ref())
}
pub use crate::format_bytes;
+42 -1134
View File
File diff suppressed because it is too large Load Diff
Executable
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
IMAGE=test.bin
QEMU_ARGS=(
-cpu max
-machine q35
-m 2048
-smp 4
-serial mon:stdio
-netdev user,id=net0
-device e1000,netdev=net0
)
if [ -e /dev/kvm ]
then
QEMU_ARGS+=(-accel kvm)
fi
set -ex
cargo build --release
rm -f "${IMAGE}"
fallocate -l 1GiB "${IMAGE}"
target/release/redox_installer -c res/test.toml "${IMAGE}"
qemu-system-x86_64 "${QEMU_ARGS[@]}" -drive "file=${IMAGE},format=raw"
+295
View File
@@ -0,0 +1,295 @@
//! Integration tests for the runtime file-collision tracker.
//!
//! These exercise the public API only (`CollisionTracker` and friends,
//! re-exported at the crate root). They live in `tests/` rather than inline
//! in `src/collision.rs` to keep the production module under the 250 pure
//! LOC ceiling and to enforce the public-API-only test contract — the
//! tracker's private helpers are not touched here, so a future internal
//! rewrite stays test-compatible as long as the public surface is stable.
use redox_installer::{
CollisionTracker, FileProvenance, Layer, ProvenanceSource, ERROR_MARKER, STRICT_ENV_VAR,
};
use std::path::PathBuf;
#[test]
fn test_no_collision_when_paths_distinct() {
// Given: an empty tracker and two distinct paths.
let mut tracker = CollisionTracker::new_with_strict(false);
let path_a = PathBuf::from("/output/etc/foo.conf");
let path_b = PathBuf::from("/output/etc/bar.conf");
// When: both paths are recorded from different layers.
tracker
.record(
path_a.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/foo.conf".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/etc/foo.conf"),
)
.unwrap();
tracker
.record(
path_b.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/bar.conf".to_string()),
layer: Layer::ConfigPostInstall,
},
Some("/etc/bar.conf"),
)
.unwrap();
// Then: no collisions, both paths tracked.
assert_eq!(tracker.collision_count(), 0, "distinct paths never collide");
assert_eq!(tracker.len(), 2);
assert!(tracker.collisions().is_empty());
}
#[test]
fn test_collision_warning_when_layer2_overwrites_layer1() {
// Given: a tracker with a Layer 1 (ConfigPreInstall) write to a path.
let mut tracker = CollisionTracker::new_with_strict(false);
let abs_path = PathBuf::from("/output/usr/lib/init.d/dbus");
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/usr/lib/init.d/dbus".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/usr/lib/init.d/dbus"),
)
.unwrap();
assert_eq!(tracker.collision_count(), 0);
// When: Layer 2 (PackageStaging) writes the same path.
let result = tracker.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/usr/lib/init.d/dbus"),
);
// Then: a collision is recorded, the call succeeds (warn-only mode),
// and the collision metadata points at the right layers.
assert!(result.is_ok(), "warn-only mode must not error");
assert_eq!(
tracker.collision_count(),
1,
"Layer 2 overwriting Layer 1 is a collision"
);
let collision = &tracker.collisions()[0];
assert_eq!(collision.path, abs_path);
assert_eq!(collision.previous.layer, Layer::ConfigPreInstall);
assert_eq!(collision.current.layer, Layer::PackageStaging);
}
#[test]
fn test_no_warning_for_postinstall_override() {
// Given: a tracker where Layer 2 (PackageStaging) has written a path.
let mut tracker = CollisionTracker::new_with_strict(false);
let abs_path = PathBuf::from("/output/etc/system.conf");
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/etc/system.conf"),
)
.unwrap();
assert_eq!(tracker.collision_count(), 0);
// When: Layer 3 (ConfigPostInstall) writes the same path — the
// documented intentional-override pattern.
let result = tracker.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/system.conf".to_string()),
layer: Layer::ConfigPostInstall,
},
Some("/etc/system.conf"),
);
// Then: no collision is recorded. Layer 3 is the override layer.
assert!(result.is_ok());
assert_eq!(
tracker.collision_count(),
0,
"Layer 3 postinstall overrides are intentional and must not warn"
);
assert!(tracker.collisions().is_empty());
}
#[test]
fn test_known_safe_override_paths_dont_warn() {
// Given: a tracker where Layer 2 (PackageStaging) has written the
// package-default init service at /usr/lib/init.d/dbus.
//
// In a real install the config override at /etc/init.d/dbus would
// resolve to a different abs_path and no collision would fire. The
// known-safe rule only matters when both writes land at the same
// abs_path — e.g. when /etc/init.d is a symlink to /usr/lib/init.d,
// or in unit tests that exercise the rule directly.
let mut tracker = CollisionTracker::new_with_strict(false);
let abs_path = PathBuf::from("/output/etc/init.d/dbus");
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/usr/lib/init.d/dbus"),
)
.unwrap();
assert_eq!(tracker.collision_count(), 0);
// When: a config write (Layer 1 — not Layer 3, so the layer-3
// suppression rule does not apply) targets /etc/init.d/dbus, which
// resolves to the same abs_path but matches the known-safe override
// pair (/etc/init.d over /usr/lib/init.d).
let result = tracker.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/init.d/dbus".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/etc/init.d/dbus"),
);
// Then: no collision — the known-safe override pair suppresses the
// warning, even though the layers differ and neither is Layer 3.
assert!(result.is_ok());
assert_eq!(
tracker.collision_count(),
0,
"known-safe override pair (/etc/init.d over /usr/lib/init.d) must not warn"
);
// Sanity: the opposite direction (default after override) is NOT
// suppressed — that would be a package clobbering a config override,
// which is the D-Bus regression class.
let result2 = tracker.record(
abs_path,
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/usr/lib/init.d/dbus-other-direction"),
);
assert!(result2.is_ok());
assert_eq!(
tracker.collision_count(),
1,
"default-after-override direction is NOT a known-safe override"
);
}
#[test]
fn test_strict_mode_promotes_collision_to_error() {
// Given: a strict-mode tracker with a Layer 1 write.
let mut tracker = CollisionTracker::new_with_strict(true);
let abs_path = PathBuf::from("/output/etc/dbus.conf");
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/etc/dbus.conf".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/etc/dbus.conf"),
)
.unwrap();
// When: Layer 2 writes the same path.
let result = tracker.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::Package("base".to_string()),
layer: Layer::PackageStaging,
},
Some("/etc/dbus.conf"),
);
// Then: the call returns Err and the error message references the
// strict env var so the operator knows how to demote if needed.
assert!(
result.is_err(),
"strict mode must promote collisions to errors"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains(ERROR_MARKER),
"strict-mode error must carry the {ERROR_MARKER} marker for log grepping"
);
assert!(
err.contains(STRICT_ENV_VAR),
"strict-mode error must name the env var so the operator can demote"
);
assert!(
err.contains("layer 1"),
"strict-mode error must name the overwritten layer"
);
assert!(
err.contains("layer 2"),
"strict-mode error must name the overwriting layer"
);
// And: the collision is still counted in the tracker's history.
assert_eq!(tracker.collision_count(), 1);
}
#[test]
fn test_scan_package_staging_detects_layer2_over_layer1_on_real_disk() {
// End-to-end: write a Layer 1 file to a tempdir, then ask
// scan_package_staging to walk it. The walk must detect the file as a
// Layer 2 write that collides with the already-recorded Layer 1 entry.
//
// This is the closest unit-level proof of the D-Bus regression class:
// a config wrote to a path, then a package stages the same path, and
// the walk catches it.
let dir = std::env::temp_dir().join(format!(
"redbear_collision_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0),
));
std::fs::create_dir_all(&dir).unwrap();
// Layer 1: a config writes /usr/lib/init.d/dbus.
let rel = "usr/lib/init.d/dbus";
let abs_path = dir.join(rel);
std::fs::create_dir_all(abs_path.parent().unwrap()).unwrap();
std::fs::write(&abs_path, b"# config layer 1").unwrap();
let mut tracker = CollisionTracker::new_with_strict(false);
tracker
.record(
abs_path.clone(),
FileProvenance {
source: ProvenanceSource::ConfigFile("/usr/lib/init.d/dbus".to_string()),
layer: Layer::ConfigPreInstall,
},
Some("/usr/lib/init.d/dbus"),
)
.unwrap();
assert_eq!(tracker.collision_count(), 0);
// Layer 2: the walk sees the same file on disk and flags the collision.
tracker.scan_package_staging(&dir).unwrap();
assert_eq!(
tracker.collision_count(),
1,
"scan_package_staging must catch a Layer 2 write to a Layer 1 path"
);
let _ = std::fs::remove_dir_all(&dir);
}