Implement system state management with TOML serialization

- Introduced `SystemState` struct to manage system state, including stage, target, architecture, and layers.
- Added functions to load and save system state from/to a TOML file.
- Implemented layer management functions: add, set, and remove packages from layers.
- Created initialization function for LBI layout, including directory and symlink creation.
- Added tests for layer management and LBI layout initialization.
This commit is contained in:
2026-05-20 19:54:00 -05:00
parent d61d59f4f5
commit 1833a2c42d
18 changed files with 8820 additions and 354 deletions
Generated
+383 -64
View File
@@ -8,6 +8,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "adobe-cmap-parser"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
dependencies = [
"pom",
]
[[package]] [[package]]
name = "aes" name = "aes"
version = "0.8.4" version = "0.8.4"
@@ -15,10 +24,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cipher", "cipher 0.4.4",
"cpufeatures 0.2.17", "cpufeatures 0.2.17",
] ]
[[package]]
name = "aes"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8"
dependencies = [
"cipher 0.5.1",
"cpubits",
"cpufeatures 0.3.0",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@@ -175,6 +195,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
dependencies = [ dependencies = [
"hybrid-array", "hybrid-array",
"zeroize",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
] ]
[[package]] [[package]]
@@ -183,6 +213,12 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytecount"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.1" version = "1.11.1"
@@ -198,6 +234,15 @@ dependencies = [
"libbz2-rs-sys", "libbz2-rs-sys",
] ]
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher 0.4.4",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.56" version = "1.2.56"
@@ -210,6 +255,12 @@ dependencies = [
"shlex", "shlex",
] ]
[[package]]
name = "cff-parser"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f5b6e9141c036f3ff4ce7b2f7e432b0f00dee416ddcd4f17741d189ddc2e9d"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@@ -240,14 +291,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [ dependencies = [
"crypto-common 0.1.7", "crypto-common 0.1.7",
"inout", "inout 0.1.4",
]
[[package]]
name = "cipher"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea"
dependencies = [
"crypto-common 0.2.1",
"inout 0.2.2",
] ]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.6.0" version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -267,18 +328,18 @@ dependencies = [
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "4.6.0" version = "4.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
dependencies = [ dependencies = [
"clap", "clap",
] ]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.6.0" version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -302,6 +363,12 @@ dependencies = [
"roff", "roff",
] ]
[[package]]
name = "cmov"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.4" version = "1.0.4"
@@ -364,6 +431,12 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpubits"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@@ -443,6 +516,15 @@ version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
[[package]]
name = "ctutils"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
dependencies = [
"cmov",
]
[[package]] [[package]]
name = "deflate64" name = "deflate64"
version = "0.1.11" version = "0.1.11"
@@ -471,13 +553,14 @@ dependencies = [
"md5", "md5",
"minisign", "minisign",
"nix", "nix",
"pdf-extract",
"petgraph", "petgraph",
"reqwest", "reqwest",
"rusqlite", "rusqlite",
"semver", "semver",
"serde", "serde",
"serde_ignored", "serde_ignored",
"sha1 0.11.0", "sha1",
"sha2 0.11.0", "sha2 0.11.0",
"signal-hook 0.4.4", "signal-hook 0.4.4",
"suppaftp", "suppaftp",
@@ -546,6 +629,8 @@ dependencies = [
"block-buffer 0.12.0", "block-buffer 0.12.0",
"const-oid", "const-oid",
"crypto-common 0.2.1", "crypto-common 0.2.1",
"ctutils",
"zeroize",
] ]
[[package]] [[package]]
@@ -574,6 +659,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ecb"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7"
dependencies = [
"cipher 0.4.4",
]
[[package]] [[package]]
name = "encode_unicode" name = "encode_unicode"
version = "1.0.0" version = "1.0.0"
@@ -605,6 +699,15 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "euclid"
version = "0.20.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "fallible-iterator" name = "fallible-iterator"
version = "0.3.0" version = "0.3.0"
@@ -646,13 +749,12 @@ dependencies = [
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.27" version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"libredox",
] ]
[[package]] [[package]]
@@ -885,6 +987,15 @@ dependencies = [
"digest 0.10.7", "digest 0.10.7",
] ]
[[package]]
name = "hmac"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
dependencies = [
"digest 0.11.2",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.4.0" version = "1.4.0"
@@ -1156,9 +1267,19 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [ dependencies = [
"block-padding",
"generic-array", "generic-array",
] ]
[[package]]
name = "inout"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
dependencies = [
"hybrid-array",
]
[[package]] [[package]]
name = "inquire" name = "inquire"
version = "0.9.4" version = "0.9.4"
@@ -1258,9 +1379,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.182" version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]] [[package]]
name = "libgit2-sys" name = "libgit2-sys"
@@ -1276,17 +1397,6 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "libredox"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
"bitflags",
"libc",
"redox_syscall 0.7.3",
]
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.37.0" version = "0.37.0"
@@ -1357,10 +1467,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lz4_flex" name = "lopdf"
version = "0.13.0" version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" checksum = "c7184fdea2bc3cd272a1acec4030c321a8f9875e877b3f92a53f2f6033fdc289"
dependencies = [
"aes 0.8.4",
"bitflags",
"cbc",
"ecb",
"encoding_rs",
"flate2",
"getrandom 0.3.4",
"indexmap",
"itoa",
"log",
"md-5",
"nom",
"nom_locate",
"rand",
"rangemap",
"sha2 0.10.9",
"stringprep",
"thiserror 2.0.18",
"ttf-parser",
"weezl",
]
[[package]]
name = "lz4_flex"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e"
dependencies = [ dependencies = [
"twox-hash", "twox-hash",
] ]
@@ -1385,6 +1523,16 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest 0.10.7",
]
[[package]] [[package]]
name = "md5" name = "md5"
version = "0.8.0" version = "0.8.0"
@@ -1450,9 +1598,9 @@ dependencies = [
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.31.2" version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@@ -1466,6 +1614,26 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "nom_locate"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
dependencies = [
"bytecount",
"memchr",
"nom",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.0" version = "0.2.0"
@@ -1561,7 +1729,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.5.18", "redox_syscall",
"smallvec", "smallvec",
"windows-link", "windows-link",
] ]
@@ -1573,7 +1741,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [ dependencies = [
"digest 0.10.7", "digest 0.10.7",
"hmac", "hmac 0.12.1",
]
[[package]]
name = "pbkdf2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
dependencies = [
"digest 0.11.2",
"hmac 0.13.0",
]
[[package]]
name = "pdf-extract"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28ba1758a3d3f361459645780e09570b573fc3c82637449e9963174c813a98"
dependencies = [
"adobe-cmap-parser",
"cff-parser",
"encoding_rs",
"euclid",
"log",
"lopdf",
"postscript",
"type1-encoding-parser",
"unicode-normalization",
] ]
[[package]] [[package]]
@@ -1612,12 +1807,24 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "pom"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6"
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.13.1" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "postscript"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.4" version = "0.1.4"
@@ -1639,6 +1846,15 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@@ -1673,6 +1889,41 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rangemap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -1682,15 +1933,6 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "redox_syscall"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.3" version = "1.12.3"
@@ -1722,9 +1964,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@@ -1850,7 +2092,7 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [ dependencies = [
"cipher", "cipher 0.4.4",
] ]
[[package]] [[package]]
@@ -1883,7 +2125,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [ dependencies = [
"pbkdf2", "pbkdf2 0.12.2",
"salsa20", "salsa20",
"sha2 0.10.9", "sha2 0.10.9",
] ]
@@ -1979,17 +2221,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.11.0" version = "0.11.0"
@@ -2127,6 +2358,17 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@@ -2141,9 +2383,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "suppaftp" name = "suppaftp"
version = "8.0.2" version = "8.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d3da253d7e9993de86df41eb89e8cb1b6f567abe215798645651fca4148d0aa" checksum = "4275c142b5be3af2eeadd70dd368caf3b65546c8af1035839372dd7a1436127d"
dependencies = [ dependencies = [
"chrono", "chrono",
"futures-lite", "futures-lite",
@@ -2311,6 +2553,21 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.49.0" version = "1.49.0"
@@ -2456,12 +2713,27 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]] [[package]]
name = "twox-hash" name = "twox-hash"
version = "2.1.2" version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
[[package]]
name = "type1-encoding-parser"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa10c302f5a53b7ad27fd42a3996e23d096ba39b5b8dd6d9e683a05b01bee749"
dependencies = [
"pom",
]
[[package]] [[package]]
name = "typed-path" name = "typed-path"
version = "0.12.3" version = "0.12.3"
@@ -2474,12 +2746,33 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-normalization"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.12.0" version = "1.12.0"
@@ -2696,6 +2989,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "weezl"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -3104,6 +3403,26 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.6" version = "0.1.6"
@@ -3166,24 +3485,24 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "8.5.0" version = "8.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2726508a48f38dceb22b35ecbbd2430efe34ff05c62bd3285f965d7911b33464" checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
dependencies = [ dependencies = [
"aes", "aes 0.9.0",
"bzip2", "bzip2",
"constant_time_eq 0.4.2", "constant_time_eq 0.4.2",
"crc32fast", "crc32fast",
"deflate64", "deflate64",
"flate2", "flate2",
"getrandom 0.4.1", "getrandom 0.4.1",
"hmac", "hmac 0.13.0",
"indexmap", "indexmap",
"lzma-rust2", "lzma-rust2",
"memchr", "memchr",
"pbkdf2", "pbkdf2 0.13.0",
"ppmd-rust", "ppmd-rust",
"sha1 0.10.6", "sha1",
"time", "time",
"typed-path", "typed-path",
"zeroize", "zeroize",
+5 -4
View File
@@ -14,7 +14,7 @@ clap = { version = "4.6.0", features = ["derive"] }
flate2 = "1.1.9" flate2 = "1.1.9"
git2 = "0.20.4" git2 = "0.20.4"
indicatif = "0.18.4" indicatif = "0.18.4"
nix = { version = "0.31.2", features = ["user"] } nix = { version = "0.31.3", features = ["user"] }
rusqlite = "0.39.0" rusqlite = "0.39.0"
semver = "1.0.28" semver = "1.0.28"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
@@ -36,17 +36,18 @@ minisign = "0.9.1"
petgraph = "0.8.3" petgraph = "0.8.3"
fd-lock = "4.0.4" fd-lock = "4.0.4"
reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "native-tls"] } reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "native-tls"] }
filetime = "0.2.27" filetime = "0.2.29"
clap_complete = "4.6.0" clap_complete = "4.6.5"
clap_mangen = "0.3.0" clap_mangen = "0.3.0"
sys-mount = { version = "3.1.0", default-features = false } sys-mount = { version = "3.1.0", default-features = false }
time = { version = "0.3.47", features = ["formatting", "parsing"] } time = { version = "0.3.47", features = ["formatting", "parsing"] }
b2sum-rust = "0.3.0" b2sum-rust = "0.3.0"
serde_ignored = "0.1.14" serde_ignored = "0.1.14"
lz4_flex = "0.13.0" lz4_flex = "0.13.1"
lzma-rust2 = "0.16.2" lzma-rust2 = "0.16.2"
signal-hook = "0.4.4" signal-hook = "0.4.4"
sha1 = "0.11.0" sha1 = "0.11.0"
pdf-extract = "0.10.0"
[dev-dependencies] [dev-dependencies]
tempfile = "=3.27.0" tempfile = "=3.27.0"
+6700
View File
File diff suppressed because it is too large Load Diff
+45 -6
View File
@@ -54,14 +54,12 @@ pub fn build(
crate::builder::apply_build_helper_context_env(&mut env_vars, spec)?; crate::builder::apply_build_helper_context_env(&mut env_vars, spec)?;
crate::builder::apply_build_helper_dirs_env(&mut env_vars, Some(src_dir), Some(&build_dir)); crate::builder::apply_build_helper_dirs_env(&mut env_vars, Some(src_dir), Some(&build_dir));
// For custom builds, look for a build.sh script in the source directory // For custom builds, the spec's build.sh is authoritative when present.
// Upstream source archives sometimes ship unrelated helper scripts with the
// same name, and the package spec needs to override them deterministically.
let build_script = src_dir.join("build.sh"); let build_script = src_dir.join("build.sh");
// If the extracted source doesn't include build.sh but the spec directory does,
// copy it into the source dir (this makes `depot install <local-spec>` behave
// like the spec's build.sh being part of the package when appropriate).
let spec_build = spec.spec_dir.join("build.sh"); let spec_build = spec.spec_dir.join("build.sh");
if !build_script.exists() && spec_build.exists() { if spec_build.exists() {
fs::create_dir_all(src_dir)?; fs::create_dir_all(src_dir)?;
fs::copy(&spec_build, &build_script).with_context(|| { fs::copy(&spec_build, &build_script).with_context(|| {
format!( format!(
@@ -477,6 +475,47 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn test_build_prefers_spec_build_sh_over_source_build_sh() -> Result<()> {
let tmp_src = tempdir()?;
let tmp_dest = tempdir()?;
let spec_dir = tempdir()?;
let source_build_sh = tmp_src.path().join("build.sh");
std::fs::write(&source_build_sh, "#!/bin/sh\nexit 77\n")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&source_build_sh)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&source_build_sh, perms)?;
}
let spec_build_sh = spec_dir.path().join("build.sh");
std::fs::write(
&spec_build_sh,
"#!/bin/sh\nprintf 'from spec\\n' > \"$DESTDIR/spec-won\"\n",
)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&spec_build_sh)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&spec_build_sh, perms)?;
}
let mut spec = mk_spec("custom-prefer-spec", "1.0");
spec.spec_dir = spec_dir.path().to_path_buf();
build(&spec, tmp_src.path(), tmp_dest.path(), None, true, None)?;
assert_eq!(
std::fs::read_to_string(tmp_dest.path().join("spec-won"))?,
"from spec\n"
);
Ok(())
}
#[test] #[test]
fn test_build_function_mode_uses_per_output_destdirs() -> Result<()> { fn test_build_function_mode_uses_per_output_destdirs() -> Result<()> {
let tmp_src = tempdir()?; let tmp_src = tempdir()?;
+105
View File
@@ -144,6 +144,30 @@ pub struct BuildArgs {
pub cleanup_deps: bool, pub cleanup_deps: bool,
} }
/// Arguments for importing the Linux by Intent book package plan into Depot layers.
#[derive(Debug, Clone, Args)]
pub struct BootstrapArgs {
/// Target sysroot whose Depot system state should receive the parsed layers
#[arg(value_name = "SYSROOT")]
pub sysroot: PathBuf,
/// Target triple used for cross and staged bootstrap builds
#[arg(long)]
pub target: Option<String>,
/// Target architecture component used by build defaults
#[arg(long)]
pub arch: Option<String>,
/// URL of the Linux by Intent PDF to parse
#[arg(long, default_value = "https://www.vertexlinux.net/lbi/book.pdf")]
pub book_url: String,
/// Use a local PDF instead of fetching book-url
#[arg(long, value_name = "PDF")]
pub book_pdf: Option<PathBuf>,
}
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct UpdateArgs { pub struct UpdateArgs {
#[command(flatten)] #[command(flatten)]
@@ -226,6 +250,70 @@ pub struct ConfigArgs {
pub rootfs_args: RootfsArgs, pub rootfs_args: RootfsArgs,
} }
/// Arguments for Depot system-build state management commands.
#[derive(Debug, Clone, Args)]
pub struct SystemArgs {
/// Root filesystem whose system state should be inspected or modified.
#[command(flatten)]
pub rootfs_args: RootfsArgs,
/// Requested system state subcommand.
#[command(subcommand)]
pub command: SystemCommands,
}
/// System-build state and Linux by Intent layout commands.
#[derive(Subcommand, Debug, Clone)]
pub enum SystemCommands {
/// Show tracked system build stage and package layers
Status,
/// Move the tracked system status to a build stage
Stage {
/// New stage name, such as cross-tools, minimal, chroot, stage2, or bootable
stage: String,
},
/// Manage package layer membership
Layer {
#[command(subcommand)]
command: SystemLayerCommands,
},
/// Initialize the Linux by Intent /system layout and Depot build defaults
InitLbi {
/// Target triple used for cross and staged builds
#[arg(long, default_value = "x86_64-unknown-linux-musl")]
target: String,
/// Target architecture component used by build defaults
#[arg(long)]
arch: Option<String>,
/// Replace an existing Depot build config generated for the target rootfs
#[arg(long)]
force: bool,
},
}
/// Package layer membership commands.
#[derive(Subcommand, Debug, Clone)]
pub enum SystemLayerCommands {
/// Add packages to a named layer
Add {
/// Layer name, such as base, devel, toolchain, or boot
layer: String,
/// Package names to add to the layer
#[arg(required = true, num_args = 1..)]
packages: Vec<String>,
},
/// Remove packages from a named layer
Remove {
/// Layer name
layer: String,
/// Package names to remove from the layer
#[arg(required = true, num_args = 1..)]
packages: Vec<String>,
},
/// List all tracked layers
List,
}
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct GenerateArtifactsArgs { pub struct GenerateArtifactsArgs {
/// Output directory for generated files /// Output directory for generated files
@@ -265,6 +353,8 @@ pub enum Commands {
Remove(RemoveArgs), Remove(RemoveArgs),
/// Build a package without installing /// Build a package without installing
Build(BuildArgs), Build(BuildArgs),
/// Parse the Linux by Intent book and populate temp/base/devel layers
Bootstrap(BootstrapArgs),
/// Update installed packages from configured repositories /// Update installed packages from configured repositories
Update(UpdateArgs), Update(UpdateArgs),
/// Scan package specs for upstream version updates /// Scan package specs for upstream version updates
@@ -283,6 +373,8 @@ pub enum Commands {
Repo(RepoArgs), Repo(RepoArgs),
/// Show current configuration /// Show current configuration
Config(ConfigArgs), Config(ConfigArgs),
/// Manage system build stage, package layers, and book-style layout state
System(SystemArgs),
/// Generate shell completion scripts and a man page into an output directory. /// Generate shell completion scripts and a man page into an output directory.
#[command(hide = true)] #[command(hide = true)]
GenerateArtifacts(GenerateArtifactsArgs), GenerateArtifacts(GenerateArtifactsArgs),
@@ -317,6 +409,19 @@ pub enum InternalCommands {
#[command(hide = true)] #[command(hide = true)]
Clone { repo: String, dest: Option<PathBuf> }, Clone { repo: String, dest: Option<PathBuf> },
#[command(hide = true)] #[command(hide = true)]
BootstrapChroot {
#[arg(long)]
rootfs: PathBuf,
#[arg(long)]
sources: PathBuf,
#[arg(long)]
destdir: PathBuf,
#[arg(long)]
workdir: String,
#[arg(long)]
script: String,
},
#[command(hide = true)]
AutotoolsConfigure { AutotoolsConfigure {
#[arg(value_name = "ARG", num_args = 0.., allow_hyphen_values = true)] #[arg(value_name = "ARG", num_args = 0.., allow_hyphen_values = true)]
args: Vec<String>, args: Vec<String>,
+43 -9
View File
@@ -1,10 +1,11 @@
use crate::cli::{ use crate::cli::{
BuildArgs, Cli, Commands, ConfigArgs, ConvertArgs, InfoArgs, InstallArgs, InternalCommands, BuildArgs, Cli, Commands, ConfigArgs, ConvertArgs, InfoArgs, InstallArgs, InternalCommands,
ListArgs, OwnsArgs, RemoveArgs, RepoCommands, RepoKindArg, SearchArgs, SignArgs, UpdateArgs, ListArgs, OwnsArgs, RemoveArgs, RepoCommands, RepoKindArg, SearchArgs, SignArgs, SystemArgs,
UpdateArgs,
}; };
use crate::{ use crate::{
builder, cli_assets, config, cross, db, deps, index, install, locking, package, planner, bootstrap, builder, cli_assets, config, cross, db, deps, index, install, locking, package,
signing, source, staging, ui, planner, signing, source, staging, ui,
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use git2::Direction; use git2::Direction;
@@ -19,10 +20,11 @@ use url::Url;
use walkdir::WalkDir; use walkdir::WalkDir;
use build_cmd::support::{ use build_cmd::support::{
automatic_tests_disabled_for_outputs, build_lib32_companion_package, clean_build_workspace, automatic_tests_disabled_for_outputs, build_lib32_companion_package, clean_build_source_dirs,
effective_lib32_only, ensure_requested_development_package_installed, make_lib32_package_spec, clean_build_workspace, effective_lib32_only, ensure_requested_development_package_installed,
maybe_disable_tests_for_missing_deps, maybe_prompt_to_skip_tests_for_missing_requested_deps, make_lib32_package_spec, maybe_disable_tests_for_missing_deps,
merge_missing_dependencies, requested_outputs, should_install_test_deps, maybe_prompt_to_skip_tests_for_missing_requested_deps, merge_missing_dependencies,
requested_outputs, should_install_test_deps,
}; };
use install_cmd::archive::{ use install_cmd::archive::{
extract_package_archive_to_staging, load_package_archive_into_staging, extract_package_archive_to_staging, load_package_archive_into_staging,
@@ -89,6 +91,7 @@ fn command_rootfs(command: &Commands) -> Option<&Path> {
Commands::Install(args) => Some(&args.rootfs_args.rootfs), Commands::Install(args) => Some(&args.rootfs_args.rootfs),
Commands::Remove(args) => Some(&args.rootfs_args.rootfs), Commands::Remove(args) => Some(&args.rootfs_args.rootfs),
Commands::Build(args) => Some(&args.rootfs_args.rootfs), Commands::Build(args) => Some(&args.rootfs_args.rootfs),
Commands::Bootstrap(args) => Some(&args.sysroot),
Commands::Update(args) => Some(&args.rootfs_args.rootfs), Commands::Update(args) => Some(&args.rootfs_args.rootfs),
Commands::Info(args) => Some(&args.rootfs_args.rootfs), Commands::Info(args) => Some(&args.rootfs_args.rootfs),
Commands::Search(args) => Some(&args.rootfs_args.rootfs), Commands::Search(args) => Some(&args.rootfs_args.rootfs),
@@ -97,6 +100,7 @@ fn command_rootfs(command: &Commands) -> Option<&Path> {
Commands::Sign(args) => Some(&args.rootfs_args.rootfs), Commands::Sign(args) => Some(&args.rootfs_args.rootfs),
Commands::Repo(args) => Some(repo_command_rootfs(&args.command)), Commands::Repo(args) => Some(repo_command_rootfs(&args.command)),
Commands::Config(args) => Some(&args.rootfs_args.rootfs), Commands::Config(args) => Some(&args.rootfs_args.rootfs),
Commands::System(args) => Some(&args.rootfs_args.rootfs),
Commands::Check(_) Commands::Check(_)
| Commands::Convert(_) | Commands::Convert(_)
| Commands::GenerateArtifacts(_) | Commands::GenerateArtifacts(_)
@@ -110,6 +114,7 @@ fn command_assume_yes(command: &Commands) -> bool {
Commands::Install(args) => args.prompt_args.yes, Commands::Install(args) => args.prompt_args.yes,
Commands::Remove(args) => args.prompt_args.yes, Commands::Remove(args) => args.prompt_args.yes,
Commands::Build(args) => args.prompt_args.yes, Commands::Build(args) => args.prompt_args.yes,
Commands::Bootstrap(_) => false,
Commands::Update(args) => args.prompt_args.yes, Commands::Update(args) => args.prompt_args.yes,
Commands::Check(_) Commands::Check(_)
| Commands::Convert(_) | Commands::Convert(_)
@@ -120,6 +125,7 @@ fn command_assume_yes(command: &Commands) -> bool {
| Commands::Sign(_) | Commands::Sign(_)
| Commands::Repo(_) | Commands::Repo(_)
| Commands::Config(_) | Commands::Config(_)
| Commands::System(_)
| Commands::GenerateArtifacts(_) | Commands::GenerateArtifacts(_)
| Commands::MakeSpec(_) | Commands::MakeSpec(_)
| Commands::Internal(_) => false, | Commands::Internal(_) => false,
@@ -913,7 +919,7 @@ fn collect_installed_replacement_packages(
Ok(replacements) Ok(replacements)
} }
fn remove_installed_package_with_hooks( pub(crate) fn remove_installed_package_with_hooks(
package: &str, package: &str,
rootfs: &Path, rootfs: &Path,
config: &config::Config, config: &config::Config,
@@ -2054,6 +2060,27 @@ fn run_direct_archive_install_requests(
Ok(true) Ok(true)
} }
struct SourceBuildCleanupGuard<'a> {
config: &'a config::Config,
enabled: bool,
}
impl<'a> SourceBuildCleanupGuard<'a> {
fn new(config: &'a config::Config, enabled: bool) -> Self {
Self { config, enabled }
}
}
impl Drop for SourceBuildCleanupGuard<'_> {
fn drop(&mut self) {
if self.enabled
&& let Err(err) = clean_build_source_dirs(self.config)
{
crate::log_warn!("Failed to clean build source dirs: {}", err);
}
}
}
fn run_direct_install_request( fn run_direct_install_request(
options: DirectInstallOptions<'_>, options: DirectInstallOptions<'_>,
config: &config::Config, config: &config::Config,
@@ -2142,6 +2169,8 @@ fn run_direct_install_request(
pkg_spec.apply_config(config); pkg_spec.apply_config(config);
(pkg_spec, None) (pkg_spec, None)
}; };
let built_from_source = staging_dir.is_none();
let _source_cleanup_guard = SourceBuildCleanupGuard::new(config, built_from_source);
if options.lib32_only && staging_dir.is_some() { if options.lib32_only && staging_dir.is_some() {
anyhow::bail!("--lib32-only is only supported when installing from a package spec"); anyhow::bail!("--lib32-only is only supported when installing from a package spec");
@@ -2438,6 +2467,7 @@ fn run_direct_install_request(
dir.path().to_path_buf() dir.path().to_path_buf()
} else { } else {
// 1-2. Fetch + extract sources (supports archives and git URL#rev) // 1-2. Fetch + extract sources (supports archives and git URL#rev)
clean_build_source_dirs(config)?;
source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?; source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?;
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?; let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
built_src_dir = Some(src_dir.clone()); built_src_dir = Some(src_dir.clone());
@@ -2551,7 +2581,8 @@ pub fn run(cli: Cli) -> Result<()> {
Commands::Install(args) => args.build_exec_args.test_deps, Commands::Install(args) => args.build_exec_args.test_deps,
Commands::Build(args) => args.build_exec_args.test_deps, Commands::Build(args) => args.build_exec_args.test_deps,
Commands::Update(args) => args.build_exec_args.test_deps, Commands::Update(args) => args.build_exec_args.test_deps,
Commands::Check(_) Commands::Bootstrap(_)
| Commands::Check(_)
| Commands::Remove(_) | Commands::Remove(_)
| Commands::Info(_) | Commands::Info(_)
| Commands::Search(_) | Commands::Search(_)
@@ -2560,6 +2591,7 @@ pub fn run(cli: Cli) -> Result<()> {
| Commands::Sign(_) | Commands::Sign(_)
| Commands::Repo(_) | Commands::Repo(_)
| Commands::Config(_) | Commands::Config(_)
| Commands::System(_)
| Commands::GenerateArtifacts(_) | Commands::GenerateArtifacts(_)
| Commands::Convert(_) | Commands::Convert(_)
| Commands::MakeSpec(_) | Commands::MakeSpec(_)
@@ -2570,6 +2602,7 @@ pub fn run(cli: Cli) -> Result<()> {
Commands::Install(args) => install_cmd::run_install(args, cli_test_deps)?, Commands::Install(args) => install_cmd::run_install(args, cli_test_deps)?,
Commands::Remove(args) => install_cmd::run_remove(args)?, Commands::Remove(args) => install_cmd::run_remove(args)?,
Commands::Build(args) => build_cmd::run_build(args, cli_test_deps)?, Commands::Build(args) => build_cmd::run_build(args, cli_test_deps)?,
Commands::Bootstrap(args) => bootstrap::run(args)?,
Commands::Update(args) => update::run_update(args, cli_test_deps)?, Commands::Update(args) => update::run_update(args, cli_test_deps)?,
Commands::Check(args) => check::run_check(args)?, Commands::Check(args) => check::run_check(args)?,
Commands::Info(args) => misc::run_info(args)?, Commands::Info(args) => misc::run_info(args)?,
@@ -2580,6 +2613,7 @@ pub fn run(cli: Cli) -> Result<()> {
Commands::Repo(args) => repo::run_repo(args.command)?, Commands::Repo(args) => repo::run_repo(args.command)?,
Commands::GenerateArtifacts(args) => misc::run_generate_artifacts(args)?, Commands::GenerateArtifacts(args) => misc::run_generate_artifacts(args)?,
Commands::Config(args) => misc::run_config(args)?, Commands::Config(args) => misc::run_config(args)?,
Commands::System(args) => misc::run_system(args)?,
Commands::MakeSpec(args) => misc::run_make_spec(args)?, Commands::MakeSpec(args) => misc::run_make_spec(args)?,
Commands::Convert(args) => misc::run_convert(args)?, Commands::Convert(args) => misc::run_convert(args)?,
Commands::Internal(args) => misc::run_internal(args)?, Commands::Internal(args) => misc::run_internal(args)?,
+13 -5
View File
@@ -4,11 +4,11 @@ pub(crate) mod support;
use self::support::{ use self::support::{
RequestedBuildToolPackageInstall, automatic_tests_disabled_for_outputs, RequestedBuildToolPackageInstall, automatic_tests_disabled_for_outputs,
build_lib32_companion_package, clean_build_workspace, effective_lib32_only, build_lib32_companion_package, clean_build_source_dirs, clean_build_workspace,
ensure_requested_build_tool_package_installed, ensure_requested_development_package_installed, effective_lib32_only, ensure_requested_build_tool_package_installed,
maybe_disable_tests_for_missing_deps, maybe_prompt_to_skip_tests_for_missing_requested_deps, ensure_requested_development_package_installed, maybe_disable_tests_for_missing_deps,
merge_missing_dependencies, requested_outputs, should_install_test_deps, maybe_prompt_to_skip_tests_for_missing_requested_deps, merge_missing_dependencies,
warn_if_running_as_root_for_build, requested_outputs, should_install_test_deps, warn_if_running_as_root_for_build,
}; };
pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> { pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
@@ -303,6 +303,7 @@ pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
watcher.check()?; watcher.check()?;
} }
clean_build_source_dirs(&config)?;
source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?; source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?;
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?; let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
if let Some(watcher) = interrupt_watcher.as_ref() { if let Some(watcher) = interrupt_watcher.as_ref() {
@@ -507,6 +508,7 @@ pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
false, false,
)?; )?;
} }
clean_build_source_dirs(&config)?;
if clean { if clean {
clean_build_workspace(&config)?; clean_build_workspace(&config)?;
} }
@@ -529,6 +531,12 @@ pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
)); ));
} }
} }
if let Err(clean_err) = clean_build_source_dirs(&config) {
ui::warn(format!(
"Failed to clean build source dirs after failed build: {}",
clean_err
));
}
if interrupted { if interrupted {
anyhow::bail!("Build interrupted by Ctrl-C"); anyhow::bail!("Build interrupted by Ctrl-C");
} }
+16
View File
@@ -115,6 +115,22 @@ pub(crate) fn clean_build_workspace(config: &config::Config) -> Result<()> {
Ok(()) Ok(())
} }
pub(crate) fn clean_build_source_dirs(config: &config::Config) -> Result<()> {
if config.build_dir.exists() {
fs::remove_dir_all(&config.build_dir).with_context(|| {
format!(
"Failed to clean build source dirs: {}",
config.build_dir.display()
)
})?;
ui::success(format!(
"Cleaned build source dirs: {}",
config.build_dir.display()
));
}
Ok(())
}
pub(crate) fn warn_if_running_as_root_for_build(command: &str, rootfs: &Path) { pub(crate) fn warn_if_running_as_root_for_build(command: &str, rootfs: &Path) {
if crate::fakeroot::is_root() { if crate::fakeroot::is_root() {
ui::warn(format!("Running '{}' as root is discouraged.", command)); ui::warn(format!("Running '{}' as root is discouraged.", command));
+111
View File
@@ -112,6 +112,117 @@ pub(super) fn run_config(args: ConfigArgs) -> Result<()> {
Ok(()) Ok(())
} }
pub(super) fn run_system(args: SystemArgs) -> Result<()> {
let SystemArgs {
rootfs_args,
command,
} = args;
let rootfs = rootfs_args.rootfs;
let config = config::Config::for_rootfs(&rootfs);
let mut system_lock = locking::open_lock(&config)?;
let system_lock_path = locking::lock_path(&config);
match command {
crate::cli::SystemCommands::Status => {
let _guard = locking::try_read(&system_lock, &system_lock_path, "system status")?;
let state = crate::system_state::load(&config)?;
print_system_state(&state);
}
crate::cli::SystemCommands::Stage { stage } => {
let _guard = locking::try_write(&mut system_lock, &system_lock_path, "system stage")?;
let state = crate::system_state::set_stage(&config, stage)?;
ui::success(format!(
"Moved system status to stage {}",
state.stage.as_deref().unwrap_or("unknown")
));
}
crate::cli::SystemCommands::Layer { command } => {
let _guard = locking::try_write(&mut system_lock, &system_lock_path, "system layer")?;
match command {
crate::cli::SystemLayerCommands::Add { layer, packages } => {
let package_count = packages.len();
crate::system_state::add_packages_to_layer(&config, layer.clone(), &packages)?;
ui::success(format!(
"Added {} package(s) to layer {}",
package_count, layer
));
}
crate::cli::SystemLayerCommands::Remove { layer, packages } => {
let package_count = packages.len();
crate::system_state::remove_packages_from_layer(
&config,
layer.clone(),
&packages,
)?;
ui::success(format!(
"Removed {} package(s) from layer {}",
package_count, layer
));
}
crate::cli::SystemLayerCommands::List => {
drop(_guard);
let _guard =
locking::try_read(&system_lock, &system_lock_path, "system layer list")?;
let state = crate::system_state::load(&config)?;
print_system_layers(&state);
}
}
}
crate::cli::SystemCommands::InitLbi {
target,
arch,
force,
} => {
let _guard =
locking::try_write(&mut system_lock, &system_lock_path, "system init-lbi")?;
let state = crate::system_state::init_lbi_layout(
&rootfs,
&config,
&target,
arch.as_deref(),
force,
)?;
ui::success(format!(
"Initialized Linux by Intent layout for {} ({})",
state.target.as_deref().unwrap_or("unknown"),
state.arch.as_deref().unwrap_or("unknown")
));
}
}
Ok(())
}
fn print_system_state(state: &crate::system_state::SystemState) {
println!(
"Stage: {}",
state.stage.as_deref().unwrap_or("uninitialized")
);
if let Some(target) = &state.target {
println!("Target: {target}");
}
if let Some(arch) = &state.arch {
println!("Arch: {arch}");
}
print_system_layers(state);
}
fn print_system_layers(state: &crate::system_state::SystemState) {
if state.layers.is_empty() {
println!("Layers: none");
return;
}
println!("Layers:");
for (layer, packages) in &state.layers {
if packages.is_empty() {
println!(" {layer}:");
} else {
println!(" {}: {}", layer, packages.join(", "));
}
}
}
pub(super) fn run_make_spec(args: crate::cli::MakeSpecArgs) -> Result<()> { pub(super) fn run_make_spec(args: crate::cli::MakeSpecArgs) -> Result<()> {
let output = args.output; let output = args.output;
let spec = package::create_interactive()?; let spec = package::create_interactive()?;
+7
View File
@@ -143,6 +143,13 @@ pub(crate) fn run_internal_command(command: InternalCommands) -> Result<()> {
&[], &[],
) )
} }
InternalCommands::BootstrapChroot {
rootfs,
sources,
destdir,
workdir,
script,
} => crate::bootstrap::run_bootstrap_chroot(&rootfs, &sources, &destdir, &workdir, &script),
InternalCommands::AutotoolsConfigure { args } => { InternalCommands::AutotoolsConfigure { args } => {
let env_vars = current_process_env_vars(); let env_vars = current_process_env_vars();
let context = current_build_helper_context()?; let context = current_build_helper_context()?;
+36
View File
@@ -282,6 +282,42 @@ fn clean_build_workspace_noops_when_dirs_are_missing() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
fn clean_build_source_dirs_removes_build_dir_only() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("tmp/build");
cfg.cache_dir = rootfs.path().join("tmp/sources");
fs::create_dir_all(&cfg.build_dir)
.with_context(|| format!("Failed to create {}", cfg.build_dir.display()))?;
fs::create_dir_all(&cfg.cache_dir)
.with_context(|| format!("Failed to create {}", cfg.cache_dir.display()))?;
clean_build_source_dirs(&cfg)?;
assert!(!cfg.build_dir.exists());
assert!(cfg.cache_dir.exists());
Ok(())
}
#[test]
fn clean_build_source_dirs_noops_when_build_dir_missing() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
let mut cfg = config::Config::for_rootfs(rootfs.path());
cfg.build_dir = rootfs.path().join("tmp/build");
cfg.cache_dir = rootfs.path().join("tmp/sources");
fs::create_dir_all(&cfg.cache_dir)
.with_context(|| format!("Failed to create {}", cfg.cache_dir.display()))?;
clean_build_source_dirs(&cfg)?;
assert!(!cfg.build_dir.exists());
assert!(cfg.cache_dir.exists());
Ok(())
}
#[test] #[test]
fn binary_install_path_uses_repo_record_metadata_without_archive_metadata() -> Result<()> { fn binary_install_path_uses_repo_record_metadata_without_archive_metadata() -> Result<()> {
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?; let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
+34 -1
View File
@@ -41,20 +41,27 @@ impl CrossConfig {
let cc = find_tool(prefix, &["gcc", "clang"], true)?; let cc = find_tool(prefix, &["gcc", "clang"], true)?;
let cxx = find_tool(prefix, &["g++", "clang++"], false) let cxx = find_tool(prefix, &["g++", "clang++"], false)
.unwrap_or_else(|_| format!("{}-g++", prefix)); .unwrap_or_else(|_| format!("{}-g++", prefix));
let ar = find_tool(prefix, &["ar", "llvm-ar"], true)?; let ar =
find_tool_with_unprefixed_fallback(prefix, &["ar", "llvm-ar"], &["llvm-ar"], true)?;
let ranlib = find_tool(prefix, &["ranlib", "llvm-ranlib"], false) let ranlib = find_tool(prefix, &["ranlib", "llvm-ranlib"], false)
.or_else(|_| find_unprefixed_tool(&["llvm-ranlib", "llvm-ar"], false))
.unwrap_or_else(|_| format!("{}-ranlib", prefix)); .unwrap_or_else(|_| format!("{}-ranlib", prefix));
let strip = find_tool(prefix, &["strip", "llvm-strip"], false) let strip = find_tool(prefix, &["strip", "llvm-strip"], false)
.or_else(|_| find_unprefixed_tool(&["llvm-strip", "llvm-objcopy"], false))
.unwrap_or_else(|_| format!("{}-strip", prefix)); .unwrap_or_else(|_| format!("{}-strip", prefix));
let ld = find_tool(prefix, &["ld", "ld.lld"], false) let ld = find_tool(prefix, &["ld", "ld.lld"], false)
.unwrap_or_else(|_| format!("{}-ld", prefix)); .unwrap_or_else(|_| format!("{}-ld", prefix));
let nm = find_tool(prefix, &["nm", "llvm-nm"], false) let nm = find_tool(prefix, &["nm", "llvm-nm"], false)
.or_else(|_| find_unprefixed_tool(&["llvm-nm"], false))
.unwrap_or_else(|_| format!("{}-nm", prefix)); .unwrap_or_else(|_| format!("{}-nm", prefix));
let objcopy = find_tool(prefix, &["objcopy", "llvm-objcopy"], false) let objcopy = find_tool(prefix, &["objcopy", "llvm-objcopy"], false)
.or_else(|_| find_unprefixed_tool(&["llvm-objcopy"], false))
.unwrap_or_else(|_| format!("{}-objcopy", prefix)); .unwrap_or_else(|_| format!("{}-objcopy", prefix));
let objdump = find_tool(prefix, &["objdump", "llvm-objdump"], false) let objdump = find_tool(prefix, &["objdump", "llvm-objdump"], false)
.or_else(|_| find_unprefixed_tool(&["llvm-objdump"], false))
.unwrap_or_else(|_| format!("{}-objdump", prefix)); .unwrap_or_else(|_| format!("{}-objdump", prefix));
let readelf = find_tool(prefix, &["readelf", "llvm-readelf"], false) let readelf = find_tool(prefix, &["readelf", "llvm-readelf"], false)
.or_else(|_| find_unprefixed_tool(&["llvm-readelf", "llvm-readobj"], false))
.unwrap_or_else(|_| format!("{}-readelf", prefix)); .unwrap_or_else(|_| format!("{}-readelf", prefix));
crate::log_info!("Cross-compilation tools discovered:"); crate::log_info!("Cross-compilation tools discovered:");
@@ -244,6 +251,32 @@ fn find_tool(prefix: &str, suffixes: &[&str], required: bool) -> Result<String>
Err(anyhow::anyhow!("Tool not found")) Err(anyhow::anyhow!("Tool not found"))
} }
fn find_tool_with_unprefixed_fallback(
prefix: &str,
suffixes: &[&str],
unprefixed: &[&str],
required: bool,
) -> Result<String> {
find_tool(prefix, suffixes, false).or_else(|_| find_unprefixed_tool(unprefixed, required))
}
fn find_unprefixed_tool(names: &[&str], required: bool) -> Result<String> {
for name in names {
if let Ok(output) = Command::new("which").arg(name).output()
&& output.status.success()
{
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
return Ok(path);
}
}
if required {
anyhow::bail!("Could not find tool: {{{}}}", names.join("|"));
}
Err(anyhow::anyhow!("Tool not found"))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::lib32_target_triple; use super::lib32_target_triple;
+84 -1
View File
@@ -10,6 +10,8 @@ use std::fs;
use std::path::Path; use std::path::Path;
use walkdir::WalkDir; use walkdir::WalkDir;
const DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS: &str = "DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS";
fn format_licenses(licenses: &[String]) -> String { fn format_licenses(licenses: &[String]) -> String {
licenses.join(", ") licenses.join(", ")
} }
@@ -18,6 +20,14 @@ fn verbose_remove_output() -> bool {
std::env::var_os("DEPOT_VERBOSE_REMOVE").is_some() std::env::var_os("DEPOT_VERBOSE_REMOVE").is_some()
} }
fn should_ignore_sbase_conflicts() -> bool {
std::env::var_os(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS).is_some()
}
fn should_auto_clear_conflict(owner: &str, path: &str) -> bool {
(owner == "sbase" && should_ignore_sbase_conflicts()) || is_auto_removable_path(path)
}
/// Installed package row from the local package database. /// Installed package row from the local package database.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledPackageRecord { pub struct InstalledPackageRecord {
@@ -213,7 +223,7 @@ pub fn register_package_with_replacement(
if let Ok(owner) = owner_res if let Ok(owner) = owner_res
&& owner != spec.package.name && owner != spec.package.name
{ {
if is_auto_removable_path(file) { if should_auto_clear_conflict(&owner, file) {
auto_conflicts.push((file.clone(), owner)); auto_conflicts.push((file.clone(), owner));
} else { } else {
fatal_conflicts.push((file.clone(), owner)); fatal_conflicts.push((file.clone(), owner));
@@ -241,6 +251,15 @@ pub fn register_package_with_replacement(
)?; )?;
if let Some(rootfs) = &rootfs_opt { if let Some(rootfs) = &rootfs_opt {
if destdir.join(f).symlink_metadata().is_ok() {
crate::log_info!(
"Auto-cleared DB ownership for path: {} (previously owned by {})",
f,
owner
);
continue;
}
let disk_path = rootfs.join(f); let disk_path = rootfs.join(f);
if disk_path.exists() { if disk_path.exists() {
let _ = std::fs::remove_file(&disk_path); let _ = std::fs::remove_file(&disk_path);
@@ -1056,6 +1075,7 @@ mod tests {
use crate::package::{ use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source, Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
}; };
use crate::test_support::TestEnv;
use std::path::PathBuf; use std::path::PathBuf;
fn mk_spec(name: &str, version: &str) -> PackageSpec { fn mk_spec(name: &str, version: &str) -> PackageSpec {
@@ -1243,6 +1263,69 @@ mod tests {
assert!(files_b.contains(&"usr/share/perl5/shared.pm".to_string())); assert!(files_b.contains(&"usr/share/perl5/shared.pm".to_string()));
} }
#[test]
fn register_package_auto_clears_sbase_conflicts_when_requested() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("rootfs");
let db_path = crate::config::Config::for_rootfs(&rootfs).installed_db_path(&rootfs);
std::fs::create_dir_all(rootfs.join("system/binaries")).unwrap();
std::fs::write(rootfs.join("system/binaries/find"), "sbase find").unwrap();
let spec_a = mk_spec("sbase", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("system/binaries")).unwrap();
std::fs::write(dest_a.join("system/binaries/find"), "sbase find").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
let spec_b = mk_spec("bfs", "4.1");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("system/binaries")).unwrap();
std::fs::write(dest_b.join("system/binaries/find"), "bfs find").unwrap();
let mut env = TestEnv::new();
env.set_var(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS, "1");
register_package(&db_path, &spec_b, &dest_b).unwrap();
let files_sbase = get_package_files(&db_path, "sbase").unwrap();
assert!(!files_sbase.contains(&"system/binaries/find".to_string()));
let files_bfs = get_package_files(&db_path, "bfs").unwrap();
assert!(files_bfs.contains(&"system/binaries/find".to_string()));
}
#[test]
fn register_package_auto_clear_preserves_new_payload_on_disk() {
let tmp = tempfile::tempdir().unwrap();
let rootfs = tmp.path().join("rootfs");
let db_path = crate::config::Config::for_rootfs(&rootfs).installed_db_path(&rootfs);
std::fs::create_dir_all(rootfs.join("system/binaries")).unwrap();
std::fs::write(rootfs.join("system/binaries/find"), "sbase find").unwrap();
let spec_a = mk_spec("sbase", "1.0");
let dest_a = tmp.path().join("dest_a");
std::fs::create_dir_all(dest_a.join("system/binaries")).unwrap();
std::fs::write(dest_a.join("system/binaries/find"), "sbase find").unwrap();
register_package(&db_path, &spec_a, &dest_a).unwrap();
std::fs::write(rootfs.join("system/binaries/find"), "bfs find").unwrap();
let spec_b = mk_spec("bfs", "4.1");
let dest_b = tmp.path().join("dest_b");
std::fs::create_dir_all(dest_b.join("system/binaries")).unwrap();
std::fs::write(dest_b.join("system/binaries/find"), "bfs find").unwrap();
let mut env = TestEnv::new();
env.set_var(DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS, "1");
register_package(&db_path, &spec_b, &dest_b).unwrap();
assert_eq!(
std::fs::read_to_string(rootfs.join("system/binaries/find")).unwrap(),
"bfs find"
);
let files_sbase = get_package_files(&db_path, "sbase").unwrap();
assert!(!files_sbase.contains(&"system/binaries/find".to_string()));
let files_bfs = get_package_files(&db_path, "bfs").unwrap();
assert!(files_bfs.contains(&"system/binaries/find".to_string()));
}
#[test] #[test]
fn get_package_files_missing_package_returns_empty() { fn get_package_files_missing_package_returns_empty() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
+2
View File
@@ -1,6 +1,7 @@
//! Depot - Not Your Average Package Manager //! Depot - Not Your Average Package Manager
//! A source-based package manager for Linux //! A source-based package manager for Linux
mod bootstrap;
mod build_options; mod build_options;
mod builder; mod builder;
mod cli; mod cli;
@@ -24,6 +25,7 @@ mod shell_helpers;
mod signing; mod signing;
mod source; mod source;
mod staging; mod staging;
mod system_state;
#[cfg(test)] #[cfg(test)]
mod test_support; mod test_support;
mod ui; mod ui;
+308 -243
View File
@@ -8,9 +8,17 @@ use std::collections::HashSet;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration;
use url::Url; use url::Url;
const MAX_MIRROR_RETRIES: usize = 8; const MAX_MIRROR_RETRIES: usize = 8;
const HTTP_FETCH_TIMEOUT: Duration = Duration::from_secs(120);
const HTTP_FETCH_RETRY_LIMIT: usize = 2;
const MUSL_1_2_6_SNAPSHOT_URL: &str =
"https://git.musl-libc.org/cgit/musl/snapshot/musl-1.2.6.tar.gz";
const MUSL_1_2_6_RELEASE_URL: &str = "https://musl.libc.org/releases/musl-1.2.6.tar.gz";
const MUSL_1_2_6_GENTOO_MIRROR_URL: &str =
"https://tw.archive.ubuntu.com/gentoo/distfiles/9d/musl-1.2.6.tar.gz";
fn scheme_uses_http_transport(scheme: &str) -> bool { fn scheme_uses_http_transport(scheme: &str) -> bool {
matches!(scheme, "http" | "https") matches!(scheme, "http" | "https")
@@ -32,10 +40,6 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
return Ok(dest_path); return Ok(dest_path);
} }
crate::log_info!("Fetching: {}", url);
// Parse URL early so we can handle non-HTTP schemes (FTP support)
let parsed_url = Url::parse(&url).with_context(|| format!("Invalid URL: {}", url))?;
let pb = ProgressBar::new(0); let pb = ProgressBar::new(0);
pb.set_style( pb.set_style(
ProgressStyle::default_bar() ProgressStyle::default_bar()
@@ -46,256 +50,46 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
.progress_chars("#>-"), .progress_chars("#>-"),
); );
// If this is an FTP URL, fetch via suppaftp and skip the HTTP client path. let candidate_urls = archive_fetch_candidates(&url);
if parsed_url.scheme() == "ftp" { let mut attempt_errors = Vec::new();
// Connect and login (anonymous fallback)
let host = parsed_url.host_str().context("FTP URL missing host")?; for (index, candidate_url) in candidate_urls.iter().enumerate() {
let port = parsed_url.port_or_known_default().unwrap_or(21); if index == 0 {
let addr = format!("{}:{}", host, port); crate::log_info!("Fetching: {}", candidate_url);
let mut ftp_stream = suppaftp::FtpStream::connect(addr.as_str())
.with_context(|| format!("Failed to connect to FTP host: {}", addr))?;
let user = if parsed_url.username().is_empty() {
"anonymous"
} else { } else {
parsed_url.username() crate::log_info!("Primary source failed; trying fallback: {}", candidate_url);
};
let pass = parsed_url.password().unwrap_or("anonymous@");
ftp_stream
.login(user, pass)
.with_context(|| format!("FTP login failed for {}", host))?;
// Retrieve the path (try with and without leading slash)
let path = parsed_url.path();
let candidates = [path.to_string(), path.trim_start_matches('/').to_string()];
let mut retrieved = false;
for p in candidates.iter().filter(|s| !s.is_empty()) {
match ftp_stream.retr(
p,
|reader: &mut dyn Read| -> std::result::Result<(), suppaftp::FtpError> {
let mut file =
File::create(&dest_path).map_err(suppaftp::FtpError::ConnectionError)?;
let mut buffer = [0u8; 8192];
loop {
let bytes_read = reader
.read(&mut buffer)
.map_err(suppaftp::FtpError::ConnectionError)?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])
.map_err(suppaftp::FtpError::ConnectionError)?;
}
Ok(())
},
) {
Ok(_) => {
retrieved = true;
break;
}
Err(_) => continue,
}
}
ftp_stream.quit().ok();
if !retrieved {
bail!("FTP error fetching {}", url);
}
} else {
if !scheme_uses_http_transport(parsed_url.scheme()) {
bail!(
"Unsupported URL scheme for source fetch: {}",
parsed_url.scheme()
);
} }
// Download with progress bar pb.set_position(0);
// Use a sensible default User-Agent so servers that reject empty/unknown agents (e.g. IANA) pb.set_length(0);
// will accept requests. Include package name/version at compile time.
let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
let client = super::build_blocking_client(&ua, None)
.with_context(|| "Failed to build HTTP client")?;
let mut response = client
.get(&url)
.send()
.with_context(|| format!("Failed to fetch: {}", url))?;
// If the server returned a non-success status, read a short body preview and fail early.
// This prevents saving HTML error pages (which then fail checksum) and gives a clearer
// diagnostic to the user.
let status = response.status();
if !status.is_success() {
let mut preview_bytes = Vec::new();
// read up to 1 KiB for a preview (ignore errors while reading preview)
let _ = response.take(1024).read_to_end(&mut preview_bytes);
let preview = String::from_utf8_lossy(&preview_bytes);
bail!(
"HTTP error fetching {}: {}{}",
url,
status,
if preview.trim().is_empty() {
"".to_string()
} else {
format!(" — preview: {}", preview.trim())
}
);
}
let total_size = response.content_length().unwrap_or(0);
pb.set_length(total_size);
let mut file = File::create(&dest_path)
.with_context(|| format!("Failed to create: {}", dest_path.display()))?;
let mut buffer = [0u8; 8192];
let mut downloaded = 0u64;
loop {
let bytes_read = response.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])?;
downloaded += bytes_read as u64;
pb.set_position(downloaded);
}
pb.finish_with_message("Download complete");
}
// Quick validation: ensure the downloaded file looks like the expected
// archive (detect obvious HTML error pages or wrong formats by magic).
// If validation returns an alternate URL (e.g. SourceForge mirror), follow
// and retry a few times.
let mut next_alt = validate_downloaded_archive(&dest_path, &filename, &url)?;
let mut seen_alts: HashSet<String> = HashSet::new();
let mut retries = 0usize;
while let Some(alt) = next_alt {
retries += 1;
if retries > MAX_MIRROR_RETRIES {
bail!(
"Exceeded mirror retry limit ({}) while fetching {}",
MAX_MIRROR_RETRIES,
url
);
}
if !seen_alts.insert(alt.clone()) {
bail!("Mirror retry loop detected for URL: {}", alt);
}
crate::log_info!("Retrying download from mirror: {}", alt);
fs::remove_file(&dest_path).ok(); fs::remove_file(&dest_path).ok();
// If mirror URL is FTP -> use suppaftp; otherwise use HTTP retry. match fetch_archive_from_candidate(candidate_url, &dest_path, &filename, &pb) {
if let Ok(alt_url) = Url::parse(&alt) { Ok(()) => {
if alt_url.scheme() == "ftp" { if !verify_checksum(&dest_path, &source.sha256)? {
// FTP mirror retrieval fs::remove_file(&dest_path)?;
let host = alt_url.host_str().context("FTP mirror URL missing host")?; attempt_errors.push(format!(
let port = alt_url.port_or_known_default().unwrap_or(21); "{}: checksum verification failed for {}",
let addr = format!("{}:{}", host, port); candidate_url, filename
let mut ftp_stream = suppaftp::FtpStream::connect(addr.as_str()) ));
.with_context(|| format!("Failed to connect to FTP host: {}", addr))?; continue;
let user = if alt_url.username().is_empty() {
"anonymous"
} else {
alt_url.username()
};
let pass = alt_url.password().unwrap_or("anonymous@");
ftp_stream
.login(user, pass)
.with_context(|| format!("FTP login failed for {}", host))?;
let path = alt_url.path();
let candidates = [path.to_string(), path.trim_start_matches('/').to_string()];
let mut retrieved = false;
for p in candidates.iter().filter(|s| !s.is_empty()) {
match ftp_stream.retr(
p,
|reader: &mut dyn Read| -> std::result::Result<(), suppaftp::FtpError> {
let mut file = File::create(&dest_path)
.map_err(suppaftp::FtpError::ConnectionError)?;
let mut buffer = [0u8; 8192];
let mut downloaded = 0u64;
loop {
let bytes_read = reader
.read(&mut buffer)
.map_err(suppaftp::FtpError::ConnectionError)?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])
.map_err(suppaftp::FtpError::ConnectionError)?;
downloaded += bytes_read as u64;
pb.set_position(downloaded);
}
Ok(())
},
) {
Ok(_) => {
retrieved = true;
break;
}
Err(_) => continue,
}
}
ftp_stream.quit().ok();
if !retrieved {
bail!("FTP mirror error fetching {}", alt);
}
} else {
// HTTP(S) mirror retry (recreate client for retry)
let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
let client = super::build_blocking_client(&ua, None)
.with_context(|| "Failed to build HTTP client")?;
let mut response = client
.get(&alt)
.send()
.with_context(|| format!("Failed to fetch mirror URL: {}", alt))?;
let status = response.status();
if !status.is_success() {
let mut preview_bytes = Vec::new();
let _ = response.take(1024).read_to_end(&mut preview_bytes);
let preview = String::from_utf8_lossy(&preview_bytes);
bail!(
"HTTP error fetching {}: {}{}",
alt,
status,
if preview.trim().is_empty() {
"".to_string()
} else {
format!(" — preview: {}", preview.trim())
}
);
} }
let mut file = File::create(&dest_path) crate::log_info!("Checksum verified: {}", filename);
.with_context(|| format!("Failed to create: {}", dest_path.display()))?; return Ok(dest_path);
let mut buffer = [0u8; 8192]; }
let mut downloaded = 0u64; Err(err) => {
loop { attempt_errors.push(format!("{}: {err:#}", candidate_url));
let bytes_read = response.read(&mut buffer)?; fs::remove_file(&dest_path).ok();
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])?;
downloaded += bytes_read as u64;
pb.set_position(downloaded);
}
} }
} else {
// Fallback: unknown/malformed alt URL -> bail
bail!("Malformed mirror URL: {}", alt);
} }
pb.finish_with_message("Download complete (mirror)");
next_alt = validate_downloaded_archive(&dest_path, &filename, &alt)?;
} }
// Verify checksum bail!(
if !verify_checksum(&dest_path, &source.sha256)? { "Failed to fetch {} from any candidate URL:\n{}",
fs::remove_file(&dest_path)?; filename,
bail!("Checksum verification failed for {}", filename); attempt_errors.join("\n")
} )
crate::log_info!("Checksum verified: {}", filename);
Ok(dest_path)
} }
/// Verify checksum of a file supporting optional algorithm prefix. /// Verify checksum of a file supporting optional algorithm prefix.
@@ -338,6 +132,238 @@ pub(crate) fn derive_filename_from_url(url: &str) -> String {
format!("source-{}.download", &hex[..12]) format!("source-{}.download", &hex[..12])
} }
fn archive_fetch_candidates(primary_url: &str) -> Vec<String> {
let mut candidates = Vec::new();
if matches!(
primary_url,
MUSL_1_2_6_SNAPSHOT_URL | MUSL_1_2_6_RELEASE_URL | MUSL_1_2_6_GENTOO_MIRROR_URL
) {
candidates.push(MUSL_1_2_6_GENTOO_MIRROR_URL.to_string());
}
if !candidates.iter().any(|url| url == primary_url) {
candidates.push(primary_url.to_string());
}
if let Some(mirror_url) = musl_release_mirror_url(primary_url)
&& !candidates.contains(&mirror_url)
{
candidates.push(mirror_url);
}
candidates
}
fn musl_release_mirror_url(url: &str) -> Option<String> {
let parsed = Url::parse(url).ok()?;
if parsed.host_str()? != "git.musl-libc.org" {
return None;
}
let mut segments = parsed.path_segments()?;
match (
segments.next(),
segments.next(),
segments.next(),
segments.next(),
segments.next(),
) {
(Some("cgit"), Some("musl"), Some("snapshot"), Some(filename), None)
if !filename.trim().is_empty() =>
{
Some(format!("https://musl.libc.org/releases/{filename}"))
}
_ => None,
}
}
fn fetch_archive_from_candidate(
candidate_url: &str,
dest_path: &Path,
filename: &str,
pb: &ProgressBar,
) -> Result<()> {
let mut current_url = candidate_url.to_string();
let mut seen_alts: HashSet<String> = HashSet::new();
let mut retries = 0usize;
loop {
download_archive_from_url(&current_url, dest_path, pb)?;
pb.finish_with_message("Download complete");
let Some(next_alt) = validate_downloaded_archive(dest_path, filename, &current_url)? else {
return Ok(());
};
retries += 1;
if retries > MAX_MIRROR_RETRIES {
bail!(
"Exceeded mirror retry limit ({}) while fetching {}",
MAX_MIRROR_RETRIES,
candidate_url
);
}
if !seen_alts.insert(next_alt.clone()) {
bail!("Mirror retry loop detected for URL: {}", next_alt);
}
crate::log_info!("Retrying download from mirror: {}", next_alt);
fs::remove_file(dest_path).ok();
pb.set_position(0);
pb.set_length(0);
current_url = next_alt;
}
}
fn download_archive_from_url(url: &str, dest_path: &Path, pb: &ProgressBar) -> Result<()> {
let parsed_url = Url::parse(url).with_context(|| format!("Invalid URL: {}", url))?;
if parsed_url.scheme() == "ftp" {
return download_ftp_archive(&parsed_url, dest_path, pb);
}
if !scheme_uses_http_transport(parsed_url.scheme()) {
bail!(
"Unsupported URL scheme for source fetch: {}",
parsed_url.scheme()
);
}
download_http_archive(url, dest_path, pb)
}
fn download_ftp_archive(parsed_url: &Url, dest_path: &Path, pb: &ProgressBar) -> Result<()> {
let host = parsed_url.host_str().context("FTP URL missing host")?;
let port = parsed_url.port_or_known_default().unwrap_or(21);
let addr = format!("{}:{}", host, port);
let mut ftp_stream = suppaftp::FtpStream::connect(addr.as_str())
.with_context(|| format!("Failed to connect to FTP host: {}", addr))?;
let user = if parsed_url.username().is_empty() {
"anonymous"
} else {
parsed_url.username()
};
let pass = parsed_url.password().unwrap_or("anonymous@");
ftp_stream
.login(user, pass)
.with_context(|| format!("FTP login failed for {}", host))?;
let path = parsed_url.path();
let candidates = [path.to_string(), path.trim_start_matches('/').to_string()];
let mut retrieved = false;
for candidate in candidates.iter().filter(|path| !path.is_empty()) {
match ftp_stream.retr(
candidate,
|reader: &mut dyn Read| -> std::result::Result<(), suppaftp::FtpError> {
let mut file =
File::create(dest_path).map_err(suppaftp::FtpError::ConnectionError)?;
let mut buffer = [0u8; 8192];
let mut downloaded = 0u64;
loop {
let bytes_read = reader
.read(&mut buffer)
.map_err(suppaftp::FtpError::ConnectionError)?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])
.map_err(suppaftp::FtpError::ConnectionError)?;
downloaded += bytes_read as u64;
pb.set_position(downloaded);
}
Ok(())
},
) {
Ok(_) => {
retrieved = true;
break;
}
Err(_) => continue,
}
}
ftp_stream.quit().ok();
if !retrieved {
bail!("FTP error fetching {}", parsed_url);
}
Ok(())
}
fn download_http_archive(url: &str, dest_path: &Path, pb: &ProgressBar) -> Result<()> {
let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
let mut last_err = None;
for attempt in 1..=HTTP_FETCH_RETRY_LIMIT {
match download_http_archive_once(url, dest_path, pb, &ua) {
Ok(()) => return Ok(()),
Err(err) if attempt < HTTP_FETCH_RETRY_LIMIT && is_transient_http_error(&err) => {
crate::log_info!(
"Fetch attempt {} for {} failed with a transient network error; retrying",
attempt,
url
);
fs::remove_file(dest_path).ok();
pb.set_position(0);
pb.set_length(0);
last_err = Some(err);
}
Err(err) => return Err(err),
}
}
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("Failed to fetch: {}", url)))
}
fn download_http_archive_once(
url: &str,
dest_path: &Path,
pb: &ProgressBar,
ua: &str,
) -> Result<()> {
let client = super::build_blocking_client(ua, Some(HTTP_FETCH_TIMEOUT))
.with_context(|| "Failed to build HTTP client")?;
let mut response = client
.get(url)
.send()
.with_context(|| format!("Failed to fetch: {}", url))?;
let status = response.status();
if !status.is_success() {
let mut preview_bytes = Vec::new();
let _ = response.take(1024).read_to_end(&mut preview_bytes);
let preview = String::from_utf8_lossy(&preview_bytes);
bail!(
"HTTP error fetching {}: {}{}",
url,
status,
if preview.trim().is_empty() {
"".to_string()
} else {
format!(" — preview: {}", preview.trim())
}
);
}
let total_size = response.content_length().unwrap_or(0);
pb.set_length(total_size);
let mut file = File::create(dest_path)
.with_context(|| format!("Failed to create: {}", dest_path.display()))?;
let mut buffer = [0u8; 8192];
let mut downloaded = 0u64;
loop {
let bytes_read = response.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])?;
downloaded += bytes_read as u64;
pb.set_position(downloaded);
}
Ok(())
}
fn is_transient_http_error(err: &anyhow::Error) -> bool {
err.chain().any(|cause| {
cause
.downcast_ref::<reqwest::Error>()
.is_some_and(|inner| inner.is_timeout() || inner.is_connect() || inner.is_request())
})
}
/// Validate downloaded file's magic header to make sure it is the expected /// Validate downloaded file's magic header to make sure it is the expected
/// archive format (avoids saving HTML pages or other unexpected content). /// archive format (avoids saving HTML pages or other unexpected content).
fn validate_downloaded_archive( fn validate_downloaded_archive(
@@ -647,6 +673,45 @@ mod tests {
assert!(name.starts_with("source-") && name.ends_with(".download")); assert!(name.starts_with("source-") && name.ends_with(".download"));
} }
#[test]
fn musl_snapshot_candidates_prefer_known_gentoo_mirror() {
let candidates = archive_fetch_candidates(MUSL_1_2_6_SNAPSHOT_URL);
assert_eq!(
candidates,
vec![
MUSL_1_2_6_GENTOO_MIRROR_URL.to_string(),
MUSL_1_2_6_SNAPSHOT_URL.to_string(),
"https://musl.libc.org/releases/musl-1.2.6.tar.gz".to_string(),
]
);
}
#[test]
fn musl_release_candidates_prefer_known_gentoo_mirror() {
let candidates = archive_fetch_candidates(MUSL_1_2_6_RELEASE_URL);
assert_eq!(
candidates,
vec![
MUSL_1_2_6_GENTOO_MIRROR_URL.to_string(),
MUSL_1_2_6_RELEASE_URL.to_string(),
]
);
}
#[test]
fn musl_release_mirror_only_matches_snapshot_urls() {
assert_eq!(
musl_release_mirror_url(
"https://git.musl-libc.org/cgit/musl/snapshot/musl-1.2.5.tar.gz"
),
Some("https://musl.libc.org/releases/musl-1.2.5.tar.gz".to_string())
);
assert_eq!(
musl_release_mirror_url("https://musl.libc.org/releases/musl-1.2.5.tar.gz"),
None
);
}
#[test] #[test]
fn sourceforge_html_no_link_falls_back_to_download_suffix() { fn sourceforge_html_no_link_falls_back_to_download_suffix() {
use std::io::Write; use std::io::Write;
+414 -21
View File
@@ -19,9 +19,12 @@ pub const INTERNAL_DEPOT_DIR: &str = ".depot";
pub const INTERNAL_OUTPUTS_DIR: &str = ".depot/outputs"; pub const INTERNAL_OUTPUTS_DIR: &str = ".depot/outputs";
fn is_info_dir_index_path(rel_path: &str) -> bool { fn is_info_dir_index_path(rel_path: &str) -> bool {
matches!(rel_path, "usr/info/dir" | "usr/share/info/dir") matches!(
|| rel_path.starts_with("usr/info/dir.") rel_path,
"usr/info/dir" | "usr/share/info/dir" | "system/documentation/info/dir"
) || rel_path.starts_with("usr/info/dir.")
|| rel_path.starts_with("usr/share/info/dir.") || rel_path.starts_with("usr/share/info/dir.")
|| rel_path.starts_with("system/documentation/info/dir.")
} }
fn is_purged_install_basename(rel_path: &str) -> bool { fn is_purged_install_basename(rel_path: &str) -> bool {
@@ -43,6 +46,8 @@ fn is_skipped_install_path(rel_path: &str) -> bool {
|| p == INTERNAL_DEPOT_DIR || p == INTERNAL_DEPOT_DIR
|| p.strip_prefix(INTERNAL_DEPOT_DIR) || p.strip_prefix(INTERNAL_DEPOT_DIR)
.is_some_and(|rest| rest.starts_with('/')) .is_some_and(|rest| rest.starts_with('/'))
|| p == "destdir"
|| p.starts_with("destdir/")
|| p == "scripts" || p == "scripts"
|| p.starts_with("scripts/") || p.starts_with("scripts/")
|| is_purged_payload_path(p) || is_purged_payload_path(p)
@@ -340,12 +345,26 @@ fn move_tree_preserving_layout(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(parent) fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?; .with_context(|| format!("Failed to create {}", parent.display()))?;
} }
if dst.symlink_metadata().is_ok() { match dst.symlink_metadata() {
anyhow::bail!( Ok(dst_metadata)
"Failed to move {} into {}: destination already exists", if duplicate_staged_path_is_equivalent(src, &metadata, dst, &dst_metadata)? =>
src.display(), {
dst.display() fs::remove_file(src).with_context(|| {
); format!("Failed to remove duplicate staged path {}", src.display())
})?;
return Ok(());
}
Ok(_) => {
anyhow::bail!(
"Failed to move {} into {}: destination already exists",
src.display(),
dst.display()
);
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err).with_context(|| format!("Failed to inspect {}", dst.display()));
}
} }
fs::rename(src, dst).with_context(|| { fs::rename(src, dst).with_context(|| {
format!( format!(
@@ -359,6 +378,59 @@ fn move_tree_preserving_layout(src: &Path, dst: &Path) -> Result<()> {
Ok(()) Ok(())
} }
fn duplicate_staged_path_is_equivalent(
src: &Path,
src_metadata: &fs::Metadata,
dst: &Path,
dst_metadata: &fs::Metadata,
) -> Result<bool> {
let src_type = src_metadata.file_type();
let dst_type = dst_metadata.file_type();
if src_type.is_symlink() || dst_type.is_symlink() {
if !(src_type.is_symlink() && dst_type.is_symlink()) {
return Ok(false);
}
let src_target = fs::read_link(src)
.with_context(|| format!("Failed to read symlink {}", src.display()))?;
let dst_target = fs::read_link(dst)
.with_context(|| format!("Failed to read symlink {}", dst.display()))?;
return Ok(src_target == dst_target);
}
if !src_metadata.is_file() || !dst_metadata.is_file() {
return Ok(false);
}
if src_metadata.len() != dst_metadata.len() {
return Ok(false);
}
files_have_same_contents(src, dst)
}
fn files_have_same_contents(left: &Path, right: &Path) -> Result<bool> {
let mut left = fs::File::open(left)
.with_context(|| format!("Failed to open staged file {}", left.display()))?;
let mut right = fs::File::open(right)
.with_context(|| format!("Failed to open staged file {}", right.display()))?;
let mut left_buf = [0u8; 8192];
let mut right_buf = [0u8; 8192];
loop {
let left_read = left.read(&mut left_buf)?;
let right_read = right.read(&mut right_buf)?;
if left_read != right_read {
return Ok(false);
}
if left_read == 0 {
return Ok(true);
}
if left_buf[..left_read] != right_buf[..right_read] {
return Ok(false);
}
}
}
fn split_docs_for_output( fn split_docs_for_output(
output_destdir: &Path, output_destdir: &Path,
docs_destdir: &Path, docs_destdir: &Path,
@@ -829,9 +901,182 @@ pub fn process(destdir: &Path, spec: &PackageSpec) -> Result<()> {
); );
} }
let normalized = normalize_lbi_layout(destdir)?;
if normalized > 0 {
crate::log_info!("Normalized {} path(s) into the /system layout", normalized);
}
Ok(()) Ok(())
} }
fn normalize_lbi_layout(destdir: &Path) -> Result<usize> {
let mut roots = vec![destdir.to_path_buf()];
let outputs = output_staging_root(destdir);
if outputs.exists() {
let mut output_dirs = fs::read_dir(&outputs)
.with_context(|| format!("Failed to read {}", outputs.display()))?
.collect::<std::result::Result<Vec<_>, _>>()
.with_context(|| {
format!(
"Failed to read output staging entry from {}",
outputs.display()
)
})?;
output_dirs.sort_by_key(|entry| entry.file_name());
for entry in output_dirs {
let path = entry.path();
if path.is_dir() {
roots.push(path);
}
}
}
let mut changed = 0usize;
for root in roots {
changed += normalize_lbi_tree_paths(&root)?;
changed += rewrite_lbi_pkgconfig_files(&root)?;
}
Ok(changed)
}
fn normalize_lbi_tree_paths(root: &Path) -> Result<usize> {
let mappings = [
("usr/share/man", "system/documentation/man-pages"),
("usr/share/info", "system/documentation/info"),
("usr/bin", "system/binaries"),
("usr/sbin", "system/systembinaries"),
("usr/lib64", "system/libraries"),
("usr/lib", "system/libraries"),
("usr/include", "system/headers"),
("usr/share", "system/share"),
("bin", "system/binaries"),
("sbin", "system/systembinaries"),
("lib64", "system/libraries"),
("lib", "system/libraries"),
("include", "system/headers"),
("etc", "system/configuration"),
("var", "system/variable"),
("system/bin", "system/binaries"),
("system/sbin", "system/systembinaries"),
("system/lib64", "system/libraries"),
("system/lib", "system/libraries"),
("system/include", "system/headers"),
("system/share/man", "system/documentation/man-pages"),
("system/share/info", "system/documentation/info"),
];
let mut changed = 0usize;
for (from, to) in mappings {
let src = root.join(from);
if !src.exists() {
continue;
}
let dst = root.join(to);
move_lbi_path(&src, &dst)?;
changed += 1;
}
prune_empty_dirs(root, &["usr", "system"])?;
Ok(changed)
}
fn move_lbi_path(src: &Path, dst: &Path) -> Result<()> {
if src == dst {
return Ok(());
}
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
if !dst.exists() {
fs::rename(src, dst)
.with_context(|| format!("Failed to move {} to {}", src.display(), dst.display()))?;
return Ok(());
}
let src_meta = fs::symlink_metadata(src)
.with_context(|| format!("Failed to inspect {}", src.display()))?;
let dst_meta = fs::symlink_metadata(dst)
.with_context(|| format!("Failed to inspect {}", dst.display()))?;
if src_meta.is_dir() && dst_meta.is_dir() {
move_directory_contents(src, dst)?;
return Ok(());
}
anyhow::bail!(
"Refusing to overwrite existing path while normalizing /system layout: {}",
dst.display()
);
}
fn prune_empty_dirs(root: &Path, dirs: &[&str]) -> Result<()> {
for dir in dirs {
let path = root.join(dir);
if path.exists() && path.is_dir() && is_directory_empty(&path)? {
fs::remove_dir(&path)
.with_context(|| format!("Failed to remove empty directory {}", path.display()))?;
}
}
Ok(())
}
fn rewrite_lbi_pkgconfig_files(root: &Path) -> Result<usize> {
let mut changed = 0usize;
if !root.exists() {
return Ok(0);
}
for entry in WalkDir::new(root).follow_links(false) {
let entry = entry.with_context(|| format!("Failed to walk {}", root.display()))?;
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
if path.extension().and_then(|ext| ext.to_str()) != Some("pc") {
continue;
}
let Ok(original) = fs::read_to_string(path) else {
continue;
};
let rewritten = rewrite_lbi_pkgconfig_text(&original);
if rewritten != original {
fs::write(path, rewritten)
.with_context(|| format!("Failed to rewrite pkg-config file {}", path.display()))?;
changed += 1;
}
}
Ok(changed)
}
fn rewrite_lbi_pkgconfig_text(input: &str) -> String {
input
.replace("prefix=/usr", "prefix=/system")
.replace("exec_prefix=/usr", "exec_prefix=/system")
.replace("bindir=${prefix}/bin", "bindir=${prefix}/binaries")
.replace("sbindir=${prefix}/sbin", "sbindir=${prefix}/systembinaries")
.replace("libdir=${prefix}/lib64", "libdir=${prefix}/libraries")
.replace("libdir=${prefix}/lib", "libdir=${prefix}/libraries")
.replace(
"includedir=${prefix}/include",
"includedir=${prefix}/headers",
)
.replace(
"mandir=${prefix}/share/man",
"mandir=${prefix}/documentation/man-pages",
)
.replace(
"infodir=${prefix}/share/info",
"infodir=${prefix}/documentation/info",
)
.replace("/usr/sbin", "/system/systembinaries")
.replace("/usr/bin", "/system/binaries")
.replace("/usr/lib64", "/system/libraries")
.replace("/usr/lib", "/system/libraries")
.replace("/usr/include", "/system/headers")
.replace("/usr/share/man", "/system/documentation/man-pages")
.replace("/usr/share/info", "/system/documentation/info")
.replace("/usr/share", "/system/share")
}
/// Copy license files into the staged tree. /// Copy license files into the staged tree.
/// ///
/// Copies common license file patterns from the source directory root into: /// Copies common license file patterns from the source directory root into:
@@ -1539,9 +1784,9 @@ mod tests {
let spec = mk_spec_for_stage_processing(); let spec = mk_spec_for_stage_processing();
process(&destdir, &spec).unwrap(); process(&destdir, &spec).unwrap();
assert!(!destdir.join("usr/lib/libfoo.a").exists()); assert!(!destdir.join("system/libraries/libfoo.a").exists());
assert!(!destdir.join("usr/lib/libfoo.la").exists()); assert!(!destdir.join("system/libraries/libfoo.la").exists());
assert!(destdir.join("usr/lib/libfoo.so").exists()); assert!(destdir.join("system/libraries/libfoo.so").exists());
} }
#[test] #[test]
@@ -1556,8 +1801,8 @@ mod tests {
spec.build.flags.no_delete_static = true; spec.build.flags.no_delete_static = true;
process(&destdir, &spec).unwrap(); process(&destdir, &spec).unwrap();
assert!(destdir.join("usr/lib/libfoo.a").exists()); assert!(destdir.join("system/libraries/libfoo.a").exists());
assert!(!destdir.join("usr/lib/libfoo.la").exists()); assert!(!destdir.join("system/libraries/libfoo.la").exists());
} }
#[test] #[test]
@@ -1580,18 +1825,18 @@ mod tests {
process(&destdir, &spec).unwrap(); process(&destdir, &spec).unwrap();
let docs_destdir = output_staging_dir(&destdir, "foo-docs"); let docs_destdir = output_staging_dir(&destdir, "foo-docs");
assert!(docs_destdir.join("usr/share/doc/foo/README").exists()); assert!(docs_destdir.join("system/share/doc/foo/README").exists());
assert!( assert!(
docs_destdir docs_destdir
.join("usr/share/gtk-doc/html/foo/index.html") .join("system/share/gtk-doc/html/foo/index.html")
.exists() .exists()
); );
assert!(docs_destdir.join("opt/foo-docs/guide.txt").exists()); assert!(docs_destdir.join("opt/foo-docs/guide.txt").exists());
assert!(destdir.join("usr/bin/foo").exists()); assert!(destdir.join("system/binaries/foo").exists());
assert!(!destdir.join("usr/share/doc/foo/README").exists()); assert!(!destdir.join("system/share/doc/foo/README").exists());
assert!( assert!(
!destdir !destdir
.join("usr/share/gtk-doc/html/foo/index.html") .join("system/share/gtk-doc/html/foo/index.html")
.exists() .exists()
); );
assert!(!destdir.join("opt/foo-docs/guide.txt").exists()); assert!(!destdir.join("opt/foo-docs/guide.txt").exists());
@@ -1623,9 +1868,129 @@ mod tests {
process(&destdir, &spec).unwrap(); process(&destdir, &spec).unwrap();
let docs_destdir = output_staging_dir(&destdir, "foo-dev-docs"); let docs_destdir = output_staging_dir(&destdir, "foo-dev-docs");
assert!(docs_destdir.join("usr/share/doc/foo-dev/README").exists()); assert!(
assert!(dev_destdir.join("usr/include/foo.h").exists()); docs_destdir
assert!(!dev_destdir.join("usr/share/doc/foo-dev/README").exists()); .join("system/share/doc/foo-dev/README")
.exists()
);
assert!(dev_destdir.join("system/headers/foo.h").exists());
assert!(!dev_destdir.join("system/share/doc/foo-dev/README").exists());
}
#[test]
fn process_normalizes_usr_paths_into_system_layout() {
let tmp = tempfile::tempdir().unwrap();
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
std::fs::create_dir_all(destdir.join("usr/lib/pkgconfig")).unwrap();
std::fs::create_dir_all(destdir.join("usr/include/foo")).unwrap();
std::fs::create_dir_all(destdir.join("usr/share/man/man1")).unwrap();
std::fs::create_dir_all(destdir.join("usr/share/info")).unwrap();
std::fs::write(destdir.join("usr/bin/foo"), "bin").unwrap();
std::fs::write(destdir.join("usr/share/man/man1/foo.1"), "man").unwrap();
std::fs::write(destdir.join("usr/share/info/foo.info"), "info").unwrap();
std::fs::write(
destdir.join("usr/lib/pkgconfig/foo.pc"),
"prefix=/usr\nbindir=${prefix}/bin\nlibdir=${prefix}/lib\nincludedir=${prefix}/include\nmandir=${prefix}/share/man\ninfodir=${prefix}/share/info\n",
)
.unwrap();
let spec = mk_spec_for_stage_processing();
process(&destdir, &spec).unwrap();
assert!(destdir.join("system/binaries/foo").exists());
assert!(destdir.join("system/headers/foo").is_dir());
assert!(
destdir
.join("system/documentation/man-pages/man1/foo.1")
.exists()
);
assert!(destdir.join("system/documentation/info/foo.info").exists());
let pc =
std::fs::read_to_string(destdir.join("system/libraries/pkgconfig/foo.pc")).unwrap();
assert!(pc.contains("prefix=/system"));
assert!(pc.contains("bindir=${prefix}/binaries"));
assert!(pc.contains("libdir=${prefix}/libraries"));
assert!(pc.contains("includedir=${prefix}/headers"));
assert!(pc.contains("mandir=${prefix}/documentation/man-pages"));
assert!(pc.contains("infodir=${prefix}/documentation/info"));
assert!(!destdir.join("usr").exists());
}
#[test]
fn process_normalizes_duplicate_identical_lbi_paths() {
let tmp = tempfile::tempdir().unwrap();
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(destdir.join("system/lib/clang/22/include")).unwrap();
std::fs::create_dir_all(destdir.join("system/libraries/clang/22/include")).unwrap();
std::fs::write(
destdir.join("system/lib/clang/22/include/builtins.h"),
"same header\n",
)
.unwrap();
std::fs::write(
destdir.join("system/libraries/clang/22/include/builtins.h"),
"same header\n",
)
.unwrap();
let spec = mk_spec_for_stage_processing();
process(&destdir, &spec).unwrap();
assert!(
!destdir
.join("system/lib/clang/22/include/builtins.h")
.exists()
);
assert_eq!(
std::fs::read_to_string(destdir.join("system/libraries/clang/22/include/builtins.h"))
.unwrap(),
"same header\n"
);
}
#[test]
fn process_rejects_conflicting_lbi_normalized_paths() {
let tmp = tempfile::tempdir().unwrap();
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(destdir.join("system/lib/clang/22/include")).unwrap();
std::fs::create_dir_all(destdir.join("system/libraries/clang/22/include")).unwrap();
std::fs::write(
destdir.join("system/lib/clang/22/include/builtins.h"),
"left header\n",
)
.unwrap();
std::fs::write(
destdir.join("system/libraries/clang/22/include/builtins.h"),
"right header\n",
)
.unwrap();
let spec = mk_spec_for_stage_processing();
let err = process(&destdir, &spec).unwrap_err();
assert!(
err.to_string().contains("destination already exists"),
"{err:#}"
);
}
#[test]
fn process_normalizes_split_output_usr_paths() {
let tmp = tempfile::tempdir().unwrap();
let destdir = tmp.path().join("dest");
let out = destdir.join(".depot/outputs/foo-devel/usr/lib/pkgconfig");
std::fs::create_dir_all(&out).unwrap();
std::fs::write(out.join("foo.pc"), "prefix=/usr\nlibdir=/usr/lib\n").unwrap();
let spec = mk_spec_for_stage_processing();
process(&destdir, &spec).unwrap();
let pc_path = destdir.join(".depot/outputs/foo-devel/system/libraries/pkgconfig/foo.pc");
assert!(pc_path.exists());
let pc = std::fs::read_to_string(pc_path).unwrap();
assert!(pc.contains("prefix=/system"));
assert!(pc.contains("libdir=/system/libraries"));
} }
#[test] #[test]
@@ -2428,6 +2793,34 @@ mod tests {
.contains(&".depot/outputs/clang/usr/bin/clang".to_string()) .contains(&".depot/outputs/clang/usr/bin/clang".to_string())
); );
} }
#[test]
fn generate_manifest_skips_bootstrap_chroot_destdir_mountpoint() {
let tmp = tempfile::tempdir().unwrap();
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(destdir.join("system/binaries")).unwrap();
std::fs::create_dir_all(destdir.join("destdir/system/documentation/xz")).unwrap();
std::fs::write(destdir.join("system/binaries/xz"), "ok").unwrap();
std::fs::write(
destdir.join("destdir/system/documentation/xz/README"),
"stale staging",
)
.unwrap();
let manifest = generate_manifest_with_dirs(&destdir).unwrap();
assert!(manifest.files.contains(&"system/binaries/xz".to_string()));
assert!(
!manifest
.files
.contains(&"destdir/system/documentation/xz/README".to_string())
);
assert!(
!manifest
.directories
.contains(&"destdir/system/documentation/xz".to_string())
);
}
} }
/// Manifest containing files and directories for a package /// Manifest containing files and directories for a package
+504
View File
@@ -0,0 +1,504 @@
use crate::config::Config;
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
const STATE_FILENAME: &str = "system.toml";
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct SystemState {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) stage: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) target: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) arch: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) layers: BTreeMap<String, Vec<String>>,
}
pub(crate) fn state_path(config: &Config) -> PathBuf {
config.db_dir.join(STATE_FILENAME)
}
pub(crate) fn load(config: &Config) -> Result<SystemState> {
let path = state_path(config);
if !path.exists() {
return Ok(SystemState::default());
}
let content =
fs::read_to_string(&path).with_context(|| format!("Failed to read {}", path.display()))?;
toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))
}
pub(crate) fn save(config: &Config, state: &SystemState) -> Result<PathBuf> {
let path = state_path(config);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
let content = toml::to_string_pretty(state).context("Failed to serialize system state")?;
fs::write(&path, content).with_context(|| format!("Failed to write {}", path.display()))?;
Ok(path)
}
pub(crate) fn set_stage(config: &Config, stage: String) -> Result<SystemState> {
let stage = normalize_token("stage", &stage)?;
let mut state = load(config)?;
state.stage = Some(stage);
save(config, &state)?;
Ok(state)
}
pub(crate) fn add_packages_to_layer(
config: &Config,
layer: String,
packages: &[String],
) -> Result<SystemState> {
let layer = normalize_token("layer", &layer)?;
let mut state = load(config)?;
let existing = state.layers.remove(&layer).unwrap_or_default();
let mut merged = existing.into_iter().collect::<BTreeSet<_>>();
for package in packages {
merged.insert(normalize_token("package", package)?);
}
state.layers.insert(layer, merged.into_iter().collect());
save(config, &state)?;
Ok(state)
}
pub(crate) fn set_layer_packages(
config: &Config,
layer: String,
packages: &[String],
) -> Result<SystemState> {
let layer = normalize_token("layer", &layer)?;
let mut state = load(config)?;
let mut seen = BTreeSet::new();
let mut normalized = Vec::new();
for package in packages {
let package = normalize_token("package", package)?;
if seen.insert(package.clone()) {
normalized.push(package);
}
}
state.layers.insert(layer, normalized);
save(config, &state)?;
Ok(state)
}
pub(crate) fn remove_packages_from_layer(
config: &Config,
layer: String,
packages: &[String],
) -> Result<SystemState> {
let layer = normalize_token("layer", &layer)?;
let remove = packages
.iter()
.map(|package| normalize_token("package", package))
.collect::<Result<BTreeSet<_>>>()?;
let mut state = load(config)?;
if let Some(existing) = state.layers.get_mut(&layer) {
existing.retain(|package| !remove.contains(package));
if existing.is_empty() {
state.layers.remove(&layer);
}
}
save(config, &state)?;
Ok(state)
}
pub(crate) fn init_lbi_layout(
rootfs: &Path,
config: &Config,
target: &str,
arch: Option<&str>,
force: bool,
) -> Result<SystemState> {
let target = normalize_token("target", target)?;
let arch = match arch {
Some(arch) => normalize_token("arch", arch)?,
None => crate::cross::target_arch_from_triple(&target).to_string(),
};
ensure_lbi_layout_paths(rootfs)?;
write_lbi_build_config(rootfs, &target, &arch, force)?;
let mut state = load(config)?;
state.target = Some(target);
state.arch = Some(arch);
state.stage.get_or_insert_with(|| "layout".to_string());
save(config, &state)?;
Ok(state)
}
pub(crate) fn ensure_lbi_layout_paths(rootfs: &Path) -> Result<()> {
create_lbi_directories(rootfs)?;
create_lbi_links(rootfs)?;
Ok(())
}
fn normalize_token(kind: &str, value: &str) -> Result<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
bail!("{kind} must not be empty");
}
if trimmed.contains('/') || trimmed.contains('\0') {
bail!("{kind} must not contain '/' or NUL bytes: {trimmed}");
}
Ok(trimmed.to_string())
}
fn create_lbi_directories(rootfs: &Path) -> Result<()> {
let dirs = [
"system",
"system/configuration",
"system/binaries",
"system/systembinaries",
"system/libraries",
"system/headers",
"system/share",
"system/documentation",
"system/documentation/man-pages",
"system/documentation/info",
"system/tools",
"system/variable",
"system/variable/lib",
"system/users",
"system/charlie",
"system/devices",
"system/devices/pts",
"system/devices/shm",
"system/processes",
"system/run",
"system/system",
"system/temporary",
"usr",
];
for dir in dirs {
let path = rootfs.join(dir);
fs::create_dir_all(&path)
.with_context(|| format!("Failed to create {}", path.display()))?;
}
Ok(())
}
fn create_lbi_links(rootfs: &Path) -> Result<()> {
let links = [
("etc", "system/configuration"),
("bin", "system/binaries"),
("sbin", "system/systembinaries"),
("lib", "system/libraries"),
("var", "system/variable"),
("home", "system/users"),
("root", "system/charlie"),
("dev", "system/devices"),
("proc", "system/processes"),
("run", "system/run"),
("sys", "system/system"),
("usr/bin", "../system/binaries"),
("usr/sbin", "../system/systembinaries"),
("usr/lib", "../system/libraries"),
("usr/include", "../system/headers"),
("usr/share", "../system/share"),
("system/lib", "libraries"),
];
for (link, target) in links {
ensure_relative_symlink(rootfs, Path::new(link), Path::new(target))?;
}
Ok(())
}
#[cfg(unix)]
fn ensure_relative_symlink(rootfs: &Path, link: &Path, target: &Path) -> Result<()> {
use std::os::unix::fs as unix_fs;
let link_path = rootfs.join(link);
if let Ok(metadata) = fs::symlink_metadata(&link_path) {
if !metadata.file_type().is_symlink() {
if metadata.is_dir() {
let target_path = link_path
.parent()
.unwrap_or(rootfs)
.join(target)
.components()
.collect::<PathBuf>();
merge_dir_contents(&link_path, &target_path).with_context(|| {
format!(
"Failed to merge existing directory {} into {}",
link_path.display(),
target_path.display()
)
})?;
fs::remove_dir(&link_path)
.with_context(|| format!("Failed to remove {}", link_path.display()))?;
} else {
bail!(
"Refusing to replace non-directory path while creating LBI layout: {}",
link_path.display()
);
}
} else {
let existing = fs::read_link(&link_path)
.with_context(|| format!("Failed to read symlink {}", link_path.display()))?;
if existing == target {
return Ok(());
}
fs::remove_file(&link_path)
.with_context(|| format!("Failed to replace symlink {}", link_path.display()))?;
}
}
if let Some(parent) = link_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
unix_fs::symlink(target, &link_path)
.with_context(|| format!("Failed to create symlink {}", link_path.display()))?;
Ok(())
}
fn merge_dir_contents(src: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let mut entries = fs::read_dir(src)
.with_context(|| format!("Failed to read {}", src.display()))?
.collect::<std::result::Result<Vec<_>, _>>()
.with_context(|| format!("Failed to read entry from {}", src.display()))?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
let source_path = entry.path();
let dest_path = dest.join(entry.file_name());
if dest_path.exists() {
let source_type = entry
.file_type()
.with_context(|| format!("Failed to inspect {}", source_path.display()))?;
let dest_type = fs::symlink_metadata(&dest_path)
.with_context(|| format!("Failed to inspect {}", dest_path.display()))?
.file_type();
if source_type.is_dir() && dest_type.is_dir() {
merge_dir_contents(&source_path, &dest_path)?;
fs::remove_dir(&source_path)
.with_context(|| format!("Failed to remove {}", source_path.display()))?;
continue;
}
bail!(
"Refusing to overwrite existing path while merging LBI directory: {}",
dest_path.display()
);
}
fs::rename(&source_path, &dest_path).with_context(|| {
format!(
"Failed to move {} to {}",
source_path.display(),
dest_path.display()
)
})?;
}
Ok(())
}
#[cfg(not(unix))]
fn ensure_relative_symlink(_rootfs: &Path, _link: &Path, _target: &Path) -> Result<()> {
bail!("LBI layout initialization requires Unix symlink support")
}
fn write_lbi_build_config(rootfs: &Path, target: &str, arch: &str, force: bool) -> Result<PathBuf> {
let path = rootfs.join("etc/depot.d/build.toml");
if path.exists() && !force {
bail!(
"{} already exists; re-run with --force to replace it",
path.display()
);
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}
let content = lbi_build_config_toml(target, arch);
fs::write(&path, content).with_context(|| format!("Failed to write {}", path.display()))?;
Ok(path)
}
fn lbi_build_config_toml(target: &str, arch: &str) -> String {
let makeflags = format!(
"-j{}",
std::thread::available_parallelism()
.map(|parallelism| parallelism.get())
.unwrap_or(1)
);
format!(
r#"# Generated by `depot system init-lbi`.
# These defaults mirror the Linux by Intent /system layout.
[flags]
prefix = "/system"
bindir = "/system/binaries"
sbindir = "/system/systembinaries"
libdir = "/system/libraries"
libexecdir = "/system/libraries"
sysconfdir = "/system/configuration"
localstatedir = "/system/variable"
sharedstatedir = "/system/variable/lib"
includedir = "/system/headers"
datarootdir = "/system/share"
datadir = "/system/share"
mandir = "/system/documentation/man-pages"
infodir = "/system/documentation/info"
cc = "clang"
cxx = "clang++"
ar = "llvm-ar"
ld = "ld.lld"
carch = "{arch}"
chost = "{target}"
target = "{target}"
cflags = ["-O2", "-pipe"]
cxxflags = ["-O2", "-pipe"]
ldflags = ["-Wl,-rpath,/system/libraries"]
makeflags = "{makeflags}"
"#,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layer_add_sorts_and_deduplicates_packages() {
let tmp = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(tmp.path());
let state = add_packages_to_layer(
&config,
"base".to_string(),
&["zlib".to_string(), "musl".to_string(), "zlib".to_string()],
)
.unwrap();
assert_eq!(state.layers["base"], vec!["musl", "zlib"]);
}
#[test]
fn set_layer_packages_replaces_only_named_layer() {
let tmp = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(tmp.path());
add_packages_to_layer(&config, "custom".to_string(), &["kept".to_string()]).unwrap();
add_packages_to_layer(
&config,
"base".to_string(),
&["old".to_string(), "zlib".to_string()],
)
.unwrap();
let state = set_layer_packages(
&config,
"base".to_string(),
&["zlib".to_string(), "musl".to_string(), "zlib".to_string()],
)
.unwrap();
assert_eq!(state.layers["base"], vec!["zlib", "musl"]);
assert_eq!(state.layers["custom"], vec!["kept"]);
}
#[test]
fn stage_is_persisted() {
let tmp = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(tmp.path());
set_stage(&config, "cross-tools".to_string()).unwrap();
let loaded = load(&config).unwrap();
assert_eq!(loaded.stage.as_deref(), Some("cross-tools"));
}
#[cfg(unix)]
#[test]
fn init_lbi_creates_layout_and_build_defaults() {
let tmp = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(tmp.path());
let state = init_lbi_layout(
tmp.path(),
&config,
"x86_64-unknown-linux-musl",
None,
false,
)
.unwrap();
assert_eq!(state.stage.as_deref(), Some("layout"));
assert_eq!(state.arch.as_deref(), Some("x86_64"));
assert!(tmp.path().join("system/binaries").is_dir());
assert!(tmp.path().join("system/devices/pts").is_dir());
assert!(tmp.path().join("system/devices/shm").is_dir());
assert_eq!(
fs::read_link(tmp.path().join("usr/bin")).unwrap(),
PathBuf::from("../system/binaries")
);
assert_eq!(
fs::read_link(tmp.path().join("dev")).unwrap(),
PathBuf::from("system/devices")
);
assert_eq!(
fs::read_link(tmp.path().join("proc")).unwrap(),
PathBuf::from("system/processes")
);
assert_eq!(
fs::read_link(tmp.path().join("sys")).unwrap(),
PathBuf::from("system/system")
);
assert_eq!(
fs::read_link(tmp.path().join("run")).unwrap(),
PathBuf::from("system/run")
);
let build_toml = fs::read_to_string(tmp.path().join("etc/depot.d/build.toml")).unwrap();
assert!(build_toml.contains("prefix = \"/system\""));
assert!(build_toml.contains("chost = \"x86_64-unknown-linux-musl\""));
let makeflags_line = build_toml
.lines()
.find(|line| line.trim_start().starts_with("makeflags = \"-j"))
.expect("expected makeflags default in generated build.toml");
let jobs = makeflags_line
.trim()
.trim_start_matches("makeflags = \"-j")
.trim_end_matches('"')
.parse::<usize>()
.expect("expected numeric makeflags job count");
assert!(jobs >= 1);
}
#[cfg(unix)]
#[test]
fn ensure_lbi_layout_paths_reconciles_existing_usr_include_directory() {
let tmp = tempfile::tempdir().unwrap();
fs::create_dir_all(tmp.path().join("usr/include")).unwrap();
fs::write(tmp.path().join("usr/include/marker.h"), "/* marker */").unwrap();
ensure_lbi_layout_paths(tmp.path()).unwrap();
assert_eq!(
fs::read_link(tmp.path().join("usr/include")).unwrap(),
PathBuf::from("../system/headers")
);
assert!(tmp.path().join("system/headers/marker.h").exists());
}
#[cfg(unix)]
#[test]
fn init_lbi_migrates_existing_var_contents_before_linking() {
let tmp = tempfile::tempdir().unwrap();
fs::create_dir_all(tmp.path().join("var/lib/depot")).unwrap();
fs::write(tmp.path().join("var/lib/depot/lock"), "").unwrap();
let config = Config::for_rootfs(tmp.path());
init_lbi_layout(
tmp.path(),
&config,
"x86_64-unknown-linux-musl",
None,
false,
)
.unwrap();
assert_eq!(
fs::read_link(tmp.path().join("var")).unwrap(),
PathBuf::from("system/variable")
);
assert!(tmp.path().join("system/variable/lib/depot/lock").exists());
}
}
+10
View File
@@ -50,6 +50,16 @@ pub fn success(message: impl AsRef<str>) {
println!("{} {}", label(Stream::Stdout, "OK", "32"), message.as_ref()); println!("{} {}", label(Stream::Stdout, "OK", "32"), message.as_ref());
} }
pub fn merge_package(layer: &str, package: &str) {
println!(
"{} {} {} into layer {}",
paint(Stream::Stdout, ">>>", "32;1"),
paint(Stream::Stdout, "merging package", "36;1"),
paint(Stream::Stdout, package, "32;1"),
layer
);
}
pub fn warn(message: impl AsRef<str>) { pub fn warn(message: impl AsRef<str>) {
eprintln!( eprintln!(
"{} {}", "{} {}",