feat: update depot version to 0.30.0 and enhance manual source validation and lifecycle hook handling
This commit is contained in:
Generated
+1
-1
@@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.29.1"
|
version = "0.30.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ar",
|
"ar",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.29.1"
|
version = "0.30.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
project(
|
project(
|
||||||
'depot',
|
'depot',
|
||||||
version: '0.29.1',
|
version: '0.30.0',
|
||||||
meson_version: '>=0.60.0',
|
meson_version: '>=0.60.0',
|
||||||
default_options: ['buildtype=release'],
|
default_options: ['buildtype=release'],
|
||||||
)
|
)
|
||||||
|
|||||||
+179
-44
@@ -1276,6 +1276,11 @@ struct PlannedPackageInstall {
|
|||||||
staged: PlannedStagedInstall,
|
staged: PlannedStagedInstall,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct PendingLifecycleHook {
|
||||||
|
hook: install::scripts::Hook,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct InstalledPackageOutcome {
|
struct InstalledPackageOutcome {
|
||||||
@@ -1782,7 +1787,7 @@ fn install_staged_to_rootfs(
|
|||||||
rootfs: &Path,
|
rootfs: &Path,
|
||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
plan: &PlannedStagedInstall,
|
plan: &PlannedStagedInstall,
|
||||||
) -> Result<()> {
|
) -> Result<Option<PendingLifecycleHook>> {
|
||||||
let staged_scripts_dir = install::scripts::staged_scripts_dir(destdir);
|
let staged_scripts_dir = install::scripts::staged_scripts_dir(destdir);
|
||||||
let installed_scripts_dir =
|
let installed_scripts_dir =
|
||||||
install::scripts::installed_scripts_dir(rootfs, &pkg_spec.package.name);
|
install::scripts::installed_scripts_dir(rootfs, &pkg_spec.package.name);
|
||||||
@@ -1848,23 +1853,13 @@ fn install_staged_to_rootfs(
|
|||||||
&pkg_spec.package.name,
|
&pkg_spec.package.name,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if plan.is_update {
|
Ok(Some(PendingLifecycleHook {
|
||||||
let _ = install::scripts::run_hook_if_present(
|
hook: if plan.is_update {
|
||||||
&installed_scripts_dir,
|
install::scripts::Hook::PostUpdate
|
||||||
install::scripts::Hook::PostUpdate,
|
} else {
|
||||||
rootfs,
|
install::scripts::Hook::PostInstall
|
||||||
&pkg_spec.package.name,
|
},
|
||||||
)?;
|
}))
|
||||||
} else {
|
|
||||||
let _ = install::scripts::run_hook_if_present(
|
|
||||||
&installed_scripts_dir,
|
|
||||||
install::scripts::Hook::PostInstall,
|
|
||||||
rootfs,
|
|
||||||
&pkg_spec.package.name,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_planned_packages_to_rootfs(
|
fn install_planned_packages_to_rootfs(
|
||||||
@@ -1873,6 +1868,7 @@ fn install_planned_packages_to_rootfs(
|
|||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut removed_replacements = HashSet::new();
|
let mut removed_replacements = HashSet::new();
|
||||||
|
let mut pending_post_hooks = Vec::new();
|
||||||
for (idx, plan) in plans.iter().enumerate() {
|
for (idx, plan) in plans.iter().enumerate() {
|
||||||
ui::info(format!(
|
ui::info(format!(
|
||||||
"{}/{} Installing package {}-{}-{}",
|
"{}/{} Installing package {}-{}-{}",
|
||||||
@@ -1887,7 +1883,20 @@ fn install_planned_packages_to_rootfs(
|
|||||||
remove_installed_package_with_hooks(package, rootfs, config)?;
|
remove_installed_package_with_hooks(package, rootfs, config)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
install_staged_to_rootfs(&plan.spec, &plan.destdir, rootfs, config, &plan.staged)?;
|
if let Some(hook) =
|
||||||
|
install_staged_to_rootfs(&plan.spec, &plan.destdir, rootfs, config, &plan.staged)?
|
||||||
|
{
|
||||||
|
pending_post_hooks.push((plan.spec.package.name.clone(), hook));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (pkg_name, pending_hook) in pending_post_hooks {
|
||||||
|
let installed_scripts_dir = install::scripts::installed_scripts_dir(rootfs, &pkg_name);
|
||||||
|
let _ = install::scripts::run_hook_if_present_or_defer(
|
||||||
|
&installed_scripts_dir,
|
||||||
|
pending_hook.hook,
|
||||||
|
rootfs,
|
||||||
|
&pkg_name,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
install::scripts::run_deferred_hooks_if_possible(rootfs)?;
|
install::scripts::run_deferred_hooks_if_possible(rootfs)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -3306,28 +3315,31 @@ fn compare_version_fallback(left: &str, right: &str) -> Ordering {
|
|||||||
ri += 1;
|
ri += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let l_num = &left[l_start..li];
|
let l_raw = std::str::from_utf8(&left[l_start..li]).unwrap_or_default();
|
||||||
let r_num = &right[r_start..ri];
|
let r_raw = std::str::from_utf8(&right[r_start..ri]).unwrap_or_default();
|
||||||
let l_trimmed = std::str::from_utf8(l_num)
|
let l_trimmed = l_raw.trim_start_matches('0');
|
||||||
.unwrap_or_default()
|
let r_trimmed = r_raw.trim_start_matches('0');
|
||||||
.trim_start_matches('0');
|
|
||||||
let r_trimmed = std::str::from_utf8(r_num)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.trim_start_matches('0');
|
|
||||||
|
|
||||||
let l_cmp = if l_trimmed.is_empty() { "0" } else { l_trimmed };
|
let l_cmp = if l_trimmed.is_empty() { "0" } else { l_trimmed };
|
||||||
let r_cmp = if r_trimmed.is_empty() { "0" } else { r_trimmed };
|
let r_cmp = if r_trimmed.is_empty() { "0" } else { r_trimmed };
|
||||||
match l_cmp.len().cmp(&r_cmp.len()) {
|
match l_cmp
|
||||||
Ordering::Equal => match l_cmp.cmp(r_cmp) {
|
.len()
|
||||||
Ordering::Equal => {}
|
.cmp(&r_cmp.len())
|
||||||
non_eq => return non_eq,
|
.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,
|
non_eq => return non_eq,
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match lch.to_ascii_lowercase().cmp(&rch.to_ascii_lowercase()) {
|
match lch
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.cmp(&rch.to_ascii_lowercase())
|
||||||
|
.then_with(|| lch.cmp(&rch))
|
||||||
|
{
|
||||||
Ordering::Equal => {
|
Ordering::Equal => {
|
||||||
li += 1;
|
li += 1;
|
||||||
ri += 1;
|
ri += 1;
|
||||||
@@ -3339,14 +3351,20 @@ fn compare_version_fallback(left: &str, right: &str) -> Ordering {
|
|||||||
left.len().cmp(&right.len())
|
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 compare_versions_for_updates(left: &str, right: &str) -> Ordering {
|
fn compare_versions_for_updates(left: &str, right: &str) -> Ordering {
|
||||||
let left_semver = left.trim_start_matches('v');
|
let left = canonical_update_version(left);
|
||||||
let right_semver = right.trim_start_matches('v');
|
let right = canonical_update_version(right);
|
||||||
if let (Ok(left), Ok(right)) = (
|
if let (Ok(left), Ok(right)) = (semver::Version::parse(left), semver::Version::parse(right)) {
|
||||||
semver::Version::parse(left_semver),
|
match left.cmp(&right) {
|
||||||
semver::Version::parse(right_semver),
|
Ordering::Equal => {}
|
||||||
) {
|
non_eq => return non_eq,
|
||||||
return left.cmp(&right);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if left.len() == 8
|
if left.len() == 8
|
||||||
@@ -4342,6 +4360,10 @@ fn run_direct_install_request(
|
|||||||
})?;
|
})?;
|
||||||
let db_path = config.installed_db_path(options.rootfs);
|
let db_path = config.installed_db_path(options.rootfs);
|
||||||
|
|
||||||
|
if staging_dir.is_none() {
|
||||||
|
source::preflight_manual_sources(&pkg_spec, &config.cache_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
if staging_dir.is_none() {
|
if staging_dir.is_none() {
|
||||||
if options.no_deps
|
if options.no_deps
|
||||||
&& should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs)
|
&& should_install_test_deps(&pkg_spec, options.install_test_deps, requested_outputs)
|
||||||
@@ -7158,8 +7180,7 @@ optional = []
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn install_planned_packages_to_rootfs_defers_replacement_removal_until_replacing_plan()
|
fn install_planned_packages_to_rootfs_runs_post_hooks_after_batch_install() -> Result<()> {
|
||||||
-> Result<()> {
|
|
||||||
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
|
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
|
||||||
let mut cfg = config::Config::for_rootfs(rootfs.path());
|
let mut cfg = config::Config::for_rootfs(rootfs.path());
|
||||||
cfg.db_dir = rootfs.path().join("var/lib/depot");
|
cfg.db_dir = rootfs.path().join("var/lib/depot");
|
||||||
@@ -7224,7 +7245,7 @@ optional = []
|
|||||||
fs::write(alpha_dest.join("usr/bin/alpha"), "alpha")?;
|
fs::write(alpha_dest.join("usr/bin/alpha"), "alpha")?;
|
||||||
fs::write(
|
fs::write(
|
||||||
alpha_dest.join("scripts/post_install"),
|
alpha_dest.join("scripts/post_install"),
|
||||||
"if [ -e \"$DEPOT_ROOTFS/usr/bin/find\" ]; then printf '%s' present > \"$DEPOT_ROOTFS/alpha-marker\"; else printf '%s' missing > \"$DEPOT_ROOTFS/alpha-marker\"; fi\n",
|
"cat \"$DEPOT_ROOTFS/usr/bin/find\" > \"$DEPOT_ROOTFS/alpha-marker\"\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let replacement_spec = package::PackageSpec {
|
let replacement_spec = package::PackageSpec {
|
||||||
@@ -7278,7 +7299,7 @@ optional = []
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fs::read_to_string(rootfs.path().join("alpha-marker"))?,
|
fs::read_to_string(rootfs.path().join("alpha-marker"))?,
|
||||||
"present"
|
"new-find"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fs::read_to_string(rootfs.path().join("usr/bin/find"))?,
|
fs::read_to_string(rootfs.path().join("usr/bin/find"))?,
|
||||||
@@ -7625,6 +7646,59 @@ optional = []
|
|||||||
compare_versions_for_updates("1.10", "1.9"),
|
compare_versions_for_updates("1.10", "1.9"),
|
||||||
Ordering::Greater
|
Ordering::Greater
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
compare_versions_for_updates("v1.0.0", "1.0.0"),
|
||||||
|
Ordering::Equal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compare_versions_for_updates_is_transitive_for_mixed_formats() {
|
||||||
|
let versions = [
|
||||||
|
"01",
|
||||||
|
"1a",
|
||||||
|
"1.0.0",
|
||||||
|
"1.2.0",
|
||||||
|
"1.2.0rc2",
|
||||||
|
"v1.0.0",
|
||||||
|
"1.0.0+meta",
|
||||||
|
"20260107.1",
|
||||||
|
"lts_2026_01_07",
|
||||||
|
];
|
||||||
|
|
||||||
|
for left in versions {
|
||||||
|
for middle in versions {
|
||||||
|
for right in versions {
|
||||||
|
let left_middle = compare_versions_for_updates(left, middle);
|
||||||
|
let middle_right = compare_versions_for_updates(middle, right);
|
||||||
|
let left_right = compare_versions_for_updates(left, right);
|
||||||
|
|
||||||
|
if left_middle == Ordering::Less && middle_right == Ordering::Less {
|
||||||
|
assert_eq!(
|
||||||
|
left_right,
|
||||||
|
Ordering::Less,
|
||||||
|
"expected transitive ordering for {left} < {middle} < {right}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if left_middle == Ordering::Greater && middle_right == Ordering::Greater {
|
||||||
|
assert_eq!(
|
||||||
|
left_right,
|
||||||
|
Ordering::Greater,
|
||||||
|
"expected transitive ordering for {left} > {middle} > {right}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if left_middle == Ordering::Equal && middle_right == Ordering::Equal {
|
||||||
|
assert_eq!(
|
||||||
|
left_right,
|
||||||
|
Ordering::Equal,
|
||||||
|
"expected transitive equality for {left} == {middle} == {right}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -7814,6 +7888,67 @@ optional = []
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn direct_install_checks_manual_sources_before_dependency_resolution() -> Result<()> {
|
||||||
|
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
|
||||||
|
let spec_dir = tempfile::tempdir().context("Failed to create temp spec dir")?;
|
||||||
|
let spec_path = spec_dir.path().join("demo.toml");
|
||||||
|
fs::write(
|
||||||
|
&spec_path,
|
||||||
|
r#"[package]
|
||||||
|
name = "demo"
|
||||||
|
version = "1.0.0"
|
||||||
|
revision = 1
|
||||||
|
description = "demo"
|
||||||
|
homepage = "https://example.test/demo"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
runtime = ["definitely-missing-dep"]
|
||||||
|
optional = []
|
||||||
|
|
||||||
|
[[manual_sources]]
|
||||||
|
file = "missing.patch"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut config = config::Config::for_rootfs(rootfs.path());
|
||||||
|
config.build_dir = rootfs.path().join("var/cache/depot/build");
|
||||||
|
config.cache_dir = rootfs.path().join("var/cache/depot/sources");
|
||||||
|
config.db_dir = rootfs.path().join("var/lib/depot");
|
||||||
|
|
||||||
|
ui::set_assume_yes(true);
|
||||||
|
let result = run_direct_install_request(
|
||||||
|
DirectInstallOptions {
|
||||||
|
rootfs: rootfs.path(),
|
||||||
|
no_deps: false,
|
||||||
|
no_flags: false,
|
||||||
|
cross_prefix: None,
|
||||||
|
clean: false,
|
||||||
|
dry_run: false,
|
||||||
|
lib32_only: false,
|
||||||
|
install_test_deps: false,
|
||||||
|
},
|
||||||
|
&config,
|
||||||
|
spec_path,
|
||||||
|
);
|
||||||
|
ui::set_assume_yes(false);
|
||||||
|
|
||||||
|
let err = result.expect_err("missing manual source should fail before dependency install");
|
||||||
|
assert!(
|
||||||
|
err.to_string()
|
||||||
|
.contains("Manual source not found: missing.patch")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!err.to_string()
|
||||||
|
.contains("Could not find package spec for dependency")
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn suppress_nested_install_output_for_planned_context() {
|
fn suppress_nested_install_output_for_planned_context() {
|
||||||
let mut env = TestEnv::new();
|
let mut env = TestEnv::new();
|
||||||
|
|||||||
+176
-20
@@ -250,8 +250,43 @@ pub fn run_hook_if_present(
|
|||||||
rootfs: &Path,
|
rootfs: &Path,
|
||||||
pkg_name: &str,
|
pkg_name: &str,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
|
Ok(matches!(
|
||||||
|
dispatch_hook_if_present(script_dir, hook, rootfs, pkg_name, false)?,
|
||||||
|
HookDispatch::Ran
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a lifecycle hook if present, or queue it for later replay when the
|
||||||
|
/// target rootfs cannot yet execute scripts inside a real chroot.
|
||||||
|
///
|
||||||
|
/// Returns `true` when a hook script was found and either executed or queued.
|
||||||
|
pub fn run_hook_if_present_or_defer(
|
||||||
|
script_dir: &Path,
|
||||||
|
hook: Hook,
|
||||||
|
rootfs: &Path,
|
||||||
|
pkg_name: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
Ok(matches!(
|
||||||
|
dispatch_hook_if_present(script_dir, hook, rootfs, pkg_name, true)?,
|
||||||
|
HookDispatch::Ran | HookDispatch::Deferred
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HookDispatch {
|
||||||
|
Missing,
|
||||||
|
Ran,
|
||||||
|
Deferred,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_hook_if_present(
|
||||||
|
script_dir: &Path,
|
||||||
|
hook: Hook,
|
||||||
|
rootfs: &Path,
|
||||||
|
pkg_name: &str,
|
||||||
|
allow_defer: bool,
|
||||||
|
) -> Result<HookDispatch> {
|
||||||
let Some(script_path) = resolve_hook_script(script_dir, hook, pkg_name)? else {
|
let Some(script_path) = resolve_hook_script(script_dir, hook, pkg_name)? else {
|
||||||
return Ok(false);
|
return Ok(HookDispatch::Missing);
|
||||||
};
|
};
|
||||||
|
|
||||||
crate::log_info!(
|
crate::log_info!(
|
||||||
@@ -260,7 +295,7 @@ pub fn run_hook_if_present(
|
|||||||
script_path.display()
|
script_path.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
match run_script_with_rootfs_context(&script_path, rootfs, pkg_name, hook)? {
|
match run_script_with_rootfs_context(&script_path, rootfs, pkg_name, hook, allow_defer)? {
|
||||||
HookRunOutcome::Ran(status) => {
|
HookRunOutcome::Ran(status) => {
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -269,14 +304,18 @@ pub fn run_hook_if_present(
|
|||||||
script_path.display()
|
script_path.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Ok(HookDispatch::Ran)
|
||||||
|
}
|
||||||
|
HookRunOutcome::Deferred(script_rel) => {
|
||||||
|
queue_deferred_hook(rootfs, pkg_name, hook, &script_rel)?;
|
||||||
|
Ok(HookDispatch::Deferred)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HookRunOutcome {
|
enum HookRunOutcome {
|
||||||
Ran(std::process::ExitStatus),
|
Ran(std::process::ExitStatus),
|
||||||
|
Deferred(PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -418,29 +457,40 @@ fn run_script_with_rootfs_context(
|
|||||||
rootfs: &Path,
|
rootfs: &Path,
|
||||||
pkg_name: &str,
|
pkg_name: &str,
|
||||||
hook: Hook,
|
hook: Hook,
|
||||||
|
allow_defer: bool,
|
||||||
) -> Result<HookRunOutcome> {
|
) -> Result<HookRunOutcome> {
|
||||||
// When root and rootfs provides /bin/sh, run inside a real chroot so scripts
|
if should_use_chroot(rootfs) && fakeroot::is_root() {
|
||||||
// can rely on `cd /` and relative paths resolving within that rootfs.
|
|
||||||
if fakeroot::is_root()
|
|
||||||
&& should_use_chroot(rootfs)
|
|
||||||
&& let Ok(rel) = script_path.strip_prefix(rootfs)
|
|
||||||
{
|
|
||||||
if rootfs.join("bin/sh").exists() {
|
if rootfs.join("bin/sh").exists() {
|
||||||
let status = run_hook_script_in_chroot(rootfs, rel, pkg_name, hook, false)?;
|
let status = if let Ok(rel) = script_path.strip_prefix(rootfs) {
|
||||||
|
run_hook_script_in_chroot(rootfs, rel, pkg_name, hook, false)?
|
||||||
|
} else {
|
||||||
|
run_hook_script_contents_in_chroot(rootfs, script_path, pkg_name, hook, false)?
|
||||||
|
};
|
||||||
return Ok(HookRunOutcome::Ran(status));
|
return Ok(HookRunOutcome::Ran(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::log_warn!(
|
if allow_defer {
|
||||||
"Running lifecycle hook {} for {} without chroot because {} has no /bin/sh",
|
let rel = script_path.strip_prefix(rootfs).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Cannot defer lifecycle hook {} for {} because {} is outside {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
pkg_name,
|
||||||
|
script_path.display(),
|
||||||
|
rootfs.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
return Ok(HookRunOutcome::Deferred(rel.to_path_buf()));
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"Lifecycle hook {} for {} requires {}/bin/sh before it can run",
|
||||||
hook.canonical_name(),
|
hook.canonical_name(),
|
||||||
pkg_name,
|
pkg_name,
|
||||||
rootfs.display()
|
rootfs.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback (non-root / no rootfs shell / script outside rootfs):
|
// Live-root installs can execute directly with the host shell.
|
||||||
// execute with host /bin/sh while setting cwd to rootfs, so relative paths
|
|
||||||
// inside scripts resolve against that rootfs root.
|
|
||||||
let script_arg = if let Ok(rel) = script_path.strip_prefix(rootfs) {
|
let script_arg = if let Ok(rel) = script_path.strip_prefix(rootfs) {
|
||||||
PathBuf::from(format!("./{}", rel.to_string_lossy()))
|
PathBuf::from(format!("./{}", rel.to_string_lossy()))
|
||||||
} else {
|
} else {
|
||||||
@@ -506,6 +556,84 @@ fn run_hook_script_in_chroot(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_hook_script_contents_in_chroot(
|
||||||
|
rootfs: &Path,
|
||||||
|
script_path: &Path,
|
||||||
|
pkg_name: &str,
|
||||||
|
hook: Hook,
|
||||||
|
quiet: bool,
|
||||||
|
) -> Result<std::process::ExitStatus> {
|
||||||
|
let _mounts = mount_chroot_filesystems(rootfs)?;
|
||||||
|
let mut cmd = Command::new("chroot");
|
||||||
|
cmd.arg(rootfs)
|
||||||
|
.arg("/bin/sh")
|
||||||
|
.arg("-s")
|
||||||
|
.env("DEPOT_PACKAGE", pkg_name)
|
||||||
|
.env("DEPOT_ROOTFS", "/")
|
||||||
|
.env("DEPOT_ACTION", hook.action())
|
||||||
|
.env("DEPOT_PHASE", hook.phase())
|
||||||
|
.env("PATH", crate::runtime_env::safe_script_path())
|
||||||
|
.stdin(Stdio::piped());
|
||||||
|
if quiet {
|
||||||
|
cmd.stdout(Stdio::null()).stderr(Stdio::null());
|
||||||
|
}
|
||||||
|
let mut child = cmd.spawn().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to execute lifecycle hook {} in chroot at {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
rootfs.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let script_bytes = fs::read(script_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to read lifecycle hook {} at {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
script_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let mut stdin = child.stdin.take().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to open stdin for lifecycle hook {} in chroot at {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
rootfs.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
stdin.write_all(&script_bytes).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to stream lifecycle hook {} into chroot at {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
rootfs.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
drop(stdin);
|
||||||
|
child.wait().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to wait for lifecycle hook {} in chroot at {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
rootfs.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_deferred_hook(rootfs: &Path, pkg_name: &str, hook: Hook, script_rel: &Path) -> Result<()> {
|
||||||
|
let path = deferred_hooks_file(rootfs);
|
||||||
|
let mut hooks = read_deferred_hooks(&path)?;
|
||||||
|
let item = DeferredHook {
|
||||||
|
pkg_name: pkg_name.to_string(),
|
||||||
|
hook,
|
||||||
|
script_rel: script_rel.to_path_buf(),
|
||||||
|
};
|
||||||
|
if !hooks.iter().any(|existing| {
|
||||||
|
existing.pkg_name == item.pkg_name
|
||||||
|
&& existing.hook == item.hook
|
||||||
|
&& existing.script_rel == item.script_rel
|
||||||
|
}) {
|
||||||
|
hooks.push(item);
|
||||||
|
write_deferred_hooks(&path, &hooks)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn deferred_hooks_file(rootfs: &Path) -> PathBuf {
|
fn deferred_hooks_file(rootfs: &Path) -> PathBuf {
|
||||||
rootfs.join(DEFERRED_HOOKS_FILE_REL)
|
rootfs.join(DEFERRED_HOOKS_FILE_REL)
|
||||||
}
|
}
|
||||||
@@ -602,7 +730,7 @@ pub fn run_deferred_hooks_if_possible(rootfs: &Path) -> Result<()> {
|
|||||||
for item in hooks {
|
for item in hooks {
|
||||||
let script_path = rootfs.join(&item.script_rel);
|
let script_path = rootfs.join(&item.script_rel);
|
||||||
if !script_path.exists() {
|
if !script_path.exists() {
|
||||||
crate::log_warn!(
|
crate::log_info!(
|
||||||
"Dropping deferred lifecycle hook {} for {} because script is missing: {}",
|
"Dropping deferred lifecycle hook {} for {} because script is missing: {}",
|
||||||
item.hook.canonical_name(),
|
item.hook.canonical_name(),
|
||||||
item.pkg_name,
|
item.pkg_name,
|
||||||
@@ -614,7 +742,7 @@ pub fn run_deferred_hooks_if_possible(rootfs: &Path) -> Result<()> {
|
|||||||
match run_hook_script_in_chroot(rootfs, &item.script_rel, &item.pkg_name, item.hook, true) {
|
match run_hook_script_in_chroot(rootfs, &item.script_rel, &item.pkg_name, item.hook, true) {
|
||||||
Ok(status) if status.success() => {}
|
Ok(status) if status.success() => {}
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
crate::log_warn!(
|
crate::log_info!(
|
||||||
"Deferred lifecycle hook {} for {} failed with status {} (kept queued)",
|
"Deferred lifecycle hook {} for {} failed with status {} (kept queued)",
|
||||||
item.hook.canonical_name(),
|
item.hook.canonical_name(),
|
||||||
item.pkg_name,
|
item.pkg_name,
|
||||||
@@ -623,7 +751,7 @@ pub fn run_deferred_hooks_if_possible(rootfs: &Path) -> Result<()> {
|
|||||||
remaining.push(item);
|
remaining.push(item);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
crate::log_warn!(
|
crate::log_info!(
|
||||||
"Deferred lifecycle hook {} for {} failed with error (kept queued): {}",
|
"Deferred lifecycle hook {} for {} failed with error (kept queued): {}",
|
||||||
item.hook.canonical_name(),
|
item.hook.canonical_name(),
|
||||||
item.pkg_name,
|
item.pkg_name,
|
||||||
@@ -636,7 +764,7 @@ pub fn run_deferred_hooks_if_possible(rootfs: &Path) -> Result<()> {
|
|||||||
|
|
||||||
write_deferred_hooks(&path, &remaining)?;
|
write_deferred_hooks(&path, &remaining)?;
|
||||||
if !remaining.is_empty() {
|
if !remaining.is_empty() {
|
||||||
crate::log_warn!(
|
crate::log_info!(
|
||||||
"{} deferred lifecycle hook(s) remain queued in {}",
|
"{} deferred lifecycle hook(s) remain queued in {}",
|
||||||
remaining.len(),
|
remaining.len(),
|
||||||
path.display()
|
path.display()
|
||||||
@@ -1209,6 +1337,34 @@ mod tests {
|
|||||||
assert_eq!(loaded[1].script_rel, hooks[1].script_rel);
|
assert_eq!(loaded[1].script_rel, hooks[1].script_rel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn queue_deferred_hook_dedupes_entries() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
queue_deferred_hook(
|
||||||
|
tmp.path(),
|
||||||
|
"foo",
|
||||||
|
Hook::PostInstall,
|
||||||
|
Path::new("usr/share/depot/foo/scripts/post_install"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
queue_deferred_hook(
|
||||||
|
tmp.path(),
|
||||||
|
"foo",
|
||||||
|
Hook::PostInstall,
|
||||||
|
Path::new("usr/share/depot/foo/scripts/post_install"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let loaded = read_deferred_hooks(&deferred_hooks_file(tmp.path())).unwrap();
|
||||||
|
assert_eq!(loaded.len(), 1);
|
||||||
|
assert_eq!(loaded[0].pkg_name, "foo");
|
||||||
|
assert_eq!(loaded[0].hook, Hook::PostInstall);
|
||||||
|
assert_eq!(
|
||||||
|
loaded[0].script_rel,
|
||||||
|
PathBuf::from("usr/share/depot/foo/scripts/post_install")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_staged_scripts_to_rootfs_replaces_existing_tree() {
|
fn sync_staged_scripts_to_rootfs_replaces_existing_tree() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
+163
-9
@@ -280,6 +280,7 @@ impl<'a> Resolver<'a> {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
if let Some(&idx) = self.by_package.get(&package_name) {
|
if let Some(&idx) = self.by_package.get(&package_name) {
|
||||||
|
self.bail_if_active_cycle(&package_name)?;
|
||||||
self.mark_requested_by(idx, requested_by);
|
self.mark_requested_by(idx, requested_by);
|
||||||
return Ok(idx);
|
return Ok(idx);
|
||||||
}
|
}
|
||||||
@@ -382,7 +383,7 @@ impl<'a> Resolver<'a> {
|
|||||||
if let Some(dep_idx) =
|
if let Some(dep_idx) =
|
||||||
self.resolve_dep_node(dep, None, format!("{} needs {}", record.name, dep))?
|
self.resolve_dep_node(dep, None, format!("{} needs {}", record.name, dep))?
|
||||||
{
|
{
|
||||||
self.graph.add_edge(dep_idx, idx, ());
|
self.add_dependency_edge(dep_idx, idx, &record.name)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,14 +642,7 @@ impl<'a> Resolver<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push_stack(&mut self, pkg: &str) -> Result<()> {
|
fn push_stack(&mut self, pkg: &str) -> Result<()> {
|
||||||
if self.stack.iter().any(|s| s == pkg) {
|
self.bail_if_active_cycle(pkg)?;
|
||||||
let mut chain = self.stack.join(" -> ");
|
|
||||||
if !chain.is_empty() {
|
|
||||||
chain.push_str(" -> ");
|
|
||||||
}
|
|
||||||
chain.push_str(pkg);
|
|
||||||
anyhow::bail!("Dependency cycle detected: {}", chain);
|
|
||||||
}
|
|
||||||
self.stack.push(pkg.to_string());
|
self.stack.push(pkg.to_string());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -658,6 +652,48 @@ impl<'a> Resolver<'a> {
|
|||||||
self.stack.pop();
|
self.stack.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_dependency_edge(
|
||||||
|
&mut self,
|
||||||
|
dep_idx: NodeIndex,
|
||||||
|
dependent_idx: NodeIndex,
|
||||||
|
dependent_package: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let dep_package = self
|
||||||
|
.graph
|
||||||
|
.node_weight(dep_idx)
|
||||||
|
.with_context(|| format!("Missing dependency plan node {dep_idx:?}"))?
|
||||||
|
.step
|
||||||
|
.package
|
||||||
|
.clone();
|
||||||
|
if self.is_active_stack_member(&dep_package) {
|
||||||
|
crate::log_warn!(
|
||||||
|
"dependency cycle detected: {} will be installed before its {} dependency",
|
||||||
|
dependent_package,
|
||||||
|
dep_package
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.graph.add_edge(dep_idx, dependent_idx, ());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bail_if_active_cycle(&self, pkg: &str) -> Result<()> {
|
||||||
|
if let Some(position) = self.stack.iter().position(|entry| entry == pkg) {
|
||||||
|
let mut chain = self.stack[position..].join(" -> ");
|
||||||
|
if !chain.is_empty() {
|
||||||
|
chain.push_str(" -> ");
|
||||||
|
}
|
||||||
|
chain.push_str(pkg);
|
||||||
|
anyhow::bail!("Dependency cycle detected: {}", chain);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_active_stack_member(&self, pkg: &str) -> bool {
|
||||||
|
self.stack.iter().any(|entry| entry == pkg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source_deps_for_install(
|
fn source_deps_for_install(
|
||||||
@@ -862,6 +898,7 @@ mod tests {
|
|||||||
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
|
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
|
||||||
};
|
};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn mk_spec() -> PackageSpec {
|
fn mk_spec() -> PackageSpec {
|
||||||
@@ -1137,4 +1174,121 @@ mod tests {
|
|||||||
assert!(plan.steps.is_empty());
|
assert!(plan.steps.is_empty());
|
||||||
assert!(plan.actionable_steps().next().is_none());
|
assert!(plan.actionable_steps().next().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_dependency_install_plan_reports_source_cycle_chain() {
|
||||||
|
let rootfs = tempfile::tempdir().unwrap();
|
||||||
|
let repo_root = tempfile::tempdir().unwrap();
|
||||||
|
let config = Config::for_rootfs(rootfs.path());
|
||||||
|
|
||||||
|
let alpha_dir = repo_root.path().join("alpha");
|
||||||
|
let beta_dir = repo_root.path().join("beta");
|
||||||
|
fs::create_dir_all(&alpha_dir).unwrap();
|
||||||
|
fs::create_dir_all(&beta_dir).unwrap();
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
alpha_dir.join("alpha.toml"),
|
||||||
|
r#"
|
||||||
|
[build]
|
||||||
|
type = "meta"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
runtime = ["beta"]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
description = "alpha"
|
||||||
|
homepage = "https://example.test/alpha"
|
||||||
|
license = "MIT"
|
||||||
|
name = "alpha"
|
||||||
|
version = "1.0.0"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
beta_dir.join("beta.toml"),
|
||||||
|
r#"
|
||||||
|
[build]
|
||||||
|
type = "meta"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
runtime = ["alpha"]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
description = "beta"
|
||||||
|
homepage = "https://example.test/beta"
|
||||||
|
license = "MIT"
|
||||||
|
name = "beta"
|
||||||
|
version = "1.0.0"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = build_dependency_install_plan(
|
||||||
|
&config,
|
||||||
|
rootfs.path(),
|
||||||
|
&["alpha".to_string()],
|
||||||
|
PlannerOptions {
|
||||||
|
assume_yes: false,
|
||||||
|
prefer_binary: false,
|
||||||
|
local_sibling_root: Some(repo_root.path().to_path_buf()),
|
||||||
|
include_test_deps: false,
|
||||||
|
lib32_only_requested_specs: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"Dependency cycle detected: alpha -> beta -> alpha"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_dependency_edge_skips_active_binary_cycle_back_edge() {
|
||||||
|
let rootfs = tempfile::tempdir().unwrap();
|
||||||
|
let config = Config::for_rootfs(rootfs.path());
|
||||||
|
let mut resolver = super::Resolver::new(
|
||||||
|
&config,
|
||||||
|
rootfs.path(),
|
||||||
|
PlannerOptions {
|
||||||
|
assume_yes: false,
|
||||||
|
prefer_binary: true,
|
||||||
|
local_sibling_root: None,
|
||||||
|
include_test_deps: false,
|
||||||
|
lib32_only_requested_specs: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let freetype2 = resolver.graph.add_node(super::NodeData {
|
||||||
|
step: super::PlannedStep {
|
||||||
|
package: "freetype2".into(),
|
||||||
|
action: super::PlanAction::InstallBinary,
|
||||||
|
origin: super::PlanOrigin::Installed,
|
||||||
|
requested_by: vec!["requested".into()],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let harfbuzz = resolver.graph.add_node(super::NodeData {
|
||||||
|
step: super::PlannedStep {
|
||||||
|
package: "harfbuzz".into(),
|
||||||
|
action: super::PlanAction::InstallBinary,
|
||||||
|
origin: super::PlanOrigin::Installed,
|
||||||
|
requested_by: vec!["requested".into()],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
resolver.stack = vec!["freetype2".into(), "harfbuzz".into()];
|
||||||
|
resolver
|
||||||
|
.add_dependency_edge(freetype2, harfbuzz, "harfbuzz")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolver.graph.edge_count(), 0);
|
||||||
|
|
||||||
|
resolver.stack.clear();
|
||||||
|
resolver
|
||||||
|
.add_dependency_edge(freetype2, harfbuzz, "harfbuzz")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolver.graph.edge_count(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-21
@@ -23,6 +23,120 @@ fn expand_manual_source_value(spec: &PackageSpec, raw: &str) -> String {
|
|||||||
.replace("${CARCH}", carch)
|
.replace("${CARCH}", carch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn manual_local_entries(manual: &crate::package::ManualSource) -> Vec<String> {
|
||||||
|
let mut local_entries = Vec::new();
|
||||||
|
if let Some(file) = manual.file.as_ref()
|
||||||
|
&& !file.trim().is_empty()
|
||||||
|
{
|
||||||
|
local_entries.push(file.clone());
|
||||||
|
}
|
||||||
|
local_entries.extend(
|
||||||
|
manual
|
||||||
|
.files
|
||||||
|
.iter()
|
||||||
|
.filter(|s| !s.trim().is_empty())
|
||||||
|
.cloned(),
|
||||||
|
);
|
||||||
|
local_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn manual_url_entries(manual: &crate::package::ManualSource) -> Vec<String> {
|
||||||
|
let mut url_entries = Vec::new();
|
||||||
|
if let Some(url) = manual.url.as_ref()
|
||||||
|
&& !url.trim().is_empty()
|
||||||
|
{
|
||||||
|
url_entries.push(url.clone());
|
||||||
|
}
|
||||||
|
url_entries.extend(manual.urls.iter().filter(|s| !s.trim().is_empty()).cloned());
|
||||||
|
url_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate manual sources early, before dependency installation and build work.
|
||||||
|
///
|
||||||
|
/// Local entries are checked for existence and optional checksum correctness.
|
||||||
|
/// Remote entries are fetched into the manual-source cache so build-time source
|
||||||
|
/// preparation can reuse the verified result later.
|
||||||
|
pub fn preflight_manual_sources(spec: &PackageSpec, cache_dir: &Path) -> Result<()> {
|
||||||
|
if spec.manual_sources.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let manual_entry_count: usize = spec
|
||||||
|
.manual_sources
|
||||||
|
.iter()
|
||||||
|
.map(|m| manual_local_entries(m).len() + manual_url_entries(m).len())
|
||||||
|
.sum();
|
||||||
|
crate::log_info!("Checking {} manual source(s)...", manual_entry_count);
|
||||||
|
|
||||||
|
for manual in &spec.manual_sources {
|
||||||
|
let local_entries = manual_local_entries(manual);
|
||||||
|
let url_entries = manual_url_entries(manual);
|
||||||
|
|
||||||
|
if !local_entries.is_empty() {
|
||||||
|
for raw_file in local_entries {
|
||||||
|
let file = expand_manual_source_value(spec, &raw_file);
|
||||||
|
let src_path = spec.spec_dir.join(&file);
|
||||||
|
if !src_path.exists() {
|
||||||
|
bail!(
|
||||||
|
"Manual source not found: {} (expected at {})",
|
||||||
|
file,
|
||||||
|
src_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expected_hash) = manual.sha256.as_ref().filter(|h| *h != "skip")
|
||||||
|
&& !verify_file_hash(&src_path, expected_hash)?
|
||||||
|
{
|
||||||
|
bail!("Checksum mismatch for {}: expected {}", file, expected_hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !url_entries.is_empty() {
|
||||||
|
for raw_url in url_entries {
|
||||||
|
let expanded_url = expand_manual_source_value(spec, &raw_url);
|
||||||
|
let parsed = Url::parse(&expanded_url)
|
||||||
|
.with_context(|| format!("Invalid URL: {}", expanded_url))?;
|
||||||
|
|
||||||
|
if parsed.scheme() == "file" {
|
||||||
|
let src_path = parsed
|
||||||
|
.to_file_path()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Invalid file URL: {}", expanded_url))?;
|
||||||
|
if !src_path.exists() {
|
||||||
|
bail!("Manual source file URL not found: {}", src_path.display());
|
||||||
|
}
|
||||||
|
if let Some(expected_hash) = manual.sha256.as_ref().filter(|h| *h != "skip")
|
||||||
|
&& !verify_file_hash(&src_path, expected_hash)?
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"Checksum mismatch for {}: expected {}",
|
||||||
|
expanded_url,
|
||||||
|
expected_hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = crate::package::Source {
|
||||||
|
url: expanded_url,
|
||||||
|
sha256: manual.sha256.clone().unwrap_or_else(|| "skip".to_string()),
|
||||||
|
extract_dir: "manual-source".to_string(),
|
||||||
|
patches: Vec::new(),
|
||||||
|
post_extract: Vec::new(),
|
||||||
|
cherry_pick: Vec::new(),
|
||||||
|
};
|
||||||
|
let _ = fetcher::fetch_archive(spec, &source, &cache_dir.join("manual"))?;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("Manual source must define one of 'file', 'files', 'url', or 'urls'");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Copy manual sources to the build directory before fetching remote sources.
|
/// Copy manual sources to the build directory before fetching remote sources.
|
||||||
///
|
///
|
||||||
/// Manual sources support:
|
/// Manual sources support:
|
||||||
@@ -47,27 +161,8 @@ pub fn copy_manual_sources(spec: &PackageSpec, cache_dir: &Path, build_dir: &Pat
|
|||||||
crate::log_info!("Copying {} manual source(s)...", manual_entry_count);
|
crate::log_info!("Copying {} manual source(s)...", manual_entry_count);
|
||||||
|
|
||||||
for manual in &spec.manual_sources {
|
for manual in &spec.manual_sources {
|
||||||
let mut local_entries: Vec<String> = Vec::new();
|
let local_entries = manual_local_entries(manual);
|
||||||
if let Some(file) = manual.file.as_ref()
|
let url_entries = manual_url_entries(manual);
|
||||||
&& !file.trim().is_empty()
|
|
||||||
{
|
|
||||||
local_entries.push(file.clone());
|
|
||||||
}
|
|
||||||
local_entries.extend(
|
|
||||||
manual
|
|
||||||
.files
|
|
||||||
.iter()
|
|
||||||
.filter(|s| !s.trim().is_empty())
|
|
||||||
.cloned(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut url_entries: Vec<String> = Vec::new();
|
|
||||||
if let Some(url) = manual.url.as_ref()
|
|
||||||
&& !url.trim().is_empty()
|
|
||||||
{
|
|
||||||
url_entries.push(url.clone());
|
|
||||||
}
|
|
||||||
url_entries.extend(manual.urls.iter().filter(|s| !s.trim().is_empty()).cloned());
|
|
||||||
|
|
||||||
if !local_entries.is_empty() {
|
if !local_entries.is_empty() {
|
||||||
for raw_file in local_entries {
|
for raw_file in local_entries {
|
||||||
|
|||||||
Reference in New Issue
Block a user