Refactor and enhance source management and installation processes
- Improved error handling and context messages in `checkout` function in `git.rs`. - Updated license field in package specification to use a vector in `hooks.rs`. - Enhanced `copy_manual_sources` function in `mod.rs` to support both local file and remote URL sources, including checksum verification. - Added new utility functions for path normalization and skipping installation of specific files in `staging/mod.rs`. - Implemented logic to handle existing files during installation, allowing for `.depotnew` suffix for kept files. - Added tests for manual source copying, installation behavior, and path validation. - Introduced a new LICENSE file with MIT License terms.
This commit is contained in:
Generated
+171
-145
@@ -21,11 +21,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.5.3"
|
version = "1.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr 0.1.11",
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -80,9 +89,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.101"
|
version = "1.0.102"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ar"
|
name = "ar"
|
||||||
@@ -204,12 +213,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.2.25"
|
version = "0.4.43"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00"
|
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num",
|
"iana-time-zone",
|
||||||
"time 0.1.45",
|
"num-traits",
|
||||||
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -224,9 +234,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.59"
|
version = "4.5.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499"
|
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -234,9 +244,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.59"
|
version = "4.5.60"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24"
|
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -284,7 +294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"memchr 2.7.6",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -389,7 +399,7 @@ dependencies = [
|
|||||||
"rustix",
|
"rustix",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-mio",
|
"signal-hook-mio",
|
||||||
"winapi 0.3.9",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -398,7 +408,7 @@ version = "0.9.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi 0.3.9",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -426,17 +436,19 @@ dependencies = [
|
|||||||
"bzip2",
|
"bzip2",
|
||||||
"clap",
|
"clap",
|
||||||
"flate2",
|
"flate2",
|
||||||
"ftp",
|
|
||||||
"git2",
|
"git2",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"inquire",
|
"inquire",
|
||||||
|
"libz-sys",
|
||||||
"md5",
|
"md5",
|
||||||
"nix",
|
"nix",
|
||||||
|
"openssl-sys",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"suppaftp",
|
||||||
"tar",
|
"tar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -634,17 +646,6 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ftp"
|
|
||||||
version = "3.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "542951aad0071952c27409e3bd7cb62d1a3ad419c4e7314106bf994e0083ad5d"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"lazy_static",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -667,6 +668,19 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-lite"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -689,7 +703,7 @@ dependencies = [
|
|||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr 2.7.6",
|
"memchr",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
"slab",
|
"slab",
|
||||||
@@ -701,7 +715,7 @@ version = "0.3.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thread_local 1.1.9",
|
"thread_local",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -723,7 +737,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -935,6 +949,30 @@ dependencies = [
|
|||||||
"windows-registry",
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -1103,7 +1141,7 @@ version = "0.7.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr 2.7.6",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1162,20 +1200,27 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kernel32-sys"
|
name = "lazy-regex"
|
||||||
version = "0.2.2"
|
version = "3.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winapi 0.2.8",
|
"lazy-regex-proc_macros",
|
||||||
"winapi-build",
|
"once_cell",
|
||||||
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy-regex-proc_macros"
|
||||||
version = "0.1.16"
|
version = "3.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417"
|
checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leb128fmt"
|
name = "leb128fmt"
|
||||||
@@ -1226,6 +1271,7 @@ version = "0.36.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
|
checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
@@ -1322,15 +1368,6 @@ version = "0.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
|
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memchr"
|
|
||||||
version = "0.1.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.6"
|
version = "2.7.6"
|
||||||
@@ -1361,7 +1398,7 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1377,43 +1414,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num"
|
|
||||||
version = "0.1.43"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c9bdb1fb680e609c2e0930c1866cafdd0be7e7c7a1ecf92aec71ed8d99d3e133"
|
|
||||||
dependencies = [
|
|
||||||
"num-integer",
|
|
||||||
"num-iter",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-integer"
|
|
||||||
version = "0.1.46"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-iter"
|
|
||||||
version = "0.1.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -1447,6 +1453,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391"
|
checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-src"
|
||||||
|
version = "300.5.5+3.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.111"
|
version = "0.9.111"
|
||||||
@@ -1455,10 +1470,17 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -1691,22 +1713,32 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "0.1.80"
|
version = "1.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f"
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr 0.1.11",
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
"thread_local 0.2.7",
|
|
||||||
"utf8-ranges",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.3.9"
|
version = "0.8.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957"
|
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
@@ -1976,7 +2008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr 2.7.6",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"zmij",
|
"zmij",
|
||||||
@@ -2109,6 +2141,19 @@ version = "2.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "suppaftp"
|
||||||
|
version = "8.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d3da253d7e9993de86df41eb89e8cb1b6f567abe215798645651fca4148d0aa"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"futures-lite",
|
||||||
|
"lazy-regex",
|
||||||
|
"log",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.114"
|
version = "2.0.114"
|
||||||
@@ -2225,25 +2270,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thread-id"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03"
|
|
||||||
dependencies = [
|
|
||||||
"kernel32-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thread_local"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5"
|
|
||||||
dependencies = [
|
|
||||||
"thread-id",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
@@ -2253,17 +2279,6 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time"
|
|
||||||
version = "0.1.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.47"
|
version = "0.3.47"
|
||||||
@@ -2515,12 +2530,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8-ranges"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -2564,12 +2573,6 @@ dependencies = [
|
|||||||
"try-lock",
|
"try-lock",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasi"
|
|
||||||
version = "0.10.0+wasi-snapshot-preview1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
@@ -2715,12 +2718,6 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi"
|
|
||||||
version = "0.2.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -2731,12 +2728,6 @@ dependencies = [
|
|||||||
"winapi-x86_64-pc-windows-gnu",
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-build"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-i686-pc-windows-gnu"
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -2758,6 +2749,41 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -3284,11 +3310,11 @@ dependencies = [
|
|||||||
"hmac",
|
"hmac",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lzma-rust2",
|
"lzma-rust2",
|
||||||
"memchr 2.7.6",
|
"memchr",
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
"ppmd-rust",
|
"ppmd-rust",
|
||||||
"sha1",
|
"sha1",
|
||||||
"time 0.3.47",
|
"time",
|
||||||
"typed-path",
|
"typed-path",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
"zopfli",
|
"zopfli",
|
||||||
|
|||||||
+9
-7
@@ -7,16 +7,17 @@ edition = "2024"
|
|||||||
warnings = "deny"
|
warnings = "deny"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.101"
|
anyhow = "1.0.102"
|
||||||
ar = "0.9.0"
|
ar = "0.9.0"
|
||||||
bzip2 = "0.6.1"
|
bzip2 = "0.6.1"
|
||||||
clap = { version = "4.5.59", features = ["derive"] }
|
clap = { version = "4.5.60", features = ["derive"] }
|
||||||
flate2 = "1.1.9"
|
flate2 = { version = "1.1.9", default-features = false, features = ["miniz_oxide"] }
|
||||||
git2 = "0.20.4"
|
git2 = { version = "0.20.4", features = ["vendored-libgit2"] }
|
||||||
indicatif = "0.18.4"
|
indicatif = "0.18.4"
|
||||||
nix = { version = "0.31.1", features = ["user"] }
|
nix = { version = "0.31.1", features = ["user"] }
|
||||||
reqwest = { version = "0.13.2", features = ["blocking"] }
|
reqwest = { version = "0.13.2", features = ["blocking", "rustls"] }
|
||||||
rusqlite = "0.38.0"
|
rusqlite = { version = "0.38.0", features = ["bundled"] }
|
||||||
|
openssl-sys = { version = "0.9", features = ["vendored"] }
|
||||||
semver = "1.0.27"
|
semver = "1.0.27"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
yaml-rust2 = "0.11.0"
|
yaml-rust2 = "0.11.0"
|
||||||
@@ -30,9 +31,10 @@ walkdir = "2.5.0"
|
|||||||
xz2 = "0.1.7"
|
xz2 = "0.1.7"
|
||||||
zip = "8.1.0"
|
zip = "8.1.0"
|
||||||
zstd = { version = "0.13.3", features = ["zstdmt"] }
|
zstd = { version = "0.13.3", features = ["zstdmt"] }
|
||||||
|
libz-sys = { version = "1.1.23", default-features = false, features = ["stock-zlib", "static"] }
|
||||||
inquire = "0.9.3"
|
inquire = "0.9.3"
|
||||||
md5 = "0.8.0"
|
md5 = "0.8.0"
|
||||||
ftp = "3.0.1"
|
suppaftp = "8.0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.25.0"
|
tempfile = "3.25.0"
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 SFG545 and ReallyUnusual
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the “Software”), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+358
-69
@@ -14,78 +14,28 @@ pub fn build(
|
|||||||
src_dir: &Path,
|
src_dir: &Path,
|
||||||
destdir: &Path,
|
destdir: &Path,
|
||||||
cross: Option<&CrossConfig>,
|
cross: Option<&CrossConfig>,
|
||||||
|
export_compiler_flags: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let flags = &spec.build.flags;
|
let flags = &spec.build.flags;
|
||||||
|
let export_compiler_flags = export_compiler_flags && !flags.no_flags;
|
||||||
// Determine actual source directory (support source_subdir)
|
let actual_src = resolve_actual_src(spec, src_dir)?;
|
||||||
let actual_src = if !flags.source_subdir.is_empty() {
|
|
||||||
src_dir.join(&flags.source_subdir)
|
|
||||||
} else {
|
|
||||||
src_dir.to_path_buf()
|
|
||||||
};
|
|
||||||
|
|
||||||
if !actual_src.exists() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Source directory not found: {} (source_subdir: {})",
|
|
||||||
actual_src.display(),
|
|
||||||
flags.source_subdir
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create destdir
|
// Create destdir
|
||||||
fs::create_dir_all(destdir)?;
|
fs::create_dir_all(destdir)?;
|
||||||
|
|
||||||
// Build environment variables
|
// Build environment variables
|
||||||
let mut env_vars: Vec<(&str, String)> = vec![];
|
let mut env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags);
|
||||||
|
let cc = if let Some(cc_cfg) = cross {
|
||||||
// Use cross-compilation tools if configured
|
cc_cfg.cc.clone()
|
||||||
let (cc, ar) = if let Some(cc_cfg) = cross {
|
|
||||||
(cc_cfg.cc.clone(), cc_cfg.ar.clone())
|
|
||||||
} else {
|
} else {
|
||||||
(flags.cc.clone(), flags.ar.clone())
|
flags.cc.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
if !flags.cflags.is_empty() {
|
if export_compiler_flags && !flags.cflags.is_empty() {
|
||||||
// Expand shell command substitutions like $($CC -print-resource-dir)
|
// Expand shell command substitutions like $($CC -print-resource-dir)
|
||||||
let cflags_str = flags.cflags.join(" ");
|
let cflags_str = flags.cflags.join(" ");
|
||||||
let expanded = expand_shell_commands(&cflags_str, &cc)?;
|
let expanded = expand_shell_commands(&cflags_str, &cc)?;
|
||||||
env_vars.push(("CFLAGS", expanded));
|
crate::builder::set_env_var(&mut env_vars, "CFLAGS", expanded);
|
||||||
}
|
|
||||||
if !flags.ldflags.is_empty() {
|
|
||||||
env_vars.push(("LDFLAGS", flags.ldflags.join(" ")));
|
|
||||||
}
|
|
||||||
env_vars.push(("CC", cc.clone()));
|
|
||||||
env_vars.push(("AR", ar));
|
|
||||||
|
|
||||||
// CARCH support
|
|
||||||
if !flags.carch.is_empty() {
|
|
||||||
env_vars.push(("CARCH", flags.carch.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export rootfs for build scripts
|
|
||||||
env_vars.push(("DEPOT_ROOTFS", flags.rootfs.clone()));
|
|
||||||
|
|
||||||
// Add cross-compilation environment
|
|
||||||
if let Some(cc_cfg) = cross {
|
|
||||||
env_vars.push(("CXX", cc_cfg.cxx.clone()));
|
|
||||||
env_vars.push(("RANLIB", cc_cfg.ranlib.clone()));
|
|
||||||
env_vars.push(("STRIP", cc_cfg.strip.clone()));
|
|
||||||
env_vars.push(("LD", cc_cfg.ld.clone()));
|
|
||||||
env_vars.push(("NM", cc_cfg.nm.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add dynamic loader flag if specified
|
|
||||||
if !flags.libc.is_empty() {
|
|
||||||
let ldflags = if flags.ldflags.is_empty() {
|
|
||||||
format!("-Wl,--dynamic-linker={}", flags.libc)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"{} -Wl,--dynamic-linker={}",
|
|
||||||
flags.ldflags.join(" "),
|
|
||||||
flags.libc
|
|
||||||
)
|
|
||||||
};
|
|
||||||
env_vars.push(("LDFLAGS", ldflags));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::builder::state::{BuildStep, StateTracker};
|
use crate::builder::state::{BuildStep, StateTracker};
|
||||||
@@ -117,18 +67,47 @@ pub fn build(
|
|||||||
|
|
||||||
configure_cmd.arg(format!("--prefix={}", flags.prefix));
|
configure_cmd.arg(format!("--prefix={}", flags.prefix));
|
||||||
|
|
||||||
if !flags.chost.is_empty() {
|
// Some projects use non-GNU configure scripts that reject --host/--build.
|
||||||
configure_cmd.arg(format!("--host={}", flags.chost));
|
// Probe support first and only add these options when advertised.
|
||||||
|
let help_text = configure_help_text(configure_path, &build_dir, &env_vars);
|
||||||
|
let supports_host = help_text
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| configure_help_supports_option(s, "--host"))
|
||||||
|
.unwrap_or(true);
|
||||||
|
let supports_build = help_text
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| configure_help_supports_option(s, "--build"))
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
let requested_host = if let Some(cc_cfg) = cross {
|
||||||
|
Some(cc_cfg.host_triple().to_string())
|
||||||
|
} else if !flags.chost.is_empty() {
|
||||||
|
Some(flags.chost.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let requested_build = if cross.is_some() {
|
||||||
|
CrossConfig::build_triple().ok()
|
||||||
|
} else if !flags.cbuild.is_empty() {
|
||||||
|
Some(flags.cbuild.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(host) = requested_host {
|
||||||
|
if supports_host {
|
||||||
|
configure_cmd.arg(format!("--host={}", host));
|
||||||
|
} else {
|
||||||
|
println!(" configure does not support --host; skipping {}", host);
|
||||||
}
|
}
|
||||||
if !flags.cbuild.is_empty() {
|
|
||||||
configure_cmd.arg(format!("--build={}", flags.cbuild));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add cross-compilation flags
|
if let Some(build) = requested_build {
|
||||||
if let Some(cc_cfg) = cross {
|
if supports_build {
|
||||||
configure_cmd.arg(format!("--host={}", cc_cfg.host_triple()));
|
|
||||||
if let Ok(build) = CrossConfig::build_triple() {
|
|
||||||
configure_cmd.arg(format!("--build={}", build));
|
configure_cmd.arg(format!("--build={}", build));
|
||||||
|
} else {
|
||||||
|
println!(" configure does not support --build; skipping {}", build);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +133,7 @@ pub fn build(
|
|||||||
let mut make_cmd = Command::new("make");
|
let mut make_cmd = Command::new("make");
|
||||||
make_cmd.current_dir(&build_dir);
|
make_cmd.current_dir(&build_dir);
|
||||||
make_cmd.arg("-j").arg(num_cpus().to_string());
|
make_cmd.arg("-j").arg(num_cpus().to_string());
|
||||||
|
add_make_variable_overrides(&mut make_cmd, &flags.make_vars, "build")?;
|
||||||
|
|
||||||
crate::builder::prepare_command(&mut make_cmd, &env_vars);
|
crate::builder::prepare_command(&mut make_cmd, &env_vars);
|
||||||
|
|
||||||
@@ -165,6 +145,30 @@ pub fn build(
|
|||||||
anyhow::bail!("make failed with status: {}", status);
|
anyhow::bail!("make failed with status: {}", status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(test_target) = maybe_find_autotools_test_target(&build_dir, flags.skip_tests)? {
|
||||||
|
println!("Running make {}...", test_target);
|
||||||
|
let mut test_cmd = Command::new("make");
|
||||||
|
test_cmd.current_dir(&build_dir);
|
||||||
|
add_make_variable_overrides(&mut test_cmd, &flags.make_test_vars, "test")?;
|
||||||
|
test_cmd.arg(test_target);
|
||||||
|
crate::builder::prepare_command(&mut test_cmd, &env_vars);
|
||||||
|
|
||||||
|
let status = test_cmd.status().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to run make {} in {}",
|
||||||
|
test_target,
|
||||||
|
build_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("make {} failed with status: {}", test_target, status);
|
||||||
|
}
|
||||||
|
} else if flags.skip_tests {
|
||||||
|
println!("Skipping tests: disabled by build.flags.skip_tests");
|
||||||
|
} else {
|
||||||
|
println!("Skipping tests: no 'check' or 'test' target in Makefile");
|
||||||
|
}
|
||||||
|
|
||||||
// Run post-compile hooks (after make, before make install)
|
// Run post-compile hooks (after make, before make install)
|
||||||
hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
|
hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
|
||||||
state.mark_done(BuildStep::PostCompileDone)?;
|
state.mark_done(BuildStep::PostCompileDone)?;
|
||||||
@@ -185,10 +189,17 @@ pub fn build(
|
|||||||
|
|
||||||
let mut install_cmd = fakeroot::wrap_install_command("make", destdir);
|
let mut install_cmd = fakeroot::wrap_install_command("make", destdir);
|
||||||
install_cmd.current_dir(&build_dir);
|
install_cmd.current_dir(&build_dir);
|
||||||
|
if !has_make_variable_override(&flags.make_install_vars, "DESTDIR") {
|
||||||
|
install_cmd.arg(format!("DESTDIR={}", destdir.to_string_lossy()));
|
||||||
|
}
|
||||||
|
add_make_variable_overrides(&mut install_cmd, &flags.make_install_vars, "install")?;
|
||||||
install_cmd.arg("install");
|
install_cmd.arg("install");
|
||||||
|
|
||||||
let mut install_env = env_vars.clone();
|
let mut install_env = env_vars.clone();
|
||||||
install_env.push(("DESTDIR", destdir.to_string_lossy().into_owned()));
|
install_env.push((
|
||||||
|
"DESTDIR".to_string(),
|
||||||
|
destdir.to_string_lossy().into_owned(),
|
||||||
|
));
|
||||||
crate::builder::prepare_command(&mut install_cmd, &install_env);
|
crate::builder::prepare_command(&mut install_cmd, &install_env);
|
||||||
|
|
||||||
let status = install_cmd
|
let status = install_cmd
|
||||||
@@ -215,6 +226,155 @@ fn num_cpus() -> usize {
|
|||||||
.unwrap_or(1)
|
.unwrap_or(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_actual_src(spec: &PackageSpec, src_dir: &Path) -> Result<std::path::PathBuf> {
|
||||||
|
let flags = &spec.build.flags;
|
||||||
|
let source_subdir = spec.expand_vars(&flags.source_subdir);
|
||||||
|
|
||||||
|
let actual_src = if source_subdir.is_empty() {
|
||||||
|
src_dir.to_path_buf()
|
||||||
|
} else {
|
||||||
|
src_dir.join(&source_subdir)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !actual_src.exists() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Source directory not found: {} (source_subdir: {} -> {})",
|
||||||
|
actual_src.display(),
|
||||||
|
flags.source_subdir,
|
||||||
|
source_subdir
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(actual_src)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_help_text(
|
||||||
|
configure_path: &str,
|
||||||
|
build_dir: &Path,
|
||||||
|
env_vars: &crate::builder::EnvVars,
|
||||||
|
) -> Option<String> {
|
||||||
|
let mut help_cmd = Command::new(configure_path);
|
||||||
|
help_cmd.current_dir(build_dir);
|
||||||
|
help_cmd.arg("--help");
|
||||||
|
crate::builder::prepare_command(&mut help_cmd, env_vars);
|
||||||
|
let output = help_cmd.output().ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut text = String::new();
|
||||||
|
text.push_str(&String::from_utf8_lossy(&output.stdout));
|
||||||
|
text.push_str(&String::from_utf8_lossy(&output.stderr));
|
||||||
|
Some(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_help_supports_option(help_text: &str, option: &str) -> bool {
|
||||||
|
let with_eq = format!("{}=", option);
|
||||||
|
let with_space = format!("{} ", option);
|
||||||
|
help_text.contains(&with_eq) || help_text.contains(&with_space) || help_text.contains(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_autotools_test_target(build_dir: &Path) -> Result<Option<&'static str>> {
|
||||||
|
for target in ["check", "test"] {
|
||||||
|
if makefile_has_target(build_dir, target)? {
|
||||||
|
return Ok(Some(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_find_autotools_test_target(
|
||||||
|
build_dir: &Path,
|
||||||
|
skip_tests: bool,
|
||||||
|
) -> Result<Option<&'static str>> {
|
||||||
|
if skip_tests {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
find_autotools_test_target(build_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makefile_has_target(build_dir: &Path, target: &str) -> Result<bool> {
|
||||||
|
for name in ["GNUmakefile", "Makefile", "makefile"] {
|
||||||
|
let path = build_dir.join(name);
|
||||||
|
if !path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(&path)
|
||||||
|
.with_context(|| format!("Failed to read makefile: {}", path.display()))?;
|
||||||
|
if makefile_content_has_target(&content, target) {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makefile_content_has_target(content: &str, target: &str) -> bool {
|
||||||
|
for raw in content.lines() {
|
||||||
|
let line = raw.trim_start();
|
||||||
|
if line.is_empty() || line.starts_with('#') || line.starts_with('\t') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rest) = line.strip_prefix(".PHONY:") {
|
||||||
|
if rest.split_whitespace().any(|t| t == target) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(colon_pos) = line.find(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let rhs = &line[colon_pos + 1..];
|
||||||
|
if rhs.starts_with('=') {
|
||||||
|
// Variable assignment (e.g. FOO:=bar), not a make target.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lhs = &line[..colon_pos];
|
||||||
|
if lhs.split_whitespace().any(|t| t == target) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_make_variable_overrides(cmd: &mut Command, vars: &[String], phase: &str) -> Result<()> {
|
||||||
|
for raw in vars {
|
||||||
|
let var = raw.trim();
|
||||||
|
if var.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some((name, _value)) = var.split_once('=') else {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Invalid make variable override '{}' for {} phase; expected NAME=VALUE",
|
||||||
|
var,
|
||||||
|
phase
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let name = name.trim();
|
||||||
|
if name.is_empty() || name.contains(char::is_whitespace) {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Invalid make variable override '{}' for {} phase; expected NAME=VALUE",
|
||||||
|
var,
|
||||||
|
phase
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cmd.arg(var);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_make_variable_override(vars: &[String], name: &str) -> bool {
|
||||||
|
vars.iter().any(|raw| {
|
||||||
|
let var = raw.trim();
|
||||||
|
let Some((lhs, _rhs)) = var.split_once('=') else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
lhs.trim() == name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Expand shell command substitutions like $($CC -print-resource-dir) in a string
|
/// Expand shell command substitutions like $($CC -print-resource-dir) in a string
|
||||||
fn expand_shell_commands(input: &str, cc: &str) -> Result<String> {
|
fn expand_shell_commands(input: &str, cc: &str) -> Result<String> {
|
||||||
let mut result = input.to_string();
|
let mut result = input.to_string();
|
||||||
@@ -253,6 +413,9 @@ fn expand_shell_commands(input: &str, cc: &str) -> Result<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_expand_shell_commands_simple() -> Result<()> {
|
fn test_expand_shell_commands_simple() -> Result<()> {
|
||||||
@@ -266,7 +429,7 @@ mod tests {
|
|||||||
// The command contains $CC which should be replaced with provided cc
|
// The command contains $CC which should be replaced with provided cc
|
||||||
let out = expand_shell_commands("start $($CC -v >/dev/null; echo OK) end", "mycc")?;
|
let out = expand_shell_commands("start $($CC -v >/dev/null; echo OK) end", "mycc")?;
|
||||||
// Since the inner command echoes OK, after replacing $CC it should run and include OK
|
// Since the inner command echoes OK, after replacing $CC it should run and include OK
|
||||||
assert!(out.contains("OK") || out.contains("") );
|
assert!(out.contains("OK") || out.contains(""));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,4 +438,130 @@ mod tests {
|
|||||||
let n = num_cpus();
|
let n = num_cpus();
|
||||||
assert!(n >= 1);
|
assert!(n >= 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_configure_help_supports_host_build() {
|
||||||
|
let help = "Usage: configure [OPTION]...\n --host=HOST cross host\n --build=BUILD";
|
||||||
|
assert!(configure_help_supports_option(help, "--host"));
|
||||||
|
assert!(configure_help_supports_option(help, "--build"));
|
||||||
|
assert!(!configure_help_supports_option(help, "--target"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_makefile_content_has_target_detects_check_and_test() {
|
||||||
|
let content = r#"
|
||||||
|
.PHONY: all check
|
||||||
|
all:
|
||||||
|
@echo all
|
||||||
|
check:
|
||||||
|
@echo check
|
||||||
|
"#;
|
||||||
|
assert!(makefile_content_has_target(content, "check"));
|
||||||
|
assert!(!makefile_content_has_target(content, "test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_makefile_content_has_target_ignores_assignments() {
|
||||||
|
let content = r#"
|
||||||
|
TEST := value
|
||||||
|
VAR:=$(shell echo hi)
|
||||||
|
foo: bar
|
||||||
|
@true
|
||||||
|
"#;
|
||||||
|
assert!(!makefile_content_has_target(content, "TEST"));
|
||||||
|
assert!(!makefile_content_has_target(content, "VAR"));
|
||||||
|
assert!(!makefile_content_has_target(content, "check"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_maybe_find_autotools_test_target_respects_skip_tests() -> Result<()> {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
std::fs::write(tmp.path().join("Makefile"), "check:\n\t@true\n").unwrap();
|
||||||
|
|
||||||
|
let skipped = maybe_find_autotools_test_target(tmp.path(), true)?;
|
||||||
|
assert_eq!(skipped, None);
|
||||||
|
|
||||||
|
let detected = maybe_find_autotools_test_target(tmp.path(), false)?;
|
||||||
|
assert_eq!(detected, Some("check"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_make_variable_overrides_accepts_valid_assignments() -> Result<()> {
|
||||||
|
let mut cmd = Command::new("make");
|
||||||
|
add_make_variable_overrides(
|
||||||
|
&mut cmd,
|
||||||
|
&[
|
||||||
|
"CC=clang".to_string(),
|
||||||
|
"V=1".to_string(),
|
||||||
|
" CFLAGS=-O2 -pipe ".to_string(),
|
||||||
|
],
|
||||||
|
"build",
|
||||||
|
)?;
|
||||||
|
let args: Vec<String> = cmd
|
||||||
|
.get_args()
|
||||||
|
.map(|a| a.to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(args, vec!["CC=clang", "V=1", "CFLAGS=-O2 -pipe"]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_make_variable_overrides_rejects_invalid_assignment() {
|
||||||
|
let mut cmd = Command::new("make");
|
||||||
|
let err = add_make_variable_overrides(&mut cmd, &["not-an-assignment".to_string()], "test")
|
||||||
|
.expect_err("expected invalid assignment to fail");
|
||||||
|
assert!(err.to_string().contains("expected NAME=VALUE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_has_make_variable_override_detects_destdir() {
|
||||||
|
assert!(has_make_variable_override(
|
||||||
|
&["DESTDIR=/tmp/pkg".to_string()],
|
||||||
|
"DESTDIR"
|
||||||
|
));
|
||||||
|
assert!(has_make_variable_override(
|
||||||
|
&[" DESTDIR =/tmp/pkg ".to_string()],
|
||||||
|
"DESTDIR"
|
||||||
|
));
|
||||||
|
assert!(!has_make_variable_override(
|
||||||
|
&["V=1".to_string(), "PREFIX=/usr".to_string()],
|
||||||
|
"DESTDIR"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_actual_src_expands_source_subdir_vars() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let src_root = tmp.path().join("srcroot");
|
||||||
|
let expanded = src_root.join("expect5.45.4").join("unix");
|
||||||
|
std::fs::create_dir_all(&expanded).unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec {
|
||||||
|
package: PackageInfo {
|
||||||
|
name: "expect".into(),
|
||||||
|
version: "5.45.4".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "d".into(),
|
||||||
|
homepage: "h".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: Default::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: Vec::new(),
|
||||||
|
build: Build {
|
||||||
|
build_type: BuildType::Autotools,
|
||||||
|
flags: BuildFlags {
|
||||||
|
source_subdir: "$name$version/unix".into(),
|
||||||
|
..BuildFlags::default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dependencies: Dependencies::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolved = resolve_actual_src(&spec, &src_root).unwrap();
|
||||||
|
assert_eq!(resolved, expanded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-10
@@ -1,12 +1,12 @@
|
|||||||
//! Binary package "build" system — used when package supplies a prebuilt binary installer
|
//! Binary package "build" system — used when package supplies a prebuilt binary installer
|
||||||
|
|
||||||
|
use crate::cross::CrossConfig;
|
||||||
|
use crate::package::PackageSpec;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::fs as unix_fs;
|
use std::os::unix::fs as unix_fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use crate::package::PackageSpec;
|
|
||||||
use crate::cross::CrossConfig;
|
|
||||||
|
|
||||||
/// For binary packages we simply copy the extracted files into DESTDIR (preserving
|
/// For binary packages we simply copy the extracted files into DESTDIR (preserving
|
||||||
/// directory structure). This is useful for .deb packages where extract step
|
/// directory structure). This is useful for .deb packages where extract step
|
||||||
@@ -16,9 +16,16 @@ pub fn build(
|
|||||||
src_dir: &Path,
|
src_dir: &Path,
|
||||||
destdir: &Path,
|
destdir: &Path,
|
||||||
_cross: Option<&CrossConfig>,
|
_cross: Option<&CrossConfig>,
|
||||||
|
_export_compiler_flags: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
println!("Binary install: copying files from {} to {} (pkg type={})", src_dir.display(), destdir.display(), _spec.build.flags.binary_type);
|
println!(
|
||||||
fs::create_dir_all(destdir).with_context(|| format!("Failed to create destdir: {}", destdir.display()))?;
|
"Binary install: copying files from {} to {} (pkg type={})",
|
||||||
|
src_dir.display(),
|
||||||
|
destdir.display(),
|
||||||
|
_spec.build.flags.binary_type
|
||||||
|
);
|
||||||
|
fs::create_dir_all(destdir)
|
||||||
|
.with_context(|| format!("Failed to create destdir: {}", destdir.display()))?;
|
||||||
|
|
||||||
for entry in WalkDir::new(src_dir) {
|
for entry in WalkDir::new(src_dir) {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
@@ -28,12 +35,18 @@ pub fn build(
|
|||||||
fs::create_dir_all(&target)?;
|
fs::create_dir_all(&target)?;
|
||||||
} else if entry.file_type().is_symlink() {
|
} else if entry.file_type().is_symlink() {
|
||||||
let link_target = fs::read_link(entry.path())?;
|
let link_target = fs::read_link(entry.path())?;
|
||||||
if let Some(parent) = target.parent() { fs::create_dir_all(parent)?; }
|
if let Some(parent) = target.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
// overwrite existing links/files
|
// overwrite existing links/files
|
||||||
if target.exists() { let _ = fs::remove_file(&target); }
|
if target.exists() {
|
||||||
|
let _ = fs::remove_file(&target);
|
||||||
|
}
|
||||||
unix_fs::symlink(link_target, &target)?;
|
unix_fs::symlink(link_target, &target)?;
|
||||||
} else {
|
} else {
|
||||||
if let Some(parent) = target.parent() { fs::create_dir_all(parent)?; }
|
if let Some(parent) = target.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
fs::copy(entry.path(), &target)?;
|
fs::copy(entry.path(), &target)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,9 +58,9 @@ pub fn build(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo};
|
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo};
|
||||||
use tempfile::tempdir;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::fs as unix_fs;
|
use std::os::unix::fs as unix_fs;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn mk_spec(name: &str, version: &str) -> PackageSpec {
|
fn mk_spec(name: &str, version: &str) -> PackageSpec {
|
||||||
PackageSpec {
|
PackageSpec {
|
||||||
@@ -57,7 +70,7 @@ mod tests {
|
|||||||
revision: 1,
|
revision: 1,
|
||||||
description: "d".into(),
|
description: "d".into(),
|
||||||
homepage: "h".into(),
|
homepage: "h".into(),
|
||||||
license: "MIT".into(),
|
license: vec!["MIT".into()],
|
||||||
},
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Default::default(),
|
alternatives: Default::default(),
|
||||||
@@ -96,7 +109,7 @@ mod tests {
|
|||||||
unix_fs::symlink(&target, src.join("usr/lib/libdummy.so.link"))?;
|
unix_fs::symlink(&target, src.join("usr/lib/libdummy.so.link"))?;
|
||||||
|
|
||||||
let spec = mk_spec("bin-test", "1.0");
|
let spec = mk_spec("bin-test", "1.0");
|
||||||
build(&spec, src, dest, None)?;
|
build(&spec, src, dest, None, true)?;
|
||||||
|
|
||||||
// Check copied file
|
// Check copied file
|
||||||
assert!(dest.join("usr/bin/hello").exists());
|
assert!(dest.join("usr/bin/hello").exists());
|
||||||
|
|||||||
+51
-62
@@ -13,13 +13,13 @@ pub fn build(
|
|||||||
src_dir: &Path,
|
src_dir: &Path,
|
||||||
destdir: &Path,
|
destdir: &Path,
|
||||||
cross: Option<&CrossConfig>,
|
cross: Option<&CrossConfig>,
|
||||||
|
export_compiler_flags: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let flags = &spec.build.flags;
|
let flags = &spec.build.flags;
|
||||||
|
|
||||||
// Determine actual source directory (support source_subdir)
|
// Determine actual source directory (support source_subdir)
|
||||||
let actual_src = resolve_actual_src(spec, src_dir)?;
|
let actual_src = resolve_actual_src(spec, src_dir)?;
|
||||||
|
|
||||||
|
|
||||||
let build_dir = actual_src.join("build");
|
let build_dir = actual_src.join("build");
|
||||||
|
|
||||||
// Create directories
|
// Create directories
|
||||||
@@ -27,55 +27,7 @@ pub fn build(
|
|||||||
fs::create_dir_all(destdir)?;
|
fs::create_dir_all(destdir)?;
|
||||||
|
|
||||||
// Environment variables
|
// Environment variables
|
||||||
let mut env_vars: Vec<(&str, String)> = vec![];
|
let env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags);
|
||||||
|
|
||||||
// Use cross-compilation tools if configured
|
|
||||||
let cc = if let Some(cc_cfg) = cross {
|
|
||||||
cc_cfg.cc.clone()
|
|
||||||
} else {
|
|
||||||
flags.cc.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
if !flags.cflags.is_empty() {
|
|
||||||
env_vars.push(("CFLAGS", flags.cflags.join(" ")));
|
|
||||||
}
|
|
||||||
if !flags.chost.is_empty() {
|
|
||||||
env_vars.push(("CHOST", flags.chost.clone()));
|
|
||||||
}
|
|
||||||
if !flags.cbuild.is_empty() {
|
|
||||||
env_vars.push(("CBUILD", flags.cbuild.clone()));
|
|
||||||
}
|
|
||||||
if !flags.ldflags.is_empty() {
|
|
||||||
let ldflags = if flags.libc.is_empty() {
|
|
||||||
flags.ldflags.join(" ")
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"{} -Wl,--dynamic-linker={}",
|
|
||||||
flags.ldflags.join(" "),
|
|
||||||
flags.libc
|
|
||||||
)
|
|
||||||
};
|
|
||||||
env_vars.push(("LDFLAGS", ldflags));
|
|
||||||
}
|
|
||||||
env_vars.push(("CC", cc));
|
|
||||||
|
|
||||||
// Ensure CXX is exported for configure flags that reference $CXX
|
|
||||||
// prefer cross-config CXX when cross-compiling (handled earlier), otherwise use flags.cxx
|
|
||||||
env_vars.push(("CXX", flags.cxx.clone()));
|
|
||||||
|
|
||||||
// Export rootfs for build scripts
|
|
||||||
env_vars.push(("DEPOT_ROOTFS", flags.rootfs.clone()));
|
|
||||||
|
|
||||||
// CARCH support
|
|
||||||
if !flags.carch.is_empty() {
|
|
||||||
env_vars.push(("CARCH", flags.carch.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add cross-compilation env
|
|
||||||
if let Some(cc_cfg) = cross {
|
|
||||||
env_vars.push(("CXX", cc_cfg.cxx.clone()));
|
|
||||||
env_vars.push(("AR", cc_cfg.ar.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract prefix from configure flags (cmake-style -DCMAKE_INSTALL_PREFIX=)
|
// Extract prefix from configure flags (cmake-style -DCMAKE_INSTALL_PREFIX=)
|
||||||
let prefix = flags
|
let prefix = flags
|
||||||
@@ -168,7 +120,10 @@ pub fn build(
|
|||||||
install_cmd.arg("--install").arg(&build_dir);
|
install_cmd.arg("--install").arg(&build_dir);
|
||||||
|
|
||||||
let mut install_env = env_vars.clone();
|
let mut install_env = env_vars.clone();
|
||||||
install_env.push(("DESTDIR", destdir.to_string_lossy().into_owned()));
|
install_env.push((
|
||||||
|
"DESTDIR".to_string(),
|
||||||
|
destdir.to_string_lossy().into_owned(),
|
||||||
|
));
|
||||||
crate::builder::prepare_command(&mut install_cmd, &install_env);
|
crate::builder::prepare_command(&mut install_cmd, &install_env);
|
||||||
|
|
||||||
let status = install_cmd
|
let status = install_cmd
|
||||||
@@ -199,7 +154,7 @@ fn expand_env_vars(input: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Expand using a provided set of env vars (used to expand flags before spawning child).
|
/// Expand using a provided set of env vars (used to expand flags before spawning child).
|
||||||
fn expand_with_envs(input: &str, envs: &Vec<(&str, String)>) -> String {
|
fn expand_with_envs(input: &str, envs: &[(String, String)]) -> String {
|
||||||
let mut result = input.to_string();
|
let mut result = input.to_string();
|
||||||
for (k, v) in envs {
|
for (k, v) in envs {
|
||||||
result = result.replace(&format!("${}", k), v);
|
result = result.replace(&format!("${}", k), v);
|
||||||
@@ -220,29 +175,38 @@ fn num_cpus() -> usize {
|
|||||||
/// - `src_dir/<sub>` -> use if exists
|
/// - `src_dir/<sub>` -> use if exists
|
||||||
/// - `spec.spec_dir/<sub>` -> use if exists (supports `../llvm`)
|
/// - `spec.spec_dir/<sub>` -> use if exists (supports `../llvm`)
|
||||||
/// - bare relative path (cwd)
|
/// - bare relative path (cwd)
|
||||||
fn resolve_actual_src(spec: &crate::package::PackageSpec, src_dir: &Path) -> anyhow::Result<std::path::PathBuf> {
|
fn resolve_actual_src(
|
||||||
|
spec: &crate::package::PackageSpec,
|
||||||
|
src_dir: &Path,
|
||||||
|
) -> anyhow::Result<std::path::PathBuf> {
|
||||||
let flags = &spec.build.flags;
|
let flags = &spec.build.flags;
|
||||||
if flags.source_subdir.is_empty() {
|
let source_subdir = spec.expand_vars(&flags.source_subdir);
|
||||||
|
if source_subdir.is_empty() {
|
||||||
return Ok(src_dir.to_path_buf());
|
return Ok(src_dir.to_path_buf());
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidate = std::path::Path::new(&flags.source_subdir);
|
let candidate = std::path::Path::new(&source_subdir);
|
||||||
// 1) absolute path -> use directly
|
// 1) absolute path -> use directly
|
||||||
if candidate.is_absolute() {
|
if candidate.is_absolute() {
|
||||||
if candidate.exists() {
|
if candidate.exists() {
|
||||||
return Ok(candidate.to_path_buf());
|
return Ok(candidate.to_path_buf());
|
||||||
}
|
}
|
||||||
anyhow::bail!("Source directory not found: {} (source_subdir: {})", candidate.display(), flags.source_subdir);
|
anyhow::bail!(
|
||||||
|
"Source directory not found: {} (source_subdir: {} -> {})",
|
||||||
|
candidate.display(),
|
||||||
|
flags.source_subdir,
|
||||||
|
source_subdir
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) src_dir/<source_subdir>
|
// 2) src_dir/<source_subdir>
|
||||||
let under_src = src_dir.join(&flags.source_subdir);
|
let under_src = src_dir.join(&source_subdir);
|
||||||
if under_src.exists() {
|
if under_src.exists() {
|
||||||
return Ok(under_src);
|
return Ok(under_src);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) spec_dir/<source_subdir> (useful for ../llvm relative to spec)
|
// 3) spec_dir/<source_subdir> (useful for ../llvm relative to spec)
|
||||||
let spec_path = spec.spec_dir.join(&flags.source_subdir);
|
let spec_path = spec.spec_dir.join(&source_subdir);
|
||||||
if spec_path.exists() {
|
if spec_path.exists() {
|
||||||
return Ok(spec_path);
|
return Ok(spec_path);
|
||||||
}
|
}
|
||||||
@@ -254,7 +218,8 @@ fn resolve_actual_src(spec: &crate::package::PackageSpec, src_dir: &Path) -> any
|
|||||||
|
|
||||||
// fallback error
|
// fallback error
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Source directory not found: {} (tried src_dir, spec_dir, and absolute path)",
|
"Source directory not found: {} (expanded from '{}'; tried src_dir, spec_dir, and absolute path)",
|
||||||
|
source_subdir,
|
||||||
flags.source_subdir
|
flags.source_subdir
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -277,7 +242,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_expand_with_envs_prefers_provided_envs() {
|
fn test_expand_with_envs_prefers_provided_envs() {
|
||||||
let envs = vec![("CXX", "my-cxx".to_string()), ("CC", "my-cc".to_string())];
|
let envs = vec![
|
||||||
|
("CXX".to_string(), "my-cxx".to_string()),
|
||||||
|
("CC".to_string(), "my-cc".to_string()),
|
||||||
|
];
|
||||||
let s = "-DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=${CXX} -DROOT=$HOME";
|
let s = "-DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=${CXX} -DROOT=$HOME";
|
||||||
let out = expand_with_envs(s, &envs);
|
let out = expand_with_envs(s, &envs);
|
||||||
assert!(out.contains("my-cc"));
|
assert!(out.contains("my-cc"));
|
||||||
@@ -297,18 +265,33 @@ mod tests {
|
|||||||
let src_root = tmp.path().join("srcroot");
|
let src_root = tmp.path().join("srcroot");
|
||||||
let spec_dir = tmp.path().join("specdir");
|
let spec_dir = tmp.path().join("specdir");
|
||||||
let external = tmp.path().join("external");
|
let external = tmp.path().join("external");
|
||||||
|
let expanded = src_root.join("x-1.0").join("sub");
|
||||||
std::fs::create_dir_all(&src_root.join("sub")).unwrap();
|
std::fs::create_dir_all(&src_root.join("sub")).unwrap();
|
||||||
|
std::fs::create_dir_all(&expanded).unwrap();
|
||||||
// create directories for candidates
|
// create directories for candidates
|
||||||
std::fs::create_dir_all(&spec_dir.join("../llvm")).unwrap();
|
std::fs::create_dir_all(&spec_dir.join("../llvm")).unwrap();
|
||||||
std::fs::create_dir_all(&external).unwrap();
|
std::fs::create_dir_all(&external).unwrap();
|
||||||
|
|
||||||
let spec = PackageSpec {
|
let spec = PackageSpec {
|
||||||
package: PackageInfo { name: "x".into(), version: "1.0".into(), revision: 1, description: "".into(), homepage: "".into(), license: "MIT".into() },
|
package: PackageInfo {
|
||||||
|
name: "x".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "".into(),
|
||||||
|
homepage: "".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Default::default(),
|
alternatives: Default::default(),
|
||||||
manual_sources: Vec::new(),
|
manual_sources: Vec::new(),
|
||||||
source: Vec::new(),
|
source: Vec::new(),
|
||||||
build: Build { build_type: BuildType::CMake, flags: BuildFlags { source_subdir: "sub".into(), ..BuildFlags::default() } },
|
build: Build {
|
||||||
|
build_type: BuildType::CMake,
|
||||||
|
flags: BuildFlags {
|
||||||
|
source_subdir: "sub".into(),
|
||||||
|
..BuildFlags::default()
|
||||||
|
},
|
||||||
|
},
|
||||||
dependencies: Default::default(),
|
dependencies: Default::default(),
|
||||||
spec_dir: spec_dir.clone(),
|
spec_dir: spec_dir.clone(),
|
||||||
};
|
};
|
||||||
@@ -328,5 +311,11 @@ mod tests {
|
|||||||
spec3.build.flags.source_subdir = external.to_string_lossy().into_owned();
|
spec3.build.flags.source_subdir = external.to_string_lossy().into_owned();
|
||||||
let p3 = resolve_actual_src(&spec3, &src_root).unwrap();
|
let p3 = resolve_actual_src(&spec3, &src_root).unwrap();
|
||||||
assert_eq!(p3, external);
|
assert_eq!(p3, external);
|
||||||
|
|
||||||
|
// case: variable expansion in source_subdir
|
||||||
|
let mut spec4 = spec.clone();
|
||||||
|
spec4.build.flags.source_subdir = "$name-$version/sub".into();
|
||||||
|
let p4 = resolve_actual_src(&spec4, &src_root).unwrap();
|
||||||
|
assert_eq!(p4, expanded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-61
@@ -12,13 +12,14 @@ pub fn build(
|
|||||||
src_dir: &Path,
|
src_dir: &Path,
|
||||||
destdir: &Path,
|
destdir: &Path,
|
||||||
cross: Option<&CrossConfig>,
|
cross: Option<&CrossConfig>,
|
||||||
|
export_compiler_flags: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let flags = &spec.build.flags;
|
let flags = &spec.build.flags;
|
||||||
|
|
||||||
// Create destdir
|
// Create destdir
|
||||||
fs::create_dir_all(destdir)?;
|
fs::create_dir_all(destdir)?;
|
||||||
|
|
||||||
let mut env_vars: Vec<(&str, String)> = vec![];
|
let mut env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags);
|
||||||
|
|
||||||
// For custom builds, look for a build.sh script in the source directory
|
// For custom builds, look for a build.sh script in the source directory
|
||||||
let build_script = src_dir.join("build.sh");
|
let build_script = src_dir.join("build.sh");
|
||||||
@@ -29,8 +30,12 @@ pub fn build(
|
|||||||
let spec_build = spec.spec_dir.join("build.sh");
|
let spec_build = spec.spec_dir.join("build.sh");
|
||||||
if !build_script.exists() && spec_build.exists() {
|
if !build_script.exists() && spec_build.exists() {
|
||||||
fs::create_dir_all(src_dir)?;
|
fs::create_dir_all(src_dir)?;
|
||||||
fs::copy(&spec_build, &build_script)
|
fs::copy(&spec_build, &build_script).with_context(|| {
|
||||||
.with_context(|| format!("Failed to copy build.sh from spec dir: {}", spec_build.display()))?;
|
format!(
|
||||||
|
"Failed to copy build.sh from spec dir: {}",
|
||||||
|
spec_build.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
// Ensure executable bit
|
// Ensure executable bit
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
@@ -50,7 +55,7 @@ pub fn build(
|
|||||||
}
|
}
|
||||||
|
|
||||||
use crate::builder::state::{BuildStep, StateTracker};
|
use crate::builder::state::{BuildStep, StateTracker};
|
||||||
let mut state = StateTracker::new(&src_dir)?;
|
let mut state = StateTracker::new(src_dir)?;
|
||||||
|
|
||||||
if !state.is_done(BuildStep::PostInstallDone) {
|
if !state.is_done(BuildStep::PostInstallDone) {
|
||||||
println!(
|
println!(
|
||||||
@@ -70,7 +75,8 @@ pub fn build(
|
|||||||
src_dir.to_path_buf()
|
src_dir.to_path_buf()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cmd = fakeroot::wrap_install_command("bash", destdir);
|
// Use POSIX `sh` (more likely to be available in minimal/chroot environments)
|
||||||
|
let mut cmd = fakeroot::wrap_install_command("sh", destdir);
|
||||||
cmd.current_dir(&build_dir);
|
cmd.current_dir(&build_dir);
|
||||||
|
|
||||||
// Ensure build script path is absolute for when we are in a sub-build-dir
|
// Ensure build script path is absolute for when we are in a sub-build-dir
|
||||||
@@ -81,62 +87,22 @@ pub fn build(
|
|||||||
};
|
};
|
||||||
cmd.arg(&abs_build_script);
|
cmd.arg(&abs_build_script);
|
||||||
|
|
||||||
if !flags.cflags.is_empty() {
|
crate::builder::set_env_var(
|
||||||
env_vars.push(("CFLAGS", flags.cflags.join(" ")));
|
&mut env_vars,
|
||||||
}
|
"DESTDIR",
|
||||||
if !flags.ldflags.is_empty() {
|
destdir.to_string_lossy().into_owned(),
|
||||||
let ldflags = if flags.libc.is_empty() {
|
);
|
||||||
flags.ldflags.join(" ")
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"{} -Wl,--dynamic-linker={}",
|
|
||||||
flags.ldflags.join(" "),
|
|
||||||
flags.libc
|
|
||||||
)
|
|
||||||
};
|
|
||||||
env_vars.push(("LDFLAGS", ldflags));
|
|
||||||
}
|
|
||||||
|
|
||||||
env_vars.push(("DESTDIR", destdir.to_string_lossy().into_owned()));
|
|
||||||
env_vars.push(("PREFIX", spec.build.flags.prefix.clone()));
|
|
||||||
env_vars.push(("DEPOT_ROOTFS", spec.build.flags.rootfs.clone()));
|
|
||||||
env_vars.push((
|
|
||||||
"DEPOT_SPECDIR",
|
|
||||||
spec.spec_dir.to_string_lossy().into_owned(),
|
|
||||||
));
|
|
||||||
|
|
||||||
if !flags.chost.is_empty() {
|
|
||||||
env_vars.push(("CHOST", flags.chost.clone()));
|
|
||||||
}
|
|
||||||
if !flags.cbuild.is_empty() {
|
|
||||||
env_vars.push(("CBUILD", flags.cbuild.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !flags.carch.is_empty() {
|
|
||||||
env_vars.push(("CARCH", flags.carch.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use cross-compilation tools if configured
|
|
||||||
if let Some(cc_cfg) = cross {
|
|
||||||
env_vars.push(("CC", cc_cfg.cc.clone()));
|
|
||||||
env_vars.push(("CXX", cc_cfg.cxx.clone()));
|
|
||||||
env_vars.push(("AR", cc_cfg.ar.clone()));
|
|
||||||
env_vars.push(("RANLIB", cc_cfg.ranlib.clone()));
|
|
||||||
env_vars.push(("STRIP", cc_cfg.strip.clone()));
|
|
||||||
env_vars.push(("LD", cc_cfg.ld.clone()));
|
|
||||||
env_vars.push(("NM", cc_cfg.nm.clone()));
|
|
||||||
env_vars.push(("CROSS_PREFIX", cc_cfg.prefix.clone()));
|
|
||||||
env_vars.push(("CROSS_COMPILE", format!("{}-", cc_cfg.prefix)));
|
|
||||||
} else {
|
|
||||||
env_vars.push(("CC", flags.cc.clone()));
|
|
||||||
env_vars.push(("AR", flags.ar.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::builder::prepare_command(&mut cmd, &env_vars);
|
crate::builder::prepare_command(&mut cmd, &env_vars);
|
||||||
|
|
||||||
let status = cmd
|
// Run the command and include the OS error on spawn failures for clearer diagnostics
|
||||||
.status()
|
let status = cmd.status().map_err(|e| {
|
||||||
.with_context(|| format!("Failed to run build script: {}", build_script.display()))?;
|
anyhow::anyhow!(
|
||||||
|
"Failed to run build script {}: {}",
|
||||||
|
build_script.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
anyhow::bail!("Custom build script failed with status: {}", status);
|
anyhow::bail!("Custom build script failed with status: {}", status);
|
||||||
@@ -163,7 +129,7 @@ mod tests {
|
|||||||
revision: 1,
|
revision: 1,
|
||||||
description: "d".into(),
|
description: "d".into(),
|
||||||
homepage: "h".into(),
|
homepage: "h".into(),
|
||||||
license: "MIT".into(),
|
license: vec!["MIT".into()],
|
||||||
},
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Default::default(),
|
alternatives: Default::default(),
|
||||||
@@ -191,7 +157,7 @@ mod tests {
|
|||||||
|
|
||||||
let spec = mk_spec("custom-no-build", "1.0");
|
let spec = mk_spec("custom-no-build", "1.0");
|
||||||
|
|
||||||
let res = build(&spec, tmp_src.path(), tmp_dest.path(), None);
|
let res = build(&spec, tmp_src.path(), tmp_dest.path(), None, true);
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -217,7 +183,7 @@ mod tests {
|
|||||||
spec.spec_dir = spec_dir.path().to_path_buf();
|
spec.spec_dir = spec_dir.path().to_path_buf();
|
||||||
|
|
||||||
// src_dir is empty; build() should copy build.sh from spec_dir and run it (no-op)
|
// src_dir is empty; build() should copy build.sh from spec_dir and run it (no-op)
|
||||||
let _ = build(&spec, tmp_src.path(), tmp_dest.path(), None)?;
|
let _ = build(&spec, tmp_src.path(), tmp_dest.path(), None, true)?;
|
||||||
// If we reached here, build() succeeded and build.sh was copied into src
|
// If we reached here, build() succeeded and build.sh was copied into src
|
||||||
assert!(tmp_src.path().join("build.sh").exists());
|
assert!(tmp_src.path().join("build.sh").exists());
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+14
-33
@@ -10,43 +10,21 @@ pub fn build(
|
|||||||
src_dir: &Path,
|
src_dir: &Path,
|
||||||
destdir: &Path,
|
destdir: &Path,
|
||||||
cross: Option<&CrossConfig>,
|
cross: Option<&CrossConfig>,
|
||||||
|
export_compiler_flags: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut state = StateTracker::new(src_dir)?;
|
let mut state = StateTracker::new(src_dir)?;
|
||||||
let flags = &spec.build.flags;
|
let flags = &spec.build.flags;
|
||||||
|
|
||||||
// Use cross-compilation tools if configured, otherwise use flags from spec
|
let mut env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags);
|
||||||
// Note: Makefile builds might rely on CC/CXX/AR being set, or might use make flags.
|
|
||||||
// We set them in env vars.
|
|
||||||
let (cc, ar) = if let Some(cc_cfg) = cross {
|
|
||||||
(cc_cfg.cc.as_str(), cc_cfg.ar.as_str())
|
|
||||||
} else {
|
|
||||||
(flags.cc.as_str(), flags.ar.as_str())
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut env_vars: Vec<(&str, String)> = vec![("CC", cc.to_string()), ("AR", ar.to_string())];
|
// Export PKG_CONFIG_SYSROOT_DIR for pkg-config
|
||||||
|
|
||||||
let cflags;
|
|
||||||
if !flags.cflags.is_empty() {
|
|
||||||
cflags = flags.cflags.join(" ");
|
|
||||||
env_vars.push(("CFLAGS", cflags));
|
|
||||||
}
|
|
||||||
|
|
||||||
let ldflags;
|
|
||||||
if !flags.ldflags.is_empty() {
|
|
||||||
ldflags = flags.ldflags.join(" ");
|
|
||||||
env_vars.push(("LDFLAGS", ldflags));
|
|
||||||
}
|
|
||||||
|
|
||||||
// CARCH support
|
|
||||||
if !flags.carch.is_empty() {
|
|
||||||
env_vars.push(("CARCH", flags.carch.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export PKG_CONFIG_SYSROOT_DIR for pkg-config and a general DEPOT_ROOTFS
|
|
||||||
if !flags.rootfs.is_empty() && flags.rootfs != "/" {
|
if !flags.rootfs.is_empty() && flags.rootfs != "/" {
|
||||||
env_vars.push(("PKG_CONFIG_SYSROOT_DIR", flags.rootfs.clone()));
|
crate::builder::set_env_var(
|
||||||
|
&mut env_vars,
|
||||||
|
"PKG_CONFIG_SYSROOT_DIR",
|
||||||
|
flags.rootfs.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
env_vars.push(("DEPOT_ROOTFS", flags.rootfs.clone()));
|
|
||||||
|
|
||||||
if !state.is_done(BuildStep::PostCompileDone) {
|
if !state.is_done(BuildStep::PostCompileDone) {
|
||||||
println!("Running makefile build commands...");
|
println!("Running makefile build commands...");
|
||||||
@@ -95,7 +73,10 @@ pub fn build(
|
|||||||
cmd.current_dir(src_dir);
|
cmd.current_dir(src_dir);
|
||||||
|
|
||||||
let mut install_env = env_vars.clone();
|
let mut install_env = env_vars.clone();
|
||||||
install_env.push(("DESTDIR", destdir.to_string_lossy().into_owned()));
|
install_env.push((
|
||||||
|
"DESTDIR".to_string(),
|
||||||
|
destdir.to_string_lossy().into_owned(),
|
||||||
|
));
|
||||||
crate::builder::prepare_command(&mut cmd, &install_env);
|
crate::builder::prepare_command(&mut cmd, &install_env);
|
||||||
|
|
||||||
let status = cmd
|
let status = cmd
|
||||||
@@ -130,7 +111,7 @@ mod tests {
|
|||||||
revision: 1,
|
revision: 1,
|
||||||
description: "d".into(),
|
description: "d".into(),
|
||||||
homepage: "h".into(),
|
homepage: "h".into(),
|
||||||
license: "MIT".into(),
|
license: vec!["MIT".into()],
|
||||||
},
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Default::default(),
|
alternatives: Default::default(),
|
||||||
@@ -168,7 +149,7 @@ mod tests {
|
|||||||
"cp built.txt $DESTDIR/usr/bin/installed.txt".into(),
|
"cp built.txt $DESTDIR/usr/bin/installed.txt".into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
build(&spec, src_path, dest_path, None)?;
|
build(&spec, src_path, dest_path, None, true)?;
|
||||||
|
|
||||||
// Verify build step
|
// Verify build step
|
||||||
let built_file = src_path.join("built.txt");
|
let built_file = src_path.join("built.txt");
|
||||||
|
|||||||
+6
-46
@@ -13,6 +13,7 @@ pub fn build(
|
|||||||
src_dir: &Path,
|
src_dir: &Path,
|
||||||
destdir: &Path,
|
destdir: &Path,
|
||||||
cross: Option<&CrossConfig>,
|
cross: Option<&CrossConfig>,
|
||||||
|
export_compiler_flags: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let flags = &spec.build.flags;
|
let flags = &spec.build.flags;
|
||||||
let build_dir = src_dir.join("builddir");
|
let build_dir = src_dir.join("builddir");
|
||||||
@@ -22,51 +23,7 @@ pub fn build(
|
|||||||
fs::create_dir_all(destdir)?;
|
fs::create_dir_all(destdir)?;
|
||||||
|
|
||||||
// Environment variables
|
// Environment variables
|
||||||
let mut env_vars: Vec<(&str, String)> = vec![];
|
let env_vars = crate::builder::standard_build_env(spec, cross, true, export_compiler_flags);
|
||||||
|
|
||||||
// Use cross-compilation tools if configured
|
|
||||||
let cc = if let Some(cc_cfg) = cross {
|
|
||||||
cc_cfg.cc.clone()
|
|
||||||
} else {
|
|
||||||
flags.cc.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
if !flags.cflags.is_empty() {
|
|
||||||
env_vars.push(("CFLAGS", flags.cflags.join(" ")));
|
|
||||||
}
|
|
||||||
if !flags.chost.is_empty() {
|
|
||||||
env_vars.push(("CHOST", flags.chost.clone()));
|
|
||||||
}
|
|
||||||
if !flags.cbuild.is_empty() {
|
|
||||||
env_vars.push(("CBUILD", flags.cbuild.clone()));
|
|
||||||
}
|
|
||||||
if !flags.ldflags.is_empty() {
|
|
||||||
let ldflags = if flags.libc.is_empty() {
|
|
||||||
flags.ldflags.join(" ")
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"{} -Wl,--dynamic-linker={}",
|
|
||||||
flags.ldflags.join(" "),
|
|
||||||
flags.libc
|
|
||||||
)
|
|
||||||
};
|
|
||||||
env_vars.push(("LDFLAGS", ldflags));
|
|
||||||
}
|
|
||||||
env_vars.push(("CC", cc));
|
|
||||||
|
|
||||||
// Export rootfs for build scripts
|
|
||||||
env_vars.push(("DEPOT_ROOTFS", flags.rootfs.clone()));
|
|
||||||
|
|
||||||
// Add cross-compilation env
|
|
||||||
if let Some(cc_cfg) = cross {
|
|
||||||
env_vars.push(("CXX", cc_cfg.cxx.clone()));
|
|
||||||
env_vars.push(("AR", cc_cfg.ar.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// CARCH support
|
|
||||||
if !flags.carch.is_empty() {
|
|
||||||
env_vars.push(("CARCH", flags.carch.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract prefix from configure flags
|
// Extract prefix from configure flags
|
||||||
let prefix = flags
|
let prefix = flags
|
||||||
@@ -150,7 +107,10 @@ pub fn build(
|
|||||||
install_cmd.arg("-C").arg(&build_dir);
|
install_cmd.arg("-C").arg(&build_dir);
|
||||||
|
|
||||||
let mut install_env = env_vars.clone();
|
let mut install_env = env_vars.clone();
|
||||||
install_env.push(("DESTDIR", destdir.to_string_lossy().into_owned()));
|
install_env.push((
|
||||||
|
"DESTDIR".to_string(),
|
||||||
|
destdir.to_string_lossy().into_owned(),
|
||||||
|
));
|
||||||
crate::builder::prepare_command(&mut install_cmd, &install_env);
|
crate::builder::prepare_command(&mut install_cmd, &install_env);
|
||||||
|
|
||||||
let status = install_cmd
|
let status = install_cmd
|
||||||
|
|||||||
+240
-18
@@ -15,8 +15,112 @@ use anyhow::Result;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
pub type EnvVars = Vec<(String, String)>;
|
||||||
|
|
||||||
|
pub fn set_env_var(env_vars: &mut EnvVars, key: &str, value: impl Into<String>) {
|
||||||
|
let value = value.into();
|
||||||
|
if let Some((_, existing)) = env_vars.iter_mut().find(|(k, _)| k == key) {
|
||||||
|
*existing = value;
|
||||||
|
} else {
|
||||||
|
env_vars.push((key.to_string(), value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn standard_build_env(
|
||||||
|
spec: &PackageSpec,
|
||||||
|
cross: Option<&CrossConfig>,
|
||||||
|
include_compiler_env: bool,
|
||||||
|
export_compiler_flags: bool,
|
||||||
|
) -> EnvVars {
|
||||||
|
let flags = &spec.build.flags;
|
||||||
|
let mut env_vars: EnvVars = Vec::new();
|
||||||
|
let export_compiler_flags = export_compiler_flags && !flags.no_flags;
|
||||||
|
|
||||||
|
if include_compiler_env && export_compiler_flags {
|
||||||
|
if !flags.cflags.is_empty() {
|
||||||
|
set_env_var(&mut env_vars, "CFLAGS", flags.cflags.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ldflags = if !flags.ldflags.is_empty() || !flags.libc.is_empty() {
|
||||||
|
if flags.libc.is_empty() {
|
||||||
|
flags.ldflags.join(" ")
|
||||||
|
} else if flags.ldflags.is_empty() {
|
||||||
|
format!("-Wl,--dynamic-linker={}", flags.libc)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} -Wl,--dynamic-linker={}",
|
||||||
|
flags.ldflags.join(" "),
|
||||||
|
flags.libc
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
if !ldflags.is_empty() {
|
||||||
|
set_env_var(&mut env_vars, "LDFLAGS", ldflags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !flags.chost.is_empty() {
|
||||||
|
set_env_var(&mut env_vars, "CHOST", flags.chost.clone());
|
||||||
|
}
|
||||||
|
if !flags.cbuild.is_empty() {
|
||||||
|
set_env_var(&mut env_vars, "CBUILD", flags.cbuild.clone());
|
||||||
|
}
|
||||||
|
if !flags.carch.is_empty() {
|
||||||
|
set_env_var(&mut env_vars, "CARCH", flags.carch.clone());
|
||||||
|
}
|
||||||
|
if !flags.prefix.is_empty() {
|
||||||
|
set_env_var(&mut env_vars, "PREFIX", flags.prefix.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
set_env_var(&mut env_vars, "DEPOT_ROOTFS", flags.rootfs.clone());
|
||||||
|
set_env_var(
|
||||||
|
&mut env_vars,
|
||||||
|
"DEPOT_SPECDIR",
|
||||||
|
spec.spec_dir.to_string_lossy().into_owned(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if include_compiler_env {
|
||||||
|
if let Some(cc_cfg) = cross {
|
||||||
|
set_env_var(&mut env_vars, "CC", cc_cfg.cc.clone());
|
||||||
|
set_env_var(&mut env_vars, "CXX", cc_cfg.cxx.clone());
|
||||||
|
set_env_var(&mut env_vars, "AR", cc_cfg.ar.clone());
|
||||||
|
set_env_var(&mut env_vars, "RANLIB", cc_cfg.ranlib.clone());
|
||||||
|
set_env_var(&mut env_vars, "STRIP", cc_cfg.strip.clone());
|
||||||
|
set_env_var(&mut env_vars, "LD", cc_cfg.ld.clone());
|
||||||
|
set_env_var(&mut env_vars, "NM", cc_cfg.nm.clone());
|
||||||
|
set_env_var(&mut env_vars, "CROSS_PREFIX", cc_cfg.prefix.clone());
|
||||||
|
set_env_var(
|
||||||
|
&mut env_vars,
|
||||||
|
"CROSS_COMPILE",
|
||||||
|
format!("{}-", cc_cfg.prefix),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
set_env_var(&mut env_vars, "CC", flags.cc.clone());
|
||||||
|
set_env_var(&mut env_vars, "CXX", flags.cxx.clone());
|
||||||
|
set_env_var(&mut env_vars, "AR", flags.ar.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in &flags.passthrough_env {
|
||||||
|
let key = key.trim();
|
||||||
|
if key.is_empty() || key.contains('=') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if env_vars.iter().any(|(existing, _)| existing == key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(value) = std::env::var(key) {
|
||||||
|
set_env_var(&mut env_vars, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env_vars
|
||||||
|
}
|
||||||
|
|
||||||
/// Prepare a Command with a hermetic environment and some essential variables preserved.
|
/// Prepare a Command with a hermetic environment and some essential variables preserved.
|
||||||
pub fn prepare_command(cmd: &mut Command, env_vars: &[(&str, String)]) {
|
pub fn prepare_command(cmd: &mut Command, env_vars: &EnvVars) {
|
||||||
cmd.env_clear();
|
cmd.env_clear();
|
||||||
// Preserve essential environment variables
|
// Preserve essential environment variables
|
||||||
for var in &["PATH", "LANG", "HOME", "SHELL", "DESTDIR", "DEPOT_ROOTFS"] {
|
for var in &["PATH", "LANG", "HOME", "SHELL", "DESTDIR", "DEPOT_ROOTFS"] {
|
||||||
@@ -36,6 +140,7 @@ pub fn build(
|
|||||||
src_dir: &Path,
|
src_dir: &Path,
|
||||||
destdir: &Path,
|
destdir: &Path,
|
||||||
cross: Option<&CrossConfig>,
|
cross: Option<&CrossConfig>,
|
||||||
|
export_compiler_flags: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some(cc) = cross {
|
if let Some(cc) = cross {
|
||||||
println!(
|
println!(
|
||||||
@@ -52,18 +157,58 @@ pub fn build(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match spec.build.build_type {
|
match spec.build.build_type {
|
||||||
BuildType::Autotools => autotools::build(spec, src_dir, destdir, cross),
|
BuildType::Autotools => {
|
||||||
BuildType::CMake => cmake::build(spec, src_dir, destdir, cross),
|
autotools::build(spec, src_dir, destdir, cross, export_compiler_flags)
|
||||||
BuildType::Meson => meson::build(spec, src_dir, destdir, cross),
|
}
|
||||||
BuildType::Custom => custom::build(spec, src_dir, destdir, cross),
|
BuildType::CMake => cmake::build(spec, src_dir, destdir, cross, export_compiler_flags),
|
||||||
BuildType::Rust => rust::build(spec, src_dir, destdir, cross),
|
BuildType::Meson => meson::build(spec, src_dir, destdir, cross, export_compiler_flags),
|
||||||
BuildType::Bin => bin::build(spec, src_dir, destdir, cross),
|
BuildType::Custom => custom::build(spec, src_dir, destdir, cross, export_compiler_flags),
|
||||||
BuildType::Makefile => makefile::build(spec, src_dir, destdir, cross),
|
BuildType::Rust => rust::build(spec, src_dir, destdir, cross, export_compiler_flags),
|
||||||
|
BuildType::Bin => bin::build(spec, src_dir, destdir, cross, export_compiler_flags),
|
||||||
|
BuildType::Makefile => {
|
||||||
|
makefile::build(spec, src_dir, destdir, cross, export_compiler_flags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn mk_spec(cflags: Vec<&str>, ldflags: Vec<&str>) -> PackageSpec {
|
||||||
|
let mut flags = BuildFlags::default();
|
||||||
|
flags.cflags = cflags.into_iter().map(String::from).collect();
|
||||||
|
flags.ldflags = ldflags.into_iter().map(String::from).collect();
|
||||||
|
PackageSpec {
|
||||||
|
package: PackageInfo {
|
||||||
|
name: "env-test".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "d".into(),
|
||||||
|
homepage: "h".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: Default::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: vec![crate::package::Source {
|
||||||
|
url: "https://example.test/src.tar.gz".into(),
|
||||||
|
sha256: "abc".into(),
|
||||||
|
extract_dir: "src".into(),
|
||||||
|
patches: Vec::new(),
|
||||||
|
post_extract: Vec::new(),
|
||||||
|
}],
|
||||||
|
build: Build {
|
||||||
|
build_type: BuildType::Custom,
|
||||||
|
flags,
|
||||||
|
},
|
||||||
|
dependencies: Dependencies::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_prepare_command() {
|
fn test_prepare_command() {
|
||||||
@@ -77,19 +222,19 @@ mod tests {
|
|||||||
std::env::set_var("DEPOT_ROOTFS", "/my/rootfs");
|
std::env::set_var("DEPOT_ROOTFS", "/my/rootfs");
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare_command(&mut cmd, &[("MYVAR", "myval".to_string())]);
|
prepare_command(&mut cmd, &vec![("MYVAR".to_string(), "myval".to_string())]);
|
||||||
|
|
||||||
let envs: std::collections::HashMap<_, _> = cmd.get_envs().collect();
|
let envs: HashMap<_, _> = cmd.get_envs().collect();
|
||||||
assert!(envs.get(std::ffi::OsStr::new("PATH")).is_some());
|
assert!(envs.get(OsStr::new("PATH")).is_some());
|
||||||
assert!(envs.get(std::ffi::OsStr::new("HOME")).is_some());
|
assert!(envs.get(OsStr::new("HOME")).is_some());
|
||||||
assert!(envs.get(std::ffi::OsStr::new("FORBIDDEN")).is_none());
|
assert!(envs.get(OsStr::new("FORBIDDEN")).is_none());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
envs.get(std::ffi::OsStr::new("MYVAR")),
|
envs.get(OsStr::new("MYVAR")),
|
||||||
Some(&Some(std::ffi::OsString::from("myval").as_os_str()))
|
Some(&Some(std::ffi::OsString::from("myval").as_os_str()))
|
||||||
);
|
);
|
||||||
// DEPOT_ROOTFS should be preserved from the parent environment
|
// DEPOT_ROOTFS should be preserved from the parent environment
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
envs.get(std::ffi::OsStr::new("DEPOT_ROOTFS")),
|
envs.get(OsStr::new("DEPOT_ROOTFS")),
|
||||||
Some(&Some(std::ffi::OsString::from("/my/rootfs").as_os_str()))
|
Some(&Some(std::ffi::OsString::from("/my/rootfs").as_os_str()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -100,11 +245,88 @@ mod tests {
|
|||||||
unsafe {
|
unsafe {
|
||||||
std::env::set_var("DESTDIR", "/tmp/dest");
|
std::env::set_var("DESTDIR", "/tmp/dest");
|
||||||
}
|
}
|
||||||
prepare_command(&mut cmd, &[]);
|
prepare_command(&mut cmd, &Vec::new());
|
||||||
let envs: std::collections::HashMap<_, _> = cmd.get_envs().collect();
|
let envs: HashMap<_, _> = cmd.get_envs().collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
envs.get(std::ffi::OsStr::new("DESTDIR")),
|
envs.get(OsStr::new("DESTDIR")),
|
||||||
Some(&Some(std::ffi::OsString::from("/tmp/dest").as_os_str()))
|
Some(&Some(std::ffi::OsString::from("/tmp/dest").as_os_str()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_standard_build_env_respects_export_compiler_flags_toggle() {
|
||||||
|
let spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
|
||||||
|
|
||||||
|
let enabled = standard_build_env(&spec, None, true, true);
|
||||||
|
assert!(
|
||||||
|
enabled.iter().any(|(k, v)| k == "CFLAGS" && v == "-O2"),
|
||||||
|
"expected CFLAGS to be exported when enabled"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
enabled
|
||||||
|
.iter()
|
||||||
|
.any(|(k, v)| k == "LDFLAGS" && v == "-Wl,--as-needed"),
|
||||||
|
"expected LDFLAGS to be exported when enabled"
|
||||||
|
);
|
||||||
|
|
||||||
|
let disabled = standard_build_env(&spec, None, true, false);
|
||||||
|
assert!(
|
||||||
|
!disabled.iter().any(|(k, _)| k == "CFLAGS"),
|
||||||
|
"expected CFLAGS to be omitted when disabled"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!disabled.iter().any(|(k, _)| k == "CXXFLAGS"),
|
||||||
|
"expected CXXFLAGS to be omitted when disabled"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!disabled.iter().any(|(k, _)| k == "LDFLAGS"),
|
||||||
|
"expected LDFLAGS to be omitted when disabled"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut disabled_by_spec = spec.clone();
|
||||||
|
disabled_by_spec.build.flags.no_flags = true;
|
||||||
|
let disabled_env = standard_build_env(&disabled_by_spec, None, true, true);
|
||||||
|
assert!(
|
||||||
|
!disabled_env.iter().any(|(k, _)| k == "CFLAGS"),
|
||||||
|
"expected CFLAGS to be omitted when no_flags is set in spec"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!disabled_env.iter().any(|(k, _)| k == "LDFLAGS"),
|
||||||
|
"expected LDFLAGS to be omitted when no_flags is set in spec"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_standard_build_env_exports_passthrough_env() {
|
||||||
|
let mut spec = mk_spec(Vec::new(), Vec::new());
|
||||||
|
spec.build.flags.passthrough_env = vec!["RUSTFLAGS".into()];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("RUSTFLAGS", "-C target-cpu=native");
|
||||||
|
}
|
||||||
|
|
||||||
|
let env = standard_build_env(&spec, None, false, true);
|
||||||
|
assert!(
|
||||||
|
env.iter()
|
||||||
|
.any(|(k, v)| k == "RUSTFLAGS" && v == "-C target-cpu=native"),
|
||||||
|
"expected RUSTFLAGS to be copied from parent environment"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_standard_build_env_passthrough_does_not_override_default_vars() {
|
||||||
|
let mut spec = mk_spec(Vec::new(), Vec::new());
|
||||||
|
spec.build.flags.cc = "spec-cc".to_string();
|
||||||
|
spec.build.flags.passthrough_env = vec!["CC".into()];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("CC", "host-cc");
|
||||||
|
}
|
||||||
|
|
||||||
|
let env = standard_build_env(&spec, None, true, true);
|
||||||
|
assert!(
|
||||||
|
env.iter().any(|(k, v)| k == "CC" && v == "spec-cc"),
|
||||||
|
"expected default CC to take precedence over passthrough CC"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-14
@@ -13,6 +13,7 @@ pub fn build(
|
|||||||
src_dir: &Path,
|
src_dir: &Path,
|
||||||
destdir: &Path,
|
destdir: &Path,
|
||||||
cross: Option<&CrossConfig>,
|
cross: Option<&CrossConfig>,
|
||||||
|
export_compiler_flags: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let flags = &spec.build.flags;
|
let flags = &spec.build.flags;
|
||||||
|
|
||||||
@@ -45,16 +46,12 @@ pub fn build(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build environment
|
// Build environment
|
||||||
let mut env_vars: Vec<(&str, String)> = vec![];
|
let mut env_vars =
|
||||||
|
crate::builder::standard_build_env(spec, cross, false, export_compiler_flags);
|
||||||
|
|
||||||
// RUSTFLAGS
|
// RUSTFLAGS
|
||||||
if !flags.rustflags.is_empty() {
|
if !flags.rustflags.is_empty() {
|
||||||
env_vars.push(("RUSTFLAGS", flags.rustflags.join(" ")));
|
crate::builder::set_env_var(&mut env_vars, "RUSTFLAGS", flags.rustflags.join(" "));
|
||||||
}
|
|
||||||
|
|
||||||
// CARCH support
|
|
||||||
if !flags.carch.is_empty() {
|
|
||||||
env_vars.push(("CARCH", flags.carch.clone()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If cross-compiling, set linker via CARGO_TARGET_*_LINKER
|
// If cross-compiling, set linker via CARGO_TARGET_*_LINKER
|
||||||
@@ -66,19 +63,16 @@ pub fn build(
|
|||||||
.to_uppercase()
|
.to_uppercase()
|
||||||
.replace('-', "_");
|
.replace('-', "_");
|
||||||
let linker_var = format!("CARGO_TARGET_{}_LINKER", target_env);
|
let linker_var = format!("CARGO_TARGET_{}_LINKER", target_env);
|
||||||
env_vars.push((linker_var.leak(), cc_cfg.cc.clone()));
|
crate::builder::set_env_var(&mut env_vars, &linker_var, cc_cfg.cc.clone());
|
||||||
env_vars.push(("CC".to_string().leak(), cc_cfg.cc.clone()));
|
crate::builder::set_env_var(&mut env_vars, "CC", cc_cfg.cc.clone());
|
||||||
env_vars.push(("AR".to_string().leak(), cc_cfg.ar.clone()));
|
crate::builder::set_env_var(&mut env_vars, "AR", cc_cfg.ar.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default rustup toolchain if not already set
|
// Set default rustup toolchain if not already set
|
||||||
if std::env::var("RUSTUP_TOOLCHAIN").is_err() {
|
if std::env::var("RUSTUP_TOOLCHAIN").is_err() {
|
||||||
env_vars.push(("RUSTUP_TOOLCHAIN", "stable".to_string()));
|
crate::builder::set_env_var(&mut env_vars, "RUSTUP_TOOLCHAIN", "stable");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export DEPOT_ROOTFS so build scripts can detect the target rootfs
|
|
||||||
env_vars.push(("DEPOT_ROOTFS", flags.rootfs.clone()));
|
|
||||||
|
|
||||||
// Run cargo build
|
// Run cargo build
|
||||||
println!(
|
println!(
|
||||||
"Running cargo build ({})...",
|
"Running cargo build ({})...",
|
||||||
|
|||||||
+25
-7
@@ -128,7 +128,10 @@ impl Config {
|
|||||||
|
|
||||||
if host_build.exists() && host_build != build_path {
|
if host_build.exists() && host_build != build_path {
|
||||||
let content = fs::read_to_string(&host_build).with_context(|| {
|
let content = fs::read_to_string(&host_build).with_context(|| {
|
||||||
format!("Failed to read system build config: {}", host_build.display())
|
format!(
|
||||||
|
"Failed to read system build config: {}",
|
||||||
|
host_build.display()
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
let (val, appends) = self.preprocess_toml(&content)?;
|
let (val, appends) = self.preprocess_toml(&content)?;
|
||||||
merge_toml_values(&mut self.build_overrides, &val);
|
merge_toml_values(&mut self.build_overrides, &val);
|
||||||
@@ -139,7 +142,10 @@ impl Config {
|
|||||||
|
|
||||||
if build_path.exists() {
|
if build_path.exists() {
|
||||||
let content = fs::read_to_string(&build_path).with_context(|| {
|
let content = fs::read_to_string(&build_path).with_context(|| {
|
||||||
format!("Failed to read system build config: {}", build_path.display())
|
format!(
|
||||||
|
"Failed to read system build config: {}",
|
||||||
|
build_path.display()
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
let (val, appends) = self.preprocess_toml(&content)?;
|
let (val, appends) = self.preprocess_toml(&content)?;
|
||||||
merge_toml_values(&mut self.build_overrides, &val);
|
merge_toml_values(&mut self.build_overrides, &val);
|
||||||
@@ -153,7 +159,10 @@ impl Config {
|
|||||||
|
|
||||||
if host_package.exists() && host_package != package_path {
|
if host_package.exists() && host_package != package_path {
|
||||||
let content = fs::read_to_string(&host_package).with_context(|| {
|
let content = fs::read_to_string(&host_package).with_context(|| {
|
||||||
format!("Failed to read system package config: {}", host_package.display())
|
format!(
|
||||||
|
"Failed to read system package config: {}",
|
||||||
|
host_package.display()
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
let (val, appends) = self.preprocess_toml(&content)?;
|
let (val, appends) = self.preprocess_toml(&content)?;
|
||||||
merge_toml_values(&mut self.package_overrides, &val);
|
merge_toml_values(&mut self.package_overrides, &val);
|
||||||
@@ -164,7 +173,10 @@ impl Config {
|
|||||||
|
|
||||||
if package_path.exists() {
|
if package_path.exists() {
|
||||||
let content = fs::read_to_string(&package_path).with_context(|| {
|
let content = fs::read_to_string(&package_path).with_context(|| {
|
||||||
format!("Failed to read system package config: {}", package_path.display())
|
format!(
|
||||||
|
"Failed to read system package config: {}",
|
||||||
|
package_path.display()
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
let (val, appends) = self.preprocess_toml(&content)?;
|
let (val, appends) = self.preprocess_toml(&content)?;
|
||||||
merge_toml_values(&mut self.package_overrides, &val);
|
merge_toml_values(&mut self.package_overrides, &val);
|
||||||
@@ -372,7 +384,9 @@ cflags += ["-g"]
|
|||||||
// Save and restore the original value so other tests aren't affected.
|
// Save and restore the original value so other tests aren't affected.
|
||||||
let orig_home = std::env::var_os("HOME");
|
let orig_home = std::env::var_os("HOME");
|
||||||
let fake_home = tempfile::tempdir().unwrap();
|
let fake_home = tempfile::tempdir().unwrap();
|
||||||
unsafe { std::env::set_var("HOME", &fake_home.path()); }
|
unsafe {
|
||||||
|
std::env::set_var("HOME", &fake_home.path());
|
||||||
|
}
|
||||||
|
|
||||||
let config = Config::for_rootfs(Path::new("/"));
|
let config = Config::for_rootfs(Path::new("/"));
|
||||||
|
|
||||||
@@ -392,9 +406,13 @@ cflags += ["-g"]
|
|||||||
|
|
||||||
// restore original HOME
|
// restore original HOME
|
||||||
if let Some(v) = orig_home {
|
if let Some(v) = orig_home {
|
||||||
unsafe { std::env::set_var("HOME", v); }
|
unsafe {
|
||||||
|
std::env::set_var("HOME", v);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
unsafe { std::env::remove_var("HOME"); }
|
unsafe {
|
||||||
|
std::env::remove_var("HOME");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-12
@@ -203,12 +203,16 @@ endian = 'little'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a cross-compilation tool in PATH
|
/// Find a cross-compilation tool in PATH and return its absolute path
|
||||||
fn find_tool(prefix: &str, suffixes: &[&str], required: bool) -> Result<String> {
|
fn find_tool(prefix: &str, suffixes: &[&str], required: bool) -> Result<String> {
|
||||||
for suffix in suffixes {
|
for suffix in suffixes {
|
||||||
let tool_name = format!("{}-{}", prefix, suffix);
|
let tool_name = format!("{}-{}", prefix, suffix);
|
||||||
if tool_exists(&tool_name) {
|
// Use `which` to resolve to an absolute path (so callers get a usable path)
|
||||||
return Ok(tool_name);
|
if let Ok(output) = Command::new("which").arg(&tool_name).output() {
|
||||||
|
if output.status.success() {
|
||||||
|
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +226,3 @@ fn find_tool(prefix: &str, suffixes: &[&str], required: bool) -> Result<String>
|
|||||||
|
|
||||||
Err(anyhow::anyhow!("Tool not found"))
|
Err(anyhow::anyhow!("Tool not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a tool exists in PATH
|
|
||||||
fn tool_exists(name: &str) -> bool {
|
|
||||||
Command::new("which")
|
|
||||||
.arg(name)
|
|
||||||
.output()
|
|
||||||
.map(|o| o.status.success())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|||||||
+20
-20
@@ -9,6 +9,10 @@ use rusqlite::{Connection, params};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn format_licenses(licenses: &[String]) -> String {
|
||||||
|
licenses.join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize database and register a package
|
/// Initialize database and register a package
|
||||||
pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> Result<()> {
|
pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> Result<()> {
|
||||||
// Create parent directory (auto-create db dir if missing)
|
// Create parent directory (auto-create db dir if missing)
|
||||||
@@ -40,7 +44,7 @@ pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> R
|
|||||||
spec.package.revision,
|
spec.package.revision,
|
||||||
spec.package.description,
|
spec.package.description,
|
||||||
spec.package.homepage,
|
spec.package.homepage,
|
||||||
spec.package.license,
|
format_licenses(&spec.package.license),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -405,16 +409,11 @@ fn init_db(conn: &Connection) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decide whether a conflicting path is safe to auto-remove ownership for.
|
/// Decide whether a conflicting path is safe to auto-remove ownership for.
|
||||||
/// This covers shared index files (e.g. `usr/share/info/dir`) and common
|
/// This covers common language-shared trees such as Perl site/vendor/lib
|
||||||
/// language-shared trees such as Perl site/vendor/lib directories.
|
/// directories.
|
||||||
fn is_auto_removable_path(path: &str) -> bool {
|
fn is_auto_removable_path(path: &str) -> bool {
|
||||||
let p = path.trim_start_matches('/');
|
let p = path.trim_start_matches('/');
|
||||||
|
|
||||||
// Exact info index file (and compressed variants)
|
|
||||||
if p == "usr/share/info/dir" || p.starts_with("usr/share/info/dir.") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perl shared trees (common multi-package locations)
|
// Perl shared trees (common multi-package locations)
|
||||||
if p.starts_with("usr/lib/perl")
|
if p.starts_with("usr/lib/perl")
|
||||||
|| p.starts_with("usr/share/perl")
|
|| p.starts_with("usr/share/perl")
|
||||||
@@ -475,7 +474,7 @@ mod tests {
|
|||||||
revision: 1,
|
revision: 1,
|
||||||
description: "d".into(),
|
description: "d".into(),
|
||||||
homepage: "h".into(),
|
homepage: "h".into(),
|
||||||
license: "MIT".into(),
|
license: vec!["MIT".into()],
|
||||||
},
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Alternatives {
|
alternatives: Alternatives {
|
||||||
@@ -578,27 +577,27 @@ mod tests {
|
|||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let db_path = tmp.path().join("packages.db");
|
let db_path = tmp.path().join("packages.db");
|
||||||
|
|
||||||
// Install package 'alpha' owning usr/share/info/dir
|
// Install package 'alpha' owning a known shared Perl path
|
||||||
let spec_a = mk_spec("alpha", "1.0");
|
let spec_a = mk_spec("alpha", "1.0");
|
||||||
let dest_a = tmp.path().join("dest_a");
|
let dest_a = tmp.path().join("dest_a");
|
||||||
std::fs::create_dir_all(dest_a.join("usr/share/info")).unwrap();
|
std::fs::create_dir_all(dest_a.join("usr/share/perl5")).unwrap();
|
||||||
std::fs::write(dest_a.join("usr/share/info/dir"), "index").unwrap();
|
std::fs::write(dest_a.join("usr/share/perl5/shared.pm"), "package A;").unwrap();
|
||||||
register_package(&db_path, &spec_a, &dest_a).unwrap();
|
register_package(&db_path, &spec_a, &dest_a).unwrap();
|
||||||
|
|
||||||
// Now install package 'beta' that also provides usr/share/info/dir -> should auto-clear
|
// Now install package 'beta' that also provides the same shared path -> should auto-clear
|
||||||
let spec_b = mk_spec("beta", "1.0");
|
let spec_b = mk_spec("beta", "1.0");
|
||||||
let dest_b = tmp.path().join("dest_b");
|
let dest_b = tmp.path().join("dest_b");
|
||||||
std::fs::create_dir_all(dest_b.join("usr/share/info")).unwrap();
|
std::fs::create_dir_all(dest_b.join("usr/share/perl5")).unwrap();
|
||||||
std::fs::write(dest_b.join("usr/share/info/dir"), "index2").unwrap();
|
std::fs::write(dest_b.join("usr/share/perl5/shared.pm"), "package B;").unwrap();
|
||||||
|
|
||||||
// This should succeed and transfer ownership of the 'dir' path to beta
|
// This should succeed and transfer ownership of the shared path to beta
|
||||||
register_package(&db_path, &spec_b, &dest_b).unwrap();
|
register_package(&db_path, &spec_b, &dest_b).unwrap();
|
||||||
|
|
||||||
// Verify DB: alpha should no longer own the path, beta should
|
// Verify DB: alpha should no longer own the path, beta should
|
||||||
let files_a = get_package_files(&db_path, "alpha").unwrap();
|
let files_a = get_package_files(&db_path, "alpha").unwrap();
|
||||||
assert!(!files_a.contains(&"usr/share/info/dir".to_string()));
|
assert!(!files_a.contains(&"usr/share/perl5/shared.pm".to_string()));
|
||||||
let files_b = get_package_files(&db_path, "beta").unwrap();
|
let files_b = get_package_files(&db_path, "beta").unwrap();
|
||||||
assert!(files_b.contains(&"usr/share/info/dir".to_string()));
|
assert!(files_b.contains(&"usr/share/perl5/shared.pm".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -668,7 +667,7 @@ mod tests {
|
|||||||
std::fs::write(dest1.join("usr/bin/shared_dir/old_file"), "old").unwrap();
|
std::fs::write(dest1.join("usr/bin/shared_dir/old_file"), "old").unwrap();
|
||||||
|
|
||||||
register_package(&db_path, &spec_v1, &dest1).unwrap();
|
register_package(&db_path, &spec_v1, &dest1).unwrap();
|
||||||
let _ = crate::staging::install_atomic(&dest1, &rootfs, &tx_base, &[]).unwrap();
|
let _ = crate::staging::install_atomic(&dest1, &rootfs, &tx_base, &[], &[]).unwrap();
|
||||||
|
|
||||||
assert!(rootfs.join("usr/bin/foo").exists());
|
assert!(rootfs.join("usr/bin/foo").exists());
|
||||||
assert!(rootfs.join("usr/bin/shared_dir/old_file").exists());
|
assert!(rootfs.join("usr/bin/shared_dir/old_file").exists());
|
||||||
@@ -689,7 +688,8 @@ mod tests {
|
|||||||
vec!["usr/bin/shared_dir/old_file".to_string()]
|
vec!["usr/bin/shared_dir/old_file".to_string()]
|
||||||
);
|
);
|
||||||
|
|
||||||
let tx = crate::staging::install_atomic(&dest2, &rootfs, &tx_base, &remove_paths).unwrap();
|
let tx =
|
||||||
|
crate::staging::install_atomic(&dest2, &rootfs, &tx_base, &remove_paths, &[]).unwrap();
|
||||||
register_package(&db_path, &spec_v2, &dest2).unwrap();
|
register_package(&db_path, &spec_v2, &dest2).unwrap();
|
||||||
tx.commit().unwrap();
|
tx.commit().unwrap();
|
||||||
|
|
||||||
|
|||||||
+80
-12
@@ -6,6 +6,23 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use zstd::stream::write::Encoder;
|
use zstd::stream::write::Encoder;
|
||||||
|
|
||||||
|
fn parse_license_text(metadata: &toml::Value) -> Option<String> {
|
||||||
|
if let Some(s) = metadata.get("license").and_then(|v| v.as_str()) {
|
||||||
|
return Some(s.to_string());
|
||||||
|
}
|
||||||
|
if let Some(arr) = metadata.get("license").and_then(|v| v.as_array()) {
|
||||||
|
let licenses: Vec<String> = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
if !licenses.is_empty() {
|
||||||
|
return Some(licenses.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RepoManager {
|
pub struct RepoManager {
|
||||||
pub repo_dir: PathBuf,
|
pub repo_dir: PathBuf,
|
||||||
}
|
}
|
||||||
@@ -129,10 +146,7 @@ impl RepoManager {
|
|||||||
.get("homepage")
|
.get("homepage")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(String::from);
|
.map(String::from);
|
||||||
license = metadata
|
license = parse_license_text(&metadata);
|
||||||
.get("license")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
if let Some(provides_arr) = metadata.get("provides").and_then(|v| v.as_array()) {
|
if let Some(provides_arr) = metadata.get("provides").and_then(|v| v.as_array()) {
|
||||||
provides = provides_arr
|
provides = provides_arr
|
||||||
@@ -209,8 +223,11 @@ impl RepoManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronize git mirrors into /usr/src/depot/<reponame>
|
/// Synchronize git mirrors into /usr/src/depot/<reponame>
|
||||||
pub fn sync_mirrors(repo_dir: &std::path::Path, mirrors: &std::collections::HashMap<String, String>) -> Result<()> {
|
pub fn sync_mirrors(
|
||||||
use git2::{Repository, FetchOptions, Cred, RemoteCallbacks, ResetType, build::RepoBuilder};
|
repo_dir: &std::path::Path,
|
||||||
|
mirrors: &std::collections::HashMap<String, String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
use git2::{Cred, FetchOptions, RemoteCallbacks, Repository, ResetType, build::RepoBuilder};
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
let base = repo_dir.to_path_buf();
|
let base = repo_dir.to_path_buf();
|
||||||
@@ -235,7 +252,9 @@ pub fn sync_mirrors(repo_dir: &std::path::Path, mirrors: &std::collections::Hash
|
|||||||
|
|
||||||
let mut builder = RepoBuilder::new();
|
let mut builder = RepoBuilder::new();
|
||||||
builder.fetch_options(fo);
|
builder.fetch_options(fo);
|
||||||
builder.clone(url, &target).with_context(|| format!("Failed to clone {}", url))?;
|
builder
|
||||||
|
.clone(url, &target)
|
||||||
|
.with_context(|| format!("Failed to clone {}", url))?;
|
||||||
} else {
|
} else {
|
||||||
println!("Updating mirror '{}' in {}", name, target.display());
|
println!("Updating mirror '{}' in {}", name, target.display());
|
||||||
// Open repository and fetch updates
|
// Open repository and fetch updates
|
||||||
@@ -251,8 +270,11 @@ pub fn sync_mirrors(repo_dir: &std::path::Path, mirrors: &std::collections::Hash
|
|||||||
fo.remote_callbacks(cb);
|
fo.remote_callbacks(cb);
|
||||||
|
|
||||||
// Fetch from origin
|
// Fetch from origin
|
||||||
let mut remote = repo.find_remote("origin").or_else(|_| repo.remote_anonymous(url))?;
|
let mut remote = repo
|
||||||
remote.fetch(&["refs/heads/*:refs/remotes/origin/*"], Some(&mut fo), None)
|
.find_remote("origin")
|
||||||
|
.or_else(|_| repo.remote_anonymous(url))?;
|
||||||
|
remote
|
||||||
|
.fetch(&["refs/heads/*:refs/remotes/origin/*"], Some(&mut fo), None)
|
||||||
.with_context(|| format!("Failed to fetch updates for {}", url))?;
|
.with_context(|| format!("Failed to fetch updates for {}", url))?;
|
||||||
|
|
||||||
// Try to fast-forward HEAD to origin/HEAD by resetting to FETCH_HEAD if present
|
// Try to fast-forward HEAD to origin/HEAD by resetting to FETCH_HEAD if present
|
||||||
@@ -280,7 +302,10 @@ pub fn sync_mirrors(repo_dir: &std::path::Path, mirrors: &std::collections::Hash
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Show status for each mirror repository: path, exists, branch/HEAD, latest commit, dirty
|
/// Show status for each mirror repository: path, exists, branch/HEAD, latest commit, dirty
|
||||||
pub fn mirrors_status(repo_dir: &std::path::Path, mirrors: &std::collections::HashMap<String, String>) -> Result<()> {
|
pub fn mirrors_status(
|
||||||
|
repo_dir: &std::path::Path,
|
||||||
|
mirrors: &std::collections::HashMap<String, String>,
|
||||||
|
) -> Result<()> {
|
||||||
use git2::Repository;
|
use git2::Repository;
|
||||||
|
|
||||||
let base = repo_dir.to_path_buf();
|
let base = repo_dir.to_path_buf();
|
||||||
@@ -308,7 +333,9 @@ pub fn mirrors_status(repo_dir: &std::path::Path, mirrors: &std::collections::Ha
|
|||||||
|
|
||||||
// Latest commit OID
|
// Latest commit OID
|
||||||
let oid = repo.refname_to_id("HEAD").ok();
|
let oid = repo.refname_to_id("HEAD").ok();
|
||||||
let short = oid.map(|o| format!("{}", o)).unwrap_or_else(|| "(unknown)".to_string());
|
let short = oid
|
||||||
|
.map(|o| format!("{}", o))
|
||||||
|
.unwrap_or_else(|| "(unknown)".to_string());
|
||||||
|
|
||||||
// Commit time (seconds since epoch) if available
|
// Commit time (seconds since epoch) if available
|
||||||
let mut commit_time = String::new();
|
let mut commit_time = String::new();
|
||||||
@@ -327,7 +354,11 @@ pub fn mirrors_status(repo_dir: &std::path::Path, mirrors: &std::collections::Ha
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let dirty = statuses.iter().any(|s| s.status().intersects(git2::Status::WT_MODIFIED | git2::Status::WT_NEW | git2::Status::WT_DELETED));
|
let dirty = statuses.iter().any(|s| {
|
||||||
|
s.status().intersects(
|
||||||
|
git2::Status::WT_MODIFIED | git2::Status::WT_NEW | git2::Status::WT_DELETED,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
println!("Path: {}", target.display());
|
println!("Path: {}", target.display());
|
||||||
println!("Branch/HEAD: {}", branch);
|
println!("Branch/HEAD: {}", branch);
|
||||||
@@ -448,4 +479,41 @@ runtime = []
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(provides_count, 1);
|
assert_eq!(provides_count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_index_package_with_multiple_licenses() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let repo_dir = tmp.path();
|
||||||
|
let pkg_path = repo_dir.join("test-1.0-1-x86_64.depot.pkg.tar.zst");
|
||||||
|
|
||||||
|
let file = fs::File::create(&pkg_path).unwrap();
|
||||||
|
let encoder = zstd::stream::write::Encoder::new(file, 3).unwrap();
|
||||||
|
let mut tar = tar::Builder::new(encoder);
|
||||||
|
|
||||||
|
let metadata = r#"
|
||||||
|
name = "test"
|
||||||
|
version = "1.0"
|
||||||
|
revision = 1
|
||||||
|
license = ["MIT", "Apache-2.0"]
|
||||||
|
"#;
|
||||||
|
let mut header = tar::Header::new_gnu();
|
||||||
|
header.set_path(".metadata.toml").unwrap();
|
||||||
|
header.set_size(metadata.len() as u64);
|
||||||
|
header.set_mode(0o644);
|
||||||
|
header.set_cksum();
|
||||||
|
tar.append(&header, metadata.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
let encoder = tar.into_inner().unwrap();
|
||||||
|
encoder.finish().unwrap();
|
||||||
|
|
||||||
|
let mut conn = Connection::open_in_memory().unwrap();
|
||||||
|
let manager = RepoManager::new(repo_dir.to_path_buf());
|
||||||
|
manager.init_repo_schema(&mut conn).unwrap();
|
||||||
|
manager.index_package(&mut conn, &pkg_path).unwrap();
|
||||||
|
|
||||||
|
let lic: Option<String> = conn
|
||||||
|
.query_row("SELECT license FROM packages", [], |r| r.get(0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(lic, Some("MIT, Apache-2.0".to_string()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+79
@@ -173,10 +173,31 @@ pub fn check_runtime_deps(spec: &PackageSpec, db_path: &Path) -> Result<Vec<Stri
|
|||||||
Ok(missing)
|
Ok(missing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if all test dependencies are satisfied
|
||||||
|
pub fn check_test_deps(spec: &PackageSpec, db_path: &Path) -> Result<Vec<String>> {
|
||||||
|
let mut missing = Vec::new();
|
||||||
|
|
||||||
|
if !db_path.exists() {
|
||||||
|
return Ok(spec.dependencies.test.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let installed = db::get_installed_packages(db_path)?;
|
||||||
|
let provides = db::get_all_provides(db_path)?;
|
||||||
|
|
||||||
|
for dep in &spec.dependencies.test {
|
||||||
|
if !is_dep_satisfied(dep, &installed, &provides, db_path)? {
|
||||||
|
missing.push(dep.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(missing)
|
||||||
|
}
|
||||||
|
|
||||||
/// Print dependency status
|
/// Print dependency status
|
||||||
pub fn print_dep_status(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
pub fn print_dep_status(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
||||||
let missing_build = check_build_deps(spec, db_path)?;
|
let missing_build = check_build_deps(spec, db_path)?;
|
||||||
let missing_runtime = check_runtime_deps(spec, db_path)?;
|
let missing_runtime = check_runtime_deps(spec, db_path)?;
|
||||||
|
let missing_test = check_test_deps(spec, db_path)?;
|
||||||
|
|
||||||
if !spec.dependencies.build.is_empty() {
|
if !spec.dependencies.build.is_empty() {
|
||||||
println!("Build dependencies: {}", spec.dependencies.build.join(", "));
|
println!("Build dependencies: {}", spec.dependencies.build.join(", "));
|
||||||
@@ -195,6 +216,13 @@ pub fn print_dep_status(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !spec.dependencies.test.is_empty() {
|
||||||
|
println!("Test dependencies: {}", spec.dependencies.test.join(", "));
|
||||||
|
if !missing_test.is_empty() {
|
||||||
|
println!(" Missing: {}", missing_test.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +240,20 @@ pub fn require_build_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verify all test dependencies are installed, error if not
|
||||||
|
pub fn require_test_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
||||||
|
let missing = check_test_deps(spec, db_path)?;
|
||||||
|
|
||||||
|
if !missing.is_empty() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Missing test dependencies: {}\nInstall them first with: nyapm install <package>",
|
||||||
|
missing.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -260,4 +302,41 @@ mod tests {
|
|||||||
assert!(compare_versions("a", "b", VersionOp::Lt));
|
assert!(compare_versions("a", "b", VersionOp::Lt));
|
||||||
assert!(compare_versions("foo", "foo", VersionOp::Exact));
|
assert!(compare_versions("foo", "foo", VersionOp::Exact));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_check_test_deps_returns_test_deps_when_db_missing() {
|
||||||
|
let spec = PackageSpec {
|
||||||
|
package: crate::package::PackageInfo {
|
||||||
|
name: "foo".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "d".into(),
|
||||||
|
homepage: "h".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: Default::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: vec![crate::package::Source {
|
||||||
|
url: "https://example.test/foo.tar.gz".into(),
|
||||||
|
sha256: "skip".into(),
|
||||||
|
extract_dir: "foo".into(),
|
||||||
|
patches: Vec::new(),
|
||||||
|
post_extract: Vec::new(),
|
||||||
|
}],
|
||||||
|
build: crate::package::Build {
|
||||||
|
build_type: crate::package::BuildType::Custom,
|
||||||
|
flags: crate::package::BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: crate::package::Dependencies {
|
||||||
|
build: Vec::new(),
|
||||||
|
runtime: Vec::new(),
|
||||||
|
test: vec!["bats".into(), "python".into()],
|
||||||
|
},
|
||||||
|
spec_dir: std::path::PathBuf::from("."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let missing = check_test_deps(&spec, Path::new("/definitely/not/a/real/db")).unwrap();
|
||||||
|
assert_eq!(missing, vec!["bats".to_string(), "python".to_string()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -45,7 +45,11 @@ impl PackageIndex {
|
|||||||
.by_name
|
.by_name
|
||||||
.insert(spec.package.name.clone(), path.clone());
|
.insert(spec.package.name.clone(), path.clone());
|
||||||
for provided in &spec.alternatives.provides {
|
for provided in &spec.alternatives.provides {
|
||||||
index.by_provides.entry(provided.clone()).or_default().push(path.clone());
|
index
|
||||||
|
.by_provides
|
||||||
|
.entry(provided.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(path.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,9 +67,15 @@ impl PackageIndex {
|
|||||||
let path = entry.path().to_path_buf();
|
let path = entry.path().to_path_buf();
|
||||||
if path.extension().map(|e| e == "toml").unwrap_or(false) {
|
if path.extension().map(|e| e == "toml").unwrap_or(false) {
|
||||||
if let Ok(spec) = PackageSpec::from_file(&path) {
|
if let Ok(spec) = PackageSpec::from_file(&path) {
|
||||||
index.by_name.insert(spec.package.name.clone(), path.clone());
|
index
|
||||||
|
.by_name
|
||||||
|
.insert(spec.package.name.clone(), path.clone());
|
||||||
for provided in &spec.alternatives.provides {
|
for provided in &spec.alternatives.provides {
|
||||||
index.by_provides.entry(provided.clone()).or_default().push(path.clone());
|
index
|
||||||
|
.by_provides
|
||||||
|
.entry(provided.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(path.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+188
-41
@@ -17,6 +17,86 @@ use clap::{Parser, Subcommand};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn parse_licenses_from_toml(metadata: &toml::Value) -> Vec<String> {
|
||||||
|
if let Some(s) = metadata.get("license").and_then(|v| v.as_str()) {
|
||||||
|
return vec![s.to_string()];
|
||||||
|
}
|
||||||
|
if let Some(arr) = metadata.get("license").and_then(|v| v.as_array()) {
|
||||||
|
return arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_build_workspace(config: &config::Config) -> Result<()> {
|
||||||
|
if config.build_dir.exists() {
|
||||||
|
fs::remove_dir_all(&config.build_dir).with_context(|| {
|
||||||
|
format!("Failed to clean build dir: {}", config.build_dir.display())
|
||||||
|
})?;
|
||||||
|
println!("Cleaned build workspace: {}", config.build_dir.display());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warn_if_running_as_root_for_build(command: &str, rootfs: &Path) {
|
||||||
|
if crate::fakeroot::is_root() {
|
||||||
|
eprintln!(
|
||||||
|
"\x1b[33mWARNING: Running '{}' as root is discouraged.\x1b[0m",
|
||||||
|
command
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"\x1b[33mA misconfigured build environment or malicious/buggy build file can overwrite or delete critical system files.\x1b[0m"
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"\x1b[33mRecommendation: use a non-root build user and only install as root.\x1b[0m"
|
||||||
|
);
|
||||||
|
eprintln!("\x1b[33mCurrent rootfs target: {}\x1b[0m", rootfs.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_staged_to_rootfs(
|
||||||
|
pkg_spec: &package::PackageSpec,
|
||||||
|
destdir: &Path,
|
||||||
|
rootfs: &Path,
|
||||||
|
config: &config::Config,
|
||||||
|
) -> Result<()> {
|
||||||
|
std::fs::create_dir_all(&config.db_dir).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to create database directory: {}",
|
||||||
|
config.db_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let db_path = config.db_dir.join("packages.db");
|
||||||
|
|
||||||
|
let new_files = staging::generate_manifest_with_dirs(destdir)?;
|
||||||
|
let remove_paths =
|
||||||
|
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_files.files)?;
|
||||||
|
|
||||||
|
let tx_base = config.build_dir.join("tx");
|
||||||
|
let tx = staging::install_atomic(
|
||||||
|
destdir,
|
||||||
|
rootfs,
|
||||||
|
&tx_base,
|
||||||
|
&remove_paths,
|
||||||
|
&pkg_spec.build.flags.keep,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for out in pkg_spec.outputs() {
|
||||||
|
let mut spec_for_out = pkg_spec.clone();
|
||||||
|
spec_for_out.package = out;
|
||||||
|
if let Err(e) = db::register_package(&db_path, &spec_for_out, destdir) {
|
||||||
|
let _ = tx.rollback();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "Depot")]
|
#[command(name = "Depot")]
|
||||||
#[command(about = "Depot - Source-based package manager for Linux", long_about = None)]
|
#[command(about = "Depot - Source-based package manager for Linux", long_about = None)]
|
||||||
@@ -30,10 +110,25 @@ struct Cli {
|
|||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
no_deps: bool,
|
no_deps: bool,
|
||||||
|
|
||||||
|
/// Do not export CFLAGS/CXXFLAGS/LDFLAGS to build commands
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
action = clap::ArgAction::Set,
|
||||||
|
num_args = 0..=1,
|
||||||
|
default_value_t = false,
|
||||||
|
default_missing_value = "true"
|
||||||
|
)]
|
||||||
|
no_flags: bool,
|
||||||
|
|
||||||
/// Cross-compilation prefix (e.g., x86_64-linux-musl, aarch64-linux-gnu)
|
/// Cross-compilation prefix (e.g., x86_64-linux-musl, aarch64-linux-gnu)
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
cross_prefix: Option<String>,
|
cross_prefix: Option<String>,
|
||||||
|
|
||||||
|
/// Clean build workspace after successful install/build
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
clean: bool,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
}
|
}
|
||||||
@@ -64,6 +159,10 @@ enum Commands {
|
|||||||
/// Explicitly specify path to package spec (.toml file)
|
/// Explicitly specify path to package spec (.toml file)
|
||||||
#[arg(short, long = "spec", visible_alias = "package", alias = "p")]
|
#[arg(short, long = "spec", visible_alias = "package", alias = "p")]
|
||||||
spec: Option<PathBuf>,
|
spec: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Install package to rootfs after creating package archive(s)
|
||||||
|
#[arg(long)]
|
||||||
|
install: bool,
|
||||||
},
|
},
|
||||||
/// Show information about a package
|
/// Show information about a package
|
||||||
Info {
|
Info {
|
||||||
@@ -109,6 +208,7 @@ fn main() -> Result<()> {
|
|||||||
spec_or_archive,
|
spec_or_archive,
|
||||||
spec,
|
spec,
|
||||||
} => {
|
} => {
|
||||||
|
warn_if_running_as_root_for_build("install", &cli.rootfs);
|
||||||
let mut spec_path = spec.unwrap_or(spec_or_archive);
|
let mut spec_path = spec.unwrap_or(spec_or_archive);
|
||||||
|
|
||||||
// Load configuration early so we can use the configured repo clone dir
|
// Load configuration early so we can use the configured repo clone dir
|
||||||
@@ -122,7 +222,8 @@ fn main() -> Result<()> {
|
|||||||
if !spec_path.exists() {
|
if !spec_path.exists() {
|
||||||
let name = spec_path.to_string_lossy().to_string();
|
let name = spec_path.to_string_lossy().to_string();
|
||||||
println!("Looking up package '{}' in local indexes...", name);
|
println!("Looking up package '{}' in local indexes...", name);
|
||||||
let pkg_index = index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
|
let pkg_index =
|
||||||
|
index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
|
||||||
if let Some(found) = pkg_index.find(&name) {
|
if let Some(found) = pkg_index.find(&name) {
|
||||||
spec_path = found;
|
spec_path = found;
|
||||||
}
|
}
|
||||||
@@ -159,10 +260,6 @@ fn main() -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to parse the metadata.toml but we don't have a direct "from_metadata"
|
|
||||||
// Let's implement a minimal reconstruction or use the metadata to fill a spec.
|
|
||||||
// Actually, PackageSpec needs a lot of fields.
|
|
||||||
// Let's extract the WHOLE archive to a temporary staging dir and use it.
|
|
||||||
let file = fs::File::open(&spec_path)?;
|
let file = fs::File::open(&spec_path)?;
|
||||||
let zstd_decoder = zstd::stream::read::Decoder::new(file)?;
|
let zstd_decoder = zstd::stream::read::Decoder::new(file)?;
|
||||||
let mut archive = tar::Archive::new(zstd_decoder);
|
let mut archive = tar::Archive::new(zstd_decoder);
|
||||||
@@ -197,11 +294,7 @@ fn main() -> Result<()> {
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string(),
|
.to_string(),
|
||||||
license: metadata
|
license: parse_licenses_from_toml(&metadata),
|
||||||
.get("license")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string(),
|
|
||||||
},
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: package::Alternatives::default(),
|
alternatives: package::Alternatives::default(),
|
||||||
@@ -225,6 +318,18 @@ fn main() -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
},
|
},
|
||||||
|
test: if let Some(deps) = metadata
|
||||||
|
.get("dependencies")
|
||||||
|
.and_then(|v| v.get("test"))
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
{
|
||||||
|
deps.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
spec_dir: PathBuf::from("."),
|
spec_dir: PathBuf::from("."),
|
||||||
};
|
};
|
||||||
@@ -266,6 +371,8 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
// Check dependencies and prompt for auto-install if needed
|
// Check dependencies and prompt for auto-install if needed
|
||||||
if !cli.no_deps {
|
if !cli.no_deps {
|
||||||
|
let needs_test_deps =
|
||||||
|
matches!(pkg_spec.build.build_type, package::BuildType::Autotools);
|
||||||
deps::print_dep_status(&pkg_spec, &db_path)?;
|
deps::print_dep_status(&pkg_spec, &db_path)?;
|
||||||
|
|
||||||
// Collect all missing dependencies (build + runtime)
|
// Collect all missing dependencies (build + runtime)
|
||||||
@@ -277,6 +384,14 @@ fn main() -> Result<()> {
|
|||||||
missing.push(dep);
|
missing.push(dep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if needs_test_deps {
|
||||||
|
let missing_test = deps::check_test_deps(&pkg_spec, &db_path)?;
|
||||||
|
for dep in missing_test {
|
||||||
|
if !missing.contains(&dep) {
|
||||||
|
missing.push(dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !missing.is_empty() {
|
if !missing.is_empty() {
|
||||||
// Check for dependency cycles via DEPOT_DEPCHAIN env var
|
// Check for dependency cycles via DEPOT_DEPCHAIN env var
|
||||||
@@ -301,7 +416,9 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
if input == "y" || input == "yes" || input.is_empty() {
|
if input == "y" || input == "yes" || input.is_empty() {
|
||||||
// Build package index for fast lookups
|
// Build package index for fast lookups
|
||||||
let pkg_index = index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
|
let pkg_index = index::PackageIndex::build_with_repo_dir(Some(
|
||||||
|
config.repo_clone_dir.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
// Build new dep chain
|
// Build new dep chain
|
||||||
let new_chain = if dep_chain.is_empty() {
|
let new_chain = if dep_chain.is_empty() {
|
||||||
@@ -324,9 +441,15 @@ fn main() -> Result<()> {
|
|||||||
if cli.no_deps {
|
if cli.no_deps {
|
||||||
cmd.arg("--no-deps");
|
cmd.arg("--no-deps");
|
||||||
}
|
}
|
||||||
|
if cli.no_flags {
|
||||||
|
cmd.arg("--no-flags");
|
||||||
|
}
|
||||||
if let Some(ref p) = cli.cross_prefix {
|
if let Some(ref p) = cli.cross_prefix {
|
||||||
cmd.arg("--cross-prefix").arg(p);
|
cmd.arg("--cross-prefix").arg(p);
|
||||||
}
|
}
|
||||||
|
if cli.clean {
|
||||||
|
cmd.arg("--clean");
|
||||||
|
}
|
||||||
|
|
||||||
cmd.arg("install").arg(&dep_spec_path);
|
cmd.arg("install").arg(&dep_spec_path);
|
||||||
cmd.env("DEPOT_DEPCHAIN", &new_chain);
|
cmd.env("DEPOT_DEPCHAIN", &new_chain);
|
||||||
@@ -348,6 +471,9 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
// Enforce build dependencies (runtime deps are warnings only if not installed/prompt declined)
|
// Enforce build dependencies (runtime deps are warnings only if not installed/prompt declined)
|
||||||
deps::require_build_deps(&pkg_spec, &db_path)?;
|
deps::require_build_deps(&pkg_spec, &db_path)?;
|
||||||
|
if needs_test_deps {
|
||||||
|
deps::require_test_deps(&pkg_spec, &db_path)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure database directory exists
|
// Ensure database directory exists
|
||||||
@@ -377,7 +503,13 @@ fn main() -> Result<()> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| cross::CrossConfig::from_prefix(p))
|
.map(|p| cross::CrossConfig::from_prefix(p))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
builder::build(&pkg_spec, &src_dir, &destdir, cross_config.as_ref())?;
|
builder::build(
|
||||||
|
&pkg_spec,
|
||||||
|
&src_dir,
|
||||||
|
&destdir,
|
||||||
|
cross_config.as_ref(),
|
||||||
|
!cli.no_flags,
|
||||||
|
)?;
|
||||||
|
|
||||||
// 3.1 Copy license files into staged tree
|
// 3.1 Copy license files into staged tree
|
||||||
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
|
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
|
||||||
@@ -388,25 +520,8 @@ fn main() -> Result<()> {
|
|||||||
// 4. Stage (clean .la files, etc.)
|
// 4. Stage (clean .la files, etc.)
|
||||||
staging::process(&destdir, &pkg_spec)?;
|
staging::process(&destdir, &pkg_spec)?;
|
||||||
|
|
||||||
// 5. Install/update to rootfs (atomic)
|
// 5-6. Install/update to rootfs and register in DB
|
||||||
let new_files = staging::generate_manifest_with_dirs(&destdir)?;
|
install_staged_to_rootfs(&pkg_spec, &destdir, &cli.rootfs, &config)?;
|
||||||
|
|
||||||
let remove_paths =
|
|
||||||
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_files.files)?;
|
|
||||||
|
|
||||||
let tx_base = config.build_dir.join("tx");
|
|
||||||
let tx = staging::install_atomic(&destdir, &cli.rootfs, &tx_base, &remove_paths)?;
|
|
||||||
|
|
||||||
// 6. Register in database (rollback install on DB error)
|
|
||||||
for out in pkg_spec.outputs() {
|
|
||||||
let mut spec_for_out = pkg_spec.clone();
|
|
||||||
spec_for_out.package = out;
|
|
||||||
if let Err(e) = db::register_package(&db_path, &spec_for_out, &destdir) {
|
|
||||||
let _ = tx.rollback();
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx.commit()?;
|
|
||||||
|
|
||||||
// 7. Check runtime dependencies (warn only)
|
// 7. Check runtime dependencies (warn only)
|
||||||
if !cli.no_deps {
|
if !cli.no_deps {
|
||||||
@@ -427,6 +542,10 @@ fn main() -> Result<()> {
|
|||||||
"Successfully installed {} v{}",
|
"Successfully installed {} v{}",
|
||||||
pkg_spec.package.name, pkg_spec.package.version
|
pkg_spec.package.name, pkg_spec.package.version
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if cli.clean {
|
||||||
|
clean_build_workspace(&config)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Commands::Remove { package } => {
|
Commands::Remove { package } => {
|
||||||
println!("Removing package: {}", package);
|
println!("Removing package: {}", package);
|
||||||
@@ -435,12 +554,12 @@ fn main() -> Result<()> {
|
|||||||
db::remove_package(&db_path, &package, &cli.rootfs)?;
|
db::remove_package(&db_path, &package, &cli.rootfs)?;
|
||||||
println!("Successfully removed {}", package);
|
println!("Successfully removed {}", package);
|
||||||
}
|
}
|
||||||
Commands::Build { spec_pos, spec } => {
|
Commands::Build {
|
||||||
if crate::fakeroot::is_root() {
|
spec_pos,
|
||||||
anyhow::bail!(
|
spec,
|
||||||
"The 'build' command must be run as a non-root user to ensure a clean build environment."
|
install,
|
||||||
);
|
} => {
|
||||||
}
|
warn_if_running_as_root_for_build("build", &cli.rootfs);
|
||||||
let spec_path = spec.or(spec_pos).context("No spec file provided")?;
|
let spec_path = spec.or(spec_pos).context("No spec file provided")?;
|
||||||
println!("Building package from: {}", spec_path.display());
|
println!("Building package from: {}", spec_path.display());
|
||||||
let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?;
|
let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?;
|
||||||
@@ -461,8 +580,13 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
// Check build dependencies
|
// Check build dependencies
|
||||||
if !cli.no_deps {
|
if !cli.no_deps {
|
||||||
|
let needs_test_deps =
|
||||||
|
matches!(pkg_spec.build.build_type, package::BuildType::Autotools);
|
||||||
deps::print_dep_status(&pkg_spec, &db_path)?;
|
deps::print_dep_status(&pkg_spec, &db_path)?;
|
||||||
deps::require_build_deps(&pkg_spec, &db_path)?;
|
deps::require_build_deps(&pkg_spec, &db_path)?;
|
||||||
|
if needs_test_deps {
|
||||||
|
deps::require_test_deps(&pkg_spec, &db_path)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
|
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
|
||||||
@@ -477,7 +601,13 @@ fn main() -> Result<()> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| cross::CrossConfig::from_prefix(p))
|
.map(|p| cross::CrossConfig::from_prefix(p))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
builder::build(&pkg_spec, &src_dir, &destdir, cross_config.as_ref())?;
|
builder::build(
|
||||||
|
&pkg_spec,
|
||||||
|
&src_dir,
|
||||||
|
&destdir,
|
||||||
|
cross_config.as_ref(),
|
||||||
|
!cli.no_flags,
|
||||||
|
)?;
|
||||||
|
|
||||||
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
|
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
|
||||||
|
|
||||||
@@ -493,7 +623,8 @@ fn main() -> Result<()> {
|
|||||||
for out in pkg_spec.outputs() {
|
for out in pkg_spec.outputs() {
|
||||||
let mut spec_for_out = pkg_spec.clone();
|
let mut spec_for_out = pkg_spec.clone();
|
||||||
spec_for_out.package = out;
|
spec_for_out.package = out;
|
||||||
let packager = package::Packager::new(spec_for_out.clone(), destdir.clone(), config.clone());
|
let packager =
|
||||||
|
package::Packager::new(spec_for_out.clone(), destdir.clone(), config.clone());
|
||||||
let pkg_file = packager.create_package(Path::new("."), arch)?;
|
let pkg_file = packager.create_package(Path::new("."), arch)?;
|
||||||
created_files.push(pkg_file);
|
created_files.push(pkg_file);
|
||||||
}
|
}
|
||||||
@@ -501,6 +632,18 @@ fn main() -> Result<()> {
|
|||||||
for f in &created_files {
|
for f in &created_files {
|
||||||
println!("Build complete. Package created: {}", f.display());
|
println!("Build complete. Package created: {}", f.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if install {
|
||||||
|
install_staged_to_rootfs(&pkg_spec, &destdir, &cli.rootfs, &config)?;
|
||||||
|
println!(
|
||||||
|
"Successfully installed {} v{}",
|
||||||
|
pkg_spec.package.name, pkg_spec.package.version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cli.clean {
|
||||||
|
clean_build_workspace(&config)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Commands::Info { package } => {
|
Commands::Info { package } => {
|
||||||
// Try as file first, then as installed package name
|
// Try as file first, then as installed package name
|
||||||
@@ -540,7 +683,10 @@ fn main() -> Result<()> {
|
|||||||
println!("No mirrors configured in /etc/depot.d/mirrors.toml");
|
println!("No mirrors configured in /etc/depot.d/mirrors.toml");
|
||||||
} else {
|
} else {
|
||||||
db::repo::sync_mirrors(&config.repo_clone_dir, &config.mirrors)?;
|
db::repo::sync_mirrors(&config.repo_clone_dir, &config.mirrors)?;
|
||||||
println!("Mirrors synchronized into {}", config.repo_clone_dir.display());
|
println!(
|
||||||
|
"Mirrors synchronized into {}",
|
||||||
|
config.repo_clone_dir.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RepoCommands::Status => {
|
RepoCommands::Status => {
|
||||||
@@ -571,7 +717,8 @@ fn main() -> Result<()> {
|
|||||||
// Produce a minimal TOML for interactive-created specs (omit defaults)
|
// Produce a minimal TOML for interactive-created specs (omit defaults)
|
||||||
let toml_string = package::spec_to_minimal_toml(&spec)?;
|
let toml_string = package::spec_to_minimal_toml(&spec)?;
|
||||||
|
|
||||||
let output_path = output.unwrap_or_else(|| PathBuf::from(format!("{}.toml", spec.package.name)));
|
let output_path =
|
||||||
|
output.unwrap_or_else(|| PathBuf::from(format!("{}.toml", spec.package.name)));
|
||||||
|
|
||||||
if output_path.exists() {
|
if output_path.exists() {
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
+777
-122
File diff suppressed because it is too large
Load Diff
+49
-2
@@ -8,6 +8,19 @@ use std::path::{Path, PathBuf};
|
|||||||
use tar::Builder;
|
use tar::Builder;
|
||||||
use zstd::stream::write::Encoder;
|
use zstd::stream::write::Encoder;
|
||||||
|
|
||||||
|
fn license_value(licenses: &[String]) -> toml::Value {
|
||||||
|
if licenses.len() == 1 {
|
||||||
|
toml::Value::String(licenses[0].clone())
|
||||||
|
} else {
|
||||||
|
toml::Value::Array(
|
||||||
|
licenses
|
||||||
|
.iter()
|
||||||
|
.map(|license| toml::Value::String(license.clone()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Packager {
|
pub struct Packager {
|
||||||
pub spec: PackageSpec,
|
pub spec: PackageSpec,
|
||||||
pub destdir: PathBuf,
|
pub destdir: PathBuf,
|
||||||
@@ -119,7 +132,7 @@ impl Packager {
|
|||||||
);
|
);
|
||||||
map.insert(
|
map.insert(
|
||||||
"license".to_string(),
|
"license".to_string(),
|
||||||
toml::Value::String(self.spec.package.license.clone()),
|
license_value(&self.spec.package.license),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add provides
|
// Add provides
|
||||||
@@ -159,6 +172,17 @@ impl Packager {
|
|||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
build_deps.insert(
|
||||||
|
"test".to_string(),
|
||||||
|
toml::Value::Array(
|
||||||
|
self.spec
|
||||||
|
.dependencies
|
||||||
|
.test
|
||||||
|
.iter()
|
||||||
|
.map(|s| toml::Value::String(s.clone()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
map.insert("dependencies".to_string(), toml::Value::Table(build_deps));
|
map.insert("dependencies".to_string(), toml::Value::Table(build_deps));
|
||||||
|
|
||||||
let toml_str = toml::to_string(&toml::Value::Table(map))
|
let toml_str = toml::to_string(&toml::Value::Table(map))
|
||||||
@@ -229,7 +253,7 @@ mod tests {
|
|||||||
revision: 1,
|
revision: 1,
|
||||||
description: "d".into(),
|
description: "d".into(),
|
||||||
homepage: "h".into(),
|
homepage: "h".into(),
|
||||||
license: "MIT".into(),
|
license: vec!["MIT".into()],
|
||||||
},
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Alternatives::default(),
|
alternatives: Alternatives::default(),
|
||||||
@@ -301,5 +325,28 @@ mod tests {
|
|||||||
|
|
||||||
let deps = val.get("dependencies").unwrap();
|
let deps = val.get("dependencies").unwrap();
|
||||||
assert!(deps.get("build").unwrap().as_array().unwrap().is_empty());
|
assert!(deps.get("build").unwrap().as_array().unwrap().is_empty());
|
||||||
|
assert!(deps.get("runtime").unwrap().as_array().unwrap().is_empty());
|
||||||
|
assert!(deps.get("test").unwrap().as_array().unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_metadata_toml_with_multiple_licenses() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let dest = tmp.path();
|
||||||
|
|
||||||
|
let mut packager = mk_packager(dest.to_path_buf());
|
||||||
|
packager.spec.package.license = vec!["MIT".into(), "Apache-2.0".into()];
|
||||||
|
packager.generate_metadata_toml().unwrap();
|
||||||
|
|
||||||
|
let meta_path = dest.join(".metadata.toml");
|
||||||
|
let content = fs::read_to_string(meta_path).unwrap();
|
||||||
|
let val: toml::Value = toml::from_str(&content).unwrap();
|
||||||
|
let arr = val
|
||||||
|
.get("license")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("license should be an array");
|
||||||
|
assert_eq!(arr.len(), 2);
|
||||||
|
assert_eq!(arr[0].as_str(), Some("MIT"));
|
||||||
|
assert_eq!(arr[1].as_str(), Some("Apache-2.0"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+765
-10
@@ -3,6 +3,8 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Deserializer;
|
use serde::Deserializer;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::Serializer;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -52,10 +54,43 @@ impl PackageSpec {
|
|||||||
if spec.source.is_empty() && spec.manual_sources.is_empty() {
|
if spec.source.is_empty() && spec.manual_sources.is_empty() {
|
||||||
anyhow::bail!("Package must have at least one source or manual_sources entry");
|
anyhow::bail!("Package must have at least one source or manual_sources entry");
|
||||||
}
|
}
|
||||||
|
spec.validate_manual_sources()?;
|
||||||
|
|
||||||
Ok(spec)
|
Ok(spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_manual_sources(&self) -> Result<()> {
|
||||||
|
for (idx, manual) in self.manual_sources.iter().enumerate() {
|
||||||
|
let has_file = manual
|
||||||
|
.file
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let has_url = manual
|
||||||
|
.url
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
match (has_file, has_url) {
|
||||||
|
(true, false) | (false, true) => {}
|
||||||
|
(false, false) => {
|
||||||
|
anyhow::bail!(
|
||||||
|
"manual_sources[{}] must specify exactly one of 'file' or 'url'",
|
||||||
|
idx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(true, true) => {
|
||||||
|
anyhow::bail!(
|
||||||
|
"manual_sources[{}] cannot specify both 'file' and 'url'",
|
||||||
|
idx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Expand variables like $name and $version in a string
|
/// Expand variables like $name and $version in a string
|
||||||
pub fn expand_vars(&self, input: &str) -> String {
|
pub fn expand_vars(&self, input: &str) -> String {
|
||||||
let specdir = self.spec_dir.to_string_lossy();
|
let specdir = self.spec_dir.to_string_lossy();
|
||||||
@@ -127,6 +162,17 @@ impl PackageSpec {
|
|||||||
self.build.flags.ldflags = vec![s.to_string()];
|
self.build.flags.ldflags = vec![s.to_string()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"keep" => {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build.flags.keep = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build.flags.keep = vec![s.to_string()];
|
||||||
|
}
|
||||||
|
}
|
||||||
"cc" => {
|
"cc" => {
|
||||||
if let Some(s) = v.as_str() {
|
if let Some(s) = v.as_str() {
|
||||||
self.build.flags.cc = s.to_string();
|
self.build.flags.cc = s.to_string();
|
||||||
@@ -162,6 +208,65 @@ impl PackageSpec {
|
|||||||
self.build.flags.carch = s.to_string();
|
self.build.flags.carch = s.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"make_vars" | "make-vars" | "make_build_vars" | "make-build-vars" => {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build.flags.make_vars = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build.flags.make_vars =
|
||||||
|
s.split_whitespace().map(String::from).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"make_test_vars" | "make-test-vars" => {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build.flags.make_test_vars = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build.flags.make_test_vars =
|
||||||
|
s.split_whitespace().map(String::from).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"make_install_vars" | "make-install-vars" => {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build.flags.make_install_vars = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build.flags.make_install_vars =
|
||||||
|
s.split_whitespace().map(String::from).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"passthrough_env" | "passthrough-env" | "pass_env" | "pass-env" | "export_env"
|
||||||
|
| "export-env" => {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build.flags.passthrough_env = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build.flags.passthrough_env =
|
||||||
|
s.split_whitespace().map(String::from).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"no_flags" | "no-flags" => {
|
||||||
|
if let Some(b) = v.as_bool() {
|
||||||
|
self.build.flags.no_flags = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"skip_tests" | "skip-tests" => {
|
||||||
|
if let Some(b) = v.as_bool() {
|
||||||
|
self.build.flags.skip_tests = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Add more fields as needed
|
// Add more fields as needed
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -194,6 +299,18 @@ impl PackageSpec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"keep" => {
|
||||||
|
for v in values {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.keep
|
||||||
|
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build.flags.keep.push(s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
"configure" => {
|
"configure" => {
|
||||||
for v in values {
|
for v in values {
|
||||||
if let Some(arr) = v.as_array() {
|
if let Some(arr) = v.as_array() {
|
||||||
@@ -260,6 +377,77 @@ impl PackageSpec {
|
|||||||
self.build.flags.carch = s.to_string();
|
self.build.flags.carch = s.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"make_vars" | "make-vars" | "make_build_vars" | "make-build-vars" => {
|
||||||
|
for v in values {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.make_vars
|
||||||
|
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.make_vars
|
||||||
|
.extend(s.split_whitespace().map(String::from));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"make_test_vars" | "make-test-vars" => {
|
||||||
|
for v in values {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.make_test_vars
|
||||||
|
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.make_test_vars
|
||||||
|
.extend(s.split_whitespace().map(String::from));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"make_install_vars" | "make-install-vars" => {
|
||||||
|
for v in values {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.make_install_vars
|
||||||
|
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.make_install_vars
|
||||||
|
.extend(s.split_whitespace().map(String::from));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"passthrough_env" | "passthrough-env" | "pass_env" | "pass-env" | "export_env"
|
||||||
|
| "export-env" => {
|
||||||
|
for v in values {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.passthrough_env
|
||||||
|
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.passthrough_env
|
||||||
|
.extend(s.split_whitespace().map(String::from));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"no_flags" | "no-flags" => {
|
||||||
|
if let Some(b) = values.last().and_then(|v| v.as_bool()) {
|
||||||
|
self.build.flags.no_flags = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"skip_tests" | "skip-tests" => {
|
||||||
|
if let Some(b) = values.last().and_then(|v| v.as_bool()) {
|
||||||
|
self.build.flags.skip_tests = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,6 +532,39 @@ type = "custom"
|
|||||||
assert_eq!(spec.sources()[1].extract_dir, "bar");
|
assert_eq!(spec.sources()[1].extract_dir, "bar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_multiple_licenses() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = ["MIT", "Apache-2.0"]
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
spec.package.license,
|
||||||
|
vec!["MIT".to_string(), "Apache-2.0".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_rejects_empty_sources() {
|
fn parse_rejects_empty_sources() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
@@ -371,6 +592,101 @@ type = "custom"
|
|||||||
assert!(PackageSpec::from_file(&path).is_err());
|
assert!(PackageSpec::from_file(&path).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_manual_source_with_url() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[[manual_sources]]
|
||||||
|
url = "https://example.com/manual.patch"
|
||||||
|
sha256 = "skip"
|
||||||
|
dest = "patches/manual.patch"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert_eq!(spec.manual_sources.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
spec.manual_sources[0].url.as_deref(),
|
||||||
|
Some("https://example.com/manual.patch")
|
||||||
|
);
|
||||||
|
assert_eq!(spec.manual_sources[0].file, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_manual_source_rejects_missing_file_and_url() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[[manual_sources]]
|
||||||
|
sha256 = "skip"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = PackageSpec::from_file(&path).expect_err("spec should be rejected");
|
||||||
|
assert!(err.to_string().contains("exactly one of 'file' or 'url'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_manual_source_rejects_file_and_url_together() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[[manual_sources]]
|
||||||
|
file = "manual.patch"
|
||||||
|
url = "https://example.com/manual.patch"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = PackageSpec::from_file(&path).expect_err("spec should be rejected");
|
||||||
|
assert!(
|
||||||
|
err.to_string()
|
||||||
|
.contains("cannot specify both 'file' and 'url'")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_apply_config() {
|
fn test_apply_config() {
|
||||||
let mut spec = mk_spec("foo", "1.0");
|
let mut spec = mk_spec("foo", "1.0");
|
||||||
@@ -382,6 +698,11 @@ type = "custom"
|
|||||||
[flags]
|
[flags]
|
||||||
cc = "my-cc"
|
cc = "my-cc"
|
||||||
cflags = ["-O2"]
|
cflags = ["-O2"]
|
||||||
|
passthrough_env = ["RUSTFLAGS"]
|
||||||
|
make_vars = ["V=1"]
|
||||||
|
no_flags = true
|
||||||
|
skip_tests = true
|
||||||
|
keep = ["etc/locale.gen"]
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -396,6 +717,24 @@ cflags = ["-O2"]
|
|||||||
toml::Value::String("opt-level=3".to_string()),
|
toml::Value::String("opt-level=3".to_string()),
|
||||||
])],
|
])],
|
||||||
);
|
);
|
||||||
|
config.appends.insert(
|
||||||
|
"build.flags.keep".to_string(),
|
||||||
|
vec![toml::Value::Array(vec![toml::Value::String(
|
||||||
|
"etc/locale.gen".to_string(),
|
||||||
|
)])],
|
||||||
|
);
|
||||||
|
config.appends.insert(
|
||||||
|
"build.flags.passthrough_env".to_string(),
|
||||||
|
vec![toml::Value::String("CARGO_HOME".to_string())],
|
||||||
|
);
|
||||||
|
config.appends.insert(
|
||||||
|
"build.flags.make_test_vars".to_string(),
|
||||||
|
vec![toml::Value::String("TESTS=smoke".to_string())],
|
||||||
|
);
|
||||||
|
config.appends.insert(
|
||||||
|
"build.flags.make_install_vars".to_string(),
|
||||||
|
vec![toml::Value::String("DESTDIR=/tmp/pkg".to_string())],
|
||||||
|
);
|
||||||
|
|
||||||
spec.apply_config(&config);
|
spec.apply_config(&config);
|
||||||
|
|
||||||
@@ -409,6 +748,326 @@ cflags = ["-O2"]
|
|||||||
.rustflags
|
.rustflags
|
||||||
.contains(&"opt-level=3".to_string())
|
.contains(&"opt-level=3".to_string())
|
||||||
);
|
);
|
||||||
|
assert!(spec.build.flags.no_flags);
|
||||||
|
assert!(
|
||||||
|
spec.build
|
||||||
|
.flags
|
||||||
|
.keep
|
||||||
|
.contains(&"etc/locale.gen".to_string())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
spec.build
|
||||||
|
.flags
|
||||||
|
.passthrough_env
|
||||||
|
.contains(&"RUSTFLAGS".to_string())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
spec.build
|
||||||
|
.flags
|
||||||
|
.passthrough_env
|
||||||
|
.contains(&"CARGO_HOME".to_string())
|
||||||
|
);
|
||||||
|
assert!(spec.build.flags.make_vars.contains(&"V=1".to_string()));
|
||||||
|
assert!(spec.build.flags.skip_tests);
|
||||||
|
assert!(
|
||||||
|
spec.build
|
||||||
|
.flags
|
||||||
|
.make_test_vars
|
||||||
|
.contains(&"TESTS=smoke".to_string())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
spec.build
|
||||||
|
.flags
|
||||||
|
.make_install_vars
|
||||||
|
.contains(&"DESTDIR=/tmp/pkg".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_no_flags_from_spec() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
|
||||||
|
[build.flags]
|
||||||
|
no_flags = true
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert!(spec.build.flags.no_flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_no_flags_alias_from_spec() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
|
||||||
|
[build.flags]
|
||||||
|
"no-flags" = true
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert!(spec.build.flags.no_flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skip_tests_from_spec() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "autotools"
|
||||||
|
|
||||||
|
[build.flags]
|
||||||
|
skip_tests = true
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert!(spec.build.flags.skip_tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_skip_tests_alias_from_spec() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "autotools"
|
||||||
|
|
||||||
|
[build.flags]
|
||||||
|
"skip-tests" = true
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert!(spec.build.flags.skip_tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_keep_from_spec() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
|
||||||
|
[build.flags]
|
||||||
|
keep = ["etc/locale.gen", "etc/resolv.conf"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
spec.build.flags.keep,
|
||||||
|
vec!["etc/locale.gen".to_string(), "etc/resolv.conf".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_passthrough_env_from_spec() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
|
||||||
|
[build.flags]
|
||||||
|
passthrough_env = ["RUSTFLAGS", "CARGO_HOME"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
spec.build.flags.passthrough_env,
|
||||||
|
vec!["RUSTFLAGS".to_string(), "CARGO_HOME".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_test_dependencies_from_spec() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "autotools"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
build = ["make"]
|
||||||
|
test = ["python", "bats"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
spec.dependencies.test,
|
||||||
|
vec!["python".to_string(), "bats".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_make_var_overrides_from_spec() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "autotools"
|
||||||
|
|
||||||
|
[build.flags]
|
||||||
|
make_vars = ["V=1", "CC=clang"]
|
||||||
|
make_test_vars = ["TESTS=unit"]
|
||||||
|
make_install_vars = ["STRIPPROG=true"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
spec.build.flags.make_vars,
|
||||||
|
vec!["V=1".to_string(), "CC=clang".to_string()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
spec.build.flags.make_test_vars,
|
||||||
|
vec!["TESTS=unit".to_string()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
spec.build.flags.make_install_vars,
|
||||||
|
vec!["STRIPPROG=true".to_string()]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -444,9 +1103,12 @@ cbuild = "x86_64-pc-linux-gnu"
|
|||||||
|
|
||||||
// Override via config
|
// Override via config
|
||||||
let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent"));
|
let mut config = crate::config::Config::for_rootfs(Path::new("/tmp/nonexistent"));
|
||||||
config.build_overrides = toml::from_str(r#"[flags]
|
config.build_overrides = toml::from_str(
|
||||||
|
r#"[flags]
|
||||||
carch = "armv7"
|
carch = "armv7"
|
||||||
"#).unwrap();
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
spec.apply_config(&config);
|
spec.apply_config(&config);
|
||||||
assert_eq!(spec.build.flags.carch, "armv7");
|
assert_eq!(spec.build.flags.carch, "armv7");
|
||||||
}
|
}
|
||||||
@@ -509,7 +1171,7 @@ type = "custom"
|
|||||||
revision: 1,
|
revision: 1,
|
||||||
description: "d".into(),
|
description: "d".into(),
|
||||||
homepage: "h".into(),
|
homepage: "h".into(),
|
||||||
license: "MIT".into(),
|
license: vec!["MIT".into()],
|
||||||
},
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Alternatives::default(),
|
alternatives: Alternatives::default(),
|
||||||
@@ -540,7 +1202,7 @@ impl fmt::Display for PackageSpec {
|
|||||||
)?;
|
)?;
|
||||||
writeln!(f, "Description: {}", self.package.description)?;
|
writeln!(f, "Description: {}", self.package.description)?;
|
||||||
writeln!(f, "Homepage: {}", self.package.homepage)?;
|
writeln!(f, "Homepage: {}", self.package.homepage)?;
|
||||||
writeln!(f, "License: {}", self.package.license)?;
|
writeln!(f, "License: {}", self.package.license.join(", "))?;
|
||||||
writeln!(f, "Sources: {}", self.source.len())?;
|
writeln!(f, "Sources: {}", self.source.len())?;
|
||||||
writeln!(f, "Build Type: {:?}", self.build.build_type)?;
|
writeln!(f, "Build Type: {:?}", self.build.build_type)?;
|
||||||
if !self.alternatives.provides.is_empty() {
|
if !self.alternatives.provides.is_empty() {
|
||||||
@@ -560,13 +1222,45 @@ pub struct PackageInfo {
|
|||||||
pub revision: u32,
|
pub revision: u32,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub homepage: String,
|
pub homepage: String,
|
||||||
pub license: String,
|
#[serde(
|
||||||
|
deserialize_with = "deserialize_licenses",
|
||||||
|
serialize_with = "serialize_licenses"
|
||||||
|
)]
|
||||||
|
pub license: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_revision() -> u32 {
|
fn default_revision() -> u32 {
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_licenses<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum StringOrArray {
|
||||||
|
String(String),
|
||||||
|
Array(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
match StringOrArray::deserialize(deserializer)? {
|
||||||
|
StringOrArray::String(s) => Ok(vec![s]),
|
||||||
|
StringOrArray::Array(v) => Ok(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_licenses<S>(licenses: &[String], serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
if licenses.len() == 1 {
|
||||||
|
serializer.serialize_str(&licenses[0])
|
||||||
|
} else {
|
||||||
|
licenses.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PackageSpec {
|
impl PackageSpec {
|
||||||
/// Generate the standard package filename: <name>-<version>-<revision>-<arch>.depot.pkg.tar.zst
|
/// Generate the standard package filename: <name>-<version>-<revision>-<arch>.depot.pkg.tar.zst
|
||||||
pub fn package_filename(&self, arch: &str) -> String {
|
pub fn package_filename(&self, arch: &str) -> String {
|
||||||
@@ -613,15 +1307,20 @@ pub struct Source {
|
|||||||
pub post_extract: Vec<String>,
|
pub post_extract: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manual (local) source file to copy before fetching remote sources.
|
/// Manual source copied before standard source fetching.
|
||||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct ManualSource {
|
pub struct ManualSource {
|
||||||
/// Filename in the spec directory
|
/// Filename in the spec directory (local manual source mode).
|
||||||
pub file: String,
|
#[serde(default)]
|
||||||
/// SHA256 checksum (optional, use "skip" to bypass verification)
|
pub file: Option<String>,
|
||||||
|
/// Remote URL to fetch (remote manual source mode).
|
||||||
|
#[serde(default)]
|
||||||
|
pub url: Option<String>,
|
||||||
|
/// Checksum (optional, use "skip" to bypass verification).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sha256: Option<String>,
|
pub sha256: Option<String>,
|
||||||
/// Destination path relative to build work directory (default: same as file)
|
/// Destination path relative to build work directory.
|
||||||
|
/// Defaults to `file` for local mode or a derived filename for URL mode.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dest: Option<String>,
|
pub dest: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -675,6 +1374,15 @@ pub struct BuildFlags {
|
|||||||
pub cflags: Vec<String>,
|
pub cflags: Vec<String>,
|
||||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||||
pub ldflags: Vec<String>,
|
pub ldflags: Vec<String>,
|
||||||
|
/// Keep existing files and install package-provided replacement as `<path>.depotnew`.
|
||||||
|
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||||
|
pub keep: Vec<String>,
|
||||||
|
/// Disable exporting CFLAGS/CXXFLAGS/LDFLAGS for this package build.
|
||||||
|
#[serde(default, alias = "no-flags")]
|
||||||
|
pub no_flags: bool,
|
||||||
|
/// Skip automatic build-system test execution (e.g. Autotools `make check`/`make test`).
|
||||||
|
#[serde(default, alias = "skip-tests")]
|
||||||
|
pub skip_tests: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub configure: Vec<String>,
|
pub configure: Vec<String>,
|
||||||
/// C compiler
|
/// C compiler
|
||||||
@@ -721,6 +1429,41 @@ pub struct BuildFlags {
|
|||||||
/// CPU architecture short name (CARCH equivalent), e.g. "x86_64", "aarch64"
|
/// CPU architecture short name (CARCH equivalent), e.g. "x86_64", "aarch64"
|
||||||
#[serde(default = "default_carch")]
|
#[serde(default = "default_carch")]
|
||||||
pub carch: String,
|
pub carch: String,
|
||||||
|
/// Variable overrides passed directly to `make` (compile step), e.g. ["V=1", "CC=clang"].
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "make-vars",
|
||||||
|
alias = "make_build_vars",
|
||||||
|
alias = "make-build-vars",
|
||||||
|
deserialize_with = "deserialize_string_or_array"
|
||||||
|
)]
|
||||||
|
pub make_vars: Vec<String>,
|
||||||
|
/// Variable overrides passed directly to `make check` / `make test`.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "make-test-vars",
|
||||||
|
deserialize_with = "deserialize_string_or_array"
|
||||||
|
)]
|
||||||
|
pub make_test_vars: Vec<String>,
|
||||||
|
/// Variable overrides passed directly to `make install`.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "make-install-vars",
|
||||||
|
deserialize_with = "deserialize_string_or_array"
|
||||||
|
)]
|
||||||
|
pub make_install_vars: Vec<String>,
|
||||||
|
/// Additional host environment variable names to export unchanged to build commands.
|
||||||
|
/// Example: ["RUSTFLAGS", "CARGO_HOME"].
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "passthrough-env",
|
||||||
|
alias = "pass_env",
|
||||||
|
alias = "pass-env",
|
||||||
|
alias = "export_env",
|
||||||
|
alias = "export-env",
|
||||||
|
deserialize_with = "deserialize_string_or_array"
|
||||||
|
)]
|
||||||
|
pub passthrough_env: Vec<String>,
|
||||||
|
|
||||||
// Rust-specific fields
|
// Rust-specific fields
|
||||||
/// Rust build profile: "debug" or "release" (default: release)
|
/// Rust build profile: "debug" or "release" (default: release)
|
||||||
@@ -756,6 +1499,9 @@ impl Default for BuildFlags {
|
|||||||
BuildFlags {
|
BuildFlags {
|
||||||
cflags: Vec::new(),
|
cflags: Vec::new(),
|
||||||
ldflags: Vec::new(),
|
ldflags: Vec::new(),
|
||||||
|
keep: Vec::new(),
|
||||||
|
no_flags: false,
|
||||||
|
skip_tests: false,
|
||||||
configure: Vec::new(),
|
configure: Vec::new(),
|
||||||
cc: default_cc(),
|
cc: default_cc(),
|
||||||
cxx: default_cxx(),
|
cxx: default_cxx(),
|
||||||
@@ -770,6 +1516,10 @@ impl Default for BuildFlags {
|
|||||||
chost: String::new(),
|
chost: String::new(),
|
||||||
cbuild: String::new(),
|
cbuild: String::new(),
|
||||||
carch: default_carch(),
|
carch: default_carch(),
|
||||||
|
make_vars: Vec::new(),
|
||||||
|
make_test_vars: Vec::new(),
|
||||||
|
make_install_vars: Vec::new(),
|
||||||
|
passthrough_env: Vec::new(),
|
||||||
profile: default_profile(),
|
profile: default_profile(),
|
||||||
target: String::new(),
|
target: String::new(),
|
||||||
rustflags: Vec::new(),
|
rustflags: Vec::new(),
|
||||||
@@ -852,8 +1602,13 @@ fn default_cxx() -> String {
|
|||||||
/// Package dependencies
|
/// Package dependencies
|
||||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Dependencies {
|
pub struct Dependencies {
|
||||||
|
/// Dependencies required for building packages.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub build: Vec<String>,
|
pub build: Vec<String>,
|
||||||
|
/// Dependencies required at runtime.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub runtime: Vec<String>,
|
pub runtime: Vec<String>,
|
||||||
|
/// Dependencies required to run package test suites.
|
||||||
|
#[serde(default)]
|
||||||
|
pub test: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-49
@@ -7,7 +7,7 @@ use std::fs::{self, File};
|
|||||||
use std::io::{Cursor, Read, Write};
|
use std::io::{Cursor, Read, Write};
|
||||||
use std::os::unix::fs as unix_fs;
|
use std::os::unix::fs as unix_fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tempfile::{tempdir, NamedTempFile};
|
use tempfile::{NamedTempFile, tempdir};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use zstd::stream::read::Decoder as ZstdDecoder;
|
use zstd::stream::read::Decoder as ZstdDecoder;
|
||||||
|
|
||||||
@@ -85,21 +85,21 @@ fn extract_tar_gz(path: &Path, dest: &Path) -> Result<()> {
|
|||||||
// strip that top directory (source tarballs like foo-1.2.3/) or preserve
|
// strip that top directory (source tarballs like foo-1.2.3/) or preserve
|
||||||
// it (system-layout archives like usr/). Otherwise move all top-level
|
// it (system-layout archives like usr/). Otherwise move all top-level
|
||||||
// entries into `dest` so `dest` always contains the source root.
|
// entries into `dest` so `dest` always contains the source root.
|
||||||
let top = fs::read_dir(tmp.path())?.filter_map(|r| r.ok()).collect::<Vec<_>>();
|
let top = fs::read_dir(tmp.path())?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
if top.len() == 1 && top[0].path().is_dir() {
|
if top.len() == 1 && top[0].path().is_dir() {
|
||||||
let top_name = top[0].file_name().to_string_lossy().to_string();
|
let top_name = top[0].file_name().to_string_lossy().to_string();
|
||||||
let expected_basename = dest
|
let expected_basename = dest.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
// blacklist of system-layout directories we should NOT strip
|
// blacklist of system-layout directories we should NOT strip
|
||||||
let sys_blacklist = [
|
let sys_blacklist = [
|
||||||
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run",
|
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run", "dev",
|
||||||
"dev", "proc", "sys", "boot", "srv", "home",
|
"proc", "sys", "boot", "srv", "home",
|
||||||
];
|
];
|
||||||
|
|
||||||
let looks_like_versioned = |s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
let looks_like_versioned =
|
||||||
|
|s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
||||||
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
||||||
&& (top_name == expected_basename
|
&& (top_name == expected_basename
|
||||||
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
||||||
@@ -130,21 +130,21 @@ fn extract_tar_xz(path: &Path, dest: &Path) -> Result<()> {
|
|||||||
let mut archive = tar::Archive::new(decoder);
|
let mut archive = tar::Archive::new(decoder);
|
||||||
archive.unpack(tmp.path())?;
|
archive.unpack(tmp.path())?;
|
||||||
|
|
||||||
let top = fs::read_dir(tmp.path())?.filter_map(|r| r.ok()).collect::<Vec<_>>();
|
let top = fs::read_dir(tmp.path())?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
if top.len() == 1 && top[0].path().is_dir() {
|
if top.len() == 1 && top[0].path().is_dir() {
|
||||||
let top_name = top[0].file_name().to_string_lossy().to_string();
|
let top_name = top[0].file_name().to_string_lossy().to_string();
|
||||||
let expected_basename = dest
|
let expected_basename = dest.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
// blacklist of system-layout directories we should NOT strip
|
// blacklist of system-layout directories we should NOT strip
|
||||||
let sys_blacklist = [
|
let sys_blacklist = [
|
||||||
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run",
|
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run", "dev",
|
||||||
"dev", "proc", "sys", "boot", "srv", "home",
|
"proc", "sys", "boot", "srv", "home",
|
||||||
];
|
];
|
||||||
|
|
||||||
let looks_like_versioned = |s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
let looks_like_versioned =
|
||||||
|
|s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
||||||
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
||||||
&& (top_name == expected_basename
|
&& (top_name == expected_basename
|
||||||
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
||||||
@@ -175,21 +175,21 @@ fn extract_tar_bz2(path: &Path, dest: &Path) -> Result<()> {
|
|||||||
let mut archive = tar::Archive::new(decoder);
|
let mut archive = tar::Archive::new(decoder);
|
||||||
archive.unpack(tmp.path())?;
|
archive.unpack(tmp.path())?;
|
||||||
|
|
||||||
let top = fs::read_dir(tmp.path())?.filter_map(|r| r.ok()).collect::<Vec<_>>();
|
let top = fs::read_dir(tmp.path())?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
if top.len() == 1 && top[0].path().is_dir() {
|
if top.len() == 1 && top[0].path().is_dir() {
|
||||||
let top_name = top[0].file_name().to_string_lossy().to_string();
|
let top_name = top[0].file_name().to_string_lossy().to_string();
|
||||||
let expected_basename = dest
|
let expected_basename = dest.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
// blacklist of system-layout directories we should NOT strip
|
// blacklist of system-layout directories we should NOT strip
|
||||||
let sys_blacklist = [
|
let sys_blacklist = [
|
||||||
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run",
|
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run", "dev",
|
||||||
"dev", "proc", "sys", "boot", "srv", "home",
|
"proc", "sys", "boot", "srv", "home",
|
||||||
];
|
];
|
||||||
|
|
||||||
let looks_like_versioned = |s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
let looks_like_versioned =
|
||||||
|
|s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
||||||
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
||||||
&& (top_name == expected_basename
|
&& (top_name == expected_basename
|
||||||
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
||||||
@@ -219,21 +219,21 @@ fn extract_tar(path: &Path, dest: &Path) -> Result<()> {
|
|||||||
let mut archive = tar::Archive::new(file);
|
let mut archive = tar::Archive::new(file);
|
||||||
archive.unpack(tmp.path())?;
|
archive.unpack(tmp.path())?;
|
||||||
|
|
||||||
let top = fs::read_dir(tmp.path())?.filter_map(|r| r.ok()).collect::<Vec<_>>();
|
let top = fs::read_dir(tmp.path())?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
if top.len() == 1 && top[0].path().is_dir() {
|
if top.len() == 1 && top[0].path().is_dir() {
|
||||||
let top_name = top[0].file_name().to_string_lossy().to_string();
|
let top_name = top[0].file_name().to_string_lossy().to_string();
|
||||||
let expected_basename = dest
|
let expected_basename = dest.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
// blacklist of system-layout directories we should NOT strip
|
// blacklist of system-layout directories we should NOT strip
|
||||||
let sys_blacklist = [
|
let sys_blacklist = [
|
||||||
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run",
|
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run", "dev",
|
||||||
"dev", "proc", "sys", "boot", "srv", "home",
|
"proc", "sys", "boot", "srv", "home",
|
||||||
];
|
];
|
||||||
|
|
||||||
let looks_like_versioned = |s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
let looks_like_versioned =
|
||||||
|
|s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
||||||
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
||||||
&& (top_name == expected_basename
|
&& (top_name == expected_basename
|
||||||
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
||||||
@@ -263,21 +263,21 @@ fn extract_zip(path: &Path, dest: &Path) -> Result<()> {
|
|||||||
let mut archive = zip::ZipArchive::new(file)?;
|
let mut archive = zip::ZipArchive::new(file)?;
|
||||||
archive.extract(tmp.path())?;
|
archive.extract(tmp.path())?;
|
||||||
|
|
||||||
let top = fs::read_dir(tmp.path())?.filter_map(|r| r.ok()).collect::<Vec<_>>();
|
let top = fs::read_dir(tmp.path())?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
if top.len() == 1 && top[0].path().is_dir() {
|
if top.len() == 1 && top[0].path().is_dir() {
|
||||||
let top_name = top[0].file_name().to_string_lossy().to_string();
|
let top_name = top[0].file_name().to_string_lossy().to_string();
|
||||||
let expected_basename = dest
|
let expected_basename = dest.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
// blacklist of system-layout directories we should NOT strip
|
// blacklist of system-layout directories we should NOT strip
|
||||||
let sys_blacklist = [
|
let sys_blacklist = [
|
||||||
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run",
|
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run", "dev",
|
||||||
"dev", "proc", "sys", "boot", "srv", "home",
|
"proc", "sys", "boot", "srv", "home",
|
||||||
];
|
];
|
||||||
|
|
||||||
let looks_like_versioned = |s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
let looks_like_versioned =
|
||||||
|
|s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
||||||
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
||||||
&& (top_name == expected_basename
|
&& (top_name == expected_basename
|
||||||
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
||||||
@@ -308,21 +308,21 @@ fn extract_tar_zst(path: &Path, dest: &Path) -> Result<()> {
|
|||||||
let mut archive = tar::Archive::new(decoder);
|
let mut archive = tar::Archive::new(decoder);
|
||||||
archive.unpack(tmp.path())?;
|
archive.unpack(tmp.path())?;
|
||||||
|
|
||||||
let top = fs::read_dir(tmp.path())?.filter_map(|r| r.ok()).collect::<Vec<_>>();
|
let top = fs::read_dir(tmp.path())?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
if top.len() == 1 && top[0].path().is_dir() {
|
if top.len() == 1 && top[0].path().is_dir() {
|
||||||
let top_name = top[0].file_name().to_string_lossy().to_string();
|
let top_name = top[0].file_name().to_string_lossy().to_string();
|
||||||
let expected_basename = dest
|
let expected_basename = dest.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
// blacklist of system-layout directories we should NOT strip
|
// blacklist of system-layout directories we should NOT strip
|
||||||
let sys_blacklist = [
|
let sys_blacklist = [
|
||||||
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run",
|
"usr", "bin", "sbin", "lib", "lib64", "etc", "share", "opt", "var", "run", "dev",
|
||||||
"dev", "proc", "sys", "boot", "srv", "home",
|
"proc", "sys", "boot", "srv", "home",
|
||||||
];
|
];
|
||||||
|
|
||||||
let looks_like_versioned = |s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
let looks_like_versioned =
|
||||||
|
|s: &str| s.contains('-') && s.chars().any(|c| c.is_ascii_digit());
|
||||||
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
let should_strip = (!sys_blacklist.contains(&top_name.as_str()))
|
||||||
&& (top_name == expected_basename
|
&& (top_name == expected_basename
|
||||||
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
|| (!expected_basename.is_empty() && top_name.contains(expected_basename))
|
||||||
|
|||||||
+388
-119
@@ -4,11 +4,14 @@ use crate::package::{PackageSpec, Source};
|
|||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
const MAX_MIRROR_RETRIES: usize = 8;
|
||||||
|
|
||||||
/// Fetch an archive source tarball, returning path to downloaded file.
|
/// Fetch an archive source tarball, returning path to downloaded file.
|
||||||
pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> Result<PathBuf> {
|
pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> Result<PathBuf> {
|
||||||
let url = spec.expand_vars(&source.url);
|
let url = spec.expand_vars(&source.url);
|
||||||
@@ -30,33 +33,48 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
|||||||
// Parse URL early so we can handle non-HTTP schemes (FTP support)
|
// 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 parsed_url = Url::parse(&url).with_context(|| format!("Invalid URL: {}", url))?;
|
||||||
|
|
||||||
// If this is an FTP URL, fetch via the ftp crate into the cache and continue
|
// If this is an FTP URL, fetch via suppaftp into the cache and continue
|
||||||
if parsed_url.scheme() == "ftp" {
|
if parsed_url.scheme() == "ftp" {
|
||||||
// Connect and login (anonymous fallback)
|
// Connect and login (anonymous fallback)
|
||||||
let host = parsed_url.host_str().context("FTP URL missing host")?;
|
let host = parsed_url.host_str().context("FTP URL missing host")?;
|
||||||
let port = parsed_url.port_or_known_default().unwrap_or(21);
|
let port = parsed_url.port_or_known_default().unwrap_or(21);
|
||||||
let addr = format!("{}:{}", host, port);
|
let addr = format!("{}:{}", host, port);
|
||||||
let mut ftp_stream = ftp::FtpStream::connect(addr.as_str())
|
let mut ftp_stream = suppaftp::FtpStream::connect(addr.as_str())
|
||||||
.with_context(|| format!("Failed to connect to FTP host: {}", addr))?;
|
.with_context(|| format!("Failed to connect to FTP host: {}", addr))?;
|
||||||
let user = if parsed_url.username().is_empty() { "anonymous" } else { parsed_url.username() };
|
let user = if parsed_url.username().is_empty() {
|
||||||
|
"anonymous"
|
||||||
|
} else {
|
||||||
|
parsed_url.username()
|
||||||
|
};
|
||||||
let pass = parsed_url.password().unwrap_or("anonymous@");
|
let pass = parsed_url.password().unwrap_or("anonymous@");
|
||||||
ftp_stream.login(user, pass).with_context(|| format!("FTP login failed for {}", host))?;
|
ftp_stream
|
||||||
|
.login(user, pass)
|
||||||
|
.with_context(|| format!("FTP login failed for {}", host))?;
|
||||||
|
|
||||||
// Retrieve the path (try with and without leading slash)
|
// Retrieve the path (try with and without leading slash)
|
||||||
let path = parsed_url.path();
|
let path = parsed_url.path();
|
||||||
let candidates = [path.to_string(), path.trim_start_matches('/').to_string()];
|
let candidates = [path.to_string(), path.trim_start_matches('/').to_string()];
|
||||||
let mut retrieved = false;
|
let mut retrieved = false;
|
||||||
for p in candidates.iter().filter(|s| !s.is_empty()) {
|
for p in candidates.iter().filter(|s| !s.is_empty()) {
|
||||||
match ftp_stream.retr(p, |reader: &mut dyn Read| -> std::result::Result<(), ftp::FtpError> {
|
match ftp_stream.retr(
|
||||||
let mut file = File::create(&dest_path).map_err(ftp::FtpError::ConnectionError)?;
|
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 buffer = [0u8; 8192];
|
||||||
loop {
|
loop {
|
||||||
let bytes_read = reader.read(&mut buffer).map_err(ftp::FtpError::ConnectionError)?;
|
let bytes_read = reader
|
||||||
if bytes_read == 0 { break; }
|
.read(&mut buffer)
|
||||||
file.write_all(&buffer[..bytes_read]).map_err(ftp::FtpError::ConnectionError)?;
|
.map_err(suppaftp::FtpError::ConnectionError)?;
|
||||||
|
if bytes_read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
file.write_all(&buffer[..bytes_read])
|
||||||
|
.map_err(suppaftp::FtpError::ConnectionError)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
retrieved = true;
|
retrieved = true;
|
||||||
break;
|
break;
|
||||||
@@ -74,10 +92,8 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
|||||||
// Use a sensible default User-Agent so servers that reject empty/unknown agents (e.g. IANA)
|
// 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.
|
// will accept requests. Include package name/version at compile time.
|
||||||
let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
||||||
let client = reqwest::blocking::Client::builder()
|
let client =
|
||||||
.user_agent(ua)
|
super::build_blocking_client(&ua, None).with_context(|| "Failed to build HTTP client")?;
|
||||||
.build()
|
|
||||||
.with_context(|| "Failed to build HTTP client")?;
|
|
||||||
|
|
||||||
let mut response = client
|
let mut response = client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
@@ -132,42 +148,71 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
|||||||
|
|
||||||
// Quick validation: ensure the downloaded file looks like the expected
|
// Quick validation: ensure the downloaded file looks like the expected
|
||||||
// archive (detect obvious HTML error pages or wrong formats by magic).
|
// archive (detect obvious HTML error pages or wrong formats by magic).
|
||||||
// If validate_downloaded_archive returns an alternate URL (e.g. SourceForge
|
// If validation returns an alternate URL (e.g. SourceForge mirror), follow
|
||||||
// mirror), retry the download with that URL once.
|
// and retry a few times.
|
||||||
if let Some(alt) = validate_downloaded_archive(&dest_path, &filename, &url)? {
|
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);
|
||||||
|
}
|
||||||
println!("Retrying download from mirror: {}", alt);
|
println!("Retrying download from mirror: {}", alt);
|
||||||
fs::remove_file(&dest_path).ok();
|
fs::remove_file(&dest_path).ok();
|
||||||
|
|
||||||
// If mirror URL is FTP -> use ftp crate; otherwise use HTTP retry.
|
// If mirror URL is FTP -> use suppaftp; otherwise use HTTP retry.
|
||||||
if let Ok(alt_url) = Url::parse(&alt) {
|
if let Ok(alt_url) = Url::parse(&alt) {
|
||||||
if alt_url.scheme() == "ftp" {
|
if alt_url.scheme() == "ftp" {
|
||||||
// FTP mirror retrieval
|
// FTP mirror retrieval
|
||||||
let host = alt_url.host_str().context("FTP mirror URL missing host")?;
|
let host = alt_url.host_str().context("FTP mirror URL missing host")?;
|
||||||
let port = alt_url.port_or_known_default().unwrap_or(21);
|
let port = alt_url.port_or_known_default().unwrap_or(21);
|
||||||
let addr = format!("{}:{}", host, port);
|
let addr = format!("{}:{}", host, port);
|
||||||
let mut ftp_stream = ftp::FtpStream::connect(addr.as_str())
|
let mut ftp_stream = suppaftp::FtpStream::connect(addr.as_str())
|
||||||
.with_context(|| format!("Failed to connect to FTP host: {}", addr))?;
|
.with_context(|| format!("Failed to connect to FTP host: {}", addr))?;
|
||||||
let user = if alt_url.username().is_empty() { "anonymous" } else { alt_url.username() };
|
let user = if alt_url.username().is_empty() {
|
||||||
|
"anonymous"
|
||||||
|
} else {
|
||||||
|
alt_url.username()
|
||||||
|
};
|
||||||
let pass = alt_url.password().unwrap_or("anonymous@");
|
let pass = alt_url.password().unwrap_or("anonymous@");
|
||||||
ftp_stream.login(user, pass).with_context(|| format!("FTP login failed for {}", host))?;
|
ftp_stream
|
||||||
|
.login(user, pass)
|
||||||
|
.with_context(|| format!("FTP login failed for {}", host))?;
|
||||||
|
|
||||||
let path = alt_url.path();
|
let path = alt_url.path();
|
||||||
let candidates = [path.to_string(), path.trim_start_matches('/').to_string()];
|
let candidates = [path.to_string(), path.trim_start_matches('/').to_string()];
|
||||||
let mut retrieved = false;
|
let mut retrieved = false;
|
||||||
for p in candidates.iter().filter(|s| !s.is_empty()) {
|
for p in candidates.iter().filter(|s| !s.is_empty()) {
|
||||||
match ftp_stream.retr(p, |reader: &mut dyn Read| -> std::result::Result<(), ftp::FtpError> {
|
match ftp_stream.retr(
|
||||||
let mut file = File::create(&dest_path).map_err(ftp::FtpError::ConnectionError)?;
|
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 buffer = [0u8; 8192];
|
||||||
let mut downloaded = 0u64;
|
let mut downloaded = 0u64;
|
||||||
loop {
|
loop {
|
||||||
let bytes_read = reader.read(&mut buffer).map_err(ftp::FtpError::ConnectionError)?;
|
let bytes_read = reader
|
||||||
if bytes_read == 0 { break; }
|
.read(&mut buffer)
|
||||||
file.write_all(&buffer[..bytes_read]).map_err(ftp::FtpError::ConnectionError)?;
|
.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;
|
downloaded += bytes_read as u64;
|
||||||
pb.set_position(downloaded);
|
pb.set_position(downloaded);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
retrieved = true;
|
retrieved = true;
|
||||||
break;
|
break;
|
||||||
@@ -182,15 +227,30 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
|||||||
} else {
|
} else {
|
||||||
// HTTP(S) mirror retry (recreate client for retry)
|
// HTTP(S) mirror retry (recreate client for retry)
|
||||||
let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
||||||
let client = reqwest::blocking::Client::builder()
|
let client = super::build_blocking_client(&ua, None)
|
||||||
.user_agent(ua)
|
|
||||||
.build()
|
|
||||||
.with_context(|| "Failed to build HTTP client")?;
|
.with_context(|| "Failed to build HTTP client")?;
|
||||||
let mut response = client
|
let mut response = client
|
||||||
.get(&alt)
|
.get(&alt)
|
||||||
.send()
|
.send()
|
||||||
.with_context(|| format!("Failed to fetch mirror URL: {}", alt))?;
|
.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)
|
let mut file = File::create(&dest_path)
|
||||||
.with_context(|| format!("Failed to create: {}", dest_path.display()))?;
|
.with_context(|| format!("Failed to create: {}", dest_path.display()))?;
|
||||||
let mut buffer = [0u8; 8192];
|
let mut buffer = [0u8; 8192];
|
||||||
@@ -211,9 +271,7 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
|||||||
}
|
}
|
||||||
|
|
||||||
pb.finish_with_message("Download complete (mirror)");
|
pb.finish_with_message("Download complete (mirror)");
|
||||||
|
next_alt = validate_downloaded_archive(&dest_path, &filename, &alt)?;
|
||||||
// Re-validate the mirrored file
|
|
||||||
validate_downloaded_archive(&dest_path, &filename, &alt)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checksum
|
// Verify checksum
|
||||||
@@ -239,13 +297,12 @@ fn verify_checksum(path: &Path, expected: &str) -> Result<bool> {
|
|||||||
super::verify_file_hash(path, expected)
|
super::verify_file_hash(path, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Derive a stable filename from a URL.
|
/// Derive a stable filename from a URL.
|
||||||
///
|
///
|
||||||
/// Rules:
|
/// Rules:
|
||||||
/// - Parse as URL and use the last path segment if it looks like a filename (contains a dot)
|
/// - Parse as URL and use the last path segment if it looks like a filename (contains a dot)
|
||||||
/// - Otherwise fall back to a stable hash-based name: source-{sha256(url)[..12]}.download
|
/// - Otherwise fall back to a stable hash-based name: source-{sha256(url)[..12]}.download
|
||||||
fn derive_filename_from_url(url: &str) -> String {
|
pub(crate) fn derive_filename_from_url(url: &str) -> String {
|
||||||
// try to parse the URL
|
// try to parse the URL
|
||||||
if let Some(last) = Url::parse(url).ok().and_then(|parsed| {
|
if let Some(last) = Url::parse(url).ok().and_then(|parsed| {
|
||||||
parsed
|
parsed
|
||||||
@@ -267,7 +324,11 @@ fn derive_filename_from_url(url: &str) -> String {
|
|||||||
|
|
||||||
/// Validate downloaded file's magic header to make sure it is the expected
|
/// Validate downloaded file's magic header to make sure it is the expected
|
||||||
/// archive format (avoids saving HTML pages or other unexpected content).
|
/// archive format (avoids saving HTML pages or other unexpected content).
|
||||||
fn validate_downloaded_archive(path: &std::path::Path, filename: &str, orig_url: &str) -> Result<Option<String>> {
|
fn validate_downloaded_archive(
|
||||||
|
path: &std::path::Path,
|
||||||
|
filename: &str,
|
||||||
|
orig_url: &str,
|
||||||
|
) -> Result<Option<String>> {
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
let mut f = std::fs::File::open(path)?;
|
let mut f = std::fs::File::open(path)?;
|
||||||
let mut buf = [0u8; 4096];
|
let mut buf = [0u8; 4096];
|
||||||
@@ -275,114 +336,251 @@ fn validate_downloaded_archive(path: &std::path::Path, filename: &str, orig_url:
|
|||||||
let head = &buf[..n.min(4096)];
|
let head = &buf[..n.min(4096)];
|
||||||
|
|
||||||
// Detect obvious HTML error pages (case-insensitive)
|
// Detect obvious HTML error pages (case-insensitive)
|
||||||
let head_str = String::from_utf8_lossy(head).to_ascii_lowercase();
|
if is_html_content(head) {
|
||||||
if head_str.starts_with("<!doctype html") || head_str.starts_with("<html") || head_str.contains("<html") {
|
if url_contains_sourceforge_host(orig_url) {
|
||||||
// If this came from SourceForge project URL, try to extract a direct
|
let body = std::fs::read(path)
|
||||||
// downloads.sourceforge.net mirror link and return it for a retry.
|
.map(|bytes| String::from_utf8_lossy(&bytes).to_string())
|
||||||
let body = String::from_utf8_lossy(head).to_string();
|
.unwrap_or_else(|_| String::from_utf8_lossy(head).to_string());
|
||||||
if orig_url.contains("sourceforge.net") {
|
if let Some(alt) = sourceforge_alt_url_from_html(&body, orig_url) {
|
||||||
// helper: find nearest href attribute before `pos` and support
|
|
||||||
// both double- and single-quoted attributes.
|
|
||||||
let find_href_before = |body: &str, pos: usize| -> Option<String> {
|
|
||||||
if let Some(start) = body[..pos].rfind("href=\"") {
|
|
||||||
let rest = &body[start + 6..];
|
|
||||||
if let Some(end) = rest.find('"') {
|
|
||||||
return Some(rest[..end].to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(start) = body[..pos].rfind("href='") {
|
|
||||||
let rest = &body[start + 6..];
|
|
||||||
if let Some(end) = rest.find('\'') {
|
|
||||||
return Some(rest[..end].to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// look for downloads.sourceforge.net links in HTML
|
|
||||||
if let Some(pos) = body.find("downloads.sourceforge.net") {
|
|
||||||
if let Some(href) = find_href_before(&body, pos) {
|
|
||||||
let alt = if href.starts_with("//") {
|
|
||||||
format!("https:{}", href)
|
|
||||||
} else if href.starts_with("/") {
|
|
||||||
format!("https://downloads.sourceforge.net{}", href)
|
|
||||||
} else {
|
|
||||||
href
|
|
||||||
};
|
|
||||||
return Ok(Some(alt));
|
return Ok(Some(alt));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also try to find '/download' link anywhere in the body
|
|
||||||
if let Some(pos) = body.find("/download") {
|
|
||||||
if let Some(href) = find_href_before(&body, pos) {
|
|
||||||
let alt = if href.starts_with("//") {
|
|
||||||
format!("https:{}", href)
|
|
||||||
} else {
|
|
||||||
href
|
|
||||||
};
|
|
||||||
return Ok(Some(alt));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final fallback for SF project pages: append '/download' to the
|
|
||||||
// original URL (this normally triggers the mirror redirect).
|
|
||||||
return Ok(Some(format!("{}/download", orig_url.trim_end_matches('/'))));
|
|
||||||
}
|
|
||||||
|
|
||||||
let preview = String::from_utf8_lossy(&head[..head.len().min(1024)]);
|
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Downloaded file '{}' looks like HTML (not an archive). Preview: {}",
|
"Downloaded file '{}' looks like HTML (not an archive). Preview: {}",
|
||||||
filename,
|
filename,
|
||||||
preview.trim()
|
html_preview(head)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate by extension (best-effort)
|
// Validate by extension (best-effort)
|
||||||
let lower = filename.to_ascii_lowercase();
|
let lower = filename.to_ascii_lowercase();
|
||||||
let is_ok = if lower.ends_with(".tar.xz") || lower.ends_with(".txz") || lower.ends_with(".xz") {
|
let is_ok = classify_archive_magic(head, &lower, path);
|
||||||
|
|
||||||
|
if !is_ok {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Downloaded file '{}' does not match expected archive magic; preview: {}",
|
||||||
|
filename,
|
||||||
|
html_preview(head)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sourceforge_alt_url_from_html(body: &str, orig_url: &str) -> Option<String> {
|
||||||
|
let lower = body.to_ascii_lowercase();
|
||||||
|
for href in extract_hrefs(body, &lower) {
|
||||||
|
if href.contains("downloads.sourceforge.net")
|
||||||
|
&& let Some(url) = sourceforge_candidate_from_href(&href)
|
||||||
|
{
|
||||||
|
return Some(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for href in extract_hrefs(body, &lower) {
|
||||||
|
if href.contains("/download")
|
||||||
|
&& let Some(url) = sourceforge_candidate_from_href(&href)
|
||||||
|
{
|
||||||
|
return Some(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceforge_download_fallback(orig_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_hrefs(body: &str, lower: &str) -> Vec<String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut i = 0usize;
|
||||||
|
while let Some(rel) = lower[i..].find("href=") {
|
||||||
|
let start = i + rel + 5;
|
||||||
|
if start >= body.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let quote = body.as_bytes()[start] as char;
|
||||||
|
if quote != '"' && quote != '\'' {
|
||||||
|
i = start + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let val_start = start + 1;
|
||||||
|
if val_start > body.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(end_rel) = body[val_start..].find(quote) {
|
||||||
|
out.push(body[val_start..val_start + end_rel].to_string());
|
||||||
|
i = val_start + end_rel + 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_downloads_sf_href(href: &str) -> String {
|
||||||
|
let href = href.replace("&", "&");
|
||||||
|
if href.starts_with("//") {
|
||||||
|
format!("https:{}", href)
|
||||||
|
} else if href.starts_with('/') {
|
||||||
|
format!("https://downloads.sourceforge.net{}", href)
|
||||||
|
} else {
|
||||||
|
href
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_sf_href(href: &str) -> String {
|
||||||
|
let href = href.replace("&", "&");
|
||||||
|
if href.starts_with("//") {
|
||||||
|
format!("https:{}", href)
|
||||||
|
} else if href.starts_with('/') {
|
||||||
|
format!("https://sourceforge.net{}", href)
|
||||||
|
} else {
|
||||||
|
href
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sourceforge_candidate_from_href(href: &str) -> Option<String> {
|
||||||
|
let normalized = if href.contains("downloads.sourceforge.net") {
|
||||||
|
normalize_downloads_sf_href(href)
|
||||||
|
} else {
|
||||||
|
normalize_sf_href(href)
|
||||||
|
};
|
||||||
|
let parsed = Url::parse(&normalized).ok()?;
|
||||||
|
let host = parsed.host_str()?.to_ascii_lowercase();
|
||||||
|
|
||||||
|
if is_sourceforge_host(&host) || is_downloads_sourceforge_host(&host) {
|
||||||
|
return Some(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some HTML pages use social share links that embed a real SourceForge URL
|
||||||
|
// in a query parameter (e.g. x.com/share?url=...).
|
||||||
|
if is_social_share_host(&host) {
|
||||||
|
for (k, v) in parsed.query_pairs() {
|
||||||
|
if (k.eq_ignore_ascii_case("url") || k.eq_ignore_ascii_case("u"))
|
||||||
|
&& let Ok(inner) = Url::parse(v.as_ref())
|
||||||
|
&& let Some(inner_host) = inner.host_str()
|
||||||
|
{
|
||||||
|
let inner_host = inner_host.to_ascii_lowercase();
|
||||||
|
if is_sourceforge_host(&inner_host) || is_downloads_sourceforge_host(&inner_host) {
|
||||||
|
return Some(inner.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sourceforge_download_fallback(orig_url: &str) -> Option<String> {
|
||||||
|
let parsed = Url::parse(orig_url).ok()?;
|
||||||
|
let host = parsed.host_str()?.to_ascii_lowercase();
|
||||||
|
|
||||||
|
if is_sourceforge_host(&host) || is_downloads_sourceforge_host(&host) {
|
||||||
|
return Some(format!("{}/download", orig_url.trim_end_matches('/')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_social_share_host(&host) {
|
||||||
|
for (k, v) in parsed.query_pairs() {
|
||||||
|
if (k.eq_ignore_ascii_case("url") || k.eq_ignore_ascii_case("u"))
|
||||||
|
&& let Ok(inner) = Url::parse(v.as_ref())
|
||||||
|
&& let Some(inner_host) = inner.host_str()
|
||||||
|
{
|
||||||
|
let inner_host = inner_host.to_ascii_lowercase();
|
||||||
|
if is_sourceforge_host(&inner_host) || is_downloads_sourceforge_host(&inner_host) {
|
||||||
|
return Some(format!("{}/download", inner.as_str().trim_end_matches('/')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_sourceforge_host(host: &str) -> bool {
|
||||||
|
host == "sourceforge.net" || host.ends_with(".sourceforge.net")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_downloads_sourceforge_host(host: &str) -> bool {
|
||||||
|
host == "downloads.sourceforge.net" || host.ends_with(".downloads.sourceforge.net")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_social_share_host(host: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
host,
|
||||||
|
"x.com"
|
||||||
|
| "www.x.com"
|
||||||
|
| "twitter.com"
|
||||||
|
| "www.twitter.com"
|
||||||
|
| "facebook.com"
|
||||||
|
| "www.facebook.com"
|
||||||
|
| "linkedin.com"
|
||||||
|
| "www.linkedin.com"
|
||||||
|
| "reddit.com"
|
||||||
|
| "www.reddit.com"
|
||||||
|
| "t.me"
|
||||||
|
| "telegram.me"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url_contains_sourceforge_host(url: &str) -> bool {
|
||||||
|
Url::parse(url)
|
||||||
|
.ok()
|
||||||
|
.and_then(|u| u.host_str().map(|h| h.to_ascii_lowercase()))
|
||||||
|
.map(|h| is_sourceforge_host(&h) || is_downloads_sourceforge_host(&h))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
url.contains("://sourceforge.net/")
|
||||||
|
|| url.contains("://downloads.sourceforge.net/")
|
||||||
|
|| url.contains(".sourceforge.net/")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn html_preview(head: &[u8]) -> String {
|
||||||
|
String::from_utf8_lossy(&head[..head.len().min(1024)])
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_html_content(head: &[u8]) -> bool {
|
||||||
|
let head_str = String::from_utf8_lossy(head).to_ascii_lowercase();
|
||||||
|
head_str.starts_with("<!doctype html")
|
||||||
|
|| head_str.starts_with("<html")
|
||||||
|
|| head_str.contains("<html")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_archive_magic(head: &[u8], lower_filename: &str, path: &Path) -> bool {
|
||||||
|
if lower_filename.ends_with(".tar.xz")
|
||||||
|
|| lower_filename.ends_with(".txz")
|
||||||
|
|| lower_filename.ends_with(".xz")
|
||||||
|
{
|
||||||
head.starts_with(&[0xFD, b'7', b'z', b'X', b'Z', 0x00])
|
head.starts_with(&[0xFD, b'7', b'z', b'X', b'Z', 0x00])
|
||||||
} else if lower.ends_with(".tar.gz") || lower.ends_with(".tgz") || lower.ends_with(".gz") {
|
} else if lower_filename.ends_with(".tar.gz")
|
||||||
|
|| lower_filename.ends_with(".tgz")
|
||||||
|
|| lower_filename.ends_with(".gz")
|
||||||
|
{
|
||||||
head.starts_with(&[0x1F, 0x8B])
|
head.starts_with(&[0x1F, 0x8B])
|
||||||
} else if lower.ends_with(".tar.zst") || lower.ends_with(".tzst") || lower.ends_with(".zst") {
|
} else if lower_filename.ends_with(".tar.zst")
|
||||||
|
|| lower_filename.ends_with(".tzst")
|
||||||
|
|| lower_filename.ends_with(".zst")
|
||||||
|
{
|
||||||
head.starts_with(&[0x28, 0xB5, 0x2F, 0xFD])
|
head.starts_with(&[0x28, 0xB5, 0x2F, 0xFD])
|
||||||
} else if lower.ends_with(".zip") {
|
} else if lower_filename.ends_with(".zip") {
|
||||||
head.starts_with(b"PK\x03\x04")
|
head.starts_with(b"PK\x03\x04")
|
||||||
} else if lower.ends_with(".tar.bz2") || lower.ends_with(".tbz2") {
|
} else if lower_filename.ends_with(".tar.bz2") || lower_filename.ends_with(".tbz2") {
|
||||||
head.starts_with(&[0x42, 0x5A, 0x68])
|
head.starts_with(&[0x42, 0x5A, 0x68])
|
||||||
} else if lower.ends_with(".tar") {
|
} else if lower_filename.ends_with(".tar") {
|
||||||
// check for ustar magic at offset 257
|
|
||||||
if let Ok(mut f2) = std::fs::File::open(path) {
|
if let Ok(mut f2) = std::fs::File::open(path) {
|
||||||
let mut hdr = [0u8; 262];
|
let mut hdr = [0u8; 262];
|
||||||
if f2.read_exact(&mut hdr).is_ok() {
|
if f2.read_exact(&mut hdr).is_ok() {
|
||||||
&hdr[257..262] == b"ustar"
|
&hdr[257..262] == b"ustar"
|
||||||
} else {
|
} else {
|
||||||
true // can't validate; be permissive
|
true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
} else if lower.ends_with(".deb") {
|
} else if lower_filename.ends_with(".deb") {
|
||||||
// ar archive starts with "!<arch>\n"
|
|
||||||
head.starts_with(b"!<arch>")
|
head.starts_with(b"!<arch>")
|
||||||
} else if lower.ends_with(".rpm") {
|
|
||||||
// rpm contains cpio magic later; best-effort: accept
|
|
||||||
true
|
|
||||||
} else {
|
} else {
|
||||||
// Unknown extension -> be permissive
|
// rpm/unknown extensions: keep permissive as before.
|
||||||
true
|
true
|
||||||
};
|
|
||||||
|
|
||||||
if !is_ok {
|
|
||||||
let preview = String::from_utf8_lossy(&head[..head.len().min(1024)]);
|
|
||||||
anyhow::bail!(
|
|
||||||
"Downloaded file '{}' does not match expected archive magic; preview: {}",
|
|
||||||
filename,
|
|
||||||
preview.trim()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -413,7 +611,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn filename_from_ftp_url() {
|
fn filename_from_ftp_url() {
|
||||||
assert_eq!(derive_filename_from_url("ftp://example.com/foo-1.2.3.tar.gz"), "foo-1.2.3.tar.gz");
|
assert_eq!(
|
||||||
|
derive_filename_from_url("ftp://example.com/foo-1.2.3.tar.gz"),
|
||||||
|
"foo-1.2.3.tar.gz"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -426,7 +627,11 @@ mod tests {
|
|||||||
fn sourceforge_html_no_link_falls_back_to_download_suffix() {
|
fn sourceforge_html_no_link_falls_back_to_download_suffix() {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let mut tmp = tempfile::NamedTempFile::new().unwrap();
|
let mut tmp = tempfile::NamedTempFile::new().unwrap();
|
||||||
write!(tmp, "<!doctype html><html><body>No direct link</body></html>").unwrap();
|
write!(
|
||||||
|
tmp,
|
||||||
|
"<!doctype html><html><body>No direct link</body></html>"
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let alt = validate_downloaded_archive(
|
let alt = validate_downloaded_archive(
|
||||||
tmp.path(),
|
tmp.path(),
|
||||||
"zsh-5.9.tar.xz",
|
"zsh-5.9.tar.xz",
|
||||||
@@ -455,7 +660,73 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
alt,
|
alt,
|
||||||
Some("https://downloads.sourceforge.net/project/zsh/zsh/5.9/zsh-5.9.tar.xz?download".to_string())
|
Some(
|
||||||
|
"https://downloads.sourceforge.net/project/zsh/zsh/5.9/zsh-5.9.tar.xz?download"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_html_large_page_extracts_downloads_link_beyond_4k() {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut tmp = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
write!(
|
||||||
|
tmp,
|
||||||
|
"<!doctype html><html><body>{}<a href='//downloads.sourceforge.net/project/tcl/tcl8.6.17-src.tar.gz?download'>download</a></body></html>",
|
||||||
|
"x".repeat(9000)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let alt = validate_downloaded_archive(
|
||||||
|
tmp.path(),
|
||||||
|
"tcl8.6.17-src.tar.gz",
|
||||||
|
"https://sourceforge.net/projects/tcl/files/tcl8.6.17-src.tar.gz",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
alt,
|
||||||
|
Some(
|
||||||
|
"https://downloads.sourceforge.net/project/tcl/tcl8.6.17-src.tar.gz?download"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_html_ignores_social_share_links_and_unwraps_url_param() {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut tmp = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
write!(
|
||||||
|
tmp,
|
||||||
|
"<!doctype html><a href='https://x.com/share?url=https://sourceforge.net/projects/tcl/files/Tcl/8.6.17/tcl8.6.17-src.tar.gz/download&text=share'>share</a>"
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let alt = validate_downloaded_archive(
|
||||||
|
tmp.path(),
|
||||||
|
"tcl8.6.17-src.tar.gz",
|
||||||
|
"https://sourceforge.net/projects/tcl/files/Tcl/8.6.17/tcl8.6.17-src.tar.gz",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
alt,
|
||||||
|
Some(
|
||||||
|
"https://sourceforge.net/projects/tcl/files/Tcl/8.6.17/tcl8.6.17-src.tar.gz/download"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sourceforge_share_url_fallback_uses_embedded_sourceforge_url() {
|
||||||
|
let fallback = sourceforge_download_fallback(
|
||||||
|
"https://x.com/share?url=https://sourceforge.net/projects/tcl/files/Tcl/8.6.17/tcl8.6.17-src.tar.gz",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fallback,
|
||||||
|
Some(
|
||||||
|
"https://sourceforge.net/projects/tcl/files/Tcl/8.6.17/tcl8.6.17-src.tar.gz/download"
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,5 +766,3 @@ mod tests {
|
|||||||
assert!(!verify_checksum(tmp.path(), "md5:deadbeef").unwrap());
|
assert!(!verify_checksum(tmp.path(), "md5:deadbeef").unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+13
-6
@@ -19,15 +19,23 @@ pub fn checkout(
|
|||||||
git_cache_dir: &Path,
|
git_cache_dir: &Path,
|
||||||
pkgname: &str,
|
pkgname: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
fs::create_dir_all(git_cache_dir)
|
fs::create_dir_all(git_cache_dir).with_context(|| {
|
||||||
.with_context(|| format!("Failed to create git cache dir: {}", git_cache_dir.display()))?;
|
format!(
|
||||||
|
"Failed to create git cache dir: {}",
|
||||||
|
git_cache_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let mirror_dir = git_cache_dir.join(mirror_key(url));
|
let mirror_dir = git_cache_dir.join(mirror_key(url));
|
||||||
ensure_mirror(url, &mirror_dir, pkgname)?;
|
ensure_mirror(url, &mirror_dir, pkgname)?;
|
||||||
|
|
||||||
if checkout_dir.exists() {
|
if checkout_dir.exists() {
|
||||||
fs::remove_dir_all(checkout_dir)
|
fs::remove_dir_all(checkout_dir).with_context(|| {
|
||||||
.with_context(|| format!("Failed to remove existing checkout: {}", checkout_dir.display()))?;
|
format!(
|
||||||
|
"Failed to remove existing checkout: {}",
|
||||||
|
checkout_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone from local mirror for speed.
|
// Clone from local mirror for speed.
|
||||||
@@ -40,8 +48,7 @@ pub fn checkout(
|
|||||||
.with_context(|| format!("Failed to clone from mirror for {}", url))?;
|
.with_context(|| format!("Failed to clone from mirror for {}", url))?;
|
||||||
|
|
||||||
let repo = Repository::open(checkout_dir)?;
|
let repo = Repository::open(checkout_dir)?;
|
||||||
checkout_rev(&repo, rev)
|
checkout_rev(&repo, rev).with_context(|| format!("Failed to checkout revision '{}'", rev))?;
|
||||||
.with_context(|| format!("Failed to checkout revision '{}'", rev))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -253,7 +253,7 @@ mod tests {
|
|||||||
revision: 1,
|
revision: 1,
|
||||||
description: "d".into(),
|
description: "d".into(),
|
||||||
homepage: "h".into(),
|
homepage: "h".into(),
|
||||||
license: "MIT".into(),
|
license: vec!["MIT".into()],
|
||||||
},
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Alternatives::default(),
|
alternatives: Alternatives::default(),
|
||||||
|
|||||||
+182
-19
@@ -11,13 +11,15 @@ use sha2::{Digest, Sha256};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::fs as unix_fs;
|
use std::os::unix::fs as unix_fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use url::Url;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
/// Copy manual (local) sources to the build directory before fetching remote sources.
|
/// Copy manual sources to the build directory before fetching remote sources.
|
||||||
///
|
///
|
||||||
/// Manual sources are checked first, allowing local files like utility source code
|
/// Manual sources support:
|
||||||
/// to be available during the build process.
|
/// - local mode: `file = "..."` (path relative to spec directory)
|
||||||
pub fn copy_manual_sources(spec: &PackageSpec, build_dir: &Path) -> Result<()> {
|
/// - remote mode: `url = "..."` (downloaded first, then copied)
|
||||||
|
pub fn copy_manual_sources(spec: &PackageSpec, cache_dir: &Path, build_dir: &Path) -> Result<()> {
|
||||||
if spec.manual_sources.is_empty() {
|
if spec.manual_sources.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -26,40 +28,83 @@ pub fn copy_manual_sources(spec: &PackageSpec, build_dir: &Path) -> Result<()> {
|
|||||||
println!("Copying {} manual source(s)...", spec.manual_sources.len());
|
println!("Copying {} manual source(s)...", spec.manual_sources.len());
|
||||||
|
|
||||||
for manual in &spec.manual_sources {
|
for manual in &spec.manual_sources {
|
||||||
let src_path = spec.spec_dir.join(&manual.file);
|
let (source_path, source_label, default_dest): (PathBuf, String, String) =
|
||||||
|
if let Some(file) = manual.file.as_ref() {
|
||||||
|
let file = spec.expand_vars(file);
|
||||||
|
let src_path = spec.spec_dir.join(&file);
|
||||||
if !src_path.exists() {
|
if !src_path.exists() {
|
||||||
bail!(
|
bail!(
|
||||||
"Manual source not found: {} (expected at {})",
|
"Manual source not found: {} (expected at {})",
|
||||||
manual.file,
|
file,
|
||||||
src_path.display()
|
src_path.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checksum if provided (supports `sha256:...`, `sha512:...`, `md5:...`, or raw SHA256).
|
// Verify checksum if provided.
|
||||||
if let Some(expected_hash) = manual.sha256.as_ref().filter(|h| *h != "skip") {
|
if let Some(expected_hash) = manual.sha256.as_ref().filter(|h| *h != "skip")
|
||||||
if !verify_file_hash(&src_path, expected_hash)? {
|
&& !verify_file_hash(&src_path, expected_hash)?
|
||||||
|
{
|
||||||
|
bail!("Checksum mismatch for {}: expected {}", file, expected_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
(src_path, file.clone(), file)
|
||||||
|
} else if let Some(url_raw) = manual.url.as_ref() {
|
||||||
|
let expanded_url = spec.expand_vars(url_raw);
|
||||||
|
let parsed = Url::parse(&expanded_url)
|
||||||
|
.with_context(|| format!("Invalid URL: {}", expanded_url))?;
|
||||||
|
|
||||||
|
if parsed.scheme() == "file" {
|
||||||
|
let src_path = parsed
|
||||||
|
.to_file_path()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Invalid file URL: {}", expanded_url))?;
|
||||||
|
if !src_path.exists() {
|
||||||
|
bail!("Manual source file URL not found: {}", src_path.display());
|
||||||
|
}
|
||||||
|
if let Some(expected_hash) = manual.sha256.as_ref().filter(|h| *h != "skip")
|
||||||
|
&& !verify_file_hash(&src_path, expected_hash)?
|
||||||
|
{
|
||||||
bail!(
|
bail!(
|
||||||
"Checksum mismatch for {}: expected {}",
|
"Checksum mismatch for {}: expected {}",
|
||||||
manual.file,
|
expanded_url,
|
||||||
expected_hash
|
expected_hash
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let default_dest = fetcher::derive_filename_from_url(&expanded_url);
|
||||||
|
(src_path, expanded_url, default_dest)
|
||||||
|
} else {
|
||||||
|
let source = crate::package::Source {
|
||||||
|
url: expanded_url.clone(),
|
||||||
|
sha256: manual.sha256.clone().unwrap_or_else(|| "skip".to_string()),
|
||||||
|
extract_dir: "manual-source".to_string(),
|
||||||
|
patches: Vec::new(),
|
||||||
|
post_extract: Vec::new(),
|
||||||
|
};
|
||||||
|
let fetched = fetcher::fetch_archive(spec, &source, &cache_dir.join("manual"))?;
|
||||||
|
let default_dest = fetcher::derive_filename_from_url(&expanded_url);
|
||||||
|
(fetched, expanded_url, default_dest)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
bail!("Manual source must define either 'file' or 'url'");
|
||||||
|
};
|
||||||
|
|
||||||
// Determine destination
|
// Determine destination
|
||||||
let dest_name = manual.dest.as_deref().unwrap_or(&manual.file);
|
let dest_name = if let Some(dest) = manual.dest.as_ref() {
|
||||||
let dest_path = build_dir.join(dest_name);
|
spec.expand_vars(dest)
|
||||||
|
} else {
|
||||||
|
default_dest
|
||||||
|
};
|
||||||
|
let dest_path = build_dir.join(&dest_name);
|
||||||
|
|
||||||
// Create parent directories if needed
|
// Create parent directories if needed
|
||||||
if let Some(parent) = dest_path.parent() {
|
if let Some(parent) = dest_path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(" {} -> {}", manual.file, dest_path.display());
|
println!(" {} -> {}", source_label, dest_path.display());
|
||||||
fs::copy(&src_path, &dest_path).with_context(|| {
|
fs::copy(&source_path, &dest_path).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to copy {} to {}",
|
"Failed to copy {} to {}",
|
||||||
src_path.display(),
|
source_path.display(),
|
||||||
dest_path.display()
|
dest_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
@@ -85,7 +130,11 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
|
|||||||
let (alg, hex) = if let Some(pos) = exp.find(':') {
|
let (alg, hex) = if let Some(pos) = exp.find(':') {
|
||||||
let a = exp[..pos].trim().to_ascii_lowercase();
|
let a = exp[..pos].trim().to_ascii_lowercase();
|
||||||
let h = exp[pos + 1..].trim().to_ascii_lowercase();
|
let h = exp[pos + 1..].trim().to_ascii_lowercase();
|
||||||
let alg = if a.is_empty() { "sha256".to_string() } else { a };
|
let alg = if a.is_empty() {
|
||||||
|
"sha256".to_string()
|
||||||
|
} else {
|
||||||
|
a
|
||||||
|
};
|
||||||
(alg, h.to_string())
|
(alg, h.to_string())
|
||||||
} else {
|
} else {
|
||||||
("sha256".to_string(), exp.to_ascii_lowercase())
|
("sha256".to_string(), exp.to_ascii_lowercase())
|
||||||
@@ -140,6 +189,22 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a blocking reqwest HTTP client using the platform-default TLS backend.
|
||||||
|
/// Any error building the client is returned directly (no fallback).
|
||||||
|
pub(crate) fn build_blocking_client(
|
||||||
|
user_agent: &str,
|
||||||
|
timeout: Option<std::time::Duration>,
|
||||||
|
) -> anyhow::Result<reqwest::blocking::Client> {
|
||||||
|
let mut builder = reqwest::blocking::Client::builder().user_agent(user_agent.to_string());
|
||||||
|
if let Some(t) = timeout {
|
||||||
|
builder = builder.timeout(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.build()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to build HTTP client: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch + extract all sources.
|
/// Fetch + extract all sources.
|
||||||
///
|
///
|
||||||
/// Returns the primary source directory (the first source entry, or work_dir for manual-only packages).
|
/// Returns the primary source directory (the first source entry, or work_dir for manual-only packages).
|
||||||
@@ -148,12 +213,12 @@ pub fn prepare(spec: &PackageSpec, cache_dir: &Path, build_dir: &Path) -> Result
|
|||||||
if spec.sources().is_empty() {
|
if spec.sources().is_empty() {
|
||||||
let work_dir = build_dir.join(&spec.package.name);
|
let work_dir = build_dir.join(&spec.package.name);
|
||||||
fs::create_dir_all(&work_dir)?;
|
fs::create_dir_all(&work_dir)?;
|
||||||
copy_manual_sources(spec, &work_dir)?;
|
copy_manual_sources(spec, cache_dir, &work_dir)?;
|
||||||
return Ok(work_dir);
|
return Ok(work_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy manual sources first (before any remote fetching)
|
// Copy manual sources first (before any remote fetching)
|
||||||
copy_manual_sources(spec, build_dir)?;
|
copy_manual_sources(spec, cache_dir, build_dir)?;
|
||||||
|
|
||||||
let mut primary: Option<PathBuf> = None;
|
let mut primary: Option<PathBuf> = None;
|
||||||
|
|
||||||
@@ -311,6 +376,39 @@ fn split_git_url(url: &str) -> Option<(String, String)> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::package::{
|
||||||
|
Alternatives, Build, BuildFlags, BuildType, Dependencies, ManualSource, PackageInfo,
|
||||||
|
PackageSpec, Source,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn mk_spec_with_manuals(spec_dir: PathBuf, manuals: Vec<ManualSource>) -> PackageSpec {
|
||||||
|
PackageSpec {
|
||||||
|
package: PackageInfo {
|
||||||
|
name: "foo".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "d".into(),
|
||||||
|
homepage: "h".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: Alternatives::default(),
|
||||||
|
manual_sources: manuals,
|
||||||
|
source: vec![Source {
|
||||||
|
url: "https://example.com/src.tar.gz".into(),
|
||||||
|
sha256: "skip".into(),
|
||||||
|
extract_dir: "src".into(),
|
||||||
|
patches: Vec::new(),
|
||||||
|
post_extract: Vec::new(),
|
||||||
|
}],
|
||||||
|
build: Build {
|
||||||
|
build_type: BuildType::Custom,
|
||||||
|
flags: BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: Dependencies::default(),
|
||||||
|
spec_dir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn split_git_url_accepts_git_with_rev() {
|
fn split_git_url_accepts_git_with_rev() {
|
||||||
@@ -365,5 +463,70 @@ mod tests {
|
|||||||
assert!(verify_file_hash(tmp.path(), &format!(":{}", sha256_hex)).unwrap());
|
assert!(verify_file_hash(tmp.path(), &format!(":{}", sha256_hex)).unwrap());
|
||||||
assert!(!verify_file_hash(tmp.path(), "md5:deadbeef").unwrap());
|
assert!(!verify_file_hash(tmp.path(), "md5:deadbeef").unwrap());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_blocking_client_with_and_without_timeout() {
|
||||||
|
use std::time::Duration;
|
||||||
|
let ua = "depot/test";
|
||||||
|
let c1 = build_blocking_client(ua, None).expect("client build failed");
|
||||||
|
assert!(c1.get("https://example.com").build().is_ok());
|
||||||
|
|
||||||
|
let c2 =
|
||||||
|
build_blocking_client(ua, Some(Duration::from_secs(5))).expect("client build failed");
|
||||||
|
assert!(c2.get("https://example.com").build().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_manual_sources_local_file_mode() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let spec_dir = tmp.path().join("spec");
|
||||||
|
let cache_dir = tmp.path().join("cache");
|
||||||
|
let build_dir = tmp.path().join("build");
|
||||||
|
std::fs::create_dir_all(&spec_dir).unwrap();
|
||||||
|
std::fs::write(spec_dir.join("manual.patch"), "patch-data").unwrap();
|
||||||
|
|
||||||
|
let spec = mk_spec_with_manuals(
|
||||||
|
spec_dir.clone(),
|
||||||
|
vec![ManualSource {
|
||||||
|
file: Some("manual.patch".into()),
|
||||||
|
url: None,
|
||||||
|
sha256: None,
|
||||||
|
dest: None,
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
|
||||||
|
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(build_dir.join("manual.patch")).unwrap(),
|
||||||
|
"patch-data"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_manual_sources_url_mode_file_scheme() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let spec_dir = tmp.path().join("spec");
|
||||||
|
let cache_dir = tmp.path().join("cache");
|
||||||
|
let build_dir = tmp.path().join("build");
|
||||||
|
std::fs::create_dir_all(&spec_dir).unwrap();
|
||||||
|
let remote_file = tmp.path().join("remote-resource.txt");
|
||||||
|
std::fs::write(&remote_file, "remote-data").unwrap();
|
||||||
|
let url = format!("file://{}", remote_file.display());
|
||||||
|
|
||||||
|
let spec = mk_spec_with_manuals(
|
||||||
|
spec_dir,
|
||||||
|
vec![ManualSource {
|
||||||
|
file: None,
|
||||||
|
url: Some(url),
|
||||||
|
sha256: Some("skip".into()),
|
||||||
|
dest: Some("assets/manual.txt".into()),
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
|
||||||
|
copy_manual_sources(&spec, &cache_dir, &build_dir).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(build_dir.join("assets/manual.txt")).unwrap(),
|
||||||
|
"remote-data"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+270
-17
@@ -2,12 +2,59 @@
|
|||||||
|
|
||||||
use crate::package::PackageSpec;
|
use crate::package::PackageSpec;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::Component;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
fn is_skipped_install_path(rel_path: &str) -> bool {
|
||||||
|
let p = rel_path.trim_start_matches('/');
|
||||||
|
p == ".metadata.toml"
|
||||||
|
|| p == ".files.yaml"
|
||||||
|
|| p == "usr/share/info/dir"
|
||||||
|
|| p.starts_with("usr/share/info/dir.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_relative_path(path: &str) -> Result<String> {
|
||||||
|
let trimmed = path.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
anyhow::bail!("keep paths must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = Path::new(trimmed);
|
||||||
|
if p.is_absolute() {
|
||||||
|
anyhow::bail!("keep paths must be relative: {}", trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut normalized = PathBuf::new();
|
||||||
|
for comp in p.components() {
|
||||||
|
match comp {
|
||||||
|
Component::Normal(seg) => normalized.push(seg),
|
||||||
|
Component::CurDir => {}
|
||||||
|
_ => {
|
||||||
|
anyhow::bail!(
|
||||||
|
"keep paths must not contain traversal or root components: {}",
|
||||||
|
trimmed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = normalized
|
||||||
|
.to_str()
|
||||||
|
.context("keep paths must be valid UTF-8")?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if s.is_empty() {
|
||||||
|
anyhow::bail!("keep paths must not resolve to empty paths: {}", trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
/// Process staged files - remove .la files, strip binaries, etc.
|
/// Process staged files - remove .la files, strip binaries, etc.
|
||||||
pub fn process(destdir: &Path, _spec: &PackageSpec) -> Result<()> {
|
pub fn process(destdir: &Path, _spec: &PackageSpec) -> Result<()> {
|
||||||
println!("Processing staged files...");
|
println!("Processing staged files...");
|
||||||
@@ -112,7 +159,7 @@ impl FsTransaction {
|
|||||||
for rel in &self.removed {
|
for rel in &self.removed {
|
||||||
let src = self.removed_backup_path(rel);
|
let src = self.removed_backup_path(rel);
|
||||||
let dst = self.rootfs.join(rel);
|
let dst = self.rootfs.join(rel);
|
||||||
if src.exists() {
|
if src.symlink_metadata().is_ok() {
|
||||||
if let Some(parent) = dst.parent() {
|
if let Some(parent) = dst.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
@@ -132,7 +179,7 @@ impl FsTransaction {
|
|||||||
for rel in &self.backed_up {
|
for rel in &self.backed_up {
|
||||||
let src = self.backup_path(rel);
|
let src = self.backup_path(rel);
|
||||||
let dst = self.rootfs.join(rel);
|
let dst = self.rootfs.join(rel);
|
||||||
if src.exists() {
|
if src.symlink_metadata().is_ok() {
|
||||||
if let Some(parent) = dst.parent() {
|
if let Some(parent) = dst.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
@@ -174,7 +221,13 @@ pub fn install_atomic(
|
|||||||
rootfs: &Path,
|
rootfs: &Path,
|
||||||
tx_base_dir: &Path,
|
tx_base_dir: &Path,
|
||||||
remove_paths: &[String],
|
remove_paths: &[String],
|
||||||
|
keep_paths: &[String],
|
||||||
) -> Result<FsTransaction> {
|
) -> Result<FsTransaction> {
|
||||||
|
let keep_set: HashSet<String> = keep_paths
|
||||||
|
.iter()
|
||||||
|
.map(|p| normalize_relative_path(p))
|
||||||
|
.collect::<Result<HashSet<_>>>()?;
|
||||||
|
|
||||||
fs::create_dir_all(tx_base_dir)
|
fs::create_dir_all(tx_base_dir)
|
||||||
.with_context(|| format!("Failed to create tx dir: {}", tx_base_dir.display()))?;
|
.with_context(|| format!("Failed to create tx dir: {}", tx_base_dir.display()))?;
|
||||||
|
|
||||||
@@ -236,7 +289,18 @@ pub fn install_atomic(
|
|||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let dest_path = rootfs.join(&rel_path);
|
if is_skipped_install_path(&rel_path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let keep_as_depotnew = keep_set.contains(&rel_path) && rootfs.join(&rel_path).exists();
|
||||||
|
let install_rel_path = if keep_as_depotnew {
|
||||||
|
format!("{}.depotnew", rel_path)
|
||||||
|
} else {
|
||||||
|
rel_path.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let dest_path = rootfs.join(&install_rel_path);
|
||||||
|
|
||||||
if let Some(parent) = dest_path.parent() {
|
if let Some(parent) = dest_path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
@@ -245,7 +309,7 @@ pub fn install_atomic(
|
|||||||
if dest_path.symlink_metadata().is_ok() {
|
if dest_path.symlink_metadata().is_ok() {
|
||||||
// lexists checks existence without following symlinks
|
// lexists checks existence without following symlinks
|
||||||
// Backup existing
|
// Backup existing
|
||||||
let backup_path = tx.backup_path(&rel_path);
|
let backup_path = tx.backup_path(&install_rel_path);
|
||||||
if let Some(parent) = backup_path.parent() {
|
if let Some(parent) = backup_path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
@@ -259,9 +323,9 @@ pub fn install_atomic(
|
|||||||
fs::copy(&dest_path, &backup_path)?;
|
fs::copy(&dest_path, &backup_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.backed_up.push(rel_path.clone());
|
tx.backed_up.push(install_rel_path.clone());
|
||||||
} else {
|
} else {
|
||||||
tx.created.push(rel_path.clone());
|
tx.created.push(install_rel_path.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install new file/symlink
|
// Install new file/symlink
|
||||||
@@ -277,10 +341,10 @@ pub fn install_atomic(
|
|||||||
if file_type.is_symlink() {
|
if file_type.is_symlink() {
|
||||||
let target = fs::read_link(src_path)?;
|
let target = fs::read_link(src_path)?;
|
||||||
std::os::unix::fs::symlink(target, &dest_path)
|
std::os::unix::fs::symlink(target, &dest_path)
|
||||||
.with_context(|| format!("Failed to create symlink: {}", rel_path))?;
|
.with_context(|| format!("Failed to create symlink: {}", install_rel_path))?;
|
||||||
} else {
|
} else {
|
||||||
fs::copy(src_path, &dest_path)
|
fs::copy(src_path, &dest_path)
|
||||||
.with_context(|| format!("Failed to install: {}", rel_path))?;
|
.with_context(|| format!("Failed to install: {}", install_rel_path))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,21 +352,38 @@ pub fn install_atomic(
|
|||||||
for rel in remove_paths {
|
for rel in remove_paths {
|
||||||
let rel = rel.as_str();
|
let rel = rel.as_str();
|
||||||
let dest_path = rootfs.join(rel);
|
let dest_path = rootfs.join(rel);
|
||||||
if !dest_path.exists() {
|
let dest_meta = match dest_path.symlink_metadata() {
|
||||||
continue;
|
Ok(m) => m,
|
||||||
}
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||||
if !dest_path.is_file() {
|
Err(e) => {
|
||||||
// Only track files for now.
|
return Err(e).with_context(|| {
|
||||||
|
format!("Failed to inspect obsolete path before removal: {}", rel)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if dest_meta.file_type().is_dir() {
|
||||||
|
// Only obsolete files/symlinks are removed here.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let backup_path = tx.removed_backup_path(rel);
|
let backup_path = tx.removed_backup_path(rel);
|
||||||
if let Some(parent) = backup_path.parent() {
|
if let Some(parent) = backup_path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dest_meta.file_type().is_symlink() {
|
||||||
|
let target = fs::read_link(&dest_path)
|
||||||
|
.with_context(|| format!("Failed to read obsolete symlink target: {}", rel))?;
|
||||||
|
std::os::unix::fs::symlink(&target, &backup_path)
|
||||||
|
.with_context(|| format!("Failed to backup removed symlink: {}", rel))?;
|
||||||
|
} else {
|
||||||
fs::copy(&dest_path, &backup_path)
|
fs::copy(&dest_path, &backup_path)
|
||||||
.with_context(|| format!("Failed to backup removed file: {}", rel))?;
|
.with_context(|| format!("Failed to backup removed file: {}", rel))?;
|
||||||
|
}
|
||||||
|
|
||||||
fs::remove_file(&dest_path)
|
fs::remove_file(&dest_path)
|
||||||
.with_context(|| format!("Failed to remove obsolete file: {}", rel))?;
|
.with_context(|| format!("Failed to remove obsolete file/symlink: {}", rel))?;
|
||||||
tx.removed.push(rel.to_string());
|
tx.removed.push(rel.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +443,7 @@ mod tests {
|
|||||||
std::fs::write(destdir.join("usr/bin/new_only"), "added").unwrap();
|
std::fs::write(destdir.join("usr/bin/new_only"), "added").unwrap();
|
||||||
|
|
||||||
let remove_paths = vec!["usr/bin/old_only".to_string()];
|
let remove_paths = vec!["usr/bin/old_only".to_string()];
|
||||||
let tx = install_atomic(&destdir, &rootfs, &tx_base, &remove_paths).unwrap();
|
let tx = install_atomic(&destdir, &rootfs, &tx_base, &remove_paths, &[]).unwrap();
|
||||||
|
|
||||||
// After install: updated + new present, obsolete removed
|
// After install: updated + new present, obsolete removed
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -382,6 +463,94 @@ mod tests {
|
|||||||
assert!(rootfs.join("usr/bin/old_only").exists());
|
assert!(rootfs.join("usr/bin/old_only").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_atomic_keep_existing_installs_depotnew() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
let tx_base = tmp.path().join("tx");
|
||||||
|
std::fs::create_dir_all(rootfs.join("etc")).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("etc")).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(rootfs.join("etc/locale.gen"), "existing").unwrap();
|
||||||
|
std::fs::write(destdir.join("etc/locale.gen"), "from-package").unwrap();
|
||||||
|
|
||||||
|
let keep = vec!["etc/locale.gen".to_string()];
|
||||||
|
let tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &keep).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(rootfs.join("etc/locale.gen")).unwrap(),
|
||||||
|
"existing"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(rootfs.join("etc/locale.gen.depotnew")).unwrap(),
|
||||||
|
"from-package"
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.rollback().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(rootfs.join("etc/locale.gen")).unwrap(),
|
||||||
|
"existing"
|
||||||
|
);
|
||||||
|
assert!(!rootfs.join("etc/locale.gen.depotnew").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_atomic_rejects_unsafe_keep_paths() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
let tx_base = tmp.path().join("tx");
|
||||||
|
std::fs::create_dir_all(&rootfs).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("etc")).unwrap();
|
||||||
|
std::fs::write(destdir.join("etc/locale.gen"), "x").unwrap();
|
||||||
|
|
||||||
|
let keep = vec!["../etc/shadow".to_string()];
|
||||||
|
let err = install_atomic(&destdir, &rootfs, &tx_base, &[], &keep)
|
||||||
|
.expect_err("expected keep path traversal to be rejected");
|
||||||
|
assert!(
|
||||||
|
err.to_string()
|
||||||
|
.contains("keep paths must not contain traversal")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_atomic_removes_obsolete_symlink_paths() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
let tx_base = tmp.path().join("tx");
|
||||||
|
std::fs::create_dir_all(rootfs.join("usr/lib")).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/bin/new"), "ok").unwrap();
|
||||||
|
|
||||||
|
std::os::unix::fs::symlink("../lib/libold.so", rootfs.join("usr/lib/libold.so.link"))
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
rootfs
|
||||||
|
.join("usr/lib/libold.so.link")
|
||||||
|
.symlink_metadata()
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
let remove_paths = vec!["usr/lib/libold.so.link".to_string()];
|
||||||
|
let tx = install_atomic(&destdir, &rootfs, &tx_base, &remove_paths, &[]).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rootfs
|
||||||
|
.join("usr/lib/libold.so.link")
|
||||||
|
.symlink_metadata()
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.rollback().unwrap();
|
||||||
|
let restored = rootfs
|
||||||
|
.join("usr/lib/libold.so.link")
|
||||||
|
.symlink_metadata()
|
||||||
|
.expect("symlink should be restored");
|
||||||
|
assert!(restored.file_type().is_symlink());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn install_atomic_commit_removes_tx_dir() {
|
fn install_atomic_commit_removes_tx_dir() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
@@ -392,7 +561,7 @@ mod tests {
|
|||||||
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||||
std::fs::write(destdir.join("usr/bin/foo"), "x").unwrap();
|
std::fs::write(destdir.join("usr/bin/foo"), "x").unwrap();
|
||||||
|
|
||||||
let tx = install_atomic(&destdir, &rootfs, &tx_base, &[]).unwrap();
|
let tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &[]).unwrap();
|
||||||
let tx_dir = tx.tx_dir.clone();
|
let tx_dir = tx.tx_dir.clone();
|
||||||
assert!(tx_dir.exists());
|
assert!(tx_dir.exists());
|
||||||
tx.commit().unwrap();
|
tx.commit().unwrap();
|
||||||
@@ -410,7 +579,7 @@ mod tests {
|
|||||||
// Create a symlink bin -> usr/bin in destdir
|
// Create a symlink bin -> usr/bin in destdir
|
||||||
std::os::unix::fs::symlink("usr/bin", destdir.join("bin")).unwrap();
|
std::os::unix::fs::symlink("usr/bin", destdir.join("bin")).unwrap();
|
||||||
|
|
||||||
let _tx = install_atomic(&destdir, &rootfs, &tx_base, &[]).unwrap();
|
let _tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &[]).unwrap();
|
||||||
|
|
||||||
// Verify rootfs/bin is a symlink, not a directory
|
// Verify rootfs/bin is a symlink, not a directory
|
||||||
let meta = rootfs
|
let meta = rootfs
|
||||||
@@ -442,6 +611,86 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_atomic_skips_info_dir_index() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
let tx_base = tmp.path().join("tx");
|
||||||
|
std::fs::create_dir_all(&rootfs).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/share/info")).unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/share/info/dir"), "index").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/share/info/dir.gz"), "index gz").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/share/info/ok.info"), "ok").unwrap();
|
||||||
|
|
||||||
|
let _tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &[]).unwrap();
|
||||||
|
|
||||||
|
assert!(!rootfs.join("usr/share/info/dir").exists());
|
||||||
|
assert!(!rootfs.join("usr/share/info/dir.gz").exists());
|
||||||
|
assert!(rootfs.join("usr/share/info/ok.info").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_atomic_skips_package_metadata_files() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
let tx_base = tmp.path().join("tx");
|
||||||
|
std::fs::create_dir_all(&rootfs).unwrap();
|
||||||
|
std::fs::create_dir_all(&destdir).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||||
|
std::fs::write(destdir.join(".metadata.toml"), "name='foo'").unwrap();
|
||||||
|
std::fs::write(destdir.join(".files.yaml"), "files: []").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/bin/ok"), "ok").unwrap();
|
||||||
|
|
||||||
|
let _tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &[]).unwrap();
|
||||||
|
|
||||||
|
assert!(!rootfs.join(".metadata.toml").exists());
|
||||||
|
assert!(!rootfs.join(".files.yaml").exists());
|
||||||
|
assert!(rootfs.join("usr/bin/ok").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_manifest_skips_info_dir_index() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/share/info")).unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/share/info/dir"), "index").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/share/info/dir.xz"), "index xz").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/share/info/ok.info"), "ok").unwrap();
|
||||||
|
|
||||||
|
let manifest = generate_manifest_with_dirs(&destdir).unwrap();
|
||||||
|
|
||||||
|
assert!(!manifest.files.contains(&"usr/share/info/dir".to_string()));
|
||||||
|
assert!(
|
||||||
|
!manifest
|
||||||
|
.files
|
||||||
|
.contains(&"usr/share/info/dir.xz".to_string())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
manifest
|
||||||
|
.files
|
||||||
|
.contains(&"usr/share/info/ok.info".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_manifest_skips_package_metadata_files() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
std::fs::create_dir_all(&destdir).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||||
|
std::fs::write(destdir.join(".metadata.toml"), "name='foo'").unwrap();
|
||||||
|
std::fs::write(destdir.join(".files.yaml"), "files: []").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/bin/ok"), "ok").unwrap();
|
||||||
|
|
||||||
|
let manifest = generate_manifest_with_dirs(&destdir).unwrap();
|
||||||
|
|
||||||
|
assert!(!manifest.files.contains(&".metadata.toml".to_string()));
|
||||||
|
assert!(!manifest.files.contains(&".files.yaml".to_string()));
|
||||||
|
assert!(manifest.files.contains(&"usr/bin/ok".to_string()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manifest containing files and directories for a package
|
/// Manifest containing files and directories for a package
|
||||||
@@ -468,6 +717,10 @@ pub fn generate_manifest_with_dirs(destdir: &Path) -> Result<Manifest> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if is_skipped_install_path(&rel_path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let file_type = entry.file_type();
|
let file_type = entry.file_type();
|
||||||
|
|
||||||
// Check for symlink first
|
// Check for symlink first
|
||||||
|
|||||||
Reference in New Issue
Block a user