feat: Implement build state tracking and interactive package specification creation

- Added StateTracker to manage build steps and allow resuming interrupted builds.
- Updated post_extract function to utilize StateTracker for patch and command execution.
- Introduced makefile.rs to handle building and installing packages via Makefile.
- Created interactive.rs for an interactive package specification creator with SHA256 computation for source URLs.
- Enhanced checksum verification to support multiple algorithms (SHA256, SHA512, MD5).
- Added example configuration files in contrib for system-wide and user-level Depot configurations.
- Updated tests to cover new functionality and ensure correctness of state tracking and interactive creation.
This commit is contained in:
2026-02-16 21:38:05 -06:00
parent 00ca2ebac6
commit 9a00608104
30 changed files with 3501 additions and 434 deletions
+93 -13
View File
@@ -79,6 +79,12 @@ enum Commands {
},
/// Show current configuration
Config,
/// Create a new package specification interactively
MakeSpec {
/// Output file path (defaults to <name>.toml)
#[arg(short, long)]
output: Option<PathBuf>,
},
}
#[derive(Subcommand)]
@@ -89,6 +95,10 @@ enum RepoCommands {
#[arg(default_value = ".")]
dir: PathBuf,
},
/// Sync git mirrors configured in /etc/depot.d/mirrors.toml into /usr/src/depot
Sync,
/// Show status of configured git mirrors
Status,
}
fn main() -> Result<()> {
@@ -99,11 +109,27 @@ fn main() -> Result<()> {
spec_or_archive,
spec,
} => {
let spec_path = spec.unwrap_or(spec_or_archive);
println!("Installing package from: {}", spec_path.display());
let mut spec_path = spec.unwrap_or(spec_or_archive);
// Load configuration early so we can use the configured repo clone dir
let config = config::Config::for_rootfs(&cli.rootfs);
// Repo clone dir is available via `config.repo_clone_dir` and
// is passed explicitly to index builders below.
// If the provided path doesn't exist, treat it as a package name and
// try to locate a spec under configured repo dir or local packages/.
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()));
if let Some(found) = pkg_index.find(&name) {
spec_path = found;
}
}
println!("Installing package from: {}", spec_path.display());
let (pkg_spec, staging_dir): (package::PackageSpec, Option<tempfile::TempDir>) =
if spec_path.to_string_lossy().ends_with(".tar.zst") {
// Install from archive
@@ -177,6 +203,7 @@ fn main() -> Result<()> {
.unwrap_or("")
.to_string(),
},
packages: Vec::new(),
alternatives: package::Alternatives::default(),
manual_sources: Vec::new(),
source: Vec::new(),
@@ -228,9 +255,6 @@ fn main() -> Result<()> {
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!(
@@ -277,7 +301,7 @@ fn main() -> Result<()> {
if input == "y" || input == "yes" || input.is_empty() {
// Build package index for fast lookups
let pkg_index = index::PackageIndex::build();
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() {
@@ -374,9 +398,13 @@ fn main() -> Result<()> {
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);
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()?;
@@ -455,15 +483,24 @@ fn main() -> Result<()> {
staging::process(&destdir, &pkg_spec)?;
// Create package archive
let packager = package::Packager::new(pkg_spec, destdir.clone(), config);
// Create package archive(s) — support multiple outputs from a single spec.
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());
let mut created_files = Vec::new();
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 pkg_file = packager.create_package(Path::new("."), arch)?;
created_files.push(pkg_file);
}
for f in &created_files {
println!("Build complete. Package created: {}", f.display());
}
}
Commands::Info { package } => {
// Try as file first, then as installed package name
@@ -493,6 +530,27 @@ fn main() -> Result<()> {
let db_path = repo.create_repo_db()?;
println!("Created repository database: {}", db_path.display());
}
RepoCommands::Sync => {
// Only root may run sync
if !crate::fakeroot::is_root() {
anyhow::bail!("The 'repo sync' command must be run as root");
}
let config = config::Config::for_rootfs(&cli.rootfs);
if config.mirrors.is_empty() {
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());
}
}
RepoCommands::Status => {
let config = config::Config::for_rootfs(&cli.rootfs);
if config.mirrors.is_empty() {
println!("No mirrors configured in /etc/depot.d/mirrors.toml");
} else {
db::repo::mirrors_status(&config.repo_clone_dir, &config.mirrors)?;
}
}
},
Commands::Config => {
let config = config::Config::for_rootfs(&cli.rootfs);
@@ -508,6 +566,28 @@ fn main() -> Result<()> {
}
}
}
Commands::MakeSpec { output } => {
let spec = package::create_interactive()?;
// 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)));
if output_path.exists() {
println!(
"Warning: File {} already exists. Overwrite? [y/N]",
output_path.display()
);
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
anyhow::bail!("Aborted");
}
}
fs::write(&output_path, toml_string)?;
println!("Package specification saved to {}", output_path.display());
}
}
Ok(())