Refactor and enhance source management and installation processes

- Improved error handling and context messages in `checkout` function in `git.rs`.
- Updated license field in package specification to use a vector in `hooks.rs`.
- Enhanced `copy_manual_sources` function in `mod.rs` to support both local file and remote URL sources, including checksum verification.
- Added new utility functions for path normalization and skipping installation of specific files in `staging/mod.rs`.
- Implemented logic to handle existing files during installation, allowing for `.depotnew` suffix for kept files.
- Added tests for manual source copying, installation behavior, and path validation.
- Introduced a new LICENSE file with MIT License terms.
This commit is contained in:
2026-02-21 13:25:14 -06:00
parent af0571c33b
commit c9bed308e2
27 changed files with 3868 additions and 939 deletions
+188 -41
View File
@@ -17,6 +17,86 @@ use clap::{Parser, Subcommand};
use std::fs;
use std::path::{Path, PathBuf};
fn parse_licenses_from_toml(metadata: &toml::Value) -> Vec<String> {
if let Some(s) = metadata.get("license").and_then(|v| v.as_str()) {
return vec![s.to_string()];
}
if let Some(arr) = metadata.get("license").and_then(|v| v.as_array()) {
return arr
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
Vec::new()
}
fn clean_build_workspace(config: &config::Config) -> Result<()> {
if config.build_dir.exists() {
fs::remove_dir_all(&config.build_dir).with_context(|| {
format!("Failed to clean build dir: {}", config.build_dir.display())
})?;
println!("Cleaned build workspace: {}", config.build_dir.display());
}
Ok(())
}
fn warn_if_running_as_root_for_build(command: &str, rootfs: &Path) {
if crate::fakeroot::is_root() {
eprintln!(
"\x1b[33mWARNING: Running '{}' as root is discouraged.\x1b[0m",
command
);
eprintln!(
"\x1b[33mA misconfigured build environment or malicious/buggy build file can overwrite or delete critical system files.\x1b[0m"
);
eprintln!(
"\x1b[33mRecommendation: use a non-root build user and only install as root.\x1b[0m"
);
eprintln!("\x1b[33mCurrent rootfs target: {}\x1b[0m", rootfs.display());
}
}
fn install_staged_to_rootfs(
pkg_spec: &package::PackageSpec,
destdir: &Path,
rootfs: &Path,
config: &config::Config,
) -> Result<()> {
std::fs::create_dir_all(&config.db_dir).with_context(|| {
format!(
"Failed to create database directory: {}",
config.db_dir.display()
)
})?;
let db_path = config.db_dir.join("packages.db");
let new_files = staging::generate_manifest_with_dirs(destdir)?;
let remove_paths =
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_files.files)?;
let tx_base = config.build_dir.join("tx");
let tx = staging::install_atomic(
destdir,
rootfs,
&tx_base,
&remove_paths,
&pkg_spec.build.flags.keep,
)?;
for out in pkg_spec.outputs() {
let mut spec_for_out = pkg_spec.clone();
spec_for_out.package = out;
if let Err(e) = db::register_package(&db_path, &spec_for_out, destdir) {
let _ = tx.rollback();
return Err(e);
}
}
tx.commit()?;
Ok(())
}
#[derive(Parser)]
#[command(name = "Depot")]
#[command(about = "Depot - Source-based package manager for Linux", long_about = None)]
@@ -30,10 +110,25 @@ struct Cli {
#[arg(long, global = true)]
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"
)]
no_flags: bool,
/// Cross-compilation prefix (e.g., x86_64-linux-musl, aarch64-linux-gnu)
#[arg(long, global = true)]
cross_prefix: Option<String>,
/// Clean build workspace after successful install/build
#[arg(long, global = true)]
clean: bool,
#[command(subcommand)]
command: Commands,
}
@@ -64,6 +159,10 @@ enum Commands {
/// 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 {
@@ -109,6 +208,7 @@ fn main() -> Result<()> {
spec_or_archive,
spec,
} => {
warn_if_running_as_root_for_build("install", &cli.rootfs);
let mut spec_path = spec.unwrap_or(spec_or_archive);
// Load configuration early so we can use the configured repo clone dir
@@ -122,7 +222,8 @@ fn main() -> Result<()> {
if !spec_path.exists() {
let name = spec_path.to_string_lossy().to_string();
println!("Looking up package '{}' in local indexes...", name);
let pkg_index = index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
let pkg_index =
index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
if let Some(found) = pkg_index.find(&name) {
spec_path = found;
}
@@ -159,10 +260,6 @@ fn main() -> Result<()> {
);
}
// We need to parse the metadata.toml but we don't have a direct "from_metadata"
// Let's implement a minimal reconstruction or use the metadata to fill a spec.
// Actually, PackageSpec needs a lot of fields.
// Let's extract the WHOLE archive to a temporary staging dir and use it.
let file = fs::File::open(&spec_path)?;
let zstd_decoder = zstd::stream::read::Decoder::new(file)?;
let mut archive = tar::Archive::new(zstd_decoder);
@@ -197,11 +294,7 @@ fn main() -> Result<()> {
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
license: metadata
.get("license")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
license: parse_licenses_from_toml(&metadata),
},
packages: Vec::new(),
alternatives: package::Alternatives::default(),
@@ -225,6 +318,18 @@ fn main() -> Result<()> {
} else {
Vec::new()
},
test: if let Some(deps) = metadata
.get("dependencies")
.and_then(|v| v.get("test"))
.and_then(|v| v.as_array())
{
deps.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect()
} else {
Vec::new()
},
},
spec_dir: PathBuf::from("."),
};
@@ -266,6 +371,8 @@ fn main() -> Result<()> {
// Check dependencies and prompt for auto-install if needed
if !cli.no_deps {
let needs_test_deps =
matches!(pkg_spec.build.build_type, package::BuildType::Autotools);
deps::print_dep_status(&pkg_spec, &db_path)?;
// Collect all missing dependencies (build + runtime)
@@ -277,6 +384,14 @@ fn main() -> Result<()> {
missing.push(dep);
}
}
if needs_test_deps {
let missing_test = deps::check_test_deps(&pkg_spec, &db_path)?;
for dep in missing_test {
if !missing.contains(&dep) {
missing.push(dep);
}
}
}
if !missing.is_empty() {
// Check for dependency cycles via DEPOT_DEPCHAIN env var
@@ -301,7 +416,9 @@ fn main() -> Result<()> {
if input == "y" || input == "yes" || input.is_empty() {
// Build package index for fast lookups
let pkg_index = index::PackageIndex::build_with_repo_dir(Some(config.repo_clone_dir.clone()));
let pkg_index = index::PackageIndex::build_with_repo_dir(Some(
config.repo_clone_dir.clone(),
));
// Build new dep chain
let new_chain = if dep_chain.is_empty() {
@@ -324,9 +441,15 @@ fn main() -> Result<()> {
if cli.no_deps {
cmd.arg("--no-deps");
}
if cli.no_flags {
cmd.arg("--no-flags");
}
if let Some(ref p) = cli.cross_prefix {
cmd.arg("--cross-prefix").arg(p);
}
if cli.clean {
cmd.arg("--clean");
}
cmd.arg("install").arg(&dep_spec_path);
cmd.env("DEPOT_DEPCHAIN", &new_chain);
@@ -348,6 +471,9 @@ fn main() -> Result<()> {
// Enforce build dependencies (runtime deps are warnings only if not installed/prompt declined)
deps::require_build_deps(&pkg_spec, &db_path)?;
if needs_test_deps {
deps::require_test_deps(&pkg_spec, &db_path)?;
}
}
// Ensure database directory exists
@@ -377,7 +503,13 @@ fn main() -> Result<()> {
.as_ref()
.map(|p| cross::CrossConfig::from_prefix(p))
.transpose()?;
builder::build(&pkg_spec, &src_dir, &destdir, cross_config.as_ref())?;
builder::build(
&pkg_spec,
&src_dir,
&destdir,
cross_config.as_ref(),
!cli.no_flags,
)?;
// 3.1 Copy license files into staged tree
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
@@ -388,25 +520,8 @@ fn main() -> Result<()> {
// 4. Stage (clean .la files, etc.)
staging::process(&destdir, &pkg_spec)?;
// 5. Install/update to rootfs (atomic)
let new_files = staging::generate_manifest_with_dirs(&destdir)?;
let remove_paths =
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_files.files)?;
let tx_base = config.build_dir.join("tx");
let tx = staging::install_atomic(&destdir, &cli.rootfs, &tx_base, &remove_paths)?;
// 6. Register in database (rollback install on DB error)
for out in pkg_spec.outputs() {
let mut spec_for_out = pkg_spec.clone();
spec_for_out.package = out;
if let Err(e) = db::register_package(&db_path, &spec_for_out, &destdir) {
let _ = tx.rollback();
return Err(e);
}
}
tx.commit()?;
// 5-6. Install/update to rootfs and register in DB
install_staged_to_rootfs(&pkg_spec, &destdir, &cli.rootfs, &config)?;
// 7. Check runtime dependencies (warn only)
if !cli.no_deps {
@@ -427,6 +542,10 @@ fn main() -> Result<()> {
"Successfully installed {} v{}",
pkg_spec.package.name, pkg_spec.package.version
);
if cli.clean {
clean_build_workspace(&config)?;
}
}
Commands::Remove { package } => {
println!("Removing package: {}", package);
@@ -435,12 +554,12 @@ fn main() -> Result<()> {
db::remove_package(&db_path, &package, &cli.rootfs)?;
println!("Successfully removed {}", package);
}
Commands::Build { spec_pos, spec } => {
if crate::fakeroot::is_root() {
anyhow::bail!(
"The 'build' command must be run as a non-root user to ensure a clean build environment."
);
}
Commands::Build {
spec_pos,
spec,
install,
} => {
warn_if_running_as_root_for_build("build", &cli.rootfs);
let spec_path = spec.or(spec_pos).context("No spec file provided")?;
println!("Building package from: {}", spec_path.display());
let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?;
@@ -461,8 +580,13 @@ fn main() -> Result<()> {
// Check build dependencies
if !cli.no_deps {
let needs_test_deps =
matches!(pkg_spec.build.build_type, package::BuildType::Autotools);
deps::print_dep_status(&pkg_spec, &db_path)?;
deps::require_build_deps(&pkg_spec, &db_path)?;
if needs_test_deps {
deps::require_test_deps(&pkg_spec, &db_path)?;
}
}
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
@@ -477,7 +601,13 @@ fn main() -> Result<()> {
.as_ref()
.map(|p| cross::CrossConfig::from_prefix(p))
.transpose()?;
builder::build(&pkg_spec, &src_dir, &destdir, cross_config.as_ref())?;
builder::build(
&pkg_spec,
&src_dir,
&destdir,
cross_config.as_ref(),
!cli.no_flags,
)?;
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
@@ -493,7 +623,8 @@ fn main() -> Result<()> {
for out in pkg_spec.outputs() {
let mut spec_for_out = pkg_spec.clone();
spec_for_out.package = out;
let packager = package::Packager::new(spec_for_out.clone(), destdir.clone(), config.clone());
let packager =
package::Packager::new(spec_for_out.clone(), destdir.clone(), config.clone());
let pkg_file = packager.create_package(Path::new("."), arch)?;
created_files.push(pkg_file);
}
@@ -501,6 +632,18 @@ fn main() -> Result<()> {
for f in &created_files {
println!("Build complete. Package created: {}", f.display());
}
if install {
install_staged_to_rootfs(&pkg_spec, &destdir, &cli.rootfs, &config)?;
println!(
"Successfully installed {} v{}",
pkg_spec.package.name, pkg_spec.package.version
);
}
if cli.clean {
clean_build_workspace(&config)?;
}
}
Commands::Info { package } => {
// Try as file first, then as installed package name
@@ -540,7 +683,10 @@ fn main() -> Result<()> {
println!("No mirrors configured in /etc/depot.d/mirrors.toml");
} else {
db::repo::sync_mirrors(&config.repo_clone_dir, &config.mirrors)?;
println!("Mirrors synchronized into {}", config.repo_clone_dir.display());
println!(
"Mirrors synchronized into {}",
config.repo_clone_dir.display()
);
}
}
RepoCommands::Status => {
@@ -571,7 +717,8 @@ fn main() -> Result<()> {
// Produce a minimal TOML for interactive-created specs (omit defaults)
let toml_string = package::spec_to_minimal_toml(&spec)?;
let output_path = output.unwrap_or_else(|| PathBuf::from(format!("{}.toml", spec.package.name)));
let output_path =
output.unwrap_or_else(|| PathBuf::from(format!("{}.toml", spec.package.name)));
if output_path.exists() {
println!(