feat: Implement lifecycle script management with staging and execution hooks
This commit is contained in:
@@ -97,8 +97,6 @@ pub fn build(
|
|||||||
anyhow::bail!("cmake build failed");
|
anyhow::bail!("cmake build failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: CMake doesn't have a direct "after make, before install" hook as easy as autotools,
|
|
||||||
// but we can run it here.
|
|
||||||
crate::source::hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
|
crate::source::hooks::run_post_compile_commands(spec, &actual_src, destdir)?;
|
||||||
state.mark_done(BuildStep::PostCompileDone)?;
|
state.mark_done(BuildStep::PostCompileDone)?;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
//! Installation helpers.
|
||||||
|
|
||||||
|
pub mod scripts;
|
||||||
@@ -0,0 +1,593 @@
|
|||||||
|
//! Lifecycle script staging, installation, and execution.
|
||||||
|
|
||||||
|
use crate::fakeroot;
|
||||||
|
use crate::package::PackageSpec;
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
const STAGED_SCRIPTS_DIR: &str = "scripts";
|
||||||
|
|
||||||
|
/// Lifecycle hook names supported by Depot package scripts.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Hook {
|
||||||
|
/// Runs before a first-time install.
|
||||||
|
PreInstall,
|
||||||
|
/// Runs after a first-time install.
|
||||||
|
PostInstall,
|
||||||
|
/// Runs before an update/upgrade.
|
||||||
|
PreUpdate,
|
||||||
|
/// Runs after an update/upgrade.
|
||||||
|
PostUpdate,
|
||||||
|
/// Runs before package removal.
|
||||||
|
PreRemove,
|
||||||
|
/// Runs after package removal.
|
||||||
|
PostRemove,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hook {
|
||||||
|
fn canonical_name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Hook::PreInstall => "pre_install",
|
||||||
|
Hook::PostInstall => "post_install",
|
||||||
|
Hook::PreUpdate => "pre_update",
|
||||||
|
Hook::PostUpdate => "post_update",
|
||||||
|
Hook::PreRemove => "pre_remove",
|
||||||
|
Hook::PostRemove => "post_remove",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Hook::PreInstall | Hook::PostInstall => "install",
|
||||||
|
Hook::PreUpdate | Hook::PostUpdate => "update",
|
||||||
|
Hook::PreRemove | Hook::PostRemove => "remove",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn phase(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Hook::PreInstall | Hook::PreUpdate | Hook::PreRemove => "pre",
|
||||||
|
Hook::PostInstall | Hook::PostUpdate | Hook::PostRemove => "post",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate_names(self) -> [String; 4] {
|
||||||
|
let canonical = self.canonical_name();
|
||||||
|
let dashed = canonical.replace('_', "-");
|
||||||
|
[
|
||||||
|
canonical.to_string(),
|
||||||
|
format!("{}.sh", canonical),
|
||||||
|
dashed.clone(),
|
||||||
|
format!("{}.sh", dashed),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the staged scripts directory path inside a package staging tree.
|
||||||
|
pub fn staged_scripts_dir(destdir: &Path) -> PathBuf {
|
||||||
|
destdir.join(STAGED_SCRIPTS_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the installed scripts directory path inside the root filesystem.
|
||||||
|
pub fn installed_scripts_dir(rootfs: &Path, pkg_name: &str) -> PathBuf {
|
||||||
|
rootfs
|
||||||
|
.join("usr/share/depot")
|
||||||
|
.join(pkg_name)
|
||||||
|
.join("scripts")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy optional scripts from `<specdir>/scripts` into `<destdir>/scripts`.
|
||||||
|
///
|
||||||
|
/// Returns `true` if scripts were found and staged.
|
||||||
|
pub fn stage_scripts_from_spec_dir(spec: &PackageSpec, destdir: &Path) -> Result<bool> {
|
||||||
|
let source_dir = spec.spec_dir.join(STAGED_SCRIPTS_DIR);
|
||||||
|
if !source_dir.exists() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
if !source_dir.is_dir() {
|
||||||
|
bail!(
|
||||||
|
"Scripts path exists but is not a directory: {}",
|
||||||
|
source_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let staged_dir = staged_scripts_dir(destdir);
|
||||||
|
if staged_dir.exists() {
|
||||||
|
remove_tree_or_file(&staged_dir).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to remove existing staged scripts dir: {}",
|
||||||
|
staged_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_tree(&source_dir, &staged_dir)
|
||||||
|
.with_context(|| format!("Failed to stage scripts from {}", source_dir.display()))?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronize staged scripts into `/usr/share/depot/<pkgname>/scripts`.
|
||||||
|
///
|
||||||
|
/// If no staged scripts are present, any previously installed scripts for the
|
||||||
|
/// package are removed.
|
||||||
|
///
|
||||||
|
/// Returns `true` when scripts are present after synchronization.
|
||||||
|
pub fn sync_staged_scripts_to_rootfs(
|
||||||
|
staged_dir: &Path,
|
||||||
|
rootfs: &Path,
|
||||||
|
pkg_name: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let installed_dir = installed_scripts_dir(rootfs, pkg_name);
|
||||||
|
|
||||||
|
if staged_dir.exists() {
|
||||||
|
if !staged_dir.is_dir() {
|
||||||
|
bail!(
|
||||||
|
"Staged scripts path exists but is not a directory: {}",
|
||||||
|
staged_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if installed_dir.exists() {
|
||||||
|
remove_tree_or_file(&installed_dir).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to remove existing installed scripts: {}",
|
||||||
|
installed_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_tree(staged_dir, &installed_dir).with_context(|| {
|
||||||
|
format!("Failed to install scripts into {}", installed_dir.display())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_installed_scripts(rootfs, pkg_name)?;
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove installed scripts for a package and clean empty package metadata dir.
|
||||||
|
pub fn remove_installed_scripts(rootfs: &Path, pkg_name: &str) -> Result<()> {
|
||||||
|
let installed_dir = installed_scripts_dir(rootfs, pkg_name);
|
||||||
|
if installed_dir.exists() {
|
||||||
|
remove_tree_or_file(&installed_dir).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to remove installed scripts directory: {}",
|
||||||
|
installed_dir.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_empty_package_dir(rootfs, pkg_name)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a lifecycle hook if its script exists in `script_dir`.
|
||||||
|
///
|
||||||
|
/// Returns `true` if a hook script was found and executed.
|
||||||
|
pub fn run_hook_if_present(
|
||||||
|
script_dir: &Path,
|
||||||
|
hook: Hook,
|
||||||
|
rootfs: &Path,
|
||||||
|
pkg_name: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let Some(script_path) = resolve_hook_script(script_dir, hook)? else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Running lifecycle hook {}: {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
script_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let status = run_script_with_rootfs_context(&script_path, rootfs, pkg_name, hook)?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"Lifecycle hook {} failed: {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
script_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_script_with_rootfs_context(
|
||||||
|
script_path: &Path,
|
||||||
|
rootfs: &Path,
|
||||||
|
pkg_name: &str,
|
||||||
|
hook: Hook,
|
||||||
|
) -> Result<std::process::ExitStatus> {
|
||||||
|
// 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.
|
||||||
|
if fakeroot::is_root()
|
||||||
|
&& rootfs.join("bin/sh").exists()
|
||||||
|
&& let Ok(rel) = script_path.strip_prefix(rootfs)
|
||||||
|
{
|
||||||
|
let rel_script = format!("./{}", rel.to_string_lossy());
|
||||||
|
return Command::new("chroot")
|
||||||
|
.arg(rootfs)
|
||||||
|
.arg("/bin/sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("cd / && exec \"$1\"")
|
||||||
|
.arg("sh")
|
||||||
|
.arg(rel_script)
|
||||||
|
.env("DEPOT_PACKAGE", pkg_name)
|
||||||
|
.env("DEPOT_ROOTFS", rootfs)
|
||||||
|
.env("DEPOT_ACTION", hook.action())
|
||||||
|
.env("DEPOT_PHASE", hook.phase())
|
||||||
|
.status()
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to execute lifecycle hook {} in chroot at {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
rootfs.display()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (non-root / no rootfs shell / script outside rootfs):
|
||||||
|
// execute with host /bin/sh while setting cwd to rootfs, so relative paths
|
||||||
|
// inside scripts resolve against that rootfs root.
|
||||||
|
Command::new("/bin/sh")
|
||||||
|
.arg(script_path)
|
||||||
|
.current_dir(rootfs)
|
||||||
|
.env("DEPOT_PACKAGE", pkg_name)
|
||||||
|
.env("DEPOT_ROOTFS", rootfs)
|
||||||
|
.env("DEPOT_ACTION", hook.action())
|
||||||
|
.env("DEPOT_PHASE", hook.phase())
|
||||||
|
.status()
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to execute lifecycle hook {} at {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
script_path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_hook_script(script_dir: &Path, hook: Hook) -> Result<Option<PathBuf>> {
|
||||||
|
if !script_dir.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !script_dir.is_dir() {
|
||||||
|
bail!(
|
||||||
|
"Scripts path exists but is not a directory: {}",
|
||||||
|
script_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut found = Vec::new();
|
||||||
|
|
||||||
|
for candidate in hook.candidate_names() {
|
||||||
|
let path = script_dir.join(&candidate);
|
||||||
|
let metadata = match path.symlink_metadata() {
|
||||||
|
Ok(meta) => meta,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e)
|
||||||
|
.with_context(|| format!("Failed to inspect script path: {}", path.display()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_type = metadata.file_type();
|
||||||
|
if !file_type.is_file() && !file_type.is_symlink() {
|
||||||
|
bail!(
|
||||||
|
"Lifecycle hook candidate exists but is not a file: {}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
found.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if found.len() > 1 {
|
||||||
|
let names = found
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.display().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
bail!(
|
||||||
|
"Ambiguous lifecycle hook '{}': multiple script candidates found: {}",
|
||||||
|
hook.canonical_name(),
|
||||||
|
names
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(found.into_iter().next())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn safe_rel_path(rel: &Path) -> Result<PathBuf> {
|
||||||
|
let mut normalized = PathBuf::new();
|
||||||
|
for component in rel.components() {
|
||||||
|
match component {
|
||||||
|
Component::Normal(seg) => normalized.push(seg),
|
||||||
|
Component::CurDir => {}
|
||||||
|
_ => {
|
||||||
|
bail!(
|
||||||
|
"Unsafe scripts path component encountered: {}",
|
||||||
|
rel.display()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalized.as_os_str().is_empty() {
|
||||||
|
bail!("Scripts path resolves to empty path: {}", rel.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_tree(src_root: &Path, dst_root: &Path) -> Result<()> {
|
||||||
|
fs::create_dir_all(dst_root)
|
||||||
|
.with_context(|| format!("Failed to create directory: {}", dst_root.display()))?;
|
||||||
|
|
||||||
|
for entry in WalkDir::new(src_root).follow_links(false) {
|
||||||
|
let entry = entry
|
||||||
|
.with_context(|| format!("Failed to walk scripts directory: {}", src_root.display()))?;
|
||||||
|
|
||||||
|
let src_path = entry.path();
|
||||||
|
let rel = src_path
|
||||||
|
.strip_prefix(src_root)
|
||||||
|
.with_context(|| format!("Failed to strip script root: {}", src_path.display()))?;
|
||||||
|
|
||||||
|
if rel.as_os_str().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel = safe_rel_path(rel)?;
|
||||||
|
let dst_path = dst_root.join(rel);
|
||||||
|
let file_type = entry.file_type();
|
||||||
|
|
||||||
|
if file_type.is_dir() {
|
||||||
|
fs::create_dir_all(&dst_path)
|
||||||
|
.with_context(|| format!("Failed to create directory: {}", dst_path.display()))?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = dst_path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Failed to create directory: {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_type.is_symlink() {
|
||||||
|
let target = fs::read_link(src_path)
|
||||||
|
.with_context(|| format!("Failed to read symlink: {}", src_path.display()))?;
|
||||||
|
if dst_path.symlink_metadata().is_ok() {
|
||||||
|
remove_tree_or_file(&dst_path).with_context(|| {
|
||||||
|
format!("Failed to replace existing path: {}", dst_path.display())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
std::os::unix::fs::symlink(target, &dst_path)
|
||||||
|
.with_context(|| format!("Failed to create symlink: {}", dst_path.display()))?;
|
||||||
|
} else if file_type.is_file() {
|
||||||
|
fs::copy(src_path, &dst_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to copy script '{}' to '{}'",
|
||||||
|
src_path.display(),
|
||||||
|
dst_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
ensure_executable(&dst_path)?;
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"Unsupported file type in scripts directory: {}",
|
||||||
|
src_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_executable(path: &Path) -> Result<()> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let metadata = fs::metadata(path)
|
||||||
|
.with_context(|| format!("Failed to inspect script permissions: {}", path.display()))?;
|
||||||
|
let mut perms = metadata.permissions();
|
||||||
|
let mode = perms.mode();
|
||||||
|
let new_mode = mode | 0o111;
|
||||||
|
if new_mode != mode {
|
||||||
|
perms.set_mode(new_mode);
|
||||||
|
fs::set_permissions(path, perms).with_context(|| {
|
||||||
|
format!("Failed to set executable permissions: {}", path.display())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_tree_or_file(path: &Path) -> Result<()> {
|
||||||
|
let metadata = path
|
||||||
|
.symlink_metadata()
|
||||||
|
.with_context(|| format!("Failed to inspect path: {}", path.display()))?;
|
||||||
|
if metadata.file_type().is_dir() {
|
||||||
|
fs::remove_dir_all(path)
|
||||||
|
.with_context(|| format!("Failed to remove directory: {}", path.display()))?;
|
||||||
|
} else {
|
||||||
|
fs::remove_file(path)
|
||||||
|
.with_context(|| format!("Failed to remove file: {}", path.display()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_empty_package_dir(rootfs: &Path, pkg_name: &str) -> Result<()> {
|
||||||
|
let package_dir = rootfs.join("usr/share/depot").join(pkg_name);
|
||||||
|
if !package_dir.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_empty = fs::read_dir(&package_dir)
|
||||||
|
.with_context(|| format!("Failed to read directory: {}", package_dir.display()))?
|
||||||
|
.next()
|
||||||
|
.is_none();
|
||||||
|
|
||||||
|
if is_empty {
|
||||||
|
fs::remove_dir(&package_dir)
|
||||||
|
.with_context(|| format!("Failed to remove directory: {}", package_dir.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::package::{
|
||||||
|
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, Source,
|
||||||
|
};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
fn mk_spec(spec_dir: &Path) -> PackageSpec {
|
||||||
|
PackageSpec {
|
||||||
|
package: PackageInfo {
|
||||||
|
name: "foo".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "d".into(),
|
||||||
|
homepage: "h".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: Alternatives::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: vec![Source {
|
||||||
|
url: "https://example.com/foo.tar.gz".into(),
|
||||||
|
sha256: "skip".into(),
|
||||||
|
extract_dir: "foo".into(),
|
||||||
|
patches: Vec::new(),
|
||||||
|
post_extract: Vec::new(),
|
||||||
|
}],
|
||||||
|
build: Build {
|
||||||
|
build_type: BuildType::Custom,
|
||||||
|
flags: BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: Dependencies::default(),
|
||||||
|
spec_dir: spec_dir.to_path_buf(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stage_scripts_from_spec_dir_copies_tree() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let spec_dir = tmp.path().join("spec");
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
std::fs::create_dir_all(spec_dir.join("scripts/lib")).unwrap();
|
||||||
|
std::fs::create_dir_all(&destdir).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(spec_dir.join("scripts/pre_install"), "echo pre").unwrap();
|
||||||
|
std::fs::write(spec_dir.join("scripts/lib/common.sh"), "echo lib").unwrap();
|
||||||
|
|
||||||
|
let spec = mk_spec(&spec_dir);
|
||||||
|
let staged = stage_scripts_from_spec_dir(&spec, &destdir).unwrap();
|
||||||
|
assert!(staged);
|
||||||
|
assert!(destdir.join("scripts/pre_install").exists());
|
||||||
|
assert!(destdir.join("scripts/lib/common.sh").exists());
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let mode = std::fs::metadata(destdir.join("scripts/pre_install"))
|
||||||
|
.unwrap()
|
||||||
|
.permissions()
|
||||||
|
.mode();
|
||||||
|
assert_ne!(mode & 0o111, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_hook_if_present_executes_script() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let scripts = tmp.path().join("scripts");
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
std::fs::create_dir_all(&scripts).unwrap();
|
||||||
|
std::fs::create_dir_all(&rootfs).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
scripts.join("pre_install"),
|
||||||
|
"echo \"$DEPOT_ACTION:$DEPOT_PHASE:$DEPOT_PACKAGE\" > \"$DEPOT_ROOTFS/hook.out\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ran = run_hook_if_present(&scripts, Hook::PreInstall, &rootfs, "foo").unwrap();
|
||||||
|
assert!(ran);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(rootfs.join("hook.out")).unwrap(),
|
||||||
|
"install:pre:foo\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_hook_if_present_rejects_ambiguous_names() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let scripts = tmp.path().join("scripts");
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
std::fs::create_dir_all(&scripts).unwrap();
|
||||||
|
std::fs::create_dir_all(&rootfs).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(scripts.join("pre_update"), "echo one").unwrap();
|
||||||
|
std::fs::write(scripts.join("pre-update"), "echo two").unwrap();
|
||||||
|
|
||||||
|
let err = run_hook_if_present(&scripts, Hook::PreUpdate, &rootfs, "foo")
|
||||||
|
.expect_err("expected ambiguous script names to fail");
|
||||||
|
assert!(err.to_string().contains("Ambiguous lifecycle hook"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_staged_scripts_to_rootfs_replaces_existing_tree() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
let staged = tmp.path().join("staged");
|
||||||
|
std::fs::create_dir_all(staged.join("scripts")).unwrap();
|
||||||
|
|
||||||
|
let installed = installed_scripts_dir(&rootfs, "foo");
|
||||||
|
std::fs::create_dir_all(&installed).unwrap();
|
||||||
|
std::fs::write(installed.join("old"), "old").unwrap();
|
||||||
|
|
||||||
|
std::fs::write(staged.join("scripts/post_install"), "echo ok").unwrap();
|
||||||
|
let has_scripts =
|
||||||
|
sync_staged_scripts_to_rootfs(&staged.join("scripts"), &rootfs, "foo").unwrap();
|
||||||
|
|
||||||
|
assert!(has_scripts);
|
||||||
|
let installed = installed_scripts_dir(&rootfs, "foo");
|
||||||
|
assert!(!installed.join("old").exists());
|
||||||
|
assert!(installed.join("post_install").exists());
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let mode = std::fs::metadata(installed.join("post_install"))
|
||||||
|
.unwrap()
|
||||||
|
.permissions()
|
||||||
|
.mode();
|
||||||
|
assert_ne!(mode & 0o111, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_staged_scripts_to_rootfs_removes_old_when_none_staged() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
let staged = tmp.path().join("staged");
|
||||||
|
std::fs::create_dir_all(&staged).unwrap();
|
||||||
|
|
||||||
|
let installed = installed_scripts_dir(&rootfs, "foo");
|
||||||
|
std::fs::create_dir_all(&installed).unwrap();
|
||||||
|
std::fs::write(installed.join("pre_remove"), "echo old").unwrap();
|
||||||
|
|
||||||
|
let has_scripts =
|
||||||
|
sync_staged_scripts_to_rootfs(&staged.join("scripts"), &rootfs, "foo").unwrap();
|
||||||
|
assert!(!has_scripts);
|
||||||
|
assert!(!installed_scripts_dir(&rootfs, "foo").exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
-5
@@ -8,6 +8,7 @@ mod db;
|
|||||||
mod deps;
|
mod deps;
|
||||||
mod fakeroot;
|
mod fakeroot;
|
||||||
mod index;
|
mod index;
|
||||||
|
mod install;
|
||||||
mod package;
|
mod package;
|
||||||
mod source;
|
mod source;
|
||||||
mod staging;
|
mod staging;
|
||||||
@@ -71,6 +72,35 @@ fn install_staged_to_rootfs(
|
|||||||
})?;
|
})?;
|
||||||
let db_path = config.db_dir.join("packages.db");
|
let db_path = config.db_dir.join("packages.db");
|
||||||
|
|
||||||
|
let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some();
|
||||||
|
let staged_scripts_dir = install::scripts::staged_scripts_dir(destdir);
|
||||||
|
let installed_scripts_dir =
|
||||||
|
install::scripts::installed_scripts_dir(rootfs, &pkg_spec.package.name);
|
||||||
|
|
||||||
|
if is_update {
|
||||||
|
let has_staged_pre = install::scripts::run_hook_if_present(
|
||||||
|
&staged_scripts_dir,
|
||||||
|
install::scripts::Hook::PreUpdate,
|
||||||
|
rootfs,
|
||||||
|
&pkg_spec.package.name,
|
||||||
|
)?;
|
||||||
|
if !has_staged_pre {
|
||||||
|
let _ = install::scripts::run_hook_if_present(
|
||||||
|
&installed_scripts_dir,
|
||||||
|
install::scripts::Hook::PreUpdate,
|
||||||
|
rootfs,
|
||||||
|
&pkg_spec.package.name,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = install::scripts::run_hook_if_present(
|
||||||
|
&staged_scripts_dir,
|
||||||
|
install::scripts::Hook::PreInstall,
|
||||||
|
rootfs,
|
||||||
|
&pkg_spec.package.name,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
let new_files = staging::generate_manifest_with_dirs(destdir)?;
|
let new_files = staging::generate_manifest_with_dirs(destdir)?;
|
||||||
let remove_paths =
|
let remove_paths =
|
||||||
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_files.files)?;
|
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_files.files)?;
|
||||||
@@ -94,6 +124,28 @@ fn install_staged_to_rootfs(
|
|||||||
}
|
}
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
|
|
||||||
|
install::scripts::sync_staged_scripts_to_rootfs(
|
||||||
|
&staged_scripts_dir,
|
||||||
|
rootfs,
|
||||||
|
&pkg_spec.package.name,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if is_update {
|
||||||
|
let _ = install::scripts::run_hook_if_present(
|
||||||
|
&installed_scripts_dir,
|
||||||
|
install::scripts::Hook::PostUpdate,
|
||||||
|
rootfs,
|
||||||
|
&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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,11 +399,6 @@ fn main() -> Result<()> {
|
|||||||
// Install from spec (normal build)
|
// Install from spec (normal build)
|
||||||
let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?;
|
let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?;
|
||||||
pkg_spec.apply_config(&config);
|
pkg_spec.apply_config(&config);
|
||||||
|
|
||||||
// ... existing build logic ...
|
|
||||||
// Jump to the part where build actually happens.
|
|
||||||
// To keep the code clean, I'll move the build/stage logic into a helper or similar?
|
|
||||||
// Actually, I'll just structure it so we can skip build if staging_dir is Some.
|
|
||||||
(pkg_spec, None)
|
(pkg_spec, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -513,6 +560,7 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
// 3.1 Copy license files into staged tree
|
// 3.1 Copy license files into staged tree
|
||||||
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
|
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
|
||||||
|
install::scripts::stage_scripts_from_spec_dir(&pkg_spec, &destdir)?;
|
||||||
|
|
||||||
destdir
|
destdir
|
||||||
};
|
};
|
||||||
@@ -551,7 +599,23 @@ fn main() -> Result<()> {
|
|||||||
println!("Removing package: {}", package);
|
println!("Removing package: {}", package);
|
||||||
let config = config::Config::for_rootfs(&cli.rootfs);
|
let config = config::Config::for_rootfs(&cli.rootfs);
|
||||||
let db_path = config.db_dir.join("packages.db");
|
let db_path = config.db_dir.join("packages.db");
|
||||||
|
let script_dir = install::scripts::installed_scripts_dir(&cli.rootfs, &package);
|
||||||
|
let _ = install::scripts::run_hook_if_present(
|
||||||
|
&script_dir,
|
||||||
|
install::scripts::Hook::PreRemove,
|
||||||
|
&cli.rootfs,
|
||||||
|
&package,
|
||||||
|
)?;
|
||||||
db::remove_package(&db_path, &package, &cli.rootfs)?;
|
db::remove_package(&db_path, &package, &cli.rootfs)?;
|
||||||
|
let post_remove = install::scripts::run_hook_if_present(
|
||||||
|
&script_dir,
|
||||||
|
install::scripts::Hook::PostRemove,
|
||||||
|
&cli.rootfs,
|
||||||
|
&package,
|
||||||
|
);
|
||||||
|
let cleanup_scripts = install::scripts::remove_installed_scripts(&cli.rootfs, &package);
|
||||||
|
post_remove?;
|
||||||
|
cleanup_scripts?;
|
||||||
println!("Successfully removed {}", package);
|
println!("Successfully removed {}", package);
|
||||||
}
|
}
|
||||||
Commands::Build {
|
Commands::Build {
|
||||||
@@ -610,6 +674,7 @@ fn main() -> Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
|
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::process(&destdir, &pkg_spec)?;
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ fn is_skipped_install_path(rel_path: &str) -> bool {
|
|||||||
let p = rel_path.trim_start_matches('/');
|
let p = rel_path.trim_start_matches('/');
|
||||||
p == ".metadata.toml"
|
p == ".metadata.toml"
|
||||||
|| p == ".files.yaml"
|
|| p == ".files.yaml"
|
||||||
|
|| p == "scripts"
|
||||||
|
|| p.starts_with("scripts/")
|
||||||
|| p == "usr/share/info/dir"
|
|| p == "usr/share/info/dir"
|
||||||
|| p.starts_with("usr/share/info/dir.")
|
|| p.starts_with("usr/share/info/dir.")
|
||||||
}
|
}
|
||||||
@@ -651,6 +653,24 @@ mod tests {
|
|||||||
assert!(rootfs.join("usr/bin/ok").exists());
|
assert!(rootfs.join("usr/bin/ok").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_atomic_skips_package_scripts_dir() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
let tx_base = tmp.path().join("tx");
|
||||||
|
std::fs::create_dir_all(&rootfs).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("scripts")).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||||
|
std::fs::write(destdir.join("scripts/pre_install"), "#!/bin/sh\necho pre\n").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/bin/ok"), "ok").unwrap();
|
||||||
|
|
||||||
|
let _tx = install_atomic(&destdir, &rootfs, &tx_base, &[], &[]).unwrap();
|
||||||
|
|
||||||
|
assert!(!rootfs.join("scripts/pre_install").exists());
|
||||||
|
assert!(rootfs.join("usr/bin/ok").exists());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_manifest_skips_info_dir_index() {
|
fn generate_manifest_skips_info_dir_index() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
@@ -691,6 +711,21 @@ mod tests {
|
|||||||
assert!(!manifest.files.contains(&".files.yaml".to_string()));
|
assert!(!manifest.files.contains(&".files.yaml".to_string()));
|
||||||
assert!(manifest.files.contains(&"usr/bin/ok".to_string()));
|
assert!(manifest.files.contains(&"usr/bin/ok".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_manifest_skips_package_scripts_dir() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
std::fs::create_dir_all(destdir.join("scripts")).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||||
|
std::fs::write(destdir.join("scripts/pre_install"), "echo pre").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/bin/ok"), "ok").unwrap();
|
||||||
|
|
||||||
|
let manifest = generate_manifest_with_dirs(&destdir).unwrap();
|
||||||
|
|
||||||
|
assert!(!manifest.files.contains(&"scripts/pre_install".to_string()));
|
||||||
|
assert!(manifest.files.contains(&"usr/bin/ok".to_string()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manifest containing files and directories for a package
|
/// Manifest containing files and directories for a package
|
||||||
|
|||||||
Reference in New Issue
Block a user