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:
Generated
+19
-19
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
project(
|
||||
'depot',
|
||||
version: '0.20.1',
|
||||
version: '0.21.0',
|
||||
meson_version: '>=0.60.0',
|
||||
default_options: ['buildtype=release'],
|
||||
)
|
||||
|
||||
@@ -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![
|
||||
|
||||
+353
-131
@@ -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,148 +32,256 @@ 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(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"
|
||||
)]
|
||||
pub spec_or_archive: Vec<PathBuf>,
|
||||
|
||||
/// Explicitly specify path to package spec (.toml file)
|
||||
#[arg(
|
||||
short,
|
||||
long = "spec",
|
||||
visible_alias = "package",
|
||||
alias = "p",
|
||||
conflicts_with = "spec_or_archive"
|
||||
)]
|
||||
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
|
||||
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")]
|
||||
pub spec_pos: Option<PathBuf>,
|
||||
|
||||
/// Explicitly specify path to package spec (.toml file)
|
||||
#[arg(short, long = "spec", visible_alias = "package", alias = "p")]
|
||||
pub spec: Option<PathBuf>,
|
||||
|
||||
/// Install package to rootfs after creating package archive(s)
|
||||
#[arg(long)]
|
||||
pub install: bool,
|
||||
|
||||
/// Automatically install missing dependencies before building
|
||||
#[arg(long)]
|
||||
pub install_deps: bool,
|
||||
|
||||
/// Remove dependencies auto-installed for this build after the command finishes
|
||||
#[arg(long)]
|
||||
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..)]
|
||||
pub packages: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct CheckArgs {
|
||||
/// Directory to scan recursively for package specs
|
||||
#[arg(default_value = ".")]
|
||||
pub dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct InfoArgs {
|
||||
#[command(flatten)]
|
||||
pub rootfs_args: RootfsArgs,
|
||||
|
||||
/// Path to package spec or installed package name
|
||||
pub package: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct SearchArgs {
|
||||
#[command(flatten)]
|
||||
pub rootfs_args: RootfsArgs,
|
||||
|
||||
/// Search query
|
||||
pub query: String,
|
||||
|
||||
/// Search repository file lists (binary repo metadata) by path substring
|
||||
#[arg(long)]
|
||||
pub files: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct OwnsArgs {
|
||||
#[command(flatten)]
|
||||
pub rootfs_args: RootfsArgs,
|
||||
|
||||
/// Path to query (absolute or relative to rootfs)
|
||||
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..)]
|
||||
pub files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct RepoArgs {
|
||||
#[command(subcommand)]
|
||||
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")]
|
||||
pub out_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct MakeSpecArgs {
|
||||
/// Output file path (defaults to <name>.toml)
|
||||
#[arg(short, long)]
|
||||
pub output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct InternalArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: InternalCommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Build and install a package from a spec file
|
||||
Install {
|
||||
/// 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>,
|
||||
|
||||
/// Explicitly specify path to package spec (.toml file)
|
||||
#[arg(
|
||||
short,
|
||||
long = "spec",
|
||||
visible_alias = "package",
|
||||
alias = "p",
|
||||
conflicts_with = "spec_or_archive"
|
||||
)]
|
||||
spec: Option<PathBuf>,
|
||||
},
|
||||
Install(InstallArgs),
|
||||
/// Remove an installed package
|
||||
Remove {
|
||||
/// Package name to remove
|
||||
package: String,
|
||||
},
|
||||
Remove(RemoveArgs),
|
||||
/// 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,
|
||||
|
||||
/// Automatically install missing dependencies before building
|
||||
#[arg(long)]
|
||||
install_deps: bool,
|
||||
|
||||
/// Remove dependencies auto-installed for this build after the command finishes
|
||||
#[arg(long)]
|
||||
cleanup_deps: bool,
|
||||
},
|
||||
Build(BuildArgs),
|
||||
/// Update installed packages from configured repositories
|
||||
Update {
|
||||
/// Optional package names to update (defaults to all installed packages with upgrades)
|
||||
#[arg(value_name = "PACKAGE", num_args = 0..)]
|
||||
packages: Vec<String>,
|
||||
},
|
||||
Update(UpdateArgs),
|
||||
/// Scan package specs for upstream version updates
|
||||
Check {
|
||||
/// Directory to scan recursively for package specs
|
||||
#[arg(default_value = ".")]
|
||||
dir: PathBuf,
|
||||
},
|
||||
Check(CheckArgs),
|
||||
/// Show information about a package
|
||||
Info {
|
||||
/// Path to package spec or installed package name
|
||||
package: String,
|
||||
},
|
||||
Info(InfoArgs),
|
||||
/// 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,
|
||||
},
|
||||
Search(SearchArgs),
|
||||
/// Show which installed package owns a filesystem path
|
||||
Owns {
|
||||
/// Path to query (absolute or relative to rootfs)
|
||||
path: PathBuf,
|
||||
},
|
||||
Owns(OwnsArgs),
|
||||
/// List installed packages
|
||||
List,
|
||||
List(ListArgs),
|
||||
/// Create a detached minisign signature for a .zst file
|
||||
Sign {
|
||||
/// One or more .zst files to sign
|
||||
#[arg(value_name = "FILE", required = true, num_args = 1..)]
|
||||
files: Vec<PathBuf>,
|
||||
},
|
||||
Sign(SignArgs),
|
||||
/// Repository management
|
||||
Repo {
|
||||
#[command(subcommand)]
|
||||
command: RepoCommands,
|
||||
},
|
||||
Repo(RepoArgs),
|
||||
/// Show current configuration
|
||||
Config,
|
||||
Config(ConfigArgs),
|
||||
/// Generate shell completion scripts and a man page into an output directory.
|
||||
#[command(hide = true)]
|
||||
GenerateArtifacts {
|
||||
/// Output directory for generated files
|
||||
#[arg(long, value_name = "DIR")]
|
||||
out_dir: PathBuf,
|
||||
},
|
||||
GenerateArtifacts(GenerateArtifactsArgs),
|
||||
/// Create a new package specification interactively
|
||||
MakeSpec {
|
||||
/// Output file path (defaults to <name>.toml)
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
MakeSpec(MakeSpecArgs),
|
||||
#[command(hide = true)]
|
||||
Internal {
|
||||
#[command(subcommand)]
|
||||
command: InternalCommands,
|
||||
},
|
||||
Internal(InternalArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+377
-208
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -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,
|
||||
|
||||
+92
-28
@@ -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> {
|
||||
);
|
||||
}
|
||||
|
||||
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()
|
||||
"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());
|
||||
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)?;
|
||||
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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user