feat: add repo planning/binary repo support and Meson build integration
- add repos.toml source/binary repo configuration with arch overrides and priorities - add dependency planner/execution plan flow with interactive provider selection and --yes support - expand binary repo DB metadata (sha512, dependencies, file lists) and repo owns/search helpers - add rootfs-scoped fd locking and minisign detached signing/verification helpers - add Meson build/test support for the project with builddir-local CARGO_HOME/CARGO_TARGET_DIR - install contrib example configs into sysconfdir (/etc/depot.d layout) via Meson - improve builder env handling (sanitized PATH, tool env passthrough, CXXFLAGS support) - support += appends in package specs and add build.flags.cxxflags parsing - prompt for git credentials when private repo auth is required - update docs/examples and dependency setup (system libs, zstdmt, tempfile pin)
This commit is contained in:
Generated
+206
-17
@@ -421,6 +421,12 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ct-codecs"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.10"
|
||||
@@ -435,14 +441,17 @@ dependencies = [
|
||||
"ar",
|
||||
"bzip2",
|
||||
"clap",
|
||||
"fd-lock",
|
||||
"flate2",
|
||||
"git2",
|
||||
"indicatif",
|
||||
"inquire",
|
||||
"libz-sys",
|
||||
"md5",
|
||||
"minisign",
|
||||
"nix",
|
||||
"openssl-sys",
|
||||
"petgraph",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"semver",
|
||||
@@ -584,6 +593,17 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fd-lock"
|
||||
version = "4.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.26"
|
||||
@@ -602,6 +622,12 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
@@ -631,6 +657,21 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -923,6 +964,22 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.19"
|
||||
@@ -1117,9 +1174,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "inquire"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "979f5ab9760427ada4fa5762b2d905e5b12704fb1fada07b6bfa66aeaa586f87"
|
||||
checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm",
|
||||
@@ -1271,7 +1328,6 @@ version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -1292,9 +1348,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.23"
|
||||
version = "1.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7"
|
||||
checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1380,6 +1436,18 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6bf96cef396a17a96f7600281aa4da9229860b7a082601b1f6db6eaa5f99ee5"
|
||||
dependencies = [
|
||||
"ct-codecs",
|
||||
"getrandom 0.3.4",
|
||||
"rpassword",
|
||||
"scrypt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -1402,6 +1470,23 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe 0.1.6",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework 2.11.1",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.1"
|
||||
@@ -1441,6 +1526,32 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
@@ -1453,15 +1564,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.111"
|
||||
@@ -1470,7 +1572,6 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -1520,6 +1621,18 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@@ -1758,10 +1871,12 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
@@ -1770,6 +1885,7 @@ dependencies = [
|
||||
"rustls-platform-verifier",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -1794,6 +1910,27 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "7.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rtoolbox",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtoolbox"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.38.0"
|
||||
@@ -1860,7 +1997,7 @@ dependencies = [
|
||||
"openssl-probe 0.2.0",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework 3.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1888,7 +2025,7 @@ dependencies = [
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework 3.5.1",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -1918,6 +2055,15 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "salsa20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -1942,6 +2088,30 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||
dependencies = [
|
||||
"pbkdf2",
|
||||
"salsa20",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.5.1"
|
||||
@@ -2338,6 +2508,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
@@ -2837,6 +3017,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
|
||||
+11
-16
@@ -11,19 +11,19 @@ anyhow = "1.0.102"
|
||||
ar = "0.9.0"
|
||||
bzip2 = "0.6.1"
|
||||
clap = { version = "4.5.60", features = ["derive"] }
|
||||
flate2 = { version = "1.1.9", default-features = false, features = ["miniz_oxide"] }
|
||||
flate2 = "1.1.9"
|
||||
git2 = { version = "0.20.4", features = ["vendored-libgit2"] }
|
||||
indicatif = "0.18.4"
|
||||
nix = { version = "0.31.1", features = ["user"] }
|
||||
reqwest = { version = "0.13.2", features = ["blocking", "rustls"] }
|
||||
rusqlite = { version = "0.38.0", features = ["bundled"] }
|
||||
openssl-sys = { version = "0.9", features = ["vendored"] }
|
||||
reqwest = { version = "0.13.2", features = ["blocking", "native-tls"] }
|
||||
rusqlite = "0.38.0"
|
||||
openssl-sys = "0.9.111"
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
yaml-rust2 = "0.11.0"
|
||||
sha2 = "0.10.9"
|
||||
tar = "0.4.44"
|
||||
tempfile = "3.25.0"
|
||||
tempfile = "=3.25.0"
|
||||
thiserror = "2.0.18"
|
||||
toml = "0.9.8"
|
||||
url = "2.5.8"
|
||||
@@ -31,18 +31,13 @@ walkdir = "2.5.0"
|
||||
xz2 = "0.1.7"
|
||||
zip = "8.1.0"
|
||||
zstd = { version = "0.13.3", features = ["zstdmt"] }
|
||||
libz-sys = { version = "1.1.23", default-features = false, features = ["stock-zlib", "static"] }
|
||||
inquire = "0.9.3"
|
||||
libz-sys = "1.1.24"
|
||||
inquire = "0.9.4"
|
||||
md5 = "0.8.0"
|
||||
suppaftp = "8.0.2"
|
||||
minisign = "0.8.0"
|
||||
petgraph = "0.8.3"
|
||||
fd-lock = "4.0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.25.0"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = "symbols"
|
||||
debug = false
|
||||
tempfile = "=3.25.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# depot
|
||||
|
||||
Depot is a source-based package manager for Linux, designed for Linux. It focuses on reproducibility, atomic installations, and ease of cross-compilation.
|
||||
Depot is a source-based package manager designed for Linux. It focuses on reproducibility, atomic installations, and ease of cross-compilation.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -36,10 +36,18 @@ depot install zlib-1.2.11-1-x86_64.depot.pkg.tar.zst
|
||||
## Command Reference
|
||||
|
||||
- `install <SPEC_OR_ARCHIVE>`: Build and install a package.
|
||||
- Resolves a full dependency plan first (binary repos and/or source specs), then executes in dependency order.
|
||||
- Use `--yes` for non-interactive confirmation/provider selection.
|
||||
- Use `--dry-run` to print the plan without performing work.
|
||||
- `remove <PACKAGE>`: Remove an installed package.
|
||||
- `build <SPEC>`: Build a package and create an archive without installing.
|
||||
- Resolves and offers to install missing build/test dependencies before fetching/building.
|
||||
- `info <PACKAGE_OR_SPEC>`: Show information about a package.
|
||||
- `search <QUERY>`: Search enabled source/binary repos by package name and provided features.
|
||||
- Use `--files` to search binary repo metadata file lists.
|
||||
- `owns <PATH>`: Show which installed package owns a path.
|
||||
- `list`: List all installed packages.
|
||||
- `repo owns <PATH>`: Query binary repo metadata for path ownership.
|
||||
- `repo create [DIR]`: Create a repository database from a directory of packages.
|
||||
- `config`: Show current configuration and overrides.
|
||||
|
||||
|
||||
+3
-1
@@ -1,7 +1,7 @@
|
||||
Contrib: example system and user configuration files for Depot
|
||||
|
||||
Place these files on your system to provide sensible defaults and examples
|
||||
for `/etc/depot.toml`, `/etc/depot.d/build.toml`, or user-level configs.
|
||||
for `/etc/depot.toml`, `/etc/depot.d/*.toml`, or user-level configs.
|
||||
|
||||
Recommended installation:
|
||||
|
||||
@@ -18,3 +18,5 @@ Notes
|
||||
- These files are examples only. Review and adapt to your distribution and
|
||||
security policies before deploying.
|
||||
- The config loader supports append syntax (e.g. `cflags += ["-g"]`).
|
||||
- `contrib/depot.d/repos.toml` shows the new source/binary repo configuration
|
||||
format used by `depot repo ...` commands.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
[settings]
|
||||
prefer_binary = true
|
||||
|
||||
[source.vertex]
|
||||
url = "https://gitlab.com/vertex-linux/packages.git"
|
||||
enabled = true
|
||||
priority = 50
|
||||
subdirs = ["core", "extra"]
|
||||
|
||||
[binary.core]
|
||||
url = "https://repo.sfgos.net/core"
|
||||
enabled = true
|
||||
priority = 10
|
||||
repo_db = "repo.db.zst"
|
||||
|
||||
[binary.core.arch.x86_64]
|
||||
enabled = true
|
||||
|
||||
[binary.extra]
|
||||
url = "https://repo.sfgos.net/extra"
|
||||
enabled = true
|
||||
priority = 20
|
||||
repo_db = "repo.db.zst"
|
||||
|
||||
[binary.extra.arch.x86_64]
|
||||
enabled = true
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
project(
|
||||
'depot',
|
||||
version: '0.1.0',
|
||||
meson_version: '>=0.60.0',
|
||||
)
|
||||
|
||||
fs = import('fs')
|
||||
|
||||
cargo = find_program('cargo', required: true)
|
||||
find_prog = find_program('find', required: true)
|
||||
sh = find_program('sh', required: true)
|
||||
|
||||
src_root = meson.project_source_root()
|
||||
build_root = meson.project_build_root()
|
||||
src_root_prefix = src_root + '/'
|
||||
|
||||
cargo_profile = 'debug'
|
||||
cargo_release = false
|
||||
if get_option('buildtype') != 'debug'
|
||||
cargo_profile = 'release'
|
||||
cargo_release = true
|
||||
endif
|
||||
|
||||
cargo_inputs = files('Cargo.toml')
|
||||
if fs.is_file('Cargo.lock')
|
||||
cargo_inputs += files('Cargo.lock')
|
||||
endif
|
||||
|
||||
cargo_src_list = run_command(
|
||||
find_prog,
|
||||
join_paths(src_root, 'src'),
|
||||
'-type',
|
||||
'f',
|
||||
'(',
|
||||
'-name',
|
||||
'*.rs',
|
||||
'-o',
|
||||
'-name',
|
||||
'*.toml',
|
||||
')',
|
||||
check: true,
|
||||
capture: true,
|
||||
).stdout().strip()
|
||||
foreach rel : cargo_src_list.split('\n')
|
||||
if rel == ''
|
||||
continue
|
||||
endif
|
||||
if rel.startswith(src_root_prefix)
|
||||
rel = rel.replace(src_root_prefix, '')
|
||||
endif
|
||||
cargo_inputs += files(rel)
|
||||
endforeach
|
||||
|
||||
depot_bin = custom_target(
|
||||
'depot-bin',
|
||||
input: cargo_inputs,
|
||||
output: 'depot',
|
||||
command: [
|
||||
sh,
|
||||
join_paths(src_root, 'tools', 'meson', 'cargo-build.sh'),
|
||||
cargo,
|
||||
src_root,
|
||||
build_root,
|
||||
cargo_profile,
|
||||
cargo_release ? '1' : '0',
|
||||
'@OUTPUT@',
|
||||
],
|
||||
console: true,
|
||||
build_by_default: true,
|
||||
install: true,
|
||||
install_dir: get_option('bindir'),
|
||||
)
|
||||
|
||||
test(
|
||||
'cargo-test',
|
||||
sh,
|
||||
args: [
|
||||
join_paths(src_root, 'tools', 'meson', 'cargo-test.sh'),
|
||||
cargo.full_path(),
|
||||
src_root,
|
||||
build_root,
|
||||
],
|
||||
suite: ['cargo'],
|
||||
)
|
||||
|
||||
# System configuration examples installed under the actual runtime lookup paths.
|
||||
install_data(
|
||||
'contrib/depot.toml',
|
||||
install_dir: get_option('sysconfdir'),
|
||||
)
|
||||
|
||||
example_sysconf_dir = join_paths(get_option('sysconfdir'), 'depot.d')
|
||||
example_depotd_list = run_command(
|
||||
find_prog,
|
||||
join_paths(src_root, 'contrib', 'depot.d'),
|
||||
'-maxdepth',
|
||||
'1',
|
||||
'-type',
|
||||
'f',
|
||||
'-name',
|
||||
'*.toml',
|
||||
check: true,
|
||||
capture: true,
|
||||
).stdout().strip()
|
||||
example_depotd_files = []
|
||||
foreach rel : example_depotd_list.split('\n')
|
||||
if rel == ''
|
||||
continue
|
||||
endif
|
||||
if rel.startswith(src_root_prefix)
|
||||
rel = rel.replace(src_root_prefix, '')
|
||||
endif
|
||||
example_depotd_files += files(rel)
|
||||
endforeach
|
||||
if example_depotd_files.length() > 0
|
||||
install_data(example_depotd_files, install_dir: example_sysconf_dir)
|
||||
endif
|
||||
|
||||
# Per-user example is documentation, not a system config file.
|
||||
install_data(
|
||||
'contrib/user.depot.toml.example',
|
||||
install_dir: join_paths(get_option('datadir'), 'doc', meson.project_name(), 'examples'),
|
||||
)
|
||||
install_data(
|
||||
'contrib/README.md',
|
||||
install_dir: join_paths(get_option('datadir'), 'doc', meson.project_name()),
|
||||
)
|
||||
@@ -60,7 +60,7 @@ pub fn build(
|
||||
let mut configure_cmd = Command::new(&configure_path);
|
||||
configure_cmd.current_dir(&build_dir);
|
||||
|
||||
crate::builder::prepare_command(&mut configure_cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars);
|
||||
|
||||
configure_cmd.arg(format!("--prefix={}", flags.prefix));
|
||||
|
||||
@@ -143,7 +143,7 @@ pub fn build(
|
||||
make_cmd.arg(target);
|
||||
}
|
||||
|
||||
crate::builder::prepare_command(&mut make_cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut make_cmd, &env_vars);
|
||||
|
||||
let status = make_cmd.status().with_context(|| {
|
||||
format!("Failed to run {} in {}", make_exec, make_dir.display())
|
||||
@@ -194,7 +194,7 @@ pub fn build(
|
||||
for test_target in &test_targets {
|
||||
test_cmd.arg(test_target);
|
||||
}
|
||||
crate::builder::prepare_command(&mut test_cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut test_cmd, &env_vars);
|
||||
|
||||
let test_targets_display = test_targets.join(" ");
|
||||
let status = test_cmd.status().with_context(|| {
|
||||
@@ -305,7 +305,7 @@ pub fn build(
|
||||
"DESTDIR".to_string(),
|
||||
destdir.to_string_lossy().into_owned(),
|
||||
));
|
||||
crate::builder::prepare_command(&mut install_cmd, &install_env);
|
||||
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
|
||||
|
||||
let status = install_cmd.status().with_context(|| {
|
||||
format!(
|
||||
|
||||
@@ -82,7 +82,7 @@ pub fn build(
|
||||
cmake_cmd.arg(&expanded);
|
||||
}
|
||||
|
||||
crate::builder::prepare_command(&mut cmake_cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut cmake_cmd, &env_vars);
|
||||
|
||||
let status = cmake_cmd.status().context("Failed to run cmake")?;
|
||||
if !status.success() {
|
||||
@@ -109,7 +109,7 @@ pub fn build(
|
||||
}
|
||||
}
|
||||
|
||||
crate::builder::prepare_command(&mut build_cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut build_cmd, &env_vars);
|
||||
|
||||
let status = build_cmd
|
||||
.status()
|
||||
@@ -131,7 +131,7 @@ pub fn build(
|
||||
for target in &test_targets {
|
||||
test_cmd.arg(target);
|
||||
}
|
||||
crate::builder::prepare_command(&mut test_cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut test_cmd, &env_vars);
|
||||
|
||||
let status = test_cmd.status().with_context(|| {
|
||||
format!(
|
||||
@@ -182,7 +182,7 @@ pub fn build(
|
||||
"DESTDIR".to_string(),
|
||||
destdir.to_string_lossy().into_owned(),
|
||||
));
|
||||
crate::builder::prepare_command(&mut install_cmd, &install_env);
|
||||
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
|
||||
|
||||
let status = install_cmd
|
||||
.status()
|
||||
|
||||
+66
-7
@@ -118,7 +118,7 @@ pub fn build(
|
||||
};
|
||||
cmd.current_dir(&build_dir);
|
||||
|
||||
crate::builder::prepare_command(&mut cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut cmd, &env_vars);
|
||||
|
||||
// Run the command and include the OS error on spawn failures for clearer diagnostics
|
||||
let status = cmd.status().map_err(|e| {
|
||||
@@ -176,9 +176,16 @@ fn build_function_mode_command(
|
||||
) -> Result<Command> {
|
||||
let mut wrapper = String::new();
|
||||
wrapper.push_str("set -eu\n");
|
||||
wrapper.push_str("depot_has_function() {\n");
|
||||
wrapper.push_str(" case \"$(type \"$1\" 2>/dev/null || :)\" in\n");
|
||||
wrapper.push_str(" *function*) return 0 ;;\n");
|
||||
wrapper.push_str(" *) return 1 ;;\n");
|
||||
wrapper.push_str(" esac\n");
|
||||
wrapper.push_str("}\n");
|
||||
wrapper.push_str("depot_build_ran=0\n");
|
||||
wrapper.push_str(". \"$1\"\n");
|
||||
wrapper.push_str("if command -v depot_build >/dev/null 2>&1; then depot_build;\n");
|
||||
wrapper.push_str("elif command -v build >/dev/null 2>&1; then build;\n");
|
||||
wrapper.push_str("if depot_has_function depot_build; then depot_build; depot_build_ran=1;\n");
|
||||
wrapper.push_str("elif depot_has_function build; then build; depot_build_ran=1;\n");
|
||||
wrapper.push_str("fi\n");
|
||||
|
||||
let primary = &spec.package.name;
|
||||
@@ -195,18 +202,28 @@ fn build_function_mode_command(
|
||||
wrapper.push_str(&format!(
|
||||
"DEPOT_OUTPUT_NAME='{q_name}'; DEPOT_OUTPUT_DESTDIR='{q_dest}'; DESTDIR=\"$DEPOT_OUTPUT_DESTDIR\"; export DEPOT_OUTPUT_NAME DEPOT_OUTPUT_DESTDIR DESTDIR\n"
|
||||
));
|
||||
wrapper.push_str("depot_output_installed=0\n");
|
||||
wrapper.push_str(&format!(
|
||||
"if command -v depot_install_{fn_suffix} >/dev/null 2>&1; then depot_install_{fn_suffix};\n"
|
||||
"if depot_has_function depot_install_{fn_suffix}; then depot_install_{fn_suffix}; depot_output_installed=1;\n"
|
||||
));
|
||||
wrapper.push_str(&format!(
|
||||
"elif command -v install_{fn_suffix} >/dev/null 2>&1; then install_{fn_suffix};\n"
|
||||
"elif depot_has_function install_{fn_suffix}; then install_{fn_suffix}; depot_output_installed=1;\n"
|
||||
));
|
||||
if out.name == *primary {
|
||||
wrapper
|
||||
.push_str("elif command -v depot_install >/dev/null 2>&1; then depot_install;\n");
|
||||
wrapper.push_str("elif command -v install >/dev/null 2>&1; then install;\n");
|
||||
.push_str("elif depot_has_function depot_install; then depot_install; depot_output_installed=1;\n");
|
||||
wrapper.push_str(
|
||||
"elif depot_has_function install; then install; depot_output_installed=1;\n",
|
||||
);
|
||||
wrapper.push_str("elif [ \"$depot_build_ran\" = 1 ]; then depot_output_installed=1;\n");
|
||||
}
|
||||
wrapper.push_str("fi\n");
|
||||
wrapper.push_str("if [ \"$depot_output_installed\" != 1 ]; then\n");
|
||||
wrapper.push_str(
|
||||
" echo \"depot: missing install handler for output '$DEPOT_OUTPUT_NAME' in custom function mode\" >&2\n",
|
||||
);
|
||||
wrapper.push_str(" exit 1\n");
|
||||
wrapper.push_str("fi\n");
|
||||
}
|
||||
|
||||
let mut cmd = fakeroot::wrap_install_command("sh", destdir);
|
||||
@@ -363,4 +380,46 @@ depot_install_dev_pkg() {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_function_mode_errors_when_output_handler_missing() -> Result<()> {
|
||||
let tmp_src = tempdir()?;
|
||||
let tmp_dest = tempdir()?;
|
||||
|
||||
let build_sh = tmp_src.path().join("build.sh");
|
||||
std::fs::write(
|
||||
&build_sh,
|
||||
r#"#!/bin/sh
|
||||
depot_build() {
|
||||
:
|
||||
}
|
||||
depot_install() {
|
||||
mkdir -p "$DESTDIR/usr/share"
|
||||
echo primary > "$DESTDIR/usr/share/primary.txt"
|
||||
}
|
||||
"#,
|
||||
)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&build_sh)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&build_sh, perms)?;
|
||||
}
|
||||
|
||||
let mut spec = mk_spec("demo", "1.0");
|
||||
spec.packages.push(crate::package::PackageInfo {
|
||||
name: "dev-pkg".into(),
|
||||
version: "1.0".into(),
|
||||
revision: 1,
|
||||
description: "d".into(),
|
||||
homepage: "h".into(),
|
||||
license: vec!["MIT".into()],
|
||||
});
|
||||
|
||||
let err = build(&spec, tmp_src.path(), tmp_dest.path(), None, true)
|
||||
.expect_err("missing per-output install handler should fail");
|
||||
assert!(err.to_string().contains("Custom build script failed"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn build(
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.arg("-c").arg(&cmd_str);
|
||||
cmd.current_dir(src_dir);
|
||||
crate::builder::prepare_command(&mut cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut cmd, &env_vars);
|
||||
|
||||
let status = cmd
|
||||
.status()
|
||||
@@ -84,7 +84,7 @@ pub fn build(
|
||||
"DESTDIR".to_string(),
|
||||
destdir.to_string_lossy().into_owned(),
|
||||
));
|
||||
crate::builder::prepare_command(&mut cmd, &install_env);
|
||||
crate::builder::prepare_tool_command(&mut cmd, &install_env);
|
||||
|
||||
let status = cmd
|
||||
.status()
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn build(
|
||||
meson_cmd.arg(arg);
|
||||
}
|
||||
|
||||
crate::builder::prepare_command(&mut meson_cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut meson_cmd, &env_vars);
|
||||
|
||||
let status = meson_cmd.status().context("Failed to run meson setup")?;
|
||||
if !status.success() {
|
||||
@@ -71,7 +71,7 @@ pub fn build(
|
||||
ninja_cmd.current_dir(&build_dir);
|
||||
ninja_cmd.arg("-j").arg(num_cpus().to_string());
|
||||
|
||||
crate::builder::prepare_command(&mut ninja_cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut ninja_cmd, &env_vars);
|
||||
|
||||
let status = ninja_cmd
|
||||
.status()
|
||||
@@ -106,7 +106,7 @@ pub fn build(
|
||||
"DESTDIR".to_string(),
|
||||
destdir.to_string_lossy().into_owned(),
|
||||
));
|
||||
crate::builder::prepare_command(&mut install_cmd, &install_env);
|
||||
crate::builder::prepare_tool_command(&mut install_cmd, &install_env);
|
||||
|
||||
let status = install_cmd
|
||||
.status()
|
||||
|
||||
+103
-3
@@ -13,8 +13,9 @@ pub mod state;
|
||||
use crate::cross::CrossConfig;
|
||||
use crate::package::{BuildType, PackageSpec};
|
||||
use anyhow::Result;
|
||||
use std::ffi::OsString;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub type EnvVars = Vec<(String, String)>;
|
||||
|
||||
@@ -41,6 +42,9 @@ pub fn standard_build_env(
|
||||
if !flags.cflags.is_empty() {
|
||||
set_env_var(&mut env_vars, "CFLAGS", flags.cflags.join(" "));
|
||||
}
|
||||
if !flags.cxxflags.is_empty() {
|
||||
set_env_var(&mut env_vars, "CXXFLAGS", flags.cxxflags.join(" "));
|
||||
}
|
||||
|
||||
let ldflags = if !flags.ldflags.is_empty() || !flags.libc.is_empty() {
|
||||
if flags.libc.is_empty() {
|
||||
@@ -123,8 +127,29 @@ pub fn standard_build_env(
|
||||
/// Prepare a Command with a hermetic environment and some essential variables preserved.
|
||||
pub fn prepare_command(cmd: &mut Command, env_vars: &EnvVars) {
|
||||
cmd.env_clear();
|
||||
|
||||
if let Some(path) = sanitized_build_path() {
|
||||
cmd.env("PATH", path);
|
||||
}
|
||||
|
||||
// Preserve essential environment variables
|
||||
for var in &["PATH", "LANG", "HOME", "DESTDIR", "DEPOT_ROOTFS"] {
|
||||
for var in &[
|
||||
"LANG",
|
||||
"HOME",
|
||||
"DESTDIR",
|
||||
"DEPOT_ROOTFS",
|
||||
"CARGO_HOME",
|
||||
"RUSTUP_HOME",
|
||||
"RUSTUP_TOOLCHAIN",
|
||||
"RUSTC",
|
||||
"RUSTDOC",
|
||||
"TERM",
|
||||
"COLORTERM",
|
||||
"NO_COLOR",
|
||||
"CLICOLOR",
|
||||
"CLICOLOR_FORCE",
|
||||
"FORCE_COLOR",
|
||||
] {
|
||||
if let Ok(val) = std::env::var(var) {
|
||||
cmd.env(var, val);
|
||||
}
|
||||
@@ -139,6 +164,35 @@ pub fn prepare_command(cmd: &mut Command, env_vars: &EnvVars) {
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitized_build_path() -> Option<OsString> {
|
||||
use std::path::PathBuf;
|
||||
|
||||
let mut parts: Vec<PathBuf> = std::env::var_os("PATH")
|
||||
.map(|raw| std::env::split_paths(&raw).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
for dir in ["/bin", "/usr/bin", "/sbin", "/usr/sbin"] {
|
||||
let path = PathBuf::from(dir);
|
||||
if path.exists() && !parts.iter().any(|p| p == &path) {
|
||||
parts.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
std::env::join_paths(parts).ok()
|
||||
}
|
||||
|
||||
/// Prepare a Command for interactive tool execution with live terminal output.
|
||||
pub fn prepare_tool_command(cmd: &mut Command, env_vars: &EnvVars) {
|
||||
prepare_command(cmd, env_vars);
|
||||
cmd.stdin(Stdio::inherit());
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
}
|
||||
|
||||
/// Build a package using the appropriate build system
|
||||
pub fn build(
|
||||
spec: &PackageSpec,
|
||||
@@ -230,6 +284,8 @@ mod tests {
|
||||
std::env::set_var("HOME", "/home/test");
|
||||
std::env::set_var("SHELL", "/bin/zsh");
|
||||
std::env::set_var("DEPOT_ROOTFS", "/my/rootfs");
|
||||
std::env::set_var("TERM", "xterm-256color");
|
||||
std::env::set_var("CLICOLOR_FORCE", "1");
|
||||
}
|
||||
|
||||
prepare_command(&mut cmd, &vec![("MYVAR".to_string(), "myval".to_string())]);
|
||||
@@ -251,6 +307,16 @@ mod tests {
|
||||
envs.get(OsStr::new("DEPOT_ROOTFS")),
|
||||
Some(&Some(std::ffi::OsString::from("/my/rootfs").as_os_str()))
|
||||
);
|
||||
assert_eq!(
|
||||
envs.get(OsStr::new("TERM")),
|
||||
Some(&Some(
|
||||
std::ffi::OsString::from("xterm-256color").as_os_str()
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
envs.get(OsStr::new("CLICOLOR_FORCE")),
|
||||
Some(&Some(std::ffi::OsString::from("1").as_os_str()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -267,15 +333,45 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepare_command_preserves_rust_toolchain_homes() {
|
||||
let mut cmd = std::process::Command::new("ls");
|
||||
unsafe {
|
||||
std::env::set_var("CARGO_HOME", "/var/cache/cargo-home");
|
||||
std::env::set_var("RUSTUP_HOME", "/var/cache/rustup-home");
|
||||
}
|
||||
prepare_command(&mut cmd, &Vec::new());
|
||||
let envs: HashMap<_, _> = cmd.get_envs().collect();
|
||||
assert_eq!(
|
||||
envs.get(OsStr::new("CARGO_HOME")),
|
||||
Some(&Some(
|
||||
std::ffi::OsString::from("/var/cache/cargo-home").as_os_str()
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
envs.get(OsStr::new("RUSTUP_HOME")),
|
||||
Some(&Some(
|
||||
std::ffi::OsString::from("/var/cache/rustup-home").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 mut spec = mk_spec(vec!["-O2"], vec!["-Wl,--as-needed"]);
|
||||
spec.build.flags.cxxflags = vec!["-O2".into(), "-fno-exceptions".into()];
|
||||
|
||||
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 == "CXXFLAGS" && v == "-O2 -fno-exceptions"),
|
||||
"expected CXXFLAGS to be exported when enabled"
|
||||
);
|
||||
assert!(
|
||||
enabled
|
||||
.iter()
|
||||
@@ -304,6 +400,10 @@ mod tests {
|
||||
!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 == "CXXFLAGS"),
|
||||
"expected CXXFLAGS 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"
|
||||
|
||||
@@ -245,7 +245,7 @@ fn build_wheel_setup_py(
|
||||
.arg("bdist_wheel")
|
||||
.arg("--dist-dir")
|
||||
.arg(dist_dir);
|
||||
crate::builder::prepare_command(&mut cmd, &env_vars.to_vec());
|
||||
crate::builder::prepare_tool_command(&mut cmd, &env_vars.to_vec());
|
||||
|
||||
let status = cmd
|
||||
.status()
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ pub fn build(
|
||||
}
|
||||
|
||||
// Set environment
|
||||
crate::builder::prepare_command(&mut cargo_cmd, &env_vars);
|
||||
crate::builder::prepare_tool_command(&mut cargo_cmd, &env_vars);
|
||||
|
||||
let status = cargo_cmd
|
||||
.status()
|
||||
|
||||
+331
-14
@@ -1,10 +1,203 @@
|
||||
//! Global configuration for Depot
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_repo_db_filename() -> String {
|
||||
"repo.db.zst".to_string()
|
||||
}
|
||||
|
||||
fn resolve_rootfs_base(rootfs: &Path) -> PathBuf {
|
||||
if rootfs.exists() {
|
||||
rootfs.canonicalize().unwrap_or_else(|_| {
|
||||
std::env::current_dir()
|
||||
.map(|cwd| cwd.join(rootfs))
|
||||
.unwrap_or_else(|_| rootfs.to_path_buf())
|
||||
})
|
||||
} else {
|
||||
std::env::current_dir()
|
||||
.map(|cwd| cwd.join(rootfs))
|
||||
.unwrap_or_else(|_| rootfs.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
/// Global repo behavior settings loaded from `repos.toml`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RepoSettings {
|
||||
/// Prefer binary repos over source repos when both can satisfy a request.
|
||||
#[serde(default)]
|
||||
pub prefer_binary: bool,
|
||||
}
|
||||
|
||||
impl Default for RepoSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
prefer_binary: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Source repository configuration entry loaded from `repos.toml`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SourceRepo {
|
||||
/// Git URL for the package repo.
|
||||
pub url: String,
|
||||
/// Whether the repo is enabled for lookups/sync.
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// Lower numbers are higher priority.
|
||||
#[serde(default)]
|
||||
pub priority: i32,
|
||||
/// Optional subdirectories to scan/index inside the git checkout.
|
||||
#[serde(default)]
|
||||
pub subdirs: Vec<String>,
|
||||
}
|
||||
|
||||
/// Binary repository configuration entry loaded from `repos.toml`.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BinaryRepoArchEntry {
|
||||
/// Whether this architecture is enabled for this repo.
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// Optional URL override for this architecture.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
/// Optional repo DB path override for this architecture.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub repo_db: Option<String>,
|
||||
}
|
||||
|
||||
/// Binary repository configuration entry loaded from `repos.toml`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BinaryRepo {
|
||||
/// Base URL for the binary repository.
|
||||
pub url: String,
|
||||
/// Whether the repo is enabled for lookups.
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// Lower numbers are higher priority.
|
||||
#[serde(default)]
|
||||
pub priority: i32,
|
||||
/// Repo database filename/path relative to `url`.
|
||||
#[serde(default = "default_repo_db_filename")]
|
||||
pub repo_db: String,
|
||||
/// Architecture-specific overrides/enablement.
|
||||
///
|
||||
/// Example:
|
||||
/// `[binary.core.arch.x86_64]`
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub arch: BTreeMap<String, BinaryRepoArchEntry>,
|
||||
/// Allow unsigned repo metadata for this repo.
|
||||
#[serde(default)]
|
||||
pub allow_unsigned: bool,
|
||||
}
|
||||
|
||||
impl Default for BinaryRepo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: String::new(),
|
||||
enabled: true,
|
||||
priority: 0,
|
||||
repo_db: default_repo_db_filename(),
|
||||
arch: BTreeMap::new(),
|
||||
allow_unsigned: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BinaryRepo {
|
||||
/// Return true if this repo is enabled for the requested machine architecture.
|
||||
pub fn supports_arch(&self, machine_arch: &str) -> bool {
|
||||
if self.arch.is_empty() {
|
||||
return true;
|
||||
}
|
||||
self.arch
|
||||
.get(machine_arch)
|
||||
.map(|entry| entry.enabled)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Return the effective base URL for `machine_arch`, including arch overrides.
|
||||
pub fn effective_url_for_arch<'a>(&'a self, machine_arch: &str) -> Option<&'a str> {
|
||||
if self.arch.is_empty() {
|
||||
return Some(self.url.as_str());
|
||||
}
|
||||
let entry = self.arch.get(machine_arch)?;
|
||||
if !entry.enabled {
|
||||
return None;
|
||||
}
|
||||
Some(entry.url.as_deref().unwrap_or(self.url.as_str()))
|
||||
}
|
||||
|
||||
/// Return the effective repo DB path for `machine_arch`, including arch overrides.
|
||||
pub fn effective_repo_db_for_arch<'a>(&'a self, machine_arch: &str) -> Option<&'a str> {
|
||||
if self.arch.is_empty() {
|
||||
return Some(self.repo_db.as_str());
|
||||
}
|
||||
let entry = self.arch.get(machine_arch)?;
|
||||
if !entry.enabled {
|
||||
return None;
|
||||
}
|
||||
Some(entry.repo_db.as_deref().unwrap_or(self.repo_db.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed contents of `/etc/depot.d/repos.toml`.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct RepoConfigFile {
|
||||
/// Global repo behavior settings.
|
||||
#[serde(default, skip_serializing_if = "repo_settings_is_default")]
|
||||
pub settings: RepoSettings,
|
||||
/// Source repos keyed by repo name.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub source: BTreeMap<String, SourceRepo>,
|
||||
/// Binary repos keyed by repo name.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub binary: BTreeMap<String, BinaryRepo>,
|
||||
}
|
||||
|
||||
fn repo_settings_is_default(settings: &RepoSettings) -> bool {
|
||||
!settings.prefer_binary
|
||||
}
|
||||
|
||||
/// Return the canonical `repos.toml` path for the given rootfs.
|
||||
pub fn repos_toml_path(rootfs: &Path) -> PathBuf {
|
||||
resolve_rootfs_base(rootfs).join("etc/depot.d/repos.toml")
|
||||
}
|
||||
|
||||
/// Load `repos.toml` for the given rootfs. Missing file returns defaults.
|
||||
pub fn load_repos_config_file(rootfs: &Path) -> Result<RepoConfigFile> {
|
||||
let path = repos_toml_path(rootfs);
|
||||
if !path.exists() {
|
||||
return Ok(RepoConfigFile::default());
|
||||
}
|
||||
|
||||
let content =
|
||||
fs::read_to_string(&path).with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
let parsed: RepoConfigFile =
|
||||
toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
/// Save `repos.toml` for the given rootfs, creating `/etc/depot.d` as needed.
|
||||
pub fn save_repos_config_file(rootfs: &Path, repos: &RepoConfigFile) -> Result<PathBuf> {
|
||||
let path = repos_toml_path(rootfs);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||
}
|
||||
let content = toml::to_string_pretty(repos).context("Failed to serialize repos.toml")?;
|
||||
fs::write(&path, content).with_context(|| format!("Failed to write {}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Global configuration settings
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
@@ -20,43 +213,43 @@ pub struct Config {
|
||||
pub package_overrides: toml::Value,
|
||||
/// Appends found in system TOML files (key -> values to append)
|
||||
pub appends: HashMap<String, Vec<toml::Value>>,
|
||||
/// Parsed repo settings from `/etc/depot.d/repos.toml`.
|
||||
pub repo_settings: RepoSettings,
|
||||
/// Source repos from `/etc/depot.d/repos.toml` (and legacy mirrors fallback).
|
||||
pub source_repos: BTreeMap<String, SourceRepo>,
|
||||
/// Binary repos from `/etc/depot.d/repos.toml`.
|
||||
pub binary_repos: BTreeMap<String, BinaryRepo>,
|
||||
/// Mirrors mapping read from /etc/depot.d/mirrors.toml (reponame -> git url)
|
||||
pub mirrors: std::collections::HashMap<String, String>,
|
||||
/// Directory where git mirrors are cloned (absolute path). Defaults to
|
||||
/// <rootfs>/usr/src/depot unless overridden in depot.toml
|
||||
pub repo_clone_dir: PathBuf,
|
||||
/// Cache directory for binary packages and repo metadata.
|
||||
pub package_cache_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create config with paths relative to the given rootfs
|
||||
pub fn for_rootfs(rootfs: &Path) -> Self {
|
||||
let abs_rootfs = if rootfs.exists() {
|
||||
rootfs.canonicalize().unwrap_or_else(|_| {
|
||||
std::env::current_dir()
|
||||
.map(|cwd| cwd.join(rootfs))
|
||||
.unwrap_or_else(|_| rootfs.to_path_buf())
|
||||
})
|
||||
} else {
|
||||
std::env::current_dir()
|
||||
.map(|cwd| cwd.join(rootfs))
|
||||
.unwrap_or_else(|_| rootfs.to_path_buf())
|
||||
};
|
||||
let abs_rootfs = resolve_rootfs_base(rootfs);
|
||||
|
||||
let is_system_root = abs_rootfs == Path::new("/") || abs_rootfs.as_os_str() == "/";
|
||||
let is_root = crate::fakeroot::is_root();
|
||||
|
||||
let (cache_dir, build_dir, db_dir) = if is_system_root && !is_root {
|
||||
let (cache_dir, package_cache_dir, build_dir, db_dir) = if is_system_root && !is_root {
|
||||
let home = std::env::var("HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from("/tmp"));
|
||||
(
|
||||
home.join(".cache/depot/sources"),
|
||||
home.join(".cache/depot/packages"),
|
||||
home.join(".cache/depot/build"),
|
||||
home.join(".local/share/depot"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
abs_rootfs.join("var/cache/depot/sources"),
|
||||
abs_rootfs.join("var/cache/depot/packages"),
|
||||
abs_rootfs.join("var/cache/depot/build"),
|
||||
abs_rootfs.join("var/lib/depot"),
|
||||
)
|
||||
@@ -69,8 +262,12 @@ impl Config {
|
||||
build_overrides: toml::Value::Table(toml::map::Map::new()),
|
||||
package_overrides: toml::Value::Table(toml::map::Map::new()),
|
||||
appends: HashMap::new(),
|
||||
repo_settings: RepoSettings::default(),
|
||||
source_repos: BTreeMap::new(),
|
||||
binary_repos: BTreeMap::new(),
|
||||
mirrors: std::collections::HashMap::new(),
|
||||
repo_clone_dir: abs_rootfs.join("usr/src/depot"),
|
||||
package_cache_dir,
|
||||
};
|
||||
|
||||
if let Err(e) = config.load_system(&abs_rootfs) {
|
||||
@@ -185,7 +382,27 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
// Load mirrors file: /etc/depot.d/mirrors.toml
|
||||
// Load new repos file: /etc/depot.d/repos.toml (host fallback then rootfs override).
|
||||
let host_repos_path = PathBuf::from("/etc/depot.d/repos.toml");
|
||||
let repos_path = rootfs.join("etc/depot.d/repos.toml");
|
||||
for path in [host_repos_path, repos_path] {
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
let content = fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read repos config: {}", path.display()))?;
|
||||
let parsed: RepoConfigFile = toml::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse repos config: {}", path.display()))?;
|
||||
self.repo_settings = parsed.settings;
|
||||
for (name, repo) in parsed.source {
|
||||
self.source_repos.insert(name, repo);
|
||||
}
|
||||
for (name, repo) in parsed.binary {
|
||||
self.binary_repos.insert(name, repo);
|
||||
}
|
||||
}
|
||||
|
||||
// Load legacy mirrors file: /etc/depot.d/mirrors.toml
|
||||
let mirrors_path = rootfs.join("etc/depot.d/mirrors.toml");
|
||||
if mirrors_path.exists() {
|
||||
let content = fs::read_to_string(&mirrors_path).with_context(|| {
|
||||
@@ -196,6 +413,12 @@ impl Config {
|
||||
for (k, v) in table {
|
||||
if let Some(s) = v.as_str() {
|
||||
self.mirrors.insert(k.clone(), s.to_string());
|
||||
self.source_repos.entry(k.clone()).or_insert(SourceRepo {
|
||||
url: s.to_string(),
|
||||
enabled: true,
|
||||
priority: 0,
|
||||
subdirs: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,6 +436,17 @@ impl Config {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return enabled source repos as `name -> git URL` pairs for legacy sync/status code.
|
||||
pub fn enabled_source_mirror_map(&self) -> std::collections::HashMap<String, String> {
|
||||
let mut out = std::collections::HashMap::new();
|
||||
for (name, repo) in &self.source_repos {
|
||||
if repo.enabled {
|
||||
out.insert(name.clone(), repo.url.clone());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_toml_values(base: &mut toml::Value, over: &toml::Value) {
|
||||
@@ -460,4 +694,87 @@ cflags = ["-O3"]
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_repos_toml() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let etc = root.join("etc/depot.d");
|
||||
fs::create_dir_all(&etc).unwrap();
|
||||
|
||||
fs::write(
|
||||
etc.join("repos.toml"),
|
||||
r#"
|
||||
[settings]
|
||||
prefer_binary = true
|
||||
|
||||
[source.vertex]
|
||||
url = "https://gitlab.com/vertex-linux/packages.git"
|
||||
enabled = true
|
||||
priority = 10
|
||||
subdirs = ["core", "extra"]
|
||||
|
||||
[binary.vertex]
|
||||
url = "https://repo.example.invalid"
|
||||
enabled = false
|
||||
priority = 5
|
||||
repo_db = "repo.db.zst"
|
||||
|
||||
[binary.vertex.arch.x86_64]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = Config::for_rootfs(root);
|
||||
assert!(config.repo_settings.prefer_binary);
|
||||
let src = config.source_repos.get("vertex").unwrap();
|
||||
assert_eq!(src.url, "https://gitlab.com/vertex-linux/packages.git");
|
||||
assert_eq!(src.subdirs, vec!["core".to_string(), "extra".to_string()]);
|
||||
let bin = config.binary_repos.get("vertex").unwrap();
|
||||
assert_eq!(bin.url, "https://repo.example.invalid");
|
||||
assert!(!bin.enabled);
|
||||
assert!(bin.arch.contains_key("x86_64"));
|
||||
assert!(bin.supports_arch("x86_64"));
|
||||
assert!(!bin.supports_arch("aarch64"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_and_load_repos_config_file_roundtrip() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let mut repos = RepoConfigFile::default();
|
||||
repos.source.insert(
|
||||
"vertex".to_string(),
|
||||
SourceRepo {
|
||||
url: "https://gitlab.com/vertex-linux/packages.git".to_string(),
|
||||
enabled: true,
|
||||
priority: 1,
|
||||
subdirs: vec!["core".to_string()],
|
||||
},
|
||||
);
|
||||
repos.binary.insert(
|
||||
"vertex".to_string(),
|
||||
BinaryRepo {
|
||||
url: "https://repo.example.invalid".to_string(),
|
||||
enabled: true,
|
||||
priority: 2,
|
||||
repo_db: "repo.db.zst".to_string(),
|
||||
arch: {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("x86_64".to_string(), BinaryRepoArchEntry::default());
|
||||
map
|
||||
},
|
||||
allow_unsigned: false,
|
||||
},
|
||||
);
|
||||
|
||||
let path = save_repos_config_file(root, &repos).unwrap();
|
||||
assert!(path.exists());
|
||||
|
||||
let loaded = load_repos_config_file(root).unwrap();
|
||||
assert!(loaded.source.contains_key("vertex"));
|
||||
assert!(loaded.binary.contains_key("vertex"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,3 +751,33 @@ pub fn get_package_version(db_path: &Path, name: &str) -> Result<Option<String>>
|
||||
.ok();
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
/// Find the installed package that owns a filesystem path from the local DB.
|
||||
pub fn owns_path(db_path: &Path, path: &Path) -> Result<Option<String>> {
|
||||
if !db_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let normalized = path
|
||||
.to_string_lossy()
|
||||
.trim_start_matches('/')
|
||||
.trim_start_matches("./")
|
||||
.to_string();
|
||||
if normalized.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
let owner = conn
|
||||
.query_row(
|
||||
"SELECT p.name
|
||||
FROM files f
|
||||
JOIN packages p ON p.id = f.package_id
|
||||
WHERE f.path = ?1
|
||||
LIMIT 1",
|
||||
params![normalized],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.ok();
|
||||
Ok(owner)
|
||||
}
|
||||
|
||||
+875
-15
File diff suppressed because it is too large
Load Diff
+16
@@ -78,6 +78,11 @@ fn parse_dep(dep: &str) -> ParsedDep<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the dependency name portion without any version/operator suffix.
|
||||
pub fn dep_name(dep: &str) -> &str {
|
||||
parse_dep(dep).name
|
||||
}
|
||||
|
||||
/// Compare two version strings using semver if possible, fallback to string compare
|
||||
fn compare_versions(installed: &str, required: &str, op: VersionOp) -> bool {
|
||||
// Try semver comparison first
|
||||
@@ -134,6 +139,17 @@ fn is_dep_satisfied(
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a dependency expression is satisfied by the installed package DB.
|
||||
pub fn is_dep_satisfied_in_db(dep: &str, db_path: &Path) -> Result<bool> {
|
||||
if !db_path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let installed = db::get_installed_packages(db_path)?;
|
||||
let provides = db::get_all_provides(db_path)?;
|
||||
is_dep_satisfied(dep, &installed, &provides, db_path)
|
||||
}
|
||||
|
||||
/// Check if all build dependencies are satisfied
|
||||
pub fn check_build_deps(spec: &PackageSpec, db_path: &Path) -> Result<Vec<String>> {
|
||||
let mut missing = Vec::new();
|
||||
|
||||
+44
-1
@@ -16,11 +16,18 @@ pub struct PackageIndex {
|
||||
by_provides: HashMap<String, Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
/// Source package search result from `PackageIndex`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SourceSearchHit {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub provides: Vec<String>,
|
||||
}
|
||||
|
||||
impl PackageIndex {
|
||||
/// Build index by scanning packages/*/*.toml and configured repo dir.
|
||||
///
|
||||
/// Use `build_with_repo_dir` to provide an explicit repo dir.
|
||||
|
||||
/// Build index scanning the local `packages/` directory and an optional
|
||||
/// system repo dir (e.g., /usr/src/depot). If `repo_dir` is None, the
|
||||
/// default `/usr/src/depot` is used.
|
||||
@@ -116,4 +123,40 @@ impl PackageIndex {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Return all source specs that provide the requested feature/package name.
|
||||
pub fn find_providers(&self, name: &str) -> Vec<PathBuf> {
|
||||
self.by_provides.get(name).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Search indexed specs by package name or provided feature.
|
||||
pub fn search(&self, query: &str) -> Vec<SourceSearchHit> {
|
||||
let q = query.to_ascii_lowercase();
|
||||
let mut provides_by_path: HashMap<PathBuf, Vec<String>> = HashMap::new();
|
||||
for (provided, paths) in &self.by_provides {
|
||||
for path in paths {
|
||||
provides_by_path
|
||||
.entry(path.clone())
|
||||
.or_default()
|
||||
.push(provided.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut hits = Vec::new();
|
||||
for (name, path) in &self.by_name {
|
||||
let provides = provides_by_path.remove(path).unwrap_or_default();
|
||||
let name_match = name.to_ascii_lowercase().contains(&q);
|
||||
let provides_match = provides.iter().any(|p| p.to_ascii_lowercase().contains(&q));
|
||||
if name_match || provides_match {
|
||||
hits.push(SourceSearchHit {
|
||||
name: name.clone(),
|
||||
path: path.clone(),
|
||||
provides,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hits.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.path.cmp(&b.path)));
|
||||
hits
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Rootfs-scoped advisory locking helpers.
|
||||
|
||||
use crate::config::Config;
|
||||
use anyhow::{Context, Result};
|
||||
use fd_lock::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Return the rootfs-scoped lock file path.
|
||||
pub(crate) fn lock_path(config: &Config) -> PathBuf {
|
||||
config.db_dir.join("lock")
|
||||
}
|
||||
|
||||
/// Open the rootfs-scoped lock file as an fd-lock reader/writer.
|
||||
pub(crate) fn open_lock(config: &Config) -> Result<RwLock<File>> {
|
||||
let path = lock_path(config);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create lock dir {}", parent.display()))?;
|
||||
}
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.truncate(false)
|
||||
.open(&path)
|
||||
.with_context(|| format!("Failed to open lock file {}", path.display()))?;
|
||||
Ok(RwLock::new(file))
|
||||
}
|
||||
|
||||
/// Acquire a shared/read lock without blocking.
|
||||
pub(crate) fn try_read<'a>(
|
||||
lock: &'a RwLock<File>,
|
||||
path: &Path,
|
||||
command_name: &str,
|
||||
) -> Result<RwLockReadGuard<'a, File>> {
|
||||
lock.try_read().map_err(|e| {
|
||||
if e.kind() == ErrorKind::WouldBlock {
|
||||
anyhow::anyhow!(
|
||||
"Depot is busy (lock held by another process). Command '{}' needs a shared lock on {}",
|
||||
command_name,
|
||||
path.display()
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!(e).context(format!(
|
||||
"Failed to acquire shared lock for '{}' on {}",
|
||||
command_name,
|
||||
path.display()
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Acquire an exclusive/write lock without blocking.
|
||||
pub(crate) fn try_write<'a>(
|
||||
lock: &'a mut RwLock<File>,
|
||||
path: &Path,
|
||||
command_name: &str,
|
||||
) -> Result<RwLockWriteGuard<'a, File>> {
|
||||
lock.try_write().map_err(|e| {
|
||||
if e.kind() == ErrorKind::WouldBlock {
|
||||
anyhow::anyhow!(
|
||||
"Depot is busy (lock held by another process). Command '{}' needs an exclusive lock on {}",
|
||||
command_name,
|
||||
path.display()
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!(e).context(format!(
|
||||
"Failed to acquire exclusive lock for '{}' on {}",
|
||||
command_name,
|
||||
path.display()
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
+1102
-10
File diff suppressed because it is too large
Load Diff
@@ -651,6 +651,19 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.cxxflags.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"cxxflags".into(),
|
||||
Value::Array(
|
||||
spec.build
|
||||
.flags
|
||||
.cxxflags
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.build.flags.ldflags.is_empty() {
|
||||
flags_tbl.insert(
|
||||
"ldflags".into(),
|
||||
@@ -1213,6 +1226,7 @@ mod tests {
|
||||
flags.makefile_install_commands = vec!["make DESTDIR=$DESTDIR install".into()];
|
||||
flags.cargs = vec!["--locked".into()];
|
||||
flags.rustflags = vec!["-Ctarget-cpu=native".into()];
|
||||
flags.cxxflags = vec!["-O2".into(), "-fno-rtti".into()];
|
||||
flags.target = "x86_64-unknown-linux-gnu".into();
|
||||
flags.keep = vec!["etc/locale.gen".into()];
|
||||
flags.no_flags = true;
|
||||
@@ -1266,6 +1280,7 @@ mod tests {
|
||||
assert!(toml.contains("makefile_install_commands = ["));
|
||||
assert!(toml.contains("cargs = ["));
|
||||
assert!(toml.contains("rustflags = ["));
|
||||
assert!(toml.contains("cxxflags = ["));
|
||||
assert!(toml.contains("target = \"x86_64-unknown-linux-gnu\""));
|
||||
assert!(toml.contains("keep = ["));
|
||||
assert!(toml.contains("\"etc/locale.gen\""));
|
||||
|
||||
+185
-1
@@ -58,12 +58,17 @@ impl PackageSpec {
|
||||
|
||||
let content = fs::read_to_string(&abs_path)
|
||||
.with_context(|| format!("Failed to read package spec: {}", abs_path.display()))?;
|
||||
let mut spec: PackageSpec = toml::from_str(&content)
|
||||
let (base_content, appends) =
|
||||
preprocess_spec_toml_appends(&content).with_context(|| {
|
||||
format!("Failed to preprocess package spec: {}", abs_path.display())
|
||||
})?;
|
||||
let mut spec: PackageSpec = toml::from_str(&base_content)
|
||||
.with_context(|| format!("Failed to parse package spec: {}", abs_path.display()))?;
|
||||
spec.spec_dir = abs_path
|
||||
.parent()
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
spec.apply_spec_appends(&appends)?;
|
||||
|
||||
// Require at least one source (remote or manual)
|
||||
if spec.source.is_empty() && spec.manual_sources.is_empty() {
|
||||
@@ -74,6 +79,28 @@ impl PackageSpec {
|
||||
Ok(spec)
|
||||
}
|
||||
|
||||
fn apply_spec_appends(
|
||||
&mut self,
|
||||
appends: &std::collections::HashMap<String, Vec<toml::Value>>,
|
||||
) -> Result<()> {
|
||||
for (key, values) in appends {
|
||||
if let Some(subkey) = key.strip_prefix("build.flags.") {
|
||||
self.apply_append(subkey, values);
|
||||
continue;
|
||||
}
|
||||
if let Some(subkey) = key.strip_prefix("flags.") {
|
||||
self.apply_append(subkey, values);
|
||||
continue;
|
||||
}
|
||||
if !key.contains('.') {
|
||||
self.apply_append(key, values);
|
||||
continue;
|
||||
}
|
||||
anyhow::bail!("Unsupported '+=' key in package spec: {}", key);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_manual_sources(&self) -> Result<()> {
|
||||
for (idx, manual) in self.manual_sources.iter().enumerate() {
|
||||
let has_file = manual
|
||||
@@ -206,6 +233,17 @@ impl PackageSpec {
|
||||
self.build.flags.cflags = vec![s.to_string()];
|
||||
}
|
||||
}
|
||||
"cxxflags" => {
|
||||
if let Some(arr) = v.as_array() {
|
||||
self.build.flags.cxxflags = arr
|
||||
.iter()
|
||||
.filter_map(|x| x.as_str())
|
||||
.map(String::from)
|
||||
.collect();
|
||||
} else if let Some(s) = v.as_str() {
|
||||
self.build.flags.cxxflags = vec![s.to_string()];
|
||||
}
|
||||
}
|
||||
"ldflags" => {
|
||||
if let Some(arr) = v.as_array() {
|
||||
self.build.flags.ldflags = arr
|
||||
@@ -485,6 +523,18 @@ impl PackageSpec {
|
||||
}
|
||||
}
|
||||
}
|
||||
"cxxflags" => {
|
||||
for v in values {
|
||||
if let Some(arr) = v.as_array() {
|
||||
self.build
|
||||
.flags
|
||||
.cxxflags
|
||||
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
|
||||
} else if let Some(s) = v.as_str() {
|
||||
self.build.flags.cxxflags.push(s.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
"ldflags" => {
|
||||
for v in values {
|
||||
if let Some(arr) = v.as_array() {
|
||||
@@ -815,6 +865,74 @@ impl PackageSpec {
|
||||
}
|
||||
}
|
||||
|
||||
fn preprocess_spec_toml_appends(
|
||||
input: &str,
|
||||
) -> Result<(String, std::collections::HashMap<String, Vec<toml::Value>>)> {
|
||||
let mut base_text = String::new();
|
||||
let mut appends = std::collections::HashMap::new();
|
||||
let mut current_table: Option<String> = None;
|
||||
let mut in_array_table = false;
|
||||
|
||||
for line in input.lines() {
|
||||
let trimmed = line.trim();
|
||||
|
||||
if trimmed.starts_with("[[") && trimmed.ends_with("]]") && trimmed.len() >= 4 {
|
||||
current_table = Some(trimmed[2..trimmed.len() - 2].trim().to_string());
|
||||
in_array_table = true;
|
||||
base_text.push_str(line);
|
||||
base_text.push('\n');
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 {
|
||||
current_table = Some(trimmed[1..trimmed.len() - 1].trim().to_string());
|
||||
in_array_table = false;
|
||||
base_text.push_str(line);
|
||||
base_text.push('\n');
|
||||
continue;
|
||||
}
|
||||
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
base_text.push_str(line);
|
||||
base_text.push('\n');
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(plus_idx) = trimmed.find("+=") {
|
||||
if in_array_table {
|
||||
anyhow::bail!(
|
||||
"'+=' is not supported inside array-of-table sections ({})",
|
||||
current_table.as_deref().unwrap_or("")
|
||||
);
|
||||
}
|
||||
let key = trimmed[..plus_idx].trim();
|
||||
let val_str = trimmed[plus_idx + 2..].trim();
|
||||
let val: toml::Value = toml::from_str::<toml::Value>(&format!("v = {}", val_str))
|
||||
.context("Failed to parse append value")?
|
||||
.get("v")
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
let full_key = if key.contains('.') {
|
||||
key.to_string()
|
||||
} else if let Some(table) = current_table.as_deref() {
|
||||
format!("{}.{}", table, key)
|
||||
} else {
|
||||
key.to_string()
|
||||
};
|
||||
|
||||
appends.entry(full_key).or_insert_with(Vec::new).push(val);
|
||||
// Preserve line numbering for parser diagnostics.
|
||||
base_text.push('\n');
|
||||
continue;
|
||||
}
|
||||
|
||||
base_text.push_str(line);
|
||||
base_text.push('\n');
|
||||
}
|
||||
|
||||
Ok((base_text, appends))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod spec_tests {
|
||||
use super::*;
|
||||
@@ -1223,6 +1341,7 @@ type = "custom"
|
||||
[flags]
|
||||
cc = "my-cc"
|
||||
cflags = ["-O2"]
|
||||
cxxflags = ["-O2", "-pipe"]
|
||||
passthrough_env = ["RUSTFLAGS"]
|
||||
make_vars = ["V=1"]
|
||||
make_dirs = ["lib"]
|
||||
@@ -1242,6 +1361,10 @@ post_configure = ["echo configured"]
|
||||
"build.flags.cflags".to_string(),
|
||||
vec![toml::Value::String("-g".to_string())],
|
||||
);
|
||||
config.appends.insert(
|
||||
"build.flags.cxxflags".to_string(),
|
||||
vec![toml::Value::String("-stdlib=libc++".to_string())],
|
||||
);
|
||||
config.appends.insert(
|
||||
"build.flags.rustflags".to_string(),
|
||||
vec![toml::Value::Array(vec![
|
||||
@@ -1301,6 +1424,14 @@ post_configure = ["echo configured"]
|
||||
assert_eq!(spec.build.flags.cc, "my-cc");
|
||||
assert!(spec.build.flags.cflags.contains(&"-O2".to_string()));
|
||||
assert!(spec.build.flags.cflags.contains(&"-g".to_string()));
|
||||
assert!(spec.build.flags.cxxflags.contains(&"-O2".to_string()));
|
||||
assert!(spec.build.flags.cxxflags.contains(&"-pipe".to_string()));
|
||||
assert!(
|
||||
spec.build
|
||||
.flags
|
||||
.cxxflags
|
||||
.contains(&"-stdlib=libc++".to_string())
|
||||
);
|
||||
assert!(spec.build.flags.rustflags.contains(&"-C".to_string()));
|
||||
assert!(
|
||||
spec.build
|
||||
@@ -1656,6 +1787,51 @@ keep = ["etc/locale.gen", "etc/resolv.conf"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_build_flags_appends_from_spec_file() {
|
||||
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]
|
||||
cxxflags = ["-O2"]
|
||||
cxxflags += [ "-Wno-gnu-statement-expression-from-macro-expansion" ]
|
||||
ldflags += "-Wl,--as-needed"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let spec = PackageSpec::from_file(&path).unwrap();
|
||||
assert_eq!(
|
||||
spec.build.flags.cxxflags,
|
||||
vec![
|
||||
"-O2".to_string(),
|
||||
"-Wno-gnu-statement-expression-from-macro-expansion".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
spec.build.flags.ldflags,
|
||||
vec!["-Wl,--as-needed".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_passthrough_env_from_spec() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -1829,8 +2005,12 @@ cbuild = "x86_64-pc-linux-gnu"
|
||||
.unwrap(),
|
||||
package_overrides: toml::Value::Table(toml::map::Map::new()),
|
||||
appends: std::collections::HashMap::new(),
|
||||
repo_settings: crate::config::RepoSettings::default(),
|
||||
source_repos: std::collections::BTreeMap::new(),
|
||||
binary_repos: std::collections::BTreeMap::new(),
|
||||
mirrors: std::collections::HashMap::new(),
|
||||
repo_clone_dir: PathBuf::from("/tmp"),
|
||||
package_cache_dir: PathBuf::from("/tmp"),
|
||||
};
|
||||
|
||||
spec.apply_config(&config);
|
||||
@@ -2124,6 +2304,9 @@ pub enum BuildType {
|
||||
pub struct BuildFlags {
|
||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||
pub cflags: Vec<String>,
|
||||
/// Extra flags exported to `CXXFLAGS`.
|
||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||
pub cxxflags: Vec<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||
pub ldflags: Vec<String>,
|
||||
/// Keep existing files and install package-provided replacement as `<path>.depotnew`.
|
||||
@@ -2328,6 +2511,7 @@ impl Default for BuildFlags {
|
||||
fn default() -> Self {
|
||||
BuildFlags {
|
||||
cflags: Vec::new(),
|
||||
cxxflags: Vec::new(),
|
||||
ldflags: Vec::new(),
|
||||
keep: Vec::new(),
|
||||
no_flags: false,
|
||||
|
||||
+710
@@ -0,0 +1,710 @@
|
||||
//! Dependency resolver and execution planner.
|
||||
//!
|
||||
//! Builds a recursive dependency closure for install/build operations, resolves
|
||||
//! dependencies by exact package name or provided feature, and returns a
|
||||
//! topologically sorted execution plan.
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::db;
|
||||
use crate::deps;
|
||||
use crate::index::PackageIndex;
|
||||
use crate::package::{BuildType, PackageSpec};
|
||||
use crate::ui;
|
||||
use anyhow::{Context, Result};
|
||||
use petgraph::algo::toposort;
|
||||
use petgraph::graph::{DiGraph, NodeIndex};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum PlanOrigin {
|
||||
Installed,
|
||||
Source {
|
||||
path: PathBuf,
|
||||
local_sibling: bool,
|
||||
},
|
||||
Binary {
|
||||
repo_name: String,
|
||||
record: Box<db::repo::BinaryRepoPackageRecord>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub(crate) enum PlanAction {
|
||||
SkipInstalled,
|
||||
BuildAndInstall,
|
||||
InstallBinary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PlannedStep {
|
||||
pub package: String,
|
||||
pub action: PlanAction,
|
||||
pub origin: PlanOrigin,
|
||||
pub requested_by: Vec<String>,
|
||||
}
|
||||
|
||||
impl PlannedStep {
|
||||
pub(crate) fn download_size(&self) -> Option<u64> {
|
||||
match &self.origin {
|
||||
PlanOrigin::Binary { record, .. } => Some(record.size),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct PlanSummary {
|
||||
pub total_packages: usize,
|
||||
pub skipped_installed: usize,
|
||||
pub binary_installs: usize,
|
||||
pub source_build_installs: usize,
|
||||
pub known_download_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ExecutionPlan {
|
||||
pub steps: Vec<PlannedStep>,
|
||||
}
|
||||
|
||||
impl ExecutionPlan {
|
||||
pub(crate) fn summary(&self) -> PlanSummary {
|
||||
let mut out = PlanSummary {
|
||||
total_packages: self.steps.len(),
|
||||
..PlanSummary::default()
|
||||
};
|
||||
for step in &self.steps {
|
||||
match step.action {
|
||||
PlanAction::SkipInstalled => out.skipped_installed += 1,
|
||||
PlanAction::InstallBinary => out.binary_installs += 1,
|
||||
PlanAction::BuildAndInstall => out.source_build_installs += 1,
|
||||
}
|
||||
if let Some(n) = step.download_size() {
|
||||
out.known_download_bytes = out.known_download_bytes.saturating_add(n);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn actionable_steps(&self) -> impl Iterator<Item = &PlannedStep> {
|
||||
self.steps
|
||||
.iter()
|
||||
.filter(|s| !matches!(s.action, PlanAction::SkipInstalled))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum InstallTarget {
|
||||
PackageName(String),
|
||||
SpecPath(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PlannerOptions {
|
||||
pub assume_yes: bool,
|
||||
pub prefer_binary: bool,
|
||||
pub local_sibling_root: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum CandidateKind {
|
||||
Source {
|
||||
path: PathBuf,
|
||||
local_sibling: bool,
|
||||
},
|
||||
Binary {
|
||||
repo_name: String,
|
||||
record: Box<db::repo::BinaryRepoPackageRecord>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum MatchKind {
|
||||
Exact,
|
||||
Provides,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Candidate {
|
||||
package: String,
|
||||
kind: CandidateKind,
|
||||
match_kind: MatchKind,
|
||||
sort_repo_priority: i32,
|
||||
sort_label: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct NodeData {
|
||||
step: PlannedStep,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalSpecHit {
|
||||
spec_name: String,
|
||||
path: PathBuf,
|
||||
provides: Vec<String>,
|
||||
}
|
||||
|
||||
struct Resolver<'a> {
|
||||
config: &'a Config,
|
||||
rootfs: &'a Path,
|
||||
db_path: PathBuf,
|
||||
opts: PlannerOptions,
|
||||
pkg_index: PackageIndex,
|
||||
graph: DiGraph<NodeData, ()>,
|
||||
by_package: HashMap<String, NodeIndex>,
|
||||
spec_cache: HashMap<PathBuf, PackageSpec>,
|
||||
local_sibling_cache: BTreeMap<PathBuf, Vec<LocalSpecHit>>,
|
||||
stack: Vec<String>,
|
||||
emitted_installed_roots: HashSet<String>,
|
||||
}
|
||||
|
||||
impl<'a> Resolver<'a> {
|
||||
fn new(config: &'a Config, rootfs: &'a Path, opts: PlannerOptions) -> Self {
|
||||
let db_path = config.db_dir.join("packages.db");
|
||||
let pkg_index = PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
|
||||
Self {
|
||||
config,
|
||||
rootfs,
|
||||
db_path,
|
||||
opts,
|
||||
pkg_index,
|
||||
graph: DiGraph::new(),
|
||||
by_package: HashMap::new(),
|
||||
spec_cache: HashMap::new(),
|
||||
local_sibling_cache: BTreeMap::new(),
|
||||
stack: Vec::new(),
|
||||
emitted_installed_roots: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_for_install_target(mut self, target: InstallTarget) -> Result<ExecutionPlan> {
|
||||
match target {
|
||||
InstallTarget::PackageName(name) => {
|
||||
if deps::is_dep_satisfied_in_db(&name, &self.db_path)? {
|
||||
if self.emitted_installed_roots.insert(name.clone()) {
|
||||
self.add_installed_root_step(name, "requested package".to_string());
|
||||
}
|
||||
} else {
|
||||
let root =
|
||||
self.resolve_dep_node(&name, None, "requested package".to_string())?;
|
||||
if let Some(root_idx) = root {
|
||||
self.mark_requested_by(root_idx, "requested package".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
InstallTarget::SpecPath(path) => {
|
||||
let root_idx =
|
||||
self.ensure_source_spec_node(&path, true, true, "requested spec".to_string())?;
|
||||
self.mark_requested_by(root_idx, "requested spec".to_string());
|
||||
}
|
||||
}
|
||||
self.finish_plan()
|
||||
}
|
||||
|
||||
fn plan_for_deps(mut self, deps_to_install: &[String]) -> Result<ExecutionPlan> {
|
||||
for dep in deps_to_install {
|
||||
if deps::is_dep_satisfied_in_db(dep, &self.db_path)? {
|
||||
continue;
|
||||
}
|
||||
if let Some(idx) = self.resolve_dep_node(dep, None, format!("dependency {}", dep))? {
|
||||
self.mark_requested_by(idx, format!("dependency {}", dep));
|
||||
}
|
||||
}
|
||||
self.finish_plan()
|
||||
}
|
||||
|
||||
fn finish_plan(self) -> Result<ExecutionPlan> {
|
||||
let order = toposort(&self.graph, None)
|
||||
.map_err(|_| anyhow::anyhow!("Dependency cycle detected in plan graph"))?;
|
||||
let mut steps = Vec::with_capacity(order.len());
|
||||
for idx in order {
|
||||
let node = self
|
||||
.graph
|
||||
.node_weight(idx)
|
||||
.with_context(|| format!("Missing plan node {idx:?}"))?;
|
||||
steps.push(node.step.clone());
|
||||
}
|
||||
Ok(ExecutionPlan { steps })
|
||||
}
|
||||
|
||||
fn add_installed_root_step(&mut self, package: String, requested_by: String) {
|
||||
let step = PlannedStep {
|
||||
package,
|
||||
action: PlanAction::SkipInstalled,
|
||||
origin: PlanOrigin::Installed,
|
||||
requested_by: vec![requested_by],
|
||||
};
|
||||
self.graph.add_node(NodeData { step });
|
||||
}
|
||||
|
||||
fn mark_requested_by(&mut self, idx: NodeIndex, reason: String) {
|
||||
if let Some(node) = self.graph.node_weight_mut(idx)
|
||||
&& !node.step.requested_by.contains(&reason)
|
||||
{
|
||||
node.step.requested_by.push(reason);
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_source_spec_node(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
allow_local_sibling_fallback: bool,
|
||||
local_sibling: bool,
|
||||
requested_by: String,
|
||||
) -> Result<NodeIndex> {
|
||||
let (package_name, deps_needed) = {
|
||||
let spec = self.load_spec(path)?;
|
||||
(spec.package.name.clone(), source_deps_for_install(spec))
|
||||
};
|
||||
if let Some(&idx) = self.by_package.get(&package_name) {
|
||||
self.mark_requested_by(idx, requested_by);
|
||||
return Ok(idx);
|
||||
}
|
||||
|
||||
self.push_stack(&package_name)?;
|
||||
let idx = self.graph.add_node(NodeData {
|
||||
step: PlannedStep {
|
||||
package: package_name.clone(),
|
||||
action: PlanAction::BuildAndInstall,
|
||||
origin: PlanOrigin::Source {
|
||||
path: path.to_path_buf(),
|
||||
local_sibling,
|
||||
},
|
||||
requested_by: vec![requested_by],
|
||||
},
|
||||
});
|
||||
self.by_package.insert(package_name.clone(), idx);
|
||||
|
||||
for dep in deps_needed {
|
||||
if let Some(dep_idx) = self.resolve_dep_node(
|
||||
&dep,
|
||||
allow_local_sibling_fallback.then_some(path),
|
||||
format!("{} needs {}", package_name, dep),
|
||||
)? {
|
||||
self.graph.add_edge(dep_idx, idx, ());
|
||||
}
|
||||
}
|
||||
|
||||
self.pop_stack(&package_name);
|
||||
Ok(idx)
|
||||
}
|
||||
|
||||
fn resolve_dep_node(
|
||||
&mut self,
|
||||
dep: &str,
|
||||
requester_spec_path: Option<&Path>,
|
||||
requested_by: String,
|
||||
) -> Result<Option<NodeIndex>> {
|
||||
if deps::is_dep_satisfied_in_db(dep, &self.db_path)? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let dep_name = deps::dep_name(dep);
|
||||
let candidates = self.collect_candidates(dep_name, requester_spec_path)?;
|
||||
if candidates.is_empty() {
|
||||
if let Some(spec_path) = requester_spec_path {
|
||||
anyhow::bail!(
|
||||
"Could not resolve dependency '{}' (from {}). Checked binary repos, source repos, and local sibling specs",
|
||||
dep,
|
||||
spec_path.display()
|
||||
);
|
||||
}
|
||||
anyhow::bail!(
|
||||
"Could not resolve dependency '{}'. Checked binary repos and source repos",
|
||||
dep
|
||||
);
|
||||
}
|
||||
|
||||
let chosen = self.choose_candidate(dep, &candidates)?;
|
||||
let idx = match chosen.kind {
|
||||
CandidateKind::Source {
|
||||
ref path,
|
||||
local_sibling,
|
||||
} => self.ensure_source_spec_node(path, true, local_sibling, requested_by)?,
|
||||
CandidateKind::Binary {
|
||||
ref repo_name,
|
||||
ref record,
|
||||
} => self.ensure_binary_node(repo_name, (**record).clone(), requested_by)?,
|
||||
};
|
||||
Ok(Some(idx))
|
||||
}
|
||||
|
||||
fn ensure_binary_node(
|
||||
&mut self,
|
||||
repo_name: &str,
|
||||
record: db::repo::BinaryRepoPackageRecord,
|
||||
requested_by: String,
|
||||
) -> Result<NodeIndex> {
|
||||
if let Some(&idx) = self.by_package.get(&record.name) {
|
||||
self.mark_requested_by(idx, requested_by);
|
||||
return Ok(idx);
|
||||
}
|
||||
|
||||
self.push_stack(&record.name)?;
|
||||
|
||||
let idx = self.graph.add_node(NodeData {
|
||||
step: PlannedStep {
|
||||
package: record.name.clone(),
|
||||
action: PlanAction::InstallBinary,
|
||||
origin: PlanOrigin::Binary {
|
||||
repo_name: repo_name.to_string(),
|
||||
record: Box::new(record.clone()),
|
||||
},
|
||||
requested_by: vec![requested_by],
|
||||
},
|
||||
});
|
||||
self.by_package.insert(record.name.clone(), idx);
|
||||
|
||||
for dep in &record.runtime_dependencies {
|
||||
if let Some(dep_idx) =
|
||||
self.resolve_dep_node(dep, None, format!("{} needs {}", record.name, dep))?
|
||||
{
|
||||
self.graph.add_edge(dep_idx, idx, ());
|
||||
}
|
||||
}
|
||||
|
||||
self.pop_stack(&record.name);
|
||||
Ok(idx)
|
||||
}
|
||||
|
||||
fn choose_candidate(&self, dep: &str, candidates: &[Candidate]) -> Result<Candidate> {
|
||||
let mut sorted = candidates.to_vec();
|
||||
sorted.sort_by(|a, b| {
|
||||
candidate_sort_key(a, self.opts.prefer_binary)
|
||||
.cmp(&candidate_sort_key(b, self.opts.prefer_binary))
|
||||
});
|
||||
|
||||
if sorted.len() == 1 {
|
||||
return Ok(sorted.remove(0));
|
||||
}
|
||||
|
||||
if self.opts.assume_yes {
|
||||
let chosen = sorted.remove(0);
|
||||
ui::info(format!(
|
||||
"Multiple providers matched '{}' - using {} ({}) due to --yes",
|
||||
dep, chosen.package, chosen.sort_label
|
||||
));
|
||||
return Ok(chosen);
|
||||
}
|
||||
|
||||
let options: Vec<String> = sorted.iter().map(format_candidate_label).collect();
|
||||
let prompt = format!("Multiple packages satisfy '{}'. Choose one", dep);
|
||||
let choice = ui::prompt_select_index(&prompt, &options, 0)?;
|
||||
Ok(sorted.remove(choice))
|
||||
}
|
||||
|
||||
fn collect_candidates(
|
||||
&mut self,
|
||||
dep_name: &str,
|
||||
requester_spec_path: Option<&Path>,
|
||||
) -> Result<Vec<Candidate>> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen = HashSet::<String>::new();
|
||||
|
||||
// Binary repos
|
||||
let host_arch = std::env::consts::ARCH;
|
||||
let mut binary_repos: Vec<_> = self
|
||||
.config
|
||||
.binary_repos
|
||||
.iter()
|
||||
.filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch))
|
||||
.collect();
|
||||
binary_repos.sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0)));
|
||||
|
||||
for (repo_name, repo_cfg) in binary_repos {
|
||||
match db::repo::find_binary_repo_packages(
|
||||
repo_name,
|
||||
repo_cfg,
|
||||
self.rootfs,
|
||||
&self.config.package_cache_dir,
|
||||
dep_name,
|
||||
) {
|
||||
Ok(records) => {
|
||||
for rec in records {
|
||||
let match_kind = if rec.name.eq_ignore_ascii_case(dep_name) {
|
||||
MatchKind::Exact
|
||||
} else {
|
||||
MatchKind::Provides
|
||||
};
|
||||
let key = format!("bin:{}:{}", repo_name, rec.name);
|
||||
if !seen.insert(key) {
|
||||
continue;
|
||||
}
|
||||
out.push(Candidate {
|
||||
package: rec.name.clone(),
|
||||
kind: CandidateKind::Binary {
|
||||
repo_name: repo_name.clone(),
|
||||
record: Box::new(rec.clone()),
|
||||
},
|
||||
match_kind,
|
||||
sort_repo_priority: repo_cfg.priority,
|
||||
sort_label: format!("binary:{}", repo_name),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => crate::log_warn!("Binary repo '{}': {}", repo_name, e),
|
||||
}
|
||||
}
|
||||
|
||||
// Global source index
|
||||
if let Some(path) = self.pkg_index.find(dep_name) {
|
||||
let spec = self.load_spec(&path)?;
|
||||
let match_kind = if spec.package.name.eq_ignore_ascii_case(dep_name) {
|
||||
MatchKind::Exact
|
||||
} else {
|
||||
MatchKind::Provides
|
||||
};
|
||||
let local_sibling = false;
|
||||
let key = format!("src:{}", path.display());
|
||||
if seen.insert(key) {
|
||||
out.push(Candidate {
|
||||
package: spec.package.name.clone(),
|
||||
kind: CandidateKind::Source {
|
||||
path: path.clone(),
|
||||
local_sibling,
|
||||
},
|
||||
match_kind,
|
||||
sort_repo_priority: 0,
|
||||
sort_label: format!("source:{}", source_label_for_path(self.config, &path)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Additional source providers from index (for provider prompt)
|
||||
for path in self.pkg_index.find_providers(dep_name) {
|
||||
let spec = self.load_spec(&path)?;
|
||||
let key = format!("src:{}", path.display());
|
||||
if !seen.insert(key) {
|
||||
continue;
|
||||
}
|
||||
out.push(Candidate {
|
||||
package: spec.package.name.clone(),
|
||||
kind: CandidateKind::Source {
|
||||
path: path.clone(),
|
||||
local_sibling: false,
|
||||
},
|
||||
match_kind: if spec.package.name.eq_ignore_ascii_case(dep_name) {
|
||||
MatchKind::Exact
|
||||
} else {
|
||||
MatchKind::Provides
|
||||
},
|
||||
sort_repo_priority: 0,
|
||||
sort_label: format!("source:{}", source_label_for_path(self.config, &path)),
|
||||
});
|
||||
}
|
||||
|
||||
// Local sibling fallback (e.g. ../foo/*.toml when building from a local tree)
|
||||
let local_sibling_root = requester_spec_path
|
||||
.and_then(|p| p.parent())
|
||||
.and_then(|p| p.parent())
|
||||
.map(Path::to_path_buf)
|
||||
.or_else(|| self.opts.local_sibling_root.clone());
|
||||
if let Some(root) = local_sibling_root {
|
||||
for hit in self.local_sibling_hits(&root)? {
|
||||
let exact = hit.spec_name.eq_ignore_ascii_case(dep_name);
|
||||
let provides = hit
|
||||
.provides
|
||||
.iter()
|
||||
.any(|p| p.eq_ignore_ascii_case(dep_name));
|
||||
if !(exact || provides) {
|
||||
continue;
|
||||
}
|
||||
let key = format!("src:{}", hit.path.display());
|
||||
if !seen.insert(key) {
|
||||
continue;
|
||||
}
|
||||
out.push(Candidate {
|
||||
package: hit.spec_name.clone(),
|
||||
kind: CandidateKind::Source {
|
||||
path: hit.path.clone(),
|
||||
local_sibling: true,
|
||||
},
|
||||
match_kind: if exact {
|
||||
MatchKind::Exact
|
||||
} else {
|
||||
MatchKind::Provides
|
||||
},
|
||||
sort_repo_priority: -10,
|
||||
sort_label: "source:local-sibling".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn load_spec(&mut self, path: &Path) -> Result<&PackageSpec> {
|
||||
if !self.spec_cache.contains_key(path) {
|
||||
let mut spec = PackageSpec::from_file(path)
|
||||
.with_context(|| format!("Failed to parse spec {}", path.display()))?;
|
||||
spec.apply_config(self.config);
|
||||
self.spec_cache.insert(path.to_path_buf(), spec);
|
||||
}
|
||||
self.spec_cache
|
||||
.get(path)
|
||||
.with_context(|| format!("Spec cache entry missing for {}", path.display()))
|
||||
}
|
||||
|
||||
fn local_sibling_hits(&mut self, root: &Path) -> Result<&[LocalSpecHit]> {
|
||||
if !self.local_sibling_cache.contains_key(root) {
|
||||
let mut hits = Vec::new();
|
||||
for entry in walkdir::WalkDir::new(root).min_depth(1).max_depth(4) {
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let path = entry.path();
|
||||
if !entry.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
if path.extension().is_none_or(|ext| ext != "toml") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(mut spec) = PackageSpec::from_file(path) {
|
||||
spec.apply_config(self.config);
|
||||
hits.push(LocalSpecHit {
|
||||
spec_name: spec.package.name.clone(),
|
||||
path: path.to_path_buf(),
|
||||
provides: spec.alternatives.provides.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
self.local_sibling_cache.insert(root.to_path_buf(), hits);
|
||||
}
|
||||
Ok(self
|
||||
.local_sibling_cache
|
||||
.get(root)
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]))
|
||||
}
|
||||
|
||||
fn push_stack(&mut self, pkg: &str) -> Result<()> {
|
||||
if self.stack.iter().any(|s| s == pkg) {
|
||||
let mut chain = self.stack.join(" -> ");
|
||||
if !chain.is_empty() {
|
||||
chain.push_str(" -> ");
|
||||
}
|
||||
chain.push_str(pkg);
|
||||
anyhow::bail!("Dependency cycle detected: {}", chain);
|
||||
}
|
||||
self.stack.push(pkg.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pop_stack(&mut self, pkg: &str) {
|
||||
if matches!(self.stack.last(), Some(last) if last == pkg) {
|
||||
self.stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn source_deps_for_install(spec: &PackageSpec) -> Vec<String> {
|
||||
let mut deps_all = Vec::new();
|
||||
for dep in &spec.dependencies.build {
|
||||
push_unique(&mut deps_all, dep.clone());
|
||||
}
|
||||
for dep in &spec.dependencies.runtime {
|
||||
push_unique(&mut deps_all, dep.clone());
|
||||
}
|
||||
if matches!(spec.build.build_type, BuildType::Autotools) {
|
||||
for dep in &spec.dependencies.test {
|
||||
push_unique(&mut deps_all, dep.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for out in spec.outputs() {
|
||||
let deps = spec.dependencies_for_output(&out.name);
|
||||
for dep in deps.runtime {
|
||||
push_unique(&mut deps_all, dep);
|
||||
}
|
||||
}
|
||||
deps_all
|
||||
}
|
||||
|
||||
fn push_unique(v: &mut Vec<String>, item: String) {
|
||||
if !v.contains(&item) {
|
||||
v.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
fn source_label_for_path(config: &Config, path: &Path) -> String {
|
||||
if let Ok(rel) = path.strip_prefix(&config.repo_clone_dir)
|
||||
&& let Some(repo) = rel.components().next()
|
||||
{
|
||||
return repo.as_os_str().to_string_lossy().into_owned();
|
||||
}
|
||||
"local".to_string()
|
||||
}
|
||||
|
||||
fn format_candidate_label(c: &Candidate) -> String {
|
||||
let match_label = match c.match_kind {
|
||||
MatchKind::Exact => "exact",
|
||||
MatchKind::Provides => "provides",
|
||||
};
|
||||
match &c.kind {
|
||||
CandidateKind::Source {
|
||||
path,
|
||||
local_sibling,
|
||||
} => format!(
|
||||
"{} [{}] {} {}",
|
||||
c.package,
|
||||
if *local_sibling {
|
||||
"source:local-sibling"
|
||||
} else {
|
||||
&c.sort_label
|
||||
},
|
||||
match_label,
|
||||
path.display()
|
||||
),
|
||||
CandidateKind::Binary { repo_name, record } => format!(
|
||||
"{} [binary:{}] {} {}-{} size={} file={}",
|
||||
c.package,
|
||||
repo_name,
|
||||
match_label,
|
||||
record.version,
|
||||
record.revision,
|
||||
record.size,
|
||||
record.filename
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate_sort_key(c: &Candidate, prefer_binary: bool) -> (i32, i32, i32, String, String) {
|
||||
let is_binary = matches!(c.kind, CandidateKind::Binary { .. });
|
||||
let kind_rank = match (prefer_binary, is_binary) {
|
||||
(true, true) => 0,
|
||||
(true, false) => 1,
|
||||
(false, false) => 0,
|
||||
(false, true) => 1,
|
||||
};
|
||||
let match_rank = match c.match_kind {
|
||||
MatchKind::Exact => 0,
|
||||
MatchKind::Provides => 1,
|
||||
};
|
||||
(
|
||||
kind_rank,
|
||||
match_rank,
|
||||
c.sort_repo_priority,
|
||||
c.package.clone(),
|
||||
c.sort_label.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_install_plan(
|
||||
config: &Config,
|
||||
rootfs: &Path,
|
||||
target: InstallTarget,
|
||||
opts: PlannerOptions,
|
||||
) -> Result<ExecutionPlan> {
|
||||
Resolver::new(config, rootfs, opts).plan_for_install_target(target)
|
||||
}
|
||||
|
||||
pub(crate) fn build_dependency_install_plan(
|
||||
config: &Config,
|
||||
rootfs: &Path,
|
||||
deps_to_install: &[String],
|
||||
opts: PlannerOptions,
|
||||
) -> Result<ExecutionPlan> {
|
||||
Resolver::new(config, rootfs, opts).plan_for_deps(deps_to_install)
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
//! Minisign-based detached signature helpers.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use minisign::{PublicKey, SecretKeyBox, SignatureBox};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const PUBLIC_KEYS_DIR_REL: &str = "usr/share/depot/keys/public";
|
||||
const SIGN_KEYS_DIR_REL: &str = "usr/share/depot/keys/sign";
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KeyLocations {
|
||||
pub public_key: Option<PathBuf>,
|
||||
pub signing_key: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn is_zst_file(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.map(|n| n.to_string_lossy().ends_with(".zst"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn candidate_roots(rootfs: &Path, host_root: &Path) -> Vec<PathBuf> {
|
||||
let mut out = Vec::new();
|
||||
out.push(rootfs.to_path_buf());
|
||||
if rootfs != host_root {
|
||||
out.push(host_root.to_path_buf());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn key_dir_candidates(rootfs: &Path, host_root: &Path, rel: &str) -> Vec<PathBuf> {
|
||||
candidate_roots(rootfs, host_root)
|
||||
.into_iter()
|
||||
.map(|root| root.join(rel))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pick_key_file(dir: &Path, is_public: bool) -> Result<Option<PathBuf>> {
|
||||
if !dir.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
if !dir.is_dir() {
|
||||
anyhow::bail!("Key path is not a directory: {}", dir.display());
|
||||
}
|
||||
|
||||
let mut files: Vec<PathBuf> = fs::read_dir(dir)
|
||||
.with_context(|| format!("Failed to read {}", dir.display()))?
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.is_file())
|
||||
.collect();
|
||||
if files.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
files.sort();
|
||||
|
||||
let preferred = if is_public {
|
||||
["depot.pub", "depot.minisign.pub", "minisign.pub"]
|
||||
} else {
|
||||
["depot.key", "depot.minisign.key", "minisign.key"]
|
||||
};
|
||||
for name in preferred {
|
||||
if let Some(found) = files
|
||||
.iter()
|
||||
.find(|p| p.file_name().is_some_and(|n| n == name))
|
||||
.cloned()
|
||||
{
|
||||
return Ok(Some(found));
|
||||
}
|
||||
}
|
||||
|
||||
let ext = if is_public { "pub" } else { "key" };
|
||||
let ext_matches: Vec<PathBuf> = files
|
||||
.iter()
|
||||
.filter(|p| p.extension().is_some_and(|e| e == ext))
|
||||
.cloned()
|
||||
.collect();
|
||||
if ext_matches.len() == 1 {
|
||||
return Ok(Some(ext_matches[0].clone()));
|
||||
}
|
||||
if files.len() == 1 {
|
||||
return Ok(Some(files[0].clone()));
|
||||
}
|
||||
|
||||
anyhow::bail!(
|
||||
"Ambiguous {} key directory {} (multiple key files found)",
|
||||
if is_public { "public" } else { "signing" },
|
||||
dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
fn locate_keys_in_roots(rootfs: &Path, host_root: &Path) -> Result<KeyLocations> {
|
||||
let mut keys = KeyLocations::default();
|
||||
|
||||
for dir in key_dir_candidates(rootfs, host_root, PUBLIC_KEYS_DIR_REL) {
|
||||
if let Some(path) = pick_key_file(&dir, true)? {
|
||||
keys.public_key = Some(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for dir in key_dir_candidates(rootfs, host_root, SIGN_KEYS_DIR_REL) {
|
||||
if let Some(path) = pick_key_file(&dir, false)? {
|
||||
keys.signing_key = Some(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Locate public/signing keys by checking both `rootfs` and the host `/`.
|
||||
pub fn locate_keys(rootfs: &Path) -> Result<KeyLocations> {
|
||||
locate_keys_in_roots(rootfs, Path::new("/"))
|
||||
}
|
||||
|
||||
fn detached_sig_path(input: &Path) -> PathBuf {
|
||||
PathBuf::from(format!("{}.sig", input.display()))
|
||||
}
|
||||
|
||||
fn sign_detached_with_key_paths(input: &Path, sig_path: &Path, keys: &KeyLocations) -> Result<()> {
|
||||
if !input.exists() {
|
||||
anyhow::bail!("File not found: {}", input.display());
|
||||
}
|
||||
if !is_zst_file(input) {
|
||||
anyhow::bail!(
|
||||
"Signing command currently only supports .zst files: {}",
|
||||
input.display()
|
||||
);
|
||||
}
|
||||
|
||||
let signing_key_path = keys.signing_key.as_ref().with_context(
|
||||
|| "No minisign signing key found in /usr/share/depot/keys/sign (checked rootfs and host)",
|
||||
)?;
|
||||
|
||||
let secret_key_text = fs::read_to_string(signing_key_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to read minisign signing key: {}",
|
||||
signing_key_path.display()
|
||||
)
|
||||
})?;
|
||||
let secret_key_box = SecretKeyBox::from_string(&secret_key_text).with_context(|| {
|
||||
format!(
|
||||
"Invalid minisign signing key file: {}",
|
||||
signing_key_path.display()
|
||||
)
|
||||
})?;
|
||||
let secret_key = match secret_key_box.clone().into_unencrypted_secret_key() {
|
||||
Ok(sk) => sk,
|
||||
Err(_) => secret_key_box.into_secret_key(None).with_context(|| {
|
||||
format!(
|
||||
"Failed to load minisign signing key: {}",
|
||||
signing_key_path.display()
|
||||
)
|
||||
})?,
|
||||
};
|
||||
let public_key = if let Some(path) = &keys.public_key {
|
||||
Some(
|
||||
PublicKey::from_file(path).with_context(|| {
|
||||
format!("Failed to load minisign public key: {}", path.display())
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
crate::log_warn!(
|
||||
"No minisign public key found in /usr/share/depot/keys/public (checked rootfs and host)"
|
||||
);
|
||||
None
|
||||
};
|
||||
|
||||
let file =
|
||||
fs::File::open(input).with_context(|| format!("Failed to open {}", input.display()))?;
|
||||
let sig = minisign::sign(
|
||||
public_key.as_ref(),
|
||||
&secret_key,
|
||||
file,
|
||||
None,
|
||||
Some(&format!("depot signature for {}", input.display())),
|
||||
)
|
||||
.with_context(|| format!("Failed to sign {}", input.display()))?;
|
||||
|
||||
fs::write(sig_path, sig.to_bytes())
|
||||
.with_context(|| format!("Failed to write detached signature {}", sig_path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_detached_with_key_paths(
|
||||
input: &Path,
|
||||
sig_path: &Path,
|
||||
keys: &KeyLocations,
|
||||
) -> Result<()> {
|
||||
if !input.exists() {
|
||||
anyhow::bail!("File not found: {}", input.display());
|
||||
}
|
||||
if !sig_path.exists() {
|
||||
anyhow::bail!("Detached signature not found: {}", sig_path.display());
|
||||
}
|
||||
if !is_zst_file(input) {
|
||||
anyhow::bail!(
|
||||
"Verification currently only supports .zst files: {}",
|
||||
input.display()
|
||||
);
|
||||
}
|
||||
|
||||
let public_key_path = keys.public_key.as_ref().with_context(
|
||||
|| "No minisign public key found in /usr/share/depot/keys/public (checked rootfs and host)",
|
||||
)?;
|
||||
let public_key = PublicKey::from_file(public_key_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to load minisign public key: {}",
|
||||
public_key_path.display()
|
||||
)
|
||||
})?;
|
||||
let sig = SignatureBox::from_file(sig_path)
|
||||
.with_context(|| format!("Failed to load detached signature: {}", sig_path.display()))?;
|
||||
let mut file =
|
||||
fs::File::open(input).with_context(|| format!("Failed to open {}", input.display()))?;
|
||||
minisign::verify(&public_key, &sig, &mut file, true, false, false).with_context(|| {
|
||||
format!(
|
||||
"Detached signature verification failed for {}",
|
||||
input.display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sign a `.zst` file with a detached minisign signature written to `<file>.sig`.
|
||||
pub fn sign_zst_file_detached(rootfs: &Path, input: &Path) -> Result<PathBuf> {
|
||||
let keys = locate_keys(rootfs)?;
|
||||
let sig_path = detached_sig_path(input);
|
||||
sign_detached_with_key_paths(input, &sig_path, &keys)?;
|
||||
Ok(sig_path)
|
||||
}
|
||||
|
||||
/// Verify a `.zst` file using a detached minisign signature.
|
||||
pub fn verify_zst_file_detached(rootfs: &Path, input: &Path, sig_path: &Path) -> Result<()> {
|
||||
let keys = locate_keys(rootfs)?;
|
||||
verify_detached_with_key_paths(input, sig_path, &keys)
|
||||
}
|
||||
|
||||
/// Attempt to sign a `.zst` file using discovered keys; skip if no signing key exists.
|
||||
pub fn auto_sign_zst_file_detached(rootfs: &Path, input: &Path) -> Result<Option<PathBuf>> {
|
||||
if !is_zst_file(input) {
|
||||
return Ok(None);
|
||||
}
|
||||
let keys = locate_keys(rootfs)?;
|
||||
if keys.signing_key.is_none() {
|
||||
crate::log_info!(
|
||||
"No minisign signing key found; skipping detached signature for {}",
|
||||
input.display()
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
let sig_path = detached_sig_path(input);
|
||||
sign_detached_with_key_paths(input, &sig_path, &keys)?;
|
||||
Ok(Some(sig_path))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use minisign::KeyPair;
|
||||
use std::io::Write;
|
||||
|
||||
fn write_test_keys(root: &Path) -> Result<(PathBuf, PathBuf)> {
|
||||
let public_dir = root.join(PUBLIC_KEYS_DIR_REL);
|
||||
let sign_dir = root.join(SIGN_KEYS_DIR_REL);
|
||||
fs::create_dir_all(&public_dir)?;
|
||||
fs::create_dir_all(&sign_dir)?;
|
||||
|
||||
let pair = KeyPair::generate_unencrypted_keypair().context("Failed to generate keypair")?;
|
||||
let pub_path = public_dir.join("depot.pub");
|
||||
let sign_path = sign_dir.join("depot.key");
|
||||
fs::write(&pub_path, pair.pk.to_box()?.to_bytes())?;
|
||||
fs::write(&sign_path, pair.sk.to_box(Some("test"))?.to_bytes())?;
|
||||
Ok((pub_path, sign_path))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locate_keys_checks_rootfs_before_host() -> Result<()> {
|
||||
let rootfs = tempfile::tempdir()?;
|
||||
let host = tempfile::tempdir()?;
|
||||
let (rootfs_pub, rootfs_sign) = write_test_keys(rootfs.path())?;
|
||||
let _ = write_test_keys(host.path())?;
|
||||
|
||||
let keys = locate_keys_in_roots(rootfs.path(), host.path())?;
|
||||
assert_eq!(keys.public_key, Some(rootfs_pub));
|
||||
assert_eq!(keys.signing_key, Some(rootfs_sign));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_zst_file_writes_detached_signature() -> Result<()> {
|
||||
let rootfs = tempfile::tempdir()?;
|
||||
let host = tempfile::tempdir()?;
|
||||
let (pub_path, sign_path) = write_test_keys(rootfs.path())?;
|
||||
|
||||
let file = rootfs.path().join("artifact.tar.zst");
|
||||
let mut f = fs::File::create(&file)?;
|
||||
f.write_all(b"test payload")?;
|
||||
f.flush()?;
|
||||
|
||||
let keys = KeyLocations {
|
||||
public_key: Some(pub_path.clone()),
|
||||
signing_key: Some(sign_path.clone()),
|
||||
};
|
||||
let sig_path = PathBuf::from(format!("{}.sig", file.display()));
|
||||
sign_detached_with_key_paths(&file, &sig_path, &keys)?;
|
||||
assert!(sig_path.exists());
|
||||
|
||||
let pk = PublicKey::from_file(pub_path)?;
|
||||
let sig_box = SignatureBox::from_file(&sig_path)?;
|
||||
let mut reader = fs::File::open(&file)?;
|
||||
minisign::verify(&pk, &sig_box, &mut reader, true, false, false)
|
||||
.context("signature verification should succeed")?;
|
||||
verify_detached_with_key_paths(&file, &sig_path, &keys)?;
|
||||
|
||||
// Also make sure host/rootfs lookup path version works without touching /.
|
||||
let _ = locate_keys_in_roots(rootfs.path(), host.path())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+139
-8
@@ -1,9 +1,11 @@
|
||||
//! Git source support via libgit2 (git2 crate)
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use git2::{Cred, FetchOptions, Oid, RemoteCallbacks, Repository};
|
||||
use git2::{Cred, CredentialType, FetchOptions, Oid, RemoteCallbacks, Repository};
|
||||
use inquire::Password;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::io::{self, IsTerminal, Write};
|
||||
use std::path::Path;
|
||||
|
||||
/// Checkout a repository URL at a specific revision into `checkout_dir`.
|
||||
@@ -115,15 +117,144 @@ fn checkout_rev(repo: &Repository, rev: &str) -> Result<()> {
|
||||
|
||||
fn remote_callbacks() -> RemoteCallbacks<'static> {
|
||||
let mut callbacks = RemoteCallbacks::new();
|
||||
let mut credential_state = CredentialState::default();
|
||||
|
||||
// Try SSH agent / default key locations.
|
||||
callbacks.credentials(|_url, username_from_url, _allowed| {
|
||||
if let Some(name) = username_from_url {
|
||||
Cred::ssh_key_from_agent(name)
|
||||
} else {
|
||||
Cred::default()
|
||||
}
|
||||
callbacks.credentials(move |url, username_from_url, allowed| {
|
||||
credential_state.provide(url, username_from_url, allowed)
|
||||
});
|
||||
|
||||
callbacks
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CredentialState {
|
||||
username: Option<String>,
|
||||
prompted_userpass: bool,
|
||||
}
|
||||
|
||||
impl CredentialState {
|
||||
fn provide(
|
||||
&mut self,
|
||||
url: &str,
|
||||
username_from_url: Option<&str>,
|
||||
allowed: CredentialType,
|
||||
) -> std::result::Result<Cred, git2::Error> {
|
||||
if allowed.contains(CredentialType::USERNAME)
|
||||
&& !allowed.intersects(
|
||||
CredentialType::SSH_KEY
|
||||
| CredentialType::USER_PASS_PLAINTEXT
|
||||
| CredentialType::DEFAULT,
|
||||
)
|
||||
{
|
||||
let username = self.username(url, username_from_url)?;
|
||||
return Cred::username(&username);
|
||||
}
|
||||
|
||||
if allowed.contains(CredentialType::SSH_KEY) {
|
||||
let ssh_username = username_from_url
|
||||
.or(self.username.as_deref())
|
||||
.unwrap_or("git");
|
||||
if let Ok(cred) = Cred::ssh_key_from_agent(ssh_username) {
|
||||
return Ok(cred);
|
||||
}
|
||||
}
|
||||
|
||||
if allowed.contains(CredentialType::DEFAULT)
|
||||
&& let Ok(cred) = Cred::default()
|
||||
{
|
||||
return Ok(cred);
|
||||
}
|
||||
|
||||
if allowed.contains(CredentialType::USER_PASS_PLAINTEXT) {
|
||||
let (username, password) = self.userpass(url, username_from_url)?;
|
||||
return Cred::userpass_plaintext(&username, &password);
|
||||
}
|
||||
|
||||
if allowed.contains(CredentialType::USERNAME) {
|
||||
let username = self.username(url, username_from_url)?;
|
||||
return Cred::username(&username);
|
||||
}
|
||||
|
||||
Err(git2::Error::from_str(
|
||||
"Unsupported authentication method requested by git remote",
|
||||
))
|
||||
}
|
||||
|
||||
fn username(
|
||||
&mut self,
|
||||
url: &str,
|
||||
username_from_url: Option<&str>,
|
||||
) -> std::result::Result<String, git2::Error> {
|
||||
if let Some(username) = username_from_url
|
||||
&& !username.trim().is_empty()
|
||||
{
|
||||
let username = username.trim().to_string();
|
||||
self.username.get_or_insert_with(|| username.clone());
|
||||
return Ok(username);
|
||||
}
|
||||
if let Some(username) = self.username.as_ref() {
|
||||
return Ok(username.clone());
|
||||
}
|
||||
|
||||
ensure_prompt_terminal(url)?;
|
||||
crate::log_warn!("Git remote requires credentials: {}", url);
|
||||
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
print!("Git username for {}: ", url);
|
||||
io::stdout()
|
||||
.flush()
|
||||
.map_err(|e| git2::Error::from_str(&format!("Failed to flush prompt: {e}")))?;
|
||||
input.clear();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.map_err(|e| git2::Error::from_str(&format!("Failed to read username: {e}")))?;
|
||||
let trimmed = input.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let username = trimmed.to_string();
|
||||
self.username = Some(username.clone());
|
||||
return Ok(username);
|
||||
}
|
||||
crate::log_warn!("Username cannot be empty.");
|
||||
}
|
||||
}
|
||||
|
||||
fn userpass(
|
||||
&mut self,
|
||||
url: &str,
|
||||
username_from_url: Option<&str>,
|
||||
) -> std::result::Result<(String, String), git2::Error> {
|
||||
if self.prompted_userpass {
|
||||
return Err(git2::Error::from_str(
|
||||
"Git credentials were rejected by the remote",
|
||||
));
|
||||
}
|
||||
|
||||
let username = self.username(url, username_from_url)?;
|
||||
ensure_prompt_terminal(url)?;
|
||||
self.prompted_userpass = true;
|
||||
|
||||
let prompt = format!("Git password/token for {} ({}):", url, username);
|
||||
let password = Password::new(&prompt)
|
||||
.without_confirmation()
|
||||
.prompt()
|
||||
.map_err(|e| {
|
||||
git2::Error::from_str(&format!("Failed to read git password/token: {e}"))
|
||||
})?;
|
||||
if password.is_empty() {
|
||||
return Err(git2::Error::from_str("Git password/token cannot be empty"));
|
||||
}
|
||||
|
||||
Ok((username, password))
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_prompt_terminal(url: &str) -> std::result::Result<(), git2::Error> {
|
||||
if io::stdin().is_terminal() && io::stdout().is_terminal() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(git2::Error::from_str(&format!(
|
||||
"Authentication required for {url}, but no interactive terminal is available"
|
||||
)))
|
||||
}
|
||||
|
||||
+2
-2
@@ -219,7 +219,7 @@ fn auto_strip_elf_files(destdir: &Path) -> Result<usize> {
|
||||
}
|
||||
|
||||
let status = Command::new("strip")
|
||||
.arg("--strip-unneeded")
|
||||
.arg("--strip-debug")
|
||||
.arg(path)
|
||||
.status()
|
||||
.with_context(|| {
|
||||
@@ -278,7 +278,7 @@ fn compress_manpages_zstd(destdir: &Path) -> Result<usize> {
|
||||
let out_file = fs::File::create(&tmp_path)
|
||||
.with_context(|| format!("Failed to create {}", tmp_path.display()))?;
|
||||
|
||||
let mut encoder = zstd::stream::write::Encoder::new(out_file, 22)
|
||||
let mut encoder = zstd::stream::write::Encoder::new(out_file, 19)
|
||||
.with_context(|| format!("Failed to start zstd encoder for {}", path.display()))?;
|
||||
io::copy(&mut input, &mut encoder)
|
||||
.with_context(|| format!("Failed to compress {}", path.display()))?;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::io::{self, IsTerminal, Write};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
static ASSUME_YES: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Stream {
|
||||
@@ -88,7 +91,17 @@ fn parse_yes_no_input(input: &str, default_yes: bool) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure global prompt behavior for the current process.
|
||||
pub fn set_assume_yes(assume_yes: bool) {
|
||||
ASSUME_YES.store(assume_yes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn prompt_yes_no(prompt: &str, default_yes: bool) -> Result<bool> {
|
||||
if ASSUME_YES.load(Ordering::Relaxed) {
|
||||
info(format!("{} [auto-yes]", prompt));
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
loop {
|
||||
print!(
|
||||
@@ -114,6 +127,59 @@ pub fn prompt_yes_no(prompt: &str, default_yes: bool) -> Result<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompt the user to choose one option by index.
|
||||
pub fn prompt_select_index(prompt: &str, options: &[String], default_idx: usize) -> Result<usize> {
|
||||
if options.is_empty() {
|
||||
anyhow::bail!("No options available for selection");
|
||||
}
|
||||
let default_idx = default_idx.min(options.len() - 1);
|
||||
if ASSUME_YES.load(Ordering::Relaxed) {
|
||||
info(format!(
|
||||
"{} [auto-select {}: {}]",
|
||||
prompt,
|
||||
default_idx + 1,
|
||||
options[default_idx]
|
||||
));
|
||||
return Ok(default_idx);
|
||||
}
|
||||
|
||||
loop {
|
||||
println!("{} {}", label(Stream::Stdout, "?", "35"), prompt);
|
||||
for (idx, option) in options.iter().enumerate() {
|
||||
println!(
|
||||
" {} {}{}",
|
||||
idx + 1,
|
||||
option,
|
||||
if idx == default_idx { " [default]" } else { "" }
|
||||
);
|
||||
}
|
||||
print!(
|
||||
"{} Select an option [1-{}] (default {}): ",
|
||||
label(Stream::Stdout, "?", "35"),
|
||||
options.len(),
|
||||
default_idx + 1
|
||||
);
|
||||
io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush prompt to stdout")?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.context("Failed to read user input from stdin")?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default_idx);
|
||||
}
|
||||
if let Ok(num) = trimmed.parse::<usize>()
|
||||
&& (1..=options.len()).contains(&num)
|
||||
{
|
||||
return Ok(num - 1);
|
||||
}
|
||||
warn("Please enter a valid option number.");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_yes_no_input;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 6 ]; then
|
||||
echo "usage: $0 <cargo> <src_root> <build_root> <profile> <release_flag> <output>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cargo_bin="$1"
|
||||
src_root="$2"
|
||||
build_root="$3"
|
||||
profile="$4"
|
||||
release_flag="$5"
|
||||
output="$6"
|
||||
|
||||
cargo_home="$build_root/cargo-home"
|
||||
cargo_target_dir="$build_root/cargo-target"
|
||||
|
||||
mkdir -p "$cargo_home" "$cargo_target_dir"
|
||||
|
||||
export CARGO_HOME="$cargo_home"
|
||||
export CARGO_TARGET_DIR="$cargo_target_dir"
|
||||
|
||||
if [ "$release_flag" = "1" ]; then
|
||||
"$cargo_bin" build --locked --manifest-path "$src_root/Cargo.toml" --release
|
||||
else
|
||||
"$cargo_bin" build --locked --manifest-path "$src_root/Cargo.toml"
|
||||
fi
|
||||
|
||||
src_bin="$cargo_target_dir/$profile/depot"
|
||||
if [ ! -f "$src_bin" ]; then
|
||||
echo "cargo build finished but binary not found: $src_bin" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$output")"
|
||||
cp "$src_bin" "$output"
|
||||
chmod 0755 "$output"
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 3 ]; then
|
||||
echo "usage: $0 <cargo> <src_root> <build_root>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cargo_bin="$1"
|
||||
src_root="$2"
|
||||
build_root="$3"
|
||||
|
||||
cargo_home="$build_root/cargo-home"
|
||||
cargo_target_dir="$build_root/cargo-target"
|
||||
|
||||
mkdir -p "$cargo_home" "$cargo_target_dir"
|
||||
|
||||
export CARGO_HOME="$cargo_home"
|
||||
export CARGO_TARGET_DIR="$cargo_target_dir"
|
||||
|
||||
exec "$cargo_bin" test --locked --manifest-path "$src_root/Cargo.toml"
|
||||
Reference in New Issue
Block a user