feat: enhance package dependency handling and signing process

- Updated `lib32_dependencies` method to ensure the package name is included in runtime dependencies if not already present.
- Modified the signing key loading process to allow for multiple password attempts, improving user experience during key access.
- Introduced functions for staging licenses of split packages, ensuring proper symlink creation for matching licenses and copying distinct ones.
- Added tests for verifying the correct functionality of manpage compression and license staging.
This commit is contained in:
2026-03-13 08:52:42 -05:00
parent 8d9e13ed71
commit 0c06be6b6b
9 changed files with 1130 additions and 399 deletions
Generated
+19 -19
View File
@@ -39,9 +39,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.21"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -60,9 +60,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
@@ -236,9 +236,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
"clap_derive",
@@ -246,9 +246,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
@@ -258,18 +258,18 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.66"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031"
checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
dependencies = [
"heck",
"proc-macro2",
@@ -285,9 +285,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clap_mangen"
version = "0.2.31"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301"
checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78"
dependencies = [
"clap",
"roff",
@@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]]
name = "depot"
version = "0.20.1"
version = "0.21.0"
dependencies = [
"anyhow",
"ar",
@@ -1606,9 +1606,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
@@ -1703,9 +1703,9 @@ dependencies = [
[[package]]
name = "roff"
version = "0.2.2"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad"
[[package]]
name = "rpassword"
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "depot"
version = "0.20.1"
version = "0.21.0"
edition = "2024"
[lints.rust]
@@ -10,7 +10,7 @@ warnings = "deny"
anyhow = "1.0.102"
ar = "0.9.0"
bzip2 = "0.6.1"
clap = { version = "4.5.60", features = ["derive"] }
clap = { version = "4.6.0", features = ["derive"] }
flate2 = "1.1.9"
git2 = "0.20.4"
indicatif = "0.18.4"
@@ -37,8 +37,8 @@ petgraph = "0.8.3"
fd-lock = "4.0.4"
reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "native-tls"] }
filetime = "0.2.27"
clap_complete = "4.5.66"
clap_mangen = "0.2.31"
clap_complete = "4.6.0"
clap_mangen = "0.2.33"
sys-mount = { version = "3.1.0", default-features = false }
time = { version = "0.3.47", features = ["formatting", "parsing"] }
b2sum-rust = "0.3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
project(
'depot',
version: '0.20.1',
version: '0.21.0',
meson_version: '>=0.60.0',
default_options: ['buildtype=release'],
)
+28
View File
@@ -110,9 +110,21 @@ pub fn build(
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo};
use crate::test_support::TestEnv;
use std::fs;
use tempfile::tempdir;
#[cfg(unix)]
fn write_executable(path: &std::path::Path, contents: &str) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
fs::write(path, contents)?;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms)?;
Ok(())
}
fn mk_spec(name: &str, version: &str) -> PackageSpec {
PackageSpec {
package: PackageInfo {
@@ -149,8 +161,24 @@ mod tests {
fn test_makefile_build_runs_commands() -> Result<()> {
let tmp_src = tempdir()?;
let tmp_dest = tempdir()?;
let tmp_tools = tempdir()?;
let src_path = tmp_src.path();
let dest_path = tmp_dest.path();
let tools_path = tmp_tools.path();
write_executable(
&tools_path.join("fakeroot"),
r#"#!/bin/sh
if [ "$1" = "--" ]; then
shift
fi
exec "$@"
"#,
)?;
let mut env = TestEnv::new();
let old_path = std::env::var("PATH").unwrap_or_default();
env.set_var("PATH", format!("{}:{}", tools_path.display(), old_path));
let mut spec = mk_spec("test-make", "1.0");
spec.build.flags.makefile_commands = vec![
+323 -101
View File
@@ -1,23 +1,29 @@
use clap::{Parser, Subcommand, ValueEnum};
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "Depot")]
#[command(about = "Source-based package manager for Linux", long_about = None)]
#[command(version)]
pub struct Cli {
#[derive(Debug, Clone, Args, Default)]
pub struct RootfsArgs {
/// Custom root filesystem path
#[arg(long, short = 'r', default_value = "/", global = true)]
#[arg(long, short = 'r', default_value = "/")]
pub rootfs: PathBuf,
}
#[derive(Debug, Clone, Args, Default)]
pub struct PromptArgs {
/// Automatically answer yes to prompts and pick the default provider choice
#[arg(long, short = 'y')]
pub yes: bool,
}
#[derive(Debug, Clone, Args, Default)]
pub struct BuildExecArgs {
/// Skip dependency checks
#[arg(long, global = true)]
#[arg(long)]
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,
@@ -26,44 +32,59 @@ pub struct Cli {
pub no_flags: bool,
/// Cross-compilation prefix (e.g., x86_64-linux-musl, aarch64-linux-gnu)
#[arg(long, global = true)]
#[arg(long)]
pub cross_prefix: Option<String>,
/// Clean build workspace and source cache after successful install/build
#[arg(long, global = true)]
#[arg(long)]
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)]
#[arg(long)]
pub dry_run: bool,
/// Install test dependencies alongside build/runtime dependencies
#[arg(long, global = true)]
#[arg(long)]
pub test_deps: bool,
}
#[derive(Debug, Clone, Args, Default)]
pub struct Lib32Args {
/// Build/install only the lib32 companion package path (skip primary package output)
#[arg(long, global = true)]
#[arg(long)]
pub lib32_only: bool,
}
#[derive(Parser)]
#[command(name = "Depot")]
#[command(about = "Source-based package manager for Linux", long_about = None)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Build and install a package from a spec file
Install {
#[derive(Debug, Clone, Args)]
pub struct InstallArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
#[command(flatten)]
pub prompt_args: PromptArgs,
#[command(flatten)]
pub build_exec_args: BuildExecArgs,
#[command(flatten)]
pub lib32_args: Lib32Args,
/// One or more package names, spec paths (.toml), or package archives (.tar.zst)
#[arg(
value_name = "SPEC_OR_ARCHIVE",
num_args = 1..,
required_unless_present = "spec"
)]
spec_or_archive: Vec<PathBuf>,
pub spec_or_archive: Vec<PathBuf>,
/// Explicitly specify path to package spec (.toml file)
#[arg(
@@ -73,101 +94,194 @@ pub enum Commands {
alias = "p",
conflicts_with = "spec_or_archive"
)]
spec: Option<PathBuf>,
},
/// Remove an installed package
Remove {
pub spec: Option<PathBuf>,
}
#[derive(Debug, Clone, Args)]
pub struct RemoveArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
#[command(flatten)]
pub prompt_args: PromptArgs,
/// Package name to remove
package: String,
},
/// Build a package without installing
Build {
pub package: String,
}
#[derive(Debug, Clone, Args)]
pub struct BuildArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
#[command(flatten)]
pub prompt_args: PromptArgs,
#[command(flatten)]
pub build_exec_args: BuildExecArgs,
#[command(flatten)]
pub lib32_args: Lib32Args,
/// Path to package spec (.toml file)
#[arg(value_name = "SPEC")]
spec_pos: Option<PathBuf>,
pub spec_pos: Option<PathBuf>,
/// Explicitly specify path to package spec (.toml file)
#[arg(short, long = "spec", visible_alias = "package", alias = "p")]
spec: Option<PathBuf>,
pub spec: Option<PathBuf>,
/// Install package to rootfs after creating package archive(s)
#[arg(long)]
install: bool,
pub install: bool,
/// Automatically install missing dependencies before building
#[arg(long)]
install_deps: bool,
pub install_deps: bool,
/// Remove dependencies auto-installed for this build after the command finishes
#[arg(long)]
cleanup_deps: bool,
},
/// Update installed packages from configured repositories
Update {
pub cleanup_deps: bool,
}
#[derive(Debug, Clone, Args)]
pub struct UpdateArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
#[command(flatten)]
pub prompt_args: PromptArgs,
#[command(flatten)]
pub build_exec_args: BuildExecArgs,
/// Optional package names to update (defaults to all installed packages with upgrades)
#[arg(value_name = "PACKAGE", num_args = 0..)]
packages: Vec<String>,
},
/// Scan package specs for upstream version updates
Check {
pub packages: Vec<String>,
}
#[derive(Debug, Clone, Args)]
pub struct CheckArgs {
/// Directory to scan recursively for package specs
#[arg(default_value = ".")]
dir: PathBuf,
},
/// Show information about a package
Info {
pub dir: PathBuf,
}
#[derive(Debug, Clone, Args)]
pub struct InfoArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
/// Path to package spec or installed package name
package: String,
},
/// Search configured source and binary repos by package name or provides
Search {
pub package: String,
}
#[derive(Debug, Clone, Args)]
pub struct SearchArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
/// Search query
query: String,
pub 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 {
pub files: bool,
}
#[derive(Debug, Clone, Args)]
pub struct OwnsArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
/// Path to query (absolute or relative to rootfs)
path: PathBuf,
},
/// List installed packages
List,
/// Create a detached minisign signature for a .zst file
Sign {
pub path: PathBuf,
}
#[derive(Debug, Clone, Args)]
pub struct ListArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
}
#[derive(Debug, Clone, Args)]
pub struct SignArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
/// One or more .zst files to sign
#[arg(value_name = "FILE", required = true, num_args = 1..)]
files: Vec<PathBuf>,
},
/// Repository management
Repo {
pub files: Vec<PathBuf>,
}
#[derive(Debug, Clone, Args)]
pub struct RepoArgs {
#[command(subcommand)]
command: RepoCommands,
},
/// Show current configuration
Config,
/// Generate shell completion scripts and a man page into an output directory.
#[command(hide = true)]
GenerateArtifacts {
pub command: RepoCommands,
}
#[derive(Debug, Clone, Args)]
pub struct ConfigArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
}
#[derive(Debug, Clone, Args)]
pub struct GenerateArtifactsArgs {
/// Output directory for generated files
#[arg(long, value_name = "DIR")]
out_dir: PathBuf,
},
/// Create a new package specification interactively
MakeSpec {
pub out_dir: PathBuf,
}
#[derive(Debug, Clone, Args)]
pub struct MakeSpecArgs {
/// Output file path (defaults to <name>.toml)
#[arg(short, long)]
output: Option<PathBuf>,
},
#[command(hide = true)]
Internal {
pub output: Option<PathBuf>,
}
#[derive(Debug, Clone, Args)]
pub struct InternalArgs {
#[command(subcommand)]
command: InternalCommands,
},
pub command: InternalCommands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Build and install a package from a spec file
Install(InstallArgs),
/// Remove an installed package
Remove(RemoveArgs),
/// Build a package without installing
Build(BuildArgs),
/// Update installed packages from configured repositories
Update(UpdateArgs),
/// Scan package specs for upstream version updates
Check(CheckArgs),
/// Show information about a package
Info(InfoArgs),
/// Search configured source and binary repos by package name or provides
Search(SearchArgs),
/// Show which installed package owns a filesystem path
Owns(OwnsArgs),
/// List installed packages
List(ListArgs),
/// Create a detached minisign signature for a .zst file
Sign(SignArgs),
/// Repository management
Repo(RepoArgs),
/// Show current configuration
Config(ConfigArgs),
/// Generate shell completion scripts and a man page into an output directory.
#[command(hide = true)]
GenerateArtifacts(GenerateArtifactsArgs),
/// Create a new package specification interactively
MakeSpec(MakeSpecArgs),
#[command(hide = true)]
Internal(InternalArgs),
}
#[derive(Subcommand, Debug, Clone)]
pub enum InternalCommands {
#[command(hide = true)]
PythonBuild {
@@ -189,23 +303,38 @@ pub enum InternalCommands {
},
}
#[derive(Subcommand)]
#[derive(Debug, Clone, Args)]
pub struct RepoCommonArgs {
#[command(flatten)]
pub rootfs_args: RootfsArgs,
}
#[derive(Subcommand, Debug, Clone)]
pub enum RepoCommands {
/// Create a repository database from a directory of packages
Create {
#[command(flatten)]
args: RepoCommonArgs,
/// 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 {
#[command(flatten)]
args: RepoCommonArgs,
},
/// Sync source repos configured in /etc/depot.d/repos.toml into /usr/src/depot
Update {
#[command(flatten)]
args: RepoCommonArgs,
/// Update only one source repo by name
name: Option<String>,
},
/// Create/update a source index at the root of a source repo
Index {
#[command(flatten)]
args: RepoCommonArgs,
/// Source repository root directory
#[arg(default_value = ".")]
dir: PathBuf,
@@ -214,9 +343,14 @@ pub enum RepoCommands {
subdirs: Vec<String>,
},
/// List configured source and binary repos
List,
List {
#[command(flatten)]
args: RepoCommonArgs,
},
/// Add or update a repo entry in /etc/depot.d/repos.toml
Add {
#[command(flatten)]
args: RepoCommonArgs,
/// Repo name (e.g. vertex)
name: String,
/// Source git URL or binary repo base URL
@@ -245,6 +379,8 @@ pub enum RepoCommands {
},
/// Remove a repo entry from /etc/depot.d/repos.toml
Remove {
#[command(flatten)]
args: RepoCommonArgs,
/// Repo name
name: String,
/// Repo kind (auto-detect if unique)
@@ -253,6 +389,8 @@ pub enum RepoCommands {
},
/// Enable a repo entry in /etc/depot.d/repos.toml
Enable {
#[command(flatten)]
args: RepoCommonArgs,
/// Repo name
name: String,
/// Repo kind (auto-detect if unique)
@@ -261,6 +399,8 @@ pub enum RepoCommands {
},
/// Disable a repo entry in /etc/depot.d/repos.toml
Disable {
#[command(flatten)]
args: RepoCommonArgs,
/// Repo name
name: String,
/// Repo kind (auto-detect if unique)
@@ -269,11 +409,16 @@ pub enum RepoCommands {
},
/// Query binary repo metadata for the package that owns a file path
Owns {
#[command(flatten)]
args: RepoCommonArgs,
/// Path to query (absolute or relative install path)
path: PathBuf,
},
/// Show status of configured git mirrors
Status,
Status {
#[command(flatten)]
args: RepoCommonArgs,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
@@ -284,18 +429,21 @@ pub enum RepoKindArg {
#[cfg(test)]
mod tests {
use super::{Cli, Commands};
use clap::Parser;
use super::{
BuildArgs, Cli, Commands, InstallArgs, RepoArgs, RepoCommands, SearchArgs, UpdateArgs,
};
use clap::{CommandFactory, Parser};
use std::path::PathBuf;
#[test]
fn install_accepts_multiple_positional_targets() {
let cli = Cli::try_parse_from(["depot", "install", "base", "linux"]).unwrap();
match cli.command {
Commands::Install {
Commands::Install(InstallArgs {
spec_or_archive,
spec,
} => {
..
}) => {
assert!(spec.is_none());
assert_eq!(
spec_or_archive,
@@ -310,10 +458,11 @@ mod tests {
fn install_accepts_spec_flag_without_positional_target() {
let cli = Cli::try_parse_from(["depot", "install", "--spec", "pkg.toml"]).unwrap();
match cli.command {
Commands::Install {
Commands::Install(InstallArgs {
spec_or_archive,
spec,
} => {
..
}) => {
assert!(spec_or_archive.is_empty());
assert_eq!(spec, Some(PathBuf::from("pkg.toml")));
}
@@ -325,7 +474,7 @@ mod tests {
fn update_accepts_no_package_names() {
let cli = Cli::try_parse_from(["depot", "update"]).unwrap();
match cli.command {
Commands::Update { packages } => assert!(packages.is_empty()),
Commands::Update(UpdateArgs { packages, .. }) => assert!(packages.is_empty()),
_ => panic!("expected update command"),
}
}
@@ -334,7 +483,7 @@ mod tests {
fn update_accepts_multiple_package_names() {
let cli = Cli::try_parse_from(["depot", "update", "linux", "openssl"]).unwrap();
match cli.command {
Commands::Update { packages } => {
Commands::Update(UpdateArgs { packages, .. }) => {
assert_eq!(packages, vec!["linux".to_string(), "openssl".to_string()])
}
_ => panic!("expected update command"),
@@ -345,22 +494,25 @@ mod tests {
fn check_accepts_custom_directory() {
let cli = Cli::try_parse_from(["depot", "check", "packages"]).unwrap();
match cli.command {
Commands::Check { dir } => assert_eq!(dir, PathBuf::from("packages")),
Commands::Check(args) => assert_eq!(args.dir, PathBuf::from("packages")),
_ => panic!("expected check command"),
}
}
#[test]
fn global_test_deps_flag_is_parsed() {
let cli = Cli::try_parse_from(["depot", "--test-deps", "install", "foo"]).unwrap();
assert!(cli.test_deps);
fn install_test_deps_flag_is_parsed_for_install() {
let cli = Cli::try_parse_from(["depot", "install", "--test-deps", "foo"]).unwrap();
match cli.command {
Commands::Install(args) => assert!(args.build_exec_args.test_deps),
_ => panic!("expected install command"),
}
}
#[test]
fn build_install_deps_flag_is_parsed() {
let cli = Cli::try_parse_from(["depot", "build", "--install-deps", "pkg.toml"]).unwrap();
match cli.command {
Commands::Build { install_deps, .. } => assert!(install_deps),
Commands::Build(BuildArgs { install_deps, .. }) => assert!(install_deps),
_ => panic!("expected build command"),
}
}
@@ -369,8 +521,78 @@ mod tests {
fn build_cleanup_deps_flag_is_parsed() {
let cli = Cli::try_parse_from(["depot", "build", "--cleanup-deps", "pkg.toml"]).unwrap();
match cli.command {
Commands::Build { cleanup_deps, .. } => assert!(cleanup_deps),
Commands::Build(BuildArgs { cleanup_deps, .. }) => assert!(cleanup_deps),
_ => panic!("expected build command"),
}
}
#[test]
fn repo_help_does_not_show_build_only_flags() {
let mut cmd = Cli::command();
let help = cmd
.find_subcommand_mut("repo")
.expect("repo subcommand")
.render_help()
.to_string();
assert!(!help.contains("--no-deps"));
assert!(!help.contains("--no-flags"));
assert!(!help.contains("--clean"));
assert!(!help.contains("--test-deps"));
assert!(!help.contains("--lib32-only"));
}
#[test]
fn repo_create_help_only_shows_repo_options() {
let mut cmd = Cli::command();
let repo = cmd.find_subcommand_mut("repo").expect("repo subcommand");
let help = repo
.find_subcommand_mut("create")
.expect("repo create subcommand")
.render_help()
.to_string();
assert!(help.contains("--rootfs"));
assert!(!help.contains("--no-deps"));
assert!(!help.contains("--test-deps"));
}
#[test]
fn search_help_shows_only_search_options() {
let mut cmd = Cli::command();
let help = cmd
.find_subcommand_mut("search")
.expect("search subcommand")
.render_help()
.to_string();
assert!(help.contains("--rootfs"));
assert!(help.contains("--files"));
assert!(!help.contains("--no-deps"));
}
#[test]
fn repo_command_parses_nested_subcommand() {
let cli = Cli::try_parse_from(["depot", "repo", "status"]).unwrap();
match cli.command {
Commands::Repo(RepoArgs {
command: RepoCommands::Status { .. },
}) => {}
_ => panic!("expected repo status command"),
}
}
#[test]
fn search_parses_rootfs_locally() {
let cli = Cli::try_parse_from(["depot", "search", "-r", "/tmp/root", "llvm"]).unwrap();
match cli.command {
Commands::Search(SearchArgs {
rootfs_args, query, ..
}) => {
assert_eq!(rootfs_args.rootfs, PathBuf::from("/tmp/root"));
assert_eq!(query, "llvm");
}
_ => panic!("expected search command"),
}
}
}
+375 -206
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -258,9 +258,14 @@ impl PackageSpec {
/// Return the effective dependency set used by the generated lib32 companion package.
pub fn lib32_dependencies(&self) -> Dependencies {
self.dependencies
let mut deps = self
.dependencies
.lib32_dependencies()
.unwrap_or_else(|| self.dependencies.primary_dependencies())
.unwrap_or_else(|| self.dependencies.primary_dependencies());
if !deps.runtime.iter().any(|dep| dep == &self.package.name) {
deps.runtime.push(self.package.name.clone());
}
deps
}
/// Return local package names/provided features for the selected output set.
@@ -1873,7 +1878,7 @@ test = ["bats"]
);
assert_eq!(
spec.dependencies_for_output("lib32-llvm").runtime,
vec!["lib32-zlib".to_string()]
vec!["lib32-zlib".to_string(), "llvm".to_string()]
);
assert_eq!(
spec.dependencies_for_output("lib32-llvm").test,
+95 -31
View File
@@ -10,6 +10,7 @@ use std::path::{Path, PathBuf};
const PUBLIC_KEYS_DIR_REL: &str = "usr/share/depot/keys/public";
const SIGN_KEYS_DIR_REL: &str = "usr/share/depot/keys/sign";
const SIGNING_PASSWORD_ENV: &str = "DEPOT_MINISIGN_PASSWORD";
const MAX_SIGNING_PASSWORD_ATTEMPTS: usize = 3;
#[derive(Debug, Clone, Default)]
pub struct KeyLocations {
@@ -210,9 +211,13 @@ fn load_signing_material(keys: &KeyLocations) -> Result<SigningMaterial> {
}
fn load_secret_key(signing_key_path: &Path, secret_key_box: SecretKeyBox) -> Result<SecretKey> {
load_secret_key_with_password_override(signing_key_path, secret_key_box, None)
match secret_key_box.clone().into_unencrypted_secret_key() {
Ok(sk) => Ok(sk),
Err(_) => load_secret_key_interactive(signing_key_path, secret_key_box),
}
}
#[cfg(test)]
fn load_secret_key_with_password_override(
signing_key_path: &Path,
secret_key_box: SecretKeyBox,
@@ -223,7 +228,7 @@ fn load_secret_key_with_password_override(
Err(_) => {
let password = match password_override {
Some(password) => password,
None => signing_key_password(signing_key_path)?,
None => signing_key_password(signing_key_path, 1)?,
};
secret_key_box
.into_secret_key(Some(password))
@@ -237,9 +242,19 @@ fn load_secret_key_with_password_override(
}
}
fn signing_key_password(signing_key_path: &Path) -> Result<String> {
fn load_secret_key_interactive(
signing_key_path: &Path,
secret_key_box: SecretKeyBox,
) -> Result<SecretKey> {
if let Some(password) = std::env::var_os(SIGNING_PASSWORD_ENV) {
return Ok(password.to_string_lossy().into_owned());
return secret_key_box
.into_secret_key(Some(password.to_string_lossy().into_owned()))
.with_context(|| {
format!(
"Failed to load minisign signing key: {}",
signing_key_path.display()
)
});
}
if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
anyhow::bail!(
@@ -249,9 +264,40 @@ fn signing_key_password(signing_key_path: &Path) -> Result<String> {
);
}
Password::new(&format!(
"Minisign password for {}:",
let mut last_error = None;
for attempt in 1..=MAX_SIGNING_PASSWORD_ATTEMPTS {
let password = signing_key_password(signing_key_path, attempt)?;
match secret_key_box.clone().into_secret_key(Some(password)) {
Ok(secret_key) => return Ok(secret_key),
Err(err) => {
last_error = Some(err);
if attempt < MAX_SIGNING_PASSWORD_ATTEMPTS {
crate::log_warn!(
"Invalid minisign password for {} ({}/{})",
signing_key_path.display(),
attempt,
MAX_SIGNING_PASSWORD_ATTEMPTS
);
}
}
}
}
Err(last_error.expect("last minisign password error should exist")).with_context(|| {
format!(
"Failed to load minisign signing key after {} attempt(s): {}",
MAX_SIGNING_PASSWORD_ATTEMPTS,
signing_key_path.display()
)
})
}
fn signing_key_password(signing_key_path: &Path, attempt: usize) -> Result<String> {
Password::new(&format!(
"Minisign password for {} ({}/{}):",
signing_key_path.display(),
attempt,
MAX_SIGNING_PASSWORD_ATTEMPTS
))
.without_confirmation()
.prompt()
@@ -344,7 +390,14 @@ pub fn verify_zst_file_detached_with_public_key(
/// Sign one or more `.zst` files with detached minisign signatures written to `<file>.sig`.
pub fn sign_zst_files_detached(rootfs: &Path, inputs: &[PathBuf]) -> Result<Vec<PathBuf>> {
if inputs.is_empty() {
let signable_inputs = collect_signable_inputs(inputs, true)?;
let keys = locate_keys(rootfs)?;
let signing_material = load_signing_material(&keys)?;
sign_signable_inputs_detached(&signable_inputs, &signing_material)
}
fn collect_signable_inputs(inputs: &[PathBuf], require_any: bool) -> Result<Vec<&PathBuf>> {
if inputs.is_empty() && require_any {
anyhow::bail!("No input files provided for signing");
}
@@ -364,40 +417,51 @@ pub fn sign_zst_files_detached(rootfs: &Path, inputs: &[PathBuf]) -> Result<Vec<
}
signable_inputs.push(input);
}
if signable_inputs.is_empty() {
if signable_inputs.is_empty() && require_any {
anyhow::bail!("No signable .zst files were provided");
}
let keys = locate_keys(rootfs)?;
let signing_material = load_signing_material(&keys)?;
let mut sig_paths = Vec::with_capacity(signable_inputs.len());
for input in signable_inputs {
let sig_path = detached_sig_path(input);
sign_detached_with_material(input, &sig_path, &signing_material)?;
sig_paths.push(sig_path);
Ok(signable_inputs)
}
fn sign_signable_inputs_detached(
signable_inputs: &[&PathBuf],
signing_material: &SigningMaterial,
) -> Result<Vec<PathBuf>> {
let mut sig_paths = Vec::with_capacity(signable_inputs.len());
for input in signable_inputs {
let sig_path = detached_sig_path(input);
sign_detached_with_material(input, &sig_path, signing_material)?;
sig_paths.push(sig_path);
}
Ok(sig_paths)
}
/// Attempt to sign one or more `.zst` files using discovered keys; skip if no signing key exists.
pub fn auto_sign_zst_files_detached(rootfs: &Path, inputs: &[PathBuf]) -> Result<Vec<PathBuf>> {
let signable_inputs = collect_signable_inputs(inputs, false)?;
if signable_inputs.is_empty() {
return Ok(Vec::new());
}
let keys = locate_keys(rootfs)?;
if keys.signing_key.is_none() {
crate::log_info!("No minisign signing key found; skipping detached signatures");
return Ok(Vec::new());
}
let signing_material = load_signing_material(&keys)?;
sign_signable_inputs_detached(&signable_inputs, &signing_material)
}
/// Attempt to sign a `.zst` file using discovered keys; skip if no signing key exists.
pub fn auto_sign_zst_file_detached(rootfs: &Path, input: &Path) -> Result<Option<PathBuf>> {
if !is_zst_file(input) {
return Ok(None);
}
let keys = locate_keys(rootfs)?;
if keys.signing_key.is_none() {
crate::log_info!(
"No minisign signing key found; skipping detached signature for {}",
input.display()
);
return Ok(None);
}
let signing_material = load_signing_material(&keys)?;
let sig_path = detached_sig_path(input);
sign_detached_with_material(input, &sig_path, &signing_material)?;
Ok(Some(sig_path))
Ok(
auto_sign_zst_files_detached(rootfs, &[input.to_path_buf()])?
.into_iter()
.next(),
)
}
#[cfg(test)]
+248 -5
View File
@@ -203,6 +203,30 @@ fn has_known_compressed_suffix(path: &str) -> bool {
|| lower.ends_with(".z")
}
fn logical_payload_rel_path(destdir: &Path, path: &Path) -> Result<Option<String>> {
let rel = path
.strip_prefix(destdir)
.context("Failed to strip destdir prefix during path normalization")?;
let output_root = Path::new(INTERNAL_OUTPUTS_DIR);
if !rel.starts_with(output_root) {
let rel = rel.to_string_lossy().to_string();
if rel.is_empty() {
return Ok(None);
}
return Ok(Some(rel));
}
let mut comps = rel.components();
let _internal = comps.next();
let _outputs = comps.next();
let _package = comps.next();
let logical = comps.as_path();
if logical.as_os_str().is_empty() {
return Ok(None);
}
Ok(Some(logical.to_string_lossy().to_string()))
}
fn is_manpage_rel_path(rel_path: &str) -> bool {
let rel = rel_path.trim_start_matches('/');
rel.starts_with("usr/share/man/") && !has_known_compressed_suffix(rel)
@@ -497,11 +521,9 @@ fn compress_manpages_zstd(destdir: &Path) -> Result<usize> {
for entry in WalkDir::new(destdir).into_iter().filter_map(|e| e.ok()) {
let path = entry.path().to_path_buf();
let rel = path
.strip_prefix(destdir)
.context("Failed to strip destdir prefix during manpage compression")?
.to_string_lossy()
.to_string();
let Some(rel) = logical_payload_rel_path(destdir, &path)? else {
continue;
};
if !is_manpage_rel_path(&rel) {
continue;
}
@@ -606,6 +628,149 @@ fn compress_manpages_zstd(destdir: &Path) -> Result<usize> {
Ok(compressed)
}
fn normalized_licenses(licenses: &[String]) -> Vec<String> {
let mut normalized: Vec<String> = licenses
.iter()
.map(|license| license.trim().to_string())
.collect();
normalized.sort();
normalized.dedup();
normalized
}
fn licenses_match(left: &[String], right: &[String]) -> bool {
normalized_licenses(left) == normalized_licenses(right)
}
fn supplemental_output_license_targets(
spec: &PackageSpec,
destdir: &Path,
) -> Vec<(crate::package::PackageInfo, PathBuf)> {
let declared_names: HashSet<String> = spec
.outputs()
.into_iter()
.map(|output| output.name)
.collect();
let mut targets = Vec::new();
let mut seen = HashSet::new();
for output in spec.outputs() {
if output.name != spec.package.name {
let out_destdir = output_staging_dir(destdir, &output.name);
if out_destdir.exists() && seen.insert(output.name.clone()) {
targets.push((output.clone(), out_destdir));
}
}
if !spec.build.flags.split_docs || output.name.ends_with("-docs") {
continue;
}
let docs_pkg = spec.docs_package_for_output(&output);
if declared_names.contains(&docs_pkg.name) && !seen.contains(&docs_pkg.name) {
let docs_destdir = output_staging_dir(destdir, &docs_pkg.name);
if docs_destdir.exists() && seen.insert(docs_pkg.name.clone()) {
targets.push((docs_pkg, docs_destdir));
}
continue;
}
let docs_destdir = output_staging_dir(destdir, &docs_pkg.name);
if docs_destdir.exists() && seen.insert(docs_pkg.name.clone()) {
targets.push((docs_pkg, docs_destdir));
}
}
targets
}
/// Stage license payloads for split package outputs.
///
/// Outputs that share the primary package license reuse the primary license directory via symlink.
/// Outputs with distinct metadata licenses receive copied license files from `src_dir`.
pub fn stage_split_package_licenses(
src_dir: &Path,
destdir: &Path,
spec: &PackageSpec,
) -> Result<()> {
let primary_license_dir = destdir.join("usr/share/licenses").join(&spec.package.name);
if !primary_license_dir.exists() {
return Ok(());
}
for (output, output_destdir) in supplemental_output_license_targets(spec, destdir) {
if licenses_match(&output.license, &spec.package.license) {
symlink_package_license(&output_destdir, &output.name, &spec.package.name)?;
} else {
add_licenses(src_dir, &output_destdir, &output.name)?;
}
}
Ok(())
}
/// Stage a package license directory as a symlink to another package's license directory.
pub fn symlink_package_license(destdir: &Path, pkgname: &str, target_pkgname: &str) -> Result<()> {
let licenses_dir = destdir.join("usr/share/licenses");
fs::create_dir_all(&licenses_dir)
.with_context(|| format!("Failed to create {}", licenses_dir.display()))?;
let link_path = licenses_dir.join(pkgname);
match link_path.symlink_metadata() {
Ok(metadata) => {
if metadata.file_type().is_symlink()
&& fs::read_link(&link_path)? == Path::new(target_pkgname)
{
return Ok(());
}
if metadata.file_type().is_dir() {
fs::remove_dir_all(&link_path).with_context(|| {
format!(
"Failed to remove existing license dir {}",
link_path.display()
)
})?;
} else {
fs::remove_file(&link_path).with_context(|| {
format!(
"Failed to remove existing license path {}",
link_path.display()
)
})?;
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err).with_context(|| {
format!(
"Failed to inspect existing license path {}",
link_path.display()
)
});
}
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(target_pkgname, &link_path).with_context(|| {
format!(
"Failed to create package license symlink {} -> {}",
link_path.display(),
target_pkgname
)
})?;
Ok(())
}
#[cfg(not(unix))]
{
anyhow::bail!(
"Package license symlinks are only supported on unix hosts: {}",
link_path.display()
);
}
}
/// Process staged files - remove .la files/static libs, strip binaries, etc.
pub fn process(destdir: &Path, spec: &PackageSpec) -> Result<()> {
crate::log_info!("Processing staged files...");
@@ -1132,6 +1297,7 @@ pub fn install_atomic(
mod tests {
use super::*;
use crate::package::{Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec};
use std::io::Read;
fn mk_spec_for_stage_processing() -> PackageSpec {
let flags = BuildFlags {
@@ -1283,6 +1449,83 @@ mod tests {
assert!(!lic_dir.join("README").exists());
}
#[test]
fn compress_manpages_zstd_detects_split_output_payload_paths() {
let tmp = tempfile::tempdir().unwrap();
let dest = tmp.path().join("dest");
let page = output_staging_dir(&dest, "clang").join("usr/share/man/man1/clang.1");
std::fs::create_dir_all(page.parent().unwrap()).unwrap();
std::fs::write(&page, b"clang manpage\n").unwrap();
let count = compress_manpages_zstd(&dest).unwrap();
assert_eq!(count, 1);
assert!(!page.exists());
let compressed = page.with_extension("1.zst");
assert!(compressed.exists());
let encoded = std::fs::read(&compressed).unwrap();
let decoded = zstd::stream::decode_all(std::io::Cursor::new(encoded)).unwrap();
assert_eq!(String::from_utf8(decoded).unwrap(), "clang manpage\n");
}
#[test]
fn stage_split_package_licenses_symlinks_matching_outputs_and_copies_distinct_ones() {
let tmp = tempfile::tempdir().unwrap();
let src_dir = tmp.path().join("src");
let destdir = tmp.path().join("dest");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::create_dir_all(&destdir).unwrap();
std::fs::write(src_dir.join("LICENSE"), "license text").unwrap();
add_licenses(&src_dir, &destdir, "foo").unwrap();
let mut spec = mk_spec_for_stage_processing();
spec.packages.push(PackageInfo {
name: "foo-dev".into(),
version: "1.0".into(),
revision: 1,
description: "dev".into(),
homepage: "h".into(),
license: vec!["MIT".into()],
});
spec.packages.push(PackageInfo {
name: "foo-extras".into(),
version: "1.0".into(),
revision: 1,
description: "extras".into(),
homepage: "h".into(),
license: vec!["Apache-2.0".into()],
});
let dev_dest = output_staging_dir(&destdir, "foo-dev").join("usr/bin");
let extras_dest = output_staging_dir(&destdir, "foo-extras").join("usr/bin");
std::fs::create_dir_all(&dev_dest).unwrap();
std::fs::create_dir_all(&extras_dest).unwrap();
std::fs::write(dev_dest.join("foo-dev"), "bin").unwrap();
std::fs::write(extras_dest.join("foo-extras"), "bin").unwrap();
stage_split_package_licenses(&src_dir, &destdir, &spec).unwrap();
let dev_license =
output_staging_dir(&destdir, "foo-dev").join("usr/share/licenses/foo-dev");
let dev_meta = std::fs::symlink_metadata(&dev_license).unwrap();
assert!(dev_meta.file_type().is_symlink());
assert_eq!(
std::fs::read_link(&dev_license).unwrap(),
PathBuf::from("foo")
);
let extras_license =
output_staging_dir(&destdir, "foo-extras").join("usr/share/licenses/foo-extras");
let extras_meta = std::fs::symlink_metadata(&extras_license).unwrap();
assert!(extras_meta.is_dir());
let mut text = String::new();
std::fs::File::open(extras_license.join("LICENSE"))
.unwrap()
.read_to_string(&mut text)
.unwrap();
assert_eq!(text, "license text");
}
#[test]
fn install_atomic_update_and_rollback_restores_state() {
let tmp = tempfile::tempdir().unwrap();