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:
Executable
+581
@@ -0,0 +1,581 @@
|
||||
//! SQLite-based package database
|
||||
|
||||
pub mod repo;
|
||||
|
||||
use crate::package::PackageSpec;
|
||||
use crate::staging;
|
||||
use anyhow::{Context, Result};
|
||||
use rusqlite::{Connection, params};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// 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)
|
||||
if let Some(parent) = db_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let mut conn = Connection::open(db_path)?;
|
||||
init_db(&conn)?;
|
||||
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
// 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)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
version=excluded.version,
|
||||
revision=excluded.revision,
|
||||
description=excluded.description,
|
||||
homepage=excluded.homepage,
|
||||
license=excluded.license",
|
||||
params![
|
||||
spec.package.name,
|
||||
spec.package.version,
|
||||
spec.package.revision,
|
||||
spec.package.description,
|
||||
spec.package.homepage,
|
||||
spec.package.license,
|
||||
],
|
||||
)?;
|
||||
|
||||
let pkg_id: i64 = tx.query_row(
|
||||
"SELECT id FROM packages WHERE name = ?1",
|
||||
params![spec.package.name],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
// Replace provides + file + directory lists for this package.
|
||||
tx.execute(
|
||||
"DELETE FROM provides WHERE package_id = ?1",
|
||||
params![pkg_id],
|
||||
)?;
|
||||
tx.execute("DELETE FROM files WHERE package_id = ?1", params![pkg_id])?;
|
||||
tx.execute(
|
||||
"DELETE FROM directories WHERE package_id = ?1",
|
||||
params![pkg_id],
|
||||
)?;
|
||||
|
||||
// Insert provides
|
||||
for provides in &spec.alternatives.provides {
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO provides (package_id, provides_name) VALUES (?1, ?2)",
|
||||
params![pkg_id, provides],
|
||||
)?;
|
||||
}
|
||||
|
||||
// Insert files
|
||||
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],
|
||||
)?;
|
||||
}
|
||||
|
||||
// Insert directories (can be shared by multiple packages)
|
||||
for dir in &manifest.directories {
|
||||
tx.execute(
|
||||
"INSERT INTO directories (package_id, path) VALUES (?1, ?2)",
|
||||
params![pkg_id, dir],
|
||||
)?;
|
||||
}
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
println!(
|
||||
"Registered {} files and {} directories in database",
|
||||
manifest.files.len(),
|
||||
manifest.directories.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the list of files owned by an installed package.
|
||||
pub fn get_package_files(db_path: &Path, name: &str) -> Result<Vec<String>> {
|
||||
if !db_path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
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))?;
|
||||
|
||||
let mut stmt = conn.prepare("SELECT path FROM files WHERE package_id = ?1")?;
|
||||
let files: Vec<String> = stmt
|
||||
.query_map(params![pkg_id], |row| row.get(0))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Remove a package from the database and filesystem
|
||||
pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
|
||||
if !db_path.exists() {
|
||||
anyhow::bail!("Package database not found");
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
// Get package ID
|
||||
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))?;
|
||||
|
||||
// Get file list
|
||||
let mut stmt = conn.prepare("SELECT path FROM files WHERE package_id = ?1")?;
|
||||
let files: Vec<String> = stmt
|
||||
.query_map(params![pkg_id], |row| row.get(0))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
// Get directory list (sorted deepest-first for proper removal order)
|
||||
let mut stmt = conn.prepare("SELECT path FROM directories WHERE package_id = ?1")?;
|
||||
let mut directories: Vec<String> = stmt
|
||||
.query_map(params![pkg_id], |row| row.get(0))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
directories.sort_by_key(|b| std::cmp::Reverse(b.matches('/').count()));
|
||||
|
||||
// Remove files
|
||||
let mut removal_errors: Vec<String> = Vec::new();
|
||||
for file in &files {
|
||||
let path = rootfs.join(file);
|
||||
match fs::remove_file(&path) {
|
||||
Ok(()) => {
|
||||
println!(" Removed file: {}", file);
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
// Already gone, keep going.
|
||||
}
|
||||
Err(e) => {
|
||||
removal_errors.push(format!("{}: {}", file, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove directories (only if empty and not owned by another package)
|
||||
let mut dirs_removed = 0;
|
||||
for dir in &directories {
|
||||
let path = rootfs.join(dir);
|
||||
|
||||
// Skip if outside rootfs
|
||||
if !path.starts_with(rootfs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if directory is owned by another package
|
||||
let other_owners: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM directories WHERE path = ?1 AND package_id != ?2",
|
||||
params![dir, pkg_id],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
if other_owners > 0 {
|
||||
println!(" Keeping directory (owned by other package): {}", dir);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to remove (will fail if not empty, which is fine)
|
||||
match fs::remove_dir(&path) {
|
||||
Ok(()) => {
|
||||
println!(" Removed directory: {}", dir);
|
||||
dirs_removed += 1;
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
// Already gone
|
||||
}
|
||||
Err(e) if e.raw_os_error() == Some(39) || e.raw_os_error() == Some(66) => {
|
||||
// ENOTEMPTY (39 on Linux, 66 on macOS) - directory not empty
|
||||
println!(" Keeping directory (not empty): {}", dir);
|
||||
}
|
||||
Err(_) => {
|
||||
// Other errors (permission, etc.) - just skip silently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from database
|
||||
conn.execute("DELETE FROM files WHERE package_id = ?1", params![pkg_id])?;
|
||||
conn.execute(
|
||||
"DELETE FROM directories WHERE package_id = ?1",
|
||||
params![pkg_id],
|
||||
)?;
|
||||
conn.execute(
|
||||
"DELETE FROM provides WHERE package_id = ?1",
|
||||
params![pkg_id],
|
||||
)?;
|
||||
conn.execute("DELETE FROM packages WHERE id = ?1", params![pkg_id])?;
|
||||
|
||||
println!(
|
||||
"Removed {} files and {} directories",
|
||||
files.len(),
|
||||
dirs_removed
|
||||
);
|
||||
|
||||
if !removal_errors.is_empty() {
|
||||
eprintln!("Warning: failed to remove some paths:");
|
||||
for err in removal_errors {
|
||||
eprintln!(" {}", err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show information about an installed package
|
||||
pub fn show_package_info(db_path: &Path, name: &str) -> Result<()> {
|
||||
if !db_path.exists() {
|
||||
anyhow::bail!("Package database not found");
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
let (version, revision, description, homepage, license): (String, u32, String, String, String) = conn
|
||||
.query_row(
|
||||
"SELECT version, revision, description, homepage, license FROM packages WHERE name = ?1",
|
||||
params![name],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)),
|
||||
)
|
||||
.context(format!("Package '{}' not found", name))?;
|
||||
|
||||
println!("Package: {} v{}-{}", name, version, revision);
|
||||
println!("Description: {}", description);
|
||||
println!("Homepage: {}", homepage);
|
||||
println!("License: {}", license);
|
||||
|
||||
// Count files
|
||||
let file_count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM files f JOIN packages p ON f.package_id = p.id WHERE p.name = ?1",
|
||||
params![name],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
println!("Files: {}", file_count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all installed packages
|
||||
pub fn list_packages(db_path: &Path) -> Result<()> {
|
||||
if !db_path.exists() {
|
||||
println!("No packages installed.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
let mut stmt = conn.prepare("SELECT name, version FROM packages ORDER BY name")?;
|
||||
let packages = stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})?;
|
||||
|
||||
println!("{:<30} VERSION", "PACKAGE");
|
||||
println!("{}", "-".repeat(50));
|
||||
|
||||
for pkg in packages {
|
||||
let (name, version) = pkg?;
|
||||
println!("{:<30} {}", name, version);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_db(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
"
|
||||
CREATE TABLE IF NOT EXISTS packages (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL DEFAULT 1,
|
||||
description TEXT,
|
||||
homepage TEXT,
|
||||
license TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS provides (
|
||||
id INTEGER PRIMARY KEY,
|
||||
package_id INTEGER NOT NULL,
|
||||
provides_name TEXT NOT NULL,
|
||||
FOREIGN KEY (package_id) REFERENCES packages(id),
|
||||
UNIQUE(package_id, provides_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id INTEGER PRIMARY KEY,
|
||||
package_id INTEGER NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
FOREIGN KEY (package_id) REFERENCES packages(id),
|
||||
UNIQUE(path)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS directories (
|
||||
id INTEGER PRIMARY KEY,
|
||||
package_id INTEGER NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
FOREIGN KEY (package_id) REFERENCES packages(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_files_package ON files(package_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_provides_name ON provides(provides_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_directories_package ON directories(package_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_directories_path ON directories(path);
|
||||
",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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(
|
||||
db_path: &Path,
|
||||
name: &str,
|
||||
new_files: &[String],
|
||||
) -> Result<Vec<String>> {
|
||||
let old_files = get_package_files(db_path, name)?;
|
||||
let mut new_set = std::collections::HashSet::new();
|
||||
for f in new_files {
|
||||
new_set.insert(f);
|
||||
}
|
||||
|
||||
let remove_paths: Vec<String> = old_files
|
||||
.into_iter()
|
||||
.filter(|p| !new_set.contains(p))
|
||||
.collect();
|
||||
|
||||
Ok(remove_paths)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::package::{
|
||||
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn mk_spec(name: &str, version: &str) -> PackageSpec {
|
||||
PackageSpec {
|
||||
package: PackageInfo {
|
||||
name: name.into(),
|
||||
version: version.into(),
|
||||
revision: 1,
|
||||
description: "d".into(),
|
||||
homepage: "h".into(),
|
||||
license: "MIT".into(),
|
||||
},
|
||||
alternatives: Alternatives {
|
||||
provides: vec![format!("{}-virtual", name)],
|
||||
replaces: Vec::new(),
|
||||
},
|
||||
manual_sources: Vec::new(),
|
||||
source: vec![Source {
|
||||
url: "https://example.com/foo.tar.gz".into(),
|
||||
sha256: "skip".into(),
|
||||
extract_dir: "foo".into(),
|
||||
patches: Vec::new(),
|
||||
post_extract: Vec::new(),
|
||||
}],
|
||||
build: Build {
|
||||
build_type: BuildType::Custom,
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_package_updates_in_place_and_replaces_file_list() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
|
||||
let spec_v1 = mk_spec("foo", "1.0");
|
||||
let dest1 = tmp.path().join("dest1");
|
||||
std::fs::create_dir_all(dest1.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest1.join("usr/bin/foo"), "v1").unwrap();
|
||||
|
||||
register_package(&db_path, &spec_v1, &dest1).unwrap();
|
||||
|
||||
// Capture package id
|
||||
let conn = Connection::open(&db_path).unwrap();
|
||||
let id1: i64 = conn
|
||||
.query_row(
|
||||
"SELECT id FROM packages WHERE name = ?1",
|
||||
params!["foo"],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Update with different file set
|
||||
let spec_v2 = mk_spec("foo", "2.0");
|
||||
let dest2 = tmp.path().join("dest2");
|
||||
std::fs::create_dir_all(dest2.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest2.join("usr/bin/foo"), "v2").unwrap();
|
||||
std::fs::write(dest2.join("usr/bin/new_only"), "x").unwrap();
|
||||
|
||||
register_package(&db_path, &spec_v2, &dest2).unwrap();
|
||||
|
||||
let id2: i64 = conn
|
||||
.query_row(
|
||||
"SELECT id FROM packages WHERE name = ?1",
|
||||
params!["foo"],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(id1, id2);
|
||||
|
||||
let files = get_package_files(&db_path, "foo").unwrap();
|
||||
assert!(files.contains(&"usr/bin/foo".to_string()));
|
||||
assert!(files.contains(&"usr/bin/new_only".to_string()));
|
||||
|
||||
let version = get_package_version(&db_path, "foo").unwrap();
|
||||
assert_eq!(version.as_deref(), Some("2.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_package_tolerates_missing_files_and_cleans_db() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
let rootfs = tmp.path().join("root");
|
||||
std::fs::create_dir_all(&rootfs).unwrap();
|
||||
|
||||
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();
|
||||
register_package(&db_path, &spec, &dest).unwrap();
|
||||
|
||||
// Create the installed file in rootfs (one real)
|
||||
std::fs::create_dir_all(rootfs.join("usr/bin")).unwrap();
|
||||
std::fs::write(rootfs.join("usr/bin/foo"), "bin").unwrap();
|
||||
|
||||
// Inject an extra missing file into DB to ensure we tolerate it.
|
||||
let conn = Connection::open(&db_path).unwrap();
|
||||
let pkg_id: i64 = conn
|
||||
.query_row(
|
||||
"SELECT id FROM packages WHERE name = ?1",
|
||||
params!["foo"],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO files (package_id, path) VALUES (?1, ?2)",
|
||||
params![pkg_id, "usr/bin/does_not_exist"],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
remove_package(&db_path, "foo", &rootfs).unwrap();
|
||||
assert!(get_package_version(&db_path, "foo").unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_package_upgrade_removes_orphaned_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("packages.db");
|
||||
let rootfs = tmp.path().join("root");
|
||||
let tx_base = tmp.path().join("tx");
|
||||
std::fs::create_dir_all(&rootfs).unwrap();
|
||||
|
||||
// 1. Install v1: usr/bin/foo, usr/bin/shared_dir/old_file
|
||||
let spec_v1 = mk_spec("foo", "1.0");
|
||||
let dest1 = tmp.path().join("dest1");
|
||||
std::fs::create_dir_all(dest1.join("usr/bin/shared_dir")).unwrap();
|
||||
std::fs::write(dest1.join("usr/bin/foo"), "v1").unwrap();
|
||||
std::fs::write(dest1.join("usr/bin/shared_dir/old_file"), "old").unwrap();
|
||||
|
||||
register_package(&db_path, &spec_v1, &dest1).unwrap();
|
||||
let _ = crate::staging::install_atomic(&dest1, &rootfs, &tx_base, &[]).unwrap();
|
||||
|
||||
assert!(rootfs.join("usr/bin/foo").exists());
|
||||
assert!(rootfs.join("usr/bin/shared_dir/old_file").exists());
|
||||
|
||||
// 2. Prepare v2: usr/bin/foo (updated), usr/bin/new_file
|
||||
// (shared_dir/old_file is removed from spec)
|
||||
let spec_v2 = mk_spec("foo", "2.0");
|
||||
let dest2 = tmp.path().join("dest2");
|
||||
std::fs::create_dir_all(dest2.join("usr/bin")).unwrap();
|
||||
std::fs::write(dest2.join("usr/bin/foo"), "v2").unwrap();
|
||||
std::fs::write(dest2.join("usr/bin/new_file"), "new").unwrap();
|
||||
|
||||
let manifest2 = crate::staging::generate_manifest_with_dirs(&dest2).unwrap();
|
||||
let remove_paths = calculate_upgrade_paths(&db_path, "foo", &manifest2.files).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
remove_paths,
|
||||
vec!["usr/bin/shared_dir/old_file".to_string()]
|
||||
);
|
||||
|
||||
let tx = crate::staging::install_atomic(&dest2, &rootfs, &tx_base, &remove_paths).unwrap();
|
||||
register_package(&db_path, &spec_v2, &dest2).unwrap();
|
||||
tx.commit().unwrap();
|
||||
|
||||
// 3. Verify filesystem
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rootfs.join("usr/bin/foo")).unwrap(),
|
||||
"v2"
|
||||
);
|
||||
assert!(rootfs.join("usr/bin/new_file").exists());
|
||||
assert!(!rootfs.join("usr/bin/shared_dir/old_file").exists());
|
||||
|
||||
// Check DB
|
||||
let files = get_package_files(&db_path, "foo").unwrap();
|
||||
assert!(files.contains(&"usr/bin/foo".to_string()));
|
||||
assert!(files.contains(&"usr/bin/new_file".to_string()));
|
||||
assert!(!files.contains(&"usr/bin/shared_dir/old_file".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Get set of all installed package names
|
||||
pub fn get_installed_packages(db_path: &Path) -> Result<std::collections::HashSet<String>> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
let mut stmt = conn.prepare("SELECT name FROM packages")?;
|
||||
let names: HashSet<String> = stmt
|
||||
.query_map([], |row| row.get(0))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
let mut stmt = conn.prepare("SELECT provides_name FROM provides")?;
|
||||
let names: HashSet<String> = stmt
|
||||
.query_map([], |row| row.get(0))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Get version of a specific installed package
|
||||
pub fn get_package_version(db_path: &Path, name: &str) -> Result<Option<String>> {
|
||||
let conn = Connection::open(db_path)?;
|
||||
let version: Option<String> = conn
|
||||
.query_row(
|
||||
"SELECT version FROM packages WHERE name = ?1",
|
||||
params![name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.ok();
|
||||
Ok(version)
|
||||
}
|
||||
Reference in New Issue
Block a user