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:
Generated
+383
-64
@@ -8,6 +8,15 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
@@ -15,10 +24,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cipher 0.4.4",
|
||||
"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]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -175,6 +195,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
|
||||
dependencies = [
|
||||
"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]]
|
||||
@@ -183,6 +213,12 @@ version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -198,6 +234,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "cc"
|
||||
version = "1.2.56"
|
||||
@@ -210,6 +255,12 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cff-parser"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31f5b6e9141c036f3ff4ce7b2f7e432b0f00dee416ddcd4f17741d189ddc2e9d"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -240,14 +291,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"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]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -267,18 +328,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.6.0"
|
||||
version = "4.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
|
||||
checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -302,6 +363,12 @@ dependencies = [
|
||||
"roff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
@@ -364,6 +431,12 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpubits"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -443,6 +516,15 @@ version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.11"
|
||||
@@ -471,13 +553,14 @@ dependencies = [
|
||||
"md5",
|
||||
"minisign",
|
||||
"nix",
|
||||
"pdf-extract",
|
||||
"petgraph",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_ignored",
|
||||
"sha1 0.11.0",
|
||||
"sha1",
|
||||
"sha2 0.11.0",
|
||||
"signal-hook 0.4.4",
|
||||
"suppaftp",
|
||||
@@ -546,6 +629,8 @@ dependencies = [
|
||||
"block-buffer 0.12.0",
|
||||
"const-oid",
|
||||
"crypto-common 0.2.1",
|
||||
"ctutils",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -574,6 +659,15 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
@@ -605,6 +699,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
@@ -646,13 +749,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -885,6 +987,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -1156,9 +1267,19 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"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]]
|
||||
name = "inquire"
|
||||
version = "0.9.4"
|
||||
@@ -1258,9 +1379,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.182"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
@@ -1276,17 +1397,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.37.0"
|
||||
@@ -1357,10 +1467,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.13.0"
|
||||
name = "lopdf"
|
||||
version = "0.38.0"
|
||||
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 = [
|
||||
"twox-hash",
|
||||
]
|
||||
@@ -1385,6 +1523,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "md5"
|
||||
version = "0.8.0"
|
||||
@@ -1450,9 +1598,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.2"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
@@ -1466,6 +1614,26 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "num-conv"
|
||||
version = "0.2.0"
|
||||
@@ -1561,7 +1729,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.5.18",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
@@ -1573,7 +1741,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"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]]
|
||||
@@ -1612,12 +1807,24 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "pom"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "postscript"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
@@ -1639,6 +1846,15 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -1673,6 +1889,41 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -1682,15 +1933,6 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
@@ -1722,9 +1964,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
||||
checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
@@ -1850,7 +2092,7 @@ version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
"cipher 0.4.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1883,7 +2125,7 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||
dependencies = [
|
||||
"pbkdf2",
|
||||
"pbkdf2 0.12.2",
|
||||
"salsa20",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
@@ -1979,17 +2221,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "sha1"
|
||||
version = "0.11.0"
|
||||
@@ -2127,6 +2358,17 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -2141,9 +2383,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "suppaftp"
|
||||
version = "8.0.2"
|
||||
version = "8.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d3da253d7e9993de86df41eb89e8cb1b6f567abe215798645651fca4148d0aa"
|
||||
checksum = "4275c142b5be3af2eeadd70dd368caf3b65546c8af1035839372dd7a1436127d"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures-lite",
|
||||
@@ -2311,6 +2553,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tokio"
|
||||
version = "1.49.0"
|
||||
@@ -2456,12 +2713,27 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "typed-path"
|
||||
version = "0.12.3"
|
||||
@@ -2474,12 +2746,33 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
@@ -2696,6 +2989,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -3104,6 +3403,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
@@ -3166,24 +3485,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "8.5.0"
|
||||
version = "8.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2726508a48f38dceb22b35ecbbd2430efe34ff05c62bd3285f965d7911b33464"
|
||||
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes 0.9.0",
|
||||
"bzip2",
|
||||
"constant_time_eq 0.4.2",
|
||||
"crc32fast",
|
||||
"deflate64",
|
||||
"flate2",
|
||||
"getrandom 0.4.1",
|
||||
"hmac",
|
||||
"hmac 0.13.0",
|
||||
"indexmap",
|
||||
"lzma-rust2",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"pbkdf2 0.13.0",
|
||||
"ppmd-rust",
|
||||
"sha1 0.10.6",
|
||||
"sha1",
|
||||
"time",
|
||||
"typed-path",
|
||||
"zeroize",
|
||||
|
||||
+5
-4
@@ -14,7 +14,7 @@ clap = { version = "4.6.0", features = ["derive"] }
|
||||
flate2 = "1.1.9"
|
||||
git2 = "0.20.4"
|
||||
indicatif = "0.18.4"
|
||||
nix = { version = "0.31.2", features = ["user"] }
|
||||
nix = { version = "0.31.3", features = ["user"] }
|
||||
rusqlite = "0.39.0"
|
||||
semver = "1.0.28"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
@@ -36,17 +36,18 @@ minisign = "0.9.1"
|
||||
petgraph = "0.8.3"
|
||||
fd-lock = "4.0.4"
|
||||
reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "native-tls"] }
|
||||
filetime = "0.2.27"
|
||||
clap_complete = "4.6.0"
|
||||
filetime = "0.2.29"
|
||||
clap_complete = "4.6.5"
|
||||
clap_mangen = "0.3.0"
|
||||
sys-mount = { version = "3.1.0", default-features = false }
|
||||
time = { version = "0.3.47", features = ["formatting", "parsing"] }
|
||||
b2sum-rust = "0.3.0"
|
||||
serde_ignored = "0.1.14"
|
||||
lz4_flex = "0.13.0"
|
||||
lz4_flex = "0.13.1"
|
||||
lzma-rust2 = "0.16.2"
|
||||
signal-hook = "0.4.4"
|
||||
sha1 = "0.11.0"
|
||||
pdf-extract = "0.10.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "=3.27.0"
|
||||
|
||||
+6700
File diff suppressed because it is too large
Load Diff
+45
-6
@@ -54,14 +54,12 @@ pub fn build(
|
||||
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));
|
||||
|
||||
// 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");
|
||||
|
||||
// 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");
|
||||
if !build_script.exists() && spec_build.exists() {
|
||||
if spec_build.exists() {
|
||||
fs::create_dir_all(src_dir)?;
|
||||
fs::copy(&spec_build, &build_script).with_context(|| {
|
||||
format!(
|
||||
@@ -477,6 +475,47 @@ mod tests {
|
||||
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]
|
||||
fn test_build_function_mode_uses_per_output_destdirs() -> Result<()> {
|
||||
let tmp_src = tempdir()?;
|
||||
|
||||
+105
@@ -144,6 +144,30 @@ pub struct BuildArgs {
|
||||
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)]
|
||||
pub struct UpdateArgs {
|
||||
#[command(flatten)]
|
||||
@@ -226,6 +250,70 @@ pub struct ConfigArgs {
|
||||
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)]
|
||||
pub struct GenerateArtifactsArgs {
|
||||
/// Output directory for generated files
|
||||
@@ -265,6 +353,8 @@ pub enum Commands {
|
||||
Remove(RemoveArgs),
|
||||
/// Build a package without installing
|
||||
Build(BuildArgs),
|
||||
/// Parse the Linux by Intent book and populate temp/base/devel layers
|
||||
Bootstrap(BootstrapArgs),
|
||||
/// Update installed packages from configured repositories
|
||||
Update(UpdateArgs),
|
||||
/// Scan package specs for upstream version updates
|
||||
@@ -283,6 +373,8 @@ pub enum Commands {
|
||||
Repo(RepoArgs),
|
||||
/// Show current configuration
|
||||
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.
|
||||
#[command(hide = true)]
|
||||
GenerateArtifacts(GenerateArtifactsArgs),
|
||||
@@ -317,6 +409,19 @@ pub enum InternalCommands {
|
||||
#[command(hide = true)]
|
||||
Clone { repo: String, dest: Option<PathBuf> },
|
||||
#[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 {
|
||||
#[arg(value_name = "ARG", num_args = 0.., allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
|
||||
+43
-9
@@ -1,10 +1,11 @@
|
||||
use crate::cli::{
|
||||
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::{
|
||||
builder, cli_assets, config, cross, db, deps, index, install, locking, package, planner,
|
||||
signing, source, staging, ui,
|
||||
bootstrap, builder, cli_assets, config, cross, db, deps, index, install, locking, package,
|
||||
planner, signing, source, staging, ui,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use git2::Direction;
|
||||
@@ -19,10 +20,11 @@ use url::Url;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use build_cmd::support::{
|
||||
automatic_tests_disabled_for_outputs, build_lib32_companion_package, clean_build_workspace,
|
||||
effective_lib32_only, ensure_requested_development_package_installed, make_lib32_package_spec,
|
||||
maybe_disable_tests_for_missing_deps, maybe_prompt_to_skip_tests_for_missing_requested_deps,
|
||||
merge_missing_dependencies, requested_outputs, should_install_test_deps,
|
||||
automatic_tests_disabled_for_outputs, build_lib32_companion_package, clean_build_source_dirs,
|
||||
clean_build_workspace, effective_lib32_only, ensure_requested_development_package_installed,
|
||||
make_lib32_package_spec, maybe_disable_tests_for_missing_deps,
|
||||
maybe_prompt_to_skip_tests_for_missing_requested_deps, merge_missing_dependencies,
|
||||
requested_outputs, should_install_test_deps,
|
||||
};
|
||||
use install_cmd::archive::{
|
||||
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::Remove(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::Info(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::Repo(args) => Some(repo_command_rootfs(&args.command)),
|
||||
Commands::Config(args) => Some(&args.rootfs_args.rootfs),
|
||||
Commands::System(args) => Some(&args.rootfs_args.rootfs),
|
||||
Commands::Check(_)
|
||||
| Commands::Convert(_)
|
||||
| Commands::GenerateArtifacts(_)
|
||||
@@ -110,6 +114,7 @@ fn command_assume_yes(command: &Commands) -> bool {
|
||||
Commands::Install(args) => args.prompt_args.yes,
|
||||
Commands::Remove(args) => args.prompt_args.yes,
|
||||
Commands::Build(args) => args.prompt_args.yes,
|
||||
Commands::Bootstrap(_) => false,
|
||||
Commands::Update(args) => args.prompt_args.yes,
|
||||
Commands::Check(_)
|
||||
| Commands::Convert(_)
|
||||
@@ -120,6 +125,7 @@ fn command_assume_yes(command: &Commands) -> bool {
|
||||
| Commands::Sign(_)
|
||||
| Commands::Repo(_)
|
||||
| Commands::Config(_)
|
||||
| Commands::System(_)
|
||||
| Commands::GenerateArtifacts(_)
|
||||
| Commands::MakeSpec(_)
|
||||
| Commands::Internal(_) => false,
|
||||
@@ -913,7 +919,7 @@ fn collect_installed_replacement_packages(
|
||||
Ok(replacements)
|
||||
}
|
||||
|
||||
fn remove_installed_package_with_hooks(
|
||||
pub(crate) fn remove_installed_package_with_hooks(
|
||||
package: &str,
|
||||
rootfs: &Path,
|
||||
config: &config::Config,
|
||||
@@ -2054,6 +2060,27 @@ fn run_direct_archive_install_requests(
|
||||
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(
|
||||
options: DirectInstallOptions<'_>,
|
||||
config: &config::Config,
|
||||
@@ -2142,6 +2169,8 @@ fn run_direct_install_request(
|
||||
pkg_spec.apply_config(config);
|
||||
(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() {
|
||||
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()
|
||||
} else {
|
||||
// 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)?;
|
||||
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
|
||||
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::Build(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::Info(_)
|
||||
| Commands::Search(_)
|
||||
@@ -2560,6 +2591,7 @@ pub fn run(cli: Cli) -> Result<()> {
|
||||
| Commands::Sign(_)
|
||||
| Commands::Repo(_)
|
||||
| Commands::Config(_)
|
||||
| Commands::System(_)
|
||||
| Commands::GenerateArtifacts(_)
|
||||
| Commands::Convert(_)
|
||||
| Commands::MakeSpec(_)
|
||||
@@ -2570,6 +2602,7 @@ pub fn run(cli: Cli) -> Result<()> {
|
||||
Commands::Install(args) => install_cmd::run_install(args, cli_test_deps)?,
|
||||
Commands::Remove(args) => install_cmd::run_remove(args)?,
|
||||
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::Check(args) => check::run_check(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::GenerateArtifacts(args) => misc::run_generate_artifacts(args)?,
|
||||
Commands::Config(args) => misc::run_config(args)?,
|
||||
Commands::System(args) => misc::run_system(args)?,
|
||||
Commands::MakeSpec(args) => misc::run_make_spec(args)?,
|
||||
Commands::Convert(args) => misc::run_convert(args)?,
|
||||
Commands::Internal(args) => misc::run_internal(args)?,
|
||||
|
||||
@@ -4,11 +4,11 @@ pub(crate) mod support;
|
||||
|
||||
use self::support::{
|
||||
RequestedBuildToolPackageInstall, automatic_tests_disabled_for_outputs,
|
||||
build_lib32_companion_package, clean_build_workspace, effective_lib32_only,
|
||||
ensure_requested_build_tool_package_installed, ensure_requested_development_package_installed,
|
||||
maybe_disable_tests_for_missing_deps, maybe_prompt_to_skip_tests_for_missing_requested_deps,
|
||||
merge_missing_dependencies, requested_outputs, should_install_test_deps,
|
||||
warn_if_running_as_root_for_build,
|
||||
build_lib32_companion_package, clean_build_source_dirs, clean_build_workspace,
|
||||
effective_lib32_only, ensure_requested_build_tool_package_installed,
|
||||
ensure_requested_development_package_installed, maybe_disable_tests_for_missing_deps,
|
||||
maybe_prompt_to_skip_tests_for_missing_requested_deps, merge_missing_dependencies,
|
||||
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<()> {
|
||||
@@ -303,6 +303,7 @@ pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
|
||||
watcher.check()?;
|
||||
}
|
||||
|
||||
clean_build_source_dirs(&config)?;
|
||||
source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?;
|
||||
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
|
||||
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,
|
||||
)?;
|
||||
}
|
||||
clean_build_source_dirs(&config)?;
|
||||
if clean {
|
||||
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 {
|
||||
anyhow::bail!("Build interrupted by Ctrl-C");
|
||||
}
|
||||
|
||||
@@ -115,6 +115,22 @@ pub(crate) fn clean_build_workspace(config: &config::Config) -> Result<()> {
|
||||
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) {
|
||||
if crate::fakeroot::is_root() {
|
||||
ui::warn(format!("Running '{}' as root is discouraged.", command));
|
||||
|
||||
@@ -112,6 +112,117 @@ pub(super) fn run_config(args: ConfigArgs) -> Result<()> {
|
||||
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<()> {
|
||||
let output = args.output;
|
||||
let spec = package::create_interactive()?;
|
||||
|
||||
@@ -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 } => {
|
||||
let env_vars = current_process_env_vars();
|
||||
let context = current_build_helper_context()?;
|
||||
|
||||
@@ -282,6 +282,42 @@ fn clean_build_workspace_noops_when_dirs_are_missing() -> Result<()> {
|
||||
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]
|
||||
fn binary_install_path_uses_repo_record_metadata_without_archive_metadata() -> Result<()> {
|
||||
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
|
||||
|
||||
+34
-1
@@ -41,20 +41,27 @@ impl CrossConfig {
|
||||
let cc = find_tool(prefix, &["gcc", "clang"], true)?;
|
||||
let cxx = find_tool(prefix, &["g++", "clang++"], false)
|
||||
.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)
|
||||
.or_else(|_| find_unprefixed_tool(&["llvm-ranlib", "llvm-ar"], false))
|
||||
.unwrap_or_else(|_| format!("{}-ranlib", prefix));
|
||||
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));
|
||||
let ld = find_tool(prefix, &["ld", "ld.lld"], false)
|
||||
.unwrap_or_else(|_| format!("{}-ld", prefix));
|
||||
let nm = find_tool(prefix, &["nm", "llvm-nm"], false)
|
||||
.or_else(|_| find_unprefixed_tool(&["llvm-nm"], false))
|
||||
.unwrap_or_else(|_| format!("{}-nm", prefix));
|
||||
let objcopy = find_tool(prefix, &["objcopy", "llvm-objcopy"], false)
|
||||
.or_else(|_| find_unprefixed_tool(&["llvm-objcopy"], false))
|
||||
.unwrap_or_else(|_| format!("{}-objcopy", prefix));
|
||||
let objdump = find_tool(prefix, &["objdump", "llvm-objdump"], false)
|
||||
.or_else(|_| find_unprefixed_tool(&["llvm-objdump"], false))
|
||||
.unwrap_or_else(|_| format!("{}-objdump", prefix));
|
||||
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));
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use super::lib32_target_triple;
|
||||
|
||||
+84
-1
@@ -10,6 +10,8 @@ use std::fs;
|
||||
use std::path::Path;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS: &str = "DEPOT_BOOTSTRAP_IGNORE_SBASE_CONFLICTS";
|
||||
|
||||
fn format_licenses(licenses: &[String]) -> String {
|
||||
licenses.join(", ")
|
||||
}
|
||||
@@ -18,6 +20,14 @@ fn verbose_remove_output() -> bool {
|
||||
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.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InstalledPackageRecord {
|
||||
@@ -213,7 +223,7 @@ pub fn register_package_with_replacement(
|
||||
if let Ok(owner) = owner_res
|
||||
&& owner != spec.package.name
|
||||
{
|
||||
if is_auto_removable_path(file) {
|
||||
if should_auto_clear_conflict(&owner, file) {
|
||||
auto_conflicts.push((file.clone(), owner));
|
||||
} else {
|
||||
fatal_conflicts.push((file.clone(), owner));
|
||||
@@ -241,6 +251,15 @@ pub fn register_package_with_replacement(
|
||||
)?;
|
||||
|
||||
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);
|
||||
if disk_path.exists() {
|
||||
let _ = std::fs::remove_file(&disk_path);
|
||||
@@ -1056,6 +1075,7 @@ mod tests {
|
||||
use crate::package::{
|
||||
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
|
||||
};
|
||||
use crate::test_support::TestEnv;
|
||||
use std::path::PathBuf;
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn get_package_files_missing_package_returns_empty() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Depot - Not Your Average Package Manager
|
||||
//! A source-based package manager for Linux
|
||||
|
||||
mod bootstrap;
|
||||
mod build_options;
|
||||
mod builder;
|
||||
mod cli;
|
||||
@@ -24,6 +25,7 @@ mod shell_helpers;
|
||||
mod signing;
|
||||
mod source;
|
||||
mod staging;
|
||||
mod system_state;
|
||||
#[cfg(test)]
|
||||
mod test_support;
|
||||
mod ui;
|
||||
|
||||
+309
-244
@@ -8,9 +8,17 @@ use std::collections::HashSet;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
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 {
|
||||
matches!(scheme, "http" | "https")
|
||||
@@ -32,10 +40,6 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
||||
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);
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
@@ -46,256 +50,46 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
||||
.progress_chars("#>-"),
|
||||
);
|
||||
|
||||
// If this is an FTP URL, fetch via suppaftp and skip the HTTP client path.
|
||||
if parsed_url.scheme() == "ftp" {
|
||||
// Connect and login (anonymous fallback)
|
||||
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"
|
||||
let candidate_urls = archive_fetch_candidates(&url);
|
||||
let mut attempt_errors = Vec::new();
|
||||
|
||||
for (index, candidate_url) in candidate_urls.iter().enumerate() {
|
||||
if index == 0 {
|
||||
crate::log_info!("Fetching: {}", candidate_url);
|
||||
} 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))?;
|
||||
|
||||
// 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()
|
||||
);
|
||||
crate::log_info!("Primary source failed; trying fallback: {}", candidate_url);
|
||||
}
|
||||
|
||||
// Download with progress bar
|
||||
// Use a sensible default User-Agent so servers that reject empty/unknown agents (e.g. IANA)
|
||||
// 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);
|
||||
pb.set_position(0);
|
||||
pb.set_length(0);
|
||||
fs::remove_file(&dest_path).ok();
|
||||
|
||||
// If mirror URL is FTP -> use suppaftp; otherwise use HTTP retry.
|
||||
if let Ok(alt_url) = Url::parse(&alt) {
|
||||
if alt_url.scheme() == "ftp" {
|
||||
// FTP mirror retrieval
|
||||
let host = alt_url.host_str().context("FTP mirror URL missing host")?;
|
||||
let port = alt_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 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)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
} 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
|
||||
match fetch_archive_from_candidate(candidate_url, &dest_path, &filename, &pb) {
|
||||
Ok(()) => {
|
||||
if !verify_checksum(&dest_path, &source.sha256)? {
|
||||
fs::remove_file(&dest_path)?;
|
||||
bail!("Checksum verification failed for {}", filename);
|
||||
attempt_errors.push(format!(
|
||||
"{}: checksum verification failed for {}",
|
||||
candidate_url, filename
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
crate::log_info!("Checksum verified: {}", filename);
|
||||
Ok(dest_path)
|
||||
return Ok(dest_path);
|
||||
}
|
||||
Err(err) => {
|
||||
attempt_errors.push(format!("{}: {err:#}", candidate_url));
|
||||
fs::remove_file(&dest_path).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!(
|
||||
"Failed to fetch {} from any candidate URL:\n{}",
|
||||
filename,
|
||||
attempt_errors.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
/// 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])
|
||||
}
|
||||
|
||||
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(¤t_url, dest_path, pb)?;
|
||||
pb.finish_with_message("Download complete");
|
||||
|
||||
let Some(next_alt) = validate_downloaded_archive(dest_path, filename, ¤t_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
|
||||
/// archive format (avoids saving HTML pages or other unexpected content).
|
||||
fn validate_downloaded_archive(
|
||||
@@ -647,6 +673,45 @@ mod tests {
|
||||
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]
|
||||
fn sourceforge_html_no_link_falls_back_to_download_suffix() {
|
||||
use std::io::Write;
|
||||
|
||||
+409
-16
@@ -19,9 +19,12 @@ pub const INTERNAL_DEPOT_DIR: &str = ".depot";
|
||||
pub const INTERNAL_OUTPUTS_DIR: &str = ".depot/outputs";
|
||||
|
||||
fn is_info_dir_index_path(rel_path: &str) -> bool {
|
||||
matches!(rel_path, "usr/info/dir" | "usr/share/info/dir")
|
||||
|| rel_path.starts_with("usr/info/dir.")
|
||||
matches!(
|
||||
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("system/documentation/info/dir.")
|
||||
}
|
||||
|
||||
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.strip_prefix(INTERNAL_DEPOT_DIR)
|
||||
.is_some_and(|rest| rest.starts_with('/'))
|
||||
|| p == "destdir"
|
||||
|| p.starts_with("destdir/")
|
||||
|| p == "scripts"
|
||||
|| p.starts_with("scripts/")
|
||||
|| is_purged_payload_path(p)
|
||||
@@ -340,13 +345,27 @@ fn move_tree_preserving_layout(src: &Path, dst: &Path) -> Result<()> {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||
}
|
||||
if dst.symlink_metadata().is_ok() {
|
||||
match dst.symlink_metadata() {
|
||||
Ok(dst_metadata)
|
||||
if duplicate_staged_path_is_equivalent(src, &metadata, dst, &dst_metadata)? =>
|
||||
{
|
||||
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(|| {
|
||||
format!(
|
||||
"Failed to move documentation path {} -> {}",
|
||||
@@ -359,6 +378,59 @@ fn move_tree_preserving_layout(src: &Path, dst: &Path) -> Result<()> {
|
||||
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(
|
||||
output_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(())
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
/// Copies common license file patterns from the source directory root into:
|
||||
@@ -1539,9 +1784,9 @@ mod tests {
|
||||
let spec = mk_spec_for_stage_processing();
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
assert!(!destdir.join("usr/lib/libfoo.a").exists());
|
||||
assert!(!destdir.join("usr/lib/libfoo.la").exists());
|
||||
assert!(destdir.join("usr/lib/libfoo.so").exists());
|
||||
assert!(!destdir.join("system/libraries/libfoo.a").exists());
|
||||
assert!(!destdir.join("system/libraries/libfoo.la").exists());
|
||||
assert!(destdir.join("system/libraries/libfoo.so").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1556,8 +1801,8 @@ mod tests {
|
||||
spec.build.flags.no_delete_static = true;
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
assert!(destdir.join("usr/lib/libfoo.a").exists());
|
||||
assert!(!destdir.join("usr/lib/libfoo.la").exists());
|
||||
assert!(destdir.join("system/libraries/libfoo.a").exists());
|
||||
assert!(!destdir.join("system/libraries/libfoo.la").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1580,18 +1825,18 @@ mod tests {
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
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!(
|
||||
docs_destdir
|
||||
.join("usr/share/gtk-doc/html/foo/index.html")
|
||||
.join("system/share/gtk-doc/html/foo/index.html")
|
||||
.exists()
|
||||
);
|
||||
assert!(docs_destdir.join("opt/foo-docs/guide.txt").exists());
|
||||
assert!(destdir.join("usr/bin/foo").exists());
|
||||
assert!(!destdir.join("usr/share/doc/foo/README").exists());
|
||||
assert!(destdir.join("system/binaries/foo").exists());
|
||||
assert!(!destdir.join("system/share/doc/foo/README").exists());
|
||||
assert!(
|
||||
!destdir
|
||||
.join("usr/share/gtk-doc/html/foo/index.html")
|
||||
.join("system/share/gtk-doc/html/foo/index.html")
|
||||
.exists()
|
||||
);
|
||||
assert!(!destdir.join("opt/foo-docs/guide.txt").exists());
|
||||
@@ -1623,9 +1868,129 @@ mod tests {
|
||||
process(&destdir, &spec).unwrap();
|
||||
|
||||
let docs_destdir = output_staging_dir(&destdir, "foo-dev-docs");
|
||||
assert!(docs_destdir.join("usr/share/doc/foo-dev/README").exists());
|
||||
assert!(dev_destdir.join("usr/include/foo.h").exists());
|
||||
assert!(!dev_destdir.join("usr/share/doc/foo-dev/README").exists());
|
||||
assert!(
|
||||
docs_destdir
|
||||
.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]
|
||||
@@ -2428,6 +2793,34 @@ mod tests {
|
||||
.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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,16 @@ pub fn success(message: impl AsRef<str>) {
|
||||
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>) {
|
||||
eprintln!(
|
||||
"{} {}",
|
||||
|
||||
Reference in New Issue
Block a user