feat: add support for deferred lifecycle hooks and improve binary package download progress tracking
This commit is contained in:
Generated
+61
-4
@@ -406,9 +406,10 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
"suppaftp",
|
"suppaftp",
|
||||||
|
"sys-mount",
|
||||||
"tar",
|
"tar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -1653,7 +1654,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
|
checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1920,6 +1921,17 @@ version = "1.15.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smart-default"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -1970,7 +1982,7 @@ dependencies = [
|
|||||||
"futures-lite",
|
"futures-lite",
|
||||||
"lazy-regex",
|
"lazy-regex",
|
||||||
"log",
|
"log",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2004,6 +2016,19 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sys-mount"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d361f5431256ea04c657c0dce767ad95e24fb054d99f0b8a9275cb1c9ea14bfb"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
"smart-default",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tar"
|
name = "tar"
|
||||||
version = "0.4.44"
|
version = "0.4.44"
|
||||||
@@ -2028,13 +2053,33 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.18"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2202,9 +2247,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ reqwest = { version = "0.13.2", default-features = false, features = ["blocking"
|
|||||||
filetime = "0.2.27"
|
filetime = "0.2.27"
|
||||||
clap_complete = "4.5.66"
|
clap_complete = "4.5.66"
|
||||||
clap_mangen = "0.2.31"
|
clap_mangen = "0.2.31"
|
||||||
|
sys-mount = { version = "3.1.0", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "=3.25.0"
|
tempfile = "=3.25.0"
|
||||||
|
|||||||
+86
-35
@@ -4,8 +4,10 @@ use crate::{
|
|||||||
signing, source, staging, ui,
|
signing, source, staging, ui,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::IsTerminal;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn parse_licenses_from_toml(metadata: &toml::Value) -> Vec<String> {
|
fn parse_licenses_from_toml(metadata: &toml::Value) -> Vec<String> {
|
||||||
@@ -920,6 +922,18 @@ fn human_bytes(bytes: u64) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
fn print_plan_summary(plan: &planner::ExecutionPlan) {
|
fn print_plan_summary(plan: &planner::ExecutionPlan) {
|
||||||
let summary = plan.summary();
|
let summary = plan.summary();
|
||||||
ui::info(format!(
|
ui::info(format!(
|
||||||
@@ -1027,24 +1041,46 @@ fn execute_install_plan_with_child_commands(
|
|||||||
"Downloading {} binary package(s) and detached signatures...",
|
"Downloading {} binary package(s) and detached signatures...",
|
||||||
binary_phase_items.len()
|
binary_phase_items.len()
|
||||||
));
|
));
|
||||||
for (idx, item) in binary_phase_items.iter().enumerate() {
|
let use_tty_progress = std::io::stderr().is_terminal();
|
||||||
ui::info(format!(
|
for item in &binary_phase_items {
|
||||||
" [{}/{}] {}-{} ({})",
|
let label = format!(
|
||||||
idx + 1,
|
"{}-{}-{}",
|
||||||
binary_phase_items.len(),
|
|
||||||
item.record.name,
|
item.record.name,
|
||||||
item.record.version,
|
item.record.version,
|
||||||
item.repo_name
|
binary_arch_from_filename(&item.record.filename)
|
||||||
));
|
);
|
||||||
|
let pb = ProgressBar::new(item.record.size.max(1));
|
||||||
|
pb.set_draw_target(if use_tty_progress {
|
||||||
|
ProgressDrawTarget::stderr()
|
||||||
|
} else {
|
||||||
|
ProgressDrawTarget::hidden()
|
||||||
|
});
|
||||||
|
pb.set_style(
|
||||||
|
ProgressStyle::default_bar()
|
||||||
|
.template("{prefix:.bold} [{bar:40.cyan/blue}] {eta}")
|
||||||
|
.unwrap_or_else(|_| ProgressStyle::default_bar())
|
||||||
|
.progress_chars("#>-"),
|
||||||
|
);
|
||||||
|
pb.set_prefix(label);
|
||||||
|
|
||||||
let repo_cfg = config
|
let repo_cfg = config
|
||||||
.binary_repos
|
.binary_repos
|
||||||
.get(&item.repo_name)
|
.get(&item.repo_name)
|
||||||
.with_context(|| format!("Binary repo '{}' not found in config", item.repo_name))?;
|
.with_context(|| format!("Binary repo '{}' not found in config", item.repo_name))?;
|
||||||
let cached = db::repo::cache_binary_package_archive(
|
let mut progress_cb = |downloaded: u64, total: Option<u64>| {
|
||||||
|
if let Some(t) = total
|
||||||
|
&& t > 0
|
||||||
|
{
|
||||||
|
pb.set_length(t);
|
||||||
|
}
|
||||||
|
pb.set_position(downloaded);
|
||||||
|
};
|
||||||
|
let cached = db::repo::cache_binary_package_archive_with_progress(
|
||||||
&item.repo_name,
|
&item.repo_name,
|
||||||
repo_cfg,
|
repo_cfg,
|
||||||
&item.record,
|
&item.record,
|
||||||
&config.package_cache_dir,
|
&config.package_cache_dir,
|
||||||
|
Some(&mut progress_cb),
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
@@ -1052,6 +1088,7 @@ fn execute_install_plan_with_child_commands(
|
|||||||
item.record.filename, item.repo_name
|
item.record.filename, item.repo_name
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
pb.finish_and_clear();
|
||||||
binary_archives.insert(
|
binary_archives.insert(
|
||||||
(item.repo_name.clone(), item.record.filename.clone()),
|
(item.repo_name.clone(), item.record.filename.clone()),
|
||||||
cached,
|
cached,
|
||||||
@@ -1062,14 +1099,20 @@ fn execute_install_plan_with_child_commands(
|
|||||||
"Verifying checksums for {} binary package(s)...",
|
"Verifying checksums for {} binary package(s)...",
|
||||||
binary_phase_items.len()
|
binary_phase_items.len()
|
||||||
));
|
));
|
||||||
for (idx, item) in binary_phase_items.iter().enumerate() {
|
let checksum_pb = ProgressBar::new(binary_phase_items.len() as u64);
|
||||||
ui::info(format!(
|
checksum_pb.set_draw_target(if use_tty_progress {
|
||||||
" [{}/{}] {}-{}",
|
ProgressDrawTarget::stderr()
|
||||||
idx + 1,
|
} else {
|
||||||
binary_phase_items.len(),
|
ProgressDrawTarget::hidden()
|
||||||
item.record.name,
|
});
|
||||||
item.record.version
|
checksum_pb.set_style(
|
||||||
));
|
ProgressStyle::default_bar()
|
||||||
|
.template("{prefix:.bold} [{bar:40.cyan/blue}] {pos}/{len} {eta}")
|
||||||
|
.unwrap_or_else(|_| ProgressStyle::default_bar())
|
||||||
|
.progress_chars("#>-"),
|
||||||
|
);
|
||||||
|
checksum_pb.set_prefix("checksums");
|
||||||
|
for item in &binary_phase_items {
|
||||||
let cached = binary_archives
|
let cached = binary_archives
|
||||||
.get(&(item.repo_name.clone(), item.record.filename.clone()))
|
.get(&(item.repo_name.clone(), item.record.filename.clone()))
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
@@ -1085,20 +1128,28 @@ fn execute_install_plan_with_child_commands(
|
|||||||
item.record.filename, item.repo_name
|
item.record.filename, item.repo_name
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
checksum_pb.inc(1);
|
||||||
}
|
}
|
||||||
|
checksum_pb.finish_and_clear();
|
||||||
|
|
||||||
ui::info(format!(
|
ui::info(format!(
|
||||||
"Verifying detached signatures for {} binary package(s)...",
|
"Verifying detached signatures for {} binary package(s)...",
|
||||||
binary_phase_items.len()
|
binary_phase_items.len()
|
||||||
));
|
));
|
||||||
for (idx, item) in binary_phase_items.iter().enumerate() {
|
let signature_pb = ProgressBar::new(binary_phase_items.len() as u64);
|
||||||
ui::info(format!(
|
signature_pb.set_draw_target(if use_tty_progress {
|
||||||
" [{}/{}] {}-{}",
|
ProgressDrawTarget::stderr()
|
||||||
idx + 1,
|
} else {
|
||||||
binary_phase_items.len(),
|
ProgressDrawTarget::hidden()
|
||||||
item.record.name,
|
});
|
||||||
item.record.version
|
signature_pb.set_style(
|
||||||
));
|
ProgressStyle::default_bar()
|
||||||
|
.template("{prefix:.bold} [{bar:40.cyan/blue}] {pos}/{len} {eta}")
|
||||||
|
.unwrap_or_else(|_| ProgressStyle::default_bar())
|
||||||
|
.progress_chars("#>-"),
|
||||||
|
);
|
||||||
|
signature_pb.set_prefix("signatures");
|
||||||
|
for item in &binary_phase_items {
|
||||||
let repo_cfg = config
|
let repo_cfg = config
|
||||||
.binary_repos
|
.binary_repos
|
||||||
.get(&item.repo_name)
|
.get(&item.repo_name)
|
||||||
@@ -1124,7 +1175,9 @@ fn execute_install_plan_with_child_commands(
|
|||||||
item.record.filename, item.repo_name
|
item.record.filename, item.repo_name
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
signature_pb.inc(1);
|
||||||
}
|
}
|
||||||
|
signature_pb.finish_and_clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
|
let exe = std::env::current_exe().context("Failed to locate depot executable")?;
|
||||||
@@ -1162,16 +1215,6 @@ fn execute_install_plan_with_child_commands(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
planner::PlanOrigin::Binary { repo_name, record } => {
|
planner::PlanOrigin::Binary { repo_name, record } => {
|
||||||
ui::info(format!(
|
|
||||||
"[{}/{}] installing {} {}-{} from binary:{}",
|
|
||||||
idx + 1,
|
|
||||||
total_steps,
|
|
||||||
record.name,
|
|
||||||
record.version,
|
|
||||||
record.revision,
|
|
||||||
repo_name
|
|
||||||
));
|
|
||||||
|
|
||||||
let cached = binary_archives
|
let cached = binary_archives
|
||||||
.get(&(repo_name.clone(), record.filename.clone()))
|
.get(&(repo_name.clone(), record.filename.clone()))
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
@@ -1191,6 +1234,8 @@ fn execute_install_plan_with_child_commands(
|
|||||||
planner::PlanOrigin::Installed => {}
|
planner::PlanOrigin::Installed => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install::scripts::run_deferred_hooks_if_possible(rootfs)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1202,7 +1247,9 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
spec_or_archive,
|
spec_or_archive,
|
||||||
spec,
|
spec,
|
||||||
} => {
|
} => {
|
||||||
warn_if_running_as_root_for_build("install", &cli.rootfs);
|
if !crate::fakeroot::is_root() {
|
||||||
|
anyhow::bail!("The 'install' command must be run as root");
|
||||||
|
}
|
||||||
let mut spec_path = spec.unwrap_or(spec_or_archive);
|
let mut spec_path = spec.unwrap_or(spec_or_archive);
|
||||||
|
|
||||||
// Load configuration early so we can use the configured repo clone dir
|
// Load configuration early so we can use the configured repo clone dir
|
||||||
@@ -1535,6 +1582,8 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install::scripts::run_deferred_hooks_if_possible(&cli.rootfs)?;
|
||||||
|
|
||||||
if cli.clean {
|
if cli.clean {
|
||||||
clean_build_workspace(&config)?;
|
clean_build_workspace(&config)?;
|
||||||
}
|
}
|
||||||
@@ -1820,6 +1869,8 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
lib32_spec.package.name, lib32_spec.package.version
|
lib32_spec.package.name, lib32_spec.package.version
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install::scripts::run_deferred_hooks_if_possible(&cli.rootfs)?;
|
||||||
} else {
|
} else {
|
||||||
if !cli.lib32_only {
|
if !cli.lib32_only {
|
||||||
for out in pkg_spec.outputs() {
|
for out in pkg_spec.outputs() {
|
||||||
|
|||||||
+49
-12
@@ -1661,9 +1661,15 @@ fn download_binary_package_archive(
|
|||||||
client: &reqwest::blocking::Client,
|
client: &reqwest::blocking::Client,
|
||||||
pkg_url: &str,
|
pkg_url: &str,
|
||||||
tmp_path: &Path,
|
tmp_path: &Path,
|
||||||
|
progress_cb: &mut Option<&mut dyn FnMut(u64, Option<u64>)>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
match copy_file_url_to_path(pkg_url, tmp_path)? {
|
match copy_file_url_to_path(pkg_url, tmp_path)? {
|
||||||
FileUrlCopyOutcome::Copied => {}
|
FileUrlCopyOutcome::Copied => {
|
||||||
|
if let Some(cb) = progress_cb.as_mut() {
|
||||||
|
let total = fs::metadata(tmp_path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
cb(total, Some(total));
|
||||||
|
}
|
||||||
|
}
|
||||||
FileUrlCopyOutcome::Missing => {
|
FileUrlCopyOutcome::Missing => {
|
||||||
anyhow::bail!("Failed to fetch {}: local file not found", pkg_url);
|
anyhow::bail!("Failed to fetch {}: local file not found", pkg_url);
|
||||||
}
|
}
|
||||||
@@ -1676,10 +1682,29 @@ fn download_binary_package_archive(
|
|||||||
anyhow::bail!("Failed to fetch {}: HTTP {}", pkg_url, resp.status());
|
anyhow::bail!("Failed to fetch {}: HTTP {}", pkg_url, resp.status());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total = resp.content_length();
|
||||||
|
if let Some(cb) = progress_cb.as_mut() {
|
||||||
|
cb(0, total);
|
||||||
|
}
|
||||||
|
|
||||||
let mut out = fs::File::create(tmp_path)
|
let mut out = fs::File::create(tmp_path)
|
||||||
.with_context(|| format!("Failed to create {}", tmp_path.display()))?;
|
.with_context(|| format!("Failed to create {}", tmp_path.display()))?;
|
||||||
std::io::copy(&mut resp, &mut out)
|
let mut downloaded = 0u64;
|
||||||
.with_context(|| format!("Failed to save {}", tmp_path.display()))?;
|
let mut buf = [0u8; 64 * 1024];
|
||||||
|
loop {
|
||||||
|
let n = resp
|
||||||
|
.read(&mut buf)
|
||||||
|
.with_context(|| format!("Failed to read {}", pkg_url))?;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.write_all(&buf[..n])
|
||||||
|
.with_context(|| format!("Failed to save {}", tmp_path.display()))?;
|
||||||
|
downloaded = downloaded.saturating_add(n as u64);
|
||||||
|
if let Some(cb) = progress_cb.as_mut() {
|
||||||
|
cb(downloaded, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
out.flush()
|
out.flush()
|
||||||
.with_context(|| format!("Failed to flush {}", tmp_path.display()))?;
|
.with_context(|| format!("Failed to flush {}", tmp_path.display()))?;
|
||||||
}
|
}
|
||||||
@@ -1744,18 +1769,13 @@ fn verify_binary_package_signature(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let verified_key = verify_with_any_trusted_public_key(rootfs, pkg_path, sig_path)
|
let _verified_key = verify_with_any_trusted_public_key(rootfs, pkg_path, sig_path)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to verify detached package signature for {}",
|
"Failed to verify detached package signature for {}",
|
||||||
pkg_path.display()
|
pkg_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
crate::log_info!(
|
|
||||||
"Verified detached package signature for '{}' using {}",
|
|
||||||
pkg_path.display(),
|
|
||||||
verified_key.display()
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1766,6 +1786,19 @@ pub fn cache_binary_package_archive(
|
|||||||
repo: &crate::config::BinaryRepo,
|
repo: &crate::config::BinaryRepo,
|
||||||
rec: &BinaryRepoPackageRecord,
|
rec: &BinaryRepoPackageRecord,
|
||||||
package_cache_dir: &Path,
|
package_cache_dir: &Path,
|
||||||
|
) -> Result<BinaryRepoCachedArchive> {
|
||||||
|
cache_binary_package_archive_with_progress(repo_name, repo, rec, package_cache_dir, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure a binary package archive and detached signature are present in cache
|
||||||
|
/// without performing checksum/signature verification, optionally reporting
|
||||||
|
/// download progress.
|
||||||
|
pub fn cache_binary_package_archive_with_progress(
|
||||||
|
repo_name: &str,
|
||||||
|
repo: &crate::config::BinaryRepo,
|
||||||
|
rec: &BinaryRepoPackageRecord,
|
||||||
|
package_cache_dir: &Path,
|
||||||
|
mut progress_cb: Option<&mut dyn FnMut(u64, Option<u64>)>,
|
||||||
) -> Result<BinaryRepoCachedArchive> {
|
) -> Result<BinaryRepoCachedArchive> {
|
||||||
let machine_arch = std::env::consts::ARCH;
|
let machine_arch = std::env::consts::ARCH;
|
||||||
let base_url = repo.effective_url_for_arch(machine_arch).with_context(|| {
|
let base_url = repo.effective_url_for_arch(machine_arch).with_context(|| {
|
||||||
@@ -1790,8 +1823,7 @@ pub fn cache_binary_package_archive(
|
|||||||
.context("Failed to build HTTP client for binary package fetch")?;
|
.context("Failed to build HTTP client for binary package fetch")?;
|
||||||
|
|
||||||
let package_downloaded = if !package_path.exists() {
|
let package_downloaded = if !package_path.exists() {
|
||||||
crate::log_info!("Fetching binary package: {}", pkg_url);
|
download_binary_package_archive(&client, &pkg_url, &tmp_path, &mut progress_cb)?;
|
||||||
download_binary_package_archive(&client, &pkg_url, &tmp_path)?;
|
|
||||||
fs::rename(&tmp_path, &package_path).with_context(|| {
|
fs::rename(&tmp_path, &package_path).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to move {} to {}",
|
"Failed to move {} to {}",
|
||||||
@@ -1801,7 +1833,12 @@ pub fn cache_binary_package_archive(
|
|||||||
})?;
|
})?;
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
crate::log_info!("Using cached binary package: {}", package_path.display());
|
if let Some(cb) = progress_cb.as_mut() {
|
||||||
|
let total = fs::metadata(&package_path)
|
||||||
|
.with_context(|| format!("Failed to stat {}", package_path.display()))?
|
||||||
|
.len();
|
||||||
|
cb(total, Some(total));
|
||||||
|
}
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+460
-35
@@ -4,11 +4,14 @@ use crate::fakeroot;
|
|||||||
use crate::package::PackageSpec;
|
use crate::package::PackageSpec;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::{BufRead, Write};
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::{Command, Stdio};
|
||||||
|
use sys_mount::{Mount, MountFlags, Unmount, UnmountFlags};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
const STAGED_SCRIPTS_DIR: &str = "scripts";
|
const STAGED_SCRIPTS_DIR: &str = "scripts";
|
||||||
|
const DEFERRED_HOOKS_FILE_REL: &str = "var/lib/depot/deferred-hooks.tsv";
|
||||||
const ALL_HOOKS: [Hook; 6] = [
|
const ALL_HOOKS: [Hook; 6] = [
|
||||||
Hook::PreInstall,
|
Hook::PreInstall,
|
||||||
Hook::PostInstall,
|
Hook::PostInstall,
|
||||||
@@ -62,6 +65,18 @@ impl Hook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn from_canonical_name(name: &str) -> Option<Self> {
|
||||||
|
match name {
|
||||||
|
"pre_install" => Some(Hook::PreInstall),
|
||||||
|
"post_install" => Some(Hook::PostInstall),
|
||||||
|
"pre_update" => Some(Hook::PreUpdate),
|
||||||
|
"post_update" => Some(Hook::PostUpdate),
|
||||||
|
"pre_remove" => Some(Hook::PreRemove),
|
||||||
|
"post_remove" => Some(Hook::PostRemove),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn candidate_names(self) -> [String; 6] {
|
fn candidate_names(self) -> [String; 6] {
|
||||||
let canonical = self.canonical_name();
|
let canonical = self.canonical_name();
|
||||||
let dashed = canonical.replace('_', "-");
|
let dashed = canonical.replace('_', "-");
|
||||||
@@ -102,6 +117,13 @@ impl Hook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct DeferredHook {
|
||||||
|
pkg_name: String,
|
||||||
|
hook: Hook,
|
||||||
|
script_rel: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the staged scripts directory path inside a package staging tree.
|
/// Return the staged scripts directory path inside a package staging tree.
|
||||||
pub fn staged_scripts_dir(destdir: &Path) -> PathBuf {
|
pub fn staged_scripts_dir(destdir: &Path) -> PathBuf {
|
||||||
destdir.join(STAGED_SCRIPTS_DIR)
|
destdir.join(STAGED_SCRIPTS_DIR)
|
||||||
@@ -233,61 +255,216 @@ pub fn run_hook_if_present(
|
|||||||
script_path.display()
|
script_path.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
let status = run_script_with_rootfs_context(&script_path, rootfs, pkg_name, hook)?;
|
match run_script_with_rootfs_context(&script_path, rootfs, pkg_name, hook)? {
|
||||||
|
HookRunOutcome::Ran(status) => {
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
bail!(
|
bail!(
|
||||||
"Lifecycle hook {} failed: {}",
|
"Lifecycle hook {} failed: {}",
|
||||||
hook.canonical_name(),
|
hook.canonical_name(),
|
||||||
script_path.display()
|
script_path.display()
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HookRunOutcome::Deferred => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HookRunOutcome {
|
||||||
|
Ran(std::process::ExitStatus),
|
||||||
|
Deferred,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ChrootMountGuard {
|
||||||
|
mounted: Vec<Mount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChrootMountGuard {
|
||||||
|
fn mount_path(
|
||||||
|
&mut self,
|
||||||
|
source: &Path,
|
||||||
|
target: &Path,
|
||||||
|
fstype: Option<&str>,
|
||||||
|
flags: MountFlags,
|
||||||
|
data: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut builder = Mount::builder().flags(flags);
|
||||||
|
if let Some(fs) = fstype {
|
||||||
|
builder = builder.fstype(fs);
|
||||||
|
}
|
||||||
|
if let Some(options) = data {
|
||||||
|
builder = builder.data(options);
|
||||||
|
}
|
||||||
|
let mount = builder
|
||||||
|
.mount(source, target)
|
||||||
|
.with_context(|| format!("Failed to mount {}", target.display()))?;
|
||||||
|
self.mounted.push(mount);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ChrootMountGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for mount in self.mounted.iter().rev() {
|
||||||
|
if mount.unmount(UnmountFlags::empty()).is_ok() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = mount.unmount(UnmountFlags::DETACH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_use_chroot(rootfs: &Path) -> bool {
|
||||||
|
let canonical_root = fs::canonicalize("/").ok();
|
||||||
|
let canonical_rootfs = fs::canonicalize(rootfs).ok();
|
||||||
|
match (canonical_rootfs, canonical_root) {
|
||||||
|
(Some(target), Some(root)) => target != root,
|
||||||
|
_ => rootfs != Path::new("/"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mount_chroot_filesystems(rootfs: &Path) -> Result<ChrootMountGuard> {
|
||||||
|
let proc_dir = rootfs.join("proc");
|
||||||
|
let dev_dir = rootfs.join("dev");
|
||||||
|
let dev_pts_dir = dev_dir.join("pts");
|
||||||
|
let sys_dir = rootfs.join("sys");
|
||||||
|
|
||||||
|
fs::create_dir_all(&proc_dir)
|
||||||
|
.with_context(|| format!("Failed to create {}", proc_dir.display()))?;
|
||||||
|
fs::create_dir_all(&dev_dir)
|
||||||
|
.with_context(|| format!("Failed to create {}", dev_dir.display()))?;
|
||||||
|
fs::create_dir_all(&dev_pts_dir)
|
||||||
|
.with_context(|| format!("Failed to create {}", dev_pts_dir.display()))?;
|
||||||
|
fs::create_dir_all(&sys_dir)
|
||||||
|
.with_context(|| format!("Failed to create {}", sys_dir.display()))?;
|
||||||
|
|
||||||
|
let mut guard = ChrootMountGuard::default();
|
||||||
|
guard.mount_path(
|
||||||
|
Path::new("proc"),
|
||||||
|
&proc_dir,
|
||||||
|
Some("proc"),
|
||||||
|
MountFlags::NODEV | MountFlags::NOEXEC | MountFlags::NOSUID,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
guard.mount_path(Path::new("/dev"), &dev_dir, None, MountFlags::BIND, None)?;
|
||||||
|
guard.mount_path(
|
||||||
|
Path::new("sysfs"),
|
||||||
|
&sys_dir,
|
||||||
|
Some("sysfs"),
|
||||||
|
MountFlags::NODEV | MountFlags::NOEXEC | MountFlags::NOSUID,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
if let Err(_e) = guard.mount_path(
|
||||||
|
Path::new("devpts"),
|
||||||
|
&dev_pts_dir,
|
||||||
|
Some("devpts"),
|
||||||
|
MountFlags::NOSUID | MountFlags::NOEXEC,
|
||||||
|
Some("gid=5,mode=620"),
|
||||||
|
) {
|
||||||
|
guard.mount_path(
|
||||||
|
Path::new("devpts"),
|
||||||
|
&dev_pts_dir,
|
||||||
|
Some("devpts"),
|
||||||
|
MountFlags::NOSUID | MountFlags::NOEXEC,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_bind_host_file_into_chroot(&mut guard, rootfs, Path::new("/etc/resolv.conf"))?;
|
||||||
|
Ok(guard)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_bind_host_file_into_chroot(
|
||||||
|
guard: &mut ChrootMountGuard,
|
||||||
|
rootfs: &Path,
|
||||||
|
host_path: &Path,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !host_path.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel = host_path
|
||||||
|
.strip_prefix(Path::new("/"))
|
||||||
|
.with_context(|| format!("Expected absolute host path: {}", host_path.display()))?;
|
||||||
|
let target = rootfs.join(rel);
|
||||||
|
|
||||||
|
if let Some(parent) = target.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
if !target.exists() {
|
||||||
|
fs::File::create(&target)
|
||||||
|
.with_context(|| format!("Failed to create {}", target.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = guard.mount_path(host_path, &target, None, MountFlags::BIND, None) {
|
||||||
|
crate::log_warn!(
|
||||||
|
"Failed to bind-mount {} into chroot at {}: {}",
|
||||||
|
host_path.display(),
|
||||||
|
target.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn run_script_with_rootfs_context(
|
fn run_script_with_rootfs_context(
|
||||||
script_path: &Path,
|
script_path: &Path,
|
||||||
rootfs: &Path,
|
rootfs: &Path,
|
||||||
pkg_name: &str,
|
pkg_name: &str,
|
||||||
hook: Hook,
|
hook: Hook,
|
||||||
) -> Result<std::process::ExitStatus> {
|
) -> Result<HookRunOutcome> {
|
||||||
// When root and rootfs provides /bin/sh, run inside a real chroot so scripts
|
// When root and rootfs provides /bin/sh, run inside a real chroot so scripts
|
||||||
// can rely on `cd /` and relative paths resolving within that rootfs.
|
// can rely on `cd /` and relative paths resolving within that rootfs.
|
||||||
if fakeroot::is_root()
|
if fakeroot::is_root()
|
||||||
&& rootfs.join("bin/sh").exists()
|
&& should_use_chroot(rootfs)
|
||||||
&& let Ok(rel) = script_path.strip_prefix(rootfs)
|
&& let Ok(rel) = script_path.strip_prefix(rootfs)
|
||||||
{
|
{
|
||||||
let rel_script = format!("./{}", rel.to_string_lossy());
|
if rootfs.join("bin/sh").exists() {
|
||||||
return Command::new("chroot")
|
let status = run_hook_script_in_chroot(rootfs, rel, pkg_name, hook, false)?;
|
||||||
.arg(rootfs)
|
return Ok(HookRunOutcome::Ran(status));
|
||||||
.arg("/bin/sh")
|
}
|
||||||
.arg("-c")
|
|
||||||
.arg("export DEPOT_PACKAGE DEPOT_ROOTFS DEPOT_ACTION DEPOT_PHASE; cd / && exec /bin/sh \"$1\"")
|
// Post hooks can be deferred until the target shell exists and chroot
|
||||||
.arg("sh")
|
// execution is available.
|
||||||
.arg(rel_script)
|
if matches!(
|
||||||
.env("DEPOT_PACKAGE", pkg_name)
|
hook,
|
||||||
.env("DEPOT_ROOTFS", "/")
|
Hook::PostInstall | Hook::PostUpdate | Hook::PostRemove
|
||||||
.env("DEPOT_ACTION", hook.action())
|
) {
|
||||||
.env("DEPOT_PHASE", hook.phase())
|
enqueue_deferred_hook(rootfs, pkg_name, hook, rel)?;
|
||||||
.status()
|
crate::log_warn!(
|
||||||
.with_context(|| {
|
"Deferred lifecycle hook {} for {} until {} has /bin/sh",
|
||||||
format!(
|
hook.canonical_name(),
|
||||||
"Failed to execute lifecycle hook {} in chroot at {}",
|
pkg_name,
|
||||||
hook.canonical_name(),
|
rootfs.display()
|
||||||
rootfs.display()
|
);
|
||||||
)
|
return Ok(HookRunOutcome::Deferred);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback (non-root / no rootfs shell / script outside rootfs):
|
// Fallback (non-root / no rootfs shell / script outside rootfs):
|
||||||
// execute with host /bin/sh while setting cwd to rootfs, so relative paths
|
// execute with host /bin/sh while setting cwd to rootfs, so relative paths
|
||||||
// inside scripts resolve against that rootfs root.
|
// inside scripts resolve against that rootfs root.
|
||||||
Command::new("/bin/sh")
|
let script_arg = if let Ok(rel) = script_path.strip_prefix(rootfs) {
|
||||||
.arg(script_path)
|
PathBuf::from(format!("./{}", rel.to_string_lossy()))
|
||||||
|
} else {
|
||||||
|
script_path.to_path_buf()
|
||||||
|
};
|
||||||
|
let rootfs_env = if rootfs.is_absolute() {
|
||||||
|
rootfs.to_path_buf()
|
||||||
|
} else {
|
||||||
|
std::env::current_dir()
|
||||||
|
.context("Failed to resolve current working directory for DEPOT_ROOTFS")?
|
||||||
|
.join(rootfs)
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = Command::new("/bin/sh")
|
||||||
|
.arg(script_arg)
|
||||||
.current_dir(rootfs)
|
.current_dir(rootfs)
|
||||||
.env("DEPOT_PACKAGE", pkg_name)
|
.env("DEPOT_PACKAGE", pkg_name)
|
||||||
.env("DEPOT_ROOTFS", rootfs)
|
.env("DEPOT_ROOTFS", &rootfs_env)
|
||||||
.env("DEPOT_ACTION", hook.action())
|
.env("DEPOT_ACTION", hook.action())
|
||||||
.env("DEPOT_PHASE", hook.phase())
|
.env("DEPOT_PHASE", hook.phase())
|
||||||
.status()
|
.status()
|
||||||
@@ -297,7 +474,201 @@ fn run_script_with_rootfs_context(
|
|||||||
hook.canonical_name(),
|
hook.canonical_name(),
|
||||||
script_path.display()
|
script_path.display()
|
||||||
)
|
)
|
||||||
})
|
})?;
|
||||||
|
Ok(HookRunOutcome::Ran(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_hook_script_in_chroot(
|
||||||
|
rootfs: &Path,
|
||||||
|
rel_script: &Path,
|
||||||
|
pkg_name: &str,
|
||||||
|
hook: Hook,
|
||||||
|
quiet: bool,
|
||||||
|
) -> Result<std::process::ExitStatus> {
|
||||||
|
let _mounts = mount_chroot_filesystems(rootfs)?;
|
||||||
|
let rel_script = format!("./{}", rel_script.to_string_lossy());
|
||||||
|
let mut cmd = Command::new("chroot");
|
||||||
|
cmd.arg(rootfs)
|
||||||
|
.arg("/bin/sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("export DEPOT_PACKAGE DEPOT_ROOTFS DEPOT_ACTION DEPOT_PHASE; cd / && exec /bin/sh \"$1\"")
|
||||||
|
.arg("sh")
|
||||||
|
.arg(rel_script)
|
||||||
|
.env("DEPOT_PACKAGE", pkg_name)
|
||||||
|
.env("DEPOT_ROOTFS", "/")
|
||||||
|
.env("DEPOT_ACTION", hook.action())
|
||||||
|
.env("DEPOT_PHASE", hook.phase());
|
||||||
|
if quiet {
|
||||||
|
cmd.stdout(Stdio::null()).stderr(Stdio::null());
|
||||||
|
}
|
||||||
|
cmd.status().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to execute lifecycle hook {} in chroot at {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
rootfs.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deferred_hooks_file(rootfs: &Path) -> PathBuf {
|
||||||
|
rootfs.join(DEFERRED_HOOKS_FILE_REL)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_deferred_hooks(path: &Path) -> Result<Vec<DeferredHook>> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file =
|
||||||
|
fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
|
||||||
|
let reader = std::io::BufReader::new(file);
|
||||||
|
let mut hooks = Vec::new();
|
||||||
|
|
||||||
|
for (idx, line) in reader.lines().enumerate() {
|
||||||
|
let line =
|
||||||
|
line.with_context(|| format!("Failed to read deferred hook line {}", idx + 1))?;
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut parts = line.splitn(3, '\t');
|
||||||
|
let pkg_name = parts
|
||||||
|
.next()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.with_context(|| format!("Malformed deferred hook line {} (package)", idx + 1))?;
|
||||||
|
let hook_name = parts
|
||||||
|
.next()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.with_context(|| format!("Malformed deferred hook line {} (hook)", idx + 1))?;
|
||||||
|
let script_rel = parts
|
||||||
|
.next()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.with_context(|| format!("Malformed deferred hook line {} (script)", idx + 1))?;
|
||||||
|
let hook = Hook::from_canonical_name(hook_name).with_context(|| {
|
||||||
|
format!("Unknown deferred hook '{}' on line {}", hook_name, idx + 1)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
hooks.push(DeferredHook {
|
||||||
|
pkg_name: pkg_name.to_string(),
|
||||||
|
hook,
|
||||||
|
script_rel: PathBuf::from(script_rel),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_deferred_hooks(path: &Path, hooks: &[DeferredHook]) -> Result<()> {
|
||||||
|
if hooks.is_empty() {
|
||||||
|
if path.exists() {
|
||||||
|
fs::remove_file(path)
|
||||||
|
.with_context(|| format!("Failed to remove {}", path.display()))?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut f =
|
||||||
|
fs::File::create(path).with_context(|| format!("Failed to create {}", path.display()))?;
|
||||||
|
for item in hooks {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{}\t{}\t{}",
|
||||||
|
item.pkg_name,
|
||||||
|
item.hook.canonical_name(),
|
||||||
|
item.script_rel.display()
|
||||||
|
)
|
||||||
|
.with_context(|| format!("Failed to write {}", path.display()))?;
|
||||||
|
}
|
||||||
|
f.flush()
|
||||||
|
.with_context(|| format!("Failed to flush {}", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enqueue_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)?;
|
||||||
|
if hooks
|
||||||
|
.iter()
|
||||||
|
.any(|h| h.pkg_name == pkg_name && h.hook == hook && h.script_rel == script_rel)
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
hooks.push(DeferredHook {
|
||||||
|
pkg_name: pkg_name.to_string(),
|
||||||
|
hook,
|
||||||
|
script_rel: script_rel.to_path_buf(),
|
||||||
|
});
|
||||||
|
write_deferred_hooks(&path, &hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to run deferred lifecycle hooks for this rootfs once.
|
||||||
|
///
|
||||||
|
/// Deferred hooks are best-effort: failures are kept in queue for later retry.
|
||||||
|
pub fn run_deferred_hooks_if_possible(rootfs: &Path) -> Result<()> {
|
||||||
|
if !fakeroot::is_root() || !should_use_chroot(rootfs) || !rootfs.join("bin/sh").exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = deferred_hooks_file(rootfs);
|
||||||
|
let hooks = read_deferred_hooks(&path)?;
|
||||||
|
if hooks.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut remaining = Vec::new();
|
||||||
|
for item in hooks {
|
||||||
|
let script_path = rootfs.join(&item.script_rel);
|
||||||
|
if !script_path.exists() {
|
||||||
|
crate::log_warn!(
|
||||||
|
"Dropping deferred lifecycle hook {} for {} because script is missing: {}",
|
||||||
|
item.hook.canonical_name(),
|
||||||
|
item.pkg_name,
|
||||||
|
script_path.display()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match run_hook_script_in_chroot(rootfs, &item.script_rel, &item.pkg_name, item.hook, true) {
|
||||||
|
Ok(status) if status.success() => {}
|
||||||
|
Ok(status) => {
|
||||||
|
crate::log_warn!(
|
||||||
|
"Deferred lifecycle hook {} for {} failed with status {} (kept queued)",
|
||||||
|
item.hook.canonical_name(),
|
||||||
|
item.pkg_name,
|
||||||
|
status
|
||||||
|
);
|
||||||
|
remaining.push(item);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
crate::log_warn!(
|
||||||
|
"Deferred lifecycle hook {} for {} failed with error (kept queued): {}",
|
||||||
|
item.hook.canonical_name(),
|
||||||
|
item.pkg_name,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
remaining.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_deferred_hooks(&path, &remaining)?;
|
||||||
|
if !remaining.is_empty() {
|
||||||
|
crate::log_warn!(
|
||||||
|
"{} deferred lifecycle hook(s) remain queued in {}",
|
||||||
|
remaining.len(),
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_hook_script(script_dir: &Path, hook: Hook, pkg_name: &str) -> Result<Option<PathBuf>> {
|
fn resolve_hook_script(script_dir: &Path, hook: Hook, pkg_name: &str) -> Result<Option<PathBuf>> {
|
||||||
@@ -762,6 +1133,60 @@ mod tests {
|
|||||||
assert!(err.to_string().contains("Ambiguous lifecycle hook"));
|
assert!(err.to_string().contains("Ambiguous lifecycle hook"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_hook_if_present_with_relative_rootfs_uses_correct_script_and_env_paths() {
|
||||||
|
let cwd = std::env::current_dir().unwrap();
|
||||||
|
let tmp = tempfile::Builder::new()
|
||||||
|
.prefix("depot-hook-rel-rootfs-")
|
||||||
|
.tempdir_in(&cwd)
|
||||||
|
.unwrap();
|
||||||
|
let rootfs_abs = tmp.path().join("root");
|
||||||
|
std::fs::create_dir_all(&rootfs_abs).unwrap();
|
||||||
|
let rootfs_rel = rootfs_abs.strip_prefix(&cwd).unwrap().to_path_buf();
|
||||||
|
let scripts = rootfs_rel.join("scripts");
|
||||||
|
std::fs::create_dir_all(&scripts).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
scripts.join("post_install"),
|
||||||
|
"echo ok > \"$DEPOT_ROOTFS/hook.out\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ran = run_hook_if_present(&scripts, Hook::PostInstall, &rootfs_rel, "foo").unwrap();
|
||||||
|
assert!(ran);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(rootfs_abs.join("hook.out")).unwrap(),
|
||||||
|
"ok\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deferred_hooks_file_roundtrip() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("hooks.tsv");
|
||||||
|
let hooks = vec![
|
||||||
|
DeferredHook {
|
||||||
|
pkg_name: "foo".into(),
|
||||||
|
hook: Hook::PostInstall,
|
||||||
|
script_rel: PathBuf::from("usr/share/depot/foo/scripts/post_install"),
|
||||||
|
},
|
||||||
|
DeferredHook {
|
||||||
|
pkg_name: "bar".into(),
|
||||||
|
hook: Hook::PostUpdate,
|
||||||
|
script_rel: PathBuf::from("usr/share/depot/bar/scripts/post_update"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
write_deferred_hooks(&path, &hooks).unwrap();
|
||||||
|
let loaded = read_deferred_hooks(&path).unwrap();
|
||||||
|
assert_eq!(loaded.len(), hooks.len());
|
||||||
|
assert_eq!(loaded[0].pkg_name, hooks[0].pkg_name);
|
||||||
|
assert_eq!(loaded[0].hook, hooks[0].hook);
|
||||||
|
assert_eq!(loaded[0].script_rel, hooks[0].script_rel);
|
||||||
|
assert_eq!(loaded[1].pkg_name, hooks[1].pkg_name);
|
||||||
|
assert_eq!(loaded[1].hook, hooks[1].hook);
|
||||||
|
assert_eq!(loaded[1].script_rel, hooks[1].script_rel);
|
||||||
|
}
|
||||||
|
|
||||||
#[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();
|
||||||
|
|||||||
Reference in New Issue
Block a user