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:
+177
-9
@@ -69,9 +69,66 @@ pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> R
|
||||
)?;
|
||||
}
|
||||
|
||||
// Insert files
|
||||
// Detect ownership conflicts and separate into auto-removable vs fatal.
|
||||
let mut fatal_conflicts: Vec<(String, String)> = Vec::new();
|
||||
let mut auto_conflicts: Vec<(String, String)> = Vec::new();
|
||||
|
||||
for file in &manifest.files {
|
||||
let owner_res: rusqlite::Result<String> = tx.query_row(
|
||||
"SELECT p.name FROM files f JOIN packages p ON f.package_id = p.id WHERE f.path = ?1",
|
||||
params![file],
|
||||
|row| row.get(0),
|
||||
);
|
||||
if let Ok(owner) = owner_res {
|
||||
if owner != spec.package.name {
|
||||
if is_auto_removable_path(file) {
|
||||
auto_conflicts.push((file.clone(), owner));
|
||||
} else {
|
||||
fatal_conflicts.push((file.clone(), owner));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !fatal_conflicts.is_empty() {
|
||||
let mut msg = String::from("File ownership conflict detected:\n");
|
||||
for (f, owner) in &fatal_conflicts {
|
||||
msg.push_str(&format!(" {} -> owned by {}\n", f, owner));
|
||||
}
|
||||
anyhow::bail!("{}", msg);
|
||||
}
|
||||
|
||||
// For auto-removable conflicts, remove previous DB ownership entries and
|
||||
// attempt to delete the on-disk file if we can infer the rootfs.
|
||||
if !auto_conflicts.is_empty() {
|
||||
let rootfs_opt = detect_rootfs_from_db_path(db_path);
|
||||
for (f, owner) in &auto_conflicts {
|
||||
// Remove DB row(s) marking the previous owner for this path
|
||||
tx.execute(
|
||||
"DELETE FROM files WHERE path = ?1 AND package_id = (SELECT id FROM packages WHERE name = ?2)",
|
||||
params![f, owner],
|
||||
)?;
|
||||
|
||||
if let Some(rootfs) = &rootfs_opt {
|
||||
let disk_path = rootfs.join(f);
|
||||
if disk_path.exists() {
|
||||
let _ = std::fs::remove_file(&disk_path);
|
||||
println!(
|
||||
"Auto-removed conflicting path: {} (was owned by {})",
|
||||
f, owner
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"Auto-cleared DB ownership for path: {} (previously owned by {})",
|
||||
f, owner
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert files (no fatal conflicts remain)
|
||||
for file in &manifest.files {
|
||||
// Enforce single-owner semantics for file paths.
|
||||
tx.execute(
|
||||
"INSERT INTO files (package_id, path) VALUES (?1, ?2)",
|
||||
params![pkg_id, file],
|
||||
@@ -103,13 +160,20 @@ pub fn get_package_files(db_path: &Path, name: &str) -> Result<Vec<String>> {
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
let pkg_id: i64 = conn
|
||||
.query_row(
|
||||
"SELECT id FROM packages WHERE name = ?1",
|
||||
params![name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.context(format!("Package '{}' not found", name))?;
|
||||
|
||||
// If the package is not present in the DB (fresh install), treat that as
|
||||
// "no previously installed files" rather than an error.
|
||||
let pkg_id_res: rusqlite::Result<i64> = conn.query_row(
|
||||
"SELECT id FROM packages WHERE name = ?1",
|
||||
params![name],
|
||||
|row| row.get(0),
|
||||
);
|
||||
|
||||
let pkg_id = match pkg_id_res {
|
||||
Ok(id) => id,
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(Vec::new()),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
let mut stmt = conn.prepare("SELECT path FROM files WHERE package_id = ?1")?;
|
||||
let files: Vec<String> = stmt
|
||||
@@ -340,6 +404,40 @@ fn init_db(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decide whether a conflicting path is safe to auto-remove ownership for.
|
||||
/// This covers shared index files (e.g. `usr/share/info/dir`) and common
|
||||
/// language-shared trees such as Perl site/vendor/lib directories.
|
||||
fn is_auto_removable_path(path: &str) -> bool {
|
||||
let p = path.trim_start_matches('/');
|
||||
|
||||
// Exact info index file (and compressed variants)
|
||||
if p == "usr/share/info/dir" || p.starts_with("usr/share/info/dir.") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Perl shared trees (common multi-package locations)
|
||||
if p.starts_with("usr/lib/perl")
|
||||
|| p.starts_with("usr/share/perl")
|
||||
|| p.starts_with("usr/lib/perl5")
|
||||
|| p.starts_with("usr/share/perl5")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Attempt to infer the rootfs path from the database path. If the DB path
|
||||
/// follows the standard layout `<rootfs>/var/lib/depot/packages.db`, return
|
||||
/// the `<rootfs>` portion. Otherwise return None.
|
||||
fn detect_rootfs_from_db_path(db_path: &Path) -> Option<std::path::PathBuf> {
|
||||
// Look for the suffix `var/lib/depot/packages.db` and return the ancestor 4 levels up
|
||||
if db_path.ends_with(std::path::Path::new("var/lib/depot/packages.db")) {
|
||||
return db_path.ancestors().nth(4).map(|p| p.to_path_buf());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Calculate which files need to be removed during an upgrade.
|
||||
/// Returns paths that exist in the old version but NOT in the new version.
|
||||
pub fn calculate_upgrade_paths(
|
||||
@@ -379,6 +477,7 @@ mod tests {
|
||||
homepage: "h".into(),
|
||||
license: "MIT".into(),
|
||||
},
|
||||
packages: Vec::new(),
|
||||
alternatives: Alternatives {
|
||||
provides: vec![format!("{}-virtual", name)],
|
||||
replaces: Vec::new(),
|
||||
@@ -448,6 +547,75 @@ mod tests {
|
||||
assert_eq!(version.as_deref(), Some("2.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_package_detects_conflicting_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
|
||||
// Install package 'alpha' owning usr/bin/shared
|
||||
let spec_a = mk_spec("alpha", "1.0");
|
||||
let dest_a = tmp.path().join("dest_a");
|
||||
std::fs::create_dir_all(dest_a.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest_a.join("usr/bin/shared"), "a").unwrap();
|
||||
register_package(&db_path, &spec_a, &dest_a).unwrap();
|
||||
|
||||
// Try to install package 'beta' that also includes the same path -> should fail
|
||||
let spec_b = mk_spec("beta", "1.0");
|
||||
let dest_b = tmp.path().join("dest_b");
|
||||
std::fs::create_dir_all(dest_b.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest_b.join("usr/bin/shared"), "b").unwrap();
|
||||
|
||||
let res = register_package(&db_path, &spec_b, &dest_b);
|
||||
assert!(res.is_err());
|
||||
let err = format!("{}", res.err().unwrap());
|
||||
assert!(err.contains("File ownership conflict detected"));
|
||||
assert!(err.contains("usr/bin/shared"));
|
||||
assert!(err.contains("alpha"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_package_auto_clears_safe_conflicts() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
|
||||
// Install package 'alpha' owning usr/share/info/dir
|
||||
let spec_a = mk_spec("alpha", "1.0");
|
||||
let dest_a = tmp.path().join("dest_a");
|
||||
std::fs::create_dir_all(dest_a.join("usr/share/info")).unwrap();
|
||||
std::fs::write(dest_a.join("usr/share/info/dir"), "index").unwrap();
|
||||
register_package(&db_path, &spec_a, &dest_a).unwrap();
|
||||
|
||||
// Now install package 'beta' that also provides usr/share/info/dir -> should auto-clear
|
||||
let spec_b = mk_spec("beta", "1.0");
|
||||
let dest_b = tmp.path().join("dest_b");
|
||||
std::fs::create_dir_all(dest_b.join("usr/share/info")).unwrap();
|
||||
std::fs::write(dest_b.join("usr/share/info/dir"), "index2").unwrap();
|
||||
|
||||
// This should succeed and transfer ownership of the 'dir' path to beta
|
||||
register_package(&db_path, &spec_b, &dest_b).unwrap();
|
||||
|
||||
// Verify DB: alpha should no longer own the path, beta should
|
||||
let files_a = get_package_files(&db_path, "alpha").unwrap();
|
||||
assert!(!files_a.contains(&"usr/share/info/dir".to_string()));
|
||||
let files_b = get_package_files(&db_path, "beta").unwrap();
|
||||
assert!(files_b.contains(&"usr/share/info/dir".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_package_files_missing_package_returns_empty() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
|
||||
// Create an empty database file with schema but no packages
|
||||
let conn = Connection::open(&db_path).unwrap();
|
||||
init_db(&conn).unwrap();
|
||||
drop(conn);
|
||||
|
||||
// Querying files for a package that doesn't exist should return an empty list
|
||||
let files = get_package_files(&db_path, "nonexistent").unwrap();
|
||||
assert!(files.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_package_tolerates_missing_files_and_cleans_db() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
+138
@@ -208,6 +208,144 @@ impl RepoManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronize git mirrors into /usr/src/depot/<reponame>
|
||||
pub fn sync_mirrors(repo_dir: &std::path::Path, mirrors: &std::collections::HashMap<String, String>) -> Result<()> {
|
||||
use git2::{Repository, FetchOptions, Cred, RemoteCallbacks, ResetType, build::RepoBuilder};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let base = repo_dir.to_path_buf();
|
||||
if !base.exists() {
|
||||
std::fs::create_dir_all(&base)?;
|
||||
}
|
||||
|
||||
for (name, url) in mirrors {
|
||||
let target = base.join(name);
|
||||
if !target.exists() {
|
||||
println!("Cloning mirror '{}' -> {}", name, target.display());
|
||||
|
||||
// Use git2 RepoBuilder to clone
|
||||
let mut cb = RemoteCallbacks::new();
|
||||
cb.credentials(|_url, username_from_url, _allowed| {
|
||||
// Try default credentials (ssh-agent / keychain)
|
||||
Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
|
||||
});
|
||||
|
||||
let mut fo = FetchOptions::new();
|
||||
fo.remote_callbacks(cb);
|
||||
|
||||
let mut builder = RepoBuilder::new();
|
||||
builder.fetch_options(fo);
|
||||
builder.clone(url, &target).with_context(|| format!("Failed to clone {}", url))?;
|
||||
} else {
|
||||
println!("Updating mirror '{}' in {}", name, target.display());
|
||||
// Open repository and fetch updates
|
||||
let repo = Repository::open(&target)
|
||||
.with_context(|| format!("Failed to open repository at {}", target.display()))?;
|
||||
|
||||
let mut cb = RemoteCallbacks::new();
|
||||
cb.credentials(|_url, username_from_url, _allowed| {
|
||||
Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
|
||||
});
|
||||
|
||||
let mut fo = FetchOptions::new();
|
||||
fo.remote_callbacks(cb);
|
||||
|
||||
// Fetch from origin
|
||||
let mut remote = repo.find_remote("origin").or_else(|_| repo.remote_anonymous(url))?;
|
||||
remote.fetch(&["refs/heads/*:refs/remotes/origin/*"], Some(&mut fo), None)
|
||||
.with_context(|| format!("Failed to fetch updates for {}", url))?;
|
||||
|
||||
// Try to fast-forward HEAD to origin/HEAD by resetting to FETCH_HEAD if present
|
||||
if let Ok(fetch_head) = repo.find_reference("FETCH_HEAD") {
|
||||
if let Some(oid) = fetch_head.target() {
|
||||
let obj = repo.find_object(oid, None)?;
|
||||
repo.reset(&obj, ResetType::Hard, None)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make the tree readable and writable by everyone
|
||||
for entry in walkdir::WalkDir::new(&target) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o777))?;
|
||||
} else if path.is_file() {
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o666))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show status for each mirror repository: path, exists, branch/HEAD, latest commit, dirty
|
||||
pub fn mirrors_status(repo_dir: &std::path::Path, mirrors: &std::collections::HashMap<String, String>) -> Result<()> {
|
||||
use git2::Repository;
|
||||
|
||||
let base = repo_dir.to_path_buf();
|
||||
if !base.exists() {
|
||||
println!("Repo base directory does not exist: {}", base.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (name, _url) in mirrors {
|
||||
let target = base.join(name);
|
||||
println!("--- {} ---", name);
|
||||
if !target.exists() {
|
||||
println!("Not cloned: {}", target.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
match Repository::open(&target) {
|
||||
Ok(repo) => {
|
||||
// Branch / HEAD
|
||||
let head = repo.head().ok();
|
||||
let branch = head
|
||||
.as_ref()
|
||||
.and_then(|h| h.shorthand().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| "(no branch)".to_string());
|
||||
|
||||
// Latest commit OID
|
||||
let oid = repo.refname_to_id("HEAD").ok();
|
||||
let short = oid.map(|o| format!("{}", o)).unwrap_or_else(|| "(unknown)".to_string());
|
||||
|
||||
// Commit time (seconds since epoch) if available
|
||||
let mut commit_time = String::new();
|
||||
if let Some(oid) = oid {
|
||||
if let Ok(commit) = repo.find_commit(oid) {
|
||||
let t = commit.time().seconds();
|
||||
commit_time = format!("{}", t);
|
||||
}
|
||||
}
|
||||
|
||||
// Dirty status
|
||||
let statuses = match repo.statuses(None) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
println!("Warning: failed to read status for {}", target.display());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let dirty = statuses.iter().any(|s| s.status().intersects(git2::Status::WT_MODIFIED | git2::Status::WT_NEW | git2::Status::WT_DELETED));
|
||||
|
||||
println!("Path: {}", target.display());
|
||||
println!("Branch/HEAD: {}", branch);
|
||||
println!("HEAD OID: {}", short);
|
||||
if !commit_time.is_empty() {
|
||||
println!("Latest commit time (epoch): {}", commit_time);
|
||||
}
|
||||
println!("Dirty: {}", if dirty { "yes" } else { "no" });
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to open repo at {}: {}", target.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn num_cpus() -> usize {
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
|
||||
Reference in New Issue
Block a user