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:
Generated
+105
-30
@@ -16,7 +16,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -168,6 +168,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
@@ -230,7 +239,7 @@ version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"crypto-common 0.1.7",
|
||||
"inout",
|
||||
]
|
||||
|
||||
@@ -285,9 +294,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "clap_mangen"
|
||||
version = "0.2.33"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78"
|
||||
checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"roff",
|
||||
@@ -312,6 +321,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.1.5"
|
||||
@@ -358,6 +373,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -404,6 +428,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ct-codecs"
|
||||
version = "1.1.6"
|
||||
@@ -418,7 +451,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
||||
|
||||
[[package]]
|
||||
name = "depot"
|
||||
version = "0.33.1"
|
||||
version = "0.34.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ar",
|
||||
@@ -444,8 +477,8 @@ dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_ignored",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"sha1 0.11.0",
|
||||
"sha2 0.11.0",
|
||||
"signal-hook 0.4.3",
|
||||
"suppaftp",
|
||||
"sys-mount",
|
||||
@@ -499,11 +532,22 @@ version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"block-buffer 0.10.4",
|
||||
"crypto-common 0.1.7",
|
||||
"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]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
@@ -838,7 +882,7 @@ version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -880,6 +924,15 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
@@ -1318,7 +1371,7 @@ version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1519,7 +1572,7 @@ version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
@@ -1832,7 +1885,7 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||
dependencies = [
|
||||
"pbkdf2",
|
||||
"salsa20",
|
||||
"sha2",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1919,9 +1972,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.4"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -1933,8 +1986,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
"cpufeatures 0.2.17",
|
||||
"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]]
|
||||
@@ -1944,8 +2008,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
"cpufeatures 0.2.17",
|
||||
"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]]
|
||||
@@ -2262,9 +2337,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc"
|
||||
checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
@@ -2277,27 +2352,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
||||
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
||||
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
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"
|
||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
@@ -2878,9 +2953,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
@@ -3108,7 +3183,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"ppmd-rust",
|
||||
"sha1",
|
||||
"sha1 0.10.6",
|
||||
"time",
|
||||
"typed-path",
|
||||
"zeroize",
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "depot"
|
||||
version = "0.33.1"
|
||||
version = "0.34.0"
|
||||
edition = "2024"
|
||||
|
||||
[lints.rust]
|
||||
@@ -19,11 +19,11 @@ rusqlite = "0.39.0"
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
yaml-rust2 = "0.11.0"
|
||||
sha2 = "0.10.9"
|
||||
sha2 = "0.11.0"
|
||||
tar = "0.4.45"
|
||||
tempfile = "=3.27.0"
|
||||
thiserror = "2.0.18"
|
||||
toml = "1.0.6"
|
||||
toml = "1.1.0+spec-1.1.0"
|
||||
url = "2.5.8"
|
||||
walkdir = "2.5.0"
|
||||
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"] }
|
||||
filetime = "0.2.27"
|
||||
clap_complete = "4.6.0"
|
||||
clap_mangen = "0.2.33"
|
||||
clap_mangen = "0.3.0"
|
||||
sys-mount = { version = "3.1.0", default-features = false }
|
||||
time = { version = "0.3.47", features = ["formatting", "parsing"] }
|
||||
b2sum-rust = "0.3.0"
|
||||
@@ -46,7 +46,7 @@ serde_ignored = "0.1.14"
|
||||
lz4_flex = "0.13.0"
|
||||
lzma-rust2 = "0.16.2"
|
||||
signal-hook = "0.4.3"
|
||||
sha1 = "0.10.6"
|
||||
sha1 = "0.11.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "=3.27.0"
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
project(
|
||||
'depot',
|
||||
version: '0.33.1',
|
||||
version: '0.34.0',
|
||||
meson_version: '>=0.60.0',
|
||||
default_options: ['buildtype=release'],
|
||||
)
|
||||
@@ -12,6 +12,15 @@ find_prog = find_program('find', required: true)
|
||||
sh = find_program('sh', required: true)
|
||||
build_html_doc = get_option('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()
|
||||
build_root = meson.project_build_root()
|
||||
@@ -66,6 +75,15 @@ depot_bin = custom_target(
|
||||
build_root,
|
||||
cargo_profile,
|
||||
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@',
|
||||
],
|
||||
console: true,
|
||||
@@ -101,6 +119,15 @@ test(
|
||||
build_root,
|
||||
cargo_profile,
|
||||
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'],
|
||||
timeout: 0,
|
||||
|
||||
@@ -4,3 +4,57 @@ option(
|
||||
value: false,
|
||||
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',
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,9 @@ pub fn build(
|
||||
let expanded = expand_configure_arg(spec, arg, &env_vars);
|
||||
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)
|
||||
.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);
|
||||
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)
|
||||
.with_context(|| format!("Failed to run host configure in {}", build_dir.display()))?;
|
||||
|
||||
+151
-45
@@ -44,8 +44,7 @@ pub fn build(
|
||||
}
|
||||
|
||||
// Extract prefix from configure flags (cmake-style -DCMAKE_INSTALL_PREFIX=)
|
||||
let prefix = cmake_cache_entry_value(&flags.configure, "CMAKE_INSTALL_PREFIX")
|
||||
.unwrap_or(flags.prefix.as_str());
|
||||
let prefix = effective_cmake_install_prefix(flags);
|
||||
|
||||
// Generate toolchain file if cross-compiling
|
||||
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(format!("-DCMAKE_INSTALL_PREFIX={}", prefix));
|
||||
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);
|
||||
}
|
||||
for arg in cmake_lib32_target_args(flags, cross) {
|
||||
@@ -98,6 +97,9 @@ pub fn build(
|
||||
let expanded = expand_with_envs(flag, &env_vars);
|
||||
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);
|
||||
|
||||
@@ -280,8 +282,7 @@ pub(crate) fn ensure_host_build(
|
||||
|
||||
let env_vars =
|
||||
crate::builder::standard_build_env(&host_spec, None, true, export_compiler_flags);
|
||||
let prefix = cmake_cache_entry_value(&flags.configure, "CMAKE_INSTALL_PREFIX")
|
||||
.unwrap_or(flags.prefix.as_str());
|
||||
let prefix = effective_cmake_install_prefix(flags);
|
||||
|
||||
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(format!("-DCMAKE_INSTALL_PREFIX={}", prefix));
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -316,6 +317,9 @@ pub(crate) fn ensure_host_build(
|
||||
let expanded = expand_with_envs(flag, &env_vars);
|
||||
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);
|
||||
|
||||
@@ -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 defaults = [
|
||||
("CMAKE_INSTALL_BINDIR", dirs.bindir),
|
||||
("CMAKE_INSTALL_SBINDIR", dirs.sbindir),
|
||||
("CMAKE_INSTALL_LIBDIR", dirs.libdir),
|
||||
("CMAKE_INSTALL_LIBEXECDIR", dirs.libexecdir),
|
||||
(
|
||||
"CMAKE_INSTALL_BINDIR",
|
||||
cmake_dir_value_for_prefix(prefix, dirs.bindir),
|
||||
),
|
||||
(
|
||||
"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_LOCALSTATEDIR", dirs.localstatedir),
|
||||
("CMAKE_INSTALL_SHAREDSTATEDIR", dirs.sharedstatedir),
|
||||
("CMAKE_INSTALL_INCLUDEDIR", dirs.includedir),
|
||||
("CMAKE_INSTALL_DATAROOTDIR", dirs.datarootdir),
|
||||
("CMAKE_INSTALL_DATADIR", dirs.datadir),
|
||||
("CMAKE_INSTALL_MANDIR", dirs.mandir),
|
||||
("CMAKE_INSTALL_INFODIR", dirs.infodir),
|
||||
(
|
||||
"CMAKE_INSTALL_INCLUDEDIR",
|
||||
cmake_dir_value_for_prefix(prefix, dirs.includedir),
|
||||
),
|
||||
(
|
||||
"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
|
||||
@@ -710,15 +765,41 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmake_install_dir_args_include_defaults() {
|
||||
let args = cmake_install_dir_args(&BuildFlags::default());
|
||||
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=/usr/bin"));
|
||||
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SBINDIR=/usr/bin"));
|
||||
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=/usr/lib"));
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=/usr/lib")
|
||||
fn test_effective_cmake_install_prefix_prefers_explicit_configure_flag() {
|
||||
let flags = BuildFlags {
|
||||
prefix: "/usr".into(),
|
||||
configure: vec!["-DCMAKE_INSTALL_PREFIX=/opt/soundtouch".into()],
|
||||
..BuildFlags::default()
|
||||
};
|
||||
|
||||
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()
|
||||
@@ -730,23 +811,17 @@ mod tests {
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=/usr/include")
|
||||
.any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=include")
|
||||
);
|
||||
assert!(
|
||||
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!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_INSTALL_DATADIR=/usr/share")
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_INSTALL_MANDIR=/usr/share/man")
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_INSTALL_INFODIR=/usr/share/info")
|
||||
.any(|a| a == "-DCMAKE_INSTALL_INFODIR=share/info")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -757,15 +832,9 @@ mod tests {
|
||||
..BuildFlags::default()
|
||||
};
|
||||
|
||||
let args = cmake_install_dir_args(&flags);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_INSTALL_LIBDIR=/usr/lib32")
|
||||
);
|
||||
assert!(
|
||||
args.iter()
|
||||
.any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=/usr/lib32")
|
||||
);
|
||||
let args = cmake_install_dir_args(&flags, "/usr");
|
||||
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib32"));
|
||||
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=lib32"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -832,7 +901,7 @@ mod tests {
|
||||
..BuildFlags::default()
|
||||
};
|
||||
|
||||
let args = cmake_install_dir_args(&flags);
|
||||
let args = cmake_install_dir_args(&flags, "/usr");
|
||||
assert!(
|
||||
!args
|
||||
.iter()
|
||||
@@ -848,7 +917,44 @@ mod tests {
|
||||
.iter()
|
||||
.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_SYSCONFDIR=/etc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -65,6 +65,9 @@ pub fn build(
|
||||
for arg in meson_setup_args(flags, cross_file.as_deref(), &env_vars) {
|
||||
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);
|
||||
|
||||
@@ -229,6 +232,9 @@ pub(crate) fn ensure_host_build(
|
||||
for arg in meson_setup_args(flags, None, &env_vars) {
|
||||
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);
|
||||
|
||||
|
||||
@@ -184,6 +184,47 @@ pub(crate) fn host_build_spec(spec: &PackageSpec) -> PackageSpec {
|
||||
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 {
|
||||
let trimmed = value.trim();
|
||||
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]
|
||||
fn test_standard_build_env_exports_native_linker_and_cpp() {
|
||||
let mut spec = mk_spec(Vec::new(), Vec::new());
|
||||
|
||||
@@ -59,6 +59,9 @@ pub fn build(
|
||||
for arg in &flags.configure {
|
||||
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);
|
||||
|
||||
let status = command_status_with_sh_fallback(&mut configure_cmd).with_context(|| {
|
||||
|
||||
+70
-7038
File diff suppressed because it is too large
Load Diff
@@ -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(())
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
¤t.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,
|
||||
¤t.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,
|
||||
¤t.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(¤t.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(¤t.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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(¤t_normalized) >= 2
|
||||
&& numeric_component_count(&normalized) == 1
|
||||
&& first_numeric_component_len(¤t_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
@@ -159,6 +159,34 @@ fn repo_settings_is_default(settings: &RepoSettings) -> bool {
|
||||
!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.
|
||||
pub fn repos_toml_path(rootfs: &Path) -> PathBuf {
|
||||
resolve_rootfs_base(rootfs).join("etc/depot.d/repos.toml")
|
||||
@@ -345,7 +373,7 @@ impl Config {
|
||||
let (val, appends) = self.preprocess_toml(&content)?;
|
||||
merge_toml_values(&mut self.build_overrides, &val);
|
||||
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)?;
|
||||
merge_toml_values(&mut self.build_overrides, &val);
|
||||
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)?;
|
||||
merge_toml_values(&mut self.package_overrides, &val);
|
||||
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)?;
|
||||
merge_toml_values(&mut self.package_overrides, &val);
|
||||
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>>)> {
|
||||
let mut base_text = String::new();
|
||||
let mut appends = HashMap::new();
|
||||
let mut current_table: Option<String> = None;
|
||||
let mut in_array_table = false;
|
||||
|
||||
for line in input.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("[[") && trimmed.ends_with("]]") && trimmed.len() >= 4 {
|
||||
current_table = Some(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('#') {
|
||||
base_text.push_str(line);
|
||||
base_text.push('\n');
|
||||
@@ -492,7 +536,13 @@ impl Config {
|
||||
}
|
||||
|
||||
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: toml::Value = toml::from_str::<toml::Value>(&format!("v = {}", val_str))
|
||||
.context("Failed to parse append value")?
|
||||
@@ -500,7 +550,16 @@ impl Config {
|
||||
.cloned()
|
||||
.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 {
|
||||
base_text.push_str(line);
|
||||
base_text.push('\n');
|
||||
@@ -574,16 +633,16 @@ ldflags += "-L/usr/local/lib"
|
||||
);
|
||||
|
||||
// Appends should be captured
|
||||
assert!(appends.contains_key("cflags"));
|
||||
assert_eq!(appends.get("cflags").unwrap().len(), 1);
|
||||
assert!(appends.contains_key("flags.cflags"));
|
||||
assert_eq!(appends.get("flags.cflags").unwrap().len(), 1);
|
||||
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")
|
||||
);
|
||||
|
||||
assert!(appends.contains_key("ldflags"));
|
||||
assert!(appends.contains_key("flags.ldflags"));
|
||||
assert_eq!(
|
||||
appends.get("ldflags").unwrap()[0].as_str(),
|
||||
appends.get("flags.ldflags").unwrap()[0].as_str(),
|
||||
Some("-L/usr/local/lib")
|
||||
);
|
||||
}
|
||||
@@ -615,9 +674,9 @@ cflags += ["-g"]
|
||||
.map(|a| a.len()),
|
||||
Some(1)
|
||||
);
|
||||
assert!(config.appends.contains_key("build.cflags"));
|
||||
assert!(config.appends.contains_key("build.flags.cflags"));
|
||||
assert_eq!(
|
||||
config.appends.get("build.cflags").unwrap()[0]
|
||||
config.appends.get("build.flags.cflags").unwrap()[0]
|
||||
.as_array()
|
||||
.unwrap()[0]
|
||||
.as_str(),
|
||||
|
||||
+8
-8
@@ -54,8 +54,8 @@ impl<R> HashingReader<R> {
|
||||
|
||||
fn finalize_hex(self) -> (String, String) {
|
||||
(
|
||||
format!("{:x}", self.sha256.finalize()),
|
||||
format!("{:x}", self.sha512.finalize()),
|
||||
crate::hex::encode_lower(self.sha256.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]);
|
||||
}
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
}
|
||||
"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]);
|
||||
}
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
}
|
||||
_ => anyhow::bail!("Unsupported checksum algorithm: {}", algorithm),
|
||||
};
|
||||
@@ -2988,12 +2988,12 @@ revision = 1
|
||||
let sha256 = {
|
||||
let mut h = Sha256::new();
|
||||
h.update(b"payload");
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
let sha512 = {
|
||||
let mut h = Sha512::new();
|
||||
h.update(b"payload");
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
|
||||
let rec = BinaryRepoPackageRecord {
|
||||
@@ -3028,12 +3028,12 @@ revision = 1
|
||||
let sha256 = {
|
||||
let mut h = Sha256::new();
|
||||
h.update(payload);
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
let sha512 = {
|
||||
let mut h = Sha512::new();
|
||||
h.update(payload);
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
|
||||
BinaryRepoPackageRecord {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+103
-11
@@ -24,6 +24,7 @@
|
||||
|
||||
use crate::fakeroot;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io::{ErrorKind, Write};
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
pub fn run_transaction_hooks_batch(
|
||||
@@ -251,10 +253,25 @@ pub fn run_transaction_hooks_batch(
|
||||
#[derive(Clone, Copy)]
|
||||
struct ScheduledHookRun {
|
||||
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();
|
||||
for (hook_idx, hook) in hooks.iter().enumerate() {
|
||||
let mut matched_operations = Vec::new();
|
||||
for (ctx_idx, ctx_owned) in contexts.iter().enumerate() {
|
||||
let normalized_paths = normalize_affected_paths(&ctx_owned.affected_paths);
|
||||
let ctx = HookExecutionContext {
|
||||
@@ -263,23 +280,54 @@ pub fn run_transaction_hooks_batch(
|
||||
package: &ctx_owned.package,
|
||||
affected_paths: &ctx_owned.affected_paths,
|
||||
};
|
||||
for (hook_idx, hook) in hooks.iter().enumerate() {
|
||||
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();
|
||||
for (run_idx, run) in scheduled.into_iter().enumerate() {
|
||||
let ctx_owned = &contexts[run.ctx_idx];
|
||||
let normalized_paths = normalize_affected_paths(&ctx_owned.affected_paths);
|
||||
let primary_ctx = &contexts[run.first_ctx_idx];
|
||||
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 {
|
||||
phase,
|
||||
operation: primary_ctx.operation,
|
||||
package: &primary_ctx.package,
|
||||
affected_paths: &primary_ctx.affected_paths,
|
||||
};
|
||||
run_hook_command(
|
||||
rootfs,
|
||||
&hooks[run.hook_idx],
|
||||
@@ -758,7 +806,8 @@ phase = "post"
|
||||
operation = ["install"]
|
||||
|
||||
[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 {
|
||||
operation: HookOperation::Install,
|
||||
package: "foo".to_string(),
|
||||
affected_paths: Vec::new(),
|
||||
affected_paths: vec!["usr/bin/foo".to_string()],
|
||||
},
|
||||
HookExecutionContextOwned {
|
||||
operation: HookOperation::Install,
|
||||
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();
|
||||
assert_eq!(ran, 2);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Depot - Not Your Average Package Manager
|
||||
//! A source-based package manager for Linux
|
||||
|
||||
mod build_options;
|
||||
mod builder;
|
||||
mod cli;
|
||||
mod cli_assets;
|
||||
@@ -10,6 +11,7 @@ mod cross;
|
||||
mod db;
|
||||
mod deps;
|
||||
mod fakeroot;
|
||||
mod hex;
|
||||
mod index;
|
||||
mod install;
|
||||
mod interrupts;
|
||||
|
||||
@@ -44,7 +44,7 @@ fn compute_sha256_for_url(u: &str) -> anyhow::Result<String> {
|
||||
}
|
||||
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.
|
||||
@@ -2261,7 +2261,7 @@ mod tests {
|
||||
let expected = {
|
||||
let mut h = TestSha256::new();
|
||||
h.update(b"abc");
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
|
||||
// plain path
|
||||
|
||||
+170
-22
@@ -100,6 +100,7 @@ impl PackageSpec {
|
||||
appends: &std::collections::HashMap<String, Vec<toml::Value>>,
|
||||
) -> Result<()> {
|
||||
for (key, values) in appends {
|
||||
let key = normalize_append_key(key);
|
||||
if let Some(subkey) = key.strip_prefix("build.flags.") {
|
||||
self.apply_append(subkey, values);
|
||||
continue;
|
||||
@@ -109,7 +110,7 @@ impl PackageSpec {
|
||||
continue;
|
||||
}
|
||||
if !key.contains('.') {
|
||||
self.apply_append(key, values);
|
||||
self.apply_append(&key, values);
|
||||
continue;
|
||||
}
|
||||
anyhow::bail!("Unsupported '+=' key in package spec: {}", key);
|
||||
@@ -323,11 +324,8 @@ impl PackageSpec {
|
||||
.package_alternatives
|
||||
.get(pkg_name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
self.alternatives
|
||||
.lib32_alternatives()
|
||||
.unwrap_or_else(|| self.alternatives.primary_alternatives())
|
||||
});
|
||||
.or_else(|| self.alternatives.lib32_alternatives())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
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"])
|
||||
for (key, values) in &config.appends {
|
||||
let key = normalize_append_key(key);
|
||||
if let Some(subkey) = key.strip_prefix("build.flags.") {
|
||||
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]) {
|
||||
match key {
|
||||
let key = normalize_append_key(key);
|
||||
match key.as_str() {
|
||||
"cflags" => {
|
||||
for v in values {
|
||||
if let Some(arr) = v.as_array() {
|
||||
@@ -1675,14 +1677,14 @@ fn preprocess_spec_toml_appends(
|
||||
let trimmed = line.trim();
|
||||
|
||||
if trimmed.starts_with("[[") && trimmed.ends_with("]]") && trimmed.len() >= 4 {
|
||||
current_table = Some(trimmed[2..trimmed.len() - 2].trim().to_string());
|
||||
current_table = Some(normalize_append_key(trimmed[2..trimmed.len() - 2].trim()));
|
||||
in_array_table = true;
|
||||
base_text.push_str(line);
|
||||
base_text.push('\n');
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 {
|
||||
current_table = Some(trimmed[1..trimmed.len() - 1].trim().to_string());
|
||||
current_table = Some(normalize_append_key(trimmed[1..trimmed.len() - 1].trim()));
|
||||
in_array_table = false;
|
||||
base_text.push_str(line);
|
||||
base_text.push('\n');
|
||||
@@ -1702,7 +1704,7 @@ fn preprocess_spec_toml_appends(
|
||||
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: toml::Value = toml::from_str::<toml::Value>(&format!("v = {}", val_str))
|
||||
.context("Failed to parse append value")?
|
||||
@@ -1711,11 +1713,11 @@ fn preprocess_spec_toml_appends(
|
||||
.unwrap();
|
||||
|
||||
let full_key = if key.contains('.') {
|
||||
key.to_string()
|
||||
key
|
||||
} else if let Some(table) = current_table.as_deref() {
|
||||
format!("{}.{}", table, key)
|
||||
} else {
|
||||
key.to_string()
|
||||
key
|
||||
};
|
||||
|
||||
appends.entry(full_key).or_insert_with(Vec::new).push(val);
|
||||
@@ -1731,6 +1733,24 @@ fn preprocess_spec_toml_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)]
|
||||
mod spec_tests {
|
||||
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]
|
||||
fn parse_python_build_type() {
|
||||
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]
|
||||
fn parse_passthrough_env_from_spec() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -3934,16 +4092,6 @@ pub struct 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.
|
||||
pub fn lib32_alternatives(&self) -> Option<Alternatives> {
|
||||
self.lib32.as_ref().map(|group| Alternatives {
|
||||
|
||||
+36
-14
@@ -12,6 +12,10 @@ use url::Url;
|
||||
|
||||
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.
|
||||
pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> Result<PathBuf> {
|
||||
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)
|
||||
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" {
|
||||
// Connect and login (anonymous fallback)
|
||||
let host = parsed_url.host_str().context("FTP URL missing host")?;
|
||||
@@ -86,14 +99,20 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
||||
if !retrieved {
|
||||
bail!("FTP error fetching {}", url);
|
||||
}
|
||||
} else {
|
||||
if !scheme_uses_http_transport(parsed_url.scheme()) {
|
||||
bail!(
|
||||
"Unsupported URL scheme for source fetch: {}",
|
||||
parsed_url.scheme()
|
||||
);
|
||||
}
|
||||
|
||||
// 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 client = super::build_blocking_client(&ua, None)
|
||||
.with_context(|| "Failed to build HTTP client")?;
|
||||
|
||||
let mut response = client
|
||||
.get(&url)
|
||||
@@ -120,13 +139,7 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
||||
);
|
||||
}
|
||||
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("#>-"),
|
||||
);
|
||||
pb.set_length(total_size);
|
||||
|
||||
let mut file = File::create(&dest_path)
|
||||
.with_context(|| format!("Failed to create: {}", dest_path.display()))?;
|
||||
@@ -145,6 +158,7 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R
|
||||
}
|
||||
|
||||
pb.finish_with_message("Download complete");
|
||||
}
|
||||
|
||||
// Quick validation: ensure the downloaded file looks like the expected
|
||||
// archive (detect obvious HTML error pages or wrong formats by magic).
|
||||
@@ -320,7 +334,7 @@ pub(crate) fn derive_filename_from_url(url: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(url.as_bytes());
|
||||
let h = hasher.finalize();
|
||||
let hex = format!("{:x}", h);
|
||||
let hex = crate::hex::encode_lower(h);
|
||||
format!("source-{}.download", &hex[..12])
|
||||
}
|
||||
|
||||
@@ -589,6 +603,14 @@ fn classify_archive_magic(head: &[u8], lower_filename: &str, path: &Path) -> boo
|
||||
mod tests {
|
||||
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]
|
||||
fn filename_from_simple_url() {
|
||||
assert_eq!(
|
||||
@@ -746,18 +768,18 @@ mod tests {
|
||||
let sha256_hex = {
|
||||
let mut h = Sha256::new();
|
||||
h.update(b"abc");
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
|
||||
let sha512_hex = {
|
||||
let mut h = Sha512::new();
|
||||
h.update(b"abc");
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
let sha1_hex = {
|
||||
let mut h = Sha1::new();
|
||||
h.update(b"abc");
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
|
||||
let md5_hex = format!("{:x}", md5::compute(b"abc"));
|
||||
|
||||
+1
-1
@@ -202,7 +202,7 @@ fn mirror_key(url: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(url.as_bytes());
|
||||
let digest = hasher.finalize();
|
||||
format!("{:x}", digest)
|
||||
crate::hex::encode_lower(digest)
|
||||
}
|
||||
|
||||
fn ensure_mirror(
|
||||
|
||||
+6
-6
@@ -13,10 +13,10 @@ use crate::builder::state::{BuildStep, StateTracker};
|
||||
fn hook_env_vars(
|
||||
spec: &PackageSpec,
|
||||
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);
|
||||
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.
|
||||
@@ -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 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 {
|
||||
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());
|
||||
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(
|
||||
&mut env_vars,
|
||||
"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());
|
||||
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(
|
||||
&mut env_vars,
|
||||
"DESTDIR",
|
||||
@@ -208,7 +208,7 @@ pub fn run_post_install_commands_in_dir(
|
||||
|
||||
crate::log_info!("Running {} post-install command(s)...", commands.len());
|
||||
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(
|
||||
&mut env_vars,
|
||||
"DESTDIR",
|
||||
|
||||
+7
-7
@@ -412,7 +412,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
|
||||
}
|
||||
hasher.update(&buf[..n]);
|
||||
}
|
||||
let actual = format!("{:x}", hasher.finalize());
|
||||
let actual = crate::hex::encode_lower(hasher.finalize());
|
||||
Ok(actual == hex)
|
||||
}
|
||||
"sha512" => {
|
||||
@@ -427,7 +427,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
|
||||
}
|
||||
hasher.update(&buf[..n]);
|
||||
}
|
||||
let actual = format!("{:x}", hasher.finalize());
|
||||
let actual = crate::hex::encode_lower(hasher.finalize());
|
||||
Ok(actual == hex)
|
||||
}
|
||||
"sha1" => {
|
||||
@@ -442,7 +442,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
|
||||
}
|
||||
hasher.update(&buf[..n]);
|
||||
}
|
||||
let actual = format!("{:x}", hasher.finalize());
|
||||
let actual = crate::hex::encode_lower(hasher.finalize());
|
||||
Ok(actual == hex)
|
||||
}
|
||||
"md5" => {
|
||||
@@ -457,7 +457,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
|
||||
ctx.consume(&buf[..n]);
|
||||
}
|
||||
let digest = ctx.finalize();
|
||||
let actual = format!("{:x}", digest);
|
||||
let actual = crate::hex::encode_lower(*digest);
|
||||
Ok(actual == hex)
|
||||
}
|
||||
"b2" | "b2sum" => {
|
||||
@@ -994,17 +994,17 @@ mod tests {
|
||||
let sha256_hex = {
|
||||
let mut h = Sha256::new();
|
||||
h.update(b"abc");
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
let sha512_hex = {
|
||||
let mut h = Sha512::new();
|
||||
h.update(b"abc");
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
let sha1_hex = {
|
||||
let mut h = Sha1::new();
|
||||
h.update(b"abc");
|
||||
format!("{:x}", h.finalize())
|
||||
crate::hex::encode_lower(h.finalize())
|
||||
};
|
||||
let md5_hex = format!("{:x}", md5::compute(b"abc"));
|
||||
let b2_hex = b2sum_rust::Blake2bSum::new(64)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 6 ]; then
|
||||
echo "usage: $0 <cargo> <src_root> <build_root> <profile> <release_flag> <output>" >&2
|
||||
if [ "$#" -ne 15 ]; then
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -11,7 +11,16 @@ src_root="$2"
|
||||
build_root="$3"
|
||||
profile="$4"
|
||||
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_target_dir="$build_root/cargo-target"
|
||||
@@ -20,6 +29,15 @@ mkdir -p "$cargo_home" "$cargo_target_dir"
|
||||
|
||||
export CARGO_HOME="$cargo_home"
|
||||
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
|
||||
"$cargo_bin" build --locked --manifest-path "$src_root/Cargo.toml" --release
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 5 ]; then
|
||||
echo "usage: $0 <cargo> <src_root> <build_root> <profile> <release_flag>" >&2
|
||||
if [ "$#" -ne 14 ]; then
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -11,6 +11,15 @@ src_root="$2"
|
||||
build_root="$3"
|
||||
profile="$4"
|
||||
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_target_dir="$build_root/cargo-target"
|
||||
@@ -19,6 +28,15 @@ mkdir -p "$cargo_home" "$cargo_target_dir"
|
||||
|
||||
export CARGO_HOME="$cargo_home"
|
||||
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
|
||||
exec "$cargo_bin" test --locked --manifest-path "$src_root/Cargo.toml" --profile "$profile"
|
||||
|
||||
Reference in New Issue
Block a user