refactor command flow and add signed package/hook safety
- split CLI definitions and command execution out of main.rs into new cli.rs and commands.rs modules\n- add transaction hook support via /etc/depot.d/hooks/*.toml with pre/post phases, operation/package/path filters, wildcard negation, and optional affected-path stdin\n- execute transaction hooks around install/update/remove operations and add package-action prompts for install/build/remove flows\n- require and verify detached minisign signatures for binary package archives when allow_unsigned=false, including cache handling and tests\n- persist optional dependencies from package metadata into repo index data and include optional deps in produced metadata\n- update dependency/planner logic to ignore dependencies provided by local split outputs and stop pulling test deps into install planning\n- auto-disable tests when required test dependencies are missing instead of hard-failing build/install paths\n- improve autotools defaults by injecting standard install directory flags when supported and preserving user overrides\n- remove static .a archives during staging by default with new build.flags.no_delete_static escape hatch\n- preserve symlinks and file metadata (permissions/timestamps) in extractor fallback copy path and add regression tests\n- refresh README docs for detached package signatures, optional dependencies, and transaction hooks\n- adjust dependency set: add filetime, disable reqwest default features, and simplify git2/OpenSSL linkage
This commit is contained in:
Generated
+115
-689
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -12,12 +12,10 @@ ar = "0.9.0"
|
|||||||
bzip2 = "0.6.1"
|
bzip2 = "0.6.1"
|
||||||
clap = { version = "4.5.60", features = ["derive"] }
|
clap = { version = "4.5.60", features = ["derive"] }
|
||||||
flate2 = "1.1.9"
|
flate2 = "1.1.9"
|
||||||
git2 = { version = "0.20.4", features = ["openssl-sys"] }
|
git2 = "0.20.4"
|
||||||
indicatif = "0.18.4"
|
indicatif = "0.18.4"
|
||||||
nix = { version = "0.31.1", features = ["user"] }
|
nix = { version = "0.31.2", features = ["user"] }
|
||||||
reqwest = { version = "0.13.2", features = ["blocking", "native-tls"] }
|
|
||||||
rusqlite = "0.38.0"
|
rusqlite = "0.38.0"
|
||||||
openssl-sys = "0.9.111"
|
|
||||||
semver = "1.0.27"
|
semver = "1.0.27"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
yaml-rust2 = "0.11.0"
|
yaml-rust2 = "0.11.0"
|
||||||
@@ -31,13 +29,14 @@ walkdir = "2.5.0"
|
|||||||
xz2 = "0.1.7"
|
xz2 = "0.1.7"
|
||||||
zip = "8.1.0"
|
zip = "8.1.0"
|
||||||
zstd = { version = "0.13.3", features = ["zstdmt"] }
|
zstd = { version = "0.13.3", features = ["zstdmt"] }
|
||||||
libz-sys = "1.1.24"
|
|
||||||
inquire = "0.9.4"
|
inquire = "0.9.4"
|
||||||
md5 = "0.8.0"
|
md5 = "0.8.0"
|
||||||
suppaftp = "8.0.2"
|
suppaftp = "8.0.2"
|
||||||
minisign = "0.8.0"
|
minisign = "0.8.0"
|
||||||
petgraph = "0.8.3"
|
petgraph = "0.8.3"
|
||||||
fd-lock = "4.0.4"
|
fd-lock = "4.0.4"
|
||||||
|
reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "native-tls"] }
|
||||||
|
filetime = "0.2.27"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "=3.25.0"
|
tempfile = "=3.25.0"
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ depot install zlib-1.2.11-1-x86_64.depot.pkg.tar.zst
|
|||||||
- Resolves a full dependency plan first (binary repos and/or source specs), then executes in dependency order.
|
- Resolves a full dependency plan first (binary repos and/or source specs), then executes in dependency order.
|
||||||
- Use `--yes` for non-interactive confirmation/provider selection.
|
- Use `--yes` for non-interactive confirmation/provider selection.
|
||||||
- Use `--dry-run` to print the plan without performing work.
|
- Use `--dry-run` to print the plan without performing work.
|
||||||
|
- Binary package installs verify both checksums and detached minisign signatures (`.sig`).
|
||||||
- `remove <PACKAGE>`: Remove an installed package.
|
- `remove <PACKAGE>`: Remove an installed package.
|
||||||
- `build <SPEC>`: Build a package and create an archive without installing.
|
- `build <SPEC>`: Build a package and create an archive without installing.
|
||||||
- Resolves and offers to install missing build/test dependencies before fetching/building.
|
- Resolves and offers to install missing build dependencies before fetching/building.
|
||||||
|
- Missing test dependencies automatically disable test execution for that build.
|
||||||
- `info <PACKAGE_OR_SPEC>`: Show information about a package.
|
- `info <PACKAGE_OR_SPEC>`: Show information about a package.
|
||||||
- `search <QUERY>`: Search enabled source/binary repos by package name and provided features.
|
- `search <QUERY>`: Search enabled source/binary repos by package name and provided features.
|
||||||
- Use `--files` to search binary repo metadata file lists.
|
- Use `--files` to search binary repo metadata file lists.
|
||||||
@@ -75,6 +77,7 @@ flags = { configure = ["--enable-feature"] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
build = ["gcc", "make"]
|
build = ["gcc", "make"]
|
||||||
runtime = ["libc"]
|
runtime = ["libc"]
|
||||||
|
optional = ["bash-completion"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -83,3 +86,26 @@ Depot can be configured via `/etc/depot.d/` (or relative to the rootfs).
|
|||||||
|
|
||||||
- `/etc/depot.d/build.toml`: System-wide build overrides and flag appends.
|
- `/etc/depot.d/build.toml`: System-wide build overrides and flag appends.
|
||||||
- `/etc/depot.d/package.toml`: Package-specific overrides.
|
- `/etc/depot.d/package.toml`: Package-specific overrides.
|
||||||
|
- `/etc/depot.d/hooks/*.toml`: Transaction hooks for `install`/`update`/`remove` pre/post phases.
|
||||||
|
|
||||||
|
### Transaction Hooks
|
||||||
|
|
||||||
|
Hook files are TOML and use Starpack-like sections:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[hook]
|
||||||
|
name = "refresh-cache"
|
||||||
|
|
||||||
|
[when]
|
||||||
|
phase = "post"
|
||||||
|
operation = ["install", "update"]
|
||||||
|
packages = ["glibc", "linux*"]
|
||||||
|
paths = ["usr/lib/*"]
|
||||||
|
negation = ["usr/lib/debug/*"]
|
||||||
|
|
||||||
|
[exec]
|
||||||
|
command = "ldconfig"
|
||||||
|
needs_paths = true
|
||||||
|
```
|
||||||
|
|
||||||
|
`needs_paths = true` passes affected paths to the command via stdin (newline-separated).
|
||||||
|
|||||||
+106
-11
@@ -65,11 +65,14 @@ pub fn build(
|
|||||||
|
|
||||||
crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars);
|
crate::builder::prepare_tool_command(&mut configure_cmd, &env_vars);
|
||||||
|
|
||||||
configure_cmd.arg(format!("--prefix={}", flags.prefix));
|
|
||||||
|
|
||||||
// Some projects use non-GNU configure scripts that reject --host/--build.
|
// Some projects use non-GNU configure scripts that reject --host/--build.
|
||||||
// Probe support first and only add these options when advertised.
|
// Probe support first and only add these options when advertised.
|
||||||
let help_text = configure_help_text(&configure_path, &build_dir, &env_vars);
|
let help_text = configure_help_text(&configure_path, &build_dir, &env_vars);
|
||||||
|
configure_cmd.arg(format!("--prefix={}", flags.prefix));
|
||||||
|
for default_dir_arg in default_configure_install_dirs(flags, help_text.as_deref()) {
|
||||||
|
configure_cmd.arg(default_dir_arg);
|
||||||
|
}
|
||||||
|
|
||||||
let supports_host =
|
let supports_host =
|
||||||
configure_supports_option(help_text.as_deref(), "--host", &flags.configure_file);
|
configure_supports_option(help_text.as_deref(), "--host", &flags.configure_file);
|
||||||
let supports_build =
|
let supports_build =
|
||||||
@@ -114,15 +117,6 @@ pub fn build(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if flags.lib32_variant {
|
|
||||||
if !has_configure_option_prefix(&flags.configure, "--libdir") {
|
|
||||||
configure_cmd.arg("--libdir=/usr/lib32");
|
|
||||||
}
|
|
||||||
if !has_configure_option_prefix(&flags.configure, "--libexecdir") {
|
|
||||||
configure_cmd.arg("--libexecdir=/usr/lib32");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for arg in &flags.configure {
|
for arg in &flags.configure {
|
||||||
let expanded = expand_configure_arg(spec, arg, &env_vars);
|
let expanded = expand_configure_arg(spec, arg, &env_vars);
|
||||||
configure_cmd.arg(expanded);
|
configure_cmd.arg(expanded);
|
||||||
@@ -439,6 +433,41 @@ fn has_configure_option_prefix(args: &[String], option: &str) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_configure_install_dirs(
|
||||||
|
flags: &crate::package::BuildFlags,
|
||||||
|
help_text: Option<&str>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let libdir = if flags.lib32_variant {
|
||||||
|
"/usr/lib32"
|
||||||
|
} else {
|
||||||
|
"/usr/lib"
|
||||||
|
};
|
||||||
|
let bindir = nonempty_trimmed(&flags.bindir).unwrap_or("/usr/bin");
|
||||||
|
let defaults = [
|
||||||
|
("--bindir", bindir),
|
||||||
|
("--sbindir", "/usr/bin"),
|
||||||
|
("--libdir", libdir),
|
||||||
|
("--libexecdir", libdir),
|
||||||
|
("--sysconfdir", "/etc"),
|
||||||
|
("--localstatedir", "/var"),
|
||||||
|
("--sharedstatedir", "/var/lib"),
|
||||||
|
("--includedir", "/usr/include"),
|
||||||
|
("--datarootdir", "/usr/share"),
|
||||||
|
("--datadir", "/usr/share"),
|
||||||
|
("--mandir", "/usr/share/man"),
|
||||||
|
("--infodir", "/usr/share/info"),
|
||||||
|
];
|
||||||
|
|
||||||
|
defaults
|
||||||
|
.iter()
|
||||||
|
.filter(|(option, _)| {
|
||||||
|
help_text.is_some_and(|text| configure_help_supports_option(text, option))
|
||||||
|
})
|
||||||
|
.filter(|(option, _)| !has_configure_option_prefix(&flags.configure, option))
|
||||||
|
.map(|(option, value)| format!("{option}={value}"))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn lib32_host_triple(host: &str) -> String {
|
fn lib32_host_triple(host: &str) -> String {
|
||||||
host.replace("x86_64", "i686")
|
host.replace("x86_64", "i686")
|
||||||
}
|
}
|
||||||
@@ -803,6 +832,72 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_configure_install_dirs_injects_expected_paths() {
|
||||||
|
let flags = BuildFlags::default();
|
||||||
|
let help = "\
|
||||||
|
--bindir=DIR
|
||||||
|
--sbindir=DIR
|
||||||
|
--libdir=DIR
|
||||||
|
--libexecdir=DIR
|
||||||
|
--sysconfdir=DIR
|
||||||
|
--localstatedir=DIR
|
||||||
|
--sharedstatedir=DIR
|
||||||
|
--includedir=DIR
|
||||||
|
--datarootdir=DIR
|
||||||
|
--datadir=DIR
|
||||||
|
--mandir=DIR
|
||||||
|
--infodir=DIR";
|
||||||
|
let args = default_configure_install_dirs(&flags, Some(help));
|
||||||
|
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
|
||||||
|
assert!(args.iter().any(|a| a == "--sbindir=/usr/bin"));
|
||||||
|
assert!(args.iter().any(|a| a == "--libdir=/usr/lib"));
|
||||||
|
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib"));
|
||||||
|
assert!(args.iter().any(|a| a == "--sysconfdir=/etc"));
|
||||||
|
assert!(args.iter().any(|a| a == "--localstatedir=/var"));
|
||||||
|
assert!(args.iter().any(|a| a == "--sharedstatedir=/var/lib"));
|
||||||
|
assert!(args.iter().any(|a| a == "--includedir=/usr/include"));
|
||||||
|
assert!(args.iter().any(|a| a == "--datarootdir=/usr/share"));
|
||||||
|
assert!(args.iter().any(|a| a == "--datadir=/usr/share"));
|
||||||
|
assert!(args.iter().any(|a| a == "--mandir=/usr/share/man"));
|
||||||
|
assert!(args.iter().any(|a| a == "--infodir=/usr/share/info"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_configure_install_dirs_respects_explicit_user_overrides() {
|
||||||
|
let mut flags = BuildFlags::default();
|
||||||
|
flags.configure = vec![
|
||||||
|
"--sbindir=/sbin".to_string(),
|
||||||
|
"--libdir=/custom/lib".to_string(),
|
||||||
|
"--datadir=/custom/share".to_string(),
|
||||||
|
];
|
||||||
|
let help = "--bindir=DIR --sbindir=DIR --libdir=DIR --datadir=DIR";
|
||||||
|
let args = default_configure_install_dirs(&flags, Some(help));
|
||||||
|
assert!(!args.iter().any(|a| a.starts_with("--sbindir=")));
|
||||||
|
assert!(!args.iter().any(|a| a.starts_with("--libdir=")));
|
||||||
|
assert!(!args.iter().any(|a| a.starts_with("--datadir=")));
|
||||||
|
assert!(args.iter().any(|a| a == "--bindir=/usr/bin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_configure_install_dirs_lib32_uses_lib32_dirs() {
|
||||||
|
let help = "--libdir=DIR --libexecdir=DIR";
|
||||||
|
let flags = BuildFlags {
|
||||||
|
lib32_variant: true,
|
||||||
|
..BuildFlags::default()
|
||||||
|
};
|
||||||
|
let args = default_configure_install_dirs(&flags, Some(help));
|
||||||
|
assert!(args.iter().any(|a| a == "--libdir=/usr/lib32"));
|
||||||
|
assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib32"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_configure_install_dirs_skips_when_not_advertised() {
|
||||||
|
let flags = BuildFlags::default();
|
||||||
|
let args = default_configure_install_dirs(&flags, Some("--prefix=PREFIX"));
|
||||||
|
assert!(args.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_makefile_content_has_target_detects_check_and_test() {
|
fn test_makefile_content_has_target_detects_check_and_test() {
|
||||||
let content = r#"
|
let content = r#"
|
||||||
|
|||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "Depot")]
|
||||||
|
#[command(about = "Depot - Source-based package manager for Linux", long_about = None)]
|
||||||
|
#[command(version)]
|
||||||
|
pub struct Cli {
|
||||||
|
/// Custom root filesystem path
|
||||||
|
#[arg(long, short = 'r', default_value = "/", global = true)]
|
||||||
|
pub rootfs: PathBuf,
|
||||||
|
|
||||||
|
/// Skip dependency checks
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub no_deps: bool,
|
||||||
|
|
||||||
|
/// Do not export CFLAGS/CXXFLAGS/LDFLAGS to build commands
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
action = clap::ArgAction::Set,
|
||||||
|
num_args = 0..=1,
|
||||||
|
default_value_t = false,
|
||||||
|
default_missing_value = "true"
|
||||||
|
)]
|
||||||
|
pub no_flags: bool,
|
||||||
|
|
||||||
|
/// Cross-compilation prefix (e.g., x86_64-linux-musl, aarch64-linux-gnu)
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub cross_prefix: Option<String>,
|
||||||
|
|
||||||
|
/// Clean build workspace after successful install/build
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub clean: bool,
|
||||||
|
|
||||||
|
/// Automatically answer yes to prompts and pick the default provider choice
|
||||||
|
#[arg(long, short = 'y', global = true)]
|
||||||
|
pub yes: bool,
|
||||||
|
|
||||||
|
/// Show what would happen without performing builds/installs
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub dry_run: bool,
|
||||||
|
|
||||||
|
/// Build/install only the lib32 companion package path (skip primary package output)
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub lib32_only: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Build and install a package from a spec file
|
||||||
|
Install {
|
||||||
|
/// Path to package spec (.toml) or package archive (.tar.zst)
|
||||||
|
#[arg(value_name = "SPEC_OR_ARCHIVE")]
|
||||||
|
spec_or_archive: PathBuf,
|
||||||
|
|
||||||
|
/// Explicitly specify path to package spec (.toml file)
|
||||||
|
#[arg(short, long = "spec", visible_alias = "package", alias = "p")]
|
||||||
|
spec: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Remove an installed package
|
||||||
|
Remove {
|
||||||
|
/// Package name to remove
|
||||||
|
package: String,
|
||||||
|
},
|
||||||
|
/// Build a package without installing
|
||||||
|
Build {
|
||||||
|
/// Path to package spec (.toml file)
|
||||||
|
#[arg(value_name = "SPEC")]
|
||||||
|
spec_pos: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Explicitly specify path to package spec (.toml file)
|
||||||
|
#[arg(short, long = "spec", visible_alias = "package", alias = "p")]
|
||||||
|
spec: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Install package to rootfs after creating package archive(s)
|
||||||
|
#[arg(long)]
|
||||||
|
install: bool,
|
||||||
|
},
|
||||||
|
/// Show information about a package
|
||||||
|
Info {
|
||||||
|
/// Path to package spec or installed package name
|
||||||
|
package: String,
|
||||||
|
},
|
||||||
|
/// Search configured source and binary repos by package name or provides
|
||||||
|
Search {
|
||||||
|
/// Search query
|
||||||
|
query: String,
|
||||||
|
/// Search repository file lists (binary repo metadata) by path substring
|
||||||
|
#[arg(long)]
|
||||||
|
files: bool,
|
||||||
|
},
|
||||||
|
/// Show which installed package owns a filesystem path
|
||||||
|
Owns {
|
||||||
|
/// Path to query (absolute or relative to rootfs)
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
/// List installed packages
|
||||||
|
List,
|
||||||
|
/// Create a detached minisign signature for a .zst file
|
||||||
|
Sign {
|
||||||
|
/// Path to the .zst file to sign
|
||||||
|
file: PathBuf,
|
||||||
|
},
|
||||||
|
/// Repository management
|
||||||
|
Repo {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: RepoCommands,
|
||||||
|
},
|
||||||
|
/// Show current configuration
|
||||||
|
Config,
|
||||||
|
/// Create a new package specification interactively
|
||||||
|
MakeSpec {
|
||||||
|
/// Output file path (defaults to <name>.toml)
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum RepoCommands {
|
||||||
|
/// Create a repository database from a directory of packages
|
||||||
|
Create {
|
||||||
|
/// Directory containing .depot.pkg.tar.zst files
|
||||||
|
#[arg(default_value = ".")]
|
||||||
|
dir: PathBuf,
|
||||||
|
},
|
||||||
|
/// Sync git mirrors configured in /etc/depot.d/mirrors.toml into /usr/src/depot
|
||||||
|
Sync,
|
||||||
|
/// Sync source repos configured in /etc/depot.d/repos.toml into /usr/src/depot
|
||||||
|
Update {
|
||||||
|
/// Update only one source repo by name
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
|
/// List configured source and binary repos
|
||||||
|
List,
|
||||||
|
/// Add or update a repo entry in /etc/depot.d/repos.toml
|
||||||
|
Add {
|
||||||
|
/// Repo name (e.g. vertex)
|
||||||
|
name: String,
|
||||||
|
/// Source git URL or binary repo base URL
|
||||||
|
url: String,
|
||||||
|
/// Repo kind
|
||||||
|
#[arg(long, value_enum, default_value_t = RepoKindArg::Source)]
|
||||||
|
kind: RepoKindArg,
|
||||||
|
/// Optional source repo subdirectory to index (repeatable)
|
||||||
|
#[arg(long = "subdir")]
|
||||||
|
subdirs: Vec<String>,
|
||||||
|
/// Repo priority (lower = higher priority)
|
||||||
|
#[arg(long, default_value_t = 0)]
|
||||||
|
priority: i32,
|
||||||
|
/// Add repo as disabled
|
||||||
|
#[arg(long)]
|
||||||
|
disabled: bool,
|
||||||
|
/// Binary repo architecture table entry to add/update (defaults to this machine's arch)
|
||||||
|
#[arg(long)]
|
||||||
|
arch: Option<String>,
|
||||||
|
/// Binary repo DB filename/path (relative to repo URL)
|
||||||
|
#[arg(long = "repo-db", default_value = "repo.db.zst")]
|
||||||
|
repo_db: String,
|
||||||
|
/// Allow unsigned repo metadata for this binary repo
|
||||||
|
#[arg(long)]
|
||||||
|
allow_unsigned: bool,
|
||||||
|
},
|
||||||
|
/// Remove a repo entry from /etc/depot.d/repos.toml
|
||||||
|
Remove {
|
||||||
|
/// Repo name
|
||||||
|
name: String,
|
||||||
|
/// Repo kind (auto-detect if unique)
|
||||||
|
#[arg(long)]
|
||||||
|
kind: Option<RepoKindArg>,
|
||||||
|
},
|
||||||
|
/// Enable a repo entry in /etc/depot.d/repos.toml
|
||||||
|
Enable {
|
||||||
|
/// Repo name
|
||||||
|
name: String,
|
||||||
|
/// Repo kind (auto-detect if unique)
|
||||||
|
#[arg(long)]
|
||||||
|
kind: Option<RepoKindArg>,
|
||||||
|
},
|
||||||
|
/// Disable a repo entry in /etc/depot.d/repos.toml
|
||||||
|
Disable {
|
||||||
|
/// Repo name
|
||||||
|
name: String,
|
||||||
|
/// Repo kind (auto-detect if unique)
|
||||||
|
#[arg(long)]
|
||||||
|
kind: Option<RepoKindArg>,
|
||||||
|
},
|
||||||
|
/// Query binary repo metadata for the package that owns a file path
|
||||||
|
Owns {
|
||||||
|
/// Path to query (absolute or relative install path)
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
/// Show status of configured git mirrors
|
||||||
|
Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
|
||||||
|
pub enum RepoKindArg {
|
||||||
|
Source,
|
||||||
|
Binary,
|
||||||
|
}
|
||||||
+2003
File diff suppressed because it is too large
Load Diff
+280
-7
@@ -170,6 +170,7 @@ impl RepoManager {
|
|||||||
let mut license = None;
|
let mut license = None;
|
||||||
let mut provides = Vec::new();
|
let mut provides = Vec::new();
|
||||||
let mut runtime_dependencies = Vec::new();
|
let mut runtime_dependencies = Vec::new();
|
||||||
|
let mut optional_dependencies = Vec::new();
|
||||||
let mut archive_files = Vec::new();
|
let mut archive_files = Vec::new();
|
||||||
|
|
||||||
for entry in archive.entries()? {
|
for entry in archive.entries()? {
|
||||||
@@ -226,6 +227,17 @@ impl RepoManager {
|
|||||||
.map(String::from)
|
.map(String::from)
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
if let Some(optional_arr) = metadata
|
||||||
|
.get("dependencies")
|
||||||
|
.and_then(|v| v.get("optional"))
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
{
|
||||||
|
optional_dependencies = optional_arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +298,12 @@ impl RepoManager {
|
|||||||
params![package_id, dep],
|
params![package_id, dep],
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
for dep in optional_dependencies {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO dependencies (package_id, kind, name) VALUES (?1, 'optional', ?2)",
|
||||||
|
params![package_id, dep],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
for file_path in archive_files {
|
for file_path in archive_files {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -1304,11 +1322,84 @@ fn verify_binary_package_record_checksums(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download a binary package archive and verify it against checksums from the
|
fn fetch_binary_package_signature(
|
||||||
/// signed repository database metadata.
|
repo_name: &str,
|
||||||
|
repo: &crate::config::BinaryRepo,
|
||||||
|
client: &reqwest::blocking::Client,
|
||||||
|
sig_url: &str,
|
||||||
|
sig_path: &Path,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let found = fetch_url_to_path(client, sig_url, sig_path)?;
|
||||||
|
if !found {
|
||||||
|
if !repo.allow_unsigned {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to fetch detached signature for binary package in repo '{}' at {}",
|
||||||
|
repo_name,
|
||||||
|
sig_url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
crate::log_warn!(
|
||||||
|
"Detached package signature missing for binary repo '{}' at {}; allow_unsigned=true so continuing",
|
||||||
|
repo_name,
|
||||||
|
sig_url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_binary_package_signature(
|
||||||
|
repo_name: &str,
|
||||||
|
repo: &crate::config::BinaryRepo,
|
||||||
|
rootfs: &Path,
|
||||||
|
pkg_path: &Path,
|
||||||
|
sig_path: &Path,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !sig_path.exists() {
|
||||||
|
if repo.allow_unsigned {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
anyhow::bail!(
|
||||||
|
"Detached package signature required but missing for {}",
|
||||||
|
pkg_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let trusted_keys = crate::signing::list_trusted_public_keys(rootfs)?;
|
||||||
|
if trusted_keys.is_empty() {
|
||||||
|
if repo.allow_unsigned {
|
||||||
|
crate::log_warn!(
|
||||||
|
"No trusted minisign public key found; skipping package signature verification for binary repo '{}' because allow_unsigned=true",
|
||||||
|
repo_name
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
anyhow::bail!(
|
||||||
|
"No trusted minisign public key found for detached package signature verification in binary repo '{}'",
|
||||||
|
repo_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let verified_key = verify_with_any_trusted_public_key(rootfs, pkg_path, sig_path)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to verify detached package signature for {}",
|
||||||
|
pkg_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
crate::log_info!(
|
||||||
|
"Verified detached package signature for '{}' using {}",
|
||||||
|
pkg_path.display(),
|
||||||
|
verified_key.display()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download a binary package archive and verify it against detached signatures
|
||||||
|
/// and checksums from signed repository metadata.
|
||||||
pub fn fetch_binary_package_archive(
|
pub fn fetch_binary_package_archive(
|
||||||
repo_name: &str,
|
repo_name: &str,
|
||||||
repo: &crate::config::BinaryRepo,
|
repo: &crate::config::BinaryRepo,
|
||||||
|
rootfs: &Path,
|
||||||
rec: &BinaryRepoPackageRecord,
|
rec: &BinaryRepoPackageRecord,
|
||||||
package_cache_dir: &Path,
|
package_cache_dir: &Path,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<PathBuf> {
|
||||||
@@ -1324,7 +1415,15 @@ pub fn fetch_binary_package_archive(
|
|||||||
fs::create_dir_all(&cache_dir)
|
fs::create_dir_all(&cache_dir)
|
||||||
.with_context(|| format!("Failed to create {}", cache_dir.display()))?;
|
.with_context(|| format!("Failed to create {}", cache_dir.display()))?;
|
||||||
let dest_path = cache_dir.join(&rec.filename);
|
let dest_path = cache_dir.join(&rec.filename);
|
||||||
|
let dest_sig_path = cache_dir.join(format!("{}.sig", rec.filename));
|
||||||
let tmp_path = cache_dir.join(format!("{}.tmp", rec.filename));
|
let tmp_path = cache_dir.join(format!("{}.tmp", rec.filename));
|
||||||
|
let tmp_sig_path = cache_dir.join(format!("{}.sig.tmp", rec.filename));
|
||||||
|
let pkg_url = join_repo_url(base_url, &rec.filename)?;
|
||||||
|
let sig_url = join_repo_url(base_url, &format!("{}.sig", rec.filename))?;
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::builder()
|
||||||
|
.build()
|
||||||
|
.context("Failed to build HTTP client for binary package fetch")?;
|
||||||
|
|
||||||
if dest_path.exists() {
|
if dest_path.exists() {
|
||||||
verify_binary_package_record_checksums(&dest_path, rec).with_context(|| {
|
verify_binary_package_record_checksums(&dest_path, rec).with_context(|| {
|
||||||
@@ -1333,11 +1432,32 @@ pub fn fetch_binary_package_archive(
|
|||||||
dest_path.display()
|
dest_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
if !dest_sig_path.exists() {
|
||||||
|
let sig_downloaded =
|
||||||
|
fetch_binary_package_signature(repo_name, repo, &client, &sig_url, &tmp_sig_path)?;
|
||||||
|
if sig_downloaded {
|
||||||
|
fs::rename(&tmp_sig_path, &dest_sig_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to move {} to {}",
|
||||||
|
tmp_sig_path.display(),
|
||||||
|
dest_sig_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
let _ = fs::remove_file(&dest_sig_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verify_binary_package_signature(repo_name, repo, rootfs, &dest_path, &dest_sig_path)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Cached binary package failed signature verification: {}",
|
||||||
|
dest_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
crate::log_info!("Using cached binary package: {}", dest_path.display());
|
crate::log_info!("Using cached binary package: {}", dest_path.display());
|
||||||
return Ok(dest_path);
|
return Ok(dest_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pkg_url = join_repo_url(base_url, &rec.filename)?;
|
|
||||||
crate::log_info!("Fetching binary package: {}", pkg_url);
|
crate::log_info!("Fetching binary package: {}", pkg_url);
|
||||||
|
|
||||||
match copy_file_url_to_path(&pkg_url, &tmp_path)? {
|
match copy_file_url_to_path(&pkg_url, &tmp_path)? {
|
||||||
@@ -1346,9 +1466,6 @@ pub fn fetch_binary_package_archive(
|
|||||||
anyhow::bail!("Failed to fetch {}: local file not found", pkg_url);
|
anyhow::bail!("Failed to fetch {}: local file not found", pkg_url);
|
||||||
}
|
}
|
||||||
FileUrlCopyOutcome::NotFileUrl => {
|
FileUrlCopyOutcome::NotFileUrl => {
|
||||||
let client = reqwest::blocking::Client::builder()
|
|
||||||
.build()
|
|
||||||
.context("Failed to build HTTP client for binary package fetch")?;
|
|
||||||
let mut resp = client
|
let mut resp = client
|
||||||
.get(&pkg_url)
|
.get(&pkg_url)
|
||||||
.send()
|
.send()
|
||||||
@@ -1373,6 +1490,9 @@ pub fn fetch_binary_package_archive(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let sig_downloaded =
|
||||||
|
fetch_binary_package_signature(repo_name, repo, &client, &sig_url, &tmp_sig_path)?;
|
||||||
|
|
||||||
fs::rename(&tmp_path, &dest_path).with_context(|| {
|
fs::rename(&tmp_path, &dest_path).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to move {} to {}",
|
"Failed to move {} to {}",
|
||||||
@@ -1380,6 +1500,29 @@ pub fn fetch_binary_package_archive(
|
|||||||
dest_path.display()
|
dest_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
if sig_downloaded {
|
||||||
|
fs::rename(&tmp_sig_path, &dest_sig_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to move {} to {}",
|
||||||
|
tmp_sig_path.display(),
|
||||||
|
dest_sig_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if let Err(err) =
|
||||||
|
verify_binary_package_signature(repo_name, repo, rootfs, &dest_path, &dest_sig_path)
|
||||||
|
{
|
||||||
|
let _ = fs::remove_file(&dest_path);
|
||||||
|
let _ = fs::remove_file(&dest_sig_path);
|
||||||
|
return Err(err).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Downloaded binary package failed signature verification: {}",
|
||||||
|
rec.filename
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = fs::remove_file(&dest_sig_path);
|
||||||
|
}
|
||||||
Ok(dest_path)
|
Ok(dest_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1599,8 +1742,8 @@ license = "MIT"
|
|||||||
provides = ["test-feature"]
|
provides = ["test-feature"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
build = []
|
|
||||||
runtime = []
|
runtime = []
|
||||||
|
optional = []
|
||||||
"#;
|
"#;
|
||||||
let mut header = tar::Header::new_gnu();
|
let mut header = tar::Header::new_gnu();
|
||||||
header.set_path(".metadata.toml").unwrap();
|
header.set_path(".metadata.toml").unwrap();
|
||||||
@@ -1798,6 +1941,136 @@ license = ["MIT", "Apache-2.0"]
|
|||||||
verify_binary_package_record_checksums(&pkg, &rec).unwrap();
|
verify_binary_package_record_checksums(&pkg, &rec).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_record_for_payload(filename: &str, payload: &[u8]) -> BinaryRepoPackageRecord {
|
||||||
|
use sha2::{Digest, Sha256, Sha512};
|
||||||
|
|
||||||
|
let sha256 = {
|
||||||
|
let mut h = Sha256::new();
|
||||||
|
h.update(payload);
|
||||||
|
format!("{:x}", h.finalize())
|
||||||
|
};
|
||||||
|
let sha512 = {
|
||||||
|
let mut h = Sha512::new();
|
||||||
|
h.update(payload);
|
||||||
|
format!("{:x}", h.finalize())
|
||||||
|
};
|
||||||
|
|
||||||
|
BinaryRepoPackageRecord {
|
||||||
|
repo_name: "repo".into(),
|
||||||
|
name: "pkg".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
filename: filename.to_string(),
|
||||||
|
size: payload.len() as u64,
|
||||||
|
sha256,
|
||||||
|
sha512,
|
||||||
|
description: None,
|
||||||
|
provides: Vec::new(),
|
||||||
|
runtime_dependencies: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fetch_binary_package_archive_requires_signature_when_unsigned_disallowed() {
|
||||||
|
let rootfs = tempfile::tempdir().unwrap();
|
||||||
|
let repo_dir = tempfile::tempdir().unwrap();
|
||||||
|
let cache_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst";
|
||||||
|
let payload = b"package payload";
|
||||||
|
std::fs::write(repo_dir.path().join(filename), payload).unwrap();
|
||||||
|
|
||||||
|
let rec = test_record_for_payload(filename, payload);
|
||||||
|
let repo_url = url::Url::from_directory_path(repo_dir.path())
|
||||||
|
.expect("file URL")
|
||||||
|
.to_string();
|
||||||
|
let repo_cfg = crate::config::BinaryRepo {
|
||||||
|
url: repo_url,
|
||||||
|
allow_unsigned: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let err =
|
||||||
|
fetch_binary_package_archive("repo", &repo_cfg, rootfs.path(), &rec, cache_dir.path())
|
||||||
|
.expect_err("missing detached signature should fail");
|
||||||
|
assert!(err.to_string().to_ascii_lowercase().contains("signature"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fetch_binary_package_archive_verifies_signature_and_checksum() {
|
||||||
|
let rootfs = tempfile::tempdir().unwrap();
|
||||||
|
let repo_dir = tempfile::tempdir().unwrap();
|
||||||
|
let cache_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let trusted_dir = crate::signing::trusted_public_keys_dir(rootfs.path());
|
||||||
|
std::fs::create_dir_all(&trusted_dir).unwrap();
|
||||||
|
|
||||||
|
let keypair = minisign::KeyPair::generate_unencrypted_keypair().unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
trusted_dir.join("repo.pub"),
|
||||||
|
keypair.pk.to_box().unwrap().to_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst";
|
||||||
|
let payload = b"signed package payload";
|
||||||
|
let package_path = repo_dir.path().join(filename);
|
||||||
|
std::fs::write(&package_path, payload).unwrap();
|
||||||
|
|
||||||
|
let sig = minisign::sign(
|
||||||
|
Some(&keypair.pk),
|
||||||
|
&keypair.sk,
|
||||||
|
std::fs::File::open(&package_path).unwrap(),
|
||||||
|
None,
|
||||||
|
Some("test signature"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(format!("{}.sig", package_path.display()), sig.to_bytes()).unwrap();
|
||||||
|
|
||||||
|
let rec = test_record_for_payload(filename, payload);
|
||||||
|
let repo_url = url::Url::from_directory_path(repo_dir.path())
|
||||||
|
.expect("file URL")
|
||||||
|
.to_string();
|
||||||
|
let repo_cfg = crate::config::BinaryRepo {
|
||||||
|
url: repo_url,
|
||||||
|
allow_unsigned: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let fetched =
|
||||||
|
fetch_binary_package_archive("repo", &repo_cfg, rootfs.path(), &rec, cache_dir.path())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(std::fs::read(&fetched).unwrap(), payload);
|
||||||
|
assert!(PathBuf::from(format!("{}.sig", fetched.display())).exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fetch_binary_package_archive_allows_missing_signature_when_configured() {
|
||||||
|
let rootfs = tempfile::tempdir().unwrap();
|
||||||
|
let repo_dir = tempfile::tempdir().unwrap();
|
||||||
|
let cache_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let filename = "pkg-1.0-1-x86_64.depot.pkg.tar.zst";
|
||||||
|
let payload = b"unsigned package payload";
|
||||||
|
std::fs::write(repo_dir.path().join(filename), payload).unwrap();
|
||||||
|
|
||||||
|
let rec = test_record_for_payload(filename, payload);
|
||||||
|
let repo_url = url::Url::from_directory_path(repo_dir.path())
|
||||||
|
.expect("file URL")
|
||||||
|
.to_string();
|
||||||
|
let repo_cfg = crate::config::BinaryRepo {
|
||||||
|
url: repo_url,
|
||||||
|
allow_unsigned: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let fetched =
|
||||||
|
fetch_binary_package_archive("repo", &repo_cfg, rootfs.path(), &rec, cache_dir.path())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(std::fs::read(&fetched).unwrap(), payload);
|
||||||
|
assert!(!PathBuf::from(format!("{}.sig", fetched.display())).exists());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_copy_file_url_to_path_supports_file_scheme() {
|
fn test_copy_file_url_to_path_supports_file_scheme() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
+84
-17
@@ -9,7 +9,7 @@
|
|||||||
//! - `package<=1.2.3` - less than or equal to 1.2.3
|
//! - `package<=1.2.3` - less than or equal to 1.2.3
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::package::PackageSpec;
|
use crate::package::{BuildType, PackageSpec};
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -139,6 +139,13 @@ fn is_dep_satisfied(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_type_runs_automatic_tests(spec: &PackageSpec) -> bool {
|
||||||
|
matches!(
|
||||||
|
spec.build.build_type,
|
||||||
|
BuildType::Autotools | BuildType::CMake
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Check whether a dependency expression is satisfied by the installed package DB.
|
/// Check whether a dependency expression is satisfied by the installed package DB.
|
||||||
pub fn is_dep_satisfied_in_db(dep: &str, db_path: &Path) -> Result<bool> {
|
pub fn is_dep_satisfied_in_db(dep: &str, db_path: &Path) -> Result<bool> {
|
||||||
if !db_path.exists() {
|
if !db_path.exists() {
|
||||||
@@ -173,15 +180,24 @@ pub fn check_build_deps(spec: &PackageSpec, db_path: &Path) -> Result<Vec<String
|
|||||||
/// Check if all runtime dependencies are satisfied
|
/// Check if all runtime dependencies are satisfied
|
||||||
pub fn check_runtime_deps(spec: &PackageSpec, db_path: &Path) -> Result<Vec<String>> {
|
pub fn check_runtime_deps(spec: &PackageSpec, db_path: &Path) -> Result<Vec<String>> {
|
||||||
let mut missing = Vec::new();
|
let mut missing = Vec::new();
|
||||||
|
let local_provides = spec.local_dependency_provides();
|
||||||
|
|
||||||
if !db_path.exists() {
|
if !db_path.exists() {
|
||||||
return Ok(spec.dependencies.runtime.clone());
|
for dep in &spec.dependencies.runtime {
|
||||||
|
if !local_provides.contains(dep_name(dep)) {
|
||||||
|
missing.push(dep.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(missing);
|
||||||
}
|
}
|
||||||
|
|
||||||
let installed = db::get_installed_packages(db_path)?;
|
let installed = db::get_installed_packages(db_path)?;
|
||||||
let provides = db::get_all_provides(db_path)?;
|
let provides = db::get_all_provides(db_path)?;
|
||||||
|
|
||||||
for dep in &spec.dependencies.runtime {
|
for dep in &spec.dependencies.runtime {
|
||||||
|
if local_provides.contains(dep_name(dep)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if !is_dep_satisfied(dep, &installed, &provides, db_path)? {
|
if !is_dep_satisfied(dep, &installed, &provides, db_path)? {
|
||||||
missing.push(dep.clone());
|
missing.push(dep.clone());
|
||||||
}
|
}
|
||||||
@@ -244,11 +260,21 @@ pub fn print_dep_status(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
|||||||
"Test dependencies: {}",
|
"Test dependencies: {}",
|
||||||
spec.dependencies.test.join(", ")
|
spec.dependencies.test.join(", ")
|
||||||
));
|
));
|
||||||
if !missing_test.is_empty() {
|
if !spec.build.flags.skip_tests
|
||||||
|
&& build_type_runs_automatic_tests(spec)
|
||||||
|
&& !missing_test.is_empty()
|
||||||
|
{
|
||||||
ui::warn(format!("Test deps missing: {}", missing_test.join(", ")));
|
ui::warn(format!("Test deps missing: {}", missing_test.join(", ")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !spec.dependencies.optional.is_empty() {
|
||||||
|
ui::info(format!(
|
||||||
|
"Optional dependencies: {}",
|
||||||
|
spec.dependencies.optional.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,20 +292,6 @@ pub fn require_build_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify all test dependencies are installed, error if not
|
|
||||||
pub fn require_test_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
|
||||||
let missing = check_test_deps(spec, db_path)?;
|
|
||||||
|
|
||||||
if !missing.is_empty() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Missing test dependencies: {}\nInstall them first with: depot install <package>",
|
|
||||||
missing.join(", ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify all runtime dependencies are installed, error if not.
|
/// Verify all runtime dependencies are installed, error if not.
|
||||||
pub fn require_runtime_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
pub fn require_runtime_deps(spec: &PackageSpec, db_path: &Path) -> Result<()> {
|
||||||
let missing = check_runtime_deps(spec, db_path)?;
|
let missing = check_runtime_deps(spec, db_path)?;
|
||||||
@@ -372,6 +384,7 @@ mod tests {
|
|||||||
build: Vec::new(),
|
build: Vec::new(),
|
||||||
runtime: Vec::new(),
|
runtime: Vec::new(),
|
||||||
test: vec!["bats".into(), "python".into()],
|
test: vec!["bats".into(), "python".into()],
|
||||||
|
optional: Vec::new(),
|
||||||
},
|
},
|
||||||
package_alternatives: Default::default(),
|
package_alternatives: Default::default(),
|
||||||
package_dependencies: Default::default(),
|
package_dependencies: Default::default(),
|
||||||
@@ -411,6 +424,7 @@ mod tests {
|
|||||||
build: Vec::new(),
|
build: Vec::new(),
|
||||||
runtime: vec!["python".into()],
|
runtime: vec!["python".into()],
|
||||||
test: Vec::new(),
|
test: Vec::new(),
|
||||||
|
optional: Vec::new(),
|
||||||
},
|
},
|
||||||
package_alternatives: Default::default(),
|
package_alternatives: Default::default(),
|
||||||
package_dependencies: Default::default(),
|
package_dependencies: Default::default(),
|
||||||
@@ -421,4 +435,57 @@ mod tests {
|
|||||||
.expect_err("runtime deps should be required");
|
.expect_err("runtime deps should be required");
|
||||||
assert!(err.to_string().contains("Missing runtime dependencies"));
|
assert!(err.to_string().contains("Missing runtime dependencies"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_check_runtime_deps_ignores_local_outputs_and_provides() {
|
||||||
|
let spec = PackageSpec {
|
||||||
|
package: crate::package::PackageInfo {
|
||||||
|
name: "foo".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "d".into(),
|
||||||
|
homepage: "h".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
|
packages: vec![crate::package::PackageInfo {
|
||||||
|
name: "foo-libs".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "d".into(),
|
||||||
|
homepage: "h".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
}],
|
||||||
|
alternatives: Default::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: vec![crate::package::Source {
|
||||||
|
url: "https://example.test/foo.tar.gz".into(),
|
||||||
|
sha256: "skip".into(),
|
||||||
|
extract_dir: "foo".into(),
|
||||||
|
patches: Vec::new(),
|
||||||
|
post_extract: Vec::new(),
|
||||||
|
}],
|
||||||
|
build: crate::package::Build {
|
||||||
|
build_type: crate::package::BuildType::Custom,
|
||||||
|
flags: crate::package::BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: crate::package::Dependencies {
|
||||||
|
build: Vec::new(),
|
||||||
|
runtime: vec!["foo-libs".into(), "libfoo".into(), "python".into()],
|
||||||
|
test: Vec::new(),
|
||||||
|
optional: Vec::new(),
|
||||||
|
},
|
||||||
|
package_alternatives: std::collections::BTreeMap::from([(
|
||||||
|
"foo-libs".to_string(),
|
||||||
|
crate::package::Alternatives {
|
||||||
|
provides: vec!["libfoo".into()],
|
||||||
|
replaces: Vec::new(),
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
package_dependencies: Default::default(),
|
||||||
|
spec_dir: std::path::PathBuf::from("."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let missing = check_runtime_deps(&spec, Path::new("/definitely/not/a/real/db")).unwrap();
|
||||||
|
assert_eq!(missing, vec!["python".to_string()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,615 @@
|
|||||||
|
//! Transaction hook loading and execution.
|
||||||
|
//!
|
||||||
|
//! Hook files live under `<rootfs>/etc/depot.d/hooks/*.toml` and follow a
|
||||||
|
//! Starpack-inspired format:
|
||||||
|
//!
|
||||||
|
//! ```toml
|
||||||
|
//! [hook]
|
||||||
|
//! name = "refresh cache"
|
||||||
|
//! description = "Refreshes cache after updates"
|
||||||
|
//!
|
||||||
|
//! [when]
|
||||||
|
//! phase = "post"
|
||||||
|
//! operation = ["install", "update"]
|
||||||
|
//! packages = ["glibc", "linux*"]
|
||||||
|
//! paths = ["usr/lib/*"]
|
||||||
|
//! negation = ["usr/lib/debug/*"]
|
||||||
|
//!
|
||||||
|
//! [exec]
|
||||||
|
//! command = "ldconfig"
|
||||||
|
//! needs_paths = true
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The legacy section/key names `[Hook]`, `[When]`, and `[Exec]` are accepted.
|
||||||
|
|
||||||
|
use crate::fakeroot;
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, ExitStatus, Stdio};
|
||||||
|
|
||||||
|
const TRANSACTION_HOOKS_DIR_REL: &str = "etc/depot.d/hooks";
|
||||||
|
|
||||||
|
/// Hook phase within an install/update/remove transaction.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum HookPhase {
|
||||||
|
/// Before files are changed.
|
||||||
|
Pre,
|
||||||
|
/// After files are changed.
|
||||||
|
Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HookPhase {
|
||||||
|
fn parse(raw: &str) -> Option<Self> {
|
||||||
|
match raw.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"pre" | "pretransaction" | "pre_transaction" | "pre-transaction" => {
|
||||||
|
Some(HookPhase::Pre)
|
||||||
|
}
|
||||||
|
"post" | "posttransaction" | "post_transaction" | "post-transaction" => {
|
||||||
|
Some(HookPhase::Post)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
HookPhase::Pre => "pre",
|
||||||
|
HookPhase::Post => "post",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction operation for hooks.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum HookOperation {
|
||||||
|
/// Package install.
|
||||||
|
Install,
|
||||||
|
/// Package update/upgrade.
|
||||||
|
Update,
|
||||||
|
/// Package removal.
|
||||||
|
Remove,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HookOperation {
|
||||||
|
fn parse(raw: &str) -> Option<Self> {
|
||||||
|
match raw.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"install" => Some(HookOperation::Install),
|
||||||
|
"update" | "upgrade" => Some(HookOperation::Update),
|
||||||
|
"remove" | "uninstall" => Some(HookOperation::Remove),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
HookOperation::Install => "install",
|
||||||
|
HookOperation::Update => "update",
|
||||||
|
HookOperation::Remove => "remove",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime context for transaction hook matching/execution.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HookExecutionContext<'a> {
|
||||||
|
/// Current phase (`pre` or `post`).
|
||||||
|
pub phase: HookPhase,
|
||||||
|
/// Current operation (`install`, `update`, `remove`).
|
||||||
|
pub operation: HookOperation,
|
||||||
|
/// Package being processed.
|
||||||
|
pub package: &'a str,
|
||||||
|
/// Filesystem paths affected by this package action.
|
||||||
|
pub affected_paths: &'a [String],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct TransactionHook {
|
||||||
|
file_path: PathBuf,
|
||||||
|
name: Option<String>,
|
||||||
|
phase: HookPhase,
|
||||||
|
operations: Vec<HookOperation>,
|
||||||
|
packages: Vec<String>,
|
||||||
|
paths: Vec<String>,
|
||||||
|
negations: Vec<String>,
|
||||||
|
command: String,
|
||||||
|
needs_paths: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionHook {
|
||||||
|
fn display_name(&self) -> String {
|
||||||
|
self.name.clone().unwrap_or_else(|| {
|
||||||
|
self.file_path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("<unnamed-hook>")
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(&self, ctx: &HookExecutionContext<'_>, normalized_paths: &[String]) -> bool {
|
||||||
|
if self.phase != ctx.phase {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.operations.is_empty() && !self.operations.contains(&ctx.operation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.packages.is_empty() {
|
||||||
|
let package = ctx.package.trim();
|
||||||
|
let package_match = self
|
||||||
|
.packages
|
||||||
|
.iter()
|
||||||
|
.any(|pattern| wildcard_match(pattern.trim(), package));
|
||||||
|
if !package_match {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.paths.is_empty() {
|
||||||
|
let positive_match = normalized_paths.iter().any(|path| {
|
||||||
|
self.paths.iter().any(|pattern| {
|
||||||
|
wildcard_match(
|
||||||
|
&normalize_match_target(pattern),
|
||||||
|
&normalize_match_target(path),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !positive_match {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.negations.is_empty() {
|
||||||
|
let negated = normalized_paths.iter().any(|path| {
|
||||||
|
self.negations.iter().any(|pattern| {
|
||||||
|
wildcard_match(
|
||||||
|
&normalize_match_target(pattern),
|
||||||
|
&normalize_match_target(path),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if negated {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the transaction hook directory for a rootfs.
|
||||||
|
pub fn transaction_hooks_dir(rootfs: &Path) -> PathBuf {
|
||||||
|
rootfs.join(TRANSACTION_HOOKS_DIR_REL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load and execute transaction hooks that match `ctx`.
|
||||||
|
///
|
||||||
|
/// Returns the number of hooks that were executed.
|
||||||
|
pub fn run_transaction_hooks(rootfs: &Path, ctx: &HookExecutionContext<'_>) -> Result<usize> {
|
||||||
|
let hook_dir = transaction_hooks_dir(rootfs);
|
||||||
|
let hook_files = discover_hook_files(&hook_dir)?;
|
||||||
|
if hook_files.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized_paths: Vec<String> = ctx
|
||||||
|
.affected_paths
|
||||||
|
.iter()
|
||||||
|
.map(|p| normalize_match_target(p))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut executed = 0usize;
|
||||||
|
for hook_file in hook_files {
|
||||||
|
let hook = parse_hook_file(&hook_file)?;
|
||||||
|
if !hook.matches(ctx, &normalized_paths) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
run_hook_command(rootfs, &hook, ctx, &normalized_paths)?;
|
||||||
|
executed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(executed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_hook_files(hook_dir: &Path) -> Result<Vec<PathBuf>> {
|
||||||
|
if !hook_dir.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
if !hook_dir.is_dir() {
|
||||||
|
bail!(
|
||||||
|
"Transaction hooks path exists but is not a directory: {}",
|
||||||
|
hook_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut files = Vec::new();
|
||||||
|
for entry in
|
||||||
|
fs::read_dir(hook_dir).with_context(|| format!("Failed to read {}", hook_dir.display()))?
|
||||||
|
{
|
||||||
|
let path = entry
|
||||||
|
.with_context(|| format!("Failed to list {}", hook_dir.display()))?
|
||||||
|
.path();
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let is_toml = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.map(|e| e.eq_ignore_ascii_case("toml"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if is_toml {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_hook_file(path: &Path) -> Result<TransactionHook> {
|
||||||
|
let content =
|
||||||
|
fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
|
||||||
|
let root: toml::Value =
|
||||||
|
toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
|
||||||
|
|
||||||
|
let root_table = root
|
||||||
|
.as_table()
|
||||||
|
.with_context(|| format!("Hook file is not a TOML table: {}", path.display()))?;
|
||||||
|
let hook_table = select_table(root_table, &["hook", "Hook"]);
|
||||||
|
let when_table = select_table(root_table, &["when", "When"])
|
||||||
|
.with_context(|| format!("Missing [when] table in {}", path.display()))?;
|
||||||
|
let exec_table = select_table(root_table, &["exec", "Exec"])
|
||||||
|
.with_context(|| format!("Missing [exec] table in {}", path.display()))?;
|
||||||
|
|
||||||
|
let phase = get_required_string(when_table, &["phase", "Phase"])
|
||||||
|
.with_context(|| format!("Missing when.phase in {}", path.display()))
|
||||||
|
.and_then(|raw| {
|
||||||
|
HookPhase::parse(&raw)
|
||||||
|
.with_context(|| format!("Invalid when.phase '{}' in {}", raw, path.display()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let operations = get_string_list(when_table, &["operation", "operations", "Operation"])
|
||||||
|
.with_context(|| format!("Invalid when.operation in {}", path.display()))?
|
||||||
|
.into_iter()
|
||||||
|
.map(|raw| {
|
||||||
|
HookOperation::parse(&raw)
|
||||||
|
.with_context(|| format!("Invalid operation '{}' in {}", raw, path.display()))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let packages = get_string_list(when_table, &["package", "packages", "Package", "Packages"])
|
||||||
|
.with_context(|| format!("Invalid when.packages in {}", path.display()))?;
|
||||||
|
let paths = get_string_list(when_table, &["path", "paths", "Paths"])
|
||||||
|
.with_context(|| format!("Invalid when.paths in {}", path.display()))?;
|
||||||
|
let negations = get_string_list(
|
||||||
|
when_table,
|
||||||
|
&[
|
||||||
|
"negation",
|
||||||
|
"negations",
|
||||||
|
"Negation",
|
||||||
|
"exclude_paths",
|
||||||
|
"not_paths",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.with_context(|| format!("Invalid when.negation in {}", path.display()))?;
|
||||||
|
|
||||||
|
let command = get_required_string(exec_table, &["command", "Command"])
|
||||||
|
.with_context(|| format!("Missing exec.command in {}", path.display()))?;
|
||||||
|
let needs_paths = get_optional_bool(exec_table, &["needs_paths", "NeedsPaths"])
|
||||||
|
.with_context(|| format!("Invalid exec.needs_paths in {}", path.display()))?
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
Ok(TransactionHook {
|
||||||
|
file_path: path.to_path_buf(),
|
||||||
|
name: hook_table.and_then(|t| get_optional_string(t, &["name", "Name"])),
|
||||||
|
phase,
|
||||||
|
operations,
|
||||||
|
packages,
|
||||||
|
paths,
|
||||||
|
negations,
|
||||||
|
command,
|
||||||
|
needs_paths,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_table<'a>(
|
||||||
|
root: &'a toml::map::Map<String, toml::Value>,
|
||||||
|
keys: &[&str],
|
||||||
|
) -> Option<&'a toml::map::Map<String, toml::Value>> {
|
||||||
|
keys.iter()
|
||||||
|
.find_map(|key| root.get(*key).and_then(toml::Value::as_table))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_optional_string(
|
||||||
|
table: &toml::map::Map<String, toml::Value>,
|
||||||
|
keys: &[&str],
|
||||||
|
) -> Option<String> {
|
||||||
|
keys.iter().find_map(|key| {
|
||||||
|
table
|
||||||
|
.get(*key)
|
||||||
|
.and_then(toml::Value::as_str)
|
||||||
|
.map(str::to_string)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_required_string(
|
||||||
|
table: &toml::map::Map<String, toml::Value>,
|
||||||
|
keys: &[&str],
|
||||||
|
) -> Result<String> {
|
||||||
|
get_optional_string(table, keys)
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.with_context(|| format!("Missing required key '{}'", keys[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_string_list(
|
||||||
|
table: &toml::map::Map<String, toml::Value>,
|
||||||
|
keys: &[&str],
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
for key in keys {
|
||||||
|
let Some(value) = table.get(*key) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
return match value {
|
||||||
|
toml::Value::String(s) => Ok(vec![s.to_string()]),
|
||||||
|
toml::Value::Array(arr) => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for item in arr {
|
||||||
|
let value = item
|
||||||
|
.as_str()
|
||||||
|
.with_context(|| format!("Expected string elements for key '{}'", key))?;
|
||||||
|
out.push(value.to_string());
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
_ => bail!("Expected string or string array for key '{}'", key),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_optional_bool(
|
||||||
|
table: &toml::map::Map<String, toml::Value>,
|
||||||
|
keys: &[&str],
|
||||||
|
) -> Result<Option<bool>> {
|
||||||
|
for key in keys {
|
||||||
|
let Some(value) = table.get(*key) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
return value
|
||||||
|
.as_bool()
|
||||||
|
.map(Some)
|
||||||
|
.with_context(|| format!("Expected boolean for key '{}'", key));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_hook_command(
|
||||||
|
rootfs: &Path,
|
||||||
|
hook: &TransactionHook,
|
||||||
|
ctx: &HookExecutionContext<'_>,
|
||||||
|
normalized_paths: &[String],
|
||||||
|
) -> Result<()> {
|
||||||
|
let hook_name = hook.display_name();
|
||||||
|
crate::log_info!(
|
||||||
|
"Running transaction hook '{}' ({}) for {}:{}:{}",
|
||||||
|
hook_name,
|
||||||
|
hook.file_path.display(),
|
||||||
|
ctx.operation.as_str(),
|
||||||
|
ctx.phase.as_str(),
|
||||||
|
ctx.package
|
||||||
|
);
|
||||||
|
|
||||||
|
let stdin_payload = if hook.needs_paths {
|
||||||
|
let mut payload = normalized_paths.join("\n");
|
||||||
|
if !payload.is_empty() {
|
||||||
|
payload.push('\n');
|
||||||
|
}
|
||||||
|
Some(payload)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut command = if fakeroot::is_root() && rootfs.join("bin/sh").exists() {
|
||||||
|
let mut cmd = Command::new("chroot");
|
||||||
|
cmd.arg(rootfs).arg("/bin/sh").arg("-lc").arg(&hook.command);
|
||||||
|
cmd
|
||||||
|
} else {
|
||||||
|
let mut cmd = Command::new("/bin/sh");
|
||||||
|
cmd.arg("-lc").arg(&hook.command).current_dir(rootfs);
|
||||||
|
cmd
|
||||||
|
};
|
||||||
|
|
||||||
|
command
|
||||||
|
.env("DEPOT_ACTION", ctx.operation.as_str())
|
||||||
|
.env("DEPOT_PHASE", ctx.phase.as_str())
|
||||||
|
.env("DEPOT_PACKAGE", ctx.package)
|
||||||
|
.env("DEPOT_ROOTFS", rootfs)
|
||||||
|
.env("DEPOT_HOOK_FILE", &hook.file_path)
|
||||||
|
.env("DEPOT_HOOK_NAME", &hook_name);
|
||||||
|
|
||||||
|
let status = run_command_with_optional_stdin(&mut command, stdin_payload.as_deref())
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to execute transaction hook '{}' ({})",
|
||||||
|
hook_name,
|
||||||
|
hook.file_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
bail!(
|
||||||
|
"Transaction hook '{}' failed with status {}",
|
||||||
|
hook_name,
|
||||||
|
status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_command_with_optional_stdin(
|
||||||
|
command: &mut Command,
|
||||||
|
stdin_payload: Option<&str>,
|
||||||
|
) -> Result<ExitStatus> {
|
||||||
|
if let Some(payload) = stdin_payload {
|
||||||
|
command.stdin(Stdio::piped());
|
||||||
|
let mut child = command.spawn()?;
|
||||||
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
|
stdin.write_all(payload.as_bytes())?;
|
||||||
|
}
|
||||||
|
Ok(child.wait()?)
|
||||||
|
} else {
|
||||||
|
Ok(command.status()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_match_target(raw: &str) -> String {
|
||||||
|
raw.trim()
|
||||||
|
.trim_start_matches("./")
|
||||||
|
.trim_start_matches('/')
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wildcard_match(pattern: &str, text: &str) -> bool {
|
||||||
|
let p = pattern.as_bytes();
|
||||||
|
let t = text.as_bytes();
|
||||||
|
let mut dp = vec![vec![false; t.len() + 1]; p.len() + 1];
|
||||||
|
dp[0][0] = true;
|
||||||
|
|
||||||
|
for i in 1..=p.len() {
|
||||||
|
if p[i - 1] == b'*' {
|
||||||
|
dp[i][0] = dp[i - 1][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 1..=p.len() {
|
||||||
|
for j in 1..=t.len() {
|
||||||
|
dp[i][j] = match p[i - 1] {
|
||||||
|
b'*' => dp[i - 1][j] || dp[i][j - 1],
|
||||||
|
b'?' => dp[i - 1][j - 1],
|
||||||
|
ch => dp[i - 1][j - 1] && ch == t[j - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dp[p.len()][t.len()]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn write_hook(rootfs: &Path, name: &str, content: &str) {
|
||||||
|
let dir = transaction_hooks_dir(rootfs);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
std::fs::write(dir.join(name), content).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wildcard_match_supports_star_and_question() {
|
||||||
|
assert!(wildcard_match("usr/lib/*", "usr/lib/libc.so"));
|
||||||
|
assert!(wildcard_match("lib??", "lib32"));
|
||||||
|
assert!(!wildcard_match("usr/bin/*", "usr/lib/libc.so"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_hook_accepts_starpack_style_sections() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
write_hook(
|
||||||
|
tmp.path(),
|
||||||
|
"demo.toml",
|
||||||
|
r#"
|
||||||
|
[Hook]
|
||||||
|
Name = "demo"
|
||||||
|
|
||||||
|
[When]
|
||||||
|
Phase = "PreTransaction"
|
||||||
|
Operation = "Install"
|
||||||
|
Package = "foo"
|
||||||
|
|
||||||
|
[Exec]
|
||||||
|
Command = "true"
|
||||||
|
NeedsPaths = false
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = transaction_hooks_dir(tmp.path()).join("demo.toml");
|
||||||
|
let hook = parse_hook_file(&path).unwrap();
|
||||||
|
assert_eq!(hook.name.as_deref(), Some("demo"));
|
||||||
|
assert_eq!(hook.phase, HookPhase::Pre);
|
||||||
|
assert_eq!(hook.operations, vec![HookOperation::Install]);
|
||||||
|
assert_eq!(hook.packages, vec!["foo".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_transaction_hooks_executes_matching_hook() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
write_hook(
|
||||||
|
tmp.path(),
|
||||||
|
"record.toml",
|
||||||
|
r#"
|
||||||
|
[hook]
|
||||||
|
name = "record"
|
||||||
|
|
||||||
|
[when]
|
||||||
|
phase = "pre"
|
||||||
|
operation = ["install"]
|
||||||
|
packages = ["foo*"]
|
||||||
|
paths = ["usr/bin/*"]
|
||||||
|
|
||||||
|
[exec]
|
||||||
|
command = "printf '%s:%s:%s' \"$DEPOT_ACTION\" \"$DEPOT_PHASE\" \"$DEPOT_PACKAGE\" > \"$DEPOT_ROOTFS/hook.out\""
|
||||||
|
needs_paths = true
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let affected = vec!["usr/bin/foo".to_string()];
|
||||||
|
let ctx = HookExecutionContext {
|
||||||
|
phase: HookPhase::Pre,
|
||||||
|
operation: HookOperation::Install,
|
||||||
|
package: "foo",
|
||||||
|
affected_paths: &affected,
|
||||||
|
};
|
||||||
|
let ran = run_transaction_hooks(tmp.path(), &ctx).unwrap();
|
||||||
|
assert_eq!(ran, 1);
|
||||||
|
let out = std::fs::read_to_string(tmp.path().join("hook.out")).unwrap();
|
||||||
|
assert_eq!(out, "install:pre:foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_transaction_hooks_respects_negation_and_filters_out() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
write_hook(
|
||||||
|
tmp.path(),
|
||||||
|
"skip.toml",
|
||||||
|
r#"
|
||||||
|
[hook]
|
||||||
|
name = "skip"
|
||||||
|
|
||||||
|
[when]
|
||||||
|
phase = "pre"
|
||||||
|
operation = "remove"
|
||||||
|
paths = ["usr/lib/*"]
|
||||||
|
negation = ["usr/lib/debug/*"]
|
||||||
|
|
||||||
|
[exec]
|
||||||
|
command = "touch \"$DEPOT_ROOTFS/should_not_exist\""
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let affected = vec!["usr/lib/debug/foo".to_string()];
|
||||||
|
let ctx = HookExecutionContext {
|
||||||
|
phase: HookPhase::Pre,
|
||||||
|
operation: HookOperation::Remove,
|
||||||
|
package: "foo",
|
||||||
|
affected_paths: &affected,
|
||||||
|
};
|
||||||
|
let ran = run_transaction_hooks(tmp.path(), &ctx).unwrap();
|
||||||
|
assert_eq!(ran, 0);
|
||||||
|
assert!(!tmp.path().join("should_not_exist").exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
//! Installation helpers.
|
//! Installation helpers.
|
||||||
|
|
||||||
|
pub mod hooks;
|
||||||
pub mod scripts;
|
pub mod scripts;
|
||||||
|
|||||||
+6
-2134
File diff suppressed because it is too large
Load Diff
@@ -640,6 +640,10 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
|||||||
"Test dependency",
|
"Test dependency",
|
||||||
"Package needed only for running package test suites",
|
"Package needed only for running package test suites",
|
||||||
)?;
|
)?;
|
||||||
|
let optional_deps = prompt_repeating_list(
|
||||||
|
"Optional dependency",
|
||||||
|
"Package that enables optional runtime functionality",
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(PackageSpec {
|
Ok(PackageSpec {
|
||||||
package: PackageInfo {
|
package: PackageInfo {
|
||||||
@@ -659,6 +663,7 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
|||||||
build: build_deps,
|
build: build_deps,
|
||||||
runtime: runtime_deps,
|
runtime: runtime_deps,
|
||||||
test: test_deps,
|
test: test_deps,
|
||||||
|
optional: optional_deps,
|
||||||
},
|
},
|
||||||
package_alternatives: Default::default(),
|
package_alternatives: Default::default(),
|
||||||
package_dependencies: Default::default(),
|
package_dependencies: Default::default(),
|
||||||
@@ -875,6 +880,12 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
|||||||
if spec.build.flags.no_strip != defaults.no_strip {
|
if spec.build.flags.no_strip != defaults.no_strip {
|
||||||
flags_tbl.insert("no_strip".into(), Value::Boolean(spec.build.flags.no_strip));
|
flags_tbl.insert("no_strip".into(), Value::Boolean(spec.build.flags.no_strip));
|
||||||
}
|
}
|
||||||
|
if spec.build.flags.no_delete_static != defaults.no_delete_static {
|
||||||
|
flags_tbl.insert(
|
||||||
|
"no_delete_static".into(),
|
||||||
|
Value::Boolean(spec.build.flags.no_delete_static),
|
||||||
|
);
|
||||||
|
}
|
||||||
if spec.build.flags.no_compress_man != defaults.no_compress_man {
|
if spec.build.flags.no_compress_man != defaults.no_compress_man {
|
||||||
flags_tbl.insert(
|
flags_tbl.insert(
|
||||||
"no_compress_man".into(),
|
"no_compress_man".into(),
|
||||||
@@ -1221,6 +1232,7 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
|||||||
if !spec.dependencies.build.is_empty()
|
if !spec.dependencies.build.is_empty()
|
||||||
|| !spec.dependencies.runtime.is_empty()
|
|| !spec.dependencies.runtime.is_empty()
|
||||||
|| !spec.dependencies.test.is_empty()
|
|| !spec.dependencies.test.is_empty()
|
||||||
|
|| !spec.dependencies.optional.is_empty()
|
||||||
{
|
{
|
||||||
let mut dep_tbl = Table::new();
|
let mut dep_tbl = Table::new();
|
||||||
if !spec.dependencies.build.is_empty() {
|
if !spec.dependencies.build.is_empty() {
|
||||||
@@ -1259,6 +1271,18 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if !spec.dependencies.optional.is_empty() {
|
||||||
|
dep_tbl.insert(
|
||||||
|
"optional".into(),
|
||||||
|
Value::Array(
|
||||||
|
spec.dependencies
|
||||||
|
.optional
|
||||||
|
.iter()
|
||||||
|
.map(|s| Value::String(s.clone()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
root.insert("dependencies".into(), Value::Table(dep_tbl));
|
root.insert("dependencies".into(), Value::Table(dep_tbl));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1412,6 +1436,7 @@ mod tests {
|
|||||||
flags.keep = vec!["etc/locale.gen".into()];
|
flags.keep = vec!["etc/locale.gen".into()];
|
||||||
flags.no_flags = true;
|
flags.no_flags = true;
|
||||||
flags.no_strip = true;
|
flags.no_strip = true;
|
||||||
|
flags.no_delete_static = true;
|
||||||
flags.no_compress_man = true;
|
flags.no_compress_man = true;
|
||||||
flags.skip_tests = true;
|
flags.skip_tests = true;
|
||||||
flags.make_vars = vec!["V=1".into()];
|
flags.make_vars = vec!["V=1".into()];
|
||||||
@@ -1467,6 +1492,7 @@ mod tests {
|
|||||||
assert!(toml.contains("\"etc/locale.gen\""));
|
assert!(toml.contains("\"etc/locale.gen\""));
|
||||||
assert!(toml.contains("no_flags = true"));
|
assert!(toml.contains("no_flags = true"));
|
||||||
assert!(toml.contains("no_strip = true"));
|
assert!(toml.contains("no_strip = true"));
|
||||||
|
assert!(toml.contains("no_delete_static = true"));
|
||||||
assert!(toml.contains("no_compress_man = true"));
|
assert!(toml.contains("no_compress_man = true"));
|
||||||
assert!(toml.contains("skip_tests = true"));
|
assert!(toml.contains("skip_tests = true"));
|
||||||
assert!(toml.contains("make_vars = ["));
|
assert!(toml.contains("make_vars = ["));
|
||||||
@@ -1515,7 +1541,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spec_to_minimal_toml_includes_test_dependencies() {
|
fn spec_to_minimal_toml_includes_test_and_optional_dependencies() {
|
||||||
let spec = PackageSpec {
|
let spec = PackageSpec {
|
||||||
package: PackageInfo {
|
package: PackageInfo {
|
||||||
name: "foo".into(),
|
name: "foo".into(),
|
||||||
@@ -1543,6 +1569,7 @@ mod tests {
|
|||||||
build: vec![],
|
build: vec![],
|
||||||
runtime: vec![],
|
runtime: vec![],
|
||||||
test: vec!["python".into(), "bats".into()],
|
test: vec!["python".into(), "bats".into()],
|
||||||
|
optional: vec!["gtk-doc".into()],
|
||||||
},
|
},
|
||||||
package_alternatives: Default::default(),
|
package_alternatives: Default::default(),
|
||||||
package_dependencies: Default::default(),
|
package_dependencies: Default::default(),
|
||||||
@@ -1559,6 +1586,13 @@ mod tests {
|
|||||||
assert_eq!(test_deps.len(), 2);
|
assert_eq!(test_deps.len(), 2);
|
||||||
assert_eq!(test_deps[0].as_str(), Some("python"));
|
assert_eq!(test_deps[0].as_str(), Some("python"));
|
||||||
assert_eq!(test_deps[1].as_str(), Some("bats"));
|
assert_eq!(test_deps[1].as_str(), Some("bats"));
|
||||||
|
let optional_deps = val
|
||||||
|
.get("dependencies")
|
||||||
|
.and_then(|d| d.get("optional"))
|
||||||
|
.and_then(|t| t.as_array())
|
||||||
|
.expect("expected dependencies.optional array");
|
||||||
|
assert_eq!(optional_deps.len(), 1);
|
||||||
|
assert_eq!(optional_deps[0].as_str(), Some("gtk-doc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1584,6 +1618,7 @@ mod tests {
|
|||||||
build: Vec::new(),
|
build: Vec::new(),
|
||||||
runtime: vec!["foo".into(), "bar".into()],
|
runtime: vec!["foo".into(), "bar".into()],
|
||||||
test: Vec::new(),
|
test: Vec::new(),
|
||||||
|
optional: Vec::new(),
|
||||||
},
|
},
|
||||||
package_alternatives: Default::default(),
|
package_alternatives: Default::default(),
|
||||||
package_dependencies: Default::default(),
|
package_dependencies: Default::default(),
|
||||||
|
|||||||
+10
-20
@@ -159,20 +159,9 @@ impl Packager {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add dependencies if useful for repo indexing
|
// Add install-relevant dependency kinds for repo/runtime consumers.
|
||||||
let mut build_deps = toml::map::Map::new();
|
let mut deps = toml::map::Map::new();
|
||||||
build_deps.insert(
|
deps.insert(
|
||||||
"build".to_string(),
|
|
||||||
toml::Value::Array(
|
|
||||||
self.spec
|
|
||||||
.dependencies
|
|
||||||
.build
|
|
||||||
.iter()
|
|
||||||
.map(|s| toml::Value::String(s.clone()))
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
build_deps.insert(
|
|
||||||
"runtime".to_string(),
|
"runtime".to_string(),
|
||||||
toml::Value::Array(
|
toml::Value::Array(
|
||||||
self.spec
|
self.spec
|
||||||
@@ -183,18 +172,18 @@ impl Packager {
|
|||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
build_deps.insert(
|
deps.insert(
|
||||||
"test".to_string(),
|
"optional".to_string(),
|
||||||
toml::Value::Array(
|
toml::Value::Array(
|
||||||
self.spec
|
self.spec
|
||||||
.dependencies
|
.dependencies
|
||||||
.test
|
.optional
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| toml::Value::String(s.clone()))
|
.map(|s| toml::Value::String(s.clone()))
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
map.insert("dependencies".to_string(), toml::Value::Table(build_deps));
|
map.insert("dependencies".to_string(), toml::Value::Table(deps));
|
||||||
|
|
||||||
let toml_str = toml::to_string(&toml::Value::Table(map))
|
let toml_str = toml::to_string(&toml::Value::Table(map))
|
||||||
.context("Failed to serialize metadata to TOML")?;
|
.context("Failed to serialize metadata to TOML")?;
|
||||||
@@ -357,9 +346,10 @@ mod tests {
|
|||||||
assert_eq!(val.get("license").and_then(|v| v.as_str()), Some("MIT"));
|
assert_eq!(val.get("license").and_then(|v| v.as_str()), Some("MIT"));
|
||||||
|
|
||||||
let deps = val.get("dependencies").unwrap();
|
let deps = val.get("dependencies").unwrap();
|
||||||
assert!(deps.get("build").unwrap().as_array().unwrap().is_empty());
|
|
||||||
assert!(deps.get("runtime").unwrap().as_array().unwrap().is_empty());
|
assert!(deps.get("runtime").unwrap().as_array().unwrap().is_empty());
|
||||||
assert!(deps.get("test").unwrap().as_array().unwrap().is_empty());
|
assert!(deps.get("optional").unwrap().as_array().unwrap().is_empty());
|
||||||
|
assert!(deps.get("build").is_none());
|
||||||
|
assert!(deps.get("test").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+54
-6
@@ -5,7 +5,7 @@ use serde::Deserialize;
|
|||||||
use serde::Deserializer;
|
use serde::Deserializer;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::Serializer;
|
use serde::Serializer;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -192,6 +192,22 @@ impl PackageSpec {
|
|||||||
.unwrap_or_else(|| self.dependencies.clone())
|
.unwrap_or_else(|| self.dependencies.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return all package names/provided features produced by this spec.
|
||||||
|
///
|
||||||
|
/// This includes all output package names and per-output `provides` entries.
|
||||||
|
pub fn local_dependency_provides(&self) -> HashSet<String> {
|
||||||
|
let mut names = HashSet::new();
|
||||||
|
for output in self.outputs() {
|
||||||
|
let output_name = output.name.clone();
|
||||||
|
names.insert(output_name.clone());
|
||||||
|
let alternatives = self.alternatives_for_output(&output_name);
|
||||||
|
for provided in alternatives.provides {
|
||||||
|
names.insert(provided);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
/// Return alternatives/provides for a specific output package name.
|
/// Return alternatives/provides for a specific output package name.
|
||||||
///
|
///
|
||||||
/// If no per-output override exists, returns the top-level alternatives.
|
/// If no per-output override exists, returns the top-level alternatives.
|
||||||
@@ -481,6 +497,11 @@ impl PackageSpec {
|
|||||||
self.build.flags.no_strip = b;
|
self.build.flags.no_strip = b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"no_delete_static" | "no-delete-static" => {
|
||||||
|
if let Some(b) = v.as_bool() {
|
||||||
|
self.build.flags.no_delete_static = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
"no_compress_man"
|
"no_compress_man"
|
||||||
| "no-compress-man"
|
| "no-compress-man"
|
||||||
| "no_compress_manpages"
|
| "no_compress_manpages"
|
||||||
@@ -997,6 +1018,11 @@ impl PackageSpec {
|
|||||||
self.build.flags.no_strip = b;
|
self.build.flags.no_strip = b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"no_delete_static" | "no-delete-static" => {
|
||||||
|
if let Some(b) = values.last().and_then(|v| v.as_bool()) {
|
||||||
|
self.build.flags.no_delete_static = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
"no_compress_man"
|
"no_compress_man"
|
||||||
| "no-compress-man"
|
| "no-compress-man"
|
||||||
| "no_compress_manpages"
|
| "no_compress_manpages"
|
||||||
@@ -1535,6 +1561,7 @@ make_test_dirs = ["tests"]
|
|||||||
make_install_dirs = ["lib"]
|
make_install_dirs = ["lib"]
|
||||||
no_flags = true
|
no_flags = true
|
||||||
no_strip = true
|
no_strip = true
|
||||||
|
no_delete_static = true
|
||||||
no_compress_man = true
|
no_compress_man = true
|
||||||
skip_tests = true
|
skip_tests = true
|
||||||
keep = ["etc/locale.gen"]
|
keep = ["etc/locale.gen"]
|
||||||
@@ -1572,6 +1599,10 @@ post_configure = ["echo configured"]
|
|||||||
"build.flags.no_compress_man".to_string(),
|
"build.flags.no_compress_man".to_string(),
|
||||||
vec![toml::Value::Boolean(false)],
|
vec![toml::Value::Boolean(false)],
|
||||||
);
|
);
|
||||||
|
config.appends.insert(
|
||||||
|
"build.flags.no_delete_static".to_string(),
|
||||||
|
vec![toml::Value::Boolean(false)],
|
||||||
|
);
|
||||||
config.appends.insert(
|
config.appends.insert(
|
||||||
"build.flags.passthrough_env".to_string(),
|
"build.flags.passthrough_env".to_string(),
|
||||||
vec![toml::Value::String("CARGO_HOME".to_string())],
|
vec![toml::Value::String("CARGO_HOME".to_string())],
|
||||||
@@ -1627,6 +1658,7 @@ post_configure = ["echo configured"]
|
|||||||
);
|
);
|
||||||
assert!(spec.build.flags.no_flags);
|
assert!(spec.build.flags.no_flags);
|
||||||
assert!(!spec.build.flags.no_strip);
|
assert!(!spec.build.flags.no_strip);
|
||||||
|
assert!(!spec.build.flags.no_delete_static);
|
||||||
assert!(!spec.build.flags.no_compress_man);
|
assert!(!spec.build.flags.no_compress_man);
|
||||||
assert!(
|
assert!(
|
||||||
spec.build
|
spec.build
|
||||||
@@ -1735,7 +1767,7 @@ no_flags = true
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_no_strip_and_no_compress_man_from_spec() {
|
fn parse_no_strip_no_delete_static_and_no_compress_man_from_spec() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let path = tmp.path().join("pkg.toml");
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
@@ -1759,6 +1791,7 @@ type = "custom"
|
|||||||
|
|
||||||
[build.flags]
|
[build.flags]
|
||||||
no_strip = true
|
no_strip = true
|
||||||
|
"no-delete-static" = true
|
||||||
no-compress-man = true
|
no-compress-man = true
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
@@ -1766,6 +1799,7 @@ no-compress-man = true
|
|||||||
|
|
||||||
let spec = PackageSpec::from_file(&path).unwrap();
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
assert!(spec.build.flags.no_strip);
|
assert!(spec.build.flags.no_strip);
|
||||||
|
assert!(spec.build.flags.no_delete_static);
|
||||||
assert!(spec.build.flags.no_compress_man);
|
assert!(spec.build.flags.no_compress_man);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2128,10 +2162,11 @@ extract_dir = "foo"
|
|||||||
[build]
|
[build]
|
||||||
type = "autotools"
|
type = "autotools"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
build = ["make"]
|
build = ["make"]
|
||||||
test = ["python", "bats"]
|
test = ["python", "bats"]
|
||||||
"#,
|
optional = ["gtk-doc"]
|
||||||
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -2140,6 +2175,7 @@ test = ["python", "bats"]
|
|||||||
spec.dependencies.test,
|
spec.dependencies.test,
|
||||||
vec!["python".to_string(), "bats".to_string()]
|
vec!["python".to_string(), "bats".to_string()]
|
||||||
);
|
);
|
||||||
|
assert_eq!(spec.dependencies.optional, vec!["gtk-doc".to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2576,6 +2612,14 @@ pub struct BuildFlags {
|
|||||||
/// Disable automatic stripping of ELF files during staging.
|
/// Disable automatic stripping of ELF files during staging.
|
||||||
#[serde(default, alias = "no-strip")]
|
#[serde(default, alias = "no-strip")]
|
||||||
pub no_strip: bool,
|
pub no_strip: bool,
|
||||||
|
/// Disable automatic deletion of static libraries (`*.a`) during staging.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "no-delete-static",
|
||||||
|
alias = "no_remove_static",
|
||||||
|
alias = "no-remove-static"
|
||||||
|
)]
|
||||||
|
pub no_delete_static: bool,
|
||||||
/// Disable automatic zstd compression of man pages during staging.
|
/// Disable automatic zstd compression of man pages during staging.
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
@@ -2814,6 +2858,7 @@ impl Default for BuildFlags {
|
|||||||
keep: Vec::new(),
|
keep: Vec::new(),
|
||||||
no_flags: false,
|
no_flags: false,
|
||||||
no_strip: false,
|
no_strip: false,
|
||||||
|
no_delete_static: false,
|
||||||
no_compress_man: false,
|
no_compress_man: false,
|
||||||
skip_tests: false,
|
skip_tests: false,
|
||||||
build_32: false,
|
build_32: false,
|
||||||
@@ -2981,4 +3026,7 @@ pub struct Dependencies {
|
|||||||
/// Dependencies required to run package test suites.
|
/// Dependencies required to run package test suites.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub test: Vec<String>,
|
pub test: Vec<String>,
|
||||||
|
/// Optional runtime integrations that enhance functionality when installed.
|
||||||
|
#[serde(default)]
|
||||||
|
pub optional: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-6
@@ -8,7 +8,7 @@ use crate::config::Config;
|
|||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::deps;
|
use crate::deps;
|
||||||
use crate::index::PackageIndex;
|
use crate::index::PackageIndex;
|
||||||
use crate::package::{BuildType, PackageSpec};
|
use crate::package::PackageSpec;
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use petgraph::algo::toposort;
|
use petgraph::algo::toposort;
|
||||||
@@ -601,16 +601,14 @@ impl<'a> Resolver<'a> {
|
|||||||
|
|
||||||
fn source_deps_for_install(spec: &PackageSpec) -> Vec<String> {
|
fn source_deps_for_install(spec: &PackageSpec) -> Vec<String> {
|
||||||
let mut deps_all = Vec::new();
|
let mut deps_all = Vec::new();
|
||||||
|
let local_provides = spec.local_dependency_provides();
|
||||||
if !spec.is_metapackage() {
|
if !spec.is_metapackage() {
|
||||||
for dep in &spec.dependencies.build {
|
for dep in &spec.dependencies.build {
|
||||||
push_unique(&mut deps_all, dep.clone());
|
push_unique(&mut deps_all, dep.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for dep in &spec.dependencies.runtime {
|
for dep in &spec.dependencies.runtime {
|
||||||
push_unique(&mut deps_all, dep.clone());
|
if !local_provides.contains(deps::dep_name(dep)) {
|
||||||
}
|
|
||||||
if matches!(spec.build.build_type, BuildType::Autotools) {
|
|
||||||
for dep in &spec.dependencies.test {
|
|
||||||
push_unique(&mut deps_all, dep.clone());
|
push_unique(&mut deps_all, dep.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,7 +616,9 @@ fn source_deps_for_install(spec: &PackageSpec) -> Vec<String> {
|
|||||||
for out in spec.outputs() {
|
for out in spec.outputs() {
|
||||||
let deps = spec.dependencies_for_output(&out.name);
|
let deps = spec.dependencies_for_output(&out.name);
|
||||||
for dep in deps.runtime {
|
for dep in deps.runtime {
|
||||||
push_unique(&mut deps_all, dep);
|
if !local_provides.contains(deps::dep_name(&dep)) {
|
||||||
|
push_unique(&mut deps_all, dep);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deps_all
|
deps_all
|
||||||
@@ -710,3 +710,88 @@ pub(crate) fn build_dependency_install_plan(
|
|||||||
) -> Result<ExecutionPlan> {
|
) -> Result<ExecutionPlan> {
|
||||||
Resolver::new(config, rootfs, opts).plan_for_deps(deps_to_install)
|
Resolver::new(config, rootfs, opts).plan_for_deps(deps_to_install)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::source_deps_for_install;
|
||||||
|
use crate::package::{
|
||||||
|
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
|
||||||
|
};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn mk_spec() -> 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![PackageInfo {
|
||||||
|
name: "foo-libs".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "d".into(),
|
||||||
|
homepage: "h".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
}],
|
||||||
|
alternatives: Alternatives::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: vec![Source {
|
||||||
|
url: "https://example.test/foo.tar.gz".into(),
|
||||||
|
sha256: "skip".into(),
|
||||||
|
extract_dir: "foo".into(),
|
||||||
|
patches: Vec::new(),
|
||||||
|
post_extract: Vec::new(),
|
||||||
|
}],
|
||||||
|
build: Build {
|
||||||
|
build_type: BuildType::Autotools,
|
||||||
|
flags: BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: Dependencies {
|
||||||
|
build: vec!["make".into()],
|
||||||
|
runtime: vec!["foo-libs".into(), "zlib".into()],
|
||||||
|
test: vec!["bats".into()],
|
||||||
|
optional: vec!["docs-viewer".into()],
|
||||||
|
},
|
||||||
|
package_alternatives: BTreeMap::from([(
|
||||||
|
"foo-libs".into(),
|
||||||
|
Alternatives {
|
||||||
|
provides: vec!["libfoo".into()],
|
||||||
|
replaces: Vec::new(),
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
package_dependencies: BTreeMap::from([(
|
||||||
|
"foo".into(),
|
||||||
|
Dependencies {
|
||||||
|
build: Vec::new(),
|
||||||
|
runtime: vec!["foo-libs".into(), "libfoo".into(), "openssl".into()],
|
||||||
|
test: Vec::new(),
|
||||||
|
optional: Vec::new(),
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_deps_for_install_excludes_local_runtime_outputs_and_provides() {
|
||||||
|
let spec = mk_spec();
|
||||||
|
let deps = source_deps_for_install(&spec);
|
||||||
|
assert!(deps.contains(&"make".to_string()));
|
||||||
|
assert!(deps.contains(&"zlib".to_string()));
|
||||||
|
assert!(deps.contains(&"openssl".to_string()));
|
||||||
|
assert!(!deps.contains(&"foo-libs".to_string()));
|
||||||
|
assert!(!deps.contains(&"libfoo".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_deps_for_install_does_not_include_test_deps() {
|
||||||
|
let spec = mk_spec();
|
||||||
|
let deps = source_deps_for_install(&spec);
|
||||||
|
assert!(!deps.contains(&"bats".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+101
-8
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use crate::package::{PackageSpec, Source};
|
use crate::package::{PackageSpec, Source};
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
|
use filetime::FileTime;
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{Cursor, Read, Write};
|
use std::io::{Cursor, Read, Write};
|
||||||
@@ -605,20 +606,61 @@ fn move_dir_contents(src: &Path, dest: &Path) -> Result<()> {
|
|||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
let src_path = entry.path();
|
let src_path = entry.path();
|
||||||
let dest_path = dest.join(entry.file_name());
|
let dest_path = dest.join(entry.file_name());
|
||||||
|
let file_type = entry.file_type()?;
|
||||||
if fs::rename(&src_path, &dest_path).is_err() {
|
if fs::rename(&src_path, &dest_path).is_err() {
|
||||||
// fallback to copy-and-remove across filesystems
|
// fallback to copy-and-remove across filesystems
|
||||||
if src_path.is_dir() {
|
copy_entry_fallback(&src_path, &dest_path, file_type)?;
|
||||||
copy_dir_recursive_local(&src_path, &dest_path)?;
|
|
||||||
fs::remove_dir_all(&src_path)?;
|
|
||||||
} else {
|
|
||||||
fs::copy(&src_path, &dest_path)?;
|
|
||||||
fs::remove_file(&src_path)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn copy_entry_fallback(src_path: &Path, dest_path: &Path, file_type: fs::FileType) -> Result<()> {
|
||||||
|
if file_type.is_dir() {
|
||||||
|
copy_dir_recursive_local(src_path, dest_path)?;
|
||||||
|
fs::remove_dir_all(src_path)?;
|
||||||
|
} else if file_type.is_symlink() {
|
||||||
|
if let Some(parent) = dest_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let link_target = fs::read_link(src_path)
|
||||||
|
.with_context(|| format!("Failed to read symlink {}", src_path.display()))?;
|
||||||
|
unix_fs::symlink(&link_target, dest_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to create symlink {} -> {}",
|
||||||
|
dest_path.display(),
|
||||||
|
link_target.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
fs::remove_file(src_path)?;
|
||||||
|
} else {
|
||||||
|
copy_file_preserve_metadata(src_path, dest_path)?;
|
||||||
|
fs::remove_file(src_path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_file_preserve_metadata(src: &Path, dst: &Path) -> Result<()> {
|
||||||
|
if let Some(parent) = dst.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::copy(src, dst)
|
||||||
|
.with_context(|| format!("Failed to copy {} -> {}", src.display(), dst.display()))?;
|
||||||
|
|
||||||
|
let src_meta = fs::metadata(src)
|
||||||
|
.with_context(|| format!("Failed to read metadata for {}", src.display()))?;
|
||||||
|
fs::set_permissions(dst, src_meta.permissions())
|
||||||
|
.with_context(|| format!("Failed to set permissions for {}", dst.display()))?;
|
||||||
|
|
||||||
|
let atime = FileTime::from_last_access_time(&src_meta);
|
||||||
|
let mtime = FileTime::from_last_modification_time(&src_meta);
|
||||||
|
filetime::set_file_times(dst, atime, mtime)
|
||||||
|
.with_context(|| format!("Failed to preserve file timestamps for {}", dst.display()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
|
fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
|
||||||
for entry in WalkDir::new(src) {
|
for entry in WalkDir::new(src) {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
@@ -633,7 +675,7 @@ fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
|
|||||||
if let Some(parent) = target.parent() {
|
if let Some(parent) = target.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
fs::copy(entry.path(), &target)?;
|
copy_file_preserve_metadata(entry.path(), &target)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -643,6 +685,7 @@ fn copy_dir_recursive_local(src: &Path, dst: &Path) -> Result<()> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -690,6 +733,56 @@ mod tests {
|
|||||||
assert!(extract_dir.join("usr/bin/hello-deb").exists());
|
assert!(extract_dir.join("usr/bin/hello-deb").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_file_preserve_metadata_keeps_mtime() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let src = tmp.path().join("src");
|
||||||
|
let dst = tmp.path().join("dst");
|
||||||
|
std::fs::write(&src, b"hello").unwrap();
|
||||||
|
|
||||||
|
let fixed = SystemTime::UNIX_EPOCH + Duration::from_secs(946684800); // 2000-01-01 UTC
|
||||||
|
let ts = FileTime::from_system_time(fixed);
|
||||||
|
filetime::set_file_times(&src, ts, ts).unwrap();
|
||||||
|
|
||||||
|
copy_file_preserve_metadata(&src, &dst).unwrap();
|
||||||
|
let src_meta = std::fs::metadata(&src).unwrap();
|
||||||
|
let dst_meta = std::fs::metadata(&dst).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
FileTime::from_last_modification_time(&dst_meta),
|
||||||
|
FileTime::from_last_modification_time(&src_meta)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_entry_fallback_preserves_symlink_when_target_is_missing() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let src_dir = tmp.path().join("src");
|
||||||
|
let dst_dir = tmp.path().join("dst");
|
||||||
|
std::fs::create_dir_all(&src_dir).unwrap();
|
||||||
|
std::fs::create_dir_all(&dst_dir).unwrap();
|
||||||
|
|
||||||
|
let src_link = src_dir.join("RELEASE-NOTES");
|
||||||
|
std::os::unix::fs::symlink("doc/RelNotes/v1.47.3.txt", &src_link).unwrap();
|
||||||
|
|
||||||
|
// Simulate target already moved/removed before copying this symlink entry.
|
||||||
|
let src_target_dir = src_dir.join("doc");
|
||||||
|
std::fs::create_dir_all(src_target_dir.join("RelNotes")).unwrap();
|
||||||
|
std::fs::write(src_target_dir.join("RelNotes/v1.47.3.txt"), "notes").unwrap();
|
||||||
|
std::fs::remove_dir_all(&src_target_dir).unwrap();
|
||||||
|
|
||||||
|
let dst_link = dst_dir.join("RELEASE-NOTES");
|
||||||
|
let file_type = std::fs::symlink_metadata(&src_link).unwrap().file_type();
|
||||||
|
copy_entry_fallback(&src_link, &dst_link, file_type).unwrap();
|
||||||
|
|
||||||
|
assert!(std::fs::symlink_metadata(&src_link).is_err());
|
||||||
|
let dst_meta = std::fs::symlink_metadata(&dst_link).unwrap();
|
||||||
|
assert!(dst_meta.file_type().is_symlink());
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_link(&dst_link).unwrap(),
|
||||||
|
PathBuf::from("doc/RelNotes/v1.47.3.txt")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn write_cpio_newc_one_file(w: &mut Vec<u8>, name: &str, data: &[u8]) {
|
fn write_cpio_newc_one_file(w: &mut Vec<u8>, name: &str, data: &[u8]) {
|
||||||
// magic + 13 fields of 8 hex chars each
|
// magic + 13 fields of 8 hex chars each
|
||||||
fn h8(v: u64) -> String {
|
fn h8(v: u64) -> String {
|
||||||
|
|||||||
+1
-1
@@ -247,7 +247,7 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result<bool> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a blocking reqwest HTTP client using the platform-default TLS backend.
|
/// Build a blocking reqwest HTTP client using the configured Cargo TLS backend.
|
||||||
/// Any error building the client is returned directly (no fallback).
|
/// Any error building the client is returned directly (no fallback).
|
||||||
pub(crate) fn build_blocking_client(
|
pub(crate) fn build_blocking_client(
|
||||||
user_agent: &str,
|
user_agent: &str,
|
||||||
|
|||||||
+100
-5
@@ -240,6 +240,28 @@ fn auto_strip_elf_files(destdir: &Path) -> Result<usize> {
|
|||||||
Ok(stripped)
|
Ok(stripped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auto_delete_static_archives(destdir: &Path) -> Result<usize> {
|
||||||
|
let mut removed = 0usize;
|
||||||
|
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
|
||||||
|
if !entry.file_type().is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().is_none_or(|ext| ext != "a") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_file(path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to remove static library {} (disable with build.flags.no_delete_static = true)",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
removed += 1;
|
||||||
|
}
|
||||||
|
Ok(removed)
|
||||||
|
}
|
||||||
|
|
||||||
fn compress_manpages_zstd(destdir: &Path) -> Result<usize> {
|
fn compress_manpages_zstd(destdir: &Path) -> Result<usize> {
|
||||||
crate::log_info!("Compressing man pages with zstd...");
|
crate::log_info!("Compressing man pages with zstd...");
|
||||||
let mut man_files = Vec::new();
|
let mut man_files = Vec::new();
|
||||||
@@ -356,11 +378,11 @@ fn compress_manpages_zstd(destdir: &Path) -> Result<usize> {
|
|||||||
Ok(compressed)
|
Ok(compressed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process staged files - remove .la files, strip binaries, etc.
|
/// Process staged files - remove .la files/static libs, strip binaries, etc.
|
||||||
pub fn process(destdir: &Path, spec: &PackageSpec) -> Result<()> {
|
pub fn process(destdir: &Path, spec: &PackageSpec) -> Result<()> {
|
||||||
crate::log_info!("Processing staged files...");
|
crate::log_info!("Processing staged files...");
|
||||||
|
|
||||||
let mut removed_count = 0;
|
let mut removed_la_count = 0;
|
||||||
|
|
||||||
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
|
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
@@ -369,12 +391,23 @@ pub fn process(destdir: &Path, spec: &PackageSpec) -> Result<()> {
|
|||||||
if path.extension().map(|e| e == "la").unwrap_or(false) {
|
if path.extension().map(|e| e == "la").unwrap_or(false) {
|
||||||
crate::log_info!(" Removing: {}", path.display());
|
crate::log_info!(" Removing: {}", path.display());
|
||||||
fs::remove_file(path)?;
|
fs::remove_file(path)?;
|
||||||
removed_count += 1;
|
removed_la_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if removed_count > 0 {
|
if removed_la_count > 0 {
|
||||||
crate::log_info!("Removed {} .la file(s)", removed_count);
|
crate::log_info!("Removed {} .la file(s)", removed_la_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.build.flags.no_delete_static {
|
||||||
|
crate::log_info!(
|
||||||
|
"Skipping static library cleanup: disabled by build.flags.no_delete_static"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let removed_static = auto_delete_static_archives(destdir)?;
|
||||||
|
if removed_static > 0 {
|
||||||
|
crate::log_info!("Removed {} static library archive(s)", removed_static);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if spec.build.flags.no_strip {
|
if spec.build.flags.no_strip {
|
||||||
@@ -736,6 +769,68 @@ pub fn install_atomic(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec};
|
||||||
|
|
||||||
|
fn mk_spec_for_stage_processing() -> PackageSpec {
|
||||||
|
let mut flags = BuildFlags::default();
|
||||||
|
flags.no_strip = true;
|
||||||
|
flags.no_compress_man = true;
|
||||||
|
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: Default::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: Vec::new(),
|
||||||
|
build: Build {
|
||||||
|
build_type: BuildType::Custom,
|
||||||
|
flags,
|
||||||
|
},
|
||||||
|
dependencies: Dependencies::default(),
|
||||||
|
package_alternatives: Default::default(),
|
||||||
|
package_dependencies: Default::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_removes_static_archives_by_default() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/lib")).unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/lib/libfoo.a"), "static").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/lib/libfoo.la"), "libtool").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/lib/libfoo.so"), "shared").unwrap();
|
||||||
|
|
||||||
|
let spec = mk_spec_for_stage_processing();
|
||||||
|
process(&destdir, &spec).unwrap();
|
||||||
|
|
||||||
|
assert!(!destdir.join("usr/lib/libfoo.a").exists());
|
||||||
|
assert!(!destdir.join("usr/lib/libfoo.la").exists());
|
||||||
|
assert!(destdir.join("usr/lib/libfoo.so").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_preserves_static_archives_when_disabled() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/lib")).unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/lib/libfoo.a"), "static").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/lib/libfoo.la"), "libtool").unwrap();
|
||||||
|
|
||||||
|
let mut spec = mk_spec_for_stage_processing();
|
||||||
|
spec.build.flags.no_delete_static = true;
|
||||||
|
process(&destdir, &spec).unwrap();
|
||||||
|
|
||||||
|
assert!(destdir.join("usr/lib/libfoo.a").exists());
|
||||||
|
assert!(!destdir.join("usr/lib/libfoo.la").exists());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn add_licenses_copies_common_files() {
|
fn add_licenses_copies_common_files() {
|
||||||
|
|||||||
@@ -104,12 +104,7 @@ pub fn prompt_yes_no(prompt: &str, default_yes: bool) -> Result<bool> {
|
|||||||
|
|
||||||
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
||||||
loop {
|
loop {
|
||||||
print!(
|
print!("{prompt} [{default_hint}]: ");
|
||||||
"{} {} [{}] ",
|
|
||||||
label(Stream::Stdout, "?", "35"),
|
|
||||||
prompt,
|
|
||||||
default_hint
|
|
||||||
);
|
|
||||||
io::stdout()
|
io::stdout()
|
||||||
.flush()
|
.flush()
|
||||||
.context("Failed to flush prompt to stdout")?;
|
.context("Failed to flush prompt to stdout")?;
|
||||||
@@ -123,10 +118,25 @@ pub fn prompt_yes_no(prompt: &str, default_yes: bool) -> Result<bool> {
|
|||||||
return Ok(answer);
|
return Ok(answer);
|
||||||
}
|
}
|
||||||
|
|
||||||
warn("Please answer with 'y' or 'n'.");
|
warn("Invalid choice. Please answer with 'y' or 'n'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prompt for a package-oriented action with a Starpack-like layout.
|
||||||
|
pub fn prompt_package_action(action: &str, packages: &[String], default_yes: bool) -> Result<bool> {
|
||||||
|
if packages.is_empty() {
|
||||||
|
return prompt_yes_no(
|
||||||
|
&format!("No packages were selected for {action}. Continue?"),
|
||||||
|
default_yes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("The following packages will be processed for {}:", action);
|
||||||
|
println!(" {}", packages.join(" "));
|
||||||
|
prompt_yes_no("Proceed?", default_yes)
|
||||||
|
}
|
||||||
|
|
||||||
/// Prompt the user to choose one option by index.
|
/// Prompt the user to choose one option by index.
|
||||||
pub fn prompt_select_index(prompt: &str, options: &[String], default_idx: usize) -> Result<usize> {
|
pub fn prompt_select_index(prompt: &str, options: &[String], default_idx: usize) -> Result<usize> {
|
||||||
if options.is_empty() {
|
if options.is_empty() {
|
||||||
@@ -144,18 +154,17 @@ pub fn prompt_select_index(prompt: &str, options: &[String], default_idx: usize)
|
|||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
println!("{} {}", label(Stream::Stdout, "?", "35"), prompt);
|
println!("{}:", prompt);
|
||||||
for (idx, option) in options.iter().enumerate() {
|
for (idx, option) in options.iter().enumerate() {
|
||||||
println!(
|
println!(
|
||||||
" {} {}{}",
|
" {}) {}{}",
|
||||||
idx + 1,
|
idx + 1,
|
||||||
option,
|
option,
|
||||||
if idx == default_idx { " [default]" } else { "" }
|
if idx == default_idx { " [default]" } else { "" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
print!(
|
print!(
|
||||||
"{} Select an option [1-{}] (default {}): ",
|
"Choose option [1-{}] (Enter = {}): ",
|
||||||
label(Stream::Stdout, "?", "35"),
|
|
||||||
options.len(),
|
options.len(),
|
||||||
default_idx + 1
|
default_idx + 1
|
||||||
);
|
);
|
||||||
@@ -176,7 +185,10 @@ pub fn prompt_select_index(prompt: &str, options: &[String], default_idx: usize)
|
|||||||
{
|
{
|
||||||
return Ok(num - 1);
|
return Ok(num - 1);
|
||||||
}
|
}
|
||||||
warn("Please enter a valid option number.");
|
warn(format!(
|
||||||
|
"Invalid choice. Please enter a number between 1 and {}.",
|
||||||
|
options.len()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user