Implement update check functionality with version comparison and archive listing

- Added `check.rs` to handle package update checks, including logic for determining available updates from remote git repositories and archive listings.
- Introduced `versions.rs` to manage version patterns, comparison logic, and extraction of candidate versions from git refs and archive listings.
- Implemented `hex.rs` for encoding byte arrays to lowercase hexadecimal strings.
This commit is contained in:
2026-03-29 12:09:44 -05:00
parent 192cdc1af2
commit 57a364aa97
41 changed files with 8304 additions and 7267 deletions
Generated
+105 -30
View File
@@ -16,7 +16,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cipher", "cipher",
"cpufeatures", "cpufeatures 0.2.17",
] ]
[[package]] [[package]]
@@ -168,6 +168,15 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "block-buffer"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
dependencies = [
"hybrid-array",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.2"
@@ -230,7 +239,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [ dependencies = [
"crypto-common", "crypto-common 0.1.7",
"inout", "inout",
] ]
@@ -285,9 +294,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]] [[package]]
name = "clap_mangen" name = "clap_mangen"
version = "0.2.33" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07"
dependencies = [ dependencies = [
"clap", "clap",
"roff", "roff",
@@ -312,6 +321,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.1.5" version = "0.1.5"
@@ -358,6 +373,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -404,6 +428,15 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "crypto-common"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
dependencies = [
"hybrid-array",
]
[[package]] [[package]]
name = "ct-codecs" name = "ct-codecs"
version = "1.1.6" version = "1.1.6"
@@ -418,7 +451,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]] [[package]]
name = "depot" name = "depot"
version = "0.33.1" version = "0.34.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ar", "ar",
@@ -444,8 +477,8 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_ignored", "serde_ignored",
"sha1", "sha1 0.11.0",
"sha2", "sha2 0.11.0",
"signal-hook 0.4.3", "signal-hook 0.4.3",
"suppaftp", "suppaftp",
"sys-mount", "sys-mount",
@@ -499,11 +532,22 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer 0.10.4",
"crypto-common", "crypto-common 0.1.7",
"subtle", "subtle",
] ]
[[package]]
name = "digest"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
dependencies = [
"block-buffer 0.12.0",
"const-oid",
"crypto-common 0.2.1",
]
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@@ -838,7 +882,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [ dependencies = [
"digest", "digest 0.10.7",
] ]
[[package]] [[package]]
@@ -880,6 +924,15 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hybrid-array"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1"
dependencies = [
"typenum",
]
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.8.1" version = "1.8.1"
@@ -1318,7 +1371,7 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae"
dependencies = [ dependencies = [
"sha2", "sha2 0.10.9",
] ]
[[package]] [[package]]
@@ -1519,7 +1572,7 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [ dependencies = [
"digest", "digest 0.10.7",
"hmac", "hmac",
] ]
@@ -1832,7 +1885,7 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [ dependencies = [
"pbkdf2", "pbkdf2",
"salsa20", "salsa20",
"sha2", "sha2 0.10.9",
] ]
[[package]] [[package]]
@@ -1919,9 +1972,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.0.4" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
@@ -1933,8 +1986,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures 0.2.17",
"digest", "digest 0.10.7",
]
[[package]]
name = "sha1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.2",
] ]
[[package]] [[package]]
@@ -1944,8 +2008,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures 0.2.17",
"digest", "digest 0.10.7",
]
[[package]]
name = "sha2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.2",
] ]
[[package]] [[package]]
@@ -2262,9 +2337,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "1.0.6+spec-1.1.0" version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde_core", "serde_core",
@@ -2277,27 +2352,27 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.0.0+spec-1.1.0" version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.9+spec-1.1.0" version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
dependencies = [ dependencies = [
"winnow", "winnow",
] ]
[[package]] [[package]]
name = "toml_writer" name = "toml_writer"
version = "1.0.6+spec-1.1.0" version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
[[package]] [[package]]
name = "tower" name = "tower"
@@ -2878,9 +2953,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.14" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
@@ -3108,7 +3183,7 @@ dependencies = [
"memchr", "memchr",
"pbkdf2", "pbkdf2",
"ppmd-rust", "ppmd-rust",
"sha1", "sha1 0.10.6",
"time", "time",
"typed-path", "typed-path",
"zeroize", "zeroize",
+5 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "depot" name = "depot"
version = "0.33.1" version = "0.34.0"
edition = "2024" edition = "2024"
[lints.rust] [lints.rust]
@@ -19,11 +19,11 @@ rusqlite = "0.39.0"
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.11.0"
tar = "0.4.45" tar = "0.4.45"
tempfile = "=3.27.0" tempfile = "=3.27.0"
thiserror = "2.0.18" thiserror = "2.0.18"
toml = "1.0.6" toml = "1.1.0+spec-1.1.0"
url = "2.5.8" url = "2.5.8"
walkdir = "2.5.0" walkdir = "2.5.0"
xz2 = "0.1.7" xz2 = "0.1.7"
@@ -38,7 +38,7 @@ fd-lock = "4.0.4"
reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "native-tls"] } reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "native-tls"] }
filetime = "0.2.27" filetime = "0.2.27"
clap_complete = "4.6.0" clap_complete = "4.6.0"
clap_mangen = "0.2.33" clap_mangen = "0.3.0"
sys-mount = { version = "3.1.0", default-features = false } sys-mount = { version = "3.1.0", default-features = false }
time = { version = "0.3.47", features = ["formatting", "parsing"] } time = { version = "0.3.47", features = ["formatting", "parsing"] }
b2sum-rust = "0.3.0" b2sum-rust = "0.3.0"
@@ -46,7 +46,7 @@ serde_ignored = "0.1.14"
lz4_flex = "0.13.0" lz4_flex = "0.13.0"
lzma-rust2 = "0.16.2" lzma-rust2 = "0.16.2"
signal-hook = "0.4.3" signal-hook = "0.4.3"
sha1 = "0.10.6" sha1 = "0.11.0"
[dev-dependencies] [dev-dependencies]
tempfile = "=3.27.0" tempfile = "=3.27.0"
+20
View File
@@ -0,0 +1,20 @@
const BUILD_OPTION_KEYS: &[&str] = &[
"BUILD_DEPOT_STATIC",
"DEPOT_AUTOTOOLS_PACKAGE",
"DEPOT_CMAKE_PACKAGE",
"DEPOT_MESON_PACKAGE",
"DEPOT_PERL_PACKAGE",
"DEPOT_CUSTOM_PACKAGE",
"DEPOT_PYTHON_PACKAGE",
"DEPOT_RUST_PACKAGE",
"DEPOT_MAKEFILE_PACKAGE",
];
fn main() {
for key in BUILD_OPTION_KEYS {
println!("cargo:rerun-if-env-changed={key}");
if let Ok(value) = std::env::var(key) {
println!("cargo:rustc-env={key}={value}");
}
}
}
+28 -1
View File
@@ -1,6 +1,6 @@
project( project(
'depot', 'depot',
version: '0.33.1', version: '0.34.0',
meson_version: '>=0.60.0', meson_version: '>=0.60.0',
default_options: ['buildtype=release'], default_options: ['buildtype=release'],
) )
@@ -12,6 +12,15 @@ find_prog = find_program('find', required: true)
sh = find_program('sh', required: true) sh = find_program('sh', required: true)
build_html_doc = get_option('build-html-doc') build_html_doc = get_option('build-html-doc')
cli_doc = find_program('cli_doc', required: build_html_doc) cli_doc = find_program('cli_doc', required: build_html_doc)
build_depot_static = get_option('BUILD_DEPOT_STATIC')
depot_autotools_package = get_option('DEPOT_AUTOTOOLS_PACKAGE')
depot_cmake_package = get_option('DEPOT_CMAKE_PACKAGE')
depot_meson_package = get_option('DEPOT_MESON_PACKAGE')
depot_perl_package = get_option('DEPOT_PERL_PACKAGE')
depot_custom_package = get_option('DEPOT_CUSTOM_PACKAGE')
depot_python_package = get_option('DEPOT_PYTHON_PACKAGE')
depot_rust_package = get_option('DEPOT_RUST_PACKAGE')
depot_makefile_package = get_option('DEPOT_MAKEFILE_PACKAGE')
src_root = meson.project_source_root() src_root = meson.project_source_root()
build_root = meson.project_build_root() build_root = meson.project_build_root()
@@ -66,6 +75,15 @@ depot_bin = custom_target(
build_root, build_root,
cargo_profile, cargo_profile,
cargo_release ? '1' : '0', cargo_release ? '1' : '0',
build_depot_static ? 'true' : 'false',
depot_autotools_package,
depot_cmake_package,
depot_meson_package,
depot_perl_package,
depot_custom_package,
depot_python_package,
depot_rust_package,
depot_makefile_package,
'@OUTPUT@', '@OUTPUT@',
], ],
console: true, console: true,
@@ -101,6 +119,15 @@ test(
build_root, build_root,
cargo_profile, cargo_profile,
cargo_release ? '1' : '0', cargo_release ? '1' : '0',
build_depot_static ? 'true' : 'false',
depot_autotools_package,
depot_cmake_package,
depot_meson_package,
depot_perl_package,
depot_custom_package,
depot_python_package,
depot_rust_package,
depot_makefile_package,
], ],
suite: ['cargo'], suite: ['cargo'],
timeout: 0, timeout: 0,
+54
View File
@@ -4,3 +4,57 @@ option(
value: false, value: false,
description: 'Generate HTML CLI documentation with cli_doc and install it', description: 'Generate HTML CLI documentation with cli_doc and install it',
) )
option(
'BUILD_DEPOT_STATIC',
type: 'boolean',
value: false,
description: 'Compile Depot with a default static-build preference for supported package builders',
)
option(
'DEPOT_AUTOTOOLS_PACKAGE',
type: 'string',
value: '',
description: 'Compile in the helper package to auto-install before autotools builds',
)
option(
'DEPOT_CMAKE_PACKAGE',
type: 'string',
value: '',
description: 'Compile in the helper package to auto-install before CMake builds',
)
option(
'DEPOT_MESON_PACKAGE',
type: 'string',
value: '',
description: 'Compile in the helper package to auto-install before Meson builds',
)
option(
'DEPOT_PERL_PACKAGE',
type: 'string',
value: '',
description: 'Compile in the helper package to auto-install before Perl builds',
)
option(
'DEPOT_CUSTOM_PACKAGE',
type: 'string',
value: '',
description: 'Compile in the helper package to auto-install before custom builds',
)
option(
'DEPOT_PYTHON_PACKAGE',
type: 'string',
value: '',
description: 'Compile in the helper package to auto-install before Python builds',
)
option(
'DEPOT_RUST_PACKAGE',
type: 'string',
value: '',
description: 'Compile in the helper package to auto-install before Rust builds',
)
option(
'DEPOT_MAKEFILE_PACKAGE',
type: 'string',
value: '',
description: 'Compile in the helper package to auto-install before makefile builds',
)
+102
View File
@@ -0,0 +1,102 @@
use crate::package::BuildType;
use anyhow::Result;
const BUILD_DEPOT_STATIC_OPTION: &str = "BUILD_DEPOT_STATIC";
fn parse_boolish_option(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" | "enable" | "enabled" | "static" => Some(true),
"0" | "false" | "no" | "off" | "disable" | "disabled" | "shared" => Some(false),
_ => None,
}
}
fn normalize_string_option(value: Option<&'static str>) -> Option<String> {
value.and_then(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
})
}
pub(crate) fn requested_static_build() -> Result<Option<bool>> {
let Some(raw) = option_env!("BUILD_DEPOT_STATIC") else {
return Ok(None);
};
let raw = raw.trim();
if raw.is_empty() {
return Ok(None);
}
parse_boolish_option(raw).ok_or_else(|| {
anyhow::anyhow!(
"Invalid -D{} value '{}'; expected true/false, on/off, enable/disable, static/shared, or 1/0",
BUILD_DEPOT_STATIC_OPTION,
raw
)
}).map(Some)
}
pub(crate) fn build_tool_package_option(build_type: BuildType) -> Option<&'static str> {
match build_type {
BuildType::Autotools => Some("DEPOT_AUTOTOOLS_PACKAGE"),
BuildType::CMake => Some("DEPOT_CMAKE_PACKAGE"),
BuildType::Meson => Some("DEPOT_MESON_PACKAGE"),
BuildType::Perl => Some("DEPOT_PERL_PACKAGE"),
BuildType::Custom => Some("DEPOT_CUSTOM_PACKAGE"),
BuildType::Python => Some("DEPOT_PYTHON_PACKAGE"),
BuildType::Rust => Some("DEPOT_RUST_PACKAGE"),
BuildType::Makefile => Some("DEPOT_MAKEFILE_PACKAGE"),
BuildType::Bin | BuildType::Meta => None,
}
}
pub(crate) fn requested_build_tool_package(build_type: BuildType) -> Option<String> {
match build_type {
BuildType::Autotools => normalize_string_option(option_env!("DEPOT_AUTOTOOLS_PACKAGE")),
BuildType::CMake => normalize_string_option(option_env!("DEPOT_CMAKE_PACKAGE")),
BuildType::Meson => normalize_string_option(option_env!("DEPOT_MESON_PACKAGE")),
BuildType::Perl => normalize_string_option(option_env!("DEPOT_PERL_PACKAGE")),
BuildType::Custom => normalize_string_option(option_env!("DEPOT_CUSTOM_PACKAGE")),
BuildType::Python => normalize_string_option(option_env!("DEPOT_PYTHON_PACKAGE")),
BuildType::Rust => normalize_string_option(option_env!("DEPOT_RUST_PACKAGE")),
BuildType::Makefile => normalize_string_option(option_env!("DEPOT_MAKEFILE_PACKAGE")),
BuildType::Bin | BuildType::Meta => None,
}
}
#[cfg(test)]
mod tests {
use super::{build_tool_package_option, normalize_string_option, parse_boolish_option};
use crate::package::BuildType;
#[test]
fn parse_boolish_option_accepts_expected_values() {
assert_eq!(parse_boolish_option("true"), Some(true));
assert_eq!(parse_boolish_option("enable"), Some(true));
assert_eq!(parse_boolish_option("shared"), Some(false));
assert_eq!(parse_boolish_option("off"), Some(false));
assert_eq!(parse_boolish_option("maybe"), None);
}
#[test]
fn normalize_string_option_trims_and_drops_empty_values() {
assert_eq!(
normalize_string_option(Some(" meson-bootstrap ")),
Some("meson-bootstrap".to_string())
);
assert_eq!(normalize_string_option(Some(" ")), None);
assert_eq!(normalize_string_option(None), None);
}
#[test]
fn build_tool_package_option_maps_supported_builders() {
assert_eq!(
build_tool_package_option(BuildType::Meson),
Some("DEPOT_MESON_PACKAGE")
);
assert_eq!(
build_tool_package_option(BuildType::CMake),
Some("DEPOT_CMAKE_PACKAGE")
);
assert_eq!(build_tool_package_option(BuildType::Bin), None);
}
}
+6
View File
@@ -134,6 +134,9 @@ pub fn build(
let expanded = expand_configure_arg(spec, arg, &env_vars); let expanded = expand_configure_arg(spec, arg, &env_vars);
configure_cmd.arg(expanded); configure_cmd.arg(expanded);
} }
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Autotools)? {
configure_cmd.arg(arg);
}
let status = crate::interrupts::command_status(&mut configure_cmd) let status = crate::interrupts::command_status(&mut configure_cmd)
.with_context(|| format!("Failed to run configure in {}", build_dir.display()))?; .with_context(|| format!("Failed to run configure in {}", build_dir.display()))?;
@@ -441,6 +444,9 @@ pub(crate) fn ensure_host_build(
let expanded = expand_configure_arg(&host_spec, arg, &env_vars); let expanded = expand_configure_arg(&host_spec, arg, &env_vars);
configure_cmd.arg(expanded); configure_cmd.arg(expanded);
} }
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Autotools)? {
configure_cmd.arg(arg);
}
let status = crate::interrupts::command_status(&mut configure_cmd) let status = crate::interrupts::command_status(&mut configure_cmd)
.with_context(|| format!("Failed to run host configure in {}", build_dir.display()))?; .with_context(|| format!("Failed to run host configure in {}", build_dir.display()))?;
+151 -45
View File
@@ -44,8 +44,7 @@ pub fn build(
} }
// Extract prefix from configure flags (cmake-style -DCMAKE_INSTALL_PREFIX=) // Extract prefix from configure flags (cmake-style -DCMAKE_INSTALL_PREFIX=)
let prefix = cmake_cache_entry_value(&flags.configure, "CMAKE_INSTALL_PREFIX") let prefix = effective_cmake_install_prefix(flags);
.unwrap_or(flags.prefix.as_str());
// Generate toolchain file if cross-compiling // Generate toolchain file if cross-compiling
let toolchain_file = if let Some(cc_cfg) = cross { let toolchain_file = if let Some(cc_cfg) = cross {
@@ -68,7 +67,7 @@ pub fn build(
cmake_cmd.arg("-B").arg(&build_dir); cmake_cmd.arg("-B").arg(&build_dir);
cmake_cmd.arg(format!("-DCMAKE_INSTALL_PREFIX={}", prefix)); cmake_cmd.arg(format!("-DCMAKE_INSTALL_PREFIX={}", prefix));
cmake_cmd.arg("-DCMAKE_BUILD_TYPE=Release"); cmake_cmd.arg("-DCMAKE_BUILD_TYPE=Release");
for arg in cmake_install_dir_args(flags) { for arg in cmake_install_dir_args(flags, prefix) {
cmake_cmd.arg(arg); cmake_cmd.arg(arg);
} }
for arg in cmake_lib32_target_args(flags, cross) { for arg in cmake_lib32_target_args(flags, cross) {
@@ -98,6 +97,9 @@ pub fn build(
let expanded = expand_with_envs(flag, &env_vars); let expanded = expand_with_envs(flag, &env_vars);
cmake_cmd.arg(&expanded); cmake_cmd.arg(&expanded);
} }
for arg in crate::builder::static_build_args_for(crate::package::BuildType::CMake)? {
cmake_cmd.arg(arg);
}
crate::builder::prepare_tool_command(&mut cmake_cmd, &env_vars); crate::builder::prepare_tool_command(&mut cmake_cmd, &env_vars);
@@ -280,8 +282,7 @@ pub(crate) fn ensure_host_build(
let env_vars = let env_vars =
crate::builder::standard_build_env(&host_spec, None, true, export_compiler_flags); crate::builder::standard_build_env(&host_spec, None, true, export_compiler_flags);
let prefix = cmake_cache_entry_value(&flags.configure, "CMAKE_INSTALL_PREFIX") let prefix = effective_cmake_install_prefix(flags);
.unwrap_or(flags.prefix.as_str());
let mut state = StateTracker::new_with_namespace(&actual_src, Some("host"))?; let mut state = StateTracker::new_with_namespace(&actual_src, Some("host"))?;
@@ -296,7 +297,7 @@ pub(crate) fn ensure_host_build(
cmake_cmd.arg("-B").arg(&build_dir); cmake_cmd.arg("-B").arg(&build_dir);
cmake_cmd.arg(format!("-DCMAKE_INSTALL_PREFIX={}", prefix)); cmake_cmd.arg(format!("-DCMAKE_INSTALL_PREFIX={}", prefix));
cmake_cmd.arg("-DCMAKE_BUILD_TYPE=Release"); cmake_cmd.arg("-DCMAKE_BUILD_TYPE=Release");
for arg in cmake_install_dir_args(flags) { for arg in cmake_install_dir_args(flags, prefix) {
cmake_cmd.arg(arg); cmake_cmd.arg(arg);
} }
@@ -316,6 +317,9 @@ pub(crate) fn ensure_host_build(
let expanded = expand_with_envs(flag, &env_vars); let expanded = expand_with_envs(flag, &env_vars);
cmake_cmd.arg(&expanded); cmake_cmd.arg(&expanded);
} }
for arg in crate::builder::static_build_args_for(crate::package::BuildType::CMake)? {
cmake_cmd.arg(arg);
}
crate::builder::prepare_tool_command(&mut cmake_cmd, &env_vars); crate::builder::prepare_tool_command(&mut cmake_cmd, &env_vars);
@@ -446,21 +450,72 @@ fn cmake_cache_entry_value<'a>(flags: &'a [String], variable: &str) -> Option<&'
}) })
} }
fn cmake_install_dir_args(flags: &crate::package::BuildFlags) -> Vec<String> { fn effective_cmake_install_prefix(flags: &crate::package::BuildFlags) -> &str {
cmake_cache_entry_value(&flags.configure, "CMAKE_INSTALL_PREFIX")
.unwrap_or(flags.prefix.as_str())
}
fn cmake_dir_value_for_prefix(prefix: &str, value: String) -> String {
let prefix_path = Path::new(prefix);
let value_path = Path::new(&value);
if prefix_path.is_absolute()
&& value_path.is_absolute()
&& let Ok(relative) = value_path.strip_prefix(prefix_path)
{
let relative = relative.to_string_lossy().replace('\\', "/");
return if relative.is_empty() {
".".to_string()
} else {
relative
};
}
value
}
fn cmake_install_dir_args(flags: &crate::package::BuildFlags, prefix: &str) -> Vec<String> {
let dirs = crate::builder::install_dirs(flags); let dirs = crate::builder::install_dirs(flags);
let defaults = [ let defaults = [
("CMAKE_INSTALL_BINDIR", dirs.bindir), (
("CMAKE_INSTALL_SBINDIR", dirs.sbindir), "CMAKE_INSTALL_BINDIR",
("CMAKE_INSTALL_LIBDIR", dirs.libdir), cmake_dir_value_for_prefix(prefix, dirs.bindir),
("CMAKE_INSTALL_LIBEXECDIR", dirs.libexecdir), ),
(
"CMAKE_INSTALL_SBINDIR",
cmake_dir_value_for_prefix(prefix, dirs.sbindir),
),
(
"CMAKE_INSTALL_LIBDIR",
cmake_dir_value_for_prefix(prefix, dirs.libdir),
),
(
"CMAKE_INSTALL_LIBEXECDIR",
cmake_dir_value_for_prefix(prefix, dirs.libexecdir),
),
("CMAKE_INSTALL_SYSCONFDIR", dirs.sysconfdir), ("CMAKE_INSTALL_SYSCONFDIR", dirs.sysconfdir),
("CMAKE_INSTALL_LOCALSTATEDIR", dirs.localstatedir), ("CMAKE_INSTALL_LOCALSTATEDIR", dirs.localstatedir),
("CMAKE_INSTALL_SHAREDSTATEDIR", dirs.sharedstatedir), ("CMAKE_INSTALL_SHAREDSTATEDIR", dirs.sharedstatedir),
("CMAKE_INSTALL_INCLUDEDIR", dirs.includedir), (
("CMAKE_INSTALL_DATAROOTDIR", dirs.datarootdir), "CMAKE_INSTALL_INCLUDEDIR",
("CMAKE_INSTALL_DATADIR", dirs.datadir), cmake_dir_value_for_prefix(prefix, dirs.includedir),
("CMAKE_INSTALL_MANDIR", dirs.mandir), ),
("CMAKE_INSTALL_INFODIR", dirs.infodir), (
"CMAKE_INSTALL_DATAROOTDIR",
cmake_dir_value_for_prefix(prefix, dirs.datarootdir),
),
(
"CMAKE_INSTALL_DATADIR",
cmake_dir_value_for_prefix(prefix, dirs.datadir),
),
(
"CMAKE_INSTALL_MANDIR",
cmake_dir_value_for_prefix(prefix, dirs.mandir),
),
(
"CMAKE_INSTALL_INFODIR",
cmake_dir_value_for_prefix(prefix, dirs.infodir),
),
]; ];
defaults defaults
@@ -710,15 +765,41 @@ mod tests {
} }
#[test] #[test]
fn test_cmake_install_dir_args_include_defaults() { fn test_effective_cmake_install_prefix_prefers_explicit_configure_flag() {
let args = cmake_install_dir_args(&BuildFlags::default()); let flags = BuildFlags {
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=/usr/bin")); prefix: "/usr".into(),
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SBINDIR=/usr/bin")); configure: vec!["-DCMAKE_INSTALL_PREFIX=/opt/soundtouch".into()],
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=/usr/lib")); ..BuildFlags::default()
assert!( };
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=/usr/lib") assert_eq!(effective_cmake_install_prefix(&flags), "/opt/soundtouch");
}
#[test]
fn test_cmake_dir_value_for_prefix_converts_prefix_owned_absolute_paths() {
assert_eq!(cmake_dir_value_for_prefix("/usr", "/usr/lib".into()), "lib");
assert_eq!(
cmake_dir_value_for_prefix("/usr", "/usr/share/man".into()),
"share/man"
); );
}
#[test]
fn test_cmake_dir_value_for_prefix_keeps_non_prefix_absolute_paths() {
assert_eq!(cmake_dir_value_for_prefix("/usr", "/etc".into()), "/etc");
assert_eq!(
cmake_dir_value_for_prefix("/opt/pkg", "/usr/bin".into()),
"/usr/bin"
);
}
#[test]
fn test_cmake_install_dir_args_include_prefix_relative_defaults() {
let args = cmake_install_dir_args(&BuildFlags::default(), "/usr");
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SBINDIR=bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=lib"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SYSCONFDIR=/etc")); assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SYSCONFDIR=/etc"));
assert!( assert!(
args.iter() args.iter()
@@ -730,23 +811,17 @@ mod tests {
); );
assert!( assert!(
args.iter() args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=/usr/include") .any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=include")
); );
assert!( assert!(
args.iter() args.iter()
.any(|a| a == "-DCMAKE_INSTALL_DATAROOTDIR=/usr/share") .any(|a| a == "-DCMAKE_INSTALL_DATAROOTDIR=share")
); );
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_DATADIR=share"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_MANDIR=share/man"));
assert!( assert!(
args.iter() args.iter()
.any(|a| a == "-DCMAKE_INSTALL_DATADIR=/usr/share") .any(|a| a == "-DCMAKE_INSTALL_INFODIR=share/info")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_MANDIR=/usr/share/man")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INFODIR=/usr/share/info")
); );
} }
@@ -757,15 +832,9 @@ mod tests {
..BuildFlags::default() ..BuildFlags::default()
}; };
let args = cmake_install_dir_args(&flags); let args = cmake_install_dir_args(&flags, "/usr");
assert!( assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib32"));
args.iter() assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=lib32"));
.any(|a| a == "-DCMAKE_INSTALL_LIBDIR=/usr/lib32")
);
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=/usr/lib32")
);
} }
#[test] #[test]
@@ -832,7 +901,7 @@ mod tests {
..BuildFlags::default() ..BuildFlags::default()
}; };
let args = cmake_install_dir_args(&flags); let args = cmake_install_dir_args(&flags, "/usr");
assert!( assert!(
!args !args
.iter() .iter()
@@ -848,7 +917,44 @@ mod tests {
.iter() .iter()
.any(|a| a.starts_with("-DCMAKE_INSTALL_DATADIR=")) .any(|a| a.starts_with("-DCMAKE_INSTALL_DATADIR="))
); );
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin"));
}
#[test]
fn test_cmake_install_dir_args_convert_custom_prefix_owned_dirs() {
let flags = BuildFlags {
prefix: "/opt/soundtouch".into(),
bindir: "/opt/soundtouch/bin".into(),
libdir: "/opt/soundtouch/lib64".into(),
includedir: "/opt/soundtouch/include/soundtouch".into(),
datadir: "/opt/soundtouch/share".into(),
mandir: "/opt/soundtouch/share/man".into(),
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, effective_cmake_install_prefix(&flags));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib64"));
assert!(
args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=include/soundtouch")
);
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_DATADIR=share"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_MANDIR=share/man"));
}
#[test]
fn test_cmake_install_dir_args_keep_absolute_dirs_outside_prefix() {
let flags = BuildFlags {
prefix: "/opt/soundtouch".into(),
bindir: "/usr/bin".into(),
sysconfdir: "/etc".into(),
..BuildFlags::default()
};
let args = cmake_install_dir_args(&flags, effective_cmake_install_prefix(&flags));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=/usr/bin")); assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=/usr/bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SYSCONFDIR=/etc"));
} }
#[test] #[test]
+6
View File
@@ -65,6 +65,9 @@ pub fn build(
for arg in meson_setup_args(flags, cross_file.as_deref(), &env_vars) { for arg in meson_setup_args(flags, cross_file.as_deref(), &env_vars) {
meson_cmd.arg(arg); meson_cmd.arg(arg);
} }
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Meson)? {
meson_cmd.arg(arg);
}
crate::builder::prepare_tool_command(&mut meson_cmd, &env_vars); crate::builder::prepare_tool_command(&mut meson_cmd, &env_vars);
@@ -229,6 +232,9 @@ pub(crate) fn ensure_host_build(
for arg in meson_setup_args(flags, None, &env_vars) { for arg in meson_setup_args(flags, None, &env_vars) {
meson_cmd.arg(arg); meson_cmd.arg(arg);
} }
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Meson)? {
meson_cmd.arg(arg);
}
crate::builder::prepare_tool_command(&mut meson_cmd, &env_vars); crate::builder::prepare_tool_command(&mut meson_cmd, &env_vars);
+54
View File
@@ -184,6 +184,47 @@ pub(crate) fn host_build_spec(spec: &PackageSpec) -> PackageSpec {
host_spec host_spec
} }
pub(crate) fn requested_static_build() -> Result<Option<bool>> {
crate::build_options::requested_static_build()
}
pub(crate) fn static_build_args_for(build_type: BuildType) -> Result<Vec<String>> {
let Some(enabled) = requested_static_build()? else {
return Ok(Vec::new());
};
let args = match build_type {
BuildType::Autotools => vec![if enabled {
"--enable-static".to_string()
} else {
"--disable-static".to_string()
}],
BuildType::CMake => vec![format!(
"-DBUILD_SHARED_LIBS={}",
if enabled { "OFF" } else { "ON" }
)],
BuildType::Meson => vec![format!(
"-Ddefault_library={}",
if enabled { "static" } else { "shared" }
)],
BuildType::Perl => vec![format!(
"LINKTYPE={}",
if enabled { "static" } else { "dynamic" }
)],
_ => Vec::new(),
};
Ok(args)
}
pub(crate) fn build_tool_package_option(build_type: BuildType) -> Option<&'static str> {
crate::build_options::build_tool_package_option(build_type)
}
pub(crate) fn requested_build_tool_package(build_type: BuildType) -> Option<String> {
crate::build_options::requested_build_tool_package(build_type)
}
fn configured_install_dir(value: &str, default: &str) -> String { fn configured_install_dir(value: &str, default: &str) -> String {
let trimmed = value.trim(); let trimmed = value.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
@@ -889,6 +930,19 @@ mod tests {
); );
} }
#[test]
fn test_build_tool_package_option_maps_supported_builders() {
assert_eq!(
build_tool_package_option(BuildType::Meson),
Some("DEPOT_MESON_PACKAGE")
);
assert_eq!(
build_tool_package_option(BuildType::CMake),
Some("DEPOT_CMAKE_PACKAGE")
);
assert_eq!(build_tool_package_option(BuildType::Bin), None);
}
#[test] #[test]
fn test_standard_build_env_exports_native_linker_and_cpp() { fn test_standard_build_env_exports_native_linker_and_cpp() {
let mut spec = mk_spec(Vec::new(), Vec::new()); let mut spec = mk_spec(Vec::new(), Vec::new());
+3
View File
@@ -59,6 +59,9 @@ pub fn build(
for arg in &flags.configure { for arg in &flags.configure {
configure_cmd.arg(spec.expand_vars(arg)); configure_cmd.arg(spec.expand_vars(arg));
} }
for arg in crate::builder::static_build_args_for(crate::package::BuildType::Perl)? {
configure_cmd.arg(arg);
}
crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars); crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars);
let status = command_status_with_sh_fallback(&mut configure_cmd).with_context(|| { let status = command_status_with_sh_fallback(&mut configure_cmd).with_context(|| {
+70 -7038
View File
File diff suppressed because it is too large Load Diff
+533
View File
@@ -0,0 +1,533 @@
use super::*;
pub(crate) mod support;
use self::support::{
RequestedBuildToolPackageInstall, automatic_tests_disabled_for_outputs,
build_lib32_companion_package, clean_build_workspace, effective_lib32_only,
ensure_requested_build_tool_package_installed, maybe_disable_tests_for_missing_deps,
maybe_prompt_to_skip_tests_for_missing_requested_deps, merge_missing_dependencies,
requested_outputs, should_install_test_deps, warn_if_running_as_root_for_build,
};
pub(super) fn run_build(args: BuildArgs, cli_test_deps: bool) -> Result<()> {
let BuildArgs {
rootfs_args,
prompt_args,
build_exec_args,
lib32_args,
spec_pos,
spec,
install,
install_deps,
cleanup_deps,
} = args;
let rootfs = rootfs_args.rootfs;
let yes = prompt_args.yes;
let no_deps = build_exec_args.no_deps;
let no_flags = build_exec_args.no_flags;
let cross_prefix = build_exec_args.cross_prefix;
let clean = build_exec_args.clean;
let dry_run = build_exec_args.dry_run;
let cli_lib32_only = lib32_args.lib32_only;
warn_if_running_as_root_for_build("build", &rootfs);
let config = config::Config::for_rootfs(&rootfs);
ensure_depot_self_update_not_required(&config, &rootfs)?;
let spec_path = spec.or(spec_pos).context("No spec file provided")?;
ui::info(format!("Building package from: {}", spec_path.display()));
let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?;
let install_test_deps = install_test_deps_enabled(cli_test_deps, &config);
let interrupt_watcher = if cleanup_deps {
Some(InterruptWatcher::install()?)
} else {
None
};
let mut auto_installed_deps = AutoInstalledDependencyTracker::default();
let build_result: Result<()> = (|| {
pkg_spec.apply_config(&config);
let lib32_only = effective_lib32_only(&pkg_spec, cli_lib32_only);
let requested_outputs = requested_outputs(&pkg_spec, lib32_only);
let build_targets = vec![format!(
"{} v{}-{}",
pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision
)];
if !ui::prompt_package_action("build", &build_targets, true)? {
anyhow::bail!("Aborted");
}
std::fs::create_dir_all(&config.db_dir).with_context(|| {
format!(
"Failed to create database directory: {}",
config.db_dir.display()
)
})?;
let db_path = config.installed_db_path(&rootfs);
if no_deps && should_install_test_deps(&pkg_spec, install_test_deps, requested_outputs) {
let missing_test =
deps::check_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
if !missing_test.is_empty()
&& !maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut pkg_spec,
&missing_test,
"Requested test dependencies are missing",
)?
{
anyhow::bail!("Missing test dependencies: {}", missing_test.join(", "));
}
} else if no_deps
|| !should_install_test_deps(&pkg_spec, install_test_deps, requested_outputs)
{
maybe_disable_tests_for_missing_deps(&mut pkg_spec, &db_path, requested_outputs)?;
}
if !no_deps {
deps::print_dep_status_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
let missing_build =
deps::check_build_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
let missing_runtime =
deps::check_runtime_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
let missing_required =
merge_missing_dependencies(missing_build.clone(), missing_runtime.clone());
if !missing_required.is_empty() {
ui::warn(format!(
"Missing dependencies: {}",
missing_required.join(", ")
));
if !install_deps {
if dry_run {
ui::info("Dry run enabled, stopping before dependency installation/build.");
return Ok(());
}
anyhow::bail!(
"Missing dependencies: {}. Re-run with --install-deps to install them automatically, or install them manually.",
missing_required.join(", ")
);
}
let local_sibling_root = spec_path
.parent()
.and_then(|p| p.parent())
.map(Path::to_path_buf);
let dep_plan = planner::build_dependency_install_plan(
&config,
&rootfs,
&missing_required,
planner::PlannerOptions {
assume_yes: yes,
prefer_binary: config.repo_settings.prefer_binary,
local_sibling_root,
include_test_deps: install_test_deps,
lib32_only_requested_specs: false,
},
)?;
if cleanup_deps {
auto_installed_deps.record_plan(
&dep_plan,
&missing_build,
AutoInstalledDependencyKind::Build,
);
auto_installed_deps.record_plan(
&dep_plan,
&missing_runtime,
AutoInstalledDependencyKind::Runtime,
);
}
print_plan_summary(&dep_plan);
if dry_run {
ui::info("Dry run enabled, stopping before dependency installation/build.");
return Ok(());
}
execute_install_plan_with_child_commands(
&dep_plan,
&rootfs,
&config,
InstallPlanExecutionOptions {
no_flags,
cross_prefix: cross_prefix.as_deref(),
clean,
dry_run,
confirm_installation: false,
lib32_only_requested_specs: false,
install_test_deps,
},
)?;
}
deps::require_build_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
deps::require_runtime_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
if should_install_test_deps(&pkg_spec, install_test_deps, requested_outputs) {
let missing_test =
deps::check_test_deps_for_outputs(&pkg_spec, &db_path, requested_outputs)?;
if !missing_test.is_empty() {
let local_sibling_root = spec_path
.parent()
.and_then(|p| p.parent())
.map(Path::to_path_buf);
let dep_plan = match planner::build_dependency_install_plan(
&config,
&rootfs,
&missing_test,
planner::PlannerOptions {
assume_yes: yes,
prefer_binary: config.repo_settings.prefer_binary,
local_sibling_root,
include_test_deps: install_test_deps,
lib32_only_requested_specs: false,
},
) {
Ok(plan) => plan,
Err(_err)
if maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut pkg_spec,
&missing_test,
"Requested test dependencies could not be resolved",
)? =>
{
planner::ExecutionPlan { steps: Vec::new() }
}
Err(err) => return Err(err),
};
if cleanup_deps
&& !automatic_tests_disabled_for_outputs(&pkg_spec, requested_outputs)
{
auto_installed_deps.record_plan(
&dep_plan,
&missing_test,
AutoInstalledDependencyKind::Test,
);
}
if !automatic_tests_disabled_for_outputs(&pkg_spec, requested_outputs)
&& !dep_plan.steps.is_empty()
{
ui::warn(format!(
"Missing test dependencies: {}",
missing_test.join(", ")
));
print_plan_summary(&dep_plan);
if !install_deps {
if dry_run {
ui::info(
"Dry run enabled, stopping before dependency installation/build.",
);
return Ok(());
}
if !maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut pkg_spec,
&missing_test,
"Requested test dependencies were not installed",
)? {
anyhow::bail!(
"Missing test dependencies: {}. Re-run with --install-deps to install them automatically, or install them manually.",
missing_test.join(", ")
);
}
} else if !dry_run {
execute_install_plan_with_child_commands(
&dep_plan,
&rootfs,
&config,
InstallPlanExecutionOptions {
no_flags,
cross_prefix: cross_prefix.as_deref(),
clean,
dry_run,
confirm_installation: false,
lib32_only_requested_specs: false,
install_test_deps,
},
)?;
}
}
if should_install_test_deps(&pkg_spec, install_test_deps, requested_outputs) {
let missing_test = deps::check_test_deps_for_outputs(
&pkg_spec,
&db_path,
requested_outputs,
)?;
if !missing_test.is_empty()
&& !maybe_prompt_to_skip_tests_for_missing_requested_deps(
&mut pkg_spec,
&missing_test,
"Requested test dependencies are still missing",
)?
{
deps::require_test_deps_for_outputs(
&pkg_spec,
&db_path,
requested_outputs,
)?;
}
}
}
}
}
ensure_requested_build_tool_package_installed(RequestedBuildToolPackageInstall {
build_type: pkg_spec.build.build_type,
rootfs: &rootfs,
config: &config,
db_path: &db_path,
spec_path: &spec_path,
execution: InstallPlanExecutionOptions {
no_flags,
cross_prefix: cross_prefix.as_deref(),
clean,
dry_run,
confirm_installation: false,
lib32_only_requested_specs: false,
install_test_deps,
},
assume_yes: yes,
})?;
if dry_run {
ui::info("Dry run enabled, stopping before fetch/build.");
return Ok(());
}
let mut build_lock = locking::open_lock(&config)?;
let build_lock_path = locking::lock_path(&config);
let _build_lock_guard = locking::try_write(&mut build_lock, &build_lock_path, "build")?;
if let Some(watcher) = interrupt_watcher.as_ref() {
watcher.check()?;
}
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
if let Some(watcher) = interrupt_watcher.as_ref() {
watcher.check()?;
}
let destdir = config
.build_dir
.join("destdir")
.join(&pkg_spec.package.name);
let cross_config = cross_prefix
.as_ref()
.map(|p| cross::CrossConfig::from_prefix(p))
.transpose()?;
let host_build_dir = builder::ensure_host_build(
&pkg_spec,
&src_dir,
cross_config.as_ref(),
!no_flags,
builder::TargetBuildKind::Primary,
)?;
if let Some(host_dir) = host_build_dir.as_ref() {
pkg_spec.build.flags.host_build_dir = Some(host_dir.to_string_lossy().into_owned());
}
if !lib32_only {
builder::build(
&pkg_spec,
&src_dir,
&destdir,
cross_config.as_ref(),
!no_flags,
host_build_dir.as_deref(),
)?;
if let Some(watcher) = interrupt_watcher.as_ref() {
watcher.check()?;
}
}
if !lib32_only {
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?;
staging::process(&destdir, &pkg_spec)?;
staging::stage_split_package_licenses(&src_dir, &destdir, &pkg_spec)?;
if let Some(watcher) = interrupt_watcher.as_ref() {
watcher.check()?;
}
}
let arch = cross_prefix.as_deref().unwrap_or(std::env::consts::ARCH);
let mut created_files = Vec::new();
let staged_outputs = if !lib32_only {
staged_output_specs(&pkg_spec, &destdir)?
} else {
Vec::new()
};
if !lib32_only {
for (spec_for_out, out_destdir) in &staged_outputs {
let packager = package::Packager::new(
spec_for_out.clone(),
out_destdir.clone(),
config.clone(),
);
let pkg_file = packager.create_package(Path::new("."), arch)?;
created_files.push(pkg_file);
if let Some(watcher) = interrupt_watcher.as_ref() {
watcher.check()?;
}
}
}
let mut lib32_install_bundle: Option<(package::PackageSpec, PathBuf)> = None;
if let Some((lib32_spec, lib32_destdir)) = build_lib32_companion_package(
&pkg_spec,
&src_dir,
&config,
cross_config.as_ref(),
!no_flags,
lib32_only,
)? {
let packager =
package::Packager::new(lib32_spec.clone(), lib32_destdir.clone(), config.clone());
let pkg_file = packager.create_package(Path::new("."), arch)?;
created_files.push(pkg_file);
lib32_install_bundle = Some((lib32_spec, lib32_destdir));
if let Some(watcher) = interrupt_watcher.as_ref() {
watcher.check()?;
}
}
for f in &created_files {
ui::success(format!("Build complete. Package created: {}", f.display()));
}
for sig_path in signing::auto_sign_zst_files_detached(&rootfs, &created_files)? {
ui::success(format!(
"Created detached signature: {}",
sig_path.display()
));
}
if install {
let mut install_targets = Vec::new();
if !lib32_only {
for (spec_for_out, _) in &staged_outputs {
let out = &spec_for_out.package;
install_targets.push(format!("{} v{}-{}", out.name, out.version, out.revision));
}
}
if let Some((lib32_spec, _)) = &lib32_install_bundle {
install_targets.push(format!(
"{} v{}-{}",
lib32_spec.package.name,
lib32_spec.package.version,
lib32_spec.package.revision
));
}
if ui::prompt_package_action("installation", &install_targets, false)? {
if let Some(watcher) = interrupt_watcher.as_ref() {
watcher.check()?;
}
if should_delegate_live_rootfs_installs(&rootfs) {
run_child_install_command(
&created_files,
&rootfs,
InstallPlanExecutionOptions {
no_flags,
cross_prefix: cross_prefix.as_deref(),
clean,
dry_run,
confirm_installation: false,
lib32_only_requested_specs: false,
install_test_deps,
},
)?;
return Ok(());
}
let mut transaction_plans = Vec::new();
if !lib32_only {
let output_plans =
plan_package_outputs_for_install(&pkg_spec, &destdir, &rootfs, &config)?;
transaction_plans.extend(output_plans);
}
if let Some((lib32_spec, lib32_destdir)) = &lib32_install_bundle {
let staged = plan_staged_install(lib32_spec, lib32_destdir, &rootfs, &config)?;
transaction_plans.push(PlannedPackageInstall {
spec: lib32_spec.clone(),
destdir: lib32_destdir.clone(),
staged,
});
}
run_transaction_hooks_for_plans(
&rootfs,
install::hooks::HookPhase::Pre,
&transaction_plans,
)?;
install_planned_packages_to_rootfs(&transaction_plans, &rootfs, &config)?;
run_transaction_hooks_for_plans(
&rootfs,
install::hooks::HookPhase::Post,
&transaction_plans,
)?;
install::scripts::run_deferred_hooks_if_possible(&rootfs)?;
} else {
if !lib32_only {
for (spec_for_out, _) in &staged_outputs {
let out = &spec_for_out.package;
ui::success(format!(
"Built successfully: {}-{}-{}",
out.name, out.version, out.revision
));
}
}
if let Some((lib32_spec, _)) = &lib32_install_bundle {
ui::success(format!(
"Built successfully: {}-{}-{}",
lib32_spec.package.name,
lib32_spec.package.version,
lib32_spec.package.revision
));
}
}
}
Ok(())
})();
let interrupted = interrupt_watcher
.as_ref()
.is_some_and(InterruptWatcher::was_interrupted);
match build_result {
Ok(()) => {
if cleanup_deps && !auto_installed_deps.is_empty() {
cleanup_auto_installed_dependencies(
&auto_installed_deps,
&rootfs,
&config,
!install,
false,
)?;
}
if clean {
clean_build_workspace(&config)?;
}
}
Err(err) => {
if cleanup_deps && !auto_installed_deps.is_empty() {
if interrupted {
ui::warn("Build interrupted by Ctrl-C.");
}
if let Err(clean_err) = cleanup_auto_installed_dependencies(
&auto_installed_deps,
&rootfs,
&config,
!install,
interrupted,
) {
ui::warn(format!(
"Failed to remove auto-installed dependencies: {}",
clean_err
));
}
}
if interrupted {
anyhow::bail!("Build interrupted by Ctrl-C");
}
return Err(err);
}
}
Ok(())
}
+325
View File
@@ -0,0 +1,325 @@
use super::*;
pub(crate) fn build_type_runs_automatic_tests(spec: &package::PackageSpec) -> bool {
matches!(
spec.build.build_type,
package::BuildType::Autotools
| package::BuildType::CMake
| package::BuildType::Meson
| package::BuildType::Perl
)
}
pub(crate) fn automatic_tests_disabled_for_outputs(
pkg_spec: &package::PackageSpec,
requested_outputs: deps::RequestedOutputs,
) -> bool {
pkg_spec.should_skip_automatic_tests() || requested_outputs.includes_lib32()
}
pub(crate) fn maybe_disable_tests_for_missing_deps(
pkg_spec: &mut package::PackageSpec,
db_path: &Path,
requested_outputs: deps::RequestedOutputs,
) -> Result<()> {
if automatic_tests_disabled_for_outputs(pkg_spec, requested_outputs)
|| !build_type_runs_automatic_tests(pkg_spec)
|| deps::declared_test_deps(pkg_spec, requested_outputs).is_empty()
{
return Ok(());
}
let missing_test = deps::check_test_deps_for_outputs(pkg_spec, db_path, requested_outputs)?;
if !missing_test.is_empty() {
ui::warn(format!(
"Missing test dependencies: {}. Tests will be skipped.",
missing_test.join(", ")
));
pkg_spec.build.flags.skip_tests = true;
}
Ok(())
}
pub(crate) fn maybe_prompt_to_skip_tests_for_missing_requested_deps(
pkg_spec: &mut package::PackageSpec,
missing_test: &[String],
reason: &str,
) -> Result<bool> {
if pkg_spec.should_skip_automatic_tests()
|| !build_type_runs_automatic_tests(pkg_spec)
|| missing_test.is_empty()
{
return Ok(false);
}
ui::warn(format!("{reason}: {}", missing_test.join(", ")));
if ui::prompt_yes_no("Continue without tests?", false)? {
pkg_spec.build.flags.skip_tests = true;
ui::warn("Tests will be skipped for this build.");
return Ok(true);
}
Ok(false)
}
pub(crate) fn requested_outputs(
pkg_spec: &package::PackageSpec,
lib32_only: bool,
) -> deps::RequestedOutputs {
if effective_lib32_only(pkg_spec, lib32_only) {
deps::RequestedOutputs::Lib32Only
} else if pkg_spec.builds_lib32_output() {
deps::RequestedOutputs::PrimaryAndLib32
} else {
deps::RequestedOutputs::PrimaryOnly
}
}
pub(crate) fn effective_lib32_only(pkg_spec: &package::PackageSpec, cli_lib32_only: bool) -> bool {
cli_lib32_only || pkg_spec.builds_only_lib32_output()
}
pub(crate) fn should_install_test_deps(
pkg_spec: &package::PackageSpec,
install_test_deps: bool,
requested_outputs: deps::RequestedOutputs,
) -> bool {
install_test_deps
&& !automatic_tests_disabled_for_outputs(pkg_spec, requested_outputs)
&& !deps::declared_test_deps(pkg_spec, requested_outputs).is_empty()
}
pub(crate) fn clean_build_workspace(config: &config::Config) -> Result<()> {
if config.build_dir.exists() {
fs::remove_dir_all(&config.build_dir).with_context(|| {
format!("Failed to clean build dir: {}", config.build_dir.display())
})?;
ui::success(format!(
"Cleaned build workspace: {}",
config.build_dir.display()
));
}
if config.cache_dir.exists() {
fs::remove_dir_all(&config.cache_dir).with_context(|| {
format!(
"Failed to clean source cache dir: {}",
config.cache_dir.display()
)
})?;
ui::success(format!(
"Cleaned source cache: {}",
config.cache_dir.display()
));
}
Ok(())
}
pub(crate) fn warn_if_running_as_root_for_build(command: &str, rootfs: &Path) {
if crate::fakeroot::is_root() {
ui::warn(format!("Running '{}' as root is discouraged.", command));
ui::warn(
"A misconfigured build environment or malicious/buggy build file can overwrite or delete critical system files.",
);
ui::warn("Recommendation: use a non-root build user and only install as root.");
ui::warn(format!("Current rootfs target: {}", rootfs.display()));
}
}
pub(crate) fn merge_missing_dependencies(mut base: Vec<String>, extra: Vec<String>) -> Vec<String> {
for dep in extra {
if !base.contains(&dep) {
base.push(dep);
}
}
base
}
pub(crate) fn make_lib32_build_spec(base: &package::PackageSpec) -> package::PackageSpec {
let mut spec = base.clone();
let flags = &mut spec.build.flags;
flags.lib32_variant = true;
flags.cflags = flags.cflags_lib32.clone();
flags.replace_cflags = flags.replace_cflags_lib32.clone();
flags.cxxflags = flags.cxxflags_lib32.clone();
flags.replace_cxxflags = flags.replace_cxxflags_lib32.clone();
if !flags.configure_lib32.is_empty() {
flags.configure = flags.configure_lib32.clone();
}
if !flags.post_configure_lib32.is_empty() {
flags.post_configure = flags.post_configure_lib32.clone();
}
if !flags.post_compile_lib32.is_empty() {
flags.post_compile = flags.post_compile_lib32.clone();
}
if !flags.post_install_lib32.is_empty() {
flags.post_install = flags.post_install_lib32.clone();
}
flags.cc = format!("{} -m32", flags.cc.trim());
flags.cxx = format!("{} -m32", flags.cxx.trim());
flags.build_dir = Some(match flags.build_dir.as_deref() {
Some(dir) if !dir.trim().is_empty() => format!("{}-lib32", dir.trim()),
_ => "build-lib32".to_string(),
});
spec
}
pub(crate) fn make_lib32_package_spec(base: &package::PackageSpec) -> package::PackageSpec {
let mut spec = base.clone();
let lib32_name = format!("lib32-{}", base.package.name);
spec.package.name = lib32_name.clone();
spec.packages.clear();
spec.alternatives = base.alternatives_for_output(&lib32_name);
spec.dependencies = base.dependencies_for_output(&lib32_name);
spec
}
pub(crate) struct RequestedBuildToolPackageInstall<'a> {
pub(crate) build_type: package::BuildType,
pub(crate) rootfs: &'a Path,
pub(crate) config: &'a config::Config,
pub(crate) db_path: &'a Path,
pub(crate) spec_path: &'a Path,
pub(crate) execution: InstallPlanExecutionOptions<'a>,
pub(crate) assume_yes: bool,
}
pub(crate) fn ensure_requested_build_tool_package_installed(
request: RequestedBuildToolPackageInstall<'_>,
) -> Result<()> {
let Some(package_name) = builder::requested_build_tool_package(request.build_type) else {
return Ok(());
};
let build_tool_option = builder::build_tool_package_option(request.build_type);
if deps::is_dep_satisfied_in_db(&package_name, request.db_path)? {
return Ok(());
}
ui::warn(format!(
"Missing required build tool package for {:?} builds{}: {}",
request.build_type,
build_tool_option
.map(|option| format!(" ({option})"))
.unwrap_or_default(),
package_name
));
let local_sibling_root = request
.spec_path
.parent()
.and_then(|path| path.parent())
.map(Path::to_path_buf);
let plan = planner::build_dependency_install_plan(
request.config,
request.rootfs,
std::slice::from_ref(&package_name),
planner::PlannerOptions {
assume_yes: request.assume_yes,
prefer_binary: request.config.repo_settings.prefer_binary,
local_sibling_root,
include_test_deps: request.execution.install_test_deps,
lib32_only_requested_specs: false,
},
)
.with_context(|| {
format!(
"Failed to resolve required build tool package '{}'",
package_name
)
})?;
if plan.steps.is_empty() {
anyhow::bail!(
"Required build tool package '{}' is not installed and no install plan could be created",
package_name
);
}
super::print_plan_summary(&plan);
super::execute_install_plan_with_child_commands(
&plan,
request.rootfs,
request.config,
request.execution,
)
}
pub(crate) fn build_lib32_companion_package(
pkg_spec: &package::PackageSpec,
src_dir: &Path,
config: &config::Config,
cross_config: Option<&cross::CrossConfig>,
export_compiler_flags: bool,
force: bool,
) -> Result<Option<(package::PackageSpec, PathBuf)>> {
if !pkg_spec.builds_lib32_output() && !force {
return Ok(None);
}
if pkg_spec.is_metapackage() {
crate::log_warn!(
"Ignoring build.flags.build-32 for metapackage {}",
pkg_spec.package.name
);
return Ok(None);
}
crate::log_info!("Running separate lib32 build pass...");
let host_build_dir = builder::ensure_host_build(
pkg_spec,
src_dir,
cross_config,
export_compiler_flags,
builder::TargetBuildKind::Lib32,
)?;
let mut lib32_input = pkg_spec.clone();
lib32_input.build.flags.build_32 = true;
let mut lib32_build_spec = make_lib32_build_spec(&lib32_input);
if let Some(host_dir) = host_build_dir.as_ref() {
lib32_build_spec.build.flags.host_build_dir = Some(host_dir.to_string_lossy().into_owned());
}
let lib32_pkg_spec = make_lib32_package_spec(pkg_spec);
let lib32_destdir = config
.build_dir
.join("destdir")
.join(&lib32_pkg_spec.package.name);
if lib32_destdir.exists() {
fs::remove_dir_all(&lib32_destdir).with_context(|| {
format!("Failed to clean lib32 destdir: {}", lib32_destdir.display())
})?;
}
fs::create_dir_all(&lib32_destdir).with_context(|| {
format!(
"Failed to create lib32 destdir: {}",
lib32_destdir.display()
)
})?;
builder::build(
&lib32_build_spec,
src_dir,
&lib32_destdir,
cross_config,
export_compiler_flags,
host_build_dir.as_deref(),
)?;
let lib32_src = lib32_destdir.join("usr/lib32");
if !lib32_src.exists() {
anyhow::bail!(
"lib32 build completed but did not install usr/lib32 into {}",
lib32_destdir.display()
);
}
install::scripts::stage_scripts_from_spec_dir(&lib32_pkg_spec, &lib32_destdir)?;
staging::process(&lib32_destdir, &lib32_pkg_spec)?;
staging::symlink_package_license(
&lib32_destdir,
&lib32_pkg_spec.package.name,
&pkg_spec.package.name,
)?;
Ok(Some((lib32_pkg_spec, lib32_destdir)))
}
+7
View File
@@ -0,0 +1,7 @@
use super::*;
use crate::commands::update::check::run_check_command;
pub(super) fn run_check(args: crate::cli::CheckArgs) -> Result<()> {
let dir = args.dir;
run_check_command(&dir)
}
+163
View File
@@ -0,0 +1,163 @@
use super::*;
use crate::commands::repo::groups::{
expand_install_requests_for_groups, expand_installed_group_targets,
};
pub(crate) mod archive;
pub(super) fn run_install(args: InstallArgs, cli_test_deps: bool) -> Result<()> {
let InstallArgs {
rootfs_args,
prompt_args,
build_exec_args,
lib32_args,
spec_or_archive,
spec,
} = args;
let rootfs = rootfs_args.rootfs;
let yes = prompt_args.yes;
let no_deps = build_exec_args.no_deps;
let no_flags = build_exec_args.no_flags;
let cross_prefix = build_exec_args.cross_prefix;
let clean = build_exec_args.clean;
let dry_run = build_exec_args.dry_run;
let lib32_only = lib32_args.lib32_only;
let install_requests = match spec {
Some(spec_path) => vec![spec_path],
None => spec_or_archive,
};
let config = config::Config::for_rootfs(&rootfs);
ensure_depot_self_update_not_required(&config, &rootfs)?;
let (install_requests, explicit_groups) =
expand_install_requests_for_groups(&config, &rootfs, &install_requests)?;
let install_test_deps = install_test_deps_enabled(cli_test_deps, &config);
let mut planned_targets = Vec::new();
let mut planned_spec_paths = Vec::new();
let mut direct_requests = Vec::new();
if no_deps {
direct_requests = install_requests;
} else {
for request in install_requests {
if is_archive_install_request(&request) {
direct_requests.push(request);
continue;
}
if request.exists() {
planned_spec_paths.push(request.clone());
planned_targets.push(planner::InstallTarget::SpecPath(request));
} else {
planned_targets.push(planner::InstallTarget::PackageName(
request.to_string_lossy().to_string(),
));
}
}
}
let mut ran_plan_mode = false;
if !planned_targets.is_empty() {
ran_plan_mode = true;
let planner_opts = planner::PlannerOptions {
assume_yes: yes,
prefer_binary: config.repo_settings.prefer_binary,
local_sibling_root: shared_local_sibling_root(&planned_spec_paths),
include_test_deps: install_test_deps,
lib32_only_requested_specs: lib32_only,
};
let plan = if planned_targets.len() == 1 {
planner::build_install_plan(&config, &rootfs, planned_targets[0].clone(), planner_opts)?
} else {
planner::build_install_plan_for_targets(
&config,
&rootfs,
&planned_targets,
planner_opts,
)?
};
print_plan_summary(&plan);
execute_install_plan_with_child_commands(
&plan,
&rootfs,
&config,
InstallPlanExecutionOptions {
no_flags,
cross_prefix: cross_prefix.as_deref(),
clean,
dry_run,
confirm_installation: true,
lib32_only_requested_specs: lib32_only,
install_test_deps,
},
)?;
}
let mut ran_direct_install = false;
let direct_install_options = DirectInstallOptions {
rootfs: &rootfs,
no_deps,
no_flags,
cross_prefix: cross_prefix.as_deref(),
clean,
dry_run,
lib32_only,
install_test_deps,
};
if direct_requests.len() > 1
&& direct_requests
.iter()
.all(|request| is_archive_install_request(request))
{
ran_direct_install |= run_direct_archive_install_requests(
direct_install_options,
&config,
&direct_requests,
true,
)?;
} else {
for request in direct_requests {
ran_direct_install |=
run_direct_install_request(direct_install_options, &config, request)?;
}
}
if ran_direct_install {
install::scripts::run_deferred_hooks_if_possible(&rootfs)?;
}
if !dry_run && !explicit_groups.is_empty() {
db::record_installed_groups(&config.installed_db_path(&rootfs), &explicit_groups)?;
}
if clean && (ran_plan_mode || ran_direct_install) {
clean_build_workspace(&config)?;
}
Ok(())
}
pub(super) fn run_remove(args: RemoveArgs) -> Result<()> {
let RemoveArgs {
rootfs_args,
package,
..
} = args;
let rootfs = rootfs_args.rootfs;
let config = config::Config::for_rootfs(&rootfs);
let mut remove_lock = locking::open_lock(&config)?;
let remove_lock_path = locking::lock_path(&config);
let _remove_lock_guard = locking::try_write(&mut remove_lock, &remove_lock_path, "remove")?;
let db_path = config.installed_db_path(&rootfs);
let (removal_targets, explicit_groups) =
expand_installed_group_targets(&db_path, std::slice::from_ref(&package))?;
if !ui::prompt_package_action("removal", &removal_targets, true)? {
anyhow::bail!("Aborted");
}
for target in &removal_targets {
remove_installed_package_with_hooks(target, &rootfs, &config)?;
}
for group in explicit_groups {
db::remove_installed_group(&db_path, &group)?;
}
Ok(())
}
+288
View File
@@ -0,0 +1,288 @@
use super::*;
fn package_spec_from_archive_metadata(metadata: &toml::Value) -> package::PackageSpec {
let mut spec = package::PackageSpec {
package: package::PackageInfo {
name: metadata
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
real_name: metadata
.get("real_name")
.and_then(|v| v.as_str())
.map(String::from),
version: metadata
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
revision: metadata
.get("revision")
.and_then(|v| v.as_integer())
.unwrap_or(1) as u32,
description: metadata
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
homepage: metadata
.get("homepage")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
abi_breaking: metadata
.get("abi_breaking")
.and_then(|v| v.as_bool())
.unwrap_or(false),
license: super::super::parse_licenses_from_toml(metadata),
},
packages: Vec::new(),
alternatives: package::Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: package::Build {
build_type: package::BuildType::Bin,
flags: package::BuildFlags {
keep: super::super::parse_keep_list(metadata),
..package::BuildFlags::default()
},
},
dependencies: package::Dependencies {
build: Vec::new(),
runtime: super::super::parse_dependency_list(metadata, "runtime"),
test: Vec::new(),
optional: super::super::parse_dependency_list(metadata, "optional"),
groups: super::super::parse_dependency_list(metadata, "groups"),
lib32: None,
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
};
if let Some(provides) = metadata.get("provides").and_then(|v| v.as_array()) {
spec.alternatives.provides = provides
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
if let Some(conflicts) = metadata.get("conflicts").and_then(|v| v.as_array()) {
spec.alternatives.conflicts = conflicts
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
if let Some(replaces) = metadata.get("replaces").and_then(|v| v.as_array()) {
spec.alternatives.replaces = replaces
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
spec
}
fn load_package_spec_from_staging(staged_dir: &Path) -> Result<package::PackageSpec> {
let metadata_path = staged_dir.join(".metadata.toml");
let metadata_content = fs::read_to_string(&metadata_path)
.with_context(|| format!("Failed to read {}", metadata_path.display()))?;
let metadata: toml::Value = toml::from_str(&metadata_content)
.with_context(|| format!("Failed to parse {}", metadata_path.display()))?;
Ok(package_spec_from_archive_metadata(&metadata))
}
fn parse_license_list_from_repo(license: &Option<String>) -> Vec<String> {
let Some(raw) = license.as_ref() else {
return Vec::new();
};
raw.split(',')
.map(|part| part.trim())
.filter(|part| !part.is_empty())
.map(String::from)
.collect()
}
pub(crate) fn package_spec_from_repo_record(
record: &db::repo::BinaryRepoPackageRecord,
) -> package::PackageSpec {
package::PackageSpec {
package: package::PackageInfo {
name: record.name.clone(),
real_name: record.real_name.clone(),
version: record.version.clone(),
revision: record.revision,
description: record.description.clone().unwrap_or_default(),
homepage: record.homepage.clone().unwrap_or_default(),
abi_breaking: record.abi_breaking,
license: parse_license_list_from_repo(&record.license),
},
packages: Vec::new(),
alternatives: package::Alternatives {
provides: record.provides.clone(),
conflicts: record.conflicts.clone(),
replaces: record.replaces.clone(),
lib32: None,
},
manual_sources: Vec::new(),
source: Vec::new(),
build: package::Build {
build_type: package::BuildType::Bin,
flags: package::BuildFlags::default(),
},
dependencies: package::Dependencies {
build: Vec::new(),
runtime: record.runtime_dependencies.clone(),
test: Vec::new(),
optional: record.optional_dependencies.clone(),
groups: Vec::new(),
lib32: None,
},
package_alternatives: Default::default(),
package_dependencies: Default::default(),
spec_dir: PathBuf::from("."),
}
}
pub(crate) fn load_package_spec_from_staging_or_repo_record(
staged_dir: &Path,
record: &db::repo::BinaryRepoPackageRecord,
) -> Result<package::PackageSpec> {
let metadata_path = staged_dir.join(".metadata.toml");
if metadata_path.exists() {
load_package_spec_from_staging(staged_dir)
} else {
Ok(package_spec_from_repo_record(record))
}
}
pub(crate) fn staging_temp_root(config: &config::Config) -> PathBuf {
config.build_dir.join("staging")
}
fn create_archive_staging_dir(
config: &config::Config,
archive_path: &Path,
) -> Result<tempfile::TempDir> {
let staging_root = staging_temp_root(config);
fs::create_dir_all(&staging_root)
.with_context(|| format!("Failed to create staging root {}", staging_root.display()))?;
tempfile::Builder::new()
.prefix("archive-")
.tempdir_in(&staging_root)
.with_context(|| {
format!(
"Failed to create staging dir for {} under {}",
archive_path.display(),
staging_root.display()
)
})
}
pub(crate) fn load_package_archive_into_staging(
config: &config::Config,
archive_path: &Path,
) -> Result<(package::PackageSpec, tempfile::TempDir)> {
let tmp_dir = create_archive_staging_dir(config, archive_path).with_context(|| {
format!(
"Failed to create staging dir for {}",
archive_path.display()
)
})?;
let extract_dir = tmp_dir.path().to_path_buf();
let file = fs::File::open(archive_path)
.with_context(|| format!("Failed to open archive {}", archive_path.display()))?;
let zstd_decoder = zstd::stream::read::Decoder::new(file)
.with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?;
let mut archive = tar::Archive::new(zstd_decoder);
let mut metadata_content = String::new();
for entry in archive.entries().with_context(|| {
format!(
"Failed to read archive entries from {}",
archive_path.display()
)
})? {
crate::interrupts::check()?;
let mut entry = entry.with_context(|| {
format!(
"Failed to read archive entry from {}",
archive_path.display()
)
})?;
if entry.path()?.to_string_lossy() == ".metadata.toml" {
use std::io::Read;
entry
.read_to_string(&mut metadata_content)
.with_context(|| {
format!(
"Failed to read .metadata.toml in {}",
archive_path.display()
)
})?;
break;
}
}
if metadata_content.is_empty() {
anyhow::bail!(
"Package archive does not contain .metadata.toml: {}",
archive_path.display()
);
}
let file = fs::File::open(archive_path)
.with_context(|| format!("Failed to open archive {}", archive_path.display()))?;
let zstd_decoder = zstd::stream::read::Decoder::new(file)
.with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?;
let mut archive = tar::Archive::new(zstd_decoder);
archive.set_preserve_permissions(true);
crate::interrupts::unpack_tar_archive(&mut archive, &extract_dir).with_context(|| {
format!(
"Failed to extract package archive {} into {}",
archive_path.display(),
extract_dir.display()
)
})?;
let metadata: toml::Value = toml::from_str(&metadata_content).with_context(|| {
format!(
"Failed to parse .metadata.toml in {}",
archive_path.display()
)
})?;
Ok((package_spec_from_archive_metadata(&metadata), tmp_dir))
}
pub(crate) fn extract_package_archive_to_staging(
config: &config::Config,
archive_path: &Path,
) -> Result<tempfile::TempDir> {
let tmp_dir = create_archive_staging_dir(config, archive_path).with_context(|| {
format!(
"Failed to create staging dir for {}",
archive_path.display()
)
})?;
let extract_dir = tmp_dir.path().to_path_buf();
let file = fs::File::open(archive_path)
.with_context(|| format!("Failed to open archive {}", archive_path.display()))?;
let zstd_decoder = zstd::stream::read::Decoder::new(file)
.with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?;
let mut archive = tar::Archive::new(zstd_decoder);
archive.set_preserve_permissions(true);
crate::interrupts::unpack_tar_archive(&mut archive, &extract_dir).with_context(|| {
format!(
"Failed to extract package archive {} into {}",
archive_path.display(),
extract_dir.display()
)
})?;
Ok(tmp_dir)
}
+194
View File
@@ -0,0 +1,194 @@
use super::*;
pub(crate) mod internal;
pub(super) fn run_info(args: InfoArgs) -> Result<()> {
let InfoArgs {
rootfs_args,
package,
} = args;
let rootfs = rootfs_args.rootfs;
let path = PathBuf::from(&package);
if path.exists() {
let config = config::Config::for_rootfs(&rootfs);
let info_lock = locking::open_lock(&config)?;
let info_lock_path = locking::lock_path(&config);
let _info_lock_guard = locking::try_read(&info_lock, &info_lock_path, "info")?;
let pkg_spec = package::PackageSpec::from_file(&path)?;
println!("{}", pkg_spec);
let db_path = config.installed_db_path(&rootfs);
deps::print_dep_status(&pkg_spec, &db_path)?;
} else {
let config = config::Config::for_rootfs(&rootfs);
let info_lock = locking::open_lock(&config)?;
let info_lock_path = locking::lock_path(&config);
let _info_lock_guard = locking::try_read(&info_lock, &info_lock_path, "info")?;
let db_path = config.installed_db_path(&rootfs);
db::show_package_info(&db_path, &package)?;
}
Ok(())
}
pub(super) fn run_owns(args: OwnsArgs) -> Result<()> {
let OwnsArgs { rootfs_args, path } = args;
let rootfs = rootfs_args.rootfs;
let config = config::Config::for_rootfs(&rootfs);
let owns_lock = locking::open_lock(&config)?;
let owns_lock_path = locking::lock_path(&config);
let _owns_lock_guard = locking::try_read(&owns_lock, &owns_lock_path, "owns")?;
let db_path = config.installed_db_path(&rootfs);
match db::owns_path(&db_path, &path)? {
Some(owner) => ui::info(format!("{} is owned by {}", path.display(), owner)),
None => ui::warn(format!("No installed package owns {}", path.display())),
}
Ok(())
}
pub(super) fn run_list(args: ListArgs) -> Result<()> {
let ListArgs { rootfs_args } = args;
let rootfs = rootfs_args.rootfs;
let config = config::Config::for_rootfs(&rootfs);
let list_lock = locking::open_lock(&config)?;
let list_lock_path = locking::lock_path(&config);
let _list_lock_guard = locking::try_read(&list_lock, &list_lock_path, "list")?;
let db_path = config.installed_db_path(&rootfs);
db::list_packages(&db_path)?;
Ok(())
}
pub(super) fn run_sign(args: SignArgs) -> Result<()> {
let SignArgs { rootfs_args, files } = args;
let rootfs = rootfs_args.rootfs;
let sig_paths = signing::sign_zst_files_detached(&rootfs, &files)?;
for sig_path in sig_paths {
ui::success(format!(
"Created detached signature: {}",
sig_path.display()
));
}
Ok(())
}
pub(super) fn run_generate_artifacts(args: crate::cli::GenerateArtifactsArgs) -> Result<()> {
let out_dir = args.out_dir;
cli_assets::generate_cli_assets(&out_dir)?;
ui::success(format!("Generated CLI assets in {}", out_dir.display()));
Ok(())
}
pub(super) fn run_config(args: ConfigArgs) -> Result<()> {
let ConfigArgs { rootfs_args } = args;
let rootfs = rootfs_args.rootfs;
let config = config::Config::for_rootfs(&rootfs);
let config_lock = locking::open_lock(&config)?;
let config_lock_path = locking::lock_path(&config);
let _config_lock_guard = locking::try_read(&config_lock, &config_lock_path, "config")?;
println!("Cache Directory: {}", config.cache_dir.display());
println!(
"Package Cache Directory: {}",
config.package_cache_dir.display()
);
println!("Build Directory: {}", config.build_dir.display());
println!("Database Directory: {}", config.db_dir.display());
println!("Repo Clone Directory: {}", config.repo_clone_dir.display());
println!("Install Test Deps: {}", config.install_test_deps);
println!(
"Configured Repos: {} source, {} binary",
config.source_repos.len(),
config.binary_repos.len()
);
println!("\nBuild Overrides: {}", config.build_overrides);
println!("Package Overrides: {}", config.package_overrides);
if !config.appends.is_empty() {
println!("\nAppends:");
for (k, v) in &config.appends {
println!(" {} = {:?}", k, v);
}
}
Ok(())
}
pub(super) fn run_make_spec(args: crate::cli::MakeSpecArgs) -> Result<()> {
let output = args.output;
let spec = package::create_interactive()?;
let toml_string = package::spec_to_minimal_toml(&spec)?;
let output_path =
output.unwrap_or_else(|| PathBuf::from(format!("{}.toml", spec.package.name)));
if output_path.exists() {
ui::warn(format!("File {} already exists.", output_path.display()));
if !ui::prompt_yes_no("Overwrite it?", false)? {
anyhow::bail!("Aborted");
}
}
fs::write(&output_path, toml_string)?;
ui::success(format!(
"Package specification saved to {}",
output_path.display()
));
Ok(())
}
pub(super) fn run_convert(args: ConvertArgs) -> Result<()> {
let ConvertArgs { input, output } = args;
let converted = package::convert_starbuild_file(&input, output.as_deref())?;
let mut outputs = vec![converted.output_path.clone()];
if let Some(build_script_path) = &converted.build_script_path {
outputs.push(build_script_path.clone());
}
let existing: Vec<_> = outputs
.iter()
.filter(|path| path.exists())
.map(|path| path.display().to_string())
.collect();
if !existing.is_empty() {
ui::warn(format!(
"Generated files already exist: {}",
existing.join(", ")
));
if !ui::prompt_yes_no("Overwrite them?", false)? {
anyhow::bail!("Aborted");
}
}
fs::write(&converted.output_path, converted.toml)
.with_context(|| format!("Failed to write {}", converted.output_path.display()))?;
if let (Some(build_script), Some(build_script_path)) =
(converted.build_script, converted.build_script_path)
{
fs::write(&build_script_path, build_script)
.with_context(|| format!("Failed to write {}", build_script_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&build_script_path)
.with_context(|| format!("Failed to stat {}", build_script_path.display()))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&build_script_path, perms)
.with_context(|| format!("Failed to chmod {}", build_script_path.display()))?;
}
ui::success(format!(
"Converted STARBUILD into {} and {}",
converted.output_path.display(),
build_script_path.display()
));
} else {
ui::success(format!(
"Converted STARBUILD into {}",
converted.output_path.display()
));
}
Ok(())
}
pub(super) fn run_internal(args: crate::cli::InternalArgs) -> Result<()> {
internal::run_internal_command(args.command)
}
+105
View File
@@ -0,0 +1,105 @@
use super::*;
fn current_process_env_vars() -> Vec<(String, String)> {
const ALLOWED_ENV_VARS: &[&str] = &[
"AR",
"CARCH",
"CBUILD",
"CC",
"CHOST",
"CPP",
"CROSS_COMPILE",
"CROSS_PREFIX",
"CFLAGS",
"CXX",
"CXXFLAGS",
"DEPOT_ROOTFS",
"DEPOT_SPECDIR",
"DESTDIR",
"LD",
"LDFLAGS",
"LTOFLAGS",
"MAKEFLAGS",
"NM",
"PREFIX",
"PYTHONDONTWRITEBYTECODE",
"PYTHONNOUSERSITE",
"RANLIB",
"RUSTFLAGS",
"RUSTLTOFLAGS",
"SETUPTOOLS_USE_DISTUTILS",
"STRIP",
];
ALLOWED_ENV_VARS
.iter()
.filter_map(|key| {
std::env::var(key)
.ok()
.map(|value| ((*key).to_string(), value))
})
.collect()
}
pub(crate) fn run_internal_command(command: InternalCommands) -> Result<()> {
match command {
InternalCommands::PythonBuild {
src_dir,
dist_dir,
config_settings,
} => {
let env_vars = current_process_env_vars();
crate::builder::python::build_wheels(&src_dir, &dist_dir, &env_vars, &config_settings)
}
InternalCommands::PythonInstall {
dist_dir,
wheels,
prefix,
} => {
let env_vars = current_process_env_vars();
let wheel_paths = if wheels.is_empty() {
crate::builder::python::collect_wheels(&dist_dir)?
} else {
wheels
};
let destdir = std::env::var("DESTDIR")
.context("DESTDIR must be set for internal python-install")?;
crate::builder::python::install_wheels(
&wheel_paths,
Path::new(&destdir),
&prefix,
&env_vars,
)
}
InternalCommands::Clone { repo, dest } => {
let (base, rev) = crate::source::split_git_url(&repo).with_context(|| {
format!("Unsupported repository URL for internal clone: {}", repo)
})?;
let dest = if let Some(dest) = dest {
dest
} else {
let cwd =
std::env::current_dir().context("Failed to determine current directory")?;
cwd.join(crate::source::git_default_checkout_dir_name(&base))
};
if dest.exists() {
anyhow::bail!("Clone destination already exists: {}", dest.display());
}
let cache_root =
tempfile::tempdir().context("Failed to create temporary git cache for clone")?;
let label = dest
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.unwrap_or("clone");
crate::source::git_checkout(
&base,
&rev,
&dest,
&cache_root.path().join("git"),
label,
&[],
)
}
}
}
+410
View File
@@ -0,0 +1,410 @@
use super::*;
pub(crate) mod config;
pub(crate) mod groups;
mod search;
use self::config::{
print_repo_list, repo_kind_label, resolve_repo_kind_for_name, selected_source_repos,
};
use self::search::run_search_command;
pub(super) fn run_search(args: SearchArgs) -> Result<()> {
let SearchArgs {
rootfs_args,
query,
files,
} = args;
let rootfs = rootfs_args.rootfs;
let config = crate::config::Config::for_rootfs(&rootfs);
let search_lock = locking::open_lock(&config)?;
let search_lock_path = locking::lock_path(&config);
let _search_lock_guard = locking::try_read(&search_lock, &search_lock_path, "search")?;
run_search_command(&query, files, &config, &rootfs)
}
pub(super) fn run_repo(command: RepoCommands) -> Result<()> {
match command {
RepoCommands::Create { args, dir } => {
let rootfs = args.rootfs_args.rootfs;
let cfg = crate::config::Config::for_rootfs(&rootfs);
let mut repo_lock = locking::open_lock(&cfg)?;
let repo_lock_path = locking::lock_path(&cfg);
let _repo_lock_guard =
locking::try_write(&mut repo_lock, &repo_lock_path, "repo create")?;
let repo = db::repo::RepoManager::new(dir);
let db_path = repo.create_repo_db()?;
if let Some(sig_path) = signing::auto_sign_zst_file_detached(&rootfs, &db_path)? {
ui::success(format!(
"Created detached signature: {}",
sig_path.display()
));
}
ui::success(format!(
"Created repository database: {}",
db_path.display()
));
}
RepoCommands::Sync { args } => {
let rootfs = args.rootfs_args.rootfs;
let cfg = crate::config::Config::for_rootfs(&rootfs);
let mut repo_lock = locking::open_lock(&cfg)?;
let repo_lock_path = locking::lock_path(&cfg);
let _repo_lock_guard =
locking::try_write(&mut repo_lock, &repo_lock_path, "repo sync")?;
if !crate::fakeroot::is_root() {
anyhow::bail!("The 'repo sync' command must be run as root");
}
let config = cfg;
let mirrors = config.enabled_source_mirror_map();
if mirrors.is_empty() {
ui::info("No enabled source repos configured");
} else {
db::repo::sync_mirrors(&config.repo_clone_dir, &mirrors)?;
ui::success(format!(
"Source repos synchronized into {}",
config.repo_clone_dir.display()
));
}
}
RepoCommands::Update { args, name } => {
let rootfs = args.rootfs_args.rootfs;
let cfg = crate::config::Config::for_rootfs(&rootfs);
let mut repo_lock = locking::open_lock(&cfg)?;
let repo_lock_path = locking::lock_path(&cfg);
let _repo_lock_guard =
locking::try_write(&mut repo_lock, &repo_lock_path, "repo update")?;
if !crate::fakeroot::is_root() {
anyhow::bail!("The 'repo update' command must be run as root");
}
let config = cfg;
let mirrors = selected_source_repos(&config, name.as_deref())?;
if mirrors.is_empty() {
ui::info("No enabled source repos configured");
} else {
db::repo::sync_mirrors(&config.repo_clone_dir, &mirrors)?;
if let Some(name) = name {
ui::success(format!(
"Source repo '{}' synchronized into {}",
name,
config.repo_clone_dir.display()
));
} else {
ui::success(format!(
"Source repos synchronized into {}",
config.repo_clone_dir.display()
));
}
}
}
RepoCommands::Index { args, dir, subdirs } => {
let rootfs = args.rootfs_args.rootfs;
let cfg = crate::config::Config::for_rootfs(&rootfs);
let mut repo_lock = locking::open_lock(&cfg)?;
let repo_lock_path = locking::lock_path(&cfg);
let _repo_lock_guard =
locking::try_write(&mut repo_lock, &repo_lock_path, "repo index")?;
let stats = index::create_source_repo_index(&dir, &subdirs)
.with_context(|| format!("Failed to create source index for {}", dir.display()))?;
ui::success(format!(
"Wrote source index: {}",
stats.index_path.display()
));
ui::info(format!(
"Indexed {} spec(s) from {} TOML file(s): package rows={} provides rows={} conflicts rows={} dependency rows={} ignored_toml={}",
stats.specs_indexed,
stats.toml_files_scanned,
stats.package_rows,
stats.provides_rows,
stats.conflicts_rows,
stats.dependency_rows,
stats.ignored_toml_files
));
}
RepoCommands::List { args } => {
let rootfs = args.rootfs_args.rootfs;
let config = crate::config::Config::for_rootfs(&rootfs);
let repo_lock = locking::open_lock(&config)?;
let repo_lock_path = locking::lock_path(&config);
let _repo_lock_guard = locking::try_read(&repo_lock, &repo_lock_path, "repo list")?;
print_repo_list(&config);
}
RepoCommands::Add {
args,
name,
url,
kind,
subdirs,
priority,
disabled,
arch,
repo_db,
allow_unsigned,
} => {
let rootfs = args.rootfs_args.rootfs;
let cfg = crate::config::Config::for_rootfs(&rootfs);
let mut repo_lock = locking::open_lock(&cfg)?;
let repo_lock_path = locking::lock_path(&cfg);
let _repo_lock_guard = locking::try_write(&mut repo_lock, &repo_lock_path, "repo add")?;
let mut repos = crate::config::load_repos_config_file(&rootfs)?;
match kind {
RepoKindArg::Source => {
if let Some(existing) = repos.source.get(&name)
&& (existing.url != url
|| existing.subdirs != subdirs
|| existing.priority != priority
|| existing.enabled == disabled)
&& !ui::prompt_yes_no(
&format!("Source repo '{}' already exists. Overwrite it?", name),
false,
)?
{
anyhow::bail!("Aborted");
}
repos.source.insert(
name.clone(),
crate::config::SourceRepo {
url,
enabled: !disabled,
priority,
subdirs,
},
);
}
RepoKindArg::Binary => {
let arch_name = arch.unwrap_or_else(|| std::env::consts::ARCH.to_string());
let mut candidate = repos.binary.get(&name).cloned().unwrap_or_default();
candidate.url = url.clone();
candidate.enabled = !disabled;
candidate.priority = priority;
candidate.repo_db = repo_db.clone();
candidate.allow_unsigned = allow_unsigned;
candidate.arch.entry(arch_name.clone()).or_default().enabled = true;
if let Some(existing) = repos.binary.get(&name)
&& (*existing != candidate)
&& !ui::prompt_yes_no(
&format!("Binary repo '{}' already exists. Overwrite it?", name),
false,
)?
{
anyhow::bail!("Aborted");
}
repos.binary.insert(name.clone(), candidate);
}
}
let path = crate::config::save_repos_config_file(&rootfs, &repos)?;
ui::success(format!(
"Saved {} repo '{}' to {}",
repo_kind_label(kind),
name,
path.display()
));
}
RepoCommands::Remove { args, name, kind } => {
let rootfs = args.rootfs_args.rootfs;
let cfg = crate::config::Config::for_rootfs(&rootfs);
let mut repo_lock = locking::open_lock(&cfg)?;
let repo_lock_path = locking::lock_path(&cfg);
let _repo_lock_guard =
locking::try_write(&mut repo_lock, &repo_lock_path, "repo remove")?;
let mut repos = crate::config::load_repos_config_file(&rootfs)?;
let kind = resolve_repo_kind_for_name(&repos, &name, kind)?;
if !ui::prompt_yes_no(
&format!("Remove {} repo '{}'?", repo_kind_label(kind), name),
false,
)? {
anyhow::bail!("Aborted");
}
match kind {
RepoKindArg::Source => {
repos.source.remove(&name);
}
RepoKindArg::Binary => {
repos.binary.remove(&name);
}
}
let path = crate::config::save_repos_config_file(&rootfs, &repos)?;
ui::success(format!(
"Removed {} repo '{}' from {}",
repo_kind_label(kind),
name,
path.display()
));
}
RepoCommands::Enable { args, name, kind } => {
let rootfs = args.rootfs_args.rootfs;
let cfg = crate::config::Config::for_rootfs(&rootfs);
let mut repo_lock = locking::open_lock(&cfg)?;
let repo_lock_path = locking::lock_path(&cfg);
let _repo_lock_guard =
locking::try_write(&mut repo_lock, &repo_lock_path, "repo enable")?;
let mut repos = crate::config::load_repos_config_file(&rootfs)?;
let kind = resolve_repo_kind_for_name(&repos, &name, kind)?;
match kind {
RepoKindArg::Source => {
let repo = repos
.source
.get_mut(&name)
.with_context(|| format!("Source repo '{}' not found", name))?;
if repo.enabled {
ui::info(format!("Source repo '{}' is already enabled", name));
} else {
repo.enabled = true;
let path = crate::config::save_repos_config_file(&rootfs, &repos)?;
ui::success(format!(
"Enabled source repo '{}' in {}",
name,
path.display()
));
}
}
RepoKindArg::Binary => {
let repo = repos
.binary
.get_mut(&name)
.with_context(|| format!("Binary repo '{}' not found", name))?;
if repo.enabled {
ui::info(format!("Binary repo '{}' is already enabled", name));
} else {
repo.enabled = true;
let path = crate::config::save_repos_config_file(&rootfs, &repos)?;
ui::success(format!(
"Enabled binary repo '{}' in {}",
name,
path.display()
));
}
}
}
}
RepoCommands::Disable { args, name, kind } => {
let rootfs = args.rootfs_args.rootfs;
let cfg = crate::config::Config::for_rootfs(&rootfs);
let mut repo_lock = locking::open_lock(&cfg)?;
let repo_lock_path = locking::lock_path(&cfg);
let _repo_lock_guard =
locking::try_write(&mut repo_lock, &repo_lock_path, "repo disable")?;
let mut repos = crate::config::load_repos_config_file(&rootfs)?;
let kind = resolve_repo_kind_for_name(&repos, &name, kind)?;
if !ui::prompt_yes_no(
&format!("Disable {} repo '{}'?", repo_kind_label(kind), name),
true,
)? {
anyhow::bail!("Aborted");
}
match kind {
RepoKindArg::Source => {
let repo = repos
.source
.get_mut(&name)
.with_context(|| format!("Source repo '{}' not found", name))?;
repo.enabled = false;
}
RepoKindArg::Binary => {
let repo = repos
.binary
.get_mut(&name)
.with_context(|| format!("Binary repo '{}' not found", name))?;
repo.enabled = false;
}
}
let path = crate::config::save_repos_config_file(&rootfs, &repos)?;
ui::success(format!(
"Disabled {} repo '{}' in {}",
repo_kind_label(kind),
name,
path.display()
));
}
RepoCommands::Owns { args, path } => {
let rootfs = args.rootfs_args.rootfs;
let config = crate::config::Config::for_rootfs(&rootfs);
let repo_lock = locking::open_lock(&config)?;
let repo_lock_path = locking::lock_path(&config);
let _repo_lock_guard = locking::try_read(&repo_lock, &repo_lock_path, "repo owns")?;
let host_arch = std::env::consts::ARCH;
let mut any = false;
let mut binary_repos: Vec<_> = 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 (name, repo) in binary_repos {
match db::repo::binary_repo_owns_path(
name,
repo,
&rootfs,
&config.package_cache_dir,
&path.to_string_lossy(),
) {
Ok(hits) => {
for hit in hits {
any = true;
ui::info(format!(
"{} [binary:{}] {}-{} size={} owns={}",
hit.package_name,
hit.repo_name,
hit.version,
hit.revision,
hit.size,
hit.path
));
}
}
Err(e) => crate::log_warn!("Binary repo '{}': {}", name, e),
}
}
if !any {
ui::warn(format!(
"No binary repo metadata entry owns {}",
path.display()
));
}
}
RepoCommands::Status { args } => {
let rootfs = args.rootfs_args.rootfs;
let config = crate::config::Config::for_rootfs(&rootfs);
let repo_lock = locking::open_lock(&config)?;
let repo_lock_path = locking::lock_path(&config);
let _repo_lock_guard = locking::try_read(&repo_lock, &repo_lock_path, "repo status")?;
let mirrors = config.enabled_source_mirror_map();
if mirrors.is_empty() {
ui::info("No enabled source repos configured");
} else {
db::repo::mirrors_status(&config.repo_clone_dir, &mirrors)?;
}
if config.binary_repos.is_empty() {
ui::info("No binary repos configured");
} else {
ui::info("Binary repo configuration:");
let host_arch = std::env::consts::ARCH;
for (name, repo) in &config.binary_repos {
let arch_keys = if repo.arch.is_empty() {
"(any)".to_string()
} else {
repo.arch.keys().cloned().collect::<Vec<_>>().join(",")
};
ui::info(format!(
" {} [{}] url={} repo_db={} arches={} host_match={}",
name,
if repo.enabled { "enabled" } else { "disabled" },
repo.url,
repo.repo_db,
arch_keys,
if repo.supports_arch(host_arch) {
"yes"
} else {
"no"
}
));
}
}
}
}
Ok(())
}
+116
View File
@@ -0,0 +1,116 @@
use super::*;
use crate::config::{Config, RepoConfigFile};
pub(crate) fn repo_kind_label(kind: RepoKindArg) -> &'static str {
match kind {
RepoKindArg::Source => "source",
RepoKindArg::Binary => "binary",
}
}
pub(crate) fn resolve_repo_kind_for_name(
repos: &RepoConfigFile,
name: &str,
kind: Option<RepoKindArg>,
) -> Result<RepoKindArg> {
if let Some(kind) = kind {
return Ok(kind);
}
let in_source = repos.source.contains_key(name);
let in_binary = repos.binary.contains_key(name);
match (in_source, in_binary) {
(true, false) => Ok(RepoKindArg::Source),
(false, true) => Ok(RepoKindArg::Binary),
(true, true) => anyhow::bail!(
"Repo '{}' exists as both source and binary; rerun with --kind source|binary",
name
),
(false, false) => anyhow::bail!("Repo '{}' not found in repos.toml", name),
}
}
pub(crate) fn print_repo_list(config: &Config) {
if config.source_repos.is_empty() && config.binary_repos.is_empty() {
ui::info("No repos configured in /etc/depot.d/repos.toml");
if !config.mirrors.is_empty() {
ui::info("Legacy mirrors.toml entries are loaded as source repos at runtime.");
}
return;
}
ui::info(format!(
"Repo settings: prefer_binary={}",
config.repo_settings.prefer_binary
));
if config.source_repos.is_empty() {
ui::info("Source repos: none");
} else {
ui::info("Source repos:");
for (name, repo) in &config.source_repos {
let subdirs = if repo.subdirs.is_empty() {
"(all)".to_string()
} else {
repo.subdirs.join(", ")
};
ui::info(format!(
" {} [{}] priority={} subdirs={} url={}",
name,
if repo.enabled { "enabled" } else { "disabled" },
repo.priority,
subdirs,
repo.url
));
}
}
if config.binary_repos.is_empty() {
ui::info("Binary repos: none");
} else {
ui::info("Binary repos:");
let host_arch = std::env::consts::ARCH;
for (name, repo) in &config.binary_repos {
let arch_keys = if repo.arch.is_empty() {
"(any)".to_string()
} else {
repo.arch.keys().cloned().collect::<Vec<_>>().join(",")
};
ui::info(format!(
" {} [{}] priority={} arches={} host_match={} repo_db={}{} url={}",
name,
if repo.enabled { "enabled" } else { "disabled" },
repo.priority,
arch_keys,
if repo.supports_arch(host_arch) {
"yes"
} else {
"no"
},
repo.repo_db,
if repo.allow_unsigned {
" allow_unsigned=true"
} else {
""
},
repo.url
));
}
}
}
pub(crate) fn selected_source_repos(
config: &Config,
name: Option<&str>,
) -> Result<std::collections::HashMap<String, String>> {
let mut mirrors = config.enabled_source_mirror_map();
if let Some(name) = name {
if let Some(url) = mirrors.remove(name) {
let mut only = std::collections::HashMap::new();
only.insert(name.to_string(), url);
return Ok(only);
}
anyhow::bail!("Enabled source repo '{}' not found", name);
}
Ok(mirrors)
}
+227
View File
@@ -0,0 +1,227 @@
use super::*;
use crate::config::Config;
pub(crate) fn scan_package_specs(dir: &Path) -> Result<Vec<PathBuf>> {
let root = dir
.canonicalize()
.with_context(|| format!("Failed to resolve scan root {}", dir.display()))?;
if !root.is_dir() {
anyhow::bail!("Scan root is not a directory: {}", root.display());
}
let mut specs = Vec::new();
for entry in WalkDir::new(&root)
.follow_links(false)
.into_iter()
.filter_entry(|entry| entry.file_name() != std::ffi::OsStr::new(".git"))
{
crate::interrupts::check()?;
let entry = entry.with_context(|| format!("Failed to walk {}", root.display()))?;
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
if path.extension().and_then(|ext| ext.to_str()) != Some("toml") {
continue;
}
let candidate = path.to_path_buf();
if package::PackageSpec::from_file(&candidate).is_ok() {
specs.push(candidate);
}
}
specs.sort();
Ok(specs)
}
fn spec_group_package_names(spec: &package::PackageSpec, group: &str) -> Vec<String> {
let mut names = Vec::new();
for output in spec.outputs() {
if spec
.dependencies_for_output(&output.name)
.groups
.iter()
.any(|candidate| candidate.eq_ignore_ascii_case(group))
{
names.push(output.name);
}
}
if spec.builds_lib32_output() {
let lib32_name = spec.lib32_package_name();
if spec
.dependencies_for_output(&lib32_name)
.groups
.iter()
.any(|candidate| candidate.eq_ignore_ascii_case(group))
{
names.push(lib32_name);
}
}
names.sort();
names.dedup();
names
}
fn collect_source_group_package_names(config: &Config, group: &str) -> Result<Vec<String>> {
if !config.repo_clone_dir.exists() {
return Ok(Vec::new());
}
let mut packages = BTreeSet::new();
for spec_path in scan_package_specs(&config.repo_clone_dir)? {
let spec = package::PackageSpec::from_file(&spec_path)
.with_context(|| format!("Failed to parse spec {}", spec_path.display()))?;
for package_name in spec_group_package_names(&spec, group) {
packages.insert(package_name);
}
}
Ok(packages.into_iter().collect())
}
fn collect_binary_group_package_names(
config: &Config,
rootfs: &Path,
group: &str,
) -> Result<Vec<String>> {
let host_arch = std::env::consts::ARCH;
let mut binary_repos: Vec<_> = 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)));
let mut packages = BTreeSet::new();
for (repo_name, repo_cfg) in binary_repos {
let matches = db::repo::find_binary_repo_packages_by_group(
repo_name,
repo_cfg,
rootfs,
&config.package_cache_dir,
group,
)?;
for record in matches {
packages.insert(record.name);
}
}
Ok(packages.into_iter().collect())
}
fn collect_install_group_package_names(
config: &Config,
rootfs: &Path,
group: &str,
) -> Result<Vec<String>> {
let mut packages = BTreeSet::new();
for package_name in collect_source_group_package_names(config, group)? {
packages.insert(package_name);
}
for package_name in collect_binary_group_package_names(config, rootfs, group)? {
packages.insert(package_name);
}
Ok(packages.into_iter().collect())
}
pub(crate) fn expand_install_requests_for_groups(
config: &Config,
rootfs: &Path,
requests: &[PathBuf],
) -> Result<(Vec<PathBuf>, Vec<String>)> {
let mut expanded = Vec::new();
let mut explicit_groups = Vec::new();
let mut seen = HashSet::new();
for request in requests {
if request.exists() {
let key = request.to_string_lossy().to_string();
if seen.insert(key) {
expanded.push(request.clone());
}
continue;
}
let request_name = request.to_string_lossy().to_string();
let group_packages = collect_install_group_package_names(config, rootfs, &request_name)?;
if group_packages.is_empty() {
if seen.insert(request_name.clone()) {
expanded.push(request.clone());
}
continue;
}
ui::info(format!(
"Expanding group '{}' ({} packages)...",
request_name,
group_packages.len()
));
explicit_groups.push(request_name);
for package_name in group_packages {
if seen.insert(package_name.clone()) {
expanded.push(PathBuf::from(package_name));
}
}
}
Ok((expanded, explicit_groups))
}
pub(crate) fn expand_installed_group_targets(
db_path: &Path,
requests: &[String],
) -> Result<(Vec<String>, Vec<String>)> {
let mut expanded = Vec::new();
let mut explicit_groups = Vec::new();
let mut seen = HashSet::new();
for request in requests {
if db::is_installed_group(db_path, request)? {
let group_packages = db::get_packages_in_installed_group(db_path, request)?;
if !group_packages.is_empty() {
ui::info(format!(
"Expanding group '{}' ({} packages)...",
request,
group_packages.len()
));
explicit_groups.push(request.clone());
for package_name in group_packages {
if seen.insert(package_name.clone()) {
expanded.push(package_name);
}
}
continue;
}
}
if seen.insert(request.clone()) {
expanded.push(request.clone());
}
}
Ok((expanded, explicit_groups))
}
pub(crate) fn human_bytes(bytes: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut value = bytes as f64;
let mut unit = 0usize;
while value >= 1024.0 && unit + 1 < UNITS.len() {
value /= 1024.0;
unit += 1;
}
if unit == 0 {
format!("{} {}", bytes, UNITS[unit])
} else {
format!("{value:.1} {}", UNITS[unit])
}
}
pub(crate) fn binary_arch_from_filename(filename: &str) -> String {
let stem = filename
.strip_suffix(".depot.pkg.tar.zst")
.unwrap_or(filename);
let mut parts = stem.rsplitn(4, '-');
parts
.next()
.filter(|s| !s.is_empty())
.unwrap_or(std::env::consts::ARCH)
.to_string()
}
+174
View File
@@ -0,0 +1,174 @@
use super::*;
use crate::config::Config;
fn source_search_hit_allowed(config: &Config, hit: &index::SourceSearchHit) -> bool {
let path = &hit.path;
if path.starts_with(Path::new("packages")) {
return true;
}
if config.source_repos.is_empty() {
return true;
}
for (repo_name, repo) in &config.source_repos {
if !repo.enabled {
continue;
}
let repo_root = config.repo_clone_dir.join(repo_name);
if repo.subdirs.is_empty() {
if path.starts_with(&repo_root) {
return true;
}
} else {
for subdir in &repo.subdirs {
if path.starts_with(repo_root.join(subdir)) {
return true;
}
}
}
}
false
}
fn source_hit_origin(config: &Config, path: &Path) -> String {
if let Ok(rel) = path.strip_prefix(&config.repo_clone_dir)
&& let Some(first) = rel.components().next()
{
return format!("source:{}", first.as_os_str().to_string_lossy());
}
"source:local".to_string()
}
pub(super) fn run_search_command(
query: &str,
files: bool,
config: &Config,
rootfs: &Path,
) -> Result<()> {
let mut any = false;
let host_arch = std::env::consts::ARCH;
let pkg_index = index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
let source_hits: Vec<_> = pkg_index
.search(query)
.into_iter()
.filter(|hit| source_search_hit_allowed(config, hit))
.collect();
if !source_hits.is_empty() {
any = true;
ui::info("Source matches:");
for hit in source_hits {
let provides = if hit.provides.is_empty() {
String::new()
} else {
format!(" provides={}", hit.provides.join(","))
};
let replaces = if hit.replaces.is_empty() {
String::new()
} else {
format!(" replaces={}", hit.replaces.join(","))
};
ui::info(format!(
" {} [{}] {}{}{}",
hit.name,
source_hit_origin(config, &hit.path),
hit.path.display(),
provides,
replaces
));
}
}
let mut binary_repos: Vec<_> = 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)));
if !binary_repos.is_empty() {
ui::info("Binary matches:");
}
let mut binary_hits_total = 0usize;
for (name, repo) in binary_repos {
match db::repo::search_binary_repo(name, repo, rootfs, &config.package_cache_dir, query) {
Ok(hits) => {
for hit in hits {
any = true;
binary_hits_total += 1;
let provides = if hit.provides.is_empty() {
String::new()
} else {
format!(" provides={}", hit.provides.join(","))
};
ui::info(format!(
" {} [binary:{}] {}-{} size={} file={}{}{}",
hit.name,
hit.repo_name,
hit.version,
hit.revision,
hit.size,
hit.filename,
hit.description
.as_ref()
.map(|d| format!(" desc={}", d))
.unwrap_or_default(),
provides
));
}
}
Err(e) => crate::log_warn!("Binary repo '{}': {}", name, e),
}
}
if !config.binary_repos.is_empty() && binary_hits_total == 0 {
ui::info(" (no binary matches)");
}
if files && !config.binary_repos.is_empty() {
ui::info("Binary file matches:");
let mut file_hits_total = 0usize;
let mut binary_repos: Vec<_> = 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 (name, repo) in binary_repos {
match db::repo::search_binary_repo_files(
name,
repo,
rootfs,
&config.package_cache_dir,
query,
) {
Ok(hits) => {
for hit in hits {
any = true;
file_hits_total += 1;
ui::info(format!(
" {} [binary:{}] {}-{} size={} owns={}",
hit.package_name,
hit.repo_name,
hit.version,
hit.revision,
hit.size,
hit.path
));
}
}
Err(e) => crate::log_warn!("Binary repo '{}': {}", name, e),
}
}
if file_hits_total == 0 {
ui::info(" (no binary file matches)");
}
}
if !any {
ui::warn(format!("No matches found for '{}'", query));
}
Ok(())
}
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
use super::*;
use crate::commands::repo::groups::expand_installed_group_targets;
pub(crate) mod candidates;
pub(crate) mod check;
pub(crate) mod versions;
use self::candidates::{
UpdateCommandOptions, run_update_command, sync_source_repositories_for_update,
};
pub(super) fn run_update(args: UpdateArgs, cli_test_deps: bool) -> Result<()> {
let UpdateArgs {
rootfs_args,
prompt_args,
build_exec_args,
packages,
} = args;
let rootfs = rootfs_args.rootfs;
let yes = prompt_args.yes;
let no_deps = build_exec_args.no_deps;
let no_flags = build_exec_args.no_flags;
let cross_prefix = build_exec_args.cross_prefix;
let clean = build_exec_args.clean;
let dry_run = build_exec_args.dry_run;
let config = config::Config::for_rootfs(&rootfs);
sync_source_repositories_for_update(&config)?;
let expanded_packages = if packages.is_empty() {
Vec::new()
} else {
expand_installed_group_targets(&config.installed_db_path(&rootfs), &packages)?.0
};
if !is_explicit_depot_self_update_request(&expanded_packages) {
ensure_depot_self_update_not_required(&config, &rootfs)?;
}
run_update_command(
&expanded_packages,
&config,
UpdateCommandOptions {
rootfs: &rootfs,
no_deps,
no_flags,
cross_prefix: cross_prefix.as_deref(),
clean,
dry_run,
assume_yes: yes,
install_test_deps: install_test_deps_enabled(cli_test_deps, &config),
},
)
}
+936
View File
@@ -0,0 +1,936 @@
use super::*;
use crate::commands::repo::groups::scan_package_specs;
#[derive(Clone, Copy)]
pub(crate) struct UpdateCommandOptions<'a> {
pub(crate) rootfs: &'a Path,
pub(crate) no_deps: bool,
pub(crate) no_flags: bool,
pub(crate) cross_prefix: Option<&'a str>,
pub(crate) clean: bool,
pub(crate) dry_run: bool,
pub(crate) assume_yes: bool,
pub(crate) install_test_deps: bool,
}
#[derive(Debug, Clone)]
pub(crate) enum UpdateOrigin {
Source {
repo_name: String,
path: PathBuf,
},
Binary {
repo_name: String,
record: Box<db::repo::BinaryRepoPackageRecord>,
},
}
#[derive(Debug, Clone)]
pub(crate) struct UpdateCandidate {
pub(crate) installed_package: String,
pub(crate) candidate_package: String,
pub(crate) replaces_installed: bool,
pub(crate) installed_version: String,
pub(crate) installed_revision: u32,
pub(crate) installed_completed_at: Option<i64>,
pub(crate) candidate_version: String,
pub(crate) candidate_revision: u32,
pub(crate) candidate_completed_at: Option<i64>,
pub(crate) runtime_dependencies: Vec<String>,
pub(crate) provides: Vec<String>,
pub(crate) conflicts: Vec<String>,
pub(crate) repo_priority: i32,
pub(crate) origin: UpdateOrigin,
}
#[derive(Debug, Clone)]
pub(crate) struct SourceUpdateCandidate {
pub(crate) repo_name: String,
pub(crate) repo_priority: i32,
pub(crate) path: PathBuf,
pub(crate) completed_at: Option<i64>,
pub(crate) spec: package::PackageSpec,
}
pub(crate) fn source_update_candidate_is_better(
candidate: &SourceUpdateCandidate,
current: &SourceUpdateCandidate,
) -> bool {
match compare_package_release(
&candidate.spec.package.version,
candidate.spec.package.revision,
&current.spec.package.version,
current.spec.package.revision,
) {
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => match compare_completed_at(candidate.completed_at, current.completed_at)
{
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => {
if candidate.repo_priority != current.repo_priority {
candidate.repo_priority < current.repo_priority
} else if candidate.repo_name != current.repo_name {
candidate.repo_name < current.repo_name
} else {
candidate.path < current.path
}
}
},
}
}
fn binary_update_candidate_is_better(
repo_name: &str,
repo_priority: i32,
record: &db::repo::BinaryRepoPackageRecord,
current_priority: i32,
current: &db::repo::BinaryRepoPackageRecord,
) -> bool {
match compare_package_release(
&record.version,
record.revision,
&current.version,
current.revision,
) {
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => match compare_completed_at(record.completed_at, current.completed_at) {
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => {
if repo_priority != current_priority {
repo_priority < current_priority
} else if repo_name != current.repo_name {
repo_name < current.repo_name.as_str()
} else {
record.filename < current.filename
}
}
},
}
}
fn compare_package_release(
left_version: &str,
left_revision: u32,
right_version: &str,
right_revision: u32,
) -> Ordering {
super::compare_versions_for_updates(left_version, right_version)
.then_with(|| left_revision.cmp(&right_revision))
}
pub(crate) fn compare_completed_at(left: Option<i64>, right: Option<i64>) -> Ordering {
match (left, right) {
(Some(left), Some(right)) => left.cmp(&right),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => Ordering::Equal,
}
}
fn update_candidate_is_newer_than_installed(
candidate_version: &str,
candidate_revision: u32,
candidate_completed_at: Option<i64>,
installed_version: &str,
installed_revision: u32,
installed_completed_at: Option<i64>,
) -> bool {
match compare_package_release(
candidate_version,
candidate_revision,
installed_version,
installed_revision,
) {
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => {
compare_completed_at(candidate_completed_at, installed_completed_at)
== Ordering::Greater
}
}
}
fn path_modified_unix_timestamp(path: &Path) -> Result<Option<i64>> {
let metadata = fs::metadata(path)
.with_context(|| format!("Failed to read metadata for {}", path.display()))?;
let modified = metadata
.modified()
.with_context(|| format!("Failed to read modification time for {}", path.display()))?;
Ok(Some(crate::metadata_time::system_time_to_unix(modified)?))
}
fn installed_package_completed_at(
installed: &db::InstalledPackageRecord,
db_path: &Path,
rootfs: &Path,
) -> Result<Option<i64>> {
if installed.completed_at.is_some() {
return Ok(installed.completed_at);
}
let mut latest = None;
for relative_path in db::get_package_files(db_path, &installed.name)? {
let path = rootfs.join(&relative_path);
if !path.exists() {
continue;
}
if let Some(modified) = path_modified_unix_timestamp(&path)? {
latest = Some(latest.map_or(modified, |current: i64| current.max(modified)));
}
}
Ok(latest)
}
fn update_origin_is_binary(origin: &UpdateOrigin) -> bool {
matches!(origin, UpdateOrigin::Binary { .. })
}
fn update_origin_label(origin: &UpdateOrigin) -> String {
match origin {
UpdateOrigin::Source { repo_name, path } => {
format!("source:{repo_name}:{}", path.display())
}
UpdateOrigin::Binary { repo_name, record } => {
format!("binary:{repo_name}:{}", record.filename)
}
}
}
pub(crate) fn update_candidate_is_preferred(
candidate: &UpdateCandidate,
current: &UpdateCandidate,
prefer_binary: bool,
) -> bool {
match compare_package_release(
&candidate.candidate_version,
candidate.candidate_revision,
&current.candidate_version,
current.candidate_revision,
) {
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => match compare_completed_at(
candidate.candidate_completed_at,
current.candidate_completed_at,
) {
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => match (
update_origin_is_binary(&candidate.origin),
update_origin_is_binary(&current.origin),
) {
(true, false) => prefer_binary,
(false, true) => !prefer_binary,
_ => {
if candidate.repo_priority != current.repo_priority {
candidate.repo_priority < current.repo_priority
} else {
update_origin_label(&candidate.origin)
< update_origin_label(&current.origin)
}
}
},
},
}
}
fn candidate_request_path(
candidate: &UpdateCandidate,
config: &config::Config,
rootfs: &Path,
) -> Result<PathBuf> {
match &candidate.origin {
UpdateOrigin::Source { path, .. } => Ok(path.clone()),
UpdateOrigin::Binary { repo_name, record } => {
let repo_cfg = config
.binary_repos
.get(repo_name)
.with_context(|| format!("Binary repo '{}' not found in config", repo_name))?;
db::repo::fetch_binary_package_archive(
repo_name,
repo_cfg,
rootfs,
record,
&config.package_cache_dir,
)
}
}
}
pub(crate) fn sync_source_repositories_for_update(config: &config::Config) -> Result<()> {
if config.repo_settings.prefer_binary {
return Ok(());
}
let mirrors = config.enabled_source_mirror_map();
if mirrors.is_empty() {
return Ok(());
}
let mut repo_lock = locking::open_lock(config)?;
let repo_lock_path = locking::lock_path(config);
let _repo_lock_guard = locking::try_write(&mut repo_lock, &repo_lock_path, "update sync")?;
db::repo::sync_mirrors(&config.repo_clone_dir, &mirrors)?;
Ok(())
}
fn configured_source_scan_roots(config: &config::Config) -> Vec<(String, i32, PathBuf)> {
let mut roots = Vec::new();
let mut repos: Vec<_> = config
.source_repos
.iter()
.filter(|(_, repo)| repo.enabled)
.collect();
repos.sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0)));
for (repo_name, repo) in repos {
let repo_root = config.repo_clone_dir.join(repo_name);
if repo.subdirs.is_empty() {
roots.push((repo_name.clone(), repo.priority, repo_root));
} else {
for subdir in &repo.subdirs {
roots.push((repo_name.clone(), repo.priority, repo_root.join(subdir)));
}
}
}
roots
}
pub(crate) fn collect_best_source_update_candidates(
config: &config::Config,
target_real_names: &HashSet<String>,
) -> Result<HashMap<String, SourceUpdateCandidate>> {
let mut best: HashMap<String, SourceUpdateCandidate> = HashMap::new();
for (repo_name, repo_priority, root) in configured_source_scan_roots(config) {
if !root.exists() {
continue;
}
for spec_path in scan_package_specs(&root)? {
let mut spec = package::PackageSpec::from_file(&spec_path)?;
spec.apply_config(config);
let stream_name = spec.package.effective_real_name().to_string();
if !target_real_names.contains(&stream_name) {
continue;
}
let candidate = SourceUpdateCandidate {
repo_name: repo_name.clone(),
repo_priority,
path: spec_path.clone(),
completed_at: path_modified_unix_timestamp(&spec_path)?,
spec,
};
let replace = match best.get(&stream_name) {
Some(current) => source_update_candidate_is_better(&candidate, current),
None => true,
};
if replace {
best.insert(stream_name, candidate);
}
}
}
Ok(best)
}
fn collect_best_source_replacement_candidates(
config: &config::Config,
target_packages: &HashSet<String>,
) -> Result<HashMap<String, SourceUpdateCandidate>> {
let mut best: HashMap<String, SourceUpdateCandidate> = HashMap::new();
for (repo_name, repo_priority, root) in configured_source_scan_roots(config) {
if !root.exists() {
continue;
}
for spec_path in scan_package_specs(&root)? {
let mut spec = package::PackageSpec::from_file(&spec_path)?;
spec.apply_config(config);
let candidate = SourceUpdateCandidate {
repo_name: repo_name.clone(),
repo_priority,
path: spec_path.clone(),
completed_at: path_modified_unix_timestamp(&spec_path)?,
spec,
};
for replaced in &candidate.spec.alternatives.replaces {
if !target_packages.contains(replaced) {
continue;
}
let replace = match best.get(replaced) {
Some(current) => source_update_candidate_is_better(&candidate, current),
None => true,
};
if replace {
best.insert(replaced.clone(), candidate.clone());
}
}
}
}
Ok(best)
}
fn collect_best_binary_update_candidates(
config: &config::Config,
rootfs: &Path,
target_real_names: &HashSet<String>,
) -> Result<HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>> {
let mut best: HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)> = HashMap::new();
let host_arch = std::env::consts::ARCH;
let mut repos: Vec<_> = config
.binary_repos
.iter()
.filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch))
.collect();
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 repos {
let records = db::repo::list_binary_repo_packages(
repo_name,
repo_cfg,
rootfs,
&config.package_cache_dir,
)
.with_context(|| format!("Failed to list binary repo '{}'", repo_name))?;
for record in records {
let stream_name = record.effective_real_name().to_string();
if !target_real_names.contains(&stream_name) {
continue;
}
let replace = match best.get(&stream_name) {
Some((current_priority, current)) => binary_update_candidate_is_better(
repo_name,
repo_cfg.priority,
&record,
*current_priority,
current,
),
None => true,
};
if replace {
best.insert(stream_name, (repo_cfg.priority, record));
}
}
}
Ok(best)
}
fn collect_best_binary_replacement_candidates(
config: &config::Config,
rootfs: &Path,
target_packages: &HashSet<String>,
) -> Result<HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>> {
let mut best: HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)> = HashMap::new();
let host_arch = std::env::consts::ARCH;
let mut repos: Vec<_> = config
.binary_repos
.iter()
.filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch))
.collect();
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 repos {
let records = db::repo::list_binary_repo_packages(
repo_name,
repo_cfg,
rootfs,
&config.package_cache_dir,
)
.with_context(|| format!("Failed to list binary repo '{}'", repo_name))?;
for record in records {
for replaced in &record.replaces {
if !target_packages.contains(replaced) {
continue;
}
let replace = match best.get(replaced) {
Some((current_priority, current)) => binary_update_candidate_is_better(
repo_name,
repo_cfg.priority,
&record,
*current_priority,
current,
),
None => true,
};
if replace {
best.insert(replaced.clone(), (repo_cfg.priority, record.clone()));
}
}
}
}
Ok(best)
}
pub(crate) fn select_update_candidate(
installed: &db::InstalledPackageRecord,
installed_completed_at: Option<i64>,
source_replacement_candidates: &HashMap<String, SourceUpdateCandidate>,
binary_replacement_candidates: &HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>,
source_candidates: &HashMap<String, SourceUpdateCandidate>,
binary_candidates: &HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>,
prefer_binary: bool,
) -> Option<UpdateCandidate> {
let mut best: Option<UpdateCandidate> = None;
let stream_name = installed.effective_real_name();
if let Some(candidate) = source_replacement_candidates.get(&installed.name) {
best = Some(UpdateCandidate {
installed_package: installed.name.clone(),
candidate_package: candidate.spec.package.name.clone(),
replaces_installed: true,
installed_version: installed.version.clone(),
installed_revision: installed.revision,
installed_completed_at,
candidate_version: candidate.spec.package.version.clone(),
candidate_revision: candidate.spec.package.revision,
candidate_completed_at: candidate.completed_at,
runtime_dependencies: candidate.spec.dependencies.runtime.clone(),
provides: candidate.spec.alternatives.provides.clone(),
conflicts: candidate.spec.alternatives.conflicts.clone(),
repo_priority: candidate.repo_priority,
origin: UpdateOrigin::Source {
repo_name: candidate.repo_name.clone(),
path: candidate.path.clone(),
},
});
}
if let Some((repo_priority, record)) = binary_replacement_candidates.get(&installed.name) {
let binary_candidate = UpdateCandidate {
installed_package: installed.name.clone(),
candidate_package: record.name.clone(),
replaces_installed: true,
installed_version: installed.version.clone(),
installed_revision: installed.revision,
installed_completed_at,
candidate_version: record.version.clone(),
candidate_revision: record.revision,
candidate_completed_at: record.completed_at,
runtime_dependencies: record.runtime_dependencies.clone(),
provides: record.provides.clone(),
conflicts: record.conflicts.clone(),
repo_priority: *repo_priority,
origin: UpdateOrigin::Binary {
repo_name: record.repo_name.clone(),
record: Box::new(record.clone()),
},
};
if best.as_ref().is_none_or(|current| {
update_candidate_is_preferred(&binary_candidate, current, prefer_binary)
}) {
best = Some(binary_candidate);
}
}
if best.is_some() {
return best;
}
if let Some(candidate) = source_candidates.get(stream_name)
&& update_candidate_is_newer_than_installed(
&candidate.spec.package.version,
candidate.spec.package.revision,
candidate.completed_at,
&installed.version,
installed.revision,
installed_completed_at,
)
{
best = Some(UpdateCandidate {
installed_package: installed.name.clone(),
candidate_package: candidate.spec.package.name.clone(),
replaces_installed: false,
installed_version: installed.version.clone(),
installed_revision: installed.revision,
installed_completed_at,
candidate_version: candidate.spec.package.version.clone(),
candidate_revision: candidate.spec.package.revision,
candidate_completed_at: candidate.completed_at,
runtime_dependencies: candidate.spec.dependencies.runtime.clone(),
provides: candidate.spec.alternatives.provides.clone(),
conflicts: candidate.spec.alternatives.conflicts.clone(),
repo_priority: candidate.repo_priority,
origin: UpdateOrigin::Source {
repo_name: candidate.repo_name.clone(),
path: candidate.path.clone(),
},
});
}
if let Some((repo_priority, record)) = binary_candidates.get(stream_name)
&& update_candidate_is_newer_than_installed(
&record.version,
record.revision,
record.completed_at,
&installed.version,
installed.revision,
installed_completed_at,
)
{
let binary_candidate = UpdateCandidate {
installed_package: installed.name.clone(),
candidate_package: record.name.clone(),
replaces_installed: false,
installed_version: installed.version.clone(),
installed_revision: installed.revision,
installed_completed_at,
candidate_version: record.version.clone(),
candidate_revision: record.revision,
candidate_completed_at: record.completed_at,
runtime_dependencies: record.runtime_dependencies.clone(),
provides: record.provides.clone(),
conflicts: record.conflicts.clone(),
repo_priority: *repo_priority,
origin: UpdateOrigin::Binary {
repo_name: record.repo_name.clone(),
record: Box::new(record.clone()),
},
};
if best.as_ref().is_none_or(|current| {
update_candidate_is_preferred(&binary_candidate, current, prefer_binary)
}) {
best = Some(binary_candidate);
}
}
best
}
pub(crate) fn collect_update_candidates(
config: &config::Config,
rootfs: &Path,
requested_packages: &[String],
) -> Result<Vec<UpdateCandidate>> {
let db_path = config.installed_db_path(rootfs);
let installed = db::list_installed_package_records(&db_path)?;
if installed.is_empty() {
return Ok(Vec::new());
}
let mut installed_by_real_name: HashMap<String, Vec<&db::InstalledPackageRecord>> =
HashMap::new();
for record in &installed {
installed_by_real_name
.entry(record.effective_real_name().to_string())
.or_default()
.push(record);
}
let active_by_real_name: HashMap<String, db::InstalledPackageRecord> = installed_by_real_name
.iter()
.filter_map(|(real_name, records)| {
super::super::select_primary_installed_record(records.iter().copied())
.cloned()
.map(|record| (real_name.clone(), record))
})
.collect();
let target_real_names: HashSet<String> = if requested_packages.is_empty() {
active_by_real_name.keys().cloned().collect()
} else {
let mut targets = HashSet::new();
for package in requested_packages {
if let Some(record) = installed.iter().find(|record| record.name == *package) {
targets.insert(record.effective_real_name().to_string());
} else if active_by_real_name.contains_key(package) {
targets.insert(package.clone());
} else {
ui::warn(format!("Package '{}' is not installed; skipping", package));
}
}
targets
};
let target_package_names: HashSet<String> = if requested_packages.is_empty() {
active_by_real_name
.values()
.map(|record| record.name.clone())
.collect()
} else {
let mut targets = HashSet::new();
for package in requested_packages {
if let Some(record) = installed.iter().find(|record| record.name == *package) {
targets.insert(record.name.clone());
} else if let Some(record) = active_by_real_name.get(package) {
targets.insert(record.name.clone());
}
}
targets
};
let source_candidates = if config.repo_settings.prefer_binary {
HashMap::new()
} else {
collect_best_source_update_candidates(config, &target_real_names)?
};
let source_replacement_candidates = if config.repo_settings.prefer_binary {
HashMap::new()
} else {
collect_best_source_replacement_candidates(config, &target_package_names)?
};
let binary_candidates =
collect_best_binary_update_candidates(config, rootfs, &target_real_names)?;
let binary_replacement_candidates =
collect_best_binary_replacement_candidates(config, rootfs, &target_package_names)?;
let mut updates = Vec::new();
let mut targets: Vec<_> = target_real_names.into_iter().collect();
targets.sort();
for target in targets {
let Some(installed) = active_by_real_name.get(&target) else {
continue;
};
let installed_completed_at = installed_package_completed_at(installed, &db_path, rootfs)?;
if let Some(candidate) = select_update_candidate(
installed,
installed_completed_at,
&source_replacement_candidates,
&binary_replacement_candidates,
&source_candidates,
&binary_candidates,
config.repo_settings.prefer_binary,
) {
updates.push(candidate);
}
}
Ok(updates)
}
pub(crate) fn collect_missing_update_dependencies(
candidates: &[UpdateCandidate],
db_path: &Path,
) -> Result<Vec<String>> {
let mut planned_provides = HashSet::new();
for candidate in candidates {
planned_provides.insert(candidate.candidate_package.clone());
for provide in &candidate.provides {
planned_provides.insert(provide.clone());
}
}
let mut missing = Vec::new();
for candidate in candidates {
for dep in &candidate.runtime_dependencies {
if planned_provides.contains(deps::dep_name(dep)) {
continue;
}
if deps::is_dep_satisfied_in_db(dep, db_path)? {
continue;
}
if !missing.contains(dep) {
missing.push(dep.clone());
}
}
}
Ok(missing)
}
fn run_update_install_command(
program: &Path,
request: &Path,
options: UpdateCommandOptions<'_>,
) -> Result<()> {
super::super::run_install_command_with_program(
program,
&[request.to_path_buf()],
options.rootfs,
super::super::ChildInstallCommandOptions {
no_deps: true,
assume_yes: true,
no_flags: options.no_flags,
cross_prefix: options.cross_prefix,
clean: options.clean,
lib32_only: false,
install_test_deps: options.install_test_deps,
install_context: Some(super::super::INSTALL_CONTEXT_UPDATE),
dep_chain: None,
},
)
}
fn confirm_update_replacements(candidates: &[UpdateCandidate]) -> Result<()> {
for candidate in candidates {
if !candidate.replaces_installed {
continue;
}
if !ui::prompt_yes_no(
&format!(
"replace {} with {}?",
candidate.installed_package, candidate.candidate_package
),
true,
)? {
anyhow::bail!(
"Replacement declined: {} -> {}",
candidate.installed_package,
candidate.candidate_package
);
}
}
Ok(())
}
pub(crate) fn run_update_command(
packages: &[String],
config: &config::Config,
options: UpdateCommandOptions<'_>,
) -> Result<()> {
let updates = collect_update_candidates(config, options.rootfs, packages)?;
if updates.is_empty() {
ui::info("All installed packages are up to date.");
return Ok(());
}
let targets: Vec<String> = updates
.iter()
.map(|candidate| {
let summary = format!(
"{} v{}-{} -> {} v{}-{}",
candidate.installed_package,
candidate.installed_version,
candidate.installed_revision,
candidate.candidate_package,
candidate.candidate_version,
candidate.candidate_revision
);
if candidate.installed_version == candidate.candidate_version
&& candidate.installed_revision == candidate.candidate_revision
&& compare_completed_at(
candidate.candidate_completed_at,
candidate.installed_completed_at,
) == Ordering::Greater
{
format!("{summary} (newer UTC build timestamp)")
} else {
summary
}
})
.collect();
let conflict_subjects: Vec<_> = updates
.iter()
.map(|candidate| super::super::InstallConflictSubject {
package: candidate.candidate_package.clone(),
provides: candidate.provides.clone(),
conflicts: candidate.conflicts.clone(),
})
.collect();
super::super::validate_no_transaction_conflicts(&conflict_subjects)?;
ui::info(format!("{} package(s) can be updated:", updates.len()));
for target in &targets {
ui::info(format!(" {}", target));
}
let db_path = config.installed_db_path(options.rootfs);
let missing_deps = if options.no_deps {
Vec::new()
} else {
collect_missing_update_dependencies(&updates, &db_path)?
};
if !missing_deps.is_empty() {
ui::info(format!(
"New dependencies required by updates: {}",
missing_deps.join(", ")
));
}
if !options.dry_run && !ui::prompt_package_action("update", &targets, true)? {
anyhow::bail!("Aborted");
}
if !options.dry_run {
confirm_update_replacements(&updates)?;
}
super::super::resolve_installed_conflicts_for_subjects(
&conflict_subjects,
options.rootfs,
config,
options.dry_run,
)?;
if !missing_deps.is_empty() {
let dep_plan = planner::build_dependency_install_plan(
config,
options.rootfs,
&missing_deps,
planner::PlannerOptions {
assume_yes: options.assume_yes,
prefer_binary: config.repo_settings.prefer_binary,
local_sibling_root: None,
include_test_deps: options.install_test_deps,
lib32_only_requested_specs: false,
},
)?;
crate::commands::print_plan_summary(&dep_plan);
if options.dry_run {
ui::info("Dry run enabled, stopping before dependency installation/update.");
return Ok(());
}
crate::commands::execute_install_plan_with_child_commands(
&dep_plan,
options.rootfs,
config,
crate::commands::InstallPlanExecutionOptions {
no_flags: options.no_flags,
cross_prefix: options.cross_prefix,
clean: options.clean,
dry_run: false,
confirm_installation: false,
lib32_only_requested_specs: false,
install_test_deps: options.install_test_deps,
},
)?;
} else if options.dry_run {
ui::info("Dry run enabled, stopping before update.");
return Ok(());
}
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
for (idx, candidate) in updates.iter().enumerate() {
ui::info(format!(
"[{}/{}] updating {} v{}-{} -> {} v{}-{}",
idx + 1,
updates.len(),
candidate.installed_package,
candidate.installed_version,
candidate.installed_revision,
candidate.candidate_package,
candidate.candidate_version,
candidate.candidate_revision
));
let request = candidate_request_path(candidate, config, options.rootfs)?;
run_update_install_command(&exe, &request, options)?;
}
if options.clean {
crate::commands::clean_build_workspace(config)?;
}
Ok(())
}
+222
View File
@@ -0,0 +1,222 @@
use super::*;
use crate::commands::update::versions::{
CheckStatus, archive_listing_probe, best_newer_version, candidate_versions_from_refs,
compare_versions_for_updates, extract_version_patterns, list_archive_versions,
remote_git_repository_from_source_url,
};
fn source_check_status(
spec: &package::PackageSpec,
source: &package::Source,
) -> Result<CheckStatus> {
let patterns = extract_version_patterns(&source.url);
if patterns.is_empty() {
anyhow::bail!("source URL does not contain $version");
}
let expanded_url = spec.expand_vars(&source.url);
let mut reasons = Vec::new();
let (candidates, source_label) =
if let Some(repo_url) = remote_git_repository_from_source_url(&expanded_url) {
match super::versions::list_remote_refs(&repo_url)
.map(|refs| candidate_versions_from_refs(&refs, &patterns))
{
Ok(candidates) if !candidates.is_empty() => {
(candidates, format!("git tags {}", repo_url))
}
Ok(_) => {
reasons.push(format!("no matching git tags found in {}", repo_url));
if let Some(probe) = archive_listing_probe(&source.url, &expanded_url) {
let candidates = list_archive_versions(&probe)?;
(candidates, format!("archive index {}", probe.listing_url))
} else {
anyhow::bail!("{}", reasons.remove(0));
}
}
Err(err) => {
reasons.push(err.to_string());
if let Some(probe) = archive_listing_probe(&source.url, &expanded_url) {
match list_archive_versions(&probe) {
Ok(candidates) => {
(candidates, format!("archive index {}", probe.listing_url))
}
Err(archive_err) => {
reasons.push(archive_err.to_string());
anyhow::bail!("{}", reasons.join("; "));
}
}
} else {
anyhow::bail!("{}", reasons.remove(0));
}
}
}
} else if let Some(probe) = archive_listing_probe(&source.url, &expanded_url) {
let candidates = list_archive_versions(&probe)?;
(candidates, format!("archive index {}", probe.listing_url))
} else {
anyhow::bail!(
"could not derive a git remote or archive index from {}",
expanded_url
);
};
if let Some(latest) =
best_newer_version(&spec.package.version, candidates.iter().map(String::as_str))
{
Ok(CheckStatus::UpdateAvailable {
latest,
source: source_label,
})
} else {
Ok(CheckStatus::UpToDate {
source: source_label,
})
}
}
fn check_package_spec(spec_path: &Path) -> CheckStatus {
let spec = match package::PackageSpec::from_file(spec_path) {
Ok(spec) => spec,
Err(err) => {
return CheckStatus::Unknown {
reason: err.to_string(),
};
}
};
let mut best_update: Option<(String, String)> = None;
let mut last_up_to_date_source: Option<String> = None;
let mut reasons = Vec::new();
for source in spec.sources() {
if let Err(err) = crate::interrupts::check() {
return CheckStatus::Unknown {
reason: err.to_string(),
};
}
match source_check_status(&spec, source) {
Ok(CheckStatus::UpdateAvailable { latest, source }) => {
let replace = match &best_update {
Some((current_best, _)) => {
compare_versions_for_updates(&latest, current_best) == Ordering::Greater
}
None => true,
};
if replace {
best_update = Some((latest, source));
}
}
Ok(CheckStatus::UpToDate { source }) => {
if last_up_to_date_source.is_none() {
last_up_to_date_source = Some(source);
}
}
Ok(CheckStatus::Unknown { reason }) => reasons.push(reason),
Err(err) => reasons.push(err.to_string()),
}
}
if let Some((latest, source)) = best_update {
return CheckStatus::UpdateAvailable { latest, source };
}
if let Some(source) = last_up_to_date_source {
return CheckStatus::UpToDate { source };
}
CheckStatus::Unknown {
reason: reasons
.into_iter()
.next()
.unwrap_or_else(|| "no versioned sources found".to_string()),
}
}
pub(crate) fn run_check_command(dir: &Path) -> Result<()> {
let scan_root = dir
.canonicalize()
.with_context(|| format!("Failed to resolve check root {}", dir.display()))?;
let specs = crate::commands::repo::groups::scan_package_specs(&scan_root)?;
if specs.is_empty() {
ui::info(format!(
"No depot package specs found under {}",
scan_root.display()
));
return Ok(());
}
let verbose = std::env::var_os("DEPOT_CHECK_VERBOSE").is_some();
let mut updates = 0usize;
let mut up_to_date = 0usize;
let mut skipped = Vec::new();
for spec_path in specs {
crate::interrupts::check()?;
let spec = match package::PackageSpec::from_file(&spec_path) {
Ok(spec) => spec,
Err(err) => {
skipped.push(format!(
"{} could not be loaded: {}",
spec_path.display(),
err
));
continue;
}
};
match check_package_spec(&spec_path) {
CheckStatus::UpdateAvailable { latest, source } => {
updates += 1;
ui::warn(format!(
"{} {} -> {} [{}] ({})",
spec.package.name,
spec.package.version,
latest,
source,
spec_path.display()
));
}
CheckStatus::UpToDate { source } => {
up_to_date += 1;
if verbose {
ui::info(format!(
"{} {} is up to date [{}] ({})",
spec.package.name,
spec.package.version,
source,
spec_path.display()
));
}
}
CheckStatus::Unknown { reason } => {
skipped.push(format!(
"{} {} could not be checked: {} ({})",
spec.package.name,
spec.package.version,
reason,
spec_path.display()
));
}
}
}
if verbose {
for entry in &skipped {
ui::warn(entry);
}
} else if !skipped.is_empty() {
ui::warn(format!(
"Skipped {} package(s) that could not be checked; set DEPOT_CHECK_VERBOSE=1 for per-package reasons.",
skipped.len()
));
}
ui::info(format!(
"Check summary: {} package(s), {} update(s), {} up to date, {} skipped",
updates + up_to_date + skipped.len(),
updates,
up_to_date,
skipped.len()
));
Ok(())
}
+553
View File
@@ -0,0 +1,553 @@
use super::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct VersionPattern {
pub(crate) prefix: String,
pub(crate) suffix: String,
}
#[derive(Debug, Clone)]
pub(crate) enum CheckStatus {
UpdateAvailable { latest: String, source: String },
UpToDate { source: String },
Unknown { reason: String },
}
#[derive(Debug, Clone)]
pub(crate) struct ArchiveListingProbe {
pub(crate) listing_url: String,
pub(crate) patterns: Vec<VersionPattern>,
}
fn strip_known_archive_suffixes(input: &str) -> &str {
for suffix in [
".tar.gz", ".tar.xz", ".tar.bz2", ".tar.zst", ".tgz", ".txz", ".tbz2", ".zip", ".tar",
".git",
] {
if let Some(stripped) = input.strip_suffix(suffix) {
return stripped;
}
}
input
}
fn version_pattern_from_template(
template: &str,
strip_archive_suffix: bool,
) -> Option<VersionPattern> {
let (prefix, suffix) = template.split_once("$version")?;
let suffix = if strip_archive_suffix {
strip_known_archive_suffixes(suffix)
} else {
suffix
};
Some(VersionPattern {
prefix: prefix.to_string(),
suffix: suffix.to_string(),
})
}
pub(crate) fn extract_version_patterns(raw: &str) -> Vec<VersionPattern> {
let mut patterns = HashSet::new();
let mut start = 0usize;
while let Some(rel_idx) = raw[start..].find("$version") {
let idx = start + rel_idx;
let prefix_start = raw[..idx]
.rfind(['/', '#', '?', '&', '='])
.map(|pos| pos + 1)
.unwrap_or(0);
let suffix_end = raw[idx + "$version".len()..]
.find(['/', '#', '?', '&', '='])
.map(|pos| idx + "$version".len() + pos)
.unwrap_or(raw.len());
if let Some(pattern) = version_pattern_from_template(&raw[prefix_start..suffix_end], true) {
patterns.insert(pattern);
}
start = idx + "$version".len();
}
let mut out: Vec<_> = patterns.into_iter().collect();
out.sort_by(|a, b| {
a.prefix
.cmp(&b.prefix)
.then_with(|| a.suffix.cmp(&b.suffix))
});
out
}
fn is_version_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '+' | '-')
}
fn looks_like_version(candidate: &str) -> bool {
!candidate.is_empty()
&& candidate.len() <= 64
&& candidate.chars().all(is_version_char)
&& candidate.chars().any(|ch| ch.is_ascii_digit())
}
fn match_version_pattern<'a>(value: &'a str, pattern: &VersionPattern) -> Option<&'a str> {
if !value.starts_with(&pattern.prefix) || !value.ends_with(&pattern.suffix) {
return None;
}
let start = pattern.prefix.len();
let end = value.len().saturating_sub(pattern.suffix.len());
if end <= start {
return None;
}
let candidate = &value[start..end];
looks_like_version(candidate).then_some(candidate)
}
fn compare_version_fallback(left: &str, right: &str) -> Ordering {
let left = left.as_bytes();
let right = right.as_bytes();
let mut li = 0usize;
let mut ri = 0usize;
while li < left.len() && ri < right.len() {
let lch = left[li] as char;
let rch = right[ri] as char;
let l_digit = lch.is_ascii_digit();
let r_digit = rch.is_ascii_digit();
if l_digit && r_digit {
let l_start = li;
let r_start = ri;
while li < left.len() && (left[li] as char).is_ascii_digit() {
li += 1;
}
while ri < right.len() && (right[ri] as char).is_ascii_digit() {
ri += 1;
}
let l_raw = std::str::from_utf8(&left[l_start..li]).unwrap_or_default();
let r_raw = std::str::from_utf8(&right[r_start..ri]).unwrap_or_default();
let l_trimmed = l_raw.trim_start_matches('0');
let r_trimmed = r_raw.trim_start_matches('0');
let l_cmp = if l_trimmed.is_empty() { "0" } else { l_trimmed };
let r_cmp = if r_trimmed.is_empty() { "0" } else { r_trimmed };
match l_cmp
.len()
.cmp(&r_cmp.len())
.then_with(|| l_cmp.cmp(r_cmp))
.then_with(|| l_raw.len().cmp(&r_raw.len()))
.then_with(|| l_raw.cmp(r_raw))
{
Ordering::Equal => {}
non_eq => return non_eq,
}
continue;
}
match lch
.to_ascii_lowercase()
.cmp(&rch.to_ascii_lowercase())
.then_with(|| lch.cmp(&rch))
{
Ordering::Equal => {
li += 1;
ri += 1;
}
non_eq => return non_eq,
}
}
left.len().cmp(&right.len())
}
fn canonical_update_version(raw: &str) -> &str {
raw.strip_prefix('v')
.filter(|rest| rest.chars().next().is_some_and(|ch| ch.is_ascii_digit()))
.unwrap_or(raw)
}
fn collapse_date_like_components(value: &str) -> String {
let parts: Vec<_> = value.split('.').collect();
if parts.len() < 3 {
return value.to_string();
}
if !parts[0].chars().all(|ch| ch.is_ascii_digit())
|| !parts[1].chars().all(|ch| ch.is_ascii_digit())
|| !parts[2].chars().all(|ch| ch.is_ascii_digit())
|| parts[0].len() != 4
|| parts[1].len() != 2
|| parts[2].len() != 2
{
return value.to_string();
}
let mut collapsed = vec![format!("{}{}{}", parts[0], parts[1], parts[2])];
collapsed.extend(parts.into_iter().skip(3).map(str::to_string));
collapsed.join(".")
}
fn normalize_comparable_version(raw: &str) -> String {
let mut value = canonical_update_version(raw).trim().to_ascii_lowercase();
if let Some(first_digit) = value.find(|ch: char| ch.is_ascii_digit())
&& first_digit > 0
&& value[..first_digit].chars().all(|ch| !ch.is_ascii_digit())
{
value = value[first_digit..].to_string();
}
let mut normalized = String::with_capacity(value.len());
let mut last_was_dot = false;
for ch in value.chars() {
let mapped = match ch {
'_' | '-' | '+' => '.',
ch if ch.is_ascii_alphanumeric() || ch == '.' => ch,
_ => continue,
};
if mapped == '.' {
if normalized.is_empty() || last_was_dot {
continue;
}
last_was_dot = true;
} else {
last_was_dot = false;
}
normalized.push(mapped);
}
while normalized.ends_with('.') {
normalized.pop();
}
collapse_date_like_components(&normalized)
}
fn numeric_component_count(value: &str) -> usize {
let mut count = 0usize;
let mut in_digits = false;
for ch in value.chars() {
if ch.is_ascii_digit() {
if !in_digits {
count += 1;
in_digits = true;
}
} else {
in_digits = false;
}
}
count
}
fn first_numeric_component_len(value: &str) -> Option<usize> {
let start = value.find(|ch: char| ch.is_ascii_digit())?;
Some(
value[start..]
.chars()
.take_while(|ch| ch.is_ascii_digit())
.count(),
)
}
fn is_prerelease_version(value: &str) -> bool {
let normalized = normalize_comparable_version(value);
[
"alpha", "beta", "rc", "pre", "preview", "snapshot", "nightly", "dev",
]
.into_iter()
.any(|marker| normalized.contains(marker))
}
fn normalize_candidate_version(current: &str, candidate: &str) -> Option<String> {
let normalized = normalize_comparable_version(candidate);
if !looks_like_version(&normalized) {
return None;
}
if !is_prerelease_version(current) && is_prerelease_version(candidate) {
return None;
}
let current_normalized = normalize_comparable_version(current);
if numeric_component_count(&current_normalized) >= 2
&& numeric_component_count(&normalized) == 1
&& first_numeric_component_len(&current_normalized).is_some_and(|len| len <= 4)
{
return None;
}
Some(normalized)
}
pub(crate) fn compare_versions_for_updates(left: &str, right: &str) -> Ordering {
let left = normalize_comparable_version(left);
let right = normalize_comparable_version(right);
if let (Ok(left), Ok(right)) = (
semver::Version::parse(&left),
semver::Version::parse(&right),
) {
match left.cmp(&right) {
Ordering::Equal => {}
non_eq => return non_eq,
}
}
if left.len() == 8
&& right.len() == 8
&& left.chars().all(|ch| ch.is_ascii_digit())
&& right.chars().all(|ch| ch.is_ascii_digit())
{
return left.cmp(&right);
}
compare_version_fallback(&left, &right)
}
pub(crate) fn best_newer_version<'a>(
current: &str,
candidates: impl IntoIterator<Item = &'a str>,
) -> Option<String> {
let mut best: Option<String> = None;
for candidate in candidates {
let Some(candidate) = normalize_candidate_version(current, candidate) else {
continue;
};
if compare_versions_for_updates(&candidate, current) != Ordering::Greater {
continue;
}
if let Some(existing) = best.as_deref()
&& compare_versions_for_updates(&candidate, existing) != Ordering::Greater
{
continue;
}
best = Some(candidate);
}
best
}
pub(crate) fn remote_git_repository_from_source_url(expanded_url: &str) -> Option<String> {
if let Some((base, _)) = source_git_url_parts(expanded_url) {
return Some(base);
}
let parsed = Url::parse(expanded_url).ok()?;
let host = parsed.host_str()?;
let segments: Vec<_> = parsed.path_segments()?.collect();
if segments.len() < 3 {
return None;
}
let keyword_idx = segments
.iter()
.position(|segment| matches!(*segment, "releases" | "archive"))?;
let repo_segments = if keyword_idx > 0 && segments.get(keyword_idx - 1) == Some(&"-") {
&segments[..keyword_idx - 1]
} else {
&segments[..keyword_idx]
};
if repo_segments.len() < 2 {
return None;
}
Some(format!(
"{}://{}/{}.git",
parsed.scheme(),
host,
repo_segments
.iter()
.enumerate()
.map(|(idx, segment)| {
if idx + 1 == repo_segments.len() {
segment.strip_suffix(".git").unwrap_or(segment)
} else {
segment
}
})
.collect::<Vec<_>>()
.join("/")
))
}
fn source_git_url_parts(url: &str) -> Option<(String, String)> {
if let Some((base, rev)) = url.split_once('#') {
let lower = base.to_ascii_lowercase();
let is_archive = lower.ends_with(".tar.gz")
|| lower.ends_with(".tgz")
|| lower.ends_with(".tar.xz")
|| lower.ends_with(".txz")
|| lower.ends_with(".tar.bz2")
|| lower.ends_with(".tbz2")
|| lower.ends_with(".zip")
|| lower.ends_with(".tar");
if is_archive {
return None;
}
let resolved_rev = if rev.trim().is_empty() { "HEAD" } else { rev };
return Some((base.to_string(), resolved_rev.to_string()));
}
url.to_ascii_lowercase()
.ends_with(".git")
.then(|| (url.to_string(), "HEAD".to_string()))
}
pub(super) fn list_remote_refs(url: &str) -> Result<Vec<String>> {
crate::interrupts::check()?;
let mut remote = git2::Remote::create_detached(url)
.with_context(|| format!("Failed to create detached git remote for {}", url))?;
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.sideband_progress(|_| !crate::interrupts::was_interrupted());
callbacks.transfer_progress(|_| !crate::interrupts::was_interrupted());
let refs = {
let connection = match remote.connect_auth(Direction::Fetch, Some(callbacks), None) {
Ok(connection) => connection,
Err(_err) if crate::interrupts::was_interrupted() => {
anyhow::bail!("Interrupted by Ctrl-C while checking {}", url);
}
Err(err) => {
return Err(err)
.with_context(|| format!("Failed to connect to git remote {}", url));
}
};
crate::interrupts::check()?;
let refs = connection
.list()
.with_context(|| format!("Failed to list refs for git remote {}", url))?
.iter()
.map(|head| head.name().trim_end_matches("^{}").to_string())
.collect();
drop(connection);
refs
};
remote.disconnect().ok();
Ok(refs)
}
pub(crate) fn candidate_versions_from_refs(
refs: &[String],
patterns: &[VersionPattern],
) -> Vec<String> {
let mut versions = HashSet::new();
for name in refs {
let Some(short) = name.strip_prefix("refs/tags/") else {
continue;
};
for pattern in patterns {
if let Some(candidate) = match_version_pattern(short, pattern) {
versions.insert(candidate.to_string());
}
}
}
let mut out: Vec<_> = versions.into_iter().collect();
out.sort_by(|a, b| compare_versions_for_updates(a, b));
out
}
pub(crate) fn archive_listing_probe(
raw_url: &str,
expanded_url: &str,
) -> Option<ArchiveListingProbe> {
let raw = Url::parse(raw_url).ok()?;
let expanded = Url::parse(expanded_url).ok()?;
if !matches!(expanded.scheme(), "http" | "https") {
return None;
}
let raw_segments: Vec<_> = raw.path_segments()?.collect();
let expanded_segments: Vec<_> = expanded.path_segments()?.collect();
if raw_segments.len() != expanded_segments.len() {
return None;
}
let first_version_idx = raw_segments
.iter()
.position(|segment| segment.contains("$version"))?;
let pattern = version_pattern_from_template(raw_segments[first_version_idx], false)?;
let mut listing_url = expanded.clone();
let listing_path = if first_version_idx == 0 {
"/".to_string()
} else {
format!("/{}/", expanded_segments[..first_version_idx].join("/"))
};
listing_url.set_path(&listing_path);
listing_url.set_query(None);
listing_url.set_fragment(None);
Some(ArchiveListingProbe {
listing_url: listing_url.to_string(),
patterns: vec![pattern],
})
}
fn archive_listing_tokens(body: &str) -> HashSet<String> {
let mut tokens = HashSet::new();
for token in body.split(|ch: char| {
ch.is_ascii_whitespace() || matches!(ch, '"' | '\'' | '<' | '>' | '(' | ')' | '[' | ']')
}) {
let token = token
.split_once('?')
.map(|(value, _)| value)
.unwrap_or(token)
.split_once('#')
.map(|(value, _)| value)
.unwrap_or(token)
.trim_matches(|ch: char| matches!(ch, ',' | ';' | '='))
.trim_end_matches('/');
if token.is_empty() {
continue;
}
let basename = token.rsplit('/').next().unwrap_or(token);
if !basename.is_empty() {
tokens.insert(basename.to_string());
}
}
tokens
}
pub(crate) fn candidate_versions_from_listing(
body: &str,
patterns: &[VersionPattern],
) -> Vec<String> {
let mut versions = HashSet::new();
for token in archive_listing_tokens(body) {
for pattern in patterns {
if let Some(candidate) = match_version_pattern(&token, pattern) {
versions.insert(candidate.to_string());
}
}
}
let mut out: Vec<_> = versions.into_iter().collect();
out.sort_by(|a, b| compare_versions_for_updates(a, b));
out
}
pub(crate) fn list_archive_versions(probe: &ArchiveListingProbe) -> Result<Vec<String>> {
crate::interrupts::check()?;
let client = source::build_blocking_client(
&format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
Some(Duration::from_secs(20)),
)?;
let response = client
.get(&probe.listing_url)
.send()
.with_context(|| format!("Failed to fetch archive index {}", probe.listing_url))?;
if !response.status().is_success() {
anyhow::bail!(
"archive index {} returned {}",
probe.listing_url,
response.status()
);
}
let body = response
.text()
.with_context(|| format!("Failed to read archive index {}", probe.listing_url))?;
crate::interrupts::check()?;
let candidates = candidate_versions_from_listing(&body, &probe.patterns);
if candidates.is_empty() {
anyhow::bail!("no matching archive entries found in {}", probe.listing_url);
}
Ok(candidates)
}
+72 -13
View File
@@ -159,6 +159,34 @@ fn repo_settings_is_default(settings: &RepoSettings) -> bool {
!settings.prefer_binary !settings.prefer_binary
} }
fn normalize_append_key(raw: &str) -> String {
raw.split('.')
.map(str::trim)
.filter(|part| !part.is_empty())
.map(|part| {
let stripped = if (part.starts_with('"') && part.ends_with('"'))
|| (part.starts_with('\'') && part.ends_with('\''))
{
&part[1..part.len() - 1]
} else {
part
};
stripped.trim().to_ascii_lowercase()
})
.collect::<Vec<_>>()
.join(".")
}
fn scope_append_key(scope: &str, key: &str) -> String {
let key = normalize_append_key(key);
let scope = normalize_append_key(scope);
if key == scope || key.starts_with(&format!("{scope}.")) {
key
} else {
format!("{scope}.{key}")
}
}
/// Return the canonical `repos.toml` path for the given rootfs. /// Return the canonical `repos.toml` path for the given rootfs.
pub fn repos_toml_path(rootfs: &Path) -> PathBuf { pub fn repos_toml_path(rootfs: &Path) -> PathBuf {
resolve_rootfs_base(rootfs).join("etc/depot.d/repos.toml") resolve_rootfs_base(rootfs).join("etc/depot.d/repos.toml")
@@ -345,7 +373,7 @@ impl Config {
let (val, appends) = self.preprocess_toml(&content)?; let (val, appends) = self.preprocess_toml(&content)?;
merge_toml_values(&mut self.build_overrides, &val); merge_toml_values(&mut self.build_overrides, &val);
for (k, v) in appends { for (k, v) in appends {
self.appends.insert(format!("build.{}", k), v); self.appends.insert(scope_append_key("build", &k), v);
} }
} }
@@ -359,7 +387,7 @@ impl Config {
let (val, appends) = self.preprocess_toml(&content)?; let (val, appends) = self.preprocess_toml(&content)?;
merge_toml_values(&mut self.build_overrides, &val); merge_toml_values(&mut self.build_overrides, &val);
for (k, v) in appends { for (k, v) in appends {
self.appends.insert(format!("build.{}", k), v); self.appends.insert(scope_append_key("build", &k), v);
} }
} }
@@ -376,7 +404,7 @@ impl Config {
let (val, appends) = self.preprocess_toml(&content)?; let (val, appends) = self.preprocess_toml(&content)?;
merge_toml_values(&mut self.package_overrides, &val); merge_toml_values(&mut self.package_overrides, &val);
for (k, v) in appends { for (k, v) in appends {
self.appends.insert(format!("package.{}", k), v); self.appends.insert(scope_append_key("package", &k), v);
} }
} }
@@ -390,7 +418,7 @@ impl Config {
let (val, appends) = self.preprocess_toml(&content)?; let (val, appends) = self.preprocess_toml(&content)?;
merge_toml_values(&mut self.package_overrides, &val); merge_toml_values(&mut self.package_overrides, &val);
for (k, v) in appends { for (k, v) in appends {
self.appends.insert(format!("package.{}", k), v); self.appends.insert(scope_append_key("package", &k), v);
} }
} }
@@ -482,9 +510,25 @@ impl Config {
) -> Result<(toml::Value, HashMap<String, Vec<toml::Value>>)> { ) -> Result<(toml::Value, HashMap<String, Vec<toml::Value>>)> {
let mut base_text = String::new(); let mut base_text = String::new();
let mut appends = HashMap::new(); let mut appends = HashMap::new();
let mut current_table: Option<String> = None;
let mut in_array_table = false;
for line in input.lines() { for line in input.lines() {
let trimmed = line.trim(); let trimmed = line.trim();
if trimmed.starts_with("[[") && trimmed.ends_with("]]") && trimmed.len() >= 4 {
current_table = Some(normalize_append_key(&trimmed[2..trimmed.len() - 2]));
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(normalize_append_key(&trimmed[1..trimmed.len() - 1]));
in_array_table = false;
base_text.push_str(line);
base_text.push('\n');
continue;
}
if trimmed.is_empty() || trimmed.starts_with('#') { if trimmed.is_empty() || trimmed.starts_with('#') {
base_text.push_str(line); base_text.push_str(line);
base_text.push('\n'); base_text.push('\n');
@@ -492,7 +536,13 @@ impl Config {
} }
if let Some(plus_idx) = trimmed.find("+=") { if let Some(plus_idx) = trimmed.find("+=") {
let key = trimmed[..plus_idx].trim().to_string(); if in_array_table {
anyhow::bail!(
"'+=' is not supported inside array-of-table sections ({})",
current_table.as_deref().unwrap_or("")
);
}
let key = normalize_append_key(trimmed[..plus_idx].trim());
let val_str = trimmed[plus_idx + 2..].trim(); let val_str = trimmed[plus_idx + 2..].trim();
let val: toml::Value = toml::from_str::<toml::Value>(&format!("v = {}", val_str)) let val: toml::Value = toml::from_str::<toml::Value>(&format!("v = {}", val_str))
.context("Failed to parse append value")? .context("Failed to parse append value")?
@@ -500,7 +550,16 @@ impl Config {
.cloned() .cloned()
.unwrap(); .unwrap();
appends.entry(key).or_insert_with(Vec::new).push(val); let full_key = if key.contains('.') {
key
} else if let Some(table) = current_table.as_deref() {
format!("{table}.{key}")
} else {
key
};
appends.entry(full_key).or_insert_with(Vec::new).push(val);
base_text.push('\n');
} else { } else {
base_text.push_str(line); base_text.push_str(line);
base_text.push('\n'); base_text.push('\n');
@@ -574,16 +633,16 @@ ldflags += "-L/usr/local/lib"
); );
// Appends should be captured // Appends should be captured
assert!(appends.contains_key("cflags")); assert!(appends.contains_key("flags.cflags"));
assert_eq!(appends.get("cflags").unwrap().len(), 1); assert_eq!(appends.get("flags.cflags").unwrap().len(), 1);
assert_eq!( assert_eq!(
appends.get("cflags").unwrap()[0].as_array().unwrap()[0].as_str(), appends.get("flags.cflags").unwrap()[0].as_array().unwrap()[0].as_str(),
Some("-DDEBUG") Some("-DDEBUG")
); );
assert!(appends.contains_key("ldflags")); assert!(appends.contains_key("flags.ldflags"));
assert_eq!( assert_eq!(
appends.get("ldflags").unwrap()[0].as_str(), appends.get("flags.ldflags").unwrap()[0].as_str(),
Some("-L/usr/local/lib") Some("-L/usr/local/lib")
); );
} }
@@ -615,9 +674,9 @@ cflags += ["-g"]
.map(|a| a.len()), .map(|a| a.len()),
Some(1) Some(1)
); );
assert!(config.appends.contains_key("build.cflags")); assert!(config.appends.contains_key("build.flags.cflags"));
assert_eq!( assert_eq!(
config.appends.get("build.cflags").unwrap()[0] config.appends.get("build.flags.cflags").unwrap()[0]
.as_array() .as_array()
.unwrap()[0] .unwrap()[0]
.as_str(), .as_str(),
+8 -8
View File
@@ -54,8 +54,8 @@ impl<R> HashingReader<R> {
fn finalize_hex(self) -> (String, String) { fn finalize_hex(self) -> (String, String) {
( (
format!("{:x}", self.sha256.finalize()), crate::hex::encode_lower(self.sha256.finalize()),
format!("{:x}", self.sha512.finalize()), crate::hex::encode_lower(self.sha512.finalize()),
) )
} }
} }
@@ -2151,7 +2151,7 @@ fn verify_hex_digest(path: &Path, algorithm: &str, expected_hex: &str) -> Result
} }
h.update(&buf[..n]); h.update(&buf[..n]);
} }
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
} }
"sha512" => { "sha512" => {
use sha2::{Digest, Sha512}; use sha2::{Digest, Sha512};
@@ -2163,7 +2163,7 @@ fn verify_hex_digest(path: &Path, algorithm: &str, expected_hex: &str) -> Result
} }
h.update(&buf[..n]); h.update(&buf[..n]);
} }
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
} }
_ => anyhow::bail!("Unsupported checksum algorithm: {}", algorithm), _ => anyhow::bail!("Unsupported checksum algorithm: {}", algorithm),
}; };
@@ -2988,12 +2988,12 @@ revision = 1
let sha256 = { let sha256 = {
let mut h = Sha256::new(); let mut h = Sha256::new();
h.update(b"payload"); h.update(b"payload");
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
let sha512 = { let sha512 = {
let mut h = Sha512::new(); let mut h = Sha512::new();
h.update(b"payload"); h.update(b"payload");
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
let rec = BinaryRepoPackageRecord { let rec = BinaryRepoPackageRecord {
@@ -3028,12 +3028,12 @@ revision = 1
let sha256 = { let sha256 = {
let mut h = Sha256::new(); let mut h = Sha256::new();
h.update(payload); h.update(payload);
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
let sha512 = { let sha512 = {
let mut h = Sha512::new(); let mut h = Sha512::new();
h.update(payload); h.update(payload);
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
BinaryRepoPackageRecord { BinaryRepoPackageRecord {
+9
View File
@@ -0,0 +1,9 @@
pub(crate) fn encode_lower(bytes: impl AsRef<[u8]>) -> String {
let bytes = bytes.as_ref();
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(&mut out, "{byte:02x}");
}
out
}
+114 -22
View File
@@ -24,6 +24,7 @@
use crate::fakeroot; use crate::fakeroot;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use std::collections::HashSet;
use std::fs; use std::fs;
use std::io::{ErrorKind, Write}; use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -225,7 +226,8 @@ pub fn run_transaction_hooks(rootfs: &Path, ctx: &HookExecutionContext<'_>) -> R
/// Load and execute hooks for a batch of transaction contexts in stable order. /// Load and execute hooks for a batch of transaction contexts in stable order.
/// ///
/// Hooks are discovered and parsed once, then matched against each context in /// Hooks are discovered and parsed once, then matched against each context in
/// input order. Matching hooks run in hook-file order for each context. /// input order. Each matching hook runs once per hook file and operation, with
/// affected paths aggregated across all matching contexts for that batch.
/// ///
/// Returns the number of hook commands executed. /// Returns the number of hook commands executed.
pub fn run_transaction_hooks_batch( pub fn run_transaction_hooks_batch(
@@ -251,34 +253,80 @@ pub fn run_transaction_hooks_batch(
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct ScheduledHookRun { struct ScheduledHookRun {
hook_idx: usize, hook_idx: usize,
ctx_idx: usize, operation: HookOperation,
first_ctx_idx: usize,
}
fn push_unique_normalized_paths(
out: &mut Vec<String>,
seen: &mut HashSet<String>,
paths: &[String],
) {
for path in normalize_affected_paths(paths) {
if seen.insert(path.clone()) {
out.push(path);
}
}
} }
let mut scheduled = Vec::new(); let mut scheduled = Vec::new();
for (ctx_idx, ctx_owned) in contexts.iter().enumerate() { for (hook_idx, hook) in hooks.iter().enumerate() {
let normalized_paths = normalize_affected_paths(&ctx_owned.affected_paths); let mut matched_operations = Vec::new();
let ctx = HookExecutionContext { for (ctx_idx, ctx_owned) in contexts.iter().enumerate() {
phase, let normalized_paths = normalize_affected_paths(&ctx_owned.affected_paths);
operation: ctx_owned.operation, let ctx = HookExecutionContext {
package: &ctx_owned.package, phase,
affected_paths: &ctx_owned.affected_paths, operation: ctx_owned.operation,
}; package: &ctx_owned.package,
for (hook_idx, hook) in hooks.iter().enumerate() { affected_paths: &ctx_owned.affected_paths,
if hook.matches(&ctx, &normalized_paths) { };
scheduled.push(ScheduledHookRun { hook_idx, ctx_idx }); if hook.matches(&ctx, &normalized_paths)
&& !matched_operations.contains(&ctx_owned.operation)
{
matched_operations.push(ctx_owned.operation);
scheduled.push(ScheduledHookRun {
hook_idx,
operation: ctx_owned.operation,
first_ctx_idx: ctx_idx,
});
} }
} }
} }
let total = scheduled.len(); let total = scheduled.len();
for (run_idx, run) in scheduled.into_iter().enumerate() { for (run_idx, run) in scheduled.into_iter().enumerate() {
let ctx_owned = &contexts[run.ctx_idx]; let primary_ctx = &contexts[run.first_ctx_idx];
let normalized_paths = normalize_affected_paths(&ctx_owned.affected_paths); let mut normalized_paths = Vec::new();
let mut seen_paths = HashSet::new();
push_unique_normalized_paths(
&mut normalized_paths,
&mut seen_paths,
&primary_ctx.affected_paths,
);
for ctx_owned in contexts.iter().skip(run.first_ctx_idx + 1) {
if ctx_owned.operation != run.operation {
continue;
}
let ctx = HookExecutionContext {
phase,
operation: ctx_owned.operation,
package: &ctx_owned.package,
affected_paths: &ctx_owned.affected_paths,
};
let ctx_paths = normalize_affected_paths(&ctx_owned.affected_paths);
if hooks[run.hook_idx].matches(&ctx, &ctx_paths) {
push_unique_normalized_paths(
&mut normalized_paths,
&mut seen_paths,
&ctx_owned.affected_paths,
);
}
}
let ctx = HookExecutionContext { let ctx = HookExecutionContext {
phase, phase,
operation: ctx_owned.operation, operation: primary_ctx.operation,
package: &ctx_owned.package, package: &primary_ctx.package,
affected_paths: &ctx_owned.affected_paths, affected_paths: &primary_ctx.affected_paths,
}; };
run_hook_command( run_hook_command(
rootfs, rootfs,
@@ -758,7 +806,8 @@ phase = "post"
operation = ["install"] operation = ["install"]
[exec] [exec]
command = "printf '%s\n' \"$DEPOT_PACKAGE\" >> \"$DEPOT_ROOTFS/batch.out\"" command = "cat >> \"$DEPOT_ROOTFS/batch.out\""
needs_paths = true
"#, "#,
); );
@@ -766,18 +815,61 @@ command = "printf '%s\n' \"$DEPOT_PACKAGE\" >> \"$DEPOT_ROOTFS/batch.out\""
HookExecutionContextOwned { HookExecutionContextOwned {
operation: HookOperation::Install, operation: HookOperation::Install,
package: "foo".to_string(), package: "foo".to_string(),
affected_paths: Vec::new(), affected_paths: vec!["usr/bin/foo".to_string()],
}, },
HookExecutionContextOwned { HookExecutionContextOwned {
operation: HookOperation::Install, operation: HookOperation::Install,
package: "bar".to_string(), package: "bar".to_string(),
affected_paths: Vec::new(), affected_paths: vec!["usr/share/doc/bar".to_string()],
},
];
let ran = run_transaction_hooks_batch(tmp.path(), HookPhase::Post, &contexts).unwrap();
assert_eq!(ran, 1);
let out = std::fs::read_to_string(tmp.path().join("batch.out")).unwrap();
assert_eq!(out, "usr/bin/foo\nusr/share/doc/bar\n");
}
#[test]
fn run_transaction_hooks_batch_runs_once_per_operation() {
let tmp = tempfile::tempdir().unwrap();
write_hook(
tmp.path(),
"batch.toml",
r#"
[hook]
name = "batch"
[when]
phase = "post"
operation = ["install", "update"]
[exec]
command = "printf '%s:%s\n' \"$DEPOT_ACTION\" \"$DEPOT_PACKAGE\" >> \"$DEPOT_ROOTFS/batch.out\""
"#,
);
let contexts = vec![
HookExecutionContextOwned {
operation: HookOperation::Install,
package: "foo".to_string(),
affected_paths: vec!["usr/bin/foo".to_string()],
},
HookExecutionContextOwned {
operation: HookOperation::Install,
package: "bar".to_string(),
affected_paths: vec!["usr/share/doc/bar".to_string()],
},
HookExecutionContextOwned {
operation: HookOperation::Update,
package: "baz".to_string(),
affected_paths: vec!["usr/bin/baz".to_string()],
}, },
]; ];
let ran = run_transaction_hooks_batch(tmp.path(), HookPhase::Post, &contexts).unwrap(); let ran = run_transaction_hooks_batch(tmp.path(), HookPhase::Post, &contexts).unwrap();
assert_eq!(ran, 2); assert_eq!(ran, 2);
let out = std::fs::read_to_string(tmp.path().join("batch.out")).unwrap(); let out = std::fs::read_to_string(tmp.path().join("batch.out")).unwrap();
assert_eq!(out, "foo\nbar\n"); assert_eq!(out, "install:foo\nupdate:baz\n");
} }
} }
+2
View File
@@ -1,6 +1,7 @@
//! Depot - Not Your Average Package Manager //! Depot - Not Your Average Package Manager
//! A source-based package manager for Linux //! A source-based package manager for Linux
mod build_options;
mod builder; mod builder;
mod cli; mod cli;
mod cli_assets; mod cli_assets;
@@ -10,6 +11,7 @@ mod cross;
mod db; mod db;
mod deps; mod deps;
mod fakeroot; mod fakeroot;
mod hex;
mod index; mod index;
mod install; mod install;
mod interrupts; mod interrupts;
+2 -2
View File
@@ -44,7 +44,7 @@ fn compute_sha256_for_url(u: &str) -> anyhow::Result<String> {
} }
hasher.update(&buf[..n]); hasher.update(&buf[..n]);
} }
Ok(format!("{:x}", hasher.finalize())) Ok(crate::hex::encode_lower(hasher.finalize()))
} }
// Try to parse as URL first; if parsing fails, treat as local path. // Try to parse as URL first; if parsing fails, treat as local path.
@@ -2261,7 +2261,7 @@ mod tests {
let expected = { let expected = {
let mut h = TestSha256::new(); let mut h = TestSha256::new();
h.update(b"abc"); h.update(b"abc");
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
// plain path // plain path
+170 -22
View File
@@ -100,6 +100,7 @@ impl PackageSpec {
appends: &std::collections::HashMap<String, Vec<toml::Value>>, appends: &std::collections::HashMap<String, Vec<toml::Value>>,
) -> Result<()> { ) -> Result<()> {
for (key, values) in appends { for (key, values) in appends {
let key = normalize_append_key(key);
if let Some(subkey) = key.strip_prefix("build.flags.") { if let Some(subkey) = key.strip_prefix("build.flags.") {
self.apply_append(subkey, values); self.apply_append(subkey, values);
continue; continue;
@@ -109,7 +110,7 @@ impl PackageSpec {
continue; continue;
} }
if !key.contains('.') { if !key.contains('.') {
self.apply_append(key, values); self.apply_append(&key, values);
continue; continue;
} }
anyhow::bail!("Unsupported '+=' key in package spec: {}", key); anyhow::bail!("Unsupported '+=' key in package spec: {}", key);
@@ -323,11 +324,8 @@ impl PackageSpec {
.package_alternatives .package_alternatives
.get(pkg_name) .get(pkg_name)
.cloned() .cloned()
.unwrap_or_else(|| { .or_else(|| self.alternatives.lib32_alternatives())
self.alternatives .unwrap_or_default();
.lib32_alternatives()
.unwrap_or_else(|| self.alternatives.primary_alternatives())
});
} }
if self.docs_parent_output_name(pkg_name).is_some() { if self.docs_parent_output_name(pkg_name).is_some() {
@@ -351,8 +349,11 @@ impl PackageSpec {
// Apply appends from /etc/depot.d/build.toml (e.g. build.flags.cflags += ["-O3"]) // Apply appends from /etc/depot.d/build.toml (e.g. build.flags.cflags += ["-O3"])
for (key, values) in &config.appends { for (key, values) in &config.appends {
let key = normalize_append_key(key);
if let Some(subkey) = key.strip_prefix("build.flags.") { if let Some(subkey) = key.strip_prefix("build.flags.") {
self.apply_append(subkey, values); self.apply_append(subkey, values);
} else if let Some(subkey) = key.strip_prefix("build.") {
self.apply_append(subkey, values);
} }
} }
} }
@@ -981,7 +982,8 @@ impl PackageSpec {
} }
fn apply_append(&mut self, key: &str, values: &[toml::Value]) { fn apply_append(&mut self, key: &str, values: &[toml::Value]) {
match key { let key = normalize_append_key(key);
match key.as_str() {
"cflags" => { "cflags" => {
for v in values { for v in values {
if let Some(arr) = v.as_array() { if let Some(arr) = v.as_array() {
@@ -1675,14 +1677,14 @@ fn preprocess_spec_toml_appends(
let trimmed = line.trim(); let trimmed = line.trim();
if trimmed.starts_with("[[") && trimmed.ends_with("]]") && trimmed.len() >= 4 { if trimmed.starts_with("[[") && trimmed.ends_with("]]") && trimmed.len() >= 4 {
current_table = Some(trimmed[2..trimmed.len() - 2].trim().to_string()); current_table = Some(normalize_append_key(trimmed[2..trimmed.len() - 2].trim()));
in_array_table = true; in_array_table = true;
base_text.push_str(line); base_text.push_str(line);
base_text.push('\n'); base_text.push('\n');
continue; continue;
} }
if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 { if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 {
current_table = Some(trimmed[1..trimmed.len() - 1].trim().to_string()); current_table = Some(normalize_append_key(trimmed[1..trimmed.len() - 1].trim()));
in_array_table = false; in_array_table = false;
base_text.push_str(line); base_text.push_str(line);
base_text.push('\n'); base_text.push('\n');
@@ -1702,7 +1704,7 @@ fn preprocess_spec_toml_appends(
current_table.as_deref().unwrap_or("") current_table.as_deref().unwrap_or("")
); );
} }
let key = trimmed[..plus_idx].trim(); let key = normalize_append_key(trimmed[..plus_idx].trim());
let val_str = trimmed[plus_idx + 2..].trim(); let val_str = trimmed[plus_idx + 2..].trim();
let val: toml::Value = toml::from_str::<toml::Value>(&format!("v = {}", val_str)) let val: toml::Value = toml::from_str::<toml::Value>(&format!("v = {}", val_str))
.context("Failed to parse append value")? .context("Failed to parse append value")?
@@ -1711,11 +1713,11 @@ fn preprocess_spec_toml_appends(
.unwrap(); .unwrap();
let full_key = if key.contains('.') { let full_key = if key.contains('.') {
key.to_string() key
} else if let Some(table) = current_table.as_deref() { } else if let Some(table) = current_table.as_deref() {
format!("{}.{}", table, key) format!("{}.{}", table, key)
} else { } else {
key.to_string() key
}; };
appends.entry(full_key).or_insert_with(Vec::new).push(val); appends.entry(full_key).or_insert_with(Vec::new).push(val);
@@ -1731,6 +1733,24 @@ fn preprocess_spec_toml_appends(
Ok((base_text, appends)) Ok((base_text, appends))
} }
fn normalize_append_key(raw: &str) -> String {
raw.split('.')
.map(str::trim)
.filter(|part| !part.is_empty())
.map(|part| {
let stripped = if (part.starts_with('"') && part.ends_with('"'))
|| (part.starts_with('\'') && part.ends_with('\''))
{
&part[1..part.len() - 1]
} else {
part
};
stripped.trim().to_ascii_lowercase()
})
.collect::<Vec<_>>()
.join(".")
}
#[cfg(test)] #[cfg(test)]
mod spec_tests { mod spec_tests {
use super::*; use super::*;
@@ -2119,6 +2139,48 @@ replaces = ["lib32-clang"]
); );
} }
#[test]
fn lib32_output_does_not_fallback_to_primary_alternatives() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "llvm"
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]
build_32 = true
[alternatives]
provides = ["toolchain"]
conflicts = ["gcc"]
replaces = ["clang"]
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
let lib32 = spec.alternatives_for_output("lib32-llvm");
assert!(lib32.provides.is_empty());
assert!(lib32.conflicts.is_empty());
assert!(lib32.replaces.is_empty());
}
#[test] #[test]
fn parse_python_build_type() { fn parse_python_build_type() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -3419,6 +3481,102 @@ replace_rustflags += "opt-level=3=>opt-level=z"
); );
} }
#[test]
fn parse_build_flags_appends_accepts_quoted_and_uppercase_keys() {
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]
"cflags" += ["-fPIC"]
CXXFLAGS += ["-stdlib=libc++"]
"LDFLAGS" += "-Wl,--as-needed"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.build.flags.cflags, vec!["-fPIC".to_string()]);
assert_eq!(
spec.build.flags.cxxflags,
vec!["-stdlib=libc++".to_string()]
);
assert_eq!(
spec.build.flags.ldflags,
vec!["-Wl,--as-needed".to_string()]
);
}
#[test]
fn apply_config_reads_build_flag_appends_from_rootfs_build_toml() {
let tmp = tempfile::tempdir().unwrap();
let config_path = tmp.path().join("etc/depot.d/build.toml");
std::fs::create_dir_all(config_path.parent().unwrap()).unwrap();
std::fs::write(
&config_path,
r#"
[flags]
cflags += ["-g"]
CXXFLAGS += ["-stdlib=libc++"]
LDFLAGS += "-Wl,--as-needed"
"#,
)
.unwrap();
let config = crate::config::Config::for_rootfs(tmp.path());
assert_eq!(
config.appends.get("build.flags.cflags").unwrap()[0]
.as_array()
.unwrap()[0]
.as_str(),
Some("-g")
);
assert_eq!(
config.appends.get("build.flags.cxxflags").unwrap()[0]
.as_array()
.unwrap()[0]
.as_str(),
Some("-stdlib=libc++")
);
assert_eq!(
config.appends.get("build.flags.ldflags").unwrap()[0].as_str(),
Some("-Wl,--as-needed")
);
let mut spec = mk_spec("foo", "1.0");
spec.apply_config(&config);
assert!(spec.build.flags.cflags.contains(&"-g".to_string()));
assert!(
spec.build
.flags
.cxxflags
.contains(&"-stdlib=libc++".to_string())
);
assert!(
spec.build
.flags
.ldflags
.contains(&"-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();
@@ -3934,16 +4092,6 @@ pub struct Alternatives {
} }
impl Alternatives { impl Alternatives {
/// Return the top-level alternatives set without any nested output-specific overrides.
pub fn primary_alternatives(&self) -> Alternatives {
Alternatives {
provides: self.provides.clone(),
conflicts: self.conflicts.clone(),
replaces: self.replaces.clone(),
lib32: None,
}
}
/// Return the optional lib32-specific alternatives override set. /// Return the optional lib32-specific alternatives override set.
pub fn lib32_alternatives(&self) -> Option<Alternatives> { pub fn lib32_alternatives(&self) -> Option<Alternatives> {
self.lib32.as_ref().map(|group| Alternatives { self.lib32.as_ref().map(|group| Alternatives {
+84 -62
View File
@@ -12,6 +12,10 @@ use url::Url;
const MAX_MIRROR_RETRIES: usize = 8; const MAX_MIRROR_RETRIES: usize = 8;
fn scheme_uses_http_transport(scheme: &str) -> bool {
matches!(scheme, "http" | "https")
}
/// Fetch an archive source tarball, returning path to downloaded file. /// Fetch an archive source tarball, returning path to downloaded file.
pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> Result<PathBuf> { pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> Result<PathBuf> {
let url = spec.expand_vars(&source.url); let url = spec.expand_vars(&source.url);
@@ -32,8 +36,17 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
// Parse URL early so we can handle non-HTTP schemes (FTP support) // Parse URL early so we can handle non-HTTP schemes (FTP support)
let parsed_url = Url::parse(&url).with_context(|| format!("Invalid URL: {}", url))?; let parsed_url = Url::parse(&url).with_context(|| format!("Invalid URL: {}", url))?;
let pb = ProgressBar::new(0);
pb.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})",
)
.unwrap()
.progress_chars("#>-"),
);
// If this is an FTP URL, fetch via suppaftp into the cache and continue // If this is an FTP URL, fetch via suppaftp and skip the HTTP client path.
if parsed_url.scheme() == "ftp" { if parsed_url.scheme() == "ftp" {
// Connect and login (anonymous fallback) // Connect and login (anonymous fallback)
let host = parsed_url.host_str().context("FTP URL missing host")?; let host = parsed_url.host_str().context("FTP URL missing host")?;
@@ -86,65 +99,66 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
if !retrieved { if !retrieved {
bail!("FTP error fetching {}", url); bail!("FTP error fetching {}", url);
} }
} } else {
if !scheme_uses_http_transport(parsed_url.scheme()) {
// Download with progress bar bail!(
// Use a sensible default User-Agent so servers that reject empty/unknown agents (e.g. IANA) "Unsupported URL scheme for source fetch: {}",
// will accept requests. Include package name/version at compile time. parsed_url.scheme()
let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); );
let client =
super::build_blocking_client(&ua, None).with_context(|| "Failed to build HTTP client")?;
let mut response = client
.get(&url)
.send()
.with_context(|| format!("Failed to fetch: {}", url))?;
// If the server returned a non-success status, read a short body preview and fail early.
// This prevents saving HTML error pages (which then fail checksum) and gives a clearer
// diagnostic to the user.
let status = response.status();
if !status.is_success() {
let mut preview_bytes = Vec::new();
// read up to 1 KiB for a preview (ignore errors while reading preview)
let _ = response.take(1024).read_to_end(&mut preview_bytes);
let preview = String::from_utf8_lossy(&preview_bytes);
bail!(
"HTTP error fetching {}: {}{}",
url,
status,
if preview.trim().is_empty() {
"".to_string()
} else {
format!(" — preview: {}", preview.trim())
}
);
}
let total_size = response.content_length().unwrap_or(0);
let pb = ProgressBar::new(total_size);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.unwrap()
.progress_chars("#>-"),
);
let mut file = File::create(&dest_path)
.with_context(|| format!("Failed to create: {}", dest_path.display()))?;
let mut buffer = [0u8; 8192];
let mut downloaded = 0u64;
loop {
let bytes_read = response.read(&mut buffer)?;
if bytes_read == 0 {
break;
} }
file.write_all(&buffer[..bytes_read])?;
downloaded += bytes_read as u64;
pb.set_position(downloaded);
}
pb.finish_with_message("Download complete"); // Download with progress bar
// Use a sensible default User-Agent so servers that reject empty/unknown agents (e.g. IANA)
// will accept requests. Include package name/version at compile time.
let ua = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
let client = super::build_blocking_client(&ua, None)
.with_context(|| "Failed to build HTTP client")?;
let mut response = client
.get(&url)
.send()
.with_context(|| format!("Failed to fetch: {}", url))?;
// If the server returned a non-success status, read a short body preview and fail early.
// This prevents saving HTML error pages (which then fail checksum) and gives a clearer
// diagnostic to the user.
let status = response.status();
if !status.is_success() {
let mut preview_bytes = Vec::new();
// read up to 1 KiB for a preview (ignore errors while reading preview)
let _ = response.take(1024).read_to_end(&mut preview_bytes);
let preview = String::from_utf8_lossy(&preview_bytes);
bail!(
"HTTP error fetching {}: {}{}",
url,
status,
if preview.trim().is_empty() {
"".to_string()
} else {
format!(" — preview: {}", preview.trim())
}
);
}
let total_size = response.content_length().unwrap_or(0);
pb.set_length(total_size);
let mut file = File::create(&dest_path)
.with_context(|| format!("Failed to create: {}", dest_path.display()))?;
let mut buffer = [0u8; 8192];
let mut downloaded = 0u64;
loop {
let bytes_read = response.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])?;
downloaded += bytes_read as u64;
pb.set_position(downloaded);
}
pb.finish_with_message("Download complete");
}
// Quick validation: ensure the downloaded file looks like the expected // Quick validation: ensure the downloaded file looks like the expected
// archive (detect obvious HTML error pages or wrong formats by magic). // archive (detect obvious HTML error pages or wrong formats by magic).
@@ -320,7 +334,7 @@ pub(crate) fn derive_filename_from_url(url: &str) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(url.as_bytes()); hasher.update(url.as_bytes());
let h = hasher.finalize(); let h = hasher.finalize();
let hex = format!("{:x}", h); let hex = crate::hex::encode_lower(h);
format!("source-{}.download", &hex[..12]) format!("source-{}.download", &hex[..12])
} }
@@ -589,6 +603,14 @@ fn classify_archive_magic(head: &[u8], lower_filename: &str, path: &Path) -> boo
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn scheme_uses_http_transport_only_for_http_and_https() {
assert!(scheme_uses_http_transport("http"));
assert!(scheme_uses_http_transport("https"));
assert!(!scheme_uses_http_transport("ftp"));
assert!(!scheme_uses_http_transport("file"));
}
#[test] #[test]
fn filename_from_simple_url() { fn filename_from_simple_url() {
assert_eq!( assert_eq!(
@@ -746,18 +768,18 @@ mod tests {
let sha256_hex = { let sha256_hex = {
let mut h = Sha256::new(); let mut h = Sha256::new();
h.update(b"abc"); h.update(b"abc");
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
let sha512_hex = { let sha512_hex = {
let mut h = Sha512::new(); let mut h = Sha512::new();
h.update(b"abc"); h.update(b"abc");
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
let sha1_hex = { let sha1_hex = {
let mut h = Sha1::new(); let mut h = Sha1::new();
h.update(b"abc"); h.update(b"abc");
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
let md5_hex = format!("{:x}", md5::compute(b"abc")); let md5_hex = format!("{:x}", md5::compute(b"abc"));
+1 -1
View File
@@ -202,7 +202,7 @@ fn mirror_key(url: &str) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(url.as_bytes()); hasher.update(url.as_bytes());
let digest = hasher.finalize(); let digest = hasher.finalize();
format!("{:x}", digest) crate::hex::encode_lower(digest)
} }
fn ensure_mirror( fn ensure_mirror(
+6 -6
View File
@@ -13,10 +13,10 @@ use crate::builder::state::{BuildStep, StateTracker};
fn hook_env_vars( fn hook_env_vars(
spec: &PackageSpec, spec: &PackageSpec,
shell_helpers: &crate::shell_helpers::ShellHelpers, shell_helpers: &crate::shell_helpers::ShellHelpers,
) -> crate::builder::EnvVars { ) -> Result<crate::builder::EnvVars> {
let mut env_vars = crate::builder::standard_build_env(spec, None, true, true); let mut env_vars = crate::builder::standard_build_env(spec, None, true, true);
shell_helpers.apply_to_env_vars(&mut env_vars); shell_helpers.apply_to_env_vars(&mut env_vars);
env_vars Ok(env_vars)
} }
/// Apply patches and run `post_extract` commands in the extracted source tree. /// Apply patches and run `post_extract` commands in the extracted source tree.
@@ -96,7 +96,7 @@ fn run_post_extract_commands(spec: &PackageSpec, source: &Source, src_dir: &Path
); );
let helper_root = tempfile::tempdir().context("Failed to create post-extract helper root")?; let helper_root = tempfile::tempdir().context("Failed to create post-extract helper root")?;
let shell_helpers = crate::shell_helpers::ShellHelpers::new(helper_root.path())?; let shell_helpers = crate::shell_helpers::ShellHelpers::new(helper_root.path())?;
let env_vars = hook_env_vars(spec, &shell_helpers); let env_vars = hook_env_vars(spec, &shell_helpers)?;
for cmd in &source.post_extract { for cmd in &source.post_extract {
let cmd_str = spec.expand_vars(cmd); let cmd_str = spec.expand_vars(cmd);
@@ -132,7 +132,7 @@ pub fn run_post_configure_commands(
crate::log_info!("Running {} post-configure command(s)...", commands.len()); crate::log_info!("Running {} post-configure command(s)...", commands.len());
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?; let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
let mut env_vars = hook_env_vars(spec, &shell_helpers); let mut env_vars = hook_env_vars(spec, &shell_helpers)?;
crate::builder::set_env_var( crate::builder::set_env_var(
&mut env_vars, &mut env_vars,
"DESTDIR", "DESTDIR",
@@ -168,7 +168,7 @@ pub fn run_post_compile_commands(spec: &PackageSpec, src_dir: &Path, destdir: &P
crate::log_info!("Running {} post-compile command(s)...", commands.len()); crate::log_info!("Running {} post-compile command(s)...", commands.len());
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?; let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
let mut env_vars = hook_env_vars(spec, &shell_helpers); let mut env_vars = hook_env_vars(spec, &shell_helpers)?;
crate::builder::set_env_var( crate::builder::set_env_var(
&mut env_vars, &mut env_vars,
"DESTDIR", "DESTDIR",
@@ -208,7 +208,7 @@ pub fn run_post_install_commands_in_dir(
crate::log_info!("Running {} post-install command(s)...", commands.len()); crate::log_info!("Running {} post-install command(s)...", commands.len());
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?; let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
let mut env_vars = hook_env_vars(spec, &shell_helpers); let mut env_vars = hook_env_vars(spec, &shell_helpers)?;
crate::builder::set_env_var( crate::builder::set_env_var(
&mut env_vars, &mut env_vars,
"DESTDIR", "DESTDIR",
+7 -7
View File
@@ -412,7 +412,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
} }
hasher.update(&buf[..n]); hasher.update(&buf[..n]);
} }
let actual = format!("{:x}", hasher.finalize()); let actual = crate::hex::encode_lower(hasher.finalize());
Ok(actual == hex) Ok(actual == hex)
} }
"sha512" => { "sha512" => {
@@ -427,7 +427,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
} }
hasher.update(&buf[..n]); hasher.update(&buf[..n]);
} }
let actual = format!("{:x}", hasher.finalize()); let actual = crate::hex::encode_lower(hasher.finalize());
Ok(actual == hex) Ok(actual == hex)
} }
"sha1" => { "sha1" => {
@@ -442,7 +442,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
} }
hasher.update(&buf[..n]); hasher.update(&buf[..n]);
} }
let actual = format!("{:x}", hasher.finalize()); let actual = crate::hex::encode_lower(hasher.finalize());
Ok(actual == hex) Ok(actual == hex)
} }
"md5" => { "md5" => {
@@ -457,7 +457,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
ctx.consume(&buf[..n]); ctx.consume(&buf[..n]);
} }
let digest = ctx.finalize(); let digest = ctx.finalize();
let actual = format!("{:x}", digest); let actual = crate::hex::encode_lower(*digest);
Ok(actual == hex) Ok(actual == hex)
} }
"b2" | "b2sum" => { "b2" | "b2sum" => {
@@ -994,17 +994,17 @@ mod tests {
let sha256_hex = { let sha256_hex = {
let mut h = Sha256::new(); let mut h = Sha256::new();
h.update(b"abc"); h.update(b"abc");
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
let sha512_hex = { let sha512_hex = {
let mut h = Sha512::new(); let mut h = Sha512::new();
h.update(b"abc"); h.update(b"abc");
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
let sha1_hex = { let sha1_hex = {
let mut h = Sha1::new(); let mut h = Sha1::new();
h.update(b"abc"); h.update(b"abc");
format!("{:x}", h.finalize()) crate::hex::encode_lower(h.finalize())
}; };
let md5_hex = format!("{:x}", md5::compute(b"abc")); let md5_hex = format!("{:x}", md5::compute(b"abc"));
let b2_hex = b2sum_rust::Blake2bSum::new(64) let b2_hex = b2sum_rust::Blake2bSum::new(64)
+21 -3
View File
@@ -1,8 +1,8 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
if [ "$#" -ne 6 ]; then if [ "$#" -ne 15 ]; then
echo "usage: $0 <cargo> <src_root> <build_root> <profile> <release_flag> <output>" >&2 echo "usage: $0 <cargo> <src_root> <build_root> <profile> <release_flag> <build_static> <autotools_pkg> <cmake_pkg> <meson_pkg> <perl_pkg> <custom_pkg> <python_pkg> <rust_pkg> <makefile_pkg> <output>" >&2
exit 2 exit 2
fi fi
@@ -11,7 +11,16 @@ src_root="$2"
build_root="$3" build_root="$3"
profile="$4" profile="$4"
release_flag="$5" release_flag="$5"
output="$6" build_static="$6"
autotools_pkg="$7"
cmake_pkg="$8"
meson_pkg="$9"
perl_pkg="${10}"
custom_pkg="${11}"
python_pkg="${12}"
rust_pkg="${13}"
makefile_pkg="${14}"
output="${15}"
cargo_home="$build_root/cargo-home" cargo_home="$build_root/cargo-home"
cargo_target_dir="$build_root/cargo-target" cargo_target_dir="$build_root/cargo-target"
@@ -20,6 +29,15 @@ mkdir -p "$cargo_home" "$cargo_target_dir"
export CARGO_HOME="$cargo_home" export CARGO_HOME="$cargo_home"
export CARGO_TARGET_DIR="$cargo_target_dir" export CARGO_TARGET_DIR="$cargo_target_dir"
export BUILD_DEPOT_STATIC="$build_static"
export DEPOT_AUTOTOOLS_PACKAGE="$autotools_pkg"
export DEPOT_CMAKE_PACKAGE="$cmake_pkg"
export DEPOT_MESON_PACKAGE="$meson_pkg"
export DEPOT_PERL_PACKAGE="$perl_pkg"
export DEPOT_CUSTOM_PACKAGE="$custom_pkg"
export DEPOT_PYTHON_PACKAGE="$python_pkg"
export DEPOT_RUST_PACKAGE="$rust_pkg"
export DEPOT_MAKEFILE_PACKAGE="$makefile_pkg"
if [ "$release_flag" = "1" ]; then if [ "$release_flag" = "1" ]; then
"$cargo_bin" build --locked --manifest-path "$src_root/Cargo.toml" --release "$cargo_bin" build --locked --manifest-path "$src_root/Cargo.toml" --release
+20 -2
View File
@@ -1,8 +1,8 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
if [ "$#" -ne 5 ]; then if [ "$#" -ne 14 ]; then
echo "usage: $0 <cargo> <src_root> <build_root> <profile> <release_flag>" >&2 echo "usage: $0 <cargo> <src_root> <build_root> <profile> <release_flag> <build_static> <autotools_pkg> <cmake_pkg> <meson_pkg> <perl_pkg> <custom_pkg> <python_pkg> <rust_pkg> <makefile_pkg>" >&2
exit 2 exit 2
fi fi
@@ -11,6 +11,15 @@ src_root="$2"
build_root="$3" build_root="$3"
profile="$4" profile="$4"
release_flag="$5" release_flag="$5"
build_static="$6"
autotools_pkg="$7"
cmake_pkg="$8"
meson_pkg="$9"
perl_pkg="${10}"
custom_pkg="${11}"
python_pkg="${12}"
rust_pkg="${13}"
makefile_pkg="${14}"
cargo_home="$build_root/cargo-home" cargo_home="$build_root/cargo-home"
cargo_target_dir="$build_root/cargo-target" cargo_target_dir="$build_root/cargo-target"
@@ -19,6 +28,15 @@ mkdir -p "$cargo_home" "$cargo_target_dir"
export CARGO_HOME="$cargo_home" export CARGO_HOME="$cargo_home"
export CARGO_TARGET_DIR="$cargo_target_dir" export CARGO_TARGET_DIR="$cargo_target_dir"
export BUILD_DEPOT_STATIC="$build_static"
export DEPOT_AUTOTOOLS_PACKAGE="$autotools_pkg"
export DEPOT_CMAKE_PACKAGE="$cmake_pkg"
export DEPOT_MESON_PACKAGE="$meson_pkg"
export DEPOT_PERL_PACKAGE="$perl_pkg"
export DEPOT_CUSTOM_PACKAGE="$custom_pkg"
export DEPOT_PYTHON_PACKAGE="$python_pkg"
export DEPOT_RUST_PACKAGE="$rust_pkg"
export DEPOT_MAKEFILE_PACKAGE="$makefile_pkg"
if [ "$release_flag" = "1" ]; then if [ "$release_flag" = "1" ]; then
exec "$cargo_bin" test --locked --manifest-path "$src_root/Cargo.toml" --profile "$profile" exec "$cargo_bin" test --locked --manifest-path "$src_root/Cargo.toml" --profile "$profile"