Add update and check commands with UTC package timestamps
This commit is contained in:
Generated
+16
-3
@@ -382,7 +382,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.12.2"
|
version = "0.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ar",
|
"ar",
|
||||||
@@ -410,6 +410,7 @@ dependencies = [
|
|||||||
"tar",
|
"tar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -2040,9 +2041,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.25.0"
|
version = "3.26.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.4.1",
|
"getrandom 0.4.1",
|
||||||
@@ -2107,11 +2108,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
|
"itoa",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2120,6 +2123,16 @@ version = "0.1.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
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]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.12.2"
|
version = "0.13.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
@@ -21,7 +21,7 @@ serde = { version = "1.0.228", features = ["derive"] }
|
|||||||
yaml-rust2 = "0.11.0"
|
yaml-rust2 = "0.11.0"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
tempfile = "=3.25.0"
|
tempfile = "=3.26.0"
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
toml = "0.9.8"
|
toml = "0.9.8"
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
@@ -40,6 +40,7 @@ filetime = "0.2.27"
|
|||||||
clap_complete = "4.5.66"
|
clap_complete = "4.5.66"
|
||||||
clap_mangen = "0.2.31"
|
clap_mangen = "0.2.31"
|
||||||
sys-mount = { version = "3.1.0", default-features = false }
|
sys-mount = { version = "3.1.0", default-features = false }
|
||||||
|
time = { version = "0.3.47", features = ["formatting", "parsing"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[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.
|
- `build <SPEC>`: Build a package and create an archive without installing.
|
||||||
- Resolves and offers to install missing build dependencies before fetching/building.
|
- Resolves and offers to install missing build dependencies before fetching/building.
|
||||||
- Missing test dependencies automatically disable test execution for that build.
|
- 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.
|
- `info <PACKAGE_OR_SPEC>`: Show information about a package.
|
||||||
- `search <QUERY>`: Search enabled source/binary repos by package name and provided features.
|
- `search <QUERY>`: Search enabled source/binary repos by package name and provided features.
|
||||||
- Use `--files` to search binary repo metadata file lists.
|
- Use `--files` to search binary repo metadata file lists.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
project(
|
project(
|
||||||
'depot',
|
'depot',
|
||||||
version: '0.12.2',
|
version: '0.13.0',
|
||||||
meson_version: '>=0.60.0',
|
meson_version: '>=0.60.0',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+41
@@ -90,6 +90,18 @@ pub enum Commands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
install: bool,
|
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
|
/// Show information about a package
|
||||||
Info {
|
Info {
|
||||||
/// Path to package spec or installed package name
|
/// Path to package spec or installed package name
|
||||||
@@ -269,4 +281,33 @@ mod tests {
|
|||||||
_ => panic!("expected install command"),
|
_ => 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 rusqlite::{Connection, params};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
fn format_licenses(licenses: &[String]) -> String {
|
fn format_licenses(licenses: &[String]) -> String {
|
||||||
licenses.join(", ")
|
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
|
/// Initialize database and register a package
|
||||||
pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> Result<()> {
|
pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> Result<()> {
|
||||||
// Create parent directory (auto-create db dir if missing)
|
// 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)?;
|
init_db(&conn)?;
|
||||||
|
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
|
let completed_at = package_completed_at(destdir)?;
|
||||||
|
|
||||||
// Generate manifest with files and directories
|
// Generate manifest with files and directories
|
||||||
let manifest = staging::generate_manifest_with_dirs(destdir)?;
|
let manifest = staging::generate_manifest_with_dirs(destdir)?;
|
||||||
|
|
||||||
// Insert/update package without changing its primary key (UPSERT keeps the existing row).
|
// Insert/update package without changing its primary key (UPSERT keeps the existing row).
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT INTO packages (name, version, revision, description, homepage, license)
|
"INSERT INTO packages (name, version, revision, description, homepage, license, completed_at)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||||
ON CONFLICT(name) DO UPDATE SET
|
ON CONFLICT(name) DO UPDATE SET
|
||||||
version=excluded.version,
|
version=excluded.version,
|
||||||
revision=excluded.revision,
|
revision=excluded.revision,
|
||||||
description=excluded.description,
|
description=excluded.description,
|
||||||
homepage=excluded.homepage,
|
homepage=excluded.homepage,
|
||||||
license=excluded.license",
|
license=excluded.license,
|
||||||
|
completed_at=excluded.completed_at",
|
||||||
params![
|
params![
|
||||||
spec.package.name,
|
spec.package.name,
|
||||||
spec.package.version,
|
spec.package.version,
|
||||||
@@ -45,6 +57,7 @@ pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> R
|
|||||||
spec.package.description,
|
spec.package.description,
|
||||||
spec.package.homepage,
|
spec.package.homepage,
|
||||||
format_licenses(&spec.package.license),
|
format_licenses(&spec.package.license),
|
||||||
|
completed_at,
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -374,7 +387,8 @@ fn init_db(conn: &Connection) -> Result<()> {
|
|||||||
revision INTEGER NOT NULL DEFAULT 1,
|
revision INTEGER NOT NULL DEFAULT 1,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
homepage TEXT,
|
homepage TEXT,
|
||||||
license TEXT
|
license TEXT,
|
||||||
|
completed_at INTEGER
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS provides (
|
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);
|
CREATE INDEX IF NOT EXISTS idx_directories_path ON directories(path);
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
|
ensure_packages_completed_at_column(conn)?;
|
||||||
Ok(())
|
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.
|
/// Decide whether a conflicting path is safe to auto-remove ownership for.
|
||||||
/// This covers common language-shared trees such as Perl site/vendor/lib
|
/// This covers common language-shared trees such as Perl site/vendor/lib
|
||||||
/// directories.
|
/// directories.
|
||||||
@@ -477,6 +544,27 @@ pub fn get_installed_packages(db_path: &Path) -> Result<std::collections::HashSe
|
|||||||
Ok(names)
|
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)
|
/// Get set of all provided package names (alternatives)
|
||||||
pub fn get_all_provides(db_path: &Path) -> Result<std::collections::HashSet<String>> {
|
pub fn get_all_provides(db_path: &Path) -> Result<std::collections::HashSet<String>> {
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -635,6 +723,50 @@ mod tests {
|
|||||||
assert_eq!(version.as_deref(), Some("2.0"));
|
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]
|
#[test]
|
||||||
fn register_package_detects_conflicting_files() {
|
fn register_package_detects_conflicting_files() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
+153
-12
@@ -1,5 +1,6 @@
|
|||||||
//! Repository management and SQLite database generation
|
//! Repository management and SQLite database generation
|
||||||
|
|
||||||
|
use crate::metadata_time;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
use sha2::{Digest, Sha256, Sha512};
|
use sha2::{Digest, Sha256, Sha512};
|
||||||
@@ -73,6 +74,7 @@ struct IndexedPackage {
|
|||||||
name: String,
|
name: String,
|
||||||
version: String,
|
version: String,
|
||||||
revision: u32,
|
revision: u32,
|
||||||
|
completed_at: Option<i64>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
homepage: Option<String>,
|
homepage: Option<String>,
|
||||||
license: Option<String>,
|
license: Option<String>,
|
||||||
@@ -107,6 +109,7 @@ pub struct BinaryRepoPackageRecord {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub revision: u32,
|
pub revision: u32,
|
||||||
|
pub completed_at: Option<i64>,
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub sha256: String,
|
pub sha256: String,
|
||||||
@@ -279,6 +282,7 @@ impl RepoManager {
|
|||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
version TEXT NOT NULL,
|
version TEXT NOT NULL,
|
||||||
revision INTEGER NOT NULL,
|
revision INTEGER NOT NULL,
|
||||||
|
completed_at INTEGER,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
homepage TEXT,
|
homepage TEXT,
|
||||||
license TEXT,
|
license TEXT,
|
||||||
@@ -335,6 +339,7 @@ impl RepoManager {
|
|||||||
let mut name = String::new();
|
let mut name = String::new();
|
||||||
let mut version = String::new();
|
let mut version = String::new();
|
||||||
let mut revision = 1;
|
let mut revision = 1;
|
||||||
|
let mut completed_at = path_modified_unix_timestamp(pkg_path)?;
|
||||||
let mut description = None;
|
let mut description = None;
|
||||||
let mut homepage = None;
|
let mut homepage = None;
|
||||||
let mut license = None;
|
let mut license = None;
|
||||||
@@ -372,6 +377,8 @@ impl RepoManager {
|
|||||||
.get("revision")
|
.get("revision")
|
||||||
.and_then(|v| v.as_integer())
|
.and_then(|v| v.as_integer())
|
||||||
.unwrap_or(1) as u32;
|
.unwrap_or(1) as u32;
|
||||||
|
completed_at =
|
||||||
|
metadata_time::parse_completed_at_value(&metadata).or(completed_at);
|
||||||
description = metadata
|
description = metadata
|
||||||
.get("description")
|
.get("description")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
@@ -444,6 +451,7 @@ impl RepoManager {
|
|||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
revision,
|
revision,
|
||||||
|
completed_at,
|
||||||
description,
|
description,
|
||||||
homepage,
|
homepage,
|
||||||
license,
|
license,
|
||||||
@@ -463,6 +471,7 @@ impl RepoManager {
|
|||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
revision,
|
revision,
|
||||||
|
completed_at,
|
||||||
description,
|
description,
|
||||||
homepage,
|
homepage,
|
||||||
license,
|
license,
|
||||||
@@ -478,12 +487,13 @@ impl RepoManager {
|
|||||||
|
|
||||||
// Insert into database
|
// Insert into database
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO packages (name, version, revision, description, homepage, license, filename, size, sha256, sha512)
|
"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)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
||||||
params![
|
params![
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
revision as i64,
|
revision as i64,
|
||||||
|
completed_at,
|
||||||
description,
|
description,
|
||||||
homepage,
|
homepage,
|
||||||
license,
|
license,
|
||||||
@@ -1503,12 +1513,18 @@ fn find_cached_binary_repo_packages(
|
|||||||
let conn = Connection::open(db_path)
|
let conn = Connection::open(db_path)
|
||||||
.with_context(|| format!("Failed to open binary repo DB {}", db_path.display()))?;
|
.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
|
"SELECT
|
||||||
p.id,
|
p.id,
|
||||||
p.name,
|
p.name,
|
||||||
p.version,
|
p.version,
|
||||||
p.revision,
|
p.revision,
|
||||||
|
{completed_at_expr},
|
||||||
p.filename,
|
p.filename,
|
||||||
p.size,
|
p.size,
|
||||||
p.sha256,
|
p.sha256,
|
||||||
@@ -1525,8 +1541,9 @@ fn find_cached_binary_repo_packages(
|
|||||||
)
|
)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE WHEN lower(p.name) = lower(?1) THEN 0 ELSE 1 END,
|
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 rows = stmt.query_map(params![query], |row| {
|
||||||
let package_id = row.get::<_, i64>(0)?;
|
let package_id = row.get::<_, i64>(0)?;
|
||||||
@@ -1537,13 +1554,14 @@ fn find_cached_binary_repo_packages(
|
|||||||
name: row.get(1)?,
|
name: row.get(1)?,
|
||||||
version: row.get(2)?,
|
version: row.get(2)?,
|
||||||
revision: row.get::<_, i64>(3)? as u32,
|
revision: row.get::<_, i64>(3)? as u32,
|
||||||
filename: row.get(4)?,
|
completed_at: row.get(4)?,
|
||||||
size: row.get::<_, i64>(5)? as u64,
|
filename: row.get(5)?,
|
||||||
sha256: row.get(6)?,
|
size: row.get::<_, i64>(6)? as u64,
|
||||||
sha512: row.get(7)?,
|
sha256: row.get(7)?,
|
||||||
description: row.get(8)?,
|
sha512: row.get(8)?,
|
||||||
homepage: row.get(9)?,
|
description: row.get(9)?,
|
||||||
license: row.get(10)?,
|
homepage: row.get(10)?,
|
||||||
|
license: row.get(11)?,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
@@ -1562,6 +1580,93 @@ fn find_cached_binary_repo_packages(
|
|||||||
Ok(out)
|
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
|
/// Resolve an exact package name/provide match from a binary repo after verifying
|
||||||
/// and caching its signed `repo.db.zst`.
|
/// and caching its signed `repo.db.zst`.
|
||||||
pub fn find_binary_repo_package(
|
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)
|
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> {
|
fn verify_hex_digest(path: &Path, algorithm: &str, expected_hex: &str) -> Result<bool> {
|
||||||
let expected = expected_hex.trim().to_ascii_lowercase();
|
let expected = expected_hex.trim().to_ascii_lowercase();
|
||||||
if expected.is_empty() {
|
if expected.is_empty() {
|
||||||
@@ -2109,6 +2225,18 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(has_sha512);
|
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]
|
#[test]
|
||||||
@@ -2129,6 +2257,7 @@ revision = 1
|
|||||||
description = "test description"
|
description = "test description"
|
||||||
homepage = "https://example.com"
|
homepage = "https://example.com"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
completed_at = "2026-03-10T12:34:56Z"
|
||||||
provides = ["test-feature"]
|
provides = ["test-feature"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -2144,6 +2273,11 @@ optional = []
|
|||||||
|
|
||||||
let encoder = tar.into_inner().unwrap();
|
let encoder = tar.into_inner().unwrap();
|
||||||
encoder.finish().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 mut conn = Connection::open_in_memory().unwrap();
|
||||||
let manager = RepoManager::new(repo_dir.to_path_buf());
|
let manager = RepoManager::new(repo_dir.to_path_buf());
|
||||||
@@ -2190,6 +2324,11 @@ optional = []
|
|||||||
assert_eq!(sha256.len(), 64);
|
assert_eq!(sha256.len(), 64);
|
||||||
assert_eq!(sha512.len(), 128);
|
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
|
let provides_count: i64 = conn
|
||||||
.query_row("SELECT count(*) FROM provides", [], |r| r.get(0))
|
.query_row("SELECT count(*) FROM provides", [], |r| r.get(0))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -2323,6 +2462,7 @@ license = ["MIT", "Apache-2.0"]
|
|||||||
name: "pkg".into(),
|
name: "pkg".into(),
|
||||||
version: "1.0".into(),
|
version: "1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
|
completed_at: None,
|
||||||
filename: "pkg.depot.pkg.tar.zst".into(),
|
filename: "pkg.depot.pkg.tar.zst".into(),
|
||||||
size: 7,
|
size: 7,
|
||||||
sha256,
|
sha256,
|
||||||
@@ -2357,6 +2497,7 @@ license = ["MIT", "Apache-2.0"]
|
|||||||
name: "pkg".into(),
|
name: "pkg".into(),
|
||||||
version: "1.0".into(),
|
version: "1.0".into(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
|
completed_at: None,
|
||||||
filename: filename.to_string(),
|
filename: filename.to_string(),
|
||||||
size: payload.len() as u64,
|
size: payload.len() as u64,
|
||||||
sha256,
|
sha256,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ mod fakeroot;
|
|||||||
mod index;
|
mod index;
|
||||||
mod install;
|
mod install;
|
||||||
mod locking;
|
mod locking;
|
||||||
|
mod metadata_time;
|
||||||
mod package;
|
mod package;
|
||||||
mod planner;
|
mod planner;
|
||||||
mod runtime_env;
|
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
|
//! Package creation and archive management
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::metadata_time;
|
||||||
use crate::package::PackageSpec;
|
use crate::package::PackageSpec;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -125,6 +126,7 @@ impl Packager {
|
|||||||
|
|
||||||
fn generate_metadata_toml(&self) -> Result<()> {
|
fn generate_metadata_toml(&self) -> Result<()> {
|
||||||
let metadata_path = self.destdir.join(".metadata.toml");
|
let metadata_path = self.destdir.join(".metadata.toml");
|
||||||
|
let completed_at = metadata_time::current_utc_timestamp_string()?;
|
||||||
|
|
||||||
// Construct a simple metadata structure
|
// Construct a simple metadata structure
|
||||||
let mut map = toml::map::Map::new();
|
let mut map = toml::map::Map::new();
|
||||||
@@ -152,6 +154,10 @@ impl Packager {
|
|||||||
"license".to_string(),
|
"license".to_string(),
|
||||||
license_value(&self.spec.package.license),
|
license_value(&self.spec.package.license),
|
||||||
);
|
);
|
||||||
|
map.insert(
|
||||||
|
"completed_at".to_string(),
|
||||||
|
toml::Value::String(completed_at),
|
||||||
|
);
|
||||||
|
|
||||||
// Add provides
|
// Add provides
|
||||||
map.insert(
|
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("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("revision").and_then(|v| v.as_integer()), Some(1));
|
||||||
assert_eq!(val.get("license").and_then(|v| v.as_str()), Some("MIT"));
|
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();
|
let deps = val.get("dependencies").unwrap();
|
||||||
assert!(deps.get("runtime").unwrap().as_array().unwrap().is_empty());
|
assert!(deps.get("runtime").unwrap().as_array().unwrap().is_empty());
|
||||||
|
|||||||
@@ -837,6 +837,7 @@ mod tests {
|
|||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
version: "1.0.0".to_string(),
|
version: "1.0.0".to_string(),
|
||||||
revision: 1,
|
revision: 1,
|
||||||
|
completed_at: None,
|
||||||
filename: format!("{name}-1.0.0-1-x86_64.depot.pkg.tar.zst"),
|
filename: format!("{name}-1.0.0-1-x86_64.depot.pkg.tar.zst"),
|
||||||
size: 1024,
|
size: 1024,
|
||||||
sha256: "sha256".to_string(),
|
sha256: "sha256".to_string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user