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:
2026-02-24 21:38:12 -06:00
parent d63ad03e98
commit 9953d4b2ee
30 changed files with 4536 additions and 111 deletions
Generated
+206 -17
View File
@@ -421,6 +421,12 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "ct-codecs"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
[[package]] [[package]]
name = "deflate64" name = "deflate64"
version = "0.1.10" version = "0.1.10"
@@ -435,14 +441,17 @@ dependencies = [
"ar", "ar",
"bzip2", "bzip2",
"clap", "clap",
"fd-lock",
"flate2", "flate2",
"git2", "git2",
"indicatif", "indicatif",
"inquire", "inquire",
"libz-sys", "libz-sys",
"md5", "md5",
"minisign",
"nix", "nix",
"openssl-sys", "openssl-sys",
"petgraph",
"reqwest", "reqwest",
"rusqlite", "rusqlite",
"semver", "semver",
@@ -584,6 +593,17 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 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]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.26" version = "0.2.26"
@@ -602,6 +622,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.9" version = "1.1.9"
@@ -631,6 +657,21 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 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]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@@ -923,6 +964,22 @@ dependencies = [
"tower-service", "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]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.19" version = "0.1.19"
@@ -1117,9 +1174,9 @@ dependencies = [
[[package]] [[package]]
name = "inquire" name = "inquire"
version = "0.9.3" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "979f5ab9760427ada4fa5762b2d905e5b12704fb1fada07b6bfa66aeaa586f87" checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossterm", "crossterm",
@@ -1271,7 +1328,6 @@ version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
dependencies = [ dependencies = [
"cc",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
@@ -1292,9 +1348,9 @@ dependencies = [
[[package]] [[package]]
name = "libz-sys" name = "libz-sys"
version = "1.1.23" version = "1.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -1380,6 +1436,18 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 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]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -1402,6 +1470,23 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "nix" name = "nix"
version = "0.31.1" version = "0.31.1"
@@ -1441,6 +1526,32 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 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]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.6" version = "0.1.6"
@@ -1453,15 +1564,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391"
[[package]]
name = "openssl-src"
version = "300.5.5+3.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.111" version = "0.9.111"
@@ -1470,7 +1572,6 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
"openssl-src",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
@@ -1520,6 +1621,18 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@@ -1758,10 +1871,12 @@ dependencies = [
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls", "hyper-rustls",
"hyper-tls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"native-tls",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
@@ -1770,6 +1885,7 @@ dependencies = [
"rustls-platform-verifier", "rustls-platform-verifier",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-native-tls",
"tokio-rustls", "tokio-rustls",
"tower", "tower",
"tower-http", "tower-http",
@@ -1794,6 +1910,27 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rusqlite" name = "rusqlite"
version = "0.38.0" version = "0.38.0"
@@ -1860,7 +1997,7 @@ dependencies = [
"openssl-probe 0.2.0", "openssl-probe 0.2.0",
"rustls-pki-types", "rustls-pki-types",
"schannel", "schannel",
"security-framework", "security-framework 3.5.1",
] ]
[[package]] [[package]]
@@ -1888,7 +2025,7 @@ dependencies = [
"rustls-native-certs", "rustls-native-certs",
"rustls-platform-verifier-android", "rustls-platform-verifier-android",
"rustls-webpki", "rustls-webpki",
"security-framework", "security-framework 3.5.1",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -1918,6 +2055,15 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@@ -1942,6 +2088,30 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 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]] [[package]]
name = "security-framework" name = "security-framework"
version = "3.5.1" version = "3.5.1"
@@ -2338,6 +2508,16 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.4" version = "0.26.4"
@@ -2837,6 +3017,15 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.60.2" version = "0.60.2"
+11 -16
View File
@@ -11,19 +11,19 @@ anyhow = "1.0.102"
ar = "0.9.0" ar = "0.9.0"
bzip2 = "0.6.1" bzip2 = "0.6.1"
clap = { version = "4.5.60", features = ["derive"] } 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"] } git2 = { version = "0.20.4", features = ["vendored-libgit2"] }
indicatif = "0.18.4" indicatif = "0.18.4"
nix = { version = "0.31.1", features = ["user"] } nix = { version = "0.31.1", features = ["user"] }
reqwest = { version = "0.13.2", features = ["blocking", "rustls"] } reqwest = { version = "0.13.2", features = ["blocking", "native-tls"] }
rusqlite = { version = "0.38.0", features = ["bundled"] } rusqlite = "0.38.0"
openssl-sys = { version = "0.9", features = ["vendored"] } openssl-sys = "0.9.111"
semver = "1.0.27" semver = "1.0.27"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
yaml-rust2 = "0.11.0" yaml-rust2 = "0.11.0"
sha2 = "0.10.9" sha2 = "0.10.9"
tar = "0.4.44" tar = "0.4.44"
tempfile = "3.25.0" tempfile = "=3.25.0"
thiserror = "2.0.18" thiserror = "2.0.18"
toml = "0.9.8" toml = "0.9.8"
url = "2.5.8" url = "2.5.8"
@@ -31,18 +31,13 @@ walkdir = "2.5.0"
xz2 = "0.1.7" xz2 = "0.1.7"
zip = "8.1.0" zip = "8.1.0"
zstd = { version = "0.13.3", features = ["zstdmt"] } zstd = { version = "0.13.3", features = ["zstdmt"] }
libz-sys = { version = "1.1.23", default-features = false, features = ["stock-zlib", "static"] } libz-sys = "1.1.24"
inquire = "0.9.3" inquire = "0.9.4"
md5 = "0.8.0" md5 = "0.8.0"
suppaftp = "8.0.2" suppaftp = "8.0.2"
minisign = "0.8.0"
petgraph = "0.8.3"
fd-lock = "4.0.4"
[dev-dependencies] [dev-dependencies]
tempfile = "3.25.0" tempfile = "=3.25.0"
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
panic = "abort"
strip = "symbols"
debug = false
+9 -1
View File
@@ -1,6 +1,6 @@
# depot # 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 ## Features
@@ -36,10 +36,18 @@ depot install zlib-1.2.11-1-x86_64.depot.pkg.tar.zst
## Command Reference ## Command Reference
- `install <SPEC_OR_ARCHIVE>`: Build and install a package. - `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. - `remove <PACKAGE>`: Remove an installed package.
- `build <SPEC>`: Build a package and create an archive without installing. - `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. - `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. - `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. - `repo create [DIR]`: Create a repository database from a directory of packages.
- `config`: Show current configuration and overrides. - `config`: Show current configuration and overrides.
+3 -1
View File
@@ -1,7 +1,7 @@
Contrib: example system and user configuration files for Depot Contrib: example system and user configuration files for Depot
Place these files on your system to provide sensible defaults and examples 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: Recommended installation:
@@ -18,3 +18,5 @@ Notes
- These files are examples only. Review and adapt to your distribution and - These files are examples only. Review and adapt to your distribution and
security policies before deploying. security policies before deploying.
- The config loader supports append syntax (e.g. `cflags += ["-g"]`). - 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.
+26
View File
@@ -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
View File
@@ -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()),
)
+4 -4
View File
@@ -60,7 +60,7 @@ pub fn build(
let mut configure_cmd = Command::new(&configure_path); let mut configure_cmd = Command::new(&configure_path);
configure_cmd.current_dir(&build_dir); 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)); configure_cmd.arg(format!("--prefix={}", flags.prefix));
@@ -143,7 +143,7 @@ pub fn build(
make_cmd.arg(target); 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(|| { let status = make_cmd.status().with_context(|| {
format!("Failed to run {} in {}", make_exec, make_dir.display()) format!("Failed to run {} in {}", make_exec, make_dir.display())
@@ -194,7 +194,7 @@ pub fn build(
for test_target in &test_targets { for test_target in &test_targets {
test_cmd.arg(test_target); 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 test_targets_display = test_targets.join(" ");
let status = test_cmd.status().with_context(|| { let status = test_cmd.status().with_context(|| {
@@ -305,7 +305,7 @@ pub fn build(
"DESTDIR".to_string(), "DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(), 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(|| { let status = install_cmd.status().with_context(|| {
format!( format!(
+4 -4
View File
@@ -82,7 +82,7 @@ pub fn build(
cmake_cmd.arg(&expanded); 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")?; let status = cmake_cmd.status().context("Failed to run cmake")?;
if !status.success() { 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 let status = build_cmd
.status() .status()
@@ -131,7 +131,7 @@ pub fn build(
for target in &test_targets { for target in &test_targets {
test_cmd.arg(target); 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(|| { let status = test_cmd.status().with_context(|| {
format!( format!(
@@ -182,7 +182,7 @@ pub fn build(
"DESTDIR".to_string(), "DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(), 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 let status = install_cmd
.status() .status()
+66 -7
View File
@@ -118,7 +118,7 @@ pub fn build(
}; };
cmd.current_dir(&build_dir); 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 // Run the command and include the OS error on spawn failures for clearer diagnostics
let status = cmd.status().map_err(|e| { let status = cmd.status().map_err(|e| {
@@ -176,9 +176,16 @@ fn build_function_mode_command(
) -> Result<Command> { ) -> Result<Command> {
let mut wrapper = String::new(); let mut wrapper = String::new();
wrapper.push_str("set -eu\n"); 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(". \"$1\"\n");
wrapper.push_str("if command -v depot_build >/dev/null 2>&1; then depot_build;\n"); wrapper.push_str("if depot_has_function depot_build; then depot_build; depot_build_ran=1;\n");
wrapper.push_str("elif command -v build >/dev/null 2>&1; then build;\n"); wrapper.push_str("elif depot_has_function build; then build; depot_build_ran=1;\n");
wrapper.push_str("fi\n"); wrapper.push_str("fi\n");
let primary = &spec.package.name; let primary = &spec.package.name;
@@ -195,18 +202,28 @@ fn build_function_mode_command(
wrapper.push_str(&format!( 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" "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!( 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!( 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 { if out.name == *primary {
wrapper wrapper
.push_str("elif command -v depot_install >/dev/null 2>&1; then depot_install;\n"); .push_str("elif depot_has_function depot_install; then depot_install; depot_output_installed=1;\n");
wrapper.push_str("elif command -v install >/dev/null 2>&1; then install;\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("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); let mut cmd = fakeroot::wrap_install_command("sh", destdir);
@@ -363,4 +380,46 @@ depot_install_dev_pkg() {
); );
Ok(()) 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(())
}
} }
+2 -2
View File
@@ -43,7 +43,7 @@ pub fn build(
let mut cmd = Command::new("sh"); let mut cmd = Command::new("sh");
cmd.arg("-c").arg(&cmd_str); cmd.arg("-c").arg(&cmd_str);
cmd.current_dir(src_dir); 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 let status = cmd
.status() .status()
@@ -84,7 +84,7 @@ pub fn build(
"DESTDIR".to_string(), "DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(), 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 let status = cmd
.status() .status()
+3 -3
View File
@@ -51,7 +51,7 @@ pub fn build(
meson_cmd.arg(arg); 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")?; let status = meson_cmd.status().context("Failed to run meson setup")?;
if !status.success() { if !status.success() {
@@ -71,7 +71,7 @@ pub fn build(
ninja_cmd.current_dir(&build_dir); ninja_cmd.current_dir(&build_dir);
ninja_cmd.arg("-j").arg(num_cpus().to_string()); 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 let status = ninja_cmd
.status() .status()
@@ -106,7 +106,7 @@ pub fn build(
"DESTDIR".to_string(), "DESTDIR".to_string(),
destdir.to_string_lossy().into_owned(), 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 let status = install_cmd
.status() .status()
+103 -3
View File
@@ -13,8 +13,9 @@ pub mod state;
use crate::cross::CrossConfig; use crate::cross::CrossConfig;
use crate::package::{BuildType, PackageSpec}; use crate::package::{BuildType, PackageSpec};
use anyhow::Result; use anyhow::Result;
use std::ffi::OsString;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::{Command, Stdio};
pub type EnvVars = Vec<(String, String)>; pub type EnvVars = Vec<(String, String)>;
@@ -41,6 +42,9 @@ pub fn standard_build_env(
if !flags.cflags.is_empty() { if !flags.cflags.is_empty() {
set_env_var(&mut env_vars, "CFLAGS", flags.cflags.join(" ")); 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() { let ldflags = if !flags.ldflags.is_empty() || !flags.libc.is_empty() {
if 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. /// Prepare a Command with a hermetic environment and some essential variables preserved.
pub fn prepare_command(cmd: &mut Command, env_vars: &EnvVars) { pub fn prepare_command(cmd: &mut Command, env_vars: &EnvVars) {
cmd.env_clear(); cmd.env_clear();
if let Some(path) = sanitized_build_path() {
cmd.env("PATH", path);
}
// Preserve essential environment variables // 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) { if let Ok(val) = std::env::var(var) {
cmd.env(var, val); 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 /// Build a package using the appropriate build system
pub fn build( pub fn build(
spec: &PackageSpec, spec: &PackageSpec,
@@ -230,6 +284,8 @@ mod tests {
std::env::set_var("HOME", "/home/test"); std::env::set_var("HOME", "/home/test");
std::env::set_var("SHELL", "/bin/zsh"); std::env::set_var("SHELL", "/bin/zsh");
std::env::set_var("DEPOT_ROOTFS", "/my/rootfs"); 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())]); prepare_command(&mut cmd, &vec![("MYVAR".to_string(), "myval".to_string())]);
@@ -251,6 +307,16 @@ mod tests {
envs.get(OsStr::new("DEPOT_ROOTFS")), envs.get(OsStr::new("DEPOT_ROOTFS")),
Some(&Some(std::ffi::OsString::from("/my/rootfs").as_os_str())) Some(&Some(std::ffi::OsString::from("/my/rootfs").as_os_str()))
); );
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] #[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] #[test]
fn test_standard_build_env_respects_export_compiler_flags_toggle() { 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); let enabled = standard_build_env(&spec, None, true, true);
assert!( assert!(
enabled.iter().any(|(k, v)| k == "CFLAGS" && v == "-O2"), enabled.iter().any(|(k, v)| k == "CFLAGS" && v == "-O2"),
"expected CFLAGS to be exported when enabled" "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!( assert!(
enabled enabled
.iter() .iter()
@@ -304,6 +400,10 @@ mod tests {
!disabled_env.iter().any(|(k, _)| k == "CFLAGS"), !disabled_env.iter().any(|(k, _)| k == "CFLAGS"),
"expected CFLAGS to be omitted when no_flags is set in spec" "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!( assert!(
!disabled_env.iter().any(|(k, _)| k == "LDFLAGS"), !disabled_env.iter().any(|(k, _)| k == "LDFLAGS"),
"expected LDFLAGS to be omitted when no_flags is set in spec" "expected LDFLAGS to be omitted when no_flags is set in spec"
+1 -1
View File
@@ -245,7 +245,7 @@ fn build_wheel_setup_py(
.arg("bdist_wheel") .arg("bdist_wheel")
.arg("--dist-dir") .arg("--dist-dir")
.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 let status = cmd
.status() .status()
+1 -1
View File
@@ -99,7 +99,7 @@ pub fn build(
} }
// Set environment // 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 let status = cargo_cmd
.status() .status()
+331 -14
View File
@@ -1,10 +1,203 @@
//! Global configuration for Depot //! Global configuration for Depot
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::collections::HashMap; use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; 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 /// Global configuration settings
#[derive(Clone)] #[derive(Clone)]
pub struct Config { pub struct Config {
@@ -20,43 +213,43 @@ pub struct Config {
pub package_overrides: toml::Value, pub package_overrides: toml::Value,
/// Appends found in system TOML files (key -> values to append) /// Appends found in system TOML files (key -> values to append)
pub appends: HashMap<String, Vec<toml::Value>>, 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) /// Mirrors mapping read from /etc/depot.d/mirrors.toml (reponame -> git url)
pub mirrors: std::collections::HashMap<String, String>, pub mirrors: std::collections::HashMap<String, String>,
/// Directory where git mirrors are cloned (absolute path). Defaults to /// Directory where git mirrors are cloned (absolute path). Defaults to
/// <rootfs>/usr/src/depot unless overridden in depot.toml /// <rootfs>/usr/src/depot unless overridden in depot.toml
pub repo_clone_dir: PathBuf, pub repo_clone_dir: PathBuf,
/// Cache directory for binary packages and repo metadata.
pub package_cache_dir: PathBuf,
} }
impl Config { impl Config {
/// Create config with paths relative to the given rootfs /// Create config with paths relative to the given rootfs
pub fn for_rootfs(rootfs: &Path) -> Self { pub fn for_rootfs(rootfs: &Path) -> Self {
let abs_rootfs = if rootfs.exists() { let abs_rootfs = resolve_rootfs_base(rootfs);
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 is_system_root = abs_rootfs == Path::new("/") || abs_rootfs.as_os_str() == "/"; let is_system_root = abs_rootfs == Path::new("/") || abs_rootfs.as_os_str() == "/";
let is_root = crate::fakeroot::is_root(); 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") let home = std::env::var("HOME")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp")); .unwrap_or_else(|_| PathBuf::from("/tmp"));
( (
home.join(".cache/depot/sources"), home.join(".cache/depot/sources"),
home.join(".cache/depot/packages"),
home.join(".cache/depot/build"), home.join(".cache/depot/build"),
home.join(".local/share/depot"), home.join(".local/share/depot"),
) )
} else { } else {
( (
abs_rootfs.join("var/cache/depot/sources"), 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/cache/depot/build"),
abs_rootfs.join("var/lib/depot"), abs_rootfs.join("var/lib/depot"),
) )
@@ -69,8 +262,12 @@ impl Config {
build_overrides: toml::Value::Table(toml::map::Map::new()), build_overrides: toml::Value::Table(toml::map::Map::new()),
package_overrides: toml::Value::Table(toml::map::Map::new()), package_overrides: toml::Value::Table(toml::map::Map::new()),
appends: HashMap::new(), appends: HashMap::new(),
repo_settings: RepoSettings::default(),
source_repos: BTreeMap::new(),
binary_repos: BTreeMap::new(),
mirrors: std::collections::HashMap::new(), mirrors: std::collections::HashMap::new(),
repo_clone_dir: abs_rootfs.join("usr/src/depot"), repo_clone_dir: abs_rootfs.join("usr/src/depot"),
package_cache_dir,
}; };
if let Err(e) = config.load_system(&abs_rootfs) { 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"); let mirrors_path = rootfs.join("etc/depot.d/mirrors.toml");
if mirrors_path.exists() { if mirrors_path.exists() {
let content = fs::read_to_string(&mirrors_path).with_context(|| { let content = fs::read_to_string(&mirrors_path).with_context(|| {
@@ -196,6 +413,12 @@ impl Config {
for (k, v) in table { for (k, v) in table {
if let Some(s) = v.as_str() { if let Some(s) = v.as_str() {
self.mirrors.insert(k.clone(), s.to_string()); 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(()) 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) { fn merge_toml_values(base: &mut toml::Value, over: &toml::Value) {
@@ -460,4 +694,87 @@ cflags = ["-O3"]
Some(1) 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"));
}
} }
+30
View File
@@ -751,3 +751,33 @@ pub fn get_package_version(db_path: &Path, name: &str) -> Result<Option<String>>
.ok(); .ok();
Ok(version) 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
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -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 /// Compare two version strings using semver if possible, fallback to string compare
fn compare_versions(installed: &str, required: &str, op: VersionOp) -> bool { fn compare_versions(installed: &str, required: &str, op: VersionOp) -> bool {
// Try semver comparison first // 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 /// Check if all build dependencies are satisfied
pub fn check_build_deps(spec: &PackageSpec, db_path: &Path) -> Result<Vec<String>> { pub fn check_build_deps(spec: &PackageSpec, db_path: &Path) -> Result<Vec<String>> {
let mut missing = Vec::new(); let mut missing = Vec::new();
+44 -1
View File
@@ -16,11 +16,18 @@ pub struct PackageIndex {
by_provides: HashMap<String, Vec<PathBuf>>, 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 { impl PackageIndex {
/// Build index by scanning packages/*/*.toml and configured repo dir. /// Build index by scanning packages/*/*.toml and configured repo dir.
/// ///
/// Use `build_with_repo_dir` to provide an explicit repo dir. /// Use `build_with_repo_dir` to provide an explicit repo dir.
/// Build index scanning the local `packages/` directory and an optional /// Build index scanning the local `packages/` directory and an optional
/// system repo dir (e.g., /usr/src/depot). If `repo_dir` is None, the /// system repo dir (e.g., /usr/src/depot). If `repo_dir` is None, the
/// default `/usr/src/depot` is used. /// default `/usr/src/depot` is used.
@@ -116,4 +123,40 @@ impl PackageIndex {
None 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
}
} }
+76
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -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() { if !spec.build.flags.ldflags.is_empty() {
flags_tbl.insert( flags_tbl.insert(
"ldflags".into(), "ldflags".into(),
@@ -1213,6 +1226,7 @@ mod tests {
flags.makefile_install_commands = vec!["make DESTDIR=$DESTDIR install".into()]; flags.makefile_install_commands = vec!["make DESTDIR=$DESTDIR install".into()];
flags.cargs = vec!["--locked".into()]; flags.cargs = vec!["--locked".into()];
flags.rustflags = vec!["-Ctarget-cpu=native".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.target = "x86_64-unknown-linux-gnu".into();
flags.keep = vec!["etc/locale.gen".into()]; flags.keep = vec!["etc/locale.gen".into()];
flags.no_flags = true; flags.no_flags = true;
@@ -1266,6 +1280,7 @@ mod tests {
assert!(toml.contains("makefile_install_commands = [")); assert!(toml.contains("makefile_install_commands = ["));
assert!(toml.contains("cargs = [")); assert!(toml.contains("cargs = ["));
assert!(toml.contains("rustflags = [")); assert!(toml.contains("rustflags = ["));
assert!(toml.contains("cxxflags = ["));
assert!(toml.contains("target = \"x86_64-unknown-linux-gnu\"")); assert!(toml.contains("target = \"x86_64-unknown-linux-gnu\""));
assert!(toml.contains("keep = [")); assert!(toml.contains("keep = ["));
assert!(toml.contains("\"etc/locale.gen\"")); assert!(toml.contains("\"etc/locale.gen\""));
+185 -1
View File
@@ -58,12 +58,17 @@ impl PackageSpec {
let content = fs::read_to_string(&abs_path) let content = fs::read_to_string(&abs_path)
.with_context(|| format!("Failed to read package spec: {}", abs_path.display()))?; .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()))?; .with_context(|| format!("Failed to parse package spec: {}", abs_path.display()))?;
spec.spec_dir = abs_path spec.spec_dir = abs_path
.parent() .parent()
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(".")); .unwrap_or_else(|| PathBuf::from("."));
spec.apply_spec_appends(&appends)?;
// Require at least one source (remote or manual) // Require at least one source (remote or manual)
if spec.source.is_empty() && spec.manual_sources.is_empty() { if spec.source.is_empty() && spec.manual_sources.is_empty() {
@@ -74,6 +79,28 @@ impl PackageSpec {
Ok(spec) 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<()> { fn validate_manual_sources(&self) -> Result<()> {
for (idx, manual) in self.manual_sources.iter().enumerate() { for (idx, manual) in self.manual_sources.iter().enumerate() {
let has_file = manual let has_file = manual
@@ -206,6 +233,17 @@ impl PackageSpec {
self.build.flags.cflags = vec![s.to_string()]; 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" => { "ldflags" => {
if let Some(arr) = v.as_array() { if let Some(arr) = v.as_array() {
self.build.flags.ldflags = arr 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" => { "ldflags" => {
for v in values { for v in values {
if let Some(arr) = v.as_array() { 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)] #[cfg(test)]
mod spec_tests { mod spec_tests {
use super::*; use super::*;
@@ -1223,6 +1341,7 @@ type = "custom"
[flags] [flags]
cc = "my-cc" cc = "my-cc"
cflags = ["-O2"] cflags = ["-O2"]
cxxflags = ["-O2", "-pipe"]
passthrough_env = ["RUSTFLAGS"] passthrough_env = ["RUSTFLAGS"]
make_vars = ["V=1"] make_vars = ["V=1"]
make_dirs = ["lib"] make_dirs = ["lib"]
@@ -1242,6 +1361,10 @@ post_configure = ["echo configured"]
"build.flags.cflags".to_string(), "build.flags.cflags".to_string(),
vec![toml::Value::String("-g".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( config.appends.insert(
"build.flags.rustflags".to_string(), "build.flags.rustflags".to_string(),
vec![toml::Value::Array(vec![ vec![toml::Value::Array(vec![
@@ -1301,6 +1424,14 @@ post_configure = ["echo configured"]
assert_eq!(spec.build.flags.cc, "my-cc"); assert_eq!(spec.build.flags.cc, "my-cc");
assert!(spec.build.flags.cflags.contains(&"-O2".to_string())); assert!(spec.build.flags.cflags.contains(&"-O2".to_string()));
assert!(spec.build.flags.cflags.contains(&"-g".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.flags.rustflags.contains(&"-C".to_string()));
assert!( assert!(
spec.build 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] #[test]
fn parse_passthrough_env_from_spec() { fn parse_passthrough_env_from_spec() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -1829,8 +2005,12 @@ cbuild = "x86_64-pc-linux-gnu"
.unwrap(), .unwrap(),
package_overrides: toml::Value::Table(toml::map::Map::new()), package_overrides: toml::Value::Table(toml::map::Map::new()),
appends: std::collections::HashMap::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(), mirrors: std::collections::HashMap::new(),
repo_clone_dir: PathBuf::from("/tmp"), repo_clone_dir: PathBuf::from("/tmp"),
package_cache_dir: PathBuf::from("/tmp"),
}; };
spec.apply_config(&config); spec.apply_config(&config);
@@ -2124,6 +2304,9 @@ pub enum BuildType {
pub struct BuildFlags { pub struct BuildFlags {
#[serde(default, deserialize_with = "deserialize_string_or_array")] #[serde(default, deserialize_with = "deserialize_string_or_array")]
pub cflags: Vec<String>, 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")] #[serde(default, deserialize_with = "deserialize_string_or_array")]
pub ldflags: Vec<String>, pub ldflags: Vec<String>,
/// Keep existing files and install package-provided replacement as `<path>.depotnew`. /// Keep existing files and install package-provided replacement as `<path>.depotnew`.
@@ -2328,6 +2511,7 @@ impl Default for BuildFlags {
fn default() -> Self { fn default() -> Self {
BuildFlags { BuildFlags {
cflags: Vec::new(), cflags: Vec::new(),
cxxflags: Vec::new(),
ldflags: Vec::new(), ldflags: Vec::new(),
keep: Vec::new(), keep: Vec::new(),
no_flags: false, no_flags: false,
+710
View File
@@ -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
View File
@@ -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
View File
@@ -1,9 +1,11 @@
//! Git source support via libgit2 (git2 crate) //! Git source support via libgit2 (git2 crate)
use anyhow::{Context, Result}; 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 sha2::{Digest, Sha256};
use std::fs; use std::fs;
use std::io::{self, IsTerminal, Write};
use std::path::Path; use std::path::Path;
/// Checkout a repository URL at a specific revision into `checkout_dir`. /// 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> { fn remote_callbacks() -> RemoteCallbacks<'static> {
let mut callbacks = RemoteCallbacks::new(); let mut callbacks = RemoteCallbacks::new();
let mut credential_state = CredentialState::default();
// Try SSH agent / default key locations. callbacks.credentials(move |url, username_from_url, allowed| {
callbacks.credentials(|_url, username_from_url, _allowed| { credential_state.provide(url, username_from_url, allowed)
if let Some(name) = username_from_url {
Cred::ssh_key_from_agent(name)
} else {
Cred::default()
}
}); });
callbacks 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
View File
@@ -219,7 +219,7 @@ fn auto_strip_elf_files(destdir: &Path) -> Result<usize> {
} }
let status = Command::new("strip") let status = Command::new("strip")
.arg("--strip-unneeded") .arg("--strip-debug")
.arg(path) .arg(path)
.status() .status()
.with_context(|| { .with_context(|| {
@@ -278,7 +278,7 @@ fn compress_manpages_zstd(destdir: &Path) -> Result<usize> {
let out_file = fs::File::create(&tmp_path) let out_file = fs::File::create(&tmp_path)
.with_context(|| format!("Failed to create {}", tmp_path.display()))?; .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()))?; .with_context(|| format!("Failed to start zstd encoder for {}", path.display()))?;
io::copy(&mut input, &mut encoder) io::copy(&mut input, &mut encoder)
.with_context(|| format!("Failed to compress {}", path.display()))?; .with_context(|| format!("Failed to compress {}", path.display()))?;
+66
View File
@@ -2,6 +2,9 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::io::{self, IsTerminal, Write}; use std::io::{self, IsTerminal, Write};
use std::sync::atomic::{AtomicBool, Ordering};
static ASSUME_YES: AtomicBool = AtomicBool::new(false);
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum Stream { 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> { 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" }; let default_hint = if default_yes { "Y/n" } else { "y/N" };
loop { loop {
print!( 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)] #[cfg(test)]
mod tests { mod tests {
use super::parse_yes_no_input; use super::parse_yes_no_input;
+38
View File
@@ -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"
+21
View File
@@ -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"