feat: initial implementation of Depot package manager

- Implement source-based package management with support for archives and Git
- Add multi-system build support (Autotools, CMake, Meson, Rust/Cargo, Custom)
- Implement atomic installation logic using transactions and fakeroot
- Add dependency resolution for build-time and runtime requirements
- Implement package indexing and local repository management
- Add comprehensive configuration system with system/package overrides
- Include Project guidelines (AGENTS.md) and README.md
This commit is contained in:
2026-02-16 11:35:23 -06:00
parent 94044a48c3
commit 00ca2ebac6
29 changed files with 9385 additions and 69 deletions
Executable
+514
View File
@@ -0,0 +1,514 @@
//! Depot - Not Your Average Package Manager
//! A source-based package manager for Linux
mod builder;
mod config;
mod cross;
mod db;
mod deps;
mod fakeroot;
mod index;
mod package;
mod source;
mod staging;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(name = "Depot")]
#[command(about = "Depot - Source-based package manager for Linux", long_about = None)]
#[command(version)]
struct Cli {
/// Custom root filesystem path
#[arg(long, short = 'r', default_value = "/", global = true)]
rootfs: PathBuf,
/// Skip dependency checks
#[arg(long, global = true)]
no_deps: bool,
/// Cross-compilation prefix (e.g., x86_64-linux-musl, aarch64-linux-gnu)
#[arg(long, global = true)]
cross_prefix: Option<String>,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Build and install a package from a spec file
Install {
/// Path to package spec (.toml) or package archive (.tar.zst)
#[arg(value_name = "SPEC_OR_ARCHIVE")]
spec_or_archive: PathBuf,
/// Explicitly specify path to package spec (.toml file)
#[arg(short, long = "spec", visible_alias = "package", alias = "p")]
spec: Option<PathBuf>,
},
/// Remove an installed package
Remove {
/// Package name to remove
package: String,
},
/// Build a package without installing
Build {
/// Path to package spec (.toml file)
#[arg(value_name = "SPEC")]
spec_pos: Option<PathBuf>,
/// Explicitly specify path to package spec (.toml file)
#[arg(short, long = "spec", visible_alias = "package", alias = "p")]
spec: Option<PathBuf>,
},
/// Show information about a package
Info {
/// Path to package spec or installed package name
package: String,
},
/// List installed packages
List,
/// Repository management
Repo {
#[command(subcommand)]
command: RepoCommands,
},
/// Show current configuration
Config,
}
#[derive(Subcommand)]
enum RepoCommands {
/// Create a repository database from a directory of packages
Create {
/// Directory containing .depot.pkg.tar.zst files
#[arg(default_value = ".")]
dir: PathBuf,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Install {
spec_or_archive,
spec,
} => {
let spec_path = spec.unwrap_or(spec_or_archive);
println!("Installing package from: {}", spec_path.display());
let config = config::Config::for_rootfs(&cli.rootfs);
let (pkg_spec, staging_dir): (package::PackageSpec, Option<tempfile::TempDir>) =
if spec_path.to_string_lossy().ends_with(".tar.zst") {
// Install from archive
println!("Detected package archive: {}", spec_path.display());
let tmp_dir = tempfile::TempDir::new()?;
let extract_dir = tmp_dir.path().to_path_buf();
// Extract metadata.toml first to get spec
let file = fs::File::open(&spec_path)?;
let zstd_decoder = zstd::stream::read::Decoder::new(file)?;
let mut archive = tar::Archive::new(zstd_decoder);
let mut metadata_content = String::new();
for entry in archive.entries()? {
let mut entry = entry?;
if entry.path()?.to_string_lossy() == ".metadata.toml" {
use std::io::Read;
entry.read_to_string(&mut metadata_content)?;
break;
}
}
if metadata_content.is_empty() {
anyhow::bail!(
"Package archive does not contain .metadata.toml: {}",
spec_path.display()
);
}
// 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);
archive.unpack(&extract_dir)?;
let metadata: toml::Value = toml::from_str(&metadata_content)?;
// Create a minimal spec from metadata
let mut spec = package::PackageSpec {
package: package::PackageInfo {
name: metadata
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
version: metadata
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
revision: metadata
.get("revision")
.and_then(|v| v.as_integer())
.unwrap_or(1) as u32,
description: metadata
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
homepage: metadata
.get("homepage")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
license: metadata
.get("license")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
},
alternatives: package::Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
build: package::Build {
build_type: package::BuildType::Bin,
flags: package::BuildFlags::default(),
},
dependencies: package::Dependencies {
build: Vec::new(),
runtime: if let Some(deps) = metadata
.get("dependencies")
.and_then(|v| v.get("runtime"))
.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("."),
};
if let Some(provides) = metadata.get("provides").and_then(|v| v.as_array()) {
spec.alternatives.provides = provides
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect();
}
(spec, Some(tmp_dir))
} else {
// Install from spec (normal build)
let mut pkg_spec = package::PackageSpec::from_file(&spec_path)?;
pkg_spec.apply_config(&config);
// ... existing build logic ...
// Jump to the part where build actually happens.
// To keep the code clean, I'll move the build/stage logic into a helper or similar?
// Actually, I'll just structure it so we can skip build if staging_dir is Some.
(pkg_spec, None)
};
println!(
"Package: {} v{}-{}",
pkg_spec.package.name, pkg_spec.package.version, pkg_spec.package.revision
);
// Get config relative to rootfs
let config = config::Config::for_rootfs(&cli.rootfs);
// Ensure database directory exists
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");
// Check dependencies and prompt for auto-install if needed
if !cli.no_deps {
deps::print_dep_status(&pkg_spec, &db_path)?;
// Collect all missing dependencies (build + runtime)
let mut missing = deps::check_build_deps(&pkg_spec, &db_path)?;
let missing_runtime = deps::check_runtime_deps(&pkg_spec, &db_path)?;
for dep in missing_runtime {
if !missing.contains(&dep) {
missing.push(dep);
}
}
if !missing.is_empty() {
// Check for dependency cycles via DEPOT_DEPCHAIN env var
let dep_chain = std::env::var("DEPOT_DEPCHAIN").unwrap_or_default();
let chain_set: std::collections::HashSet<&str> =
dep_chain.split(',').filter(|s| !s.is_empty()).collect();
if chain_set.contains(pkg_spec.package.name.as_str()) {
anyhow::bail!(
"Dependency cycle detected! {} is already in chain: {}",
pkg_spec.package.name,
dep_chain
);
}
println!("\nMissing dependencies: {}", missing.join(", "));
println!("Do you want to attempt to install them? [Y/n] ");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let input = input.trim().to_lowercase();
if input == "y" || input == "yes" || input.is_empty() {
// Build package index for fast lookups
let pkg_index = index::PackageIndex::build();
// Build new dep chain
let new_chain = if dep_chain.is_empty() {
pkg_spec.package.name.clone()
} else {
format!("{},{}", dep_chain, pkg_spec.package.name)
};
// Attempt to install missing deps
for dep in missing {
// Use package index for O(1) lookup
let candidate = pkg_index.find(&dep);
if let Some(dep_spec_path) = candidate {
println!("Installing dependency: {}...", dep);
let mut cmd = std::process::Command::new(std::env::current_exe()?);
cmd.arg("-r").arg(&cli.rootfs);
if cli.no_deps {
cmd.arg("--no-deps");
}
if let Some(ref p) = cli.cross_prefix {
cmd.arg("--cross-prefix").arg(p);
}
cmd.arg("install").arg(&dep_spec_path);
cmd.env("DEPOT_DEPCHAIN", &new_chain);
let status = cmd.status()?;
if !status.success() {
anyhow::bail!("Failed to install dependency: {}", dep);
}
} else {
anyhow::bail!(
"Could not find package spec for dependency: {}",
dep
);
}
}
}
}
// Enforce build dependencies (runtime deps are warnings only if not installed/prompt declined)
deps::require_build_deps(&pkg_spec, &db_path)?;
}
// Ensure database directory exists
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 destdir = if let Some(dir) = &staging_dir {
dir.path().to_path_buf()
} else {
// 1-2. Fetch + extract sources (supports archives and git URL#rev)
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
// 3. Build
let destdir = config
.build_dir
.join("destdir")
.join(&pkg_spec.package.name);
// Build with optional cross-compilation
let cross_config = cli
.cross_prefix
.as_ref()
.map(|p| cross::CrossConfig::from_prefix(p))
.transpose()?;
builder::build(&pkg_spec, &src_dir, &destdir, cross_config.as_ref())?;
// 3.1 Copy license files into staged tree
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
destdir
};
// 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)
if let Err(e) = db::register_package(&db_path, &pkg_spec, &destdir) {
let _ = tx.rollback();
return Err(e);
}
tx.commit()?;
// 7. Check runtime dependencies (warn only)
if !cli.no_deps {
let missing_runtime = deps::check_runtime_deps(&pkg_spec, &db_path)?;
if !missing_runtime.is_empty() {
eprintln!(
"\x1b[33mWarning: Missing runtime dependencies: {}\x1b[0m",
missing_runtime.join(", ")
);
eprintln!(
"\x1b[33mContinuing without runtime deps; binaries may not run correctly.\x1b[0m"
);
eprintln!("\x1b[33mUse --no-deps to suppress this warning.\x1b[0m");
}
}
println!(
"Successfully installed {} v{}",
pkg_spec.package.name, pkg_spec.package.version
);
}
Commands::Remove { package } => {
println!("Removing package: {}", package);
let config = config::Config::for_rootfs(&cli.rootfs);
let db_path = config.db_dir.join("packages.db");
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."
);
}
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)?;
let config = config::Config::for_rootfs(&cli.rootfs);
// Apply system overrides
pkg_spec.apply_config(&config);
// Ensure database directory exists
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");
// Check build dependencies
if !cli.no_deps {
deps::print_dep_status(&pkg_spec, &db_path)?;
deps::require_build_deps(&pkg_spec, &db_path)?;
}
let src_dir = source::prepare(&pkg_spec, &config.cache_dir, &config.build_dir)?;
let destdir = config
.build_dir
.join("destdir")
.join(&pkg_spec.package.name);
// Build with optional cross-compilation
let cross_config = cli
.cross_prefix
.as_ref()
.map(|p| cross::CrossConfig::from_prefix(p))
.transpose()?;
builder::build(&pkg_spec, &src_dir, &destdir, cross_config.as_ref())?;
staging::add_licenses(&src_dir, &destdir, &pkg_spec.package.name)?;
staging::process(&destdir, &pkg_spec)?;
// Create package archive
let packager = package::Packager::new(pkg_spec, destdir.clone(), config);
let arch = cli
.cross_prefix
.as_deref()
.unwrap_or(std::env::consts::ARCH);
let pkg_file = packager.create_package(Path::new("."), arch)?;
println!("Build complete. Package created: {}", pkg_file.display());
}
Commands::Info { package } => {
// Try as file first, then as installed package name
let path = PathBuf::from(&package);
if path.exists() {
let pkg_spec = package::PackageSpec::from_file(&path)?;
println!("{}", pkg_spec);
// Also show dependency status
let config = config::Config::for_rootfs(&cli.rootfs);
let db_path = config.db_dir.join("packages.db");
deps::print_dep_status(&pkg_spec, &db_path)?;
} else {
let config = config::Config::for_rootfs(&cli.rootfs);
let db_path = config.db_dir.join("packages.db");
db::show_package_info(&db_path, &package)?;
}
}
Commands::List => {
let config = config::Config::for_rootfs(&cli.rootfs);
let db_path = config.db_dir.join("packages.db");
db::list_packages(&db_path)?;
}
Commands::Repo { command } => match command {
RepoCommands::Create { dir } => {
let repo = db::repo::RepoManager::new(dir);
let db_path = repo.create_repo_db()?;
println!("Created repository database: {}", db_path.display());
}
},
Commands::Config => {
let config = config::Config::for_rootfs(&cli.rootfs);
println!("Cache Directory: {}", config.cache_dir.display());
println!("Build Directory: {}", config.build_dir.display());
println!("Database Directory: {}", config.db_dir.display());
println!("\nBuild Overrides: {}", config.build_overrides);
println!("Package Overrides: {}", config.package_overrides);
if !config.appends.is_empty() {
println!("\nAppends:");
for (k, v) in &config.appends {
println!(" {} = {:?}", k, v);
}
}
}
}
Ok(())
}