Add update and check commands with UTC package timestamps
This commit is contained in:
Generated
+16
-3
@@ -382,7 +382,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
||||
|
||||
[[package]]
|
||||
name = "depot"
|
||||
version = "0.12.2"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ar",
|
||||
@@ -410,6 +410,7 @@ dependencies = [
|
||||
"tar",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"toml",
|
||||
"url",
|
||||
"walkdir",
|
||||
@@ -2040,9 +2041,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.25.0"
|
||||
version = "3.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
||||
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.1",
|
||||
@@ -2107,11 +2108,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"js-sys",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2120,6 +2123,16 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "depot"
|
||||
version = "0.12.2"
|
||||
version = "0.13.0"
|
||||
edition = "2024"
|
||||
|
||||
[lints.rust]
|
||||
@@ -21,7 +21,7 @@ serde = { version = "1.0.228", features = ["derive"] }
|
||||
yaml-rust2 = "0.11.0"
|
||||
sha2 = "0.10.9"
|
||||
tar = "0.4.44"
|
||||
tempfile = "=3.25.0"
|
||||
tempfile = "=3.26.0"
|
||||
thiserror = "2.0.18"
|
||||
toml = "0.9.8"
|
||||
url = "2.5.8"
|
||||
@@ -40,6 +40,7 @@ filetime = "0.2.27"
|
||||
clap_complete = "4.5.66"
|
||||
clap_mangen = "0.2.31"
|
||||
sys-mount = { version = "3.1.0", default-features = false }
|
||||
time = { version = "0.3.47", features = ["formatting", "parsing"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "=3.25.0"
|
||||
tempfile = "=3.26.0"
|
||||
|
||||
@@ -48,6 +48,12 @@ depot install zlib-1.2.11-1-x86_64.depot.pkg.tar.zst
|
||||
- `build <SPEC>`: Build a package and create an archive without installing.
|
||||
- Resolves and offers to install missing build dependencies before fetching/building.
|
||||
- Missing test dependencies automatically disable test execution for that build.
|
||||
- `update [PACKAGE ...]`: Update installed packages from configured repositories.
|
||||
- With no package names, updates every installed package that has a newer repo version available.
|
||||
- Refreshes source repos first, compares installed package version/revision and UTC completion time against repo metadata, and installs any newly introduced runtime dependencies before applying updates.
|
||||
- `check [DIR]`: Recursively scan `DIR` (default `.`) for package specs and report newer upstream versions when they can be inferred from versioned source URLs.
|
||||
- Supports git tag-style sources such as `...git#v$version`.
|
||||
- Also checks tag-style release URLs such as GitHub `releases/download/$version/...`.
|
||||
- `info <PACKAGE_OR_SPEC>`: Show information about a package.
|
||||
- `search <QUERY>`: Search enabled source/binary repos by package name and provided features.
|
||||
- Use `--files` to search binary repo metadata file lists.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
project(
|
||||
'depot',
|
||||
version: '0.12.2',
|
||||
version: '0.13.0',
|
||||
meson_version: '>=0.60.0',
|
||||
)
|
||||
|
||||
|
||||
+41
@@ -90,6 +90,18 @@ pub enum Commands {
|
||||
#[arg(long)]
|
||||
install: bool,
|
||||
},
|
||||
/// 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>,
|
||||
},
|
||||
/// Scan package specs for upstream version updates
|
||||
Check {
|
||||
/// Directory to scan recursively for package specs
|
||||
#[arg(default_value = ".")]
|
||||
dir: PathBuf,
|
||||
},
|
||||
/// Show information about a package
|
||||
Info {
|
||||
/// Path to package spec or installed package name
|
||||
@@ -269,4 +281,33 @@ mod tests {
|
||||
_ => panic!("expected install command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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()),
|
||||
_ => panic!("expected update command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_accepts_multiple_package_names() {
|
||||
let cli = Cli::try_parse_from(["depot", "update", "linux", "openssl"]).unwrap();
|
||||
match cli.command {
|
||||
Commands::Update { packages } => {
|
||||
assert_eq!(packages, vec!["linux".to_string(), "openssl".to_string()])
|
||||
}
|
||||
_ => panic!("expected update command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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")),
|
||||
_ => panic!("expected check command"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1404
-5
File diff suppressed because it is too large
Load Diff
+136
-4
@@ -8,11 +8,21 @@ use anyhow::{Context, Result};
|
||||
use rusqlite::{Connection, params};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
fn format_licenses(licenses: &[String]) -> String {
|
||||
licenses.join(", ")
|
||||
}
|
||||
|
||||
/// Installed package row from the local package database.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InstalledPackageRecord {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub revision: u32,
|
||||
pub completed_at: Option<i64>,
|
||||
}
|
||||
|
||||
/// Initialize database and register a package
|
||||
pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> Result<()> {
|
||||
// Create parent directory (auto-create db dir if missing)
|
||||
@@ -24,20 +34,22 @@ pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> R
|
||||
init_db(&conn)?;
|
||||
|
||||
let tx = conn.transaction()?;
|
||||
let completed_at = package_completed_at(destdir)?;
|
||||
|
||||
// Generate manifest with files and directories
|
||||
let manifest = staging::generate_manifest_with_dirs(destdir)?;
|
||||
|
||||
// Insert/update package without changing its primary key (UPSERT keeps the existing row).
|
||||
tx.execute(
|
||||
"INSERT INTO packages (name, version, revision, description, homepage, license)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
"INSERT INTO packages (name, version, revision, description, homepage, license, completed_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
version=excluded.version,
|
||||
revision=excluded.revision,
|
||||
description=excluded.description,
|
||||
homepage=excluded.homepage,
|
||||
license=excluded.license",
|
||||
license=excluded.license,
|
||||
completed_at=excluded.completed_at",
|
||||
params![
|
||||
spec.package.name,
|
||||
spec.package.version,
|
||||
@@ -45,6 +57,7 @@ pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> R
|
||||
spec.package.description,
|
||||
spec.package.homepage,
|
||||
format_licenses(&spec.package.license),
|
||||
completed_at,
|
||||
],
|
||||
)?;
|
||||
|
||||
@@ -374,7 +387,8 @@ fn init_db(conn: &Connection) -> Result<()> {
|
||||
revision INTEGER NOT NULL DEFAULT 1,
|
||||
description TEXT,
|
||||
homepage TEXT,
|
||||
license TEXT
|
||||
license TEXT,
|
||||
completed_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS provides (
|
||||
@@ -406,9 +420,62 @@ fn init_db(conn: &Connection) -> Result<()> {
|
||||
CREATE INDEX IF NOT EXISTS idx_directories_path ON directories(path);
|
||||
",
|
||||
)?;
|
||||
ensure_packages_completed_at_column(conn)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_packages_completed_at_column(conn: &Connection) -> Result<()> {
|
||||
let has_completed_at: bool = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'completed_at'",
|
||||
[],
|
||||
|row| {
|
||||
let count: i64 = row.get(0)?;
|
||||
Ok(count > 0)
|
||||
},
|
||||
)
|
||||
.context("Failed to inspect installed package DB schema")?;
|
||||
if !has_completed_at {
|
||||
conn.execute("ALTER TABLE packages ADD COLUMN completed_at INTEGER", [])
|
||||
.context("Failed to add completed_at column to installed package DB")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn package_completed_at(destdir: &Path) -> Result<Option<i64>> {
|
||||
let metadata_path = destdir.join(".metadata.toml");
|
||||
if let Some(completed_at) =
|
||||
crate::metadata_time::read_completed_at_from_metadata_path(&metadata_path)?
|
||||
{
|
||||
return Ok(Some(completed_at));
|
||||
}
|
||||
latest_tree_mtime(destdir)
|
||||
}
|
||||
|
||||
fn latest_tree_mtime(path: &Path) -> Result<Option<i64>> {
|
||||
let mut latest = None;
|
||||
|
||||
for entry in WalkDir::new(path)
|
||||
.into_iter()
|
||||
.filter_map(|entry| entry.ok())
|
||||
{
|
||||
let modified = entry
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to read metadata for {}", entry.path().display()))?
|
||||
.modified()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to read modification time for {}",
|
||||
entry.path().display()
|
||||
)
|
||||
})?;
|
||||
let modified = crate::metadata_time::system_time_to_unix(modified)?;
|
||||
latest = Some(latest.map_or(modified, |current: i64| current.max(modified)));
|
||||
}
|
||||
|
||||
Ok(latest)
|
||||
}
|
||||
|
||||
/// Decide whether a conflicting path is safe to auto-remove ownership for.
|
||||
/// This covers common language-shared trees such as Perl site/vendor/lib
|
||||
/// directories.
|
||||
@@ -477,6 +544,27 @@ pub fn get_installed_packages(db_path: &Path) -> Result<std::collections::HashSe
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// List installed packages with version and revision metadata.
|
||||
pub fn list_installed_package_records(db_path: &Path) -> Result<Vec<InstalledPackageRecord>> {
|
||||
if !db_path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
init_db(&conn)?;
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT name, version, revision, completed_at FROM packages ORDER BY name")?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(InstalledPackageRecord {
|
||||
name: row.get(0)?,
|
||||
version: row.get(1)?,
|
||||
revision: row.get::<_, i64>(2)? as u32,
|
||||
completed_at: row.get(3)?,
|
||||
})
|
||||
})?;
|
||||
Ok(rows.filter_map(|row| row.ok()).collect())
|
||||
}
|
||||
|
||||
/// Get set of all provided package names (alternatives)
|
||||
pub fn get_all_provides(db_path: &Path) -> Result<std::collections::HashSet<String>> {
|
||||
use std::collections::HashSet;
|
||||
@@ -635,6 +723,50 @@ mod tests {
|
||||
assert_eq!(version.as_deref(), Some("2.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_package_uses_metadata_completed_at_when_present() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
let spec = mk_spec("foo", "1.0");
|
||||
let dest = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest.join("usr/bin/foo"), "bin").unwrap();
|
||||
std::fs::write(
|
||||
dest.join(".metadata.toml"),
|
||||
"completed_at = \"2026-03-10T12:34:56Z\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
register_package(&db_path, &spec, &dest).unwrap();
|
||||
|
||||
let records = list_installed_package_records(&db_path).unwrap();
|
||||
assert_eq!(records.len(), 1);
|
||||
assert_eq!(records[0].completed_at, Some(1_773_146_096));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_package_falls_back_to_destdir_mtime_when_metadata_missing() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
let spec = mk_spec("foo", "1.0");
|
||||
let dest = tmp.path().join("dest");
|
||||
std::fs::create_dir_all(dest.join("usr/bin")).unwrap();
|
||||
let file = dest.join("usr/bin/foo");
|
||||
std::fs::write(&file, "bin").unwrap();
|
||||
|
||||
let ts = filetime::FileTime::from_unix_time(1_700_000_000, 0);
|
||||
filetime::set_file_mtime(&file, ts).unwrap();
|
||||
filetime::set_file_mtime(&dest.join("usr"), ts).unwrap();
|
||||
filetime::set_file_mtime(&dest.join("usr/bin"), ts).unwrap();
|
||||
filetime::set_file_mtime(&dest, ts).unwrap();
|
||||
|
||||
register_package(&db_path, &spec, &dest).unwrap();
|
||||
|
||||
let records = list_installed_package_records(&db_path).unwrap();
|
||||
assert_eq!(records.len(), 1);
|
||||
assert_eq!(records[0].completed_at, Some(1_700_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_package_detects_conflicting_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
+153
-12
@@ -1,5 +1,6 @@
|
||||
//! Repository management and SQLite database generation
|
||||
|
||||
use crate::metadata_time;
|
||||
use anyhow::{Context, Result};
|
||||
use rusqlite::{Connection, params};
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
@@ -73,6 +74,7 @@ struct IndexedPackage {
|
||||
name: String,
|
||||
version: String,
|
||||
revision: u32,
|
||||
completed_at: Option<i64>,
|
||||
description: Option<String>,
|
||||
homepage: Option<String>,
|
||||
license: Option<String>,
|
||||
@@ -107,6 +109,7 @@ pub struct BinaryRepoPackageRecord {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub revision: u32,
|
||||
pub completed_at: Option<i64>,
|
||||
pub filename: String,
|
||||
pub size: u64,
|
||||
pub sha256: String,
|
||||
@@ -279,6 +282,7 @@ impl RepoManager {
|
||||
name TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
completed_at INTEGER,
|
||||
description TEXT,
|
||||
homepage TEXT,
|
||||
license TEXT,
|
||||
@@ -335,6 +339,7 @@ impl RepoManager {
|
||||
let mut name = String::new();
|
||||
let mut version = String::new();
|
||||
let mut revision = 1;
|
||||
let mut completed_at = path_modified_unix_timestamp(pkg_path)?;
|
||||
let mut description = None;
|
||||
let mut homepage = None;
|
||||
let mut license = None;
|
||||
@@ -372,6 +377,8 @@ impl RepoManager {
|
||||
.get("revision")
|
||||
.and_then(|v| v.as_integer())
|
||||
.unwrap_or(1) as u32;
|
||||
completed_at =
|
||||
metadata_time::parse_completed_at_value(&metadata).or(completed_at);
|
||||
description = metadata
|
||||
.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -444,6 +451,7 @@ impl RepoManager {
|
||||
name,
|
||||
version,
|
||||
revision,
|
||||
completed_at,
|
||||
description,
|
||||
homepage,
|
||||
license,
|
||||
@@ -463,6 +471,7 @@ impl RepoManager {
|
||||
name,
|
||||
version,
|
||||
revision,
|
||||
completed_at,
|
||||
description,
|
||||
homepage,
|
||||
license,
|
||||
@@ -478,12 +487,13 @@ impl RepoManager {
|
||||
|
||||
// Insert into database
|
||||
conn.execute(
|
||||
"INSERT INTO packages (name, version, revision, description, homepage, license, filename, size, sha256, sha512)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||
"INSERT INTO packages (name, version, revision, completed_at, description, homepage, license, filename, size, sha256, sha512)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
||||
params![
|
||||
name,
|
||||
version,
|
||||
revision as i64,
|
||||
completed_at,
|
||||
description,
|
||||
homepage,
|
||||
license,
|
||||
@@ -1503,12 +1513,18 @@ fn find_cached_binary_repo_packages(
|
||||
let conn = Connection::open(db_path)
|
||||
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
let completed_at_expr = if repo_packages_have_completed_at(&conn)? {
|
||||
"p.completed_at"
|
||||
} else {
|
||||
"NULL"
|
||||
};
|
||||
let sql = format!(
|
||||
"SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.version,
|
||||
p.revision,
|
||||
{completed_at_expr},
|
||||
p.filename,
|
||||
p.size,
|
||||
p.sha256,
|
||||
@@ -1525,8 +1541,9 @@ fn find_cached_binary_repo_packages(
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN lower(p.name) = lower(?1) THEN 0 ELSE 1 END,
|
||||
p.name ASC",
|
||||
)?;
|
||||
p.name ASC"
|
||||
);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
|
||||
let rows = stmt.query_map(params![query], |row| {
|
||||
let package_id = row.get::<_, i64>(0)?;
|
||||
@@ -1537,13 +1554,14 @@ fn find_cached_binary_repo_packages(
|
||||
name: row.get(1)?,
|
||||
version: row.get(2)?,
|
||||
revision: row.get::<_, i64>(3)? as u32,
|
||||
filename: row.get(4)?,
|
||||
size: row.get::<_, i64>(5)? as u64,
|
||||
sha256: row.get(6)?,
|
||||
sha512: row.get(7)?,
|
||||
description: row.get(8)?,
|
||||
homepage: row.get(9)?,
|
||||
license: row.get(10)?,
|
||||
completed_at: row.get(4)?,
|
||||
filename: row.get(5)?,
|
||||
size: row.get::<_, i64>(6)? as u64,
|
||||
sha256: row.get(7)?,
|
||||
sha512: row.get(8)?,
|
||||
description: row.get(9)?,
|
||||
homepage: row.get(10)?,
|
||||
license: row.get(11)?,
|
||||
provides: Vec::new(),
|
||||
runtime_dependencies: Vec::new(),
|
||||
optional_dependencies: Vec::new(),
|
||||
@@ -1562,6 +1580,93 @@ fn find_cached_binary_repo_packages(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn list_cached_binary_repo_packages(
|
||||
repo_name: &str,
|
||||
db_path: &Path,
|
||||
) -> Result<Vec<BinaryRepoPackageRecord>> {
|
||||
let conn = Connection::open(db_path)
|
||||
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
|
||||
|
||||
let completed_at_expr = if repo_packages_have_completed_at(&conn)? {
|
||||
"p.completed_at"
|
||||
} else {
|
||||
"NULL"
|
||||
};
|
||||
let sql = format!(
|
||||
"SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.version,
|
||||
p.revision,
|
||||
{completed_at_expr},
|
||||
p.filename,
|
||||
p.size,
|
||||
p.sha256,
|
||||
p.sha512,
|
||||
p.description,
|
||||
p.homepage,
|
||||
p.license
|
||||
FROM packages p
|
||||
ORDER BY p.name ASC, p.version ASC, p.revision ASC"
|
||||
);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let package_id = row.get::<_, i64>(0)?;
|
||||
Ok((
|
||||
package_id,
|
||||
BinaryRepoPackageRecord {
|
||||
repo_name: repo_name.to_string(),
|
||||
name: row.get(1)?,
|
||||
version: row.get(2)?,
|
||||
revision: row.get::<_, i64>(3)? as u32,
|
||||
completed_at: row.get(4)?,
|
||||
filename: row.get(5)?,
|
||||
size: row.get::<_, i64>(6)? as u64,
|
||||
sha256: row.get(7)?,
|
||||
sha512: row.get(8)?,
|
||||
description: row.get(9)?,
|
||||
homepage: row.get(10)?,
|
||||
license: row.get(11)?,
|
||||
provides: Vec::new(),
|
||||
runtime_dependencies: Vec::new(),
|
||||
optional_dependencies: Vec::new(),
|
||||
},
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
for row in rows {
|
||||
let (package_id, mut rec) = row?;
|
||||
rec.provides = query_package_provides(&conn, package_id)?;
|
||||
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
||||
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
||||
out.push(rec);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn repo_packages_have_completed_at(conn: &Connection) -> Result<bool> {
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'completed_at'",
|
||||
[],
|
||||
|row| {
|
||||
let count: i64 = row.get(0)?;
|
||||
Ok(count > 0)
|
||||
},
|
||||
)
|
||||
.context("Failed to inspect binary repo DB schema")
|
||||
}
|
||||
|
||||
fn path_modified_unix_timestamp(path: &Path) -> Result<Option<i64>> {
|
||||
let metadata = fs::metadata(path)
|
||||
.with_context(|| format!("Failed to read metadata for {}", path.display()))?;
|
||||
let modified = metadata
|
||||
.modified()
|
||||
.with_context(|| format!("Failed to read modification time for {}", path.display()))?;
|
||||
Ok(Some(metadata_time::system_time_to_unix(modified)?))
|
||||
}
|
||||
|
||||
/// Resolve an exact package name/provide match from a binary repo after verifying
|
||||
/// and caching its signed `repo.db.zst`.
|
||||
pub fn find_binary_repo_package(
|
||||
@@ -1595,6 +1700,17 @@ pub fn find_binary_repo_packages(
|
||||
find_cached_binary_repo_packages(repo_name, &db_path, query)
|
||||
}
|
||||
|
||||
/// List all binary packages from a cached, verified repository database.
|
||||
pub fn list_binary_repo_packages(
|
||||
repo_name: &str,
|
||||
repo: &crate::config::BinaryRepo,
|
||||
rootfs: &Path,
|
||||
package_cache_dir: &Path,
|
||||
) -> Result<Vec<BinaryRepoPackageRecord>> {
|
||||
let db_path = fetch_binary_repo_db(repo_name, repo, rootfs, package_cache_dir)?;
|
||||
list_cached_binary_repo_packages(repo_name, &db_path)
|
||||
}
|
||||
|
||||
fn verify_hex_digest(path: &Path, algorithm: &str, expected_hex: &str) -> Result<bool> {
|
||||
let expected = expected_hex.trim().to_ascii_lowercase();
|
||||
if expected.is_empty() {
|
||||
@@ -2109,6 +2225,18 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(has_sha512);
|
||||
|
||||
let has_completed_at: bool = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'completed_at'",
|
||||
[],
|
||||
|r| {
|
||||
let n: i64 = r.get(0)?;
|
||||
Ok(n > 0)
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(has_completed_at);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2129,6 +2257,7 @@ revision = 1
|
||||
description = "test description"
|
||||
homepage = "https://example.com"
|
||||
license = "MIT"
|
||||
completed_at = "2026-03-10T12:34:56Z"
|
||||
provides = ["test-feature"]
|
||||
|
||||
[dependencies]
|
||||
@@ -2144,6 +2273,11 @@ optional = []
|
||||
|
||||
let encoder = tar.into_inner().unwrap();
|
||||
encoder.finish().unwrap();
|
||||
filetime::set_file_mtime(
|
||||
&pkg_path,
|
||||
filetime::FileTime::from_unix_time(1_700_000_000, 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut conn = Connection::open_in_memory().unwrap();
|
||||
let manager = RepoManager::new(repo_dir.to_path_buf());
|
||||
@@ -2190,6 +2324,11 @@ optional = []
|
||||
assert_eq!(sha256.len(), 64);
|
||||
assert_eq!(sha512.len(), 128);
|
||||
|
||||
let completed_at: Option<i64> = conn
|
||||
.query_row("SELECT completed_at FROM packages", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(completed_at, Some(1_773_146_096));
|
||||
|
||||
let provides_count: i64 = conn
|
||||
.query_row("SELECT count(*) FROM provides", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
@@ -2323,6 +2462,7 @@ license = ["MIT", "Apache-2.0"]
|
||||
name: "pkg".into(),
|
||||
version: "1.0".into(),
|
||||
revision: 1,
|
||||
completed_at: None,
|
||||
filename: "pkg.depot.pkg.tar.zst".into(),
|
||||
size: 7,
|
||||
sha256,
|
||||
@@ -2357,6 +2497,7 @@ license = ["MIT", "Apache-2.0"]
|
||||
name: "pkg".into(),
|
||||
version: "1.0".into(),
|
||||
revision: 1,
|
||||
completed_at: None,
|
||||
filename: filename.to_string(),
|
||||
size: payload.len() as u64,
|
||||
sha256,
|
||||
|
||||
@@ -13,6 +13,7 @@ mod fakeroot;
|
||||
mod index;
|
||||
mod install;
|
||||
mod locking;
|
||||
mod metadata_time;
|
||||
mod package;
|
||||
mod planner;
|
||||
mod runtime_env;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
|
||||
pub(crate) fn current_utc_timestamp_string() -> Result<String> {
|
||||
OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.context("Failed to format UTC timestamp")
|
||||
}
|
||||
|
||||
pub(crate) fn parse_completed_at_value(metadata: &toml::Value) -> Option<i64> {
|
||||
metadata
|
||||
.get("completed_at")
|
||||
.and_then(|value| completed_at_value_to_unix(value).ok().flatten())
|
||||
}
|
||||
|
||||
pub(crate) fn read_completed_at_from_metadata_path(path: &Path) -> Result<Option<i64>> {
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let content =
|
||||
fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
let metadata: toml::Value =
|
||||
toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
|
||||
Ok(parse_completed_at_value(&metadata))
|
||||
}
|
||||
|
||||
pub(crate) fn system_time_to_unix(time: SystemTime) -> Result<i64> {
|
||||
let duration = time
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|err| anyhow::anyhow!("System clock is before UNIX_EPOCH: {}", err))?;
|
||||
Ok(duration.as_secs() as i64)
|
||||
}
|
||||
|
||||
fn completed_at_value_to_unix(value: &toml::Value) -> Result<Option<i64>> {
|
||||
if let Some(raw) = value.as_integer() {
|
||||
return Ok(Some(raw));
|
||||
}
|
||||
|
||||
let Some(raw) = value.as_str() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let parsed = OffsetDateTime::parse(raw, &Rfc3339)
|
||||
.with_context(|| format!("Invalid UTC timestamp '{}'", raw))?;
|
||||
Ok(Some(parsed.unix_timestamp()))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Package creation and archive management
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::metadata_time;
|
||||
use crate::package::PackageSpec;
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
@@ -125,6 +126,7 @@ impl Packager {
|
||||
|
||||
fn generate_metadata_toml(&self) -> Result<()> {
|
||||
let metadata_path = self.destdir.join(".metadata.toml");
|
||||
let completed_at = metadata_time::current_utc_timestamp_string()?;
|
||||
|
||||
// Construct a simple metadata structure
|
||||
let mut map = toml::map::Map::new();
|
||||
@@ -152,6 +154,10 @@ impl Packager {
|
||||
"license".to_string(),
|
||||
license_value(&self.spec.package.license),
|
||||
);
|
||||
map.insert(
|
||||
"completed_at".to_string(),
|
||||
toml::Value::String(completed_at),
|
||||
);
|
||||
|
||||
// Add provides
|
||||
map.insert(
|
||||
@@ -405,6 +411,10 @@ mod tests {
|
||||
assert_eq!(val.get("version").and_then(|v| v.as_str()), Some("1.0"));
|
||||
assert_eq!(val.get("revision").and_then(|v| v.as_integer()), Some(1));
|
||||
assert_eq!(val.get("license").and_then(|v| v.as_str()), Some("MIT"));
|
||||
assert!(
|
||||
crate::metadata_time::parse_completed_at_value(&val).is_some(),
|
||||
"expected RFC3339 UTC completed_at"
|
||||
);
|
||||
|
||||
let deps = val.get("dependencies").unwrap();
|
||||
assert!(deps.get("runtime").unwrap().as_array().unwrap().is_empty());
|
||||
|
||||
@@ -837,6 +837,7 @@ mod tests {
|
||||
name: name.to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
revision: 1,
|
||||
completed_at: None,
|
||||
filename: format!("{name}-1.0.0-1-x86_64.depot.pkg.tar.zst"),
|
||||
size: 1024,
|
||||
sha256: "sha256".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user