mirror of
https://codeberg.org/Toasterson/ips.git
synced 2026-04-10 13:20:42 +00:00
Refactor to align with Rust formatting guidelines and enhance code readability.
- Adjusted indentation and line breaks for structs, functions, and method calls to comply with Rust formatting standards. - Improved error message formatting and consistency across `PkgTreeError` instances. - Restructured long function arguments and chained calls for clarity and maintainability. - Simplified conditional statements and loops for better readability. - No functional changes introduced.
This commit is contained in:
parent
d0fcdbec20
commit
d2d1c297cc
56 changed files with 5356 additions and 2748 deletions
|
|
@ -9,8 +9,8 @@ use miette::Diagnostic;
|
|||
use thiserror::Error;
|
||||
use tracing::info;
|
||||
|
||||
use crate::actions::{Link as LinkAction, Manifest};
|
||||
use crate::actions::{Dir as DirAction, File as FileAction};
|
||||
use crate::actions::{Link as LinkAction, Manifest};
|
||||
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
pub enum InstallerError {
|
||||
|
|
@ -23,16 +23,25 @@ pub enum InstallerError {
|
|||
},
|
||||
|
||||
#[error("Absolute paths are forbidden in actions: {path}")]
|
||||
#[diagnostic(code(ips::installer_error::absolute_path_forbidden), help("Provide paths relative to the image root"))]
|
||||
#[diagnostic(
|
||||
code(ips::installer_error::absolute_path_forbidden),
|
||||
help("Provide paths relative to the image root")
|
||||
)]
|
||||
AbsolutePathForbidden { path: String },
|
||||
|
||||
#[error("Path escapes image root via traversal: {rel}")]
|
||||
#[diagnostic(code(ips::installer_error::path_outside_image), help("Remove '..' components that escape the image root"))]
|
||||
#[diagnostic(
|
||||
code(ips::installer_error::path_outside_image),
|
||||
help("Remove '..' components that escape the image root")
|
||||
)]
|
||||
PathTraversalOutsideImage { rel: String },
|
||||
|
||||
#[error("Unsupported or not yet implemented action: {action} ({reason})")]
|
||||
#[diagnostic(code(ips::installer_error::unsupported_action))]
|
||||
UnsupportedAction { action: &'static str, reason: String },
|
||||
UnsupportedAction {
|
||||
action: &'static str,
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn parse_mode(mode: &str, default: u32) -> u32 {
|
||||
|
|
@ -74,7 +83,7 @@ pub fn safe_join(image_root: &Path, rel: &str) -> Result<PathBuf, InstallerError
|
|||
Component::Prefix(_) | Component::RootDir => {
|
||||
return Err(InstallerError::AbsolutePathForbidden {
|
||||
path: rel.to_string(),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +116,10 @@ impl std::fmt::Debug for ApplyOptions {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ApplyOptions")
|
||||
.field("dry_run", &self.dry_run)
|
||||
.field("progress", &self.progress.as_ref().map(|_| "Some(callback)"))
|
||||
.field(
|
||||
"progress",
|
||||
&self.progress.as_ref().map(|_| "Some(callback)"),
|
||||
)
|
||||
.field("progress_interval", &self.progress_interval)
|
||||
.finish()
|
||||
}
|
||||
|
|
@ -115,65 +127,154 @@ impl std::fmt::Debug for ApplyOptions {
|
|||
|
||||
impl Default for ApplyOptions {
|
||||
fn default() -> Self {
|
||||
Self { dry_run: false, progress: None, progress_interval: 0 }
|
||||
Self {
|
||||
dry_run: false,
|
||||
progress: None,
|
||||
progress_interval: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Progress event emitted by apply_manifest when a callback is provided.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ProgressEvent {
|
||||
StartingPhase { phase: &'static str, total: usize },
|
||||
Progress { phase: &'static str, current: usize, total: usize },
|
||||
FinishedPhase { phase: &'static str, total: usize },
|
||||
StartingPhase {
|
||||
phase: &'static str,
|
||||
total: usize,
|
||||
},
|
||||
Progress {
|
||||
phase: &'static str,
|
||||
current: usize,
|
||||
total: usize,
|
||||
},
|
||||
FinishedPhase {
|
||||
phase: &'static str,
|
||||
total: usize,
|
||||
},
|
||||
}
|
||||
|
||||
pub type ProgressCallback = Arc<dyn Fn(ProgressEvent) + Send + Sync + 'static>;
|
||||
|
||||
/// Apply a manifest to the filesystem rooted at image_root.
|
||||
/// This function enforces ordering: directories, then files, then links, then others (no-ops for now).
|
||||
pub fn apply_manifest(image_root: &Path, manifest: &Manifest, opts: &ApplyOptions) -> Result<(), InstallerError> {
|
||||
pub fn apply_manifest(
|
||||
image_root: &Path,
|
||||
manifest: &Manifest,
|
||||
opts: &ApplyOptions,
|
||||
) -> Result<(), InstallerError> {
|
||||
let emit = |evt: ProgressEvent, cb: &Option<ProgressCallback>| {
|
||||
if let Some(cb) = cb.as_ref() { (cb)(evt); }
|
||||
if let Some(cb) = cb.as_ref() {
|
||||
(cb)(evt);
|
||||
}
|
||||
};
|
||||
|
||||
// Directories first
|
||||
let total_dirs = manifest.directories.len();
|
||||
if total_dirs > 0 { emit(ProgressEvent::StartingPhase { phase: "directories", total: total_dirs }, &opts.progress); }
|
||||
if total_dirs > 0 {
|
||||
emit(
|
||||
ProgressEvent::StartingPhase {
|
||||
phase: "directories",
|
||||
total: total_dirs,
|
||||
},
|
||||
&opts.progress,
|
||||
);
|
||||
}
|
||||
let mut i = 0usize;
|
||||
for d in &manifest.directories {
|
||||
apply_dir(image_root, d, opts)?;
|
||||
i += 1;
|
||||
if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_dirs) {
|
||||
emit(ProgressEvent::Progress { phase: "directories", current: i, total: total_dirs }, &opts.progress);
|
||||
emit(
|
||||
ProgressEvent::Progress {
|
||||
phase: "directories",
|
||||
current: i,
|
||||
total: total_dirs,
|
||||
},
|
||||
&opts.progress,
|
||||
);
|
||||
}
|
||||
}
|
||||
if total_dirs > 0 { emit(ProgressEvent::FinishedPhase { phase: "directories", total: total_dirs }, &opts.progress); }
|
||||
if total_dirs > 0 {
|
||||
emit(
|
||||
ProgressEvent::FinishedPhase {
|
||||
phase: "directories",
|
||||
total: total_dirs,
|
||||
},
|
||||
&opts.progress,
|
||||
);
|
||||
}
|
||||
|
||||
// Files next
|
||||
let total_files = manifest.files.len();
|
||||
if total_files > 0 { emit(ProgressEvent::StartingPhase { phase: "files", total: total_files }, &opts.progress); }
|
||||
if total_files > 0 {
|
||||
emit(
|
||||
ProgressEvent::StartingPhase {
|
||||
phase: "files",
|
||||
total: total_files,
|
||||
},
|
||||
&opts.progress,
|
||||
);
|
||||
}
|
||||
i = 0;
|
||||
for f_action in &manifest.files {
|
||||
apply_file(image_root, f_action, opts)?;
|
||||
i += 1;
|
||||
if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_files) {
|
||||
emit(ProgressEvent::Progress { phase: "files", current: i, total: total_files }, &opts.progress);
|
||||
emit(
|
||||
ProgressEvent::Progress {
|
||||
phase: "files",
|
||||
current: i,
|
||||
total: total_files,
|
||||
},
|
||||
&opts.progress,
|
||||
);
|
||||
}
|
||||
}
|
||||
if total_files > 0 { emit(ProgressEvent::FinishedPhase { phase: "files", total: total_files }, &opts.progress); }
|
||||
if total_files > 0 {
|
||||
emit(
|
||||
ProgressEvent::FinishedPhase {
|
||||
phase: "files",
|
||||
total: total_files,
|
||||
},
|
||||
&opts.progress,
|
||||
);
|
||||
}
|
||||
|
||||
// Links
|
||||
let total_links = manifest.links.len();
|
||||
if total_links > 0 { emit(ProgressEvent::StartingPhase { phase: "links", total: total_links }, &opts.progress); }
|
||||
if total_links > 0 {
|
||||
emit(
|
||||
ProgressEvent::StartingPhase {
|
||||
phase: "links",
|
||||
total: total_links,
|
||||
},
|
||||
&opts.progress,
|
||||
);
|
||||
}
|
||||
i = 0;
|
||||
for l in &manifest.links {
|
||||
apply_link(image_root, l, opts)?;
|
||||
i += 1;
|
||||
if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_links) {
|
||||
emit(ProgressEvent::Progress { phase: "links", current: i, total: total_links }, &opts.progress);
|
||||
emit(
|
||||
ProgressEvent::Progress {
|
||||
phase: "links",
|
||||
current: i,
|
||||
total: total_links,
|
||||
},
|
||||
&opts.progress,
|
||||
);
|
||||
}
|
||||
}
|
||||
if total_links > 0 { emit(ProgressEvent::FinishedPhase { phase: "links", total: total_links }, &opts.progress); }
|
||||
if total_links > 0 {
|
||||
emit(
|
||||
ProgressEvent::FinishedPhase {
|
||||
phase: "links",
|
||||
total: total_links,
|
||||
},
|
||||
&opts.progress,
|
||||
);
|
||||
}
|
||||
|
||||
// Other action kinds are ignored for now and left for future extension.
|
||||
Ok(())
|
||||
|
|
@ -216,7 +317,11 @@ fn ensure_parent(image_root: &Path, p: &str, opts: &ApplyOptions) -> Result<(),
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_file(image_root: &Path, f: &FileAction, opts: &ApplyOptions) -> Result<(), InstallerError> {
|
||||
fn apply_file(
|
||||
image_root: &Path,
|
||||
f: &FileAction,
|
||||
opts: &ApplyOptions,
|
||||
) -> Result<(), InstallerError> {
|
||||
let full = safe_join(image_root, &f.path)?;
|
||||
|
||||
// Ensure parent exists (directories should already be applied, but be robust)
|
||||
|
|
@ -248,7 +353,11 @@ fn apply_file(image_root: &Path, f: &FileAction, opts: &ApplyOptions) -> Result<
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_link(image_root: &Path, l: &LinkAction, opts: &ApplyOptions) -> Result<(), InstallerError> {
|
||||
fn apply_link(
|
||||
image_root: &Path,
|
||||
l: &LinkAction,
|
||||
opts: &ApplyOptions,
|
||||
) -> Result<(), InstallerError> {
|
||||
let link_path = safe_join(image_root, &l.path)?;
|
||||
|
||||
// Determine link type (default to symlink). If properties contain type=hard, create hard link.
|
||||
|
|
@ -275,7 +384,9 @@ fn apply_link(image_root: &Path, l: &LinkAction, opts: &ApplyOptions) -> Result<
|
|||
} else {
|
||||
// Symlink: require non-absolute target to avoid embedding full host paths
|
||||
if Path::new(&l.target).is_absolute() {
|
||||
return Err(InstallerError::AbsolutePathForbidden { path: l.target.clone() });
|
||||
return Err(InstallerError::AbsolutePathForbidden {
|
||||
path: l.target.clone(),
|
||||
});
|
||||
}
|
||||
// Create relative symlink as provided (do not convert to absolute to avoid embedding full paths)
|
||||
#[cfg(target_family = "unix")]
|
||||
|
|
|
|||
|
|
@ -898,7 +898,10 @@ impl Manifest {
|
|||
match serde_json::from_str::<Manifest>(&content) {
|
||||
Ok(manifest) => Ok(manifest),
|
||||
Err(err) => {
|
||||
debug!("Manifest::parse_file: Error in JSON deserialization: {}. Continuing with mtree like format parsing", err);
|
||||
debug!(
|
||||
"Manifest::parse_file: Error in JSON deserialization: {}. Continuing with mtree like format parsing",
|
||||
err
|
||||
);
|
||||
// If JSON parsing fails, fall back to string format
|
||||
Manifest::parse_string(content)
|
||||
}
|
||||
|
|
@ -933,17 +936,24 @@ impl Manifest {
|
|||
property.key = prop.as_str().to_owned();
|
||||
}
|
||||
Rule::property_value => {
|
||||
let str_val: String = prop.as_str().to_owned();
|
||||
property.value = str_val
|
||||
.replace(['\"', '\\'], "");
|
||||
let str_val: String =
|
||||
prop.as_str().to_owned();
|
||||
property.value =
|
||||
str_val.replace(['\"', '\\'], "");
|
||||
}
|
||||
_ => panic!("unexpected rule {:?} inside action expected property_name or property_value", prop.as_rule())
|
||||
_ => panic!(
|
||||
"unexpected rule {:?} inside action expected property_name or property_value",
|
||||
prop.as_rule()
|
||||
),
|
||||
}
|
||||
}
|
||||
act.properties.push(property);
|
||||
}
|
||||
Rule::EOI => (),
|
||||
_ => panic!("unexpected rule {:?} inside action expected payload, property, action_name", action.as_rule()),
|
||||
_ => panic!(
|
||||
"unexpected rule {:?} inside action expected payload, property, action_name",
|
||||
action.as_rule()
|
||||
),
|
||||
}
|
||||
}
|
||||
m.add_action(act);
|
||||
|
|
|
|||
|
|
@ -58,12 +58,17 @@ use walkdir::WalkDir;
|
|||
|
||||
pub use crate::actions::Manifest;
|
||||
// Core typed manifest
|
||||
use crate::actions::{Attr, Dependency as DependAction, File as FileAction, License as LicenseAction, Link as LinkAction, Property};
|
||||
use crate::actions::{
|
||||
Attr, Dependency as DependAction, File as FileAction, License as LicenseAction,
|
||||
Link as LinkAction, Property,
|
||||
};
|
||||
pub use crate::depend::{FileDep, GenerateOptions as DependGenerateOptions};
|
||||
pub use crate::fmri::Fmri;
|
||||
// For BaseMeta
|
||||
use crate::repository::file_backend::{FileBackend, Transaction};
|
||||
use crate::repository::{ReadableRepository, RepositoryError, RepositoryVersion, WritableRepository};
|
||||
use crate::repository::{
|
||||
ReadableRepository, RepositoryError, RepositoryVersion, WritableRepository,
|
||||
};
|
||||
use crate::transformer;
|
||||
pub use crate::transformer::TransformRule;
|
||||
|
||||
|
|
@ -87,7 +92,10 @@ pub enum IpsError {
|
|||
Io(String),
|
||||
|
||||
#[error("Unimplemented feature: {feature}")]
|
||||
#[diagnostic(code(ips::api_error::unimplemented), help("See doc/forge_docs/ips_integration.md for roadmap."))]
|
||||
#[diagnostic(
|
||||
code(ips::api_error::unimplemented),
|
||||
help("See doc/forge_docs/ips_integration.md for roadmap.")
|
||||
)]
|
||||
Unimplemented { feature: &'static str },
|
||||
}
|
||||
|
||||
|
|
@ -183,19 +191,32 @@ impl ManifestBuilder {
|
|||
let mut props = std::collections::HashMap::new();
|
||||
props.insert(
|
||||
"path".to_string(),
|
||||
Property { key: "path".to_string(), value: path.to_string() },
|
||||
Property {
|
||||
key: "path".to_string(),
|
||||
value: path.to_string(),
|
||||
},
|
||||
);
|
||||
props.insert(
|
||||
"license".to_string(),
|
||||
Property { key: "license".to_string(), value: license_name.to_string() },
|
||||
Property {
|
||||
key: "license".to_string(),
|
||||
value: license_name.to_string(),
|
||||
},
|
||||
);
|
||||
self.manifest.licenses.push(LicenseAction { payload: String::new(), properties: props });
|
||||
self.manifest.licenses.push(LicenseAction {
|
||||
payload: String::new(),
|
||||
properties: props,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a link action
|
||||
pub fn add_link(&mut self, path: &str, target: &str) -> &mut Self {
|
||||
self.manifest.links.push(LinkAction { path: path.to_string(), target: target.to_string(), properties: Default::default() });
|
||||
self.manifest.links.push(LinkAction {
|
||||
path: path.to_string(),
|
||||
target: target.to_string(),
|
||||
properties: Default::default(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +232,9 @@ impl ManifestBuilder {
|
|||
}
|
||||
/// Start a new empty builder
|
||||
pub fn new() -> Self {
|
||||
Self { manifest: Manifest::new() }
|
||||
Self {
|
||||
manifest: Manifest::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: construct a Manifest directly by scanning a prototype directory.
|
||||
|
|
@ -223,9 +246,9 @@ impl ManifestBuilder {
|
|||
proto.display()
|
||||
)));
|
||||
}
|
||||
let root = proto
|
||||
.canonicalize()
|
||||
.map_err(|e| IpsError::Io(format!("failed to canonicalize {}: {}", proto.display(), e)))?;
|
||||
let root = proto.canonicalize().map_err(|e| {
|
||||
IpsError::Io(format!("failed to canonicalize {}: {}", proto.display(), e))
|
||||
})?;
|
||||
|
||||
let mut m = Manifest::new();
|
||||
for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) {
|
||||
|
|
@ -260,10 +283,18 @@ impl ManifestBuilder {
|
|||
if let Some(fmri) = meta.fmri {
|
||||
push_attr("pkg.fmri", fmri.to_string());
|
||||
}
|
||||
if let Some(s) = meta.summary { push_attr("pkg.summary", s); }
|
||||
if let Some(c) = meta.classification { push_attr("info.classification", c); }
|
||||
if let Some(u) = meta.upstream_url { push_attr("info.upstream-url", u); }
|
||||
if let Some(su) = meta.source_url { push_attr("info.source-url", su); }
|
||||
if let Some(s) = meta.summary {
|
||||
push_attr("pkg.summary", s);
|
||||
}
|
||||
if let Some(c) = meta.classification {
|
||||
push_attr("info.classification", c);
|
||||
}
|
||||
if let Some(u) = meta.upstream_url {
|
||||
push_attr("info.upstream-url", u);
|
||||
}
|
||||
if let Some(su) = meta.source_url {
|
||||
push_attr("info.source-url", su);
|
||||
}
|
||||
if let Some(l) = meta.license {
|
||||
// Represent base license via an attribute named 'license'; callers may add dedicated license actions separately
|
||||
self.manifest.attributes.push(Attr {
|
||||
|
|
@ -310,12 +341,16 @@ impl Repository {
|
|||
pub fn open(path: &Path) -> Result<Self, IpsError> {
|
||||
// Validate by opening backend
|
||||
let _ = FileBackend::open(path)?;
|
||||
Ok(Self { path: path.to_path_buf() })
|
||||
Ok(Self {
|
||||
path: path.to_path_buf(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create(path: &Path) -> Result<Self, IpsError> {
|
||||
let _ = FileBackend::create(path, RepositoryVersion::default())?;
|
||||
Ok(Self { path: path.to_path_buf() })
|
||||
Ok(Self {
|
||||
path: path.to_path_buf(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_publisher(&self, name: &str) -> Result<bool, IpsError> {
|
||||
|
|
@ -330,7 +365,9 @@ impl Repository {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path { &self.path }
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
/// High-level publishing client for starting repository transactions.
|
||||
|
|
@ -352,14 +389,21 @@ pub struct PublisherClient {
|
|||
|
||||
impl PublisherClient {
|
||||
pub fn new(repo: Repository, publisher: impl Into<String>) -> Self {
|
||||
Self { repo, publisher: publisher.into() }
|
||||
Self {
|
||||
repo,
|
||||
publisher: publisher.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Begin a new transaction
|
||||
pub fn begin(&self) -> Result<Txn, IpsError> {
|
||||
let backend = FileBackend::open(self.repo.path())?;
|
||||
let tx = backend.begin_transaction()?; // returns Transaction bound to repo path
|
||||
Ok(Txn { backend_path: self.repo.path().to_path_buf(), tx, publisher: self.publisher.clone() })
|
||||
Ok(Txn {
|
||||
backend_path: self.repo.path().to_path_buf(),
|
||||
tx,
|
||||
publisher: self.publisher.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -389,9 +433,9 @@ pub struct Txn {
|
|||
impl Txn {
|
||||
/// Add all files from the given payload/prototype directory
|
||||
pub fn add_payload_dir(&mut self, dir: &Path) -> Result<(), IpsError> {
|
||||
let root = dir
|
||||
.canonicalize()
|
||||
.map_err(|e| IpsError::Io(format!("failed to canonicalize {}: {}", dir.display(), e)))?;
|
||||
let root = dir.canonicalize().map_err(|e| {
|
||||
IpsError::Io(format!("failed to canonicalize {}: {}", dir.display(), e))
|
||||
})?;
|
||||
for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) {
|
||||
let p = entry.path();
|
||||
if p.is_file() {
|
||||
|
|
@ -451,7 +495,11 @@ pub struct DependencyGenerator;
|
|||
impl DependencyGenerator {
|
||||
/// Compute file-level dependencies for the given manifest, using `proto` as base for local file resolution.
|
||||
/// This is a helper for callers that want to inspect raw file deps before mapping them to package FMRIs.
|
||||
pub fn file_deps(proto: &Path, manifest: &Manifest, mut opts: DependGenerateOptions) -> Result<Vec<FileDep>, IpsError> {
|
||||
pub fn file_deps(
|
||||
proto: &Path,
|
||||
manifest: &Manifest,
|
||||
mut opts: DependGenerateOptions,
|
||||
) -> Result<Vec<FileDep>, IpsError> {
|
||||
if opts.proto_dir.is_none() {
|
||||
opts.proto_dir = Some(proto.to_path_buf());
|
||||
}
|
||||
|
|
@ -463,7 +511,9 @@ impl DependencyGenerator {
|
|||
/// Intentionally not implemented in this facade: mapping raw file dependencies to package FMRIs
|
||||
/// requires repository/catalog context. Call `generate_with_repo` instead.
|
||||
pub fn generate(_proto: &Path, _manifest: &Manifest) -> Result<Manifest, IpsError> {
|
||||
Err(IpsError::Unimplemented { feature: "DependencyGenerator::generate (use generate_with_repo)" })
|
||||
Err(IpsError::Unimplemented {
|
||||
feature: "DependencyGenerator::generate (use generate_with_repo)",
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate dependencies using a repository to resolve file-level deps into package FMRIs.
|
||||
|
|
@ -562,10 +612,8 @@ impl Resolver {
|
|||
if f.version.is_none() {
|
||||
// Query repository for this package name
|
||||
let pkgs = repo.list_packages(publisher, Some(&f.name))?;
|
||||
let matches: Vec<&crate::repository::PackageInfo> = pkgs
|
||||
.iter()
|
||||
.filter(|pi| pi.fmri.name == f.name)
|
||||
.collect();
|
||||
let matches: Vec<&crate::repository::PackageInfo> =
|
||||
pkgs.iter().filter(|pi| pi.fmri.name == f.name).collect();
|
||||
if matches.len() == 1 {
|
||||
let fmri = &matches[0].fmri;
|
||||
if f.publisher.is_none() {
|
||||
|
|
@ -597,7 +645,6 @@ fn manifest_fmri(manifest: &Manifest) -> Option<Fmri> {
|
|||
None
|
||||
}
|
||||
|
||||
|
||||
/// Lint facade providing a typed, extensible rule engine with enable/disable controls.
|
||||
///
|
||||
/// Configure which rules to run, override severities, and pass rule-specific parameters.
|
||||
|
|
@ -618,8 +665,8 @@ pub struct LintConfig {
|
|||
pub reference_repos: Vec<PathBuf>,
|
||||
pub rulesets: Vec<String>,
|
||||
// Rule configurability
|
||||
pub disabled_rules: Vec<String>, // rule IDs to disable
|
||||
pub enabled_only: Option<Vec<String>>, // if Some, only these rule IDs run
|
||||
pub disabled_rules: Vec<String>, // rule IDs to disable
|
||||
pub enabled_only: Option<Vec<String>>, // if Some, only these rule IDs run
|
||||
pub severity_overrides: std::collections::HashMap<String, lint::LintSeverity>,
|
||||
pub rule_params: std::collections::HashMap<String, std::collections::HashMap<String, String>>, // rule_id -> (key->val)
|
||||
}
|
||||
|
|
@ -639,30 +686,47 @@ pub mod lint {
|
|||
#[derive(Debug, Error, Diagnostic)]
|
||||
pub enum LintIssue {
|
||||
#[error("Manifest is missing pkg.fmri or it is invalid")]
|
||||
#[diagnostic(code(ips::lint_error::missing_fmri), help("Add a valid set name=pkg.fmri value=... attribute"))]
|
||||
#[diagnostic(
|
||||
code(ips::lint_error::missing_fmri),
|
||||
help("Add a valid set name=pkg.fmri value=... attribute")
|
||||
)]
|
||||
MissingOrInvalidFmri,
|
||||
|
||||
#[error("Manifest has multiple pkg.fmri attributes")]
|
||||
#[diagnostic(code(ips::lint_error::duplicate_fmri), help("Ensure only one pkg.fmri set action is present"))]
|
||||
#[diagnostic(
|
||||
code(ips::lint_error::duplicate_fmri),
|
||||
help("Ensure only one pkg.fmri set action is present")
|
||||
)]
|
||||
DuplicateFmri,
|
||||
|
||||
#[error("Manifest is missing pkg.summary")]
|
||||
#[diagnostic(code(ips::lint_error::missing_summary), help("Add a set name=pkg.summary value=... attribute"))]
|
||||
#[diagnostic(
|
||||
code(ips::lint_error::missing_summary),
|
||||
help("Add a set name=pkg.summary value=... attribute")
|
||||
)]
|
||||
MissingSummary,
|
||||
|
||||
#[error("Dependency is missing FMRI or name")]
|
||||
#[diagnostic(code(ips::lint_error::dependency_missing_fmri), help("Each depend action should include a valid fmri (name or full fmri)"))]
|
||||
#[diagnostic(
|
||||
code(ips::lint_error::dependency_missing_fmri),
|
||||
help("Each depend action should include a valid fmri (name or full fmri)")
|
||||
)]
|
||||
DependencyMissingFmri,
|
||||
|
||||
#[error("Dependency type is missing")]
|
||||
#[diagnostic(code(ips::lint_error::dependency_missing_type), help("Set depend type (e.g., require, incorporate, optional)"))]
|
||||
#[diagnostic(
|
||||
code(ips::lint_error::dependency_missing_type),
|
||||
help("Set depend type (e.g., require, incorporate, optional)")
|
||||
)]
|
||||
DependencyMissingType,
|
||||
}
|
||||
|
||||
pub trait LintRule {
|
||||
fn id(&self) -> &'static str;
|
||||
fn description(&self) -> &'static str;
|
||||
fn default_severity(&self) -> LintSeverity { LintSeverity::Error }
|
||||
fn default_severity(&self) -> LintSeverity {
|
||||
LintSeverity::Error
|
||||
}
|
||||
/// Run this rule against the manifest. Implementors may ignore `config` (prefix with `_`) if not needed.
|
||||
/// The config carries enable/disable lists, severity overrides and rule-specific parameters for extensibility.
|
||||
fn check(&self, manifest: &Manifest, config: &LintConfig) -> Vec<miette::Report>;
|
||||
|
|
@ -670,8 +734,12 @@ pub mod lint {
|
|||
|
||||
struct RuleManifestFmri;
|
||||
impl LintRule for RuleManifestFmri {
|
||||
fn id(&self) -> &'static str { "manifest.fmri" }
|
||||
fn description(&self) -> &'static str { "Validate pkg.fmri presence/uniqueness/parse" }
|
||||
fn id(&self) -> &'static str {
|
||||
"manifest.fmri"
|
||||
}
|
||||
fn description(&self) -> &'static str {
|
||||
"Validate pkg.fmri presence/uniqueness/parse"
|
||||
}
|
||||
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
|
||||
let mut diags = Vec::new();
|
||||
let mut fmri_attr_count = 0usize;
|
||||
|
|
@ -679,13 +747,21 @@ pub mod lint {
|
|||
for attr in &manifest.attributes {
|
||||
if attr.key == "pkg.fmri" {
|
||||
fmri_attr_count += 1;
|
||||
if let Some(v) = attr.values.get(0) { fmri_text = Some(v.clone()); }
|
||||
if let Some(v) = attr.values.get(0) {
|
||||
fmri_text = Some(v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if fmri_attr_count > 1 { diags.push(miette::Report::new(LintIssue::DuplicateFmri)); }
|
||||
if fmri_attr_count > 1 {
|
||||
diags.push(miette::Report::new(LintIssue::DuplicateFmri));
|
||||
}
|
||||
match (fmri_attr_count, fmri_text) {
|
||||
(0, _) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)),
|
||||
(_, Some(txt)) => { if crate::fmri::Fmri::parse(&txt).is_err() { diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)); } },
|
||||
(_, Some(txt)) => {
|
||||
if crate::fmri::Fmri::parse(&txt).is_err() {
|
||||
diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri));
|
||||
}
|
||||
}
|
||||
(_, None) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)),
|
||||
}
|
||||
diags
|
||||
|
|
@ -694,29 +770,47 @@ pub mod lint {
|
|||
|
||||
struct RuleManifestSummary;
|
||||
impl LintRule for RuleManifestSummary {
|
||||
fn id(&self) -> &'static str { "manifest.summary" }
|
||||
fn description(&self) -> &'static str { "Validate pkg.summary presence" }
|
||||
fn id(&self) -> &'static str {
|
||||
"manifest.summary"
|
||||
}
|
||||
fn description(&self) -> &'static str {
|
||||
"Validate pkg.summary presence"
|
||||
}
|
||||
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
|
||||
let mut diags = Vec::new();
|
||||
let has_summary = manifest
|
||||
.attributes
|
||||
.iter()
|
||||
.any(|a| a.key == "pkg.summary" && a.values.iter().any(|v| !v.trim().is_empty()));
|
||||
if !has_summary { diags.push(miette::Report::new(LintIssue::MissingSummary)); }
|
||||
if !has_summary {
|
||||
diags.push(miette::Report::new(LintIssue::MissingSummary));
|
||||
}
|
||||
diags
|
||||
}
|
||||
}
|
||||
|
||||
struct RuleDependencyFields;
|
||||
impl LintRule for RuleDependencyFields {
|
||||
fn id(&self) -> &'static str { "depend.fields" }
|
||||
fn description(&self) -> &'static str { "Validate basic dependency fields" }
|
||||
fn id(&self) -> &'static str {
|
||||
"depend.fields"
|
||||
}
|
||||
fn description(&self) -> &'static str {
|
||||
"Validate basic dependency fields"
|
||||
}
|
||||
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
|
||||
let mut diags = Vec::new();
|
||||
for dep in &manifest.dependencies {
|
||||
let fmri_ok = dep.fmri.as_ref().map(|f| !f.name.trim().is_empty()).unwrap_or(false);
|
||||
if !fmri_ok { diags.push(miette::Report::new(LintIssue::DependencyMissingFmri)); }
|
||||
if dep.dependency_type.trim().is_empty() { diags.push(miette::Report::new(LintIssue::DependencyMissingType)); }
|
||||
let fmri_ok = dep
|
||||
.fmri
|
||||
.as_ref()
|
||||
.map(|f| !f.name.trim().is_empty())
|
||||
.unwrap_or(false);
|
||||
if !fmri_ok {
|
||||
diags.push(miette::Report::new(LintIssue::DependencyMissingFmri));
|
||||
}
|
||||
if dep.dependency_type.trim().is_empty() {
|
||||
diags.push(miette::Report::new(LintIssue::DependencyMissingType));
|
||||
}
|
||||
}
|
||||
diags
|
||||
}
|
||||
|
|
@ -735,7 +829,8 @@ pub mod lint {
|
|||
let set: std::collections::HashSet<&str> = only.iter().map(|s| s.as_str()).collect();
|
||||
return set.contains(rule_id);
|
||||
}
|
||||
let disabled: std::collections::HashSet<&str> = cfg.disabled_rules.iter().map(|s| s.as_str()).collect();
|
||||
let disabled: std::collections::HashSet<&str> =
|
||||
cfg.disabled_rules.iter().map(|s| s.as_str()).collect();
|
||||
!disabled.contains(rule_id)
|
||||
}
|
||||
|
||||
|
|
@ -751,7 +846,10 @@ pub mod lint {
|
|||
/// assert!(diags.is_empty());
|
||||
/// # Ok::<(), ips::IpsError>(())
|
||||
/// ```
|
||||
pub fn lint_manifest(manifest: &Manifest, config: &LintConfig) -> Result<Vec<miette::Report>, IpsError> {
|
||||
pub fn lint_manifest(
|
||||
manifest: &Manifest,
|
||||
config: &LintConfig,
|
||||
) -> Result<Vec<miette::Report>, IpsError> {
|
||||
let mut diags: Vec<miette::Report> = Vec::new();
|
||||
for rule in default_rules().into_iter() {
|
||||
if rule_enabled(rule.id(), config) {
|
||||
|
|
@ -769,7 +867,11 @@ mod tests {
|
|||
|
||||
fn make_manifest_with_fmri(fmri_str: &str) -> Manifest {
|
||||
let mut m = Manifest::new();
|
||||
m.attributes.push(Attr { key: "pkg.fmri".into(), values: vec![fmri_str.to_string()], properties: Default::default() });
|
||||
m.attributes.push(Attr {
|
||||
key: "pkg.fmri".into(),
|
||||
values: vec![fmri_str.to_string()],
|
||||
properties: Default::default(),
|
||||
});
|
||||
m
|
||||
}
|
||||
|
||||
|
|
@ -799,14 +901,17 @@ mod tests {
|
|||
let fmri = dep.fmri.as_ref().unwrap();
|
||||
assert_eq!(fmri.name, "pkgA");
|
||||
assert_eq!(fmri.publisher.as_deref(), Some("pub"));
|
||||
assert!(fmri.version.is_some(), "expected version to be filled from provider");
|
||||
assert!(
|
||||
fmri.version.is_some(),
|
||||
"expected version to be filled from provider"
|
||||
);
|
||||
assert_eq!(fmri.version.as_ref().unwrap().to_string(), "1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_uses_repository_for_provider() {
|
||||
use crate::repository::file_backend::FileBackend;
|
||||
use crate::repository::RepositoryVersion;
|
||||
use crate::repository::file_backend::FileBackend;
|
||||
|
||||
// Create a temporary repository and add a publisher
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
|
@ -816,7 +921,11 @@ mod tests {
|
|||
|
||||
// Publish provider package pkgA@1.0
|
||||
let mut provider = Manifest::new();
|
||||
provider.attributes.push(Attr { key: "pkg.fmri".into(), values: vec!["pkg://pub/pkgA@1.0".to_string()], properties: Default::default() });
|
||||
provider.attributes.push(Attr {
|
||||
key: "pkg.fmri".into(),
|
||||
values: vec!["pkg://pub/pkgA@1.0".to_string()],
|
||||
properties: Default::default(),
|
||||
});
|
||||
let mut tx = backend.begin_transaction().unwrap();
|
||||
tx.update_manifest(provider);
|
||||
tx.set_publisher("pub");
|
||||
|
|
@ -854,8 +963,16 @@ mod tests {
|
|||
#[test]
|
||||
fn lint_accepts_valid_manifest() {
|
||||
let mut m = Manifest::new();
|
||||
m.attributes.push(Attr { key: "pkg.fmri".into(), values: vec!["pkg://pub/name@1.0".to_string()], properties: Default::default() });
|
||||
m.attributes.push(Attr { key: "pkg.summary".into(), values: vec!["A package".to_string()], properties: Default::default() });
|
||||
m.attributes.push(Attr {
|
||||
key: "pkg.fmri".into(),
|
||||
values: vec!["pkg://pub/name@1.0".to_string()],
|
||||
properties: Default::default(),
|
||||
});
|
||||
m.attributes.push(Attr {
|
||||
key: "pkg.summary".into(),
|
||||
values: vec!["A package".to_string()],
|
||||
properties: Default::default(),
|
||||
});
|
||||
let cfg = LintConfig::default();
|
||||
let diags = lint::lint_manifest(&m, &cfg).unwrap();
|
||||
assert!(diags.is_empty(), "unexpected diags: {:?}", diags);
|
||||
|
|
@ -865,14 +982,22 @@ mod tests {
|
|||
fn lint_disable_summary_rule() {
|
||||
// Manifest with valid fmri but missing summary
|
||||
let mut m = Manifest::new();
|
||||
m.attributes.push(Attr { key: "pkg.fmri".into(), values: vec!["pkg://pub/name@1.0".to_string()], properties: Default::default() });
|
||||
m.attributes.push(Attr {
|
||||
key: "pkg.fmri".into(),
|
||||
values: vec!["pkg://pub/name@1.0".to_string()],
|
||||
properties: Default::default(),
|
||||
});
|
||||
|
||||
// Disable the summary rule; expect no diagnostics
|
||||
let mut cfg = LintConfig::default();
|
||||
cfg.disabled_rules = vec!["manifest.summary".to_string()];
|
||||
let diags = lint::lint_manifest(&m, &cfg).unwrap();
|
||||
// fmri is valid, dependencies empty, summary rule disabled => no diags
|
||||
assert!(diags.is_empty(), "expected no diagnostics when summary rule disabled, got: {:?}", diags);
|
||||
assert!(
|
||||
diags.is_empty(),
|
||||
"expected no diagnostics when summary rule disabled, got: {:?}",
|
||||
diags
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -889,14 +1014,29 @@ mod tests {
|
|||
let m = b.build();
|
||||
|
||||
// Validate attributes include fmri and summary
|
||||
assert!(m.attributes.iter().any(|a| a.key == "pkg.fmri" && a.values.get(0).map(|v| v == &fmri.to_string()).unwrap_or(false)));
|
||||
assert!(m.attributes.iter().any(|a| a.key == "pkg.summary" && a.values.get(0).map(|v| v == "Summary").unwrap_or(false)));
|
||||
assert!(m.attributes.iter().any(|a| {
|
||||
a.key == "pkg.fmri"
|
||||
&& a.values
|
||||
.get(0)
|
||||
.map(|v| v == &fmri.to_string())
|
||||
.unwrap_or(false)
|
||||
}));
|
||||
assert!(
|
||||
m.attributes.iter().any(|a| a.key == "pkg.summary"
|
||||
&& a.values.get(0).map(|v| v == "Summary").unwrap_or(false))
|
||||
);
|
||||
|
||||
// Validate license
|
||||
assert_eq!(m.licenses.len(), 1);
|
||||
let lic = &m.licenses[0];
|
||||
assert_eq!(lic.properties.get("path").map(|p| p.value.as_str()), Some("LICENSE"));
|
||||
assert_eq!(lic.properties.get("license").map(|p| p.value.as_str()), Some("MIT"));
|
||||
assert_eq!(
|
||||
lic.properties.get("path").map(|p| p.value.as_str()),
|
||||
Some("LICENSE")
|
||||
);
|
||||
assert_eq!(
|
||||
lic.properties.get("license").map(|p| p.value.as_str()),
|
||||
Some("MIT")
|
||||
);
|
||||
|
||||
// Validate link
|
||||
assert_eq!(m.links.len(), 1);
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ use miette::Diagnostic;
|
|||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error as StdError;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::error::Error as StdError;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
|
|
@ -27,10 +27,16 @@ pub struct DependError {
|
|||
|
||||
impl DependError {
|
||||
fn new(message: impl Into<String>) -> Self {
|
||||
Self { message: message.into(), source: None }
|
||||
Self {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
fn with_source(message: impl Into<String>, source: Box<dyn StdError + Send + Sync>) -> Self {
|
||||
Self { message: message.into(), source: Some(source) }
|
||||
Self {
|
||||
message: message.into(),
|
||||
source: Some(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,16 +90,26 @@ pub struct FileDep {
|
|||
}
|
||||
|
||||
/// Convert manifest file actions into FileDep entries (ELF only for now).
|
||||
pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &GenerateOptions) -> Result<Vec<FileDep>> {
|
||||
pub fn generate_file_dependencies_from_manifest(
|
||||
manifest: &Manifest,
|
||||
opts: &GenerateOptions,
|
||||
) -> Result<Vec<FileDep>> {
|
||||
let mut out = Vec::new();
|
||||
let bypass = compile_bypass(&opts.bypass_patterns)?;
|
||||
|
||||
for f in &manifest.files {
|
||||
// Determine installed path (manifests typically do not start with '/').
|
||||
let installed_path = if f.path.starts_with('/') { f.path.clone() } else { format!("/{}", f.path) };
|
||||
let installed_path = if f.path.starts_with('/') {
|
||||
f.path.clone()
|
||||
} else {
|
||||
format!("/{}", f.path)
|
||||
};
|
||||
|
||||
if should_bypass(&installed_path, &bypass) {
|
||||
debug!("bypassing dependency generation for {} per patterns", installed_path);
|
||||
debug!(
|
||||
"bypassing dependency generation for {} per patterns",
|
||||
installed_path
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -142,16 +158,28 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene
|
|||
// Normalize /bin -> /usr/bin
|
||||
let interp_path = normalize_bin_path(&interp);
|
||||
if !interp_path.starts_with('/') {
|
||||
warn!("Script shebang for {} specifies non-absolute interpreter: {}", installed_path, interp_path);
|
||||
warn!(
|
||||
"Script shebang for {} specifies non-absolute interpreter: {}",
|
||||
installed_path, interp_path
|
||||
);
|
||||
} else {
|
||||
// Derive dir and base name
|
||||
let (dir, base) = split_dir_base(&interp_path);
|
||||
if let Some(dir) = dir {
|
||||
out.push(FileDep { kind: FileDepKind::Script { base_name: base.to_string(), run_paths: vec![dir.to_string()], installed_path: installed_path.clone() } });
|
||||
out.push(FileDep {
|
||||
kind: FileDepKind::Script {
|
||||
base_name: base.to_string(),
|
||||
run_paths: vec![dir.to_string()],
|
||||
installed_path: installed_path.clone(),
|
||||
},
|
||||
});
|
||||
// If Python interpreter, perform Python analysis
|
||||
if interp_path.contains("python") {
|
||||
if let Some((maj, min)) = infer_python_version_from_paths(&installed_path, Some(&interp_path)) {
|
||||
let mut pydeps = process_python(&bytes, &installed_path, (maj, min), opts);
|
||||
if let Some((maj, min)) =
|
||||
infer_python_version_from_paths(&installed_path, Some(&interp_path))
|
||||
{
|
||||
let mut pydeps =
|
||||
process_python(&bytes, &installed_path, (maj, min), opts);
|
||||
out.append(&mut pydeps);
|
||||
}
|
||||
}
|
||||
|
|
@ -171,7 +199,13 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene
|
|||
if exec_path.starts_with('/') {
|
||||
let (dir, base) = split_dir_base(&exec_path);
|
||||
if let Some(dir) = dir {
|
||||
out.push(FileDep { kind: FileDepKind::Script { base_name: base.to_string(), run_paths: vec![dir.to_string()], installed_path: installed_path.clone() } });
|
||||
out.push(FileDep {
|
||||
kind: FileDepKind::Script {
|
||||
base_name: base.to_string(),
|
||||
run_paths: vec![dir.to_string()],
|
||||
installed_path: installed_path.clone(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -183,14 +217,19 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene
|
|||
}
|
||||
|
||||
/// Insert default runpaths into provided runpaths based on PD_DEFAULT_RUNPATH token
|
||||
fn insert_default_runpath(defaults: &[String], provided: &[String]) -> std::result::Result<Vec<String>, DependError> {
|
||||
fn insert_default_runpath(
|
||||
defaults: &[String],
|
||||
provided: &[String],
|
||||
) -> std::result::Result<Vec<String>, DependError> {
|
||||
let mut out = Vec::new();
|
||||
let mut token_count = 0;
|
||||
for p in provided {
|
||||
if p == PD_DEFAULT_RUNPATH {
|
||||
token_count += 1;
|
||||
if token_count > 1 {
|
||||
return Err(DependError::new("Multiple PD_DEFAULT_RUNPATH tokens in runpath override"));
|
||||
return Err(DependError::new(
|
||||
"Multiple PD_DEFAULT_RUNPATH tokens in runpath override",
|
||||
));
|
||||
}
|
||||
out.extend_from_slice(defaults);
|
||||
} else {
|
||||
|
|
@ -208,7 +247,9 @@ fn insert_default_runpath(defaults: &[String], provided: &[String]) -> std::resu
|
|||
fn compile_bypass(patterns: &[String]) -> Result<Vec<Regex>> {
|
||||
let mut out = Vec::new();
|
||||
for p in patterns {
|
||||
out.push(Regex::new(p).map_err(|e| DependError::with_source(format!("invalid bypass pattern: {}", p), Box::new(e)))?);
|
||||
out.push(Regex::new(p).map_err(|e| {
|
||||
DependError::with_source(format!("invalid bypass pattern: {}", p), Box::new(e))
|
||||
})?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
|
@ -259,11 +300,18 @@ fn process_elf(bytes: &[u8], installed_path: &str, opts: &GenerateOptions) -> Ve
|
|||
}
|
||||
} else {
|
||||
// If no override, prefer DT_RUNPATH if present else defaults
|
||||
if runpaths.is_empty() { defaults.clone() } else { runpaths.clone() }
|
||||
if runpaths.is_empty() {
|
||||
defaults.clone()
|
||||
} else {
|
||||
runpaths.clone()
|
||||
}
|
||||
};
|
||||
|
||||
// Expand $ORIGIN
|
||||
let origin = Path::new(installed_path).parent().map(|p| p.display().to_string()).unwrap_or_else(|| "/".to_string());
|
||||
let origin = Path::new(installed_path)
|
||||
.parent()
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|| "/".to_string());
|
||||
let expanded: Vec<String> = effective
|
||||
.into_iter()
|
||||
.map(|p| p.replace("$ORIGIN", &origin))
|
||||
|
|
@ -271,7 +319,13 @@ fn process_elf(bytes: &[u8], installed_path: &str, opts: &GenerateOptions) -> Ve
|
|||
|
||||
// Emit FileDep for each DT_NEEDED base name
|
||||
for bn in needed.drain(..) {
|
||||
out.push(FileDep { kind: FileDepKind::Elf { base_name: bn, run_paths: expanded.clone(), installed_path: installed_path.to_string() } });
|
||||
out.push(FileDep {
|
||||
kind: FileDepKind::Elf {
|
||||
base_name: bn,
|
||||
run_paths: expanded.clone(),
|
||||
installed_path: installed_path.to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) => warn!("ELF parse error for {}: {}", installed_path, err),
|
||||
|
|
@ -292,7 +346,11 @@ pub fn resolve_dependencies<R: ReadableRepository>(
|
|||
|
||||
for fd in file_deps {
|
||||
match &fd.kind {
|
||||
FileDepKind::Elf { base_name, run_paths, .. } => {
|
||||
FileDepKind::Elf {
|
||||
base_name,
|
||||
run_paths,
|
||||
..
|
||||
} => {
|
||||
let mut providers: Vec<Fmri> = Vec::new();
|
||||
for dir in run_paths {
|
||||
let full = normalize_join(dir, base_name);
|
||||
|
|
@ -330,7 +388,11 @@ pub fn resolve_dependencies<R: ReadableRepository>(
|
|||
// unresolved -> skip for now; future: emit analysis warnings
|
||||
}
|
||||
}
|
||||
FileDepKind::Script { base_name, run_paths, .. } => {
|
||||
FileDepKind::Script {
|
||||
base_name,
|
||||
run_paths,
|
||||
..
|
||||
} => {
|
||||
let mut providers: Vec<Fmri> = Vec::new();
|
||||
for dir in run_paths {
|
||||
let full = normalize_join(dir, base_name);
|
||||
|
|
@ -366,7 +428,11 @@ pub fn resolve_dependencies<R: ReadableRepository>(
|
|||
} else {
|
||||
}
|
||||
}
|
||||
FileDepKind::Python { base_names, run_paths, .. } => {
|
||||
FileDepKind::Python {
|
||||
base_names,
|
||||
run_paths,
|
||||
..
|
||||
} => {
|
||||
let mut providers: Vec<Fmri> = Vec::new();
|
||||
for dir in run_paths {
|
||||
for base in base_names {
|
||||
|
|
@ -418,7 +484,10 @@ fn normalize_join(dir: &str, base: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn build_path_provider_map<R: ReadableRepository>(repo: &R, publisher: Option<&str>) -> Result<HashMap<String, Vec<Fmri>>> {
|
||||
fn build_path_provider_map<R: ReadableRepository>(
|
||||
repo: &R,
|
||||
publisher: Option<&str>,
|
||||
) -> Result<HashMap<String, Vec<Fmri>>> {
|
||||
// Ask repo to show contents for all packages (files only)
|
||||
let contents = repo
|
||||
.show_contents(publisher, None, Some(&["file".to_string()]))
|
||||
|
|
@ -429,14 +498,21 @@ fn build_path_provider_map<R: ReadableRepository>(repo: &R, publisher: Option<&s
|
|||
let fmri = match pc.package_id.parse::<Fmri>() {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
warn!("Skipping package with invalid FMRI {}: {}", pc.package_id, e);
|
||||
warn!(
|
||||
"Skipping package with invalid FMRI {}: {}",
|
||||
pc.package_id, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(files) = pc.files {
|
||||
for p in files {
|
||||
// Ensure leading slash
|
||||
let key = if p.starts_with('/') { p } else { format!("/{}", p) };
|
||||
let key = if p.starts_with('/') {
|
||||
p
|
||||
} else {
|
||||
format!("/{}", p)
|
||||
};
|
||||
map.entry(key).or_default().push(fmri.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -444,7 +520,6 @@ fn build_path_provider_map<R: ReadableRepository>(repo: &R, publisher: Option<&s
|
|||
Ok(map)
|
||||
}
|
||||
|
||||
|
||||
// --- Helpers for script processing ---
|
||||
fn parse_shebang(bytes: &[u8]) -> Option<String> {
|
||||
if bytes.len() < 2 || bytes[0] != b'#' || bytes[1] != b'!' {
|
||||
|
|
@ -501,7 +576,6 @@ fn split_dir_base(path: &str) -> (Option<&str>, &str) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fn looks_like_smf_manifest(bytes: &[u8]) -> bool {
|
||||
// Very lightweight detection: SMF manifests are XML files with a <service_bundle ...> root
|
||||
// We do a lossy UTF-8 conversion and look for the tag to avoid a full XML parser.
|
||||
|
|
@ -510,7 +584,10 @@ fn looks_like_smf_manifest(bytes: &[u8]) -> bool {
|
|||
}
|
||||
|
||||
// --- Python helpers ---
|
||||
fn infer_python_version_from_paths(installed_path: &str, shebang_path: Option<&str>) -> Option<(u8, u8)> {
|
||||
fn infer_python_version_from_paths(
|
||||
installed_path: &str,
|
||||
shebang_path: Option<&str>,
|
||||
) -> Option<(u8, u8)> {
|
||||
// Prefer version implied by installed path under /usr/lib/pythonX.Y
|
||||
if let Ok(re) = Regex::new(r"^/usr/lib/python(\d+)\.(\d+)(/|$)") {
|
||||
if let Some(c) = re.captures(installed_path) {
|
||||
|
|
@ -526,7 +603,9 @@ fn infer_python_version_from_paths(installed_path: &str, shebang_path: Option<&s
|
|||
if let Ok(re) = Regex::new(r"python(\d+)\.(\d+)") {
|
||||
if let Some(c) = re.captures(sb) {
|
||||
if let (Some(ma), Some(mi)) = (c.get(1), c.get(2)) {
|
||||
if let (Ok(maj), Ok(min)) = (ma.as_str().parse::<u8>(), mi.as_str().parse::<u8>()) {
|
||||
if let (Ok(maj), Ok(min)) =
|
||||
(ma.as_str().parse::<u8>(), mi.as_str().parse::<u8>())
|
||||
{
|
||||
return Some((maj, min));
|
||||
}
|
||||
}
|
||||
|
|
@ -580,7 +659,12 @@ fn collect_python_imports(src: &str) -> Vec<String> {
|
|||
mods
|
||||
}
|
||||
|
||||
fn process_python(bytes: &[u8], installed_path: &str, version: (u8, u8), opts: &GenerateOptions) -> Vec<FileDep> {
|
||||
fn process_python(
|
||||
bytes: &[u8],
|
||||
installed_path: &str,
|
||||
version: (u8, u8),
|
||||
opts: &GenerateOptions,
|
||||
) -> Vec<FileDep> {
|
||||
let text = String::from_utf8_lossy(bytes);
|
||||
let imports = collect_python_imports(&text);
|
||||
if imports.is_empty() {
|
||||
|
|
@ -591,11 +675,21 @@ fn process_python(bytes: &[u8], installed_path: &str, version: (u8, u8), opts: &
|
|||
for m in imports {
|
||||
let py = format!("{}.py", m);
|
||||
let so = format!("{}.so", m);
|
||||
if !base_names.contains(&py) { base_names.push(py); }
|
||||
if !base_names.contains(&so) { base_names.push(so); }
|
||||
if !base_names.contains(&py) {
|
||||
base_names.push(py);
|
||||
}
|
||||
if !base_names.contains(&so) {
|
||||
base_names.push(so);
|
||||
}
|
||||
}
|
||||
let run_paths = compute_python_runpaths(version, opts);
|
||||
vec![FileDep { kind: FileDepKind::Python { base_names, run_paths, installed_path: installed_path.to_string() } }]
|
||||
vec![FileDep {
|
||||
kind: FileDepKind::Python {
|
||||
base_names,
|
||||
run_paths,
|
||||
installed_path: installed_path.to_string(),
|
||||
},
|
||||
}]
|
||||
}
|
||||
|
||||
// --- SMF helpers ---
|
||||
|
|
@ -608,7 +702,9 @@ fn extract_smf_execs(bytes: &[u8]) -> Vec<String> {
|
|||
let m = cap.get(1).or_else(|| cap.get(2));
|
||||
if let Some(v) = m {
|
||||
let val = v.as_str().to_string();
|
||||
if !out.contains(&val) { out.push(val); }
|
||||
if !out.contains(&val) {
|
||||
out.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ impl Digest {
|
|||
x => {
|
||||
return Err(DigestError::UnknownAlgorithm {
|
||||
algorithm: x.to_string(),
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -152,7 +152,9 @@ pub enum DigestError {
|
|||
#[error("hashing algorithm {algorithm:?} is not known by this library")]
|
||||
#[diagnostic(
|
||||
code(ips::digest_error::unknown_algorithm),
|
||||
help("Use one of the supported algorithms: sha1, sha256t, sha512t, sha512t_256, sha3256t, sha3512t_256, sha3512t")
|
||||
help(
|
||||
"Use one of the supported algorithms: sha1, sha256t, sha512t, sha512t_256, sha3256t, sha3512t_256, sha3512t"
|
||||
)
|
||||
)]
|
||||
UnknownAlgorithm { algorithm: String },
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::path::Path;
|
||||
|
||||
use crate::actions::executors::{apply_manifest, ApplyOptions, InstallerError};
|
||||
use crate::actions::Manifest;
|
||||
use crate::actions::executors::{ApplyOptions, InstallerError, apply_manifest};
|
||||
use crate::solver::InstallPlan;
|
||||
|
||||
/// ActionPlan represents a merged list of actions across all manifests
|
||||
|
|
@ -50,12 +50,20 @@ mod tests {
|
|||
#[test]
|
||||
fn build_and_apply_empty_plan_dry_run() {
|
||||
// Empty install plan should produce empty action plan and apply should be no-op.
|
||||
let plan = SInstallPlan { add: vec![], remove: vec![], update: vec![], reasons: vec![] };
|
||||
let plan = SInstallPlan {
|
||||
add: vec![],
|
||||
remove: vec![],
|
||||
update: vec![],
|
||||
reasons: vec![],
|
||||
};
|
||||
let ap = ActionPlan::from_install_plan(&plan);
|
||||
assert!(ap.manifest.directories.is_empty());
|
||||
assert!(ap.manifest.files.is_empty());
|
||||
assert!(ap.manifest.links.is_empty());
|
||||
let opts = ApplyOptions { dry_run: true, ..Default::default() };
|
||||
let opts = ApplyOptions {
|
||||
dry_run: true,
|
||||
..Default::default()
|
||||
};
|
||||
let root = Path::new("/tmp/ips_image_test_nonexistent_root");
|
||||
// Even if root doesn't exist, dry_run should not perform any IO and succeed.
|
||||
let res = ap.apply(root, &opts);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
use crate::actions::{Manifest};
|
||||
use crate::actions::Manifest;
|
||||
use crate::fmri::Fmri;
|
||||
use crate::repository::catalog::{CatalogManager, CatalogPart, PackageVersionEntry};
|
||||
use lz4::{Decoder as Lz4Decoder, EncoderBuilder as Lz4EncoderBuilder};
|
||||
use miette::Diagnostic;
|
||||
use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use tracing::{info, warn, trace};
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use lz4::{Decoder as Lz4Decoder, EncoderBuilder as Lz4EncoderBuilder};
|
||||
use std::collections::HashMap;
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
/// Table definition for the catalog database
|
||||
/// Key: stem@version
|
||||
|
|
@ -27,7 +27,6 @@ pub const OBSOLETED_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("
|
|||
/// Value: version string as bytes (same format as Fmri::version())
|
||||
pub const INCORPORATE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("incorporate");
|
||||
|
||||
|
||||
/// Errors that can occur when working with the image catalog
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
pub enum CatalogError {
|
||||
|
|
@ -66,8 +65,12 @@ pub type Result<T> = std::result::Result<T, CatalogError>;
|
|||
// Internal helpers for (de)compressing manifest JSON payloads stored in redb
|
||||
fn is_likely_json(bytes: &[u8]) -> bool {
|
||||
let mut i = 0;
|
||||
while i < bytes.len() && matches!(bytes[i], b' ' | b'\n' | b'\r' | b'\t') { i += 1; }
|
||||
if i >= bytes.len() { return false; }
|
||||
while i < bytes.len() && matches!(bytes[i], b' ' | b'\n' | b'\r' | b'\t') {
|
||||
i += 1;
|
||||
}
|
||||
if i >= bytes.len() {
|
||||
return false;
|
||||
}
|
||||
matches!(bytes[i], b'{' | b'[')
|
||||
}
|
||||
|
||||
|
|
@ -144,35 +147,56 @@ impl ImageCatalog {
|
|||
// Determine which table to dump and open the appropriate database
|
||||
match table_name {
|
||||
"catalog" => {
|
||||
let db = Database::open(&self.db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||
let tx = db.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
let db = Database::open(&self.db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||
})?;
|
||||
let tx = db.begin_read().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to begin transaction: {}", e))
|
||||
})?;
|
||||
self.dump_catalog_table(&tx)?;
|
||||
}
|
||||
"obsoleted" => {
|
||||
let db = Database::open(&self.obsoleted_db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
||||
let tx = db.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
let db = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||
})?;
|
||||
let tx = db.begin_read().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to begin transaction: {}", e))
|
||||
})?;
|
||||
self.dump_obsoleted_table(&tx)?;
|
||||
}
|
||||
"incorporate" => {
|
||||
let db = Database::open(&self.db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||
let tx = db.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
let db = Database::open(&self.db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||
})?;
|
||||
let tx = db.begin_read().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to begin transaction: {}", e))
|
||||
})?;
|
||||
// Simple dump of incorporate locks
|
||||
if let Ok(table) = tx.open_table(INCORPORATE_TABLE) {
|
||||
for entry in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate incorporate table: {}", e)))? {
|
||||
let (k, v) = entry.map_err(|e| CatalogError::Database(format!("Failed to read incorporate table entry: {}", e)))?;
|
||||
for entry in table.iter().map_err(|e| {
|
||||
CatalogError::Database(format!(
|
||||
"Failed to iterate incorporate table: {}",
|
||||
e
|
||||
))
|
||||
})? {
|
||||
let (k, v) = entry.map_err(|e| {
|
||||
CatalogError::Database(format!(
|
||||
"Failed to read incorporate table entry: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let stem = k.value();
|
||||
let ver = String::from_utf8_lossy(v.value());
|
||||
println!("{} -> {}", stem, ver);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err(CatalogError::Database(format!("Unknown table: {}", table_name))),
|
||||
_ => {
|
||||
return Err(CatalogError::Database(format!(
|
||||
"Unknown table: {}",
|
||||
table_name
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -181,17 +205,21 @@ impl ImageCatalog {
|
|||
/// Dump the contents of all tables to stdout for debugging
|
||||
pub fn dump_all_tables(&self) -> Result<()> {
|
||||
// Catalog DB
|
||||
let db_cat = Database::open(&self.db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||
let tx_cat = db_cat.begin_read()
|
||||
let db_cat = Database::open(&self.db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||
})?;
|
||||
let tx_cat = db_cat
|
||||
.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
println!("=== CATALOG TABLE ===");
|
||||
let _ = self.dump_catalog_table(&tx_cat);
|
||||
|
||||
// Obsoleted DB
|
||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
||||
let tx_obs = db_obs.begin_read()
|
||||
let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||
})?;
|
||||
let tx_obs = db_obs
|
||||
.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
println!("\n=== OBSOLETED TABLE ===");
|
||||
let _ = self.dump_obsoleted_table(&tx_obs);
|
||||
|
|
@ -204,15 +232,24 @@ impl ImageCatalog {
|
|||
match tx.open_table(CATALOG_TABLE) {
|
||||
Ok(table) => {
|
||||
let mut count = 0;
|
||||
for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? {
|
||||
let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?;
|
||||
for entry_result in table.iter().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to iterate catalog table: {}", e))
|
||||
})? {
|
||||
let (key, value) = entry_result.map_err(|e| {
|
||||
CatalogError::Database(format!(
|
||||
"Failed to get entry from catalog table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let key_str = key.value();
|
||||
|
||||
// Try to deserialize the manifest (supports JSON or LZ4-compressed JSON)
|
||||
match decode_manifest_bytes(value.value()) {
|
||||
Ok(manifest) => {
|
||||
// Extract the publisher from the FMRI attribute
|
||||
let publisher = manifest.attributes.iter()
|
||||
let publisher = manifest
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|attr| attr.key == "pkg.fmri")
|
||||
.and_then(|attr| attr.values.get(0).cloned())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
|
@ -223,7 +260,7 @@ impl ImageCatalog {
|
|||
println!(" Files: {}", manifest.files.len());
|
||||
println!(" Directories: {}", manifest.directories.len());
|
||||
println!(" Dependencies: {}", manifest.dependencies.len());
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Key: {}", key_str);
|
||||
println!(" Error deserializing manifest: {}", e);
|
||||
|
|
@ -233,10 +270,13 @@ impl ImageCatalog {
|
|||
}
|
||||
println!("Total entries in catalog table: {}", count);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error opening catalog table: {}", e);
|
||||
Err(CatalogError::Database(format!("Failed to open catalog table: {}", e)))
|
||||
Err(CatalogError::Database(format!(
|
||||
"Failed to open catalog table: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -246,8 +286,15 @@ impl ImageCatalog {
|
|||
match tx.open_table(OBSOLETED_TABLE) {
|
||||
Ok(table) => {
|
||||
let mut count = 0;
|
||||
for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? {
|
||||
let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?;
|
||||
for entry_result in table.iter().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e))
|
||||
})? {
|
||||
let (key, _) = entry_result.map_err(|e| {
|
||||
CatalogError::Database(format!(
|
||||
"Failed to get entry from obsoleted table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let key_str = key.value();
|
||||
|
||||
println!("Key: {}", key_str);
|
||||
|
|
@ -255,27 +302,33 @@ impl ImageCatalog {
|
|||
}
|
||||
println!("Total entries in obsoleted table: {}", count);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error opening obsoleted table: {}", e);
|
||||
Err(CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))
|
||||
Err(CatalogError::Database(format!(
|
||||
"Failed to open obsoleted table: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get database statistics
|
||||
pub fn get_db_stats(&self) -> Result<()> {
|
||||
// Open the catalog database
|
||||
let db_cat = Database::open(&self.db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||
let tx_cat = db_cat.begin_read()
|
||||
let db_cat = Database::open(&self.db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||
})?;
|
||||
let tx_cat = db_cat
|
||||
.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Open the obsoleted database
|
||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
||||
let tx_obs = db_obs.begin_read()
|
||||
let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||
})?;
|
||||
let tx_obs = db_obs
|
||||
.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Get table statistics
|
||||
|
|
@ -284,23 +337,37 @@ impl ImageCatalog {
|
|||
|
||||
// Count catalog entries
|
||||
if let Ok(table) = tx_cat.open_table(CATALOG_TABLE) {
|
||||
for result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? {
|
||||
let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?;
|
||||
for result in table.iter().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to iterate catalog table: {}", e))
|
||||
})? {
|
||||
let _ = result.map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to get entry from catalog table: {}", e))
|
||||
})?;
|
||||
catalog_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Count obsoleted entries (separate DB)
|
||||
if let Ok(table) = tx_obs.open_table(OBSOLETED_TABLE) {
|
||||
for result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? {
|
||||
let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?;
|
||||
for result in table.iter().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e))
|
||||
})? {
|
||||
let _ = result.map_err(|e| {
|
||||
CatalogError::Database(format!(
|
||||
"Failed to get entry from obsoleted table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
obsoleted_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Print statistics
|
||||
println!("Catalog database path: {}", self.db_path.display());
|
||||
println!("Obsoleted database path: {}", self.obsoleted_db_path.display());
|
||||
println!(
|
||||
"Obsoleted database path: {}",
|
||||
self.obsoleted_db_path.display()
|
||||
);
|
||||
println!("Catalog directory: {}", self.catalog_dir.display());
|
||||
println!("Table statistics:");
|
||||
println!(" Catalog table: {} entries", catalog_count);
|
||||
|
|
@ -313,30 +380,43 @@ impl ImageCatalog {
|
|||
/// Initialize the catalog database
|
||||
pub fn init_db(&self) -> Result<()> {
|
||||
// Ensure parent directories exist
|
||||
if let Some(parent) = self.db_path.parent() { fs::create_dir_all(parent)?; }
|
||||
if let Some(parent) = self.obsoleted_db_path.parent() { fs::create_dir_all(parent)?; }
|
||||
if let Some(parent) = self.db_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
if let Some(parent) = self.obsoleted_db_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
// Create/open catalog database and tables
|
||||
let db_cat = Database::create(&self.db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to create catalog database: {}", e)))?;
|
||||
let tx_cat = db_cat.begin_write()
|
||||
let db_cat = Database::create(&self.db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to create catalog database: {}", e))
|
||||
})?;
|
||||
let tx_cat = db_cat
|
||||
.begin_write()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
tx_cat.open_table(CATALOG_TABLE)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to create catalog table: {}", e)))?;
|
||||
tx_cat.open_table(INCORPORATE_TABLE)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to create incorporate table: {}", e)))?;
|
||||
tx_cat.commit()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to commit catalog transaction: {}", e)))?;
|
||||
tx_cat.open_table(CATALOG_TABLE).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to create catalog table: {}", e))
|
||||
})?;
|
||||
tx_cat.open_table(INCORPORATE_TABLE).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to create incorporate table: {}", e))
|
||||
})?;
|
||||
tx_cat.commit().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to commit catalog transaction: {}", e))
|
||||
})?;
|
||||
|
||||
// Create/open obsoleted database and table
|
||||
let db_obs = Database::create(&self.obsoleted_db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to create obsoleted database: {}", e)))?;
|
||||
let tx_obs = db_obs.begin_write()
|
||||
let db_obs = Database::create(&self.obsoleted_db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to create obsoleted database: {}", e))
|
||||
})?;
|
||||
let tx_obs = db_obs
|
||||
.begin_write()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
tx_obs.open_table(OBSOLETED_TABLE)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to create obsoleted table: {}", e)))?;
|
||||
tx_obs.commit()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e)))?;
|
||||
tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to create obsoleted table: {}", e))
|
||||
})?;
|
||||
tx_obs.commit().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -352,28 +432,37 @@ impl ImageCatalog {
|
|||
}
|
||||
|
||||
// Open the databases
|
||||
trace!("Opening databases at {:?} and {:?}", self.db_path, self.obsoleted_db_path);
|
||||
let db_cat = Database::open(&self.db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
||||
trace!(
|
||||
"Opening databases at {:?} and {:?}",
|
||||
self.db_path, self.obsoleted_db_path
|
||||
);
|
||||
let db_cat = Database::open(&self.db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||
})?;
|
||||
let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||
})?;
|
||||
|
||||
// Begin writing transactions
|
||||
trace!("Beginning write transactions");
|
||||
let tx_cat = db_cat.begin_write()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin catalog transaction: {}", e)))?;
|
||||
let tx_obs = db_obs.begin_write()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin obsoleted transaction: {}", e)))?;
|
||||
let tx_cat = db_cat.begin_write().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to begin catalog transaction: {}", e))
|
||||
})?;
|
||||
let tx_obs = db_obs.begin_write().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to begin obsoleted transaction: {}", e))
|
||||
})?;
|
||||
|
||||
// Open the catalog table
|
||||
trace!("Opening catalog table");
|
||||
let mut catalog_table = tx_cat.open_table(CATALOG_TABLE)
|
||||
let mut catalog_table = tx_cat
|
||||
.open_table(CATALOG_TABLE)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
||||
|
||||
// Open the obsoleted table
|
||||
trace!("Opening obsoleted table");
|
||||
let mut obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
||||
let mut obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open obsoleted table: {}", e))
|
||||
})?;
|
||||
|
||||
// Process each publisher
|
||||
for publisher in publishers {
|
||||
|
|
@ -383,28 +472,46 @@ impl ImageCatalog {
|
|||
|
||||
// Skip if the publisher catalog directory doesn't exist
|
||||
if !publisher_catalog_dir.exists() {
|
||||
warn!("Publisher catalog directory not found: {}", publisher_catalog_dir.display());
|
||||
warn!(
|
||||
"Publisher catalog directory not found: {}",
|
||||
publisher_catalog_dir.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine where catalog parts live. Support both legacy nested layout
|
||||
// (publisher/<publisher>/catalog) and flat layout (directly under publisher dir).
|
||||
let nested_dir = publisher_catalog_dir.join("publisher").join(publisher).join("catalog");
|
||||
let nested_dir = publisher_catalog_dir
|
||||
.join("publisher")
|
||||
.join(publisher)
|
||||
.join("catalog");
|
||||
let flat_dir = publisher_catalog_dir.clone();
|
||||
|
||||
let catalog_parts_dir = if nested_dir.exists() { &nested_dir } else { &flat_dir };
|
||||
let catalog_parts_dir = if nested_dir.exists() {
|
||||
&nested_dir
|
||||
} else {
|
||||
&flat_dir
|
||||
};
|
||||
|
||||
trace!("Creating catalog manager for publisher: {}", publisher);
|
||||
trace!("Catalog parts directory: {:?}", catalog_parts_dir);
|
||||
|
||||
// Check if the catalog parts directory exists (either layout)
|
||||
if !catalog_parts_dir.exists() {
|
||||
warn!("Catalog parts directory not found: {}", catalog_parts_dir.display());
|
||||
warn!(
|
||||
"Catalog parts directory not found: {}",
|
||||
catalog_parts_dir.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut catalog_manager = CatalogManager::new(catalog_parts_dir, publisher)
|
||||
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to create catalog manager: {}", e))))?;
|
||||
let mut catalog_manager =
|
||||
CatalogManager::new(catalog_parts_dir, publisher).map_err(|e| {
|
||||
CatalogError::Repository(crate::repository::RepositoryError::Other(format!(
|
||||
"Failed to create catalog manager: {}",
|
||||
e
|
||||
)))
|
||||
})?;
|
||||
|
||||
// Get all catalog parts
|
||||
trace!("Getting catalog parts for publisher: {}", publisher);
|
||||
|
|
@ -414,8 +521,12 @@ impl ImageCatalog {
|
|||
// Load all catalog parts
|
||||
for part_name in parts.keys() {
|
||||
trace!("Loading catalog part: {}", part_name);
|
||||
catalog_manager.load_part(part_name)
|
||||
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to load catalog part: {}", e))))?;
|
||||
catalog_manager.load_part(part_name).map_err(|e| {
|
||||
CatalogError::Repository(crate::repository::RepositoryError::Other(format!(
|
||||
"Failed to load catalog part: {}",
|
||||
e
|
||||
)))
|
||||
})?;
|
||||
}
|
||||
|
||||
// New approach: Merge information across all catalog parts per stem@version, then process once
|
||||
|
|
@ -425,7 +536,12 @@ impl ImageCatalog {
|
|||
loaded_parts.push(part);
|
||||
}
|
||||
}
|
||||
self.process_publisher_merged(&mut catalog_table, &mut obsoleted_table, publisher, &loaded_parts)?;
|
||||
self.process_publisher_merged(
|
||||
&mut catalog_table,
|
||||
&mut obsoleted_table,
|
||||
publisher,
|
||||
&loaded_parts,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Drop the tables to release the borrow on tx
|
||||
|
|
@ -433,10 +549,12 @@ impl ImageCatalog {
|
|||
drop(obsoleted_table);
|
||||
|
||||
// Commit the transactions
|
||||
tx_cat.commit()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to commit catalog transaction: {}", e)))?;
|
||||
tx_obs.commit()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e)))?;
|
||||
tx_cat.commit().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to commit catalog transaction: {}", e))
|
||||
})?;
|
||||
tx_obs.commit().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e))
|
||||
})?;
|
||||
|
||||
info!("Catalog built successfully");
|
||||
Ok(())
|
||||
|
|
@ -472,11 +590,18 @@ impl ImageCatalog {
|
|||
|
||||
// Process each package stem
|
||||
for (stem, versions) in publisher_packages {
|
||||
trace!("Processing package stem: {} ({} versions)", stem, versions.len());
|
||||
trace!(
|
||||
"Processing package stem: {} ({} versions)",
|
||||
stem,
|
||||
versions.len()
|
||||
);
|
||||
|
||||
// Process each package version
|
||||
for version_entry in versions {
|
||||
trace!("Processing version: {} | actions: {:?}", version_entry.version, version_entry.actions);
|
||||
trace!(
|
||||
"Processing version: {} | actions: {:?}",
|
||||
version_entry.version, version_entry.actions
|
||||
);
|
||||
|
||||
// Create the FMRI
|
||||
let version = if !version_entry.version.is_empty() {
|
||||
|
|
@ -499,17 +624,18 @@ impl ImageCatalog {
|
|||
// obsolete in an earlier part (present in obsoleted_table) and is NOT present
|
||||
// in the catalog_table, skip importing it from this part.
|
||||
if !part_name.contains(".base") {
|
||||
let has_catalog = matches!(catalog_table.get(catalog_key.as_str()), Ok(Some(_)));
|
||||
let has_catalog =
|
||||
matches!(catalog_table.get(catalog_key.as_str()), Ok(Some(_)));
|
||||
if !has_catalog {
|
||||
let was_obsoleted = matches!(obsoleted_table.get(obsoleted_key.as_str()), Ok(Some(_)));
|
||||
let was_obsoleted =
|
||||
matches!(obsoleted_table.get(obsoleted_key.as_str()), Ok(Some(_)));
|
||||
if was_obsoleted {
|
||||
// Count as obsolete for progress accounting, even though we skip processing
|
||||
obsolete_count_incl_skipped += 1;
|
||||
skipped_obsolete += 1;
|
||||
trace!(
|
||||
"Skipping {} from part {} because it is marked obsolete and not present in catalog",
|
||||
obsoleted_key,
|
||||
part_name
|
||||
obsoleted_key, part_name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -523,11 +649,18 @@ impl ImageCatalog {
|
|||
};
|
||||
|
||||
// Create or update the manifest
|
||||
let manifest = self.create_or_update_manifest(existing_manifest, version_entry, stem, publisher)?;
|
||||
let manifest = self.create_or_update_manifest(
|
||||
existing_manifest,
|
||||
version_entry,
|
||||
stem,
|
||||
publisher,
|
||||
)?;
|
||||
|
||||
// Check if the package is obsolete
|
||||
let is_obsolete = self.is_package_obsolete(&manifest);
|
||||
if is_obsolete { obsolete_count_incl_skipped += 1; }
|
||||
if is_obsolete {
|
||||
obsolete_count_incl_skipped += 1;
|
||||
}
|
||||
|
||||
// Serialize the manifest
|
||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
||||
|
|
@ -538,13 +671,23 @@ impl ImageCatalog {
|
|||
let empty_bytes: &[u8] = &[0u8; 0];
|
||||
obsoleted_table
|
||||
.insert(obsoleted_key.as_str(), empty_bytes)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to insert into obsoleted table: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
CatalogError::Database(format!(
|
||||
"Failed to insert into obsoleted table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
} else {
|
||||
// Store non-obsolete packages in the catalog table with stem@version as a key
|
||||
let compressed = compress_json_lz4(&manifest_bytes)?;
|
||||
catalog_table
|
||||
.insert(catalog_key.as_str(), compressed.as_slice())
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to insert into catalog table: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
CatalogError::Database(format!(
|
||||
"Failed to insert into catalog table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
processed += 1;
|
||||
|
|
@ -565,11 +708,7 @@ impl ImageCatalog {
|
|||
// Final summary for this part/publisher
|
||||
info!(
|
||||
"Finished import for publisher {}, part {}: {} versions processed ({} obsolete incl. skipped, {} skipped)",
|
||||
publisher,
|
||||
part_name,
|
||||
processed,
|
||||
obsolete_count_incl_skipped,
|
||||
skipped_obsolete
|
||||
publisher, part_name, processed, obsolete_count_incl_skipped, skipped_obsolete
|
||||
);
|
||||
} else {
|
||||
trace!("No packages found for publisher: {}", publisher);
|
||||
|
|
@ -596,16 +735,19 @@ impl ImageCatalog {
|
|||
for (stem, versions) in publisher_packages {
|
||||
let stem_map = merged.entry(stem.clone()).or_default();
|
||||
for v in versions {
|
||||
let entry = stem_map
|
||||
.entry(v.version.clone())
|
||||
.or_insert(PackageVersionEntry {
|
||||
version: v.version.clone(),
|
||||
actions: None,
|
||||
signature_sha1: None,
|
||||
});
|
||||
let entry =
|
||||
stem_map
|
||||
.entry(v.version.clone())
|
||||
.or_insert(PackageVersionEntry {
|
||||
version: v.version.clone(),
|
||||
actions: None,
|
||||
signature_sha1: None,
|
||||
});
|
||||
// Merge signature if not yet set
|
||||
if entry.signature_sha1.is_none() {
|
||||
if let Some(sig) = &v.signature_sha1 { entry.signature_sha1 = Some(sig.clone()); }
|
||||
if let Some(sig) = &v.signature_sha1 {
|
||||
entry.signature_sha1 = Some(sig.clone());
|
||||
}
|
||||
}
|
||||
// Merge actions, de-duplicating
|
||||
if let Some(actions) = &v.actions {
|
||||
|
|
@ -649,40 +791,55 @@ impl ImageCatalog {
|
|||
};
|
||||
|
||||
// Build/update manifest with merged actions
|
||||
let manifest = self.create_or_update_manifest(existing_manifest, entry, stem, publisher)?;
|
||||
let manifest =
|
||||
self.create_or_update_manifest(existing_manifest, entry, stem, publisher)?;
|
||||
|
||||
// Obsolete decision based on merged actions in manifest
|
||||
let is_obsolete = self.is_package_obsolete(&manifest);
|
||||
if is_obsolete { obsolete_count += 1; }
|
||||
if is_obsolete {
|
||||
obsolete_count += 1;
|
||||
}
|
||||
|
||||
// Serialize and write
|
||||
if is_obsolete {
|
||||
// Compute full FMRI for obsoleted key
|
||||
let version_obj = if !entry.version.is_empty() {
|
||||
match crate::fmri::Version::parse(&entry.version) { Ok(v) => Some(v), Err(_) => None }
|
||||
} else { None };
|
||||
match crate::fmri::Version::parse(&entry.version) {
|
||||
Ok(v) => Some(v),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let fmri = Fmri::with_publisher(publisher, stem, version_obj);
|
||||
let obsoleted_key = fmri.to_string();
|
||||
let empty_bytes: &[u8] = &[0u8; 0];
|
||||
obsoleted_table
|
||||
.insert(obsoleted_key.as_str(), empty_bytes)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to insert into obsoleted table: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
CatalogError::Database(format!(
|
||||
"Failed to insert into obsoleted table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
} else {
|
||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
||||
let compressed = compress_json_lz4(&manifest_bytes)?;
|
||||
catalog_table
|
||||
.insert(catalog_key.as_str(), compressed.as_slice())
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to insert into catalog table: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
CatalogError::Database(format!(
|
||||
"Failed to insert into catalog table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
processed += 1;
|
||||
if processed % progress_step == 0 {
|
||||
info!(
|
||||
"Import progress (publisher {}, merged): {}/{} versions processed ({} obsolete)",
|
||||
publisher,
|
||||
processed,
|
||||
total_versions,
|
||||
obsolete_count
|
||||
publisher, processed, total_versions, obsolete_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -691,9 +848,7 @@ impl ImageCatalog {
|
|||
|
||||
info!(
|
||||
"Finished merged import for publisher {}: {} versions processed ({} obsolete)",
|
||||
publisher,
|
||||
processed,
|
||||
obsolete_count
|
||||
publisher, processed, obsolete_count
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
|
@ -728,11 +883,12 @@ impl ImageCatalog {
|
|||
|
||||
// Remove quotes if present
|
||||
if value.starts_with('"') && value.ends_with('"') {
|
||||
value = &value[1..value.len()-1];
|
||||
value = &value[1..value.len() - 1];
|
||||
}
|
||||
|
||||
// Add or update the attribute in the manifest
|
||||
let attr_index = manifest.attributes.iter().position(|attr| attr.key == key);
|
||||
let attr_index =
|
||||
manifest.attributes.iter().position(|attr| attr.key == key);
|
||||
if let Some(index) = attr_index {
|
||||
manifest.attributes[index].values = vec![value.to_string()];
|
||||
} else {
|
||||
|
|
@ -758,10 +914,14 @@ impl ImageCatalog {
|
|||
match k {
|
||||
"type" => dep_type = v.to_string(),
|
||||
"predicate" => {
|
||||
if let Ok(f) = crate::fmri::Fmri::parse(v) { dep_predicate = Some(f); }
|
||||
if let Ok(f) = crate::fmri::Fmri::parse(v) {
|
||||
dep_predicate = Some(f);
|
||||
}
|
||||
}
|
||||
"fmri" => {
|
||||
if let Ok(f) = crate::fmri::Fmri::parse(v) { dep_fmris.push(f); }
|
||||
if let Ok(f) = crate::fmri::Fmri::parse(v) {
|
||||
dep_fmris.push(f);
|
||||
}
|
||||
}
|
||||
"root-image" => {
|
||||
root_image = v.to_string();
|
||||
|
|
@ -792,9 +952,10 @@ impl ImageCatalog {
|
|||
Err(e) => {
|
||||
// Map the FmriError to a CatalogError
|
||||
return Err(CatalogError::Repository(
|
||||
crate::repository::RepositoryError::Other(
|
||||
format!("Invalid version format: {}", e)
|
||||
)
|
||||
crate::repository::RepositoryError::Other(format!(
|
||||
"Invalid version format: {}",
|
||||
e
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -812,7 +973,10 @@ impl ImageCatalog {
|
|||
/// Ensure the manifest has the correct FMRI attribute
|
||||
fn ensure_fmri_attribute(&self, manifest: &mut Manifest, fmri: &Fmri) {
|
||||
// Check if the manifest already has an FMRI attribute
|
||||
let has_fmri = manifest.attributes.iter().any(|attr| attr.key == "pkg.fmri");
|
||||
let has_fmri = manifest
|
||||
.attributes
|
||||
.iter()
|
||||
.any(|attr| attr.key == "pkg.fmri");
|
||||
|
||||
// If not, add it
|
||||
if !has_fmri {
|
||||
|
|
@ -833,30 +997,40 @@ impl ImageCatalog {
|
|||
/// Query the catalog for packages matching a pattern
|
||||
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
|
||||
// Open the catalog database
|
||||
let db_cat = Database::open(&self.db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||
let db_cat = Database::open(&self.db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||
})?;
|
||||
// Begin a read transaction
|
||||
let tx_cat = db_cat.begin_read()
|
||||
let tx_cat = db_cat
|
||||
.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Open the catalog table
|
||||
let catalog_table = tx_cat.open_table(CATALOG_TABLE)
|
||||
let catalog_table = tx_cat
|
||||
.open_table(CATALOG_TABLE)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
||||
|
||||
// Open the obsoleted database
|
||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
||||
let tx_obs = db_obs.begin_read()
|
||||
let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||
})?;
|
||||
let tx_obs = db_obs
|
||||
.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
||||
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open obsoleted table: {}", e))
|
||||
})?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Process the catalog table (non-obsolete packages)
|
||||
// Iterate through all entries in the table
|
||||
for entry_result in catalog_table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? {
|
||||
let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?;
|
||||
for entry_result in catalog_table.iter().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to iterate catalog table: {}", e))
|
||||
})? {
|
||||
let (key, value) = entry_result.map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to get entry from catalog table: {}", e))
|
||||
})?;
|
||||
let key_str = key.value();
|
||||
|
||||
// Skip if the key doesn't match the pattern
|
||||
|
|
@ -880,7 +1054,9 @@ impl ImageCatalog {
|
|||
let manifest: Manifest = decode_manifest_bytes(value.value())?;
|
||||
|
||||
// Extract the publisher from the FMRI attribute
|
||||
let publisher = manifest.attributes.iter()
|
||||
let publisher = manifest
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|attr| attr.key == "pkg.fmri")
|
||||
.map(|attr| {
|
||||
if let Some(fmri_str) = attr.values.get(0) {
|
||||
|
|
@ -918,8 +1094,12 @@ impl ImageCatalog {
|
|||
|
||||
// Process the obsoleted table (obsolete packages)
|
||||
// Iterate through all entries in the table
|
||||
for entry_result in obsoleted_table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? {
|
||||
let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?;
|
||||
for entry_result in obsoleted_table.iter().map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e))
|
||||
})? {
|
||||
let (key, _) = entry_result.map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e))
|
||||
})?;
|
||||
let key_str = key.value();
|
||||
|
||||
// Skip if the key doesn't match the pattern
|
||||
|
|
@ -933,7 +1113,10 @@ impl ImageCatalog {
|
|||
match Fmri::parse(key_str) {
|
||||
Ok(fmri) => {
|
||||
// Extract the publisher
|
||||
let publisher = fmri.publisher.clone().unwrap_or_else(|| "unknown".to_string());
|
||||
let publisher = fmri
|
||||
.publisher
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// Add to results (obsolete)
|
||||
results.push(PackageInfo {
|
||||
|
|
@ -941,9 +1124,12 @@ impl ImageCatalog {
|
|||
obsolete: true,
|
||||
publisher,
|
||||
});
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to parse FMRI from obsoleted table key: {}: {}", key_str, e);
|
||||
warn!(
|
||||
"Failed to parse FMRI from obsoleted table key: {}: {}",
|
||||
key_str, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -955,14 +1141,17 @@ impl ImageCatalog {
|
|||
/// Get a manifest from the catalog
|
||||
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
|
||||
// Open the catalog database
|
||||
let db_cat = Database::open(&self.db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||
let db_cat = Database::open(&self.db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||
})?;
|
||||
// Begin a read transaction
|
||||
let tx_cat = db_cat.begin_read()
|
||||
let tx_cat = db_cat
|
||||
.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Open the catalog table
|
||||
let catalog_table = tx_cat.open_table(CATALOG_TABLE)
|
||||
let catalog_table = tx_cat
|
||||
.open_table(CATALOG_TABLE)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
||||
|
||||
// Create the key for the catalog table (stem@version)
|
||||
|
|
@ -974,12 +1163,15 @@ impl ImageCatalog {
|
|||
}
|
||||
|
||||
// If not found in catalog DB, check obsoleted DB
|
||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
||||
let tx_obs = db_obs.begin_read()
|
||||
let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||
})?;
|
||||
let tx_obs = db_obs
|
||||
.begin_read()
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE)
|
||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
||||
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| {
|
||||
CatalogError::Database(format!("Failed to open obsoleted table: {}", e))
|
||||
})?;
|
||||
let obsoleted_key = fmri.to_string();
|
||||
if let Ok(Some(_)) = obsoleted_table.get(obsoleted_key.as_str()) {
|
||||
let mut manifest = Manifest::new();
|
||||
|
|
|
|||
|
|
@ -83,22 +83,32 @@ impl InstalledPackages {
|
|||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||
|
||||
// Begin a read transaction
|
||||
let tx = db.begin_read()
|
||||
let tx = db
|
||||
.begin_read()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Open the installed table
|
||||
match tx.open_table(INSTALLED_TABLE) {
|
||||
Ok(table) => {
|
||||
let mut count = 0;
|
||||
for entry_result in table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? {
|
||||
let (key, value) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", e)))?;
|
||||
for entry_result in table.iter().map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to iterate installed table: {}", e))
|
||||
})? {
|
||||
let (key, value) = entry_result.map_err(|e| {
|
||||
InstalledError::Database(format!(
|
||||
"Failed to get entry from installed table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let key_str = key.value();
|
||||
|
||||
// Try to deserialize the manifest
|
||||
match serde_json::from_slice::<Manifest>(value.value()) {
|
||||
Ok(manifest) => {
|
||||
// Extract the publisher from the FMRI attribute
|
||||
let publisher = manifest.attributes.iter()
|
||||
let publisher = manifest
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|attr| attr.key == "pkg.fmri")
|
||||
.and_then(|attr| attr.values.get(0).cloned())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
|
@ -109,7 +119,7 @@ impl InstalledPackages {
|
|||
println!(" Files: {}", manifest.files.len());
|
||||
println!(" Directories: {}", manifest.directories.len());
|
||||
println!(" Dependencies: {}", manifest.dependencies.len());
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Key: {}", key_str);
|
||||
println!(" Error deserializing manifest: {}", e);
|
||||
|
|
@ -119,10 +129,13 @@ impl InstalledPackages {
|
|||
}
|
||||
println!("Total entries in installed table: {}", count);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error opening installed table: {}", e);
|
||||
Err(InstalledError::Database(format!("Failed to open installed table: {}", e)))
|
||||
Err(InstalledError::Database(format!(
|
||||
"Failed to open installed table: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -134,7 +147,8 @@ impl InstalledPackages {
|
|||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||
|
||||
// Begin a read transaction
|
||||
let tx = db.begin_read()
|
||||
let tx = db
|
||||
.begin_read()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Get table statistics
|
||||
|
|
@ -142,8 +156,15 @@ impl InstalledPackages {
|
|||
|
||||
// Count installed entries
|
||||
if let Ok(table) = tx.open_table(INSTALLED_TABLE) {
|
||||
for result in table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? {
|
||||
let _ = result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", e)))?;
|
||||
for result in table.iter().map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to iterate installed table: {}", e))
|
||||
})? {
|
||||
let _ = result.map_err(|e| {
|
||||
InstalledError::Database(format!(
|
||||
"Failed to get entry from installed table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
installed_count += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -169,14 +190,17 @@ impl InstalledPackages {
|
|||
.map_err(|e| InstalledError::Database(format!("Failed to create database: {}", e)))?;
|
||||
|
||||
// Create tables
|
||||
let tx = db.begin_write()
|
||||
let tx = db
|
||||
.begin_write()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
tx.open_table(INSTALLED_TABLE)
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to create installed table: {}", e)))?;
|
||||
tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to create installed table: {}", e))
|
||||
})?;
|
||||
|
||||
tx.commit()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
|
||||
tx.commit().map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to commit transaction: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -188,7 +212,8 @@ impl InstalledPackages {
|
|||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||
|
||||
// Begin a writing transaction
|
||||
let tx = db.begin_write()
|
||||
let tx = db
|
||||
.begin_write()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Create the key (full FMRI including publisher)
|
||||
|
|
@ -200,19 +225,27 @@ impl InstalledPackages {
|
|||
// Use a block scope to ensure the table is dropped before committing the transaction
|
||||
{
|
||||
// Open the installed table
|
||||
let mut installed_table = tx.open_table(INSTALLED_TABLE)
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
||||
let mut installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||
})?;
|
||||
|
||||
// Insert the package into the installed table
|
||||
installed_table.insert(key.as_str(), manifest_bytes.as_slice())
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to insert into installed table: {}", e)))?;
|
||||
installed_table
|
||||
.insert(key.as_str(), manifest_bytes.as_slice())
|
||||
.map_err(|e| {
|
||||
InstalledError::Database(format!(
|
||||
"Failed to insert into installed table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
// The table is dropped at the end of this block, releasing its borrow of tx
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
tx.commit()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
|
||||
tx.commit().map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to commit transaction: {}", e))
|
||||
})?;
|
||||
|
||||
info!("Added package to installed database: {}", key);
|
||||
Ok(())
|
||||
|
|
@ -225,7 +258,8 @@ impl InstalledPackages {
|
|||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||
|
||||
// Begin a writing transaction
|
||||
let tx = db.begin_write()
|
||||
let tx = db
|
||||
.begin_write()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Create the key (full FMRI including publisher)
|
||||
|
|
@ -234,8 +268,9 @@ impl InstalledPackages {
|
|||
// Use a block scope to ensure the table is dropped before committing the transaction
|
||||
{
|
||||
// Open the installed table
|
||||
let mut installed_table = tx.open_table(INSTALLED_TABLE)
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
||||
let mut installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||
})?;
|
||||
|
||||
// Check if the package exists
|
||||
if let Ok(None) = installed_table.get(key.as_str()) {
|
||||
|
|
@ -243,15 +278,17 @@ impl InstalledPackages {
|
|||
}
|
||||
|
||||
// Remove the package from the installed table
|
||||
installed_table.remove(key.as_str())
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to remove from installed table: {}", e)))?;
|
||||
installed_table.remove(key.as_str()).map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to remove from installed table: {}", e))
|
||||
})?;
|
||||
|
||||
// The table is dropped at the end of this block, releasing its borrow of tx
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
tx.commit()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
|
||||
tx.commit().map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to commit transaction: {}", e))
|
||||
})?;
|
||||
|
||||
info!("Removed package from installed database: {}", key);
|
||||
Ok(())
|
||||
|
|
@ -264,21 +301,30 @@ impl InstalledPackages {
|
|||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||
|
||||
// Begin a read transaction
|
||||
let tx = db.begin_read()
|
||||
let tx = db
|
||||
.begin_read()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Use a block scope to ensure the table is dropped when no longer needed
|
||||
let results = {
|
||||
// Open the installed table
|
||||
let installed_table = tx.open_table(INSTALLED_TABLE)
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
||||
let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||
})?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Process the installed table
|
||||
// Iterate through all entries in the table
|
||||
for entry_result in installed_table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? {
|
||||
let (key, _) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", e)))?;
|
||||
for entry_result in installed_table.iter().map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to iterate installed table: {}", e))
|
||||
})? {
|
||||
let (key, _) = entry_result.map_err(|e| {
|
||||
InstalledError::Database(format!(
|
||||
"Failed to get entry from installed table: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let key_str = key.value();
|
||||
|
||||
// Skip if the key doesn't match the pattern
|
||||
|
|
@ -292,13 +338,13 @@ impl InstalledPackages {
|
|||
let fmri = Fmri::from_str(key_str)?;
|
||||
|
||||
// Get the publisher (handling the Option<String>)
|
||||
let publisher = fmri.publisher.clone().unwrap_or_else(|| "unknown".to_string());
|
||||
let publisher = fmri
|
||||
.publisher
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// Add to results
|
||||
results.push(InstalledPackageInfo {
|
||||
fmri,
|
||||
publisher,
|
||||
});
|
||||
results.push(InstalledPackageInfo { fmri, publisher });
|
||||
}
|
||||
|
||||
results
|
||||
|
|
@ -315,7 +361,8 @@ impl InstalledPackages {
|
|||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||
|
||||
// Begin a read transaction
|
||||
let tx = db.begin_read()
|
||||
let tx = db
|
||||
.begin_read()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Create the key (full FMRI including publisher)
|
||||
|
|
@ -324,8 +371,9 @@ impl InstalledPackages {
|
|||
// Use a block scope to ensure the table is dropped when no longer needed
|
||||
let manifest_option = {
|
||||
// Open the installed table
|
||||
let installed_table = tx.open_table(INSTALLED_TABLE)
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
||||
let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||
})?;
|
||||
|
||||
// Try to get the manifest from the installed table
|
||||
if let Ok(Some(bytes)) = installed_table.get(key.as_str()) {
|
||||
|
|
@ -346,7 +394,8 @@ impl InstalledPackages {
|
|||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||
|
||||
// Begin a read transaction
|
||||
let tx = db.begin_read()
|
||||
let tx = db
|
||||
.begin_read()
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||
|
||||
// Create the key (full FMRI including publisher)
|
||||
|
|
@ -355,8 +404,9 @@ impl InstalledPackages {
|
|||
// Use a block scope to ensure the table is dropped when no longer needed
|
||||
let is_installed = {
|
||||
// Open the installed table
|
||||
let installed_table = tx.open_table(INSTALLED_TABLE)
|
||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
||||
let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||
})?;
|
||||
|
||||
// Check if the package exists
|
||||
if let Ok(Some(_)) = installed_table.get(key.as_str()) {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,10 @@ fn test_installed_packages() {
|
|||
|
||||
// Verify that the package is in the results
|
||||
assert_eq!(packages.len(), 1);
|
||||
assert_eq!(packages[0].fmri.to_string(), "pkg://test/example/package@1.0");
|
||||
assert_eq!(
|
||||
packages[0].fmri.to_string(),
|
||||
"pkg://test/example/package@1.0"
|
||||
);
|
||||
assert_eq!(packages[0].publisher, "test");
|
||||
|
||||
// Get the manifest from the installed packages database
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ mod tests;
|
|||
|
||||
use miette::Diagnostic;
|
||||
use properties::*;
|
||||
use redb::{Database, ReadableDatabase, ReadableTable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use redb::{Database, ReadableDatabase, ReadableTable};
|
||||
|
||||
use crate::repository::{ReadableRepository, RepositoryError, RestBackend, FileBackend};
|
||||
use crate::repository::{FileBackend, ReadableRepository, RepositoryError, RestBackend};
|
||||
|
||||
// Export the catalog module
|
||||
pub mod catalog;
|
||||
use catalog::{ImageCatalog, PackageInfo, INCORPORATE_TABLE};
|
||||
use catalog::{INCORPORATE_TABLE, ImageCatalog, PackageInfo};
|
||||
|
||||
// Export the installed packages module
|
||||
pub mod installed;
|
||||
|
|
@ -150,7 +150,13 @@ impl Image {
|
|||
}
|
||||
|
||||
/// Add a publisher to the image
|
||||
pub fn add_publisher(&mut self, name: &str, origin: &str, mirrors: Vec<String>, is_default: bool) -> Result<()> {
|
||||
pub fn add_publisher(
|
||||
&mut self,
|
||||
name: &str,
|
||||
origin: &str,
|
||||
mirrors: Vec<String>,
|
||||
is_default: bool,
|
||||
) -> Result<()> {
|
||||
// Check if publisher already exists
|
||||
if self.publishers.iter().any(|p| p.name == name) {
|
||||
// Update existing publisher
|
||||
|
|
@ -304,7 +310,10 @@ impl Image {
|
|||
fs::create_dir_all(&metadata_dir).map_err(|e| {
|
||||
ImageError::IO(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to create metadata directory at {:?}: {}", metadata_dir, e),
|
||||
format!(
|
||||
"Failed to create metadata directory at {:?}: {}",
|
||||
metadata_dir, e
|
||||
),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
@ -315,7 +324,10 @@ impl Image {
|
|||
fs::create_dir_all(&manifest_dir).map_err(|e| {
|
||||
ImageError::IO(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to create manifest directory at {:?}: {}", manifest_dir, e),
|
||||
format!(
|
||||
"Failed to create manifest directory at {:?}: {}",
|
||||
manifest_dir, e
|
||||
),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
@ -326,7 +338,10 @@ impl Image {
|
|||
fs::create_dir_all(&catalog_dir).map_err(|e| {
|
||||
ImageError::IO(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to create catalog directory at {:?}: {}", catalog_dir, e),
|
||||
format!(
|
||||
"Failed to create catalog directory at {:?}: {}",
|
||||
catalog_dir, e
|
||||
),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
@ -338,12 +353,19 @@ impl Image {
|
|||
// Create the installed packages database
|
||||
let installed = InstalledPackages::new(&db_path);
|
||||
installed.init_db().map_err(|e| {
|
||||
ImageError::Database(format!("Failed to initialize installed packages database: {}", e))
|
||||
ImageError::Database(format!(
|
||||
"Failed to initialize installed packages database: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a package to the installed packages database
|
||||
pub fn install_package(&self, fmri: &crate::fmri::Fmri, manifest: &crate::actions::Manifest) -> Result<()> {
|
||||
pub fn install_package(
|
||||
&self,
|
||||
fmri: &crate::fmri::Fmri,
|
||||
manifest: &crate::actions::Manifest,
|
||||
) -> Result<()> {
|
||||
// Precheck incorporation dependencies: fail if any stem already has a lock
|
||||
for d in &manifest.dependencies {
|
||||
if d.dependency_type == "incorporate" {
|
||||
|
|
@ -351,7 +373,8 @@ impl Image {
|
|||
let stem = df.stem();
|
||||
if let Some(_) = self.get_incorporated_release(stem)? {
|
||||
return Err(ImageError::Database(format!(
|
||||
"Incorporation lock already exists for stem {}", stem
|
||||
"Incorporation lock already exists for stem {}",
|
||||
stem
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
@ -361,7 +384,10 @@ impl Image {
|
|||
// Add to installed database
|
||||
let installed = InstalledPackages::new(self.installed_db_path());
|
||||
installed.add_package(fmri, manifest).map_err(|e| {
|
||||
ImageError::Database(format!("Failed to add package to installed database: {}", e))
|
||||
ImageError::Database(format!(
|
||||
"Failed to add package to installed database: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
// Write incorporation locks for any incorporate dependencies
|
||||
|
|
@ -385,23 +411,35 @@ impl Image {
|
|||
pub fn uninstall_package(&self, fmri: &crate::fmri::Fmri) -> Result<()> {
|
||||
let installed = InstalledPackages::new(self.installed_db_path());
|
||||
installed.remove_package(fmri).map_err(|e| {
|
||||
ImageError::Database(format!("Failed to remove package from installed database: {}", e))
|
||||
ImageError::Database(format!(
|
||||
"Failed to remove package from installed database: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Query the installed packages database for packages matching a pattern
|
||||
pub fn query_installed_packages(&self, pattern: Option<&str>) -> Result<Vec<InstalledPackageInfo>> {
|
||||
pub fn query_installed_packages(
|
||||
&self,
|
||||
pattern: Option<&str>,
|
||||
) -> Result<Vec<InstalledPackageInfo>> {
|
||||
let installed = InstalledPackages::new(self.installed_db_path());
|
||||
installed.query_packages(pattern).map_err(|e| {
|
||||
ImageError::Database(format!("Failed to query installed packages: {}", e))
|
||||
})
|
||||
installed
|
||||
.query_packages(pattern)
|
||||
.map_err(|e| ImageError::Database(format!("Failed to query installed packages: {}", e)))
|
||||
}
|
||||
|
||||
/// Get a manifest from the installed packages database
|
||||
pub fn get_manifest_from_installed(&self, fmri: &crate::fmri::Fmri) -> Result<Option<crate::actions::Manifest>> {
|
||||
pub fn get_manifest_from_installed(
|
||||
&self,
|
||||
fmri: &crate::fmri::Fmri,
|
||||
) -> Result<Option<crate::actions::Manifest>> {
|
||||
let installed = InstalledPackages::new(self.installed_db_path());
|
||||
installed.get_manifest(fmri).map_err(|e| {
|
||||
ImageError::Database(format!("Failed to get manifest from installed database: {}", e))
|
||||
ImageError::Database(format!(
|
||||
"Failed to get manifest from installed database: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -419,7 +457,11 @@ impl Image {
|
|||
/// and stored under a flattened path:
|
||||
/// manifests/<publisher>/<encoded_stem>@<encoded_version>.p5m
|
||||
/// Missing publisher will fall back to the image default publisher, then "unknown".
|
||||
pub fn save_manifest(&self, fmri: &crate::fmri::Fmri, _manifest: &crate::actions::Manifest) -> Result<std::path::PathBuf> {
|
||||
pub fn save_manifest(
|
||||
&self,
|
||||
fmri: &crate::fmri::Fmri,
|
||||
_manifest: &crate::actions::Manifest,
|
||||
) -> Result<std::path::PathBuf> {
|
||||
// Determine publisher name
|
||||
let pub_name = if let Some(p) = &fmri.publisher {
|
||||
p.clone()
|
||||
|
|
@ -438,7 +480,9 @@ impl Image {
|
|||
let mut out = String::new();
|
||||
for b in s.bytes() {
|
||||
match b {
|
||||
b'-' | b'_' | b'.' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => out.push(b as char),
|
||||
b'-' | b'_' | b'.' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => {
|
||||
out.push(b as char)
|
||||
}
|
||||
b' ' => out.push('+'),
|
||||
_ => {
|
||||
out.push('%');
|
||||
|
|
@ -484,7 +528,11 @@ impl Image {
|
|||
|
||||
/// Initialize the catalog database
|
||||
pub fn init_catalog_db(&self) -> Result<()> {
|
||||
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path());
|
||||
let catalog = ImageCatalog::new(
|
||||
self.catalog_dir(),
|
||||
self.catalog_db_path(),
|
||||
self.obsoleted_db_path(),
|
||||
);
|
||||
catalog.init_db().map_err(|e| {
|
||||
ImageError::Database(format!("Failed to initialize catalog database: {}", e))
|
||||
})
|
||||
|
|
@ -526,7 +574,8 @@ impl Image {
|
|||
self.publishers.iter().collect()
|
||||
} else {
|
||||
// Otherwise, filter publishers by name
|
||||
self.publishers.iter()
|
||||
self.publishers
|
||||
.iter()
|
||||
.filter(|p| publishers.contains(&p.name))
|
||||
.collect()
|
||||
};
|
||||
|
|
@ -541,19 +590,25 @@ impl Image {
|
|||
for publisher in &publishers_to_refresh {
|
||||
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
|
||||
if publisher_catalog_dir.exists() {
|
||||
fs::remove_dir_all(&publisher_catalog_dir)
|
||||
.map_err(|e| ImageError::IO(std::io::Error::new(
|
||||
fs::remove_dir_all(&publisher_catalog_dir).map_err(|e| {
|
||||
ImageError::IO(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to remove catalog directory for publisher {}: {}",
|
||||
publisher.name, e)
|
||||
)))?;
|
||||
format!(
|
||||
"Failed to remove catalog directory for publisher {}: {}",
|
||||
publisher.name, e
|
||||
),
|
||||
))
|
||||
})?;
|
||||
}
|
||||
fs::create_dir_all(&publisher_catalog_dir)
|
||||
.map_err(|e| ImageError::IO(std::io::Error::new(
|
||||
fs::create_dir_all(&publisher_catalog_dir).map_err(|e| {
|
||||
ImageError::IO(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to create catalog directory for publisher {}: {}",
|
||||
publisher.name, e)
|
||||
)))?;
|
||||
format!(
|
||||
"Failed to create catalog directory for publisher {}: {}",
|
||||
publisher.name, e
|
||||
),
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -574,23 +629,29 @@ impl Image {
|
|||
self.init_catalog_db()?;
|
||||
|
||||
// Get publisher names
|
||||
let publisher_names: Vec<String> = self.publishers.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect();
|
||||
let publisher_names: Vec<String> = self.publishers.iter().map(|p| p.name.clone()).collect();
|
||||
|
||||
// Create the catalog and build it
|
||||
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path());
|
||||
catalog.build_catalog(&publisher_names).map_err(|e| {
|
||||
ImageError::Database(format!("Failed to build catalog: {}", e))
|
||||
})
|
||||
let catalog = ImageCatalog::new(
|
||||
self.catalog_dir(),
|
||||
self.catalog_db_path(),
|
||||
self.obsoleted_db_path(),
|
||||
);
|
||||
catalog
|
||||
.build_catalog(&publisher_names)
|
||||
.map_err(|e| ImageError::Database(format!("Failed to build catalog: {}", e)))
|
||||
}
|
||||
|
||||
/// Query the catalog for packages matching a pattern
|
||||
pub fn query_catalog(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
|
||||
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path());
|
||||
catalog.query_packages(pattern).map_err(|e| {
|
||||
ImageError::Database(format!("Failed to query catalog: {}", e))
|
||||
})
|
||||
let catalog = ImageCatalog::new(
|
||||
self.catalog_dir(),
|
||||
self.catalog_db_path(),
|
||||
self.obsoleted_db_path(),
|
||||
);
|
||||
catalog
|
||||
.query_packages(pattern)
|
||||
.map_err(|e| ImageError::Database(format!("Failed to query catalog: {}", e)))
|
||||
}
|
||||
|
||||
/// Look up an incorporation lock for a given stem.
|
||||
|
|
@ -598,16 +659,18 @@ impl Image {
|
|||
pub fn get_incorporated_release(&self, stem: &str) -> Result<Option<String>> {
|
||||
let db = Database::open(self.catalog_db_path())
|
||||
.map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||
let tx = db.begin_read()
|
||||
.map_err(|e| ImageError::Database(format!("Failed to begin read transaction: {}", e)))?;
|
||||
let tx = db.begin_read().map_err(|e| {
|
||||
ImageError::Database(format!("Failed to begin read transaction: {}", e))
|
||||
})?;
|
||||
match tx.open_table(INCORPORATE_TABLE) {
|
||||
Ok(table) => {
|
||||
match table.get(stem) {
|
||||
Ok(Some(val)) => Ok(Some(String::from_utf8_lossy(val.value()).to_string())),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(ImageError::Database(format!("Failed to read incorporate lock: {}", e))),
|
||||
}
|
||||
}
|
||||
Ok(table) => match table.get(stem) {
|
||||
Ok(Some(val)) => Ok(Some(String::from_utf8_lossy(val.value()).to_string())),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(ImageError::Database(format!(
|
||||
"Failed to read incorporate lock: {}",
|
||||
e
|
||||
))),
|
||||
},
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
|
@ -617,26 +680,39 @@ impl Image {
|
|||
pub fn add_incorporation_lock(&self, stem: &str, release: &str) -> Result<()> {
|
||||
let db = Database::open(self.catalog_db_path())
|
||||
.map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||
let tx = db.begin_write()
|
||||
.map_err(|e| ImageError::Database(format!("Failed to begin write transaction: {}", e)))?;
|
||||
let tx = db.begin_write().map_err(|e| {
|
||||
ImageError::Database(format!("Failed to begin write transaction: {}", e))
|
||||
})?;
|
||||
{
|
||||
let mut table = tx.open_table(INCORPORATE_TABLE)
|
||||
.map_err(|e| ImageError::Database(format!("Failed to open incorporate table: {}", e)))?;
|
||||
let mut table = tx.open_table(INCORPORATE_TABLE).map_err(|e| {
|
||||
ImageError::Database(format!("Failed to open incorporate table: {}", e))
|
||||
})?;
|
||||
if let Ok(Some(_)) = table.get(stem) {
|
||||
return Err(ImageError::Database(format!("Incorporation lock already exists for stem {}", stem)));
|
||||
return Err(ImageError::Database(format!(
|
||||
"Incorporation lock already exists for stem {}",
|
||||
stem
|
||||
)));
|
||||
}
|
||||
table.insert(stem, release.as_bytes())
|
||||
.map_err(|e| ImageError::Database(format!("Failed to insert incorporate lock: {}", e)))?;
|
||||
table.insert(stem, release.as_bytes()).map_err(|e| {
|
||||
ImageError::Database(format!("Failed to insert incorporate lock: {}", e))
|
||||
})?;
|
||||
}
|
||||
tx.commit()
|
||||
.map_err(|e| ImageError::Database(format!("Failed to commit incorporate lock: {}", e)))?
|
||||
;
|
||||
tx.commit().map_err(|e| {
|
||||
ImageError::Database(format!("Failed to commit incorporate lock: {}", e))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a manifest from the catalog
|
||||
pub fn get_manifest_from_catalog(&self, fmri: &crate::fmri::Fmri) -> Result<Option<crate::actions::Manifest>> {
|
||||
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path());
|
||||
pub fn get_manifest_from_catalog(
|
||||
&self,
|
||||
fmri: &crate::fmri::Fmri,
|
||||
) -> Result<Option<crate::actions::Manifest>> {
|
||||
let catalog = ImageCatalog::new(
|
||||
self.catalog_dir(),
|
||||
self.catalog_db_path(),
|
||||
self.obsoleted_db_path(),
|
||||
);
|
||||
catalog.get_manifest(fmri).map_err(|e| {
|
||||
ImageError::Database(format!("Failed to get manifest from catalog: {}", e))
|
||||
})
|
||||
|
|
@ -647,7 +723,10 @@ impl Image {
|
|||
/// This bypasses the local catalog database and retrieves the full manifest from
|
||||
/// the configured publisher origin (REST for http/https origins; File backend for
|
||||
/// file:// origins). A versioned FMRI is required.
|
||||
pub fn get_manifest_from_repository(&self, fmri: &crate::fmri::Fmri) -> Result<crate::actions::Manifest> {
|
||||
pub fn get_manifest_from_repository(
|
||||
&self,
|
||||
fmri: &crate::fmri::Fmri,
|
||||
) -> Result<crate::actions::Manifest> {
|
||||
// Determine publisher: use FMRI's publisher if present, otherwise default publisher
|
||||
let publisher_name = if let Some(p) = &fmri.publisher {
|
||||
p.clone()
|
||||
|
|
@ -671,13 +750,15 @@ impl Image {
|
|||
let path_str = origin.trim_start_matches("file://");
|
||||
let path = PathBuf::from(path_str);
|
||||
let mut repo = FileBackend::open(&path)?;
|
||||
repo.fetch_manifest(&publisher_name, fmri).map_err(Into::into)
|
||||
repo.fetch_manifest(&publisher_name, fmri)
|
||||
.map_err(Into::into)
|
||||
} else {
|
||||
let mut repo = RestBackend::open(origin)?;
|
||||
// Optionally set a per-publisher cache directory (used by other REST ops)
|
||||
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
|
||||
repo.set_local_cache_path(&publisher_catalog_dir)?;
|
||||
repo.fetch_manifest(&publisher_name, fmri).map_err(Into::into)
|
||||
repo.fetch_manifest(&publisher_name, fmri)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ fn test_catalog_methods() {
|
|||
println!("Catalog dir: {:?}", image.catalog_dir());
|
||||
|
||||
// Add a publisher
|
||||
image.add_publisher("test", "http://example.com/repo", vec![], true).unwrap();
|
||||
image
|
||||
.add_publisher("test", "http://example.com/repo", vec![], true)
|
||||
.unwrap();
|
||||
|
||||
// Print the publishers
|
||||
println!("Publishers: {:?}", image.publishers());
|
||||
|
|
@ -59,7 +61,10 @@ fn test_catalog_methods() {
|
|||
"updates": {},
|
||||
"version": 1
|
||||
}"#;
|
||||
println!("Writing catalog.attrs to {:?}", publisher_dir.join("catalog.attrs"));
|
||||
println!(
|
||||
"Writing catalog.attrs to {:?}",
|
||||
publisher_dir.join("catalog.attrs")
|
||||
);
|
||||
println!("catalog.attrs content: {}", attrs_content);
|
||||
fs::write(publisher_dir.join("catalog.attrs"), attrs_content).unwrap();
|
||||
|
||||
|
|
@ -88,13 +93,22 @@ fn test_catalog_methods() {
|
|||
]
|
||||
}
|
||||
}"#;
|
||||
println!("Writing base catalog part to {:?}", publisher_dir.join("base"));
|
||||
println!(
|
||||
"Writing base catalog part to {:?}",
|
||||
publisher_dir.join("base")
|
||||
);
|
||||
println!("base catalog part content: {}", base_content);
|
||||
fs::write(publisher_dir.join("base"), base_content).unwrap();
|
||||
|
||||
// Verify that the files were written correctly
|
||||
println!("Checking if catalog.attrs exists: {}", publisher_dir.join("catalog.attrs").exists());
|
||||
println!("Checking if base catalog part exists: {}", publisher_dir.join("base").exists());
|
||||
println!(
|
||||
"Checking if catalog.attrs exists: {}",
|
||||
publisher_dir.join("catalog.attrs").exists()
|
||||
);
|
||||
println!(
|
||||
"Checking if base catalog part exists: {}",
|
||||
publisher_dir.join("base").exists()
|
||||
);
|
||||
|
||||
// Build the catalog
|
||||
println!("Building catalog...");
|
||||
|
|
@ -109,7 +123,7 @@ fn test_catalog_methods() {
|
|||
Ok(pkgs) => {
|
||||
println!("Found {} packages", pkgs.len());
|
||||
pkgs
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to query catalog: {:?}", e);
|
||||
panic!("Failed to query catalog: {:?}", e);
|
||||
|
|
@ -126,7 +140,10 @@ fn test_catalog_methods() {
|
|||
|
||||
// Verify that the obsolete package has the full FMRI as key
|
||||
// This is indirectly verified by checking that the publisher is included in the FMRI
|
||||
assert_eq!(obsolete_packages[0].fmri.publisher, Some("test".to_string()));
|
||||
assert_eq!(
|
||||
obsolete_packages[0].fmri.publisher,
|
||||
Some("test".to_string())
|
||||
);
|
||||
|
||||
// Verify that one package is not marked as obsolete
|
||||
let non_obsolete_packages: Vec<_> = packages.iter().filter(|p| !p.obsolete).collect();
|
||||
|
|
@ -164,8 +181,12 @@ fn test_refresh_catalogs_directory_clearing() {
|
|||
let mut image = Image::create_image(&image_path, ImageType::Full).unwrap();
|
||||
|
||||
// Add two publishers
|
||||
image.add_publisher("test1", "http://example.com/repo1", vec![], true).unwrap();
|
||||
image.add_publisher("test2", "http://example.com/repo2", vec![], false).unwrap();
|
||||
image
|
||||
.add_publisher("test1", "http://example.com/repo1", vec![], true)
|
||||
.unwrap();
|
||||
image
|
||||
.add_publisher("test2", "http://example.com/repo2", vec![], false)
|
||||
.unwrap();
|
||||
|
||||
// Create the catalog directory structure for both publishers
|
||||
let catalog_dir = image.catalog_dir();
|
||||
|
|
@ -177,8 +198,16 @@ fn test_refresh_catalogs_directory_clearing() {
|
|||
// Create marker files in both publisher directories
|
||||
let marker_file1 = publisher1_dir.join("marker");
|
||||
let marker_file2 = publisher2_dir.join("marker");
|
||||
fs::write(&marker_file1, "This file should be removed during full refresh").unwrap();
|
||||
fs::write(&marker_file2, "This file should be removed during full refresh").unwrap();
|
||||
fs::write(
|
||||
&marker_file1,
|
||||
"This file should be removed during full refresh",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
&marker_file2,
|
||||
"This file should be removed during full refresh",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(marker_file1.exists());
|
||||
assert!(marker_file2.exists());
|
||||
|
||||
|
|
@ -195,7 +224,11 @@ fn test_refresh_catalogs_directory_clearing() {
|
|||
assert!(marker_file2.exists());
|
||||
|
||||
// Create a new marker file for publisher1
|
||||
fs::write(&marker_file1, "This file should be removed during full refresh").unwrap();
|
||||
fs::write(
|
||||
&marker_file1,
|
||||
"This file should be removed during full refresh",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(marker_file1.exists());
|
||||
|
||||
// Directly test the directory clearing functionality for all publishers
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@
|
|||
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub mod actions;
|
||||
pub mod api;
|
||||
pub mod depend;
|
||||
pub mod digest;
|
||||
pub mod fmri;
|
||||
pub mod image;
|
||||
pub mod payload;
|
||||
pub mod repository;
|
||||
pub mod publisher;
|
||||
pub mod transformer;
|
||||
pub mod repository;
|
||||
pub mod solver;
|
||||
pub mod depend;
|
||||
pub mod api;
|
||||
mod test_json_manifest;
|
||||
pub mod transformer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod publisher_tests;
|
||||
|
|
@ -69,91 +69,101 @@ set name=pkg.summary value=\"'XZ Utils - loss-less file compression application
|
|||
);
|
||||
|
||||
let test_results = vec![
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("pkg.fmri"),
|
||||
values: vec![String::from("pkg://openindiana.org/web/server/nginx@1.18.0,5.11-2020.0.1.0:20200421T195136Z")],
|
||||
values: vec![String::from(
|
||||
"pkg://openindiana.org/web/server/nginx@1.18.0,5.11-2020.0.1.0:20200421T195136Z",
|
||||
)],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("com.oracle.info.name"),
|
||||
values: vec![String::from("nginx"), String::from("test")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("userland.info.git-remote"),
|
||||
values: vec![String::from("git://github.com/OpenIndiana/oi-userland.git")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("userland.info.git-branch"),
|
||||
values: vec![String::from("HEAD")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("userland.info.git-rev"),
|
||||
values: vec![String::from("1665491ba61bd494bf73e2916cd2250f3024260e")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("pkg.summary"),
|
||||
values: vec![String::from("Nginx Webserver")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("info.classification"),
|
||||
values: vec![String::from("org.opensolaris.category.2008:Web Services/Application and Web Servers")],
|
||||
values: vec![String::from(
|
||||
"org.opensolaris.category.2008:Web Services/Application and Web Servers",
|
||||
)],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("info.upstream-url"),
|
||||
values: vec![String::from("http://nginx.net/")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("info.source-url"),
|
||||
values: vec![String::from("http://nginx.org/download/nginx-1.18.0.tar.gz")],
|
||||
values: vec![String::from(
|
||||
"http://nginx.org/download/nginx-1.18.0.tar.gz",
|
||||
)],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("org.opensolaris.consolidation"),
|
||||
values: vec![String::from("userland")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("com.oracle.info.version"),
|
||||
values: vec![String::from("1.18.0")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("pkg.summary"),
|
||||
values: vec![String::from("provided mouse accessibility enhancements")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("info.upstream"),
|
||||
values: vec![String::from("X.Org Foundation")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("pkg.description"),
|
||||
values: vec![String::from("Latvian language support's extra files")],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("variant.arch"),
|
||||
values: vec![String::from("i386")],
|
||||
properties: optional_hash,
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("info.source-url"),
|
||||
values: vec![String::from("http://www.pgpool.net/download.php?f=pgpool-II-3.3.1.tar.gz")],
|
||||
values: vec![String::from(
|
||||
"http://www.pgpool.net/download.php?f=pgpool-II-3.3.1.tar.gz",
|
||||
)],
|
||||
properties: HashMap::new(),
|
||||
},
|
||||
Attr{
|
||||
Attr {
|
||||
key: String::from("pkg.summary"),
|
||||
values: vec![String::from("'XZ Utils - loss-less file compression application and library.'")], //TODO knock out the single quotes
|
||||
values: vec![String::from(
|
||||
"'XZ Utils - loss-less file compression application and library.'",
|
||||
)], //TODO knock out the single quotes
|
||||
properties: HashMap::new(),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
let res = Manifest::parse_string(manifest_string);
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
// MPL was not distributed with this file, You can
|
||||
// obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::actions::{File as FileAction, Manifest, Transform as TransformAction};
|
||||
use crate::repository::{ReadableRepository, RepositoryError, WritableRepository};
|
||||
use crate::repository::file_backend::{FileBackend, Transaction};
|
||||
use crate::repository::{ReadableRepository, RepositoryError, WritableRepository};
|
||||
use crate::transformer;
|
||||
|
||||
/// Error type for high-level publishing operations
|
||||
|
|
@ -30,7 +30,10 @@ pub enum PublisherError {
|
|||
Io(String),
|
||||
|
||||
#[error("invalid root path: {0}")]
|
||||
#[diagnostic(code(ips::publisher_error::invalid_root_path), help("Ensure the directory exists and is readable"))]
|
||||
#[diagnostic(
|
||||
code(ips::publisher_error::invalid_root_path),
|
||||
help("Ensure the directory exists and is readable")
|
||||
)]
|
||||
InvalidRoot(String),
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +54,12 @@ impl PublisherClient {
|
|||
/// Open an existing repository located at `path` with a selected `publisher`.
|
||||
pub fn open<P: AsRef<Path>>(path: P, publisher: impl Into<String>) -> Result<Self> {
|
||||
let backend = FileBackend::open(path)?;
|
||||
Ok(Self { backend, publisher: publisher.into(), tx: None, transform_rules: Vec::new() })
|
||||
Ok(Self {
|
||||
backend,
|
||||
publisher: publisher.into(),
|
||||
tx: None,
|
||||
transform_rules: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Open a transaction if not already open and return whether a new transaction was created.
|
||||
|
|
@ -70,9 +78,13 @@ impl PublisherClient {
|
|||
return Err(PublisherError::InvalidRoot(root.display().to_string()));
|
||||
}
|
||||
let mut manifest = Manifest::new();
|
||||
let root = root.canonicalize().map_err(|_| PublisherError::InvalidRoot(root.display().to_string()))?;
|
||||
let root = root
|
||||
.canonicalize()
|
||||
.map_err(|_| PublisherError::InvalidRoot(root.display().to_string()))?;
|
||||
|
||||
let walker = walkdir::WalkDir::new(&root).into_iter().filter_map(|e| e.ok());
|
||||
let walker = walkdir::WalkDir::new(&root)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok());
|
||||
// Ensure a transaction is open
|
||||
if self.tx.is_none() {
|
||||
self.open_transaction()?;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ mod tests {
|
|||
let repo_path = tmp.path().to_path_buf();
|
||||
|
||||
// Initialize repository
|
||||
let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo");
|
||||
let mut backend =
|
||||
FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo");
|
||||
backend.add_publisher("test").expect("add publisher");
|
||||
|
||||
// Prepare a prototype directory with a nested file
|
||||
|
|
@ -36,16 +37,27 @@ mod tests {
|
|||
// Use PublisherClient to publish
|
||||
let mut client = PublisherClient::open(&repo_path, "test").expect("open client");
|
||||
client.open_transaction().expect("open tx");
|
||||
let manifest = client.build_manifest_from_dir(&proto_dir).expect("build manifest");
|
||||
let manifest = client
|
||||
.build_manifest_from_dir(&proto_dir)
|
||||
.expect("build manifest");
|
||||
client.publish(manifest, true).expect("publish");
|
||||
|
||||
// Verify the manifest exists at the default path for unknown version
|
||||
let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest");
|
||||
assert!(manifest_path.exists(), "manifest not found at {}", manifest_path.display());
|
||||
let manifest_path =
|
||||
FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest");
|
||||
assert!(
|
||||
manifest_path.exists(),
|
||||
"manifest not found at {}",
|
||||
manifest_path.display()
|
||||
);
|
||||
|
||||
// Verify at least one file was stored under publisher/test/file
|
||||
let file_root = repo_path.join("publisher").join("test").join("file");
|
||||
assert!(file_root.exists(), "file store root does not exist: {}", file_root.display());
|
||||
assert!(
|
||||
file_root.exists(),
|
||||
"file store root does not exist: {}",
|
||||
file_root.display()
|
||||
);
|
||||
let mut any_file = false;
|
||||
if let Ok(entries) = fs::read_dir(&file_root) {
|
||||
for entry in entries.flatten() {
|
||||
|
|
@ -62,14 +74,15 @@ mod tests {
|
|||
} else if path.is_file() {
|
||||
any_file = true;
|
||||
}
|
||||
if any_file { break; }
|
||||
if any_file {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(any_file, "no stored file found in file store");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod transform_rule_integration_tests {
|
||||
use crate::actions::Manifest;
|
||||
|
|
@ -85,7 +98,8 @@ mod transform_rule_integration_tests {
|
|||
// Setup repository and publisher
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let repo_path = tmp.path().to_path_buf();
|
||||
let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo");
|
||||
let mut backend =
|
||||
FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo");
|
||||
backend.add_publisher("test").expect("add publisher");
|
||||
|
||||
// Prototype directory with a file
|
||||
|
|
@ -102,18 +116,33 @@ mod transform_rule_integration_tests {
|
|||
|
||||
// Use PublisherClient to load rules, build manifest and publish
|
||||
let mut client = PublisherClient::open(&repo_path, "test").expect("open client");
|
||||
let loaded = client.load_transform_rules_from_file(&rules_path).expect("load rules");
|
||||
let loaded = client
|
||||
.load_transform_rules_from_file(&rules_path)
|
||||
.expect("load rules");
|
||||
assert!(loaded >= 1, "expected at least one rule loaded");
|
||||
client.open_transaction().expect("open tx");
|
||||
let manifest = client.build_manifest_from_dir(&proto_dir).expect("build manifest");
|
||||
let manifest = client
|
||||
.build_manifest_from_dir(&proto_dir)
|
||||
.expect("build manifest");
|
||||
client.publish(manifest, false).expect("publish");
|
||||
|
||||
// Read stored manifest and verify attribute
|
||||
let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest");
|
||||
assert!(manifest_path.exists(), "manifest missing: {}", manifest_path.display());
|
||||
let manifest_path =
|
||||
FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest");
|
||||
assert!(
|
||||
manifest_path.exists(),
|
||||
"manifest missing: {}",
|
||||
manifest_path.display()
|
||||
);
|
||||
let json = fs::read_to_string(&manifest_path).expect("read manifest");
|
||||
let parsed: Manifest = serde_json::from_str(&json).expect("parse manifest json");
|
||||
let has_summary = parsed.attributes.iter().any(|a| a.key == "pkg.summary" && a.values.iter().any(|v| v == "Added via rules"));
|
||||
assert!(has_summary, "pkg.summary attribute added via rules not found");
|
||||
let has_summary = parsed
|
||||
.attributes
|
||||
.iter()
|
||||
.any(|a| a.key == "pkg.summary" && a.values.iter().any(|v| v == "Added via rules"));
|
||||
assert!(
|
||||
has_summary,
|
||||
"pkg.summary attribute added via rules not found"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,21 +21,31 @@ fn sha1_hex(bytes: &[u8]) -> String {
|
|||
|
||||
fn atomic_write_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
|
||||
let parent = path.parent().unwrap_or(Path::new("."));
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| RepositoryError::DirectoryCreateError { path: parent.to_path_buf(), source: e })?;
|
||||
fs::create_dir_all(parent).map_err(|e| RepositoryError::DirectoryCreateError {
|
||||
path: parent.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
let tmp: PathBuf = path.with_extension("tmp");
|
||||
{
|
||||
let mut f = std::fs::File::create(&tmp)
|
||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
||||
let mut f = std::fs::File::create(&tmp).map_err(|e| RepositoryError::FileWriteError {
|
||||
path: tmp.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
f.write_all(bytes)
|
||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
||||
f.flush()
|
||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
||||
.map_err(|e| RepositoryError::FileWriteError {
|
||||
path: tmp.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
f.flush().map_err(|e| RepositoryError::FileWriteError {
|
||||
path: tmp.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
}
|
||||
fs::rename(&tmp, path)
|
||||
.map_err(|e| RepositoryError::FileWriteError { path: path.to_path_buf(), source: e })?
|
||||
;
|
||||
fs::rename(&tmp, path).map_err(|e| RepositoryError::FileWriteError {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -43,53 +53,71 @@ fn atomic_write_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
|
|||
pub(crate) fn write_catalog_attrs(path: &Path, attrs: &mut CatalogAttrs) -> Result<String> {
|
||||
// Compute signature over content without _SIGNATURE
|
||||
attrs.signature = None;
|
||||
let bytes_without_sig = serde_json::to_vec(&attrs)
|
||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)))?;
|
||||
let bytes_without_sig = serde_json::to_vec(&attrs).map_err(|e| {
|
||||
RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e))
|
||||
})?;
|
||||
let sig = sha1_hex(&bytes_without_sig);
|
||||
let mut sig_map = std::collections::HashMap::new();
|
||||
sig_map.insert("sha-1".to_string(), sig);
|
||||
attrs.signature = Some(sig_map);
|
||||
|
||||
let final_bytes = serde_json::to_vec(&attrs)
|
||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)))?;
|
||||
let final_bytes = serde_json::to_vec(&attrs).map_err(|e| {
|
||||
RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e))
|
||||
})?;
|
||||
debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog.attrs");
|
||||
atomic_write_bytes(path, &final_bytes)?;
|
||||
// safe to unwrap as signature was just inserted
|
||||
Ok(attrs.signature.as_ref().and_then(|m| m.get("sha-1").cloned()).unwrap_or_default())
|
||||
Ok(attrs
|
||||
.signature
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("sha-1").cloned())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(part))]
|
||||
pub(crate) fn write_catalog_part(path: &Path, part: &mut CatalogPart) -> Result<String> {
|
||||
// Compute signature over content without _SIGNATURE
|
||||
part.signature = None;
|
||||
let bytes_without_sig = serde_json::to_vec(&part)
|
||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)))?;
|
||||
let bytes_without_sig = serde_json::to_vec(&part).map_err(|e| {
|
||||
RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e))
|
||||
})?;
|
||||
let sig = sha1_hex(&bytes_without_sig);
|
||||
let mut sig_map = std::collections::HashMap::new();
|
||||
sig_map.insert("sha-1".to_string(), sig);
|
||||
part.signature = Some(sig_map);
|
||||
|
||||
let final_bytes = serde_json::to_vec(&part)
|
||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)))?;
|
||||
let final_bytes = serde_json::to_vec(&part).map_err(|e| {
|
||||
RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e))
|
||||
})?;
|
||||
debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog part");
|
||||
atomic_write_bytes(path, &final_bytes)?;
|
||||
Ok(part.signature.as_ref().and_then(|m| m.get("sha-1").cloned()).unwrap_or_default())
|
||||
Ok(part
|
||||
.signature
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("sha-1").cloned())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(log))]
|
||||
pub(crate) fn write_update_log(path: &Path, log: &mut UpdateLog) -> Result<String> {
|
||||
// Compute signature over content without _SIGNATURE
|
||||
log.signature = None;
|
||||
let bytes_without_sig = serde_json::to_vec(&log)
|
||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?;
|
||||
let bytes_without_sig = serde_json::to_vec(&log).map_err(|e| {
|
||||
RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e))
|
||||
})?;
|
||||
let sig = sha1_hex(&bytes_without_sig);
|
||||
let mut sig_map = std::collections::HashMap::new();
|
||||
sig_map.insert("sha-1".to_string(), sig);
|
||||
log.signature = Some(sig_map);
|
||||
|
||||
let final_bytes = serde_json::to_vec(&log)
|
||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?;
|
||||
let final_bytes = serde_json::to_vec(&log).map_err(|e| {
|
||||
RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e))
|
||||
})?;
|
||||
debug!(path = %path.display(), bytes = final_bytes.len(), "writing update log");
|
||||
atomic_write_bytes(path, &final_bytes)?;
|
||||
Ok(log.signature.as_ref().and_then(|m| m.get("sha-1").cloned()).unwrap_or_default())
|
||||
Ok(log
|
||||
.signature
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("sha-1").cloned())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
// obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use super::{RepositoryError, Result};
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression as GzipCompression;
|
||||
use flate2::write::GzEncoder;
|
||||
use lz4::EncoderBuilder;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -25,11 +25,11 @@ use crate::digest::Digest;
|
|||
use crate::fmri::Fmri;
|
||||
use crate::payload::{Payload, PayloadCompressionAlgorithm};
|
||||
|
||||
use super::{
|
||||
PackageContents, PackageInfo, PublisherInfo, ReadableRepository, RepositoryConfig,
|
||||
RepositoryInfo, RepositoryVersion, WritableRepository, REPOSITORY_CONFIG_FILENAME,
|
||||
};
|
||||
use super::catalog_writer;
|
||||
use super::{
|
||||
PackageContents, PackageInfo, PublisherInfo, REPOSITORY_CONFIG_FILENAME, ReadableRepository,
|
||||
RepositoryConfig, RepositoryInfo, RepositoryVersion, WritableRepository,
|
||||
};
|
||||
use ini::Ini;
|
||||
|
||||
// Define a struct to hold the content vectors for each package
|
||||
|
|
@ -224,7 +224,8 @@ pub struct FileBackend {
|
|||
/// Uses RefCell for interior mutability to allow mutation through immutable references
|
||||
catalog_manager: Option<std::cell::RefCell<crate::repository::catalog::CatalogManager>>,
|
||||
/// Manager for obsoleted packages
|
||||
obsoleted_manager: Option<std::cell::RefCell<crate::repository::obsoleted::ObsoletedPackageManager>>,
|
||||
obsoleted_manager:
|
||||
Option<std::cell::RefCell<crate::repository::obsoleted::ObsoletedPackageManager>>,
|
||||
}
|
||||
|
||||
/// Format a SystemTime as an ISO 8601 timestamp string
|
||||
|
|
@ -342,20 +343,16 @@ impl Transaction {
|
|||
// Check if the temp file already exists
|
||||
if temp_file_path.exists() {
|
||||
// If it exists, remove it to avoid any issues with existing content
|
||||
fs::remove_file(&temp_file_path).map_err(|e| {
|
||||
RepositoryError::FileWriteError {
|
||||
path: temp_file_path.clone(),
|
||||
source: e,
|
||||
}
|
||||
fs::remove_file(&temp_file_path).map_err(|e| RepositoryError::FileWriteError {
|
||||
path: temp_file_path.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
let file_content = fs::read(file_path).map_err(|e| {
|
||||
RepositoryError::FileReadError {
|
||||
path: file_path.to_path_buf(),
|
||||
source: e,
|
||||
}
|
||||
let file_content = fs::read(file_path).map_err(|e| RepositoryError::FileReadError {
|
||||
path: file_path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
// Create a payload with the hash information if it doesn't exist
|
||||
|
|
@ -493,7 +490,8 @@ impl Transaction {
|
|||
// Copy files to their final location
|
||||
for (source_path, hash) in self.files {
|
||||
// Create the destination path using the helper function with publisher
|
||||
let dest_path = FileBackend::construct_file_path_with_publisher(&self.repo, &publisher, &hash);
|
||||
let dest_path =
|
||||
FileBackend::construct_file_path_with_publisher(&self.repo, &publisher, &hash);
|
||||
|
||||
// Create parent directories if they don't exist
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
|
|
@ -567,7 +565,8 @@ impl Transaction {
|
|||
// Construct the manifest path using the helper method
|
||||
let pkg_manifest_path = if package_version.is_empty() {
|
||||
// If no version was provided, store as a default manifest file
|
||||
FileBackend::construct_package_dir(&self.repo, &publisher, &package_stem).join("manifest")
|
||||
FileBackend::construct_package_dir(&self.repo, &publisher, &package_stem)
|
||||
.join("manifest")
|
||||
} else {
|
||||
FileBackend::construct_manifest_path(
|
||||
&self.repo,
|
||||
|
|
@ -667,13 +666,15 @@ impl ReadableRepository for FileBackend {
|
|||
let config5_path = path.join("pkg5.repository");
|
||||
|
||||
let config: RepositoryConfig = if config6_path.exists() {
|
||||
let config_data = fs::read_to_string(&config6_path)
|
||||
.map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config6_path.display(), e)))?;
|
||||
let config_data = fs::read_to_string(&config6_path).map_err(|e| {
|
||||
RepositoryError::ConfigReadError(format!("{}: {}", config6_path.display(), e))
|
||||
})?;
|
||||
serde_json::from_str(&config_data)?
|
||||
} else if config5_path.exists() {
|
||||
// Minimal mapping for legacy INI: take publishers only from INI; do not scan disk.
|
||||
let ini = Ini::load_from_file(&config5_path)
|
||||
.map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config5_path.display(), e)))?;
|
||||
let ini = Ini::load_from_file(&config5_path).map_err(|e| {
|
||||
RepositoryError::ConfigReadError(format!("{}: {}", config5_path.display(), e))
|
||||
})?;
|
||||
|
||||
// Default repository version for legacy format is v4
|
||||
let mut cfg = RepositoryConfig::default();
|
||||
|
|
@ -829,7 +830,10 @@ impl ReadableRepository for FileBackend {
|
|||
pattern: Option<&str>,
|
||||
action_types: Option<&[String]>,
|
||||
) -> Result<Vec<PackageContents>> {
|
||||
debug!("show_contents called with publisher: {:?}, pattern: {:?}", publisher, pattern);
|
||||
debug!(
|
||||
"show_contents called with publisher: {:?}, pattern: {:?}",
|
||||
publisher, pattern
|
||||
);
|
||||
// Use a HashMap to store package information
|
||||
let mut packages = HashMap::new();
|
||||
|
||||
|
|
@ -889,7 +893,9 @@ impl ReadableRepository for FileBackend {
|
|||
|
||||
// Check if the file starts with a valid manifest marker
|
||||
if bytes_read == 0
|
||||
|| (buffer[0] != b'{' && buffer[0] != b'<' && buffer[0] != b's')
|
||||
|| (buffer[0] != b'{'
|
||||
&& buffer[0] != b'<'
|
||||
&& buffer[0] != b's')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -901,7 +907,9 @@ impl ReadableRepository for FileBackend {
|
|||
let mut pkg_id = String::new();
|
||||
|
||||
for attr in &manifest.attributes {
|
||||
if attr.key == "pkg.fmri" && !attr.values.is_empty() {
|
||||
if attr.key == "pkg.fmri"
|
||||
&& !attr.values.is_empty()
|
||||
{
|
||||
let fmri = &attr.values[0];
|
||||
|
||||
// Parse the FMRI using our Fmri type
|
||||
|
|
@ -913,14 +921,22 @@ impl ReadableRepository for FileBackend {
|
|||
match Regex::new(pat) {
|
||||
Ok(regex) => {
|
||||
// Use regex matching
|
||||
if !regex.is_match(parsed_fmri.stem()) {
|
||||
if !regex.is_match(
|
||||
parsed_fmri.stem(),
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Log the error but fall back to the simple string contains
|
||||
error!("FileBackend::show_contents: Error compiling regex pattern '{}': {}", pat, err);
|
||||
if !parsed_fmri.stem().contains(pat) {
|
||||
error!(
|
||||
"FileBackend::show_contents: Error compiling regex pattern '{}': {}",
|
||||
pat, err
|
||||
);
|
||||
if !parsed_fmri
|
||||
.stem()
|
||||
.contains(pat)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -970,7 +986,9 @@ impl ReadableRepository for FileBackend {
|
|||
.contains(&"file".to_string())
|
||||
{
|
||||
for file in &manifest.files {
|
||||
content_vectors.files.push(file.path.clone());
|
||||
content_vectors
|
||||
.files
|
||||
.push(file.path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -982,7 +1000,9 @@ impl ReadableRepository for FileBackend {
|
|||
.contains(&"dir".to_string())
|
||||
{
|
||||
for dir in &manifest.directories {
|
||||
content_vectors.directories.push(dir.path.clone());
|
||||
content_vectors
|
||||
.directories
|
||||
.push(dir.path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -994,7 +1014,9 @@ impl ReadableRepository for FileBackend {
|
|||
.contains(&"link".to_string())
|
||||
{
|
||||
for link in &manifest.links {
|
||||
content_vectors.links.push(link.path.clone());
|
||||
content_vectors
|
||||
.links
|
||||
.push(link.path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1007,7 +1029,9 @@ impl ReadableRepository for FileBackend {
|
|||
{
|
||||
for depend in &manifest.dependencies {
|
||||
if let Some(fmri) = &depend.fmri {
|
||||
content_vectors.dependencies.push(fmri.to_string());
|
||||
content_vectors
|
||||
.dependencies
|
||||
.push(fmri.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1020,12 +1044,22 @@ impl ReadableRepository for FileBackend {
|
|||
.contains(&"license".to_string())
|
||||
{
|
||||
for license in &manifest.licenses {
|
||||
if let Some(path_prop) = license.properties.get("path") {
|
||||
content_vectors.licenses.push(path_prop.value.clone());
|
||||
} else if let Some(license_prop) = license.properties.get("license") {
|
||||
content_vectors.licenses.push(license_prop.value.clone());
|
||||
if let Some(path_prop) =
|
||||
license.properties.get("path")
|
||||
{
|
||||
content_vectors
|
||||
.licenses
|
||||
.push(path_prop.value.clone());
|
||||
} else if let Some(license_prop) =
|
||||
license.properties.get("license")
|
||||
{
|
||||
content_vectors
|
||||
.licenses
|
||||
.push(license_prop.value.clone());
|
||||
} else {
|
||||
content_vectors.licenses.push(license.payload.clone());
|
||||
content_vectors
|
||||
.licenses
|
||||
.push(license.payload.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1103,7 +1137,10 @@ impl ReadableRepository for FileBackend {
|
|||
}
|
||||
Err(err) => {
|
||||
// Log the error but fall back to the simple string contains
|
||||
error!("FileBackend::show_contents: Error compiling regex pattern '{}': {}", pat, err);
|
||||
error!(
|
||||
"FileBackend::show_contents: Error compiling regex pattern '{}': {}",
|
||||
pat, err
|
||||
);
|
||||
if !parsed_fmri.stem().contains(pat)
|
||||
{
|
||||
continue;
|
||||
|
|
@ -1323,16 +1360,30 @@ impl ReadableRepository for FileBackend {
|
|||
|
||||
// If destination already exists and matches digest, do nothing
|
||||
if dest.exists() {
|
||||
let bytes = fs::read(dest).map_err(|e| RepositoryError::FileReadError { path: dest.to_path_buf(), source: e })?;
|
||||
match crate::digest::Digest::from_bytes(&bytes, algo.clone(), crate::digest::DigestSource::PrimaryPayloadHash) {
|
||||
let bytes = fs::read(dest).map_err(|e| RepositoryError::FileReadError {
|
||||
path: dest.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
match crate::digest::Digest::from_bytes(
|
||||
&bytes,
|
||||
algo.clone(),
|
||||
crate::digest::DigestSource::PrimaryPayloadHash,
|
||||
) {
|
||||
Ok(comp) if comp.hash == hash => return Ok(()),
|
||||
_ => { /* fall through to overwrite */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Read source content and verify digest
|
||||
let bytes = fs::read(&source_path).map_err(|e| RepositoryError::FileReadError { path: source_path.clone(), source: e })?;
|
||||
match crate::digest::Digest::from_bytes(&bytes, algo, crate::digest::DigestSource::PrimaryPayloadHash) {
|
||||
let bytes = fs::read(&source_path).map_err(|e| RepositoryError::FileReadError {
|
||||
path: source_path.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
match crate::digest::Digest::from_bytes(
|
||||
&bytes,
|
||||
algo,
|
||||
crate::digest::DigestSource::PrimaryPayloadHash,
|
||||
) {
|
||||
Ok(comp) => {
|
||||
if comp.hash != hash {
|
||||
return Err(RepositoryError::DigestError(format!(
|
||||
|
|
@ -1363,7 +1414,9 @@ impl ReadableRepository for FileBackend {
|
|||
// Require a concrete version
|
||||
let version = fmri.version();
|
||||
if version.is_empty() {
|
||||
return Err(RepositoryError::Other("FMRI must include a version to fetch manifest".into()));
|
||||
return Err(RepositoryError::Other(
|
||||
"FMRI must include a version to fetch manifest".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Preferred path: publisher-scoped manifest path
|
||||
|
|
@ -1375,7 +1428,11 @@ impl ReadableRepository for FileBackend {
|
|||
// Fallbacks: global pkg layout without publisher
|
||||
let encoded_stem = Self::url_encode(fmri.stem());
|
||||
let encoded_version = Self::url_encode(&version);
|
||||
let alt1 = self.path.join("pkg").join(&encoded_stem).join(&encoded_version);
|
||||
let alt1 = self
|
||||
.path
|
||||
.join("pkg")
|
||||
.join(&encoded_stem)
|
||||
.join(&encoded_version);
|
||||
if alt1.exists() {
|
||||
return crate::actions::Manifest::parse_file(&alt1).map_err(RepositoryError::from);
|
||||
}
|
||||
|
|
@ -1744,7 +1801,10 @@ impl FileBackend {
|
|||
locale: &str,
|
||||
fmri: &crate::fmri::Fmri,
|
||||
op_type: crate::repository::catalog::CatalogOperationType,
|
||||
catalog_parts: std::collections::HashMap<String, std::collections::HashMap<String, Vec<String>>>,
|
||||
catalog_parts: std::collections::HashMap<
|
||||
String,
|
||||
std::collections::HashMap<String, Vec<String>>,
|
||||
>,
|
||||
signature_sha1: Option<String>,
|
||||
) -> Result<()> {
|
||||
let catalog_dir = Self::construct_catalog_path(&self.path, publisher);
|
||||
|
|
@ -1816,19 +1876,29 @@ impl FileBackend {
|
|||
// Require a concrete version
|
||||
let version = fmri.version();
|
||||
if version.is_empty() {
|
||||
return Err(RepositoryError::Other("FMRI must include a version to fetch manifest".into()));
|
||||
return Err(RepositoryError::Other(
|
||||
"FMRI must include a version to fetch manifest".into(),
|
||||
));
|
||||
}
|
||||
// Preferred path: publisher-scoped manifest path
|
||||
let path = Self::construct_manifest_path(&self.path, publisher, fmri.stem(), &version);
|
||||
if path.exists() {
|
||||
return std::fs::read_to_string(&path).map_err(|e| RepositoryError::FileReadError { path, source: e });
|
||||
return std::fs::read_to_string(&path)
|
||||
.map_err(|e| RepositoryError::FileReadError { path, source: e });
|
||||
}
|
||||
// Fallbacks: global pkg layout without publisher
|
||||
let encoded_stem = Self::url_encode(fmri.stem());
|
||||
let encoded_version = Self::url_encode(&version);
|
||||
let alt1 = self.path.join("pkg").join(&encoded_stem).join(&encoded_version);
|
||||
let alt1 = self
|
||||
.path
|
||||
.join("pkg")
|
||||
.join(&encoded_stem)
|
||||
.join(&encoded_version);
|
||||
if alt1.exists() {
|
||||
return std::fs::read_to_string(&alt1).map_err(|e| RepositoryError::FileReadError { path: alt1, source: e });
|
||||
return std::fs::read_to_string(&alt1).map_err(|e| RepositoryError::FileReadError {
|
||||
path: alt1,
|
||||
source: e,
|
||||
});
|
||||
}
|
||||
let alt2 = self
|
||||
.path
|
||||
|
|
@ -1838,9 +1908,15 @@ impl FileBackend {
|
|||
.join(&encoded_stem)
|
||||
.join(&encoded_version);
|
||||
if alt2.exists() {
|
||||
return std::fs::read_to_string(&alt2).map_err(|e| RepositoryError::FileReadError { path: alt2, source: e });
|
||||
return std::fs::read_to_string(&alt2).map_err(|e| RepositoryError::FileReadError {
|
||||
path: alt2,
|
||||
source: e,
|
||||
});
|
||||
}
|
||||
Err(RepositoryError::NotFound(format!("manifest for {} not found", fmri)))
|
||||
Err(RepositoryError::NotFound(format!(
|
||||
"manifest for {} not found",
|
||||
fmri
|
||||
)))
|
||||
}
|
||||
/// Fetch catalog file path
|
||||
pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result<PathBuf> {
|
||||
|
|
@ -1880,8 +1956,7 @@ impl FileBackend {
|
|||
.set("check-certificate-revocation", "False");
|
||||
|
||||
// Add CONFIGURATION section with version
|
||||
conf.with_section(Some("CONFIGURATION"))
|
||||
.set("version", "4");
|
||||
conf.with_section(Some("CONFIGURATION")).set("version", "4");
|
||||
|
||||
// Write the INI file
|
||||
conf.write_to_file(legacy_config_path)?;
|
||||
|
|
@ -1939,10 +2014,7 @@ impl FileBackend {
|
|||
/// Helper method to construct a catalog path consistently
|
||||
///
|
||||
/// Format: base_path/publisher/publisher_name/catalog
|
||||
pub fn construct_catalog_path(
|
||||
base_path: &Path,
|
||||
publisher: &str,
|
||||
) -> PathBuf {
|
||||
pub fn construct_catalog_path(base_path: &Path, publisher: &str) -> PathBuf {
|
||||
base_path.join("publisher").join(publisher).join("catalog")
|
||||
}
|
||||
|
||||
|
|
@ -1963,23 +2035,20 @@ impl FileBackend {
|
|||
/// Helper method to construct a package directory path consistently
|
||||
///
|
||||
/// Format: base_path/publisher/publisher_name/pkg/url_encoded_stem
|
||||
pub fn construct_package_dir(
|
||||
base_path: &Path,
|
||||
publisher: &str,
|
||||
stem: &str,
|
||||
) -> PathBuf {
|
||||
pub fn construct_package_dir(base_path: &Path, publisher: &str, stem: &str) -> PathBuf {
|
||||
let encoded_stem = Self::url_encode(stem);
|
||||
base_path.join("publisher").join(publisher).join("pkg").join(encoded_stem)
|
||||
base_path
|
||||
.join("publisher")
|
||||
.join(publisher)
|
||||
.join("pkg")
|
||||
.join(encoded_stem)
|
||||
}
|
||||
|
||||
/// Helper method to construct a file path consistently
|
||||
///
|
||||
/// Format: base_path/file/XX/hash
|
||||
/// Where XX is the first two characters of the hash
|
||||
pub fn construct_file_path(
|
||||
base_path: &Path,
|
||||
hash: &str,
|
||||
) -> PathBuf {
|
||||
pub fn construct_file_path(base_path: &Path, hash: &str) -> PathBuf {
|
||||
if hash.len() < 2 {
|
||||
// Fallback for very short hashes (shouldn't happen with SHA256)
|
||||
base_path.join("file").join(hash)
|
||||
|
|
@ -1988,10 +2057,7 @@ impl FileBackend {
|
|||
let first_two = &hash[0..2];
|
||||
|
||||
// Create the path: $REPO/file/XX/XXYY...
|
||||
base_path
|
||||
.join("file")
|
||||
.join(first_two)
|
||||
.join(hash)
|
||||
base_path.join("file").join(first_two).join(hash)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2006,7 +2072,11 @@ impl FileBackend {
|
|||
) -> PathBuf {
|
||||
if hash.len() < 2 {
|
||||
// Fallback for very short hashes (shouldn't happen with SHA256)
|
||||
base_path.join("publisher").join(publisher).join("file").join(hash)
|
||||
base_path
|
||||
.join("publisher")
|
||||
.join(publisher)
|
||||
.join("file")
|
||||
.join(hash)
|
||||
} else {
|
||||
// Extract the first two characters from the hash
|
||||
let first_two = &hash[0..2];
|
||||
|
|
@ -2094,7 +2164,10 @@ impl FileBackend {
|
|||
}
|
||||
Err(err) => {
|
||||
// Log the error but fall back to the simple string contains
|
||||
error!("FileBackend::find_manifests_recursive: Error compiling regex pattern '{}': {}", pat, err);
|
||||
error!(
|
||||
"FileBackend::find_manifests_recursive: Error compiling regex pattern '{}': {}",
|
||||
pat, err
|
||||
);
|
||||
if !parsed_fmri.stem().contains(pat) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -2113,8 +2186,12 @@ impl FileBackend {
|
|||
};
|
||||
|
||||
// Check if the package is obsoleted
|
||||
let is_obsoleted = if let Some(obsoleted_manager) = &self.obsoleted_manager {
|
||||
obsoleted_manager.borrow().is_obsoleted(publisher, &final_fmri)
|
||||
let is_obsoleted = if let Some(obsoleted_manager) =
|
||||
&self.obsoleted_manager
|
||||
{
|
||||
obsoleted_manager
|
||||
.borrow()
|
||||
.is_obsoleted(publisher, &final_fmri)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
|
@ -2122,9 +2199,7 @@ impl FileBackend {
|
|||
// Only add the package if it's not obsoleted
|
||||
if !is_obsoleted {
|
||||
// Create a PackageInfo struct and add it to the list
|
||||
packages.push(PackageInfo {
|
||||
fmri: final_fmri,
|
||||
});
|
||||
packages.push(PackageInfo { fmri: final_fmri });
|
||||
}
|
||||
|
||||
// Found the package info, no need to check other attributes
|
||||
|
|
@ -2245,7 +2320,11 @@ impl FileBackend {
|
|||
}
|
||||
|
||||
// Read the manifest content for hash calculation
|
||||
let manifest_content = fs::read_to_string(&manifest_path).map_err(|e| RepositoryError::FileReadError { path: manifest_path.clone(), source: e })?;
|
||||
let manifest_content =
|
||||
fs::read_to_string(&manifest_path).map_err(|e| RepositoryError::FileReadError {
|
||||
path: manifest_path.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
// Parse the manifest using parse_file which handles JSON correctly
|
||||
let manifest = Manifest::parse_file(&manifest_path)?;
|
||||
|
|
@ -2334,7 +2413,12 @@ impl FileBackend {
|
|||
processed_in_batch += 1;
|
||||
if processed_in_batch >= opts.batch_size {
|
||||
batch_no += 1;
|
||||
tracing::debug!(publisher, batch_no, processed_in_batch, "catalog rebuild batch processed");
|
||||
tracing::debug!(
|
||||
publisher,
|
||||
batch_no,
|
||||
processed_in_batch,
|
||||
"catalog rebuild batch processed"
|
||||
);
|
||||
processed_in_batch = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -2407,7 +2491,8 @@ impl FileBackend {
|
|||
for (fmri, actions, signature) in dependency_entries {
|
||||
dependency_part.add_package(publisher, &fmri, actions, Some(signature));
|
||||
}
|
||||
let dependency_sig = catalog_writer::write_catalog_part(&dependency_part_path, &mut dependency_part)?;
|
||||
let dependency_sig =
|
||||
catalog_writer::write_catalog_part(&dependency_part_path, &mut dependency_part)?;
|
||||
debug!("Wrote dependency part file");
|
||||
|
||||
// Summary part
|
||||
|
|
@ -2417,7 +2502,8 @@ impl FileBackend {
|
|||
for (fmri, actions, signature) in summary_entries {
|
||||
summary_part.add_package(publisher, &fmri, actions, Some(signature));
|
||||
}
|
||||
let summary_sig = catalog_writer::write_catalog_part(&summary_part_path, &mut summary_part)?;
|
||||
let summary_sig =
|
||||
catalog_writer::write_catalog_part(&summary_part_path, &mut summary_part)?;
|
||||
debug!("Wrote summary part file");
|
||||
|
||||
// Update part signatures in attrs (written after parts)
|
||||
|
|
@ -2495,25 +2581,42 @@ impl FileBackend {
|
|||
|
||||
// Ensure catalog dir exists
|
||||
let catalog_dir = Self::construct_catalog_path(&self.path, publisher);
|
||||
std::fs::create_dir_all(&catalog_dir).map_err(|e| RepositoryError::DirectoryCreateError { path: catalog_dir.clone(), source: e })?;
|
||||
std::fs::create_dir_all(&catalog_dir).map_err(|e| {
|
||||
RepositoryError::DirectoryCreateError {
|
||||
path: catalog_dir.clone(),
|
||||
source: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
// Serialize JSON
|
||||
let json = serde_json::to_vec_pretty(log)
|
||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?;
|
||||
let json = serde_json::to_vec_pretty(log).map_err(|e| {
|
||||
RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e))
|
||||
})?;
|
||||
|
||||
// Write atomically
|
||||
let target = catalog_dir.join(log_filename);
|
||||
let tmp = target.with_extension("tmp");
|
||||
{
|
||||
let mut f = std::fs::File::create(&tmp)
|
||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
||||
let mut f =
|
||||
std::fs::File::create(&tmp).map_err(|e| RepositoryError::FileWriteError {
|
||||
path: tmp.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
use std::io::Write as _;
|
||||
f.write_all(&json)
|
||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
||||
f.flush().map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
||||
.map_err(|e| RepositoryError::FileWriteError {
|
||||
path: tmp.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
f.flush().map_err(|e| RepositoryError::FileWriteError {
|
||||
path: tmp.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
}
|
||||
std::fs::rename(&tmp, &target)
|
||||
.map_err(|e| RepositoryError::FileWriteError { path: target.clone(), source: e })?;
|
||||
std::fs::rename(&tmp, &target).map_err(|e| RepositoryError::FileWriteError {
|
||||
path: target.clone(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -2536,7 +2639,8 @@ impl FileBackend {
|
|||
) -> Result<std::cell::RefMut<'_, crate::repository::catalog::CatalogManager>> {
|
||||
if self.catalog_manager.is_none() {
|
||||
let publisher_dir = self.path.join("publisher");
|
||||
let manager = crate::repository::catalog::CatalogManager::new(&publisher_dir, publisher)?;
|
||||
let manager =
|
||||
crate::repository::catalog::CatalogManager::new(&publisher_dir, publisher)?;
|
||||
let refcell = std::cell::RefCell::new(manager);
|
||||
self.catalog_manager = Some(refcell);
|
||||
}
|
||||
|
|
@ -2669,18 +2773,17 @@ impl FileBackend {
|
|||
None
|
||||
};
|
||||
|
||||
let directories =
|
||||
if !manifest.directories.is_empty() {
|
||||
Some(
|
||||
manifest
|
||||
.directories
|
||||
.iter()
|
||||
.map(|d| d.path.clone())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let directories = if !manifest.directories.is_empty() {
|
||||
Some(
|
||||
manifest
|
||||
.directories
|
||||
.iter()
|
||||
.map(|d| d.path.clone())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let links = if !manifest.links.is_empty() {
|
||||
Some(
|
||||
|
|
@ -2694,22 +2797,20 @@ impl FileBackend {
|
|||
None
|
||||
};
|
||||
|
||||
let dependencies =
|
||||
if !manifest.dependencies.is_empty() {
|
||||
Some(
|
||||
manifest
|
||||
.dependencies
|
||||
.iter()
|
||||
.filter_map(|d| {
|
||||
d.fmri
|
||||
.as_ref()
|
||||
.map(|f| f.to_string())
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let dependencies = if !manifest.dependencies.is_empty()
|
||||
{
|
||||
Some(
|
||||
manifest
|
||||
.dependencies
|
||||
.iter()
|
||||
.filter_map(|d| {
|
||||
d.fmri.as_ref().map(|f| f.to_string())
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let licenses = if !manifest.licenses.is_empty() {
|
||||
Some(
|
||||
|
|
@ -2746,7 +2847,10 @@ impl FileBackend {
|
|||
};
|
||||
|
||||
// Add the package to the index
|
||||
index.add_package(&package_info, Some(&package_contents));
|
||||
index.add_package(
|
||||
&package_info,
|
||||
Some(&package_contents),
|
||||
);
|
||||
|
||||
// Found the package info, no need to check other attributes
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -216,8 +216,8 @@ impl From<bincode::error::EncodeError> for RepositoryError {
|
|||
}
|
||||
}
|
||||
pub mod catalog;
|
||||
pub(crate) mod file_backend;
|
||||
mod catalog_writer;
|
||||
pub(crate) mod file_backend;
|
||||
mod obsoleted;
|
||||
pub mod progress;
|
||||
mod rest_backend;
|
||||
|
|
@ -231,7 +231,7 @@ pub use catalog::{
|
|||
};
|
||||
pub use file_backend::FileBackend;
|
||||
pub use obsoleted::{ObsoletedPackageManager, ObsoletedPackageMetadata};
|
||||
pub use progress::{ProgressInfo, ProgressReporter, NoopProgressReporter};
|
||||
pub use progress::{NoopProgressReporter, ProgressInfo, ProgressReporter};
|
||||
pub use rest_backend::RestBackend;
|
||||
|
||||
/// Repository configuration filename
|
||||
|
|
@ -248,7 +248,10 @@ pub struct BatchOptions {
|
|||
|
||||
impl Default for BatchOptions {
|
||||
fn default() -> Self {
|
||||
BatchOptions { batch_size: 2000, flush_every_n: 1 }
|
||||
BatchOptions {
|
||||
batch_size: 2000,
|
||||
flush_every_n: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -367,12 +370,7 @@ pub trait ReadableRepository {
|
|||
/// Fetch a content payload identified by digest into the destination path.
|
||||
/// Implementations should download/copy the payload to a temporary path,
|
||||
/// verify integrity, and atomically move into `dest`.
|
||||
fn fetch_payload(
|
||||
&mut self,
|
||||
publisher: &str,
|
||||
digest: &str,
|
||||
dest: &Path,
|
||||
) -> Result<()>;
|
||||
fn fetch_payload(&mut self, publisher: &str, digest: &str, dest: &Path) -> Result<()>;
|
||||
|
||||
/// Fetch a package manifest by FMRI from the repository.
|
||||
/// Implementations should retrieve and parse the manifest for the given
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -13,12 +13,12 @@ use tracing::{debug, info, warn};
|
|||
use reqwest::blocking::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::catalog::CatalogManager;
|
||||
use super::{
|
||||
NoopProgressReporter, PackageContents, PackageInfo, ProgressInfo, ProgressReporter,
|
||||
PublisherInfo, ReadableRepository, RepositoryConfig, RepositoryError, RepositoryInfo,
|
||||
RepositoryVersion, Result, WritableRepository,
|
||||
};
|
||||
use super::catalog::CatalogManager;
|
||||
|
||||
/// Repository implementation that uses a REST API to interact with a remote repository.
|
||||
///
|
||||
|
|
@ -132,7 +132,10 @@ impl WritableRepository for RestBackend {
|
|||
}
|
||||
|
||||
// Check if the directory was created
|
||||
println!("Publisher directory exists after creation: {}", publisher_dir.exists());
|
||||
println!(
|
||||
"Publisher directory exists after creation: {}",
|
||||
publisher_dir.exists()
|
||||
);
|
||||
|
||||
// Create catalog directory
|
||||
let catalog_dir = publisher_dir.join("catalog");
|
||||
|
|
@ -144,7 +147,10 @@ impl WritableRepository for RestBackend {
|
|||
}
|
||||
|
||||
// Check if the directory was created
|
||||
println!("Catalog directory exists after creation: {}", catalog_dir.exists());
|
||||
println!(
|
||||
"Catalog directory exists after creation: {}",
|
||||
catalog_dir.exists()
|
||||
);
|
||||
|
||||
debug!("Created publisher directory: {}", publisher_dir.display());
|
||||
} else {
|
||||
|
|
@ -259,7 +265,9 @@ impl WritableRepository for RestBackend {
|
|||
|
||||
// Check if we have a local cache path
|
||||
if cloned_self.local_cache_path.is_none() {
|
||||
return Err(RepositoryError::Other("No local cache path set".to_string()));
|
||||
return Err(RepositoryError::Other(
|
||||
"No local cache path set".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Filter publishers if specified
|
||||
|
|
@ -336,21 +344,26 @@ impl ReadableRepository for RestBackend {
|
|||
match response.json::<Value>() {
|
||||
Ok(json) => {
|
||||
// Extract publisher information
|
||||
if let Some(publishers) = json.get("publishers").and_then(|p| p.as_object()) {
|
||||
if let Some(publishers) =
|
||||
json.get("publishers").and_then(|p| p.as_object())
|
||||
{
|
||||
for (name, _) in publishers {
|
||||
debug!("Found publisher: {}", name);
|
||||
config.publishers.push(name.clone());
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to parse publisher information: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Failed to fetch publisher information: HTTP status {}", response.status());
|
||||
warn!(
|
||||
"Failed to fetch publisher information: HTTP status {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to connect to repository: {}", e);
|
||||
}
|
||||
|
|
@ -536,12 +549,7 @@ impl ReadableRepository for RestBackend {
|
|||
Ok(package_contents)
|
||||
}
|
||||
|
||||
fn fetch_payload(
|
||||
&mut self,
|
||||
publisher: &str,
|
||||
digest: &str,
|
||||
dest: &Path,
|
||||
) -> Result<()> {
|
||||
fn fetch_payload(&mut self, publisher: &str, digest: &str, dest: &Path) -> Result<()> {
|
||||
// Determine hash and algorithm from the provided digest string
|
||||
let mut hash = digest.to_string();
|
||||
let mut algo: Option<crate::digest::DigestAlgorithm> = None;
|
||||
|
|
@ -556,10 +564,17 @@ impl ReadableRepository for RestBackend {
|
|||
return Err(RepositoryError::Other("Empty digest provided".to_string()));
|
||||
}
|
||||
|
||||
let shard = if hash.len() >= 2 { &hash[0..2] } else { &hash[..] };
|
||||
let shard = if hash.len() >= 2 {
|
||||
&hash[0..2]
|
||||
} else {
|
||||
&hash[..]
|
||||
};
|
||||
let candidates = vec![
|
||||
format!("{}/file/{}/{}", self.uri, shard, hash),
|
||||
format!("{}/publisher/{}/file/{}/{}", self.uri, publisher, shard, hash),
|
||||
format!(
|
||||
"{}/publisher/{}/file/{}/{}",
|
||||
self.uri, publisher, shard, hash
|
||||
),
|
||||
];
|
||||
|
||||
// Ensure destination directory exists
|
||||
|
|
@ -571,11 +586,17 @@ impl ReadableRepository for RestBackend {
|
|||
for url in candidates {
|
||||
match self.client.get(&url).send() {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
let body = resp.bytes().map_err(|e| RepositoryError::Other(format!("Failed to read payload body: {}", e)))?;
|
||||
let body = resp.bytes().map_err(|e| {
|
||||
RepositoryError::Other(format!("Failed to read payload body: {}", e))
|
||||
})?;
|
||||
|
||||
// Verify digest if algorithm is known
|
||||
if let Some(alg) = algo.clone() {
|
||||
match crate::digest::Digest::from_bytes(&body, alg, crate::digest::DigestSource::PrimaryPayloadHash) {
|
||||
match crate::digest::Digest::from_bytes(
|
||||
&body,
|
||||
alg,
|
||||
crate::digest::DigestSource::PrimaryPayloadHash,
|
||||
) {
|
||||
Ok(comp) => {
|
||||
if comp.hash != hash {
|
||||
return Err(RepositoryError::DigestError(format!(
|
||||
|
|
@ -605,7 +626,9 @@ impl ReadableRepository for RestBackend {
|
|||
}
|
||||
}
|
||||
|
||||
Err(RepositoryError::NotFound(last_err.unwrap_or_else(|| "payload not found".to_string())))
|
||||
Err(RepositoryError::NotFound(
|
||||
last_err.unwrap_or_else(|| "payload not found".to_string()),
|
||||
))
|
||||
}
|
||||
|
||||
fn fetch_manifest(
|
||||
|
|
@ -636,14 +659,18 @@ impl RestBackend {
|
|||
// Require versioned FMRI
|
||||
let version = fmri.version();
|
||||
if version.is_empty() {
|
||||
return Err(RepositoryError::Other("FMRI must include a version to fetch manifest".into()));
|
||||
return Err(RepositoryError::Other(
|
||||
"FMRI must include a version to fetch manifest".into(),
|
||||
));
|
||||
}
|
||||
// URL-encode helper
|
||||
let url_encode = |s: &str| -> String {
|
||||
let mut out = String::new();
|
||||
for b in s.bytes() {
|
||||
match b {
|
||||
b'-' | b'_' | b'.' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => out.push(b as char),
|
||||
b'-' | b'_' | b'.' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => {
|
||||
out.push(b as char)
|
||||
}
|
||||
b' ' => out.push('+'),
|
||||
_ => {
|
||||
out.push('%');
|
||||
|
|
@ -658,16 +685,24 @@ impl RestBackend {
|
|||
let encoded_version = url_encode(&version);
|
||||
let candidates = vec![
|
||||
format!("{}/manifest/0/{}", self.uri, encoded_fmri),
|
||||
format!("{}/publisher/{}/manifest/0/{}", self.uri, publisher, encoded_fmri),
|
||||
format!(
|
||||
"{}/publisher/{}/manifest/0/{}",
|
||||
self.uri, publisher, encoded_fmri
|
||||
),
|
||||
// Fallbacks to direct file-style paths if server exposes static files
|
||||
format!("{}/pkg/{}/{}", self.uri, encoded_stem, encoded_version),
|
||||
format!("{}/publisher/{}/pkg/{}/{}", self.uri, publisher, encoded_stem, encoded_version),
|
||||
format!(
|
||||
"{}/publisher/{}/pkg/{}/{}",
|
||||
self.uri, publisher, encoded_stem, encoded_version
|
||||
),
|
||||
];
|
||||
let mut last_err: Option<String> = None;
|
||||
for url in candidates {
|
||||
match self.client.get(&url).send() {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
let text = resp.text().map_err(|e| RepositoryError::Other(format!("Failed to read manifest body: {}", e)))?;
|
||||
let text = resp.text().map_err(|e| {
|
||||
RepositoryError::Other(format!("Failed to read manifest body: {}", e))
|
||||
})?;
|
||||
return Ok(text);
|
||||
}
|
||||
Ok(resp) => {
|
||||
|
|
@ -678,7 +713,9 @@ impl RestBackend {
|
|||
}
|
||||
}
|
||||
}
|
||||
Err(RepositoryError::NotFound(last_err.unwrap_or_else(|| "manifest not found".to_string())))
|
||||
Err(RepositoryError::NotFound(
|
||||
last_err.unwrap_or_else(|| "manifest not found".to_string()),
|
||||
))
|
||||
}
|
||||
/// Sets the local path where catalog files will be cached.
|
||||
///
|
||||
|
|
@ -729,7 +766,9 @@ impl RestBackend {
|
|||
pub fn initialize(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> {
|
||||
// Check if we have a local cache path
|
||||
if self.local_cache_path.is_none() {
|
||||
return Err(RepositoryError::Other("No local cache path set".to_string()));
|
||||
return Err(RepositoryError::Other(
|
||||
"No local cache path set".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Download catalogs for all publishers
|
||||
|
|
@ -743,7 +782,11 @@ impl RestBackend {
|
|||
// Check if we have a local cache path
|
||||
let cache_path = match &self.local_cache_path {
|
||||
Some(path) => path,
|
||||
None => return Err(RepositoryError::Other("No local cache path set".to_string())),
|
||||
None => {
|
||||
return Err(RepositoryError::Other(
|
||||
"No local cache path set".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// The local cache path is expected to already point to the per-publisher directory
|
||||
|
|
@ -753,7 +796,8 @@ impl RestBackend {
|
|||
// Get or create the catalog manager pointing at the per-publisher directory directly
|
||||
if !self.catalog_managers.contains_key(publisher) {
|
||||
let catalog_manager = CatalogManager::new(cache_path, publisher)?;
|
||||
self.catalog_managers.insert(publisher.to_string(), catalog_manager);
|
||||
self.catalog_managers
|
||||
.insert(publisher.to_string(), catalog_manager);
|
||||
}
|
||||
|
||||
Ok(self.catalog_managers.get_mut(publisher).unwrap())
|
||||
|
|
@ -789,12 +833,18 @@ impl RestBackend {
|
|||
// Prepare candidate URLs to support both modern and legacy pkg5 depotd layouts
|
||||
let mut urls: Vec<String> = vec![
|
||||
format!("{}/catalog/1/{}", self.uri, file_name),
|
||||
format!("{}/publisher/{}/catalog/1/{}", self.uri, publisher, file_name),
|
||||
format!(
|
||||
"{}/publisher/{}/catalog/1/{}",
|
||||
self.uri, publisher, file_name
|
||||
),
|
||||
];
|
||||
if file_name == "catalog.attrs" {
|
||||
// Some older depots expose catalog.attrs at the root or under publisher path
|
||||
urls.insert(1, format!("{}/catalog.attrs", self.uri));
|
||||
urls.push(format!("{}/publisher/{}/catalog.attrs", self.uri, publisher));
|
||||
urls.push(format!(
|
||||
"{}/publisher/{}/catalog.attrs",
|
||||
self.uri, publisher
|
||||
));
|
||||
}
|
||||
|
||||
debug!(
|
||||
|
|
@ -855,10 +905,7 @@ impl RestBackend {
|
|||
"Failed to download '{}' from any known endpoint: {}",
|
||||
file_name, s
|
||||
),
|
||||
None => format!(
|
||||
"Failed to download '{}' from any known endpoint",
|
||||
file_name
|
||||
),
|
||||
None => format!("Failed to download '{}' from any known endpoint", file_name),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -890,7 +937,11 @@ impl RestBackend {
|
|||
// Check if we have a local cache path
|
||||
let cache_path = match &self.local_cache_path {
|
||||
Some(path) => path,
|
||||
None => return Err(RepositoryError::Other("No local cache path set".to_string())),
|
||||
None => {
|
||||
return Err(RepositoryError::Other(
|
||||
"No local cache path set".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure the per-publisher directory (local cache path) exists
|
||||
|
|
@ -913,19 +964,23 @@ impl RestBackend {
|
|||
|
||||
// Store the file directly under the per-publisher directory
|
||||
let file_path = cache_path.join(file_name);
|
||||
let mut file = File::create(&file_path)
|
||||
.map_err(|e| {
|
||||
// Report failure
|
||||
progress.finish(&progress_info);
|
||||
RepositoryError::FileWriteError { path: file_path.clone(), source: e }
|
||||
})?;
|
||||
let mut file = File::create(&file_path).map_err(|e| {
|
||||
// Report failure
|
||||
progress.finish(&progress_info);
|
||||
RepositoryError::FileWriteError {
|
||||
path: file_path.clone(),
|
||||
source: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
file.write_all(&content)
|
||||
.map_err(|e| {
|
||||
// Report failure
|
||||
progress.finish(&progress_info);
|
||||
RepositoryError::FileWriteError { path: file_path.clone(), source: e }
|
||||
})?;
|
||||
file.write_all(&content).map_err(|e| {
|
||||
// Report failure
|
||||
progress.finish(&progress_info);
|
||||
RepositoryError::FileWriteError {
|
||||
path: file_path.clone(),
|
||||
source: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
debug!("Stored catalog file: {}", file_path.display());
|
||||
|
||||
|
|
@ -969,26 +1024,29 @@ impl RestBackend {
|
|||
let progress_reporter = progress.unwrap_or(&NoopProgressReporter);
|
||||
|
||||
// Create progress info for the overall operation
|
||||
let mut overall_progress = ProgressInfo::new(format!("Downloading catalog for {}", publisher));
|
||||
let mut overall_progress =
|
||||
ProgressInfo::new(format!("Downloading catalog for {}", publisher));
|
||||
|
||||
// Notify that we're starting the download
|
||||
progress_reporter.start(&overall_progress);
|
||||
|
||||
// First download catalog.attrs to get the list of available parts
|
||||
let attrs_path = self.download_and_store_catalog_file(publisher, "catalog.attrs", progress)?;
|
||||
let attrs_path =
|
||||
self.download_and_store_catalog_file(publisher, "catalog.attrs", progress)?;
|
||||
|
||||
// Parse the catalog.attrs file to get the list of parts
|
||||
let attrs_content = fs::read_to_string(&attrs_path)
|
||||
.map_err(|e| {
|
||||
progress_reporter.finish(&overall_progress);
|
||||
RepositoryError::FileReadError { path: attrs_path.clone(), source: e }
|
||||
})?;
|
||||
let attrs_content = fs::read_to_string(&attrs_path).map_err(|e| {
|
||||
progress_reporter.finish(&overall_progress);
|
||||
RepositoryError::FileReadError {
|
||||
path: attrs_path.clone(),
|
||||
source: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
let attrs: Value = serde_json::from_str(&attrs_content)
|
||||
.map_err(|e| {
|
||||
progress_reporter.finish(&overall_progress);
|
||||
RepositoryError::JsonParseError(format!("Failed to parse catalog.attrs: {}", e))
|
||||
})?;
|
||||
let attrs: Value = serde_json::from_str(&attrs_content).map_err(|e| {
|
||||
progress_reporter.finish(&overall_progress);
|
||||
RepositoryError::JsonParseError(format!("Failed to parse catalog.attrs: {}", e))
|
||||
})?;
|
||||
|
||||
// Get the list of parts
|
||||
let parts = attrs["parts"].as_object().ok_or_else(|| {
|
||||
|
|
@ -1006,7 +1064,8 @@ impl RestBackend {
|
|||
debug!("Downloading catalog part: {}", part_name);
|
||||
|
||||
// Update progress with current part
|
||||
overall_progress = overall_progress.with_current(i as u64 + 2) // +2 because we already downloaded catalog.attrs
|
||||
overall_progress = overall_progress
|
||||
.with_current(i as u64 + 2) // +2 because we already downloaded catalog.attrs
|
||||
.with_context(format!("Downloading part: {}", part_name));
|
||||
progress_reporter.update(&overall_progress);
|
||||
|
||||
|
|
@ -1091,7 +1150,11 @@ impl RestBackend {
|
|||
/// # Returns
|
||||
///
|
||||
/// * `Result<()>` - Ok if the catalog was refreshed successfully, Err otherwise
|
||||
pub fn refresh_catalog(&mut self, publisher: &str, progress: Option<&dyn ProgressReporter>) -> Result<()> {
|
||||
pub fn refresh_catalog(
|
||||
&mut self,
|
||||
publisher: &str,
|
||||
progress: Option<&dyn ProgressReporter>,
|
||||
) -> Result<()> {
|
||||
self.download_catalog(publisher, progress)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ mod tests {
|
|||
use crate::actions::Manifest;
|
||||
use crate::fmri::Fmri;
|
||||
use crate::repository::{
|
||||
CatalogManager, FileBackend, ProgressInfo, ProgressReporter,
|
||||
ReadableRepository, RepositoryError, RepositoryVersion, RestBackend, Result, WritableRepository,
|
||||
REPOSITORY_CONFIG_FILENAME,
|
||||
CatalogManager, FileBackend, ProgressInfo, ProgressReporter, REPOSITORY_CONFIG_FILENAME,
|
||||
ReadableRepository, RepositoryError, RepositoryVersion, RestBackend, Result,
|
||||
WritableRepository,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -210,8 +210,14 @@ mod tests {
|
|||
assert!(FileBackend::construct_package_dir(&repo_path, "example.com", "").exists());
|
||||
|
||||
// Check that the pub.p5i file was created for backward compatibility
|
||||
let pub_p5i_path = repo_path.join("publisher").join("example.com").join("pub.p5i");
|
||||
assert!(pub_p5i_path.exists(), "pub.p5i file should be created for backward compatibility");
|
||||
let pub_p5i_path = repo_path
|
||||
.join("publisher")
|
||||
.join("example.com")
|
||||
.join("pub.p5i");
|
||||
assert!(
|
||||
pub_p5i_path.exists(),
|
||||
"pub.p5i file should be created for backward compatibility"
|
||||
);
|
||||
|
||||
// Verify the content of the pub.p5i file
|
||||
let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap();
|
||||
|
|
@ -246,7 +252,9 @@ mod tests {
|
|||
|
||||
// Add a package to the part using the stored publisher
|
||||
let fmri = Fmri::parse("pkg://test/example@1.0.0").unwrap();
|
||||
catalog_manager.add_package_to_part("test_part", &fmri, None, None).unwrap();
|
||||
catalog_manager
|
||||
.add_package_to_part("test_part", &fmri, None, None)
|
||||
.unwrap();
|
||||
|
||||
// Save the part
|
||||
catalog_manager.save_part("test_part").unwrap();
|
||||
|
|
@ -286,7 +294,13 @@ mod tests {
|
|||
publish_package(&mut repo, &manifest_path, &prototype_dir, "test").unwrap();
|
||||
|
||||
// Check that the files were published in the publisher-specific directory
|
||||
assert!(repo_path.join("publisher").join("test").join("file").exists());
|
||||
assert!(
|
||||
repo_path
|
||||
.join("publisher")
|
||||
.join("test")
|
||||
.join("file")
|
||||
.exists()
|
||||
);
|
||||
|
||||
// Get repository information
|
||||
let repo_info = repo.get_info().unwrap();
|
||||
|
|
@ -364,9 +378,11 @@ mod tests {
|
|||
|
||||
// Check for specific files
|
||||
assert!(files.iter().any(|f| f.contains("usr/bin/hello")));
|
||||
assert!(files
|
||||
.iter()
|
||||
.any(|f| f.contains("usr/share/doc/example/README.txt")));
|
||||
assert!(
|
||||
files
|
||||
.iter()
|
||||
.any(|f| f.contains("usr/share/doc/example/README.txt"))
|
||||
);
|
||||
assert!(files.iter().any(|f| f.contains("etc/config/example.conf")));
|
||||
|
||||
// Clean up
|
||||
|
|
@ -428,7 +444,8 @@ mod tests {
|
|||
let hash = repo.store_file(&test_file_path, "test").unwrap();
|
||||
|
||||
// Check if the file was stored in the correct directory structure
|
||||
let expected_path = FileBackend::construct_file_path_with_publisher(&repo_path, "test", &hash);
|
||||
let expected_path =
|
||||
FileBackend::construct_file_path_with_publisher(&repo_path, "test", &hash);
|
||||
|
||||
// Verify that the file exists at the expected path
|
||||
assert!(
|
||||
|
|
@ -480,7 +497,10 @@ mod tests {
|
|||
|
||||
// Check that the pub.p5i file was created for the new publisher
|
||||
let pub_p5i_path = repo_path.join("publisher").join(publisher).join("pub.p5i");
|
||||
assert!(pub_p5i_path.exists(), "pub.p5i file should be created for new publisher in transaction");
|
||||
assert!(
|
||||
pub_p5i_path.exists(),
|
||||
"pub.p5i file should be created for new publisher in transaction"
|
||||
);
|
||||
|
||||
// Verify the content of the pub.p5i file
|
||||
let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap();
|
||||
|
|
@ -514,7 +534,10 @@ mod tests {
|
|||
|
||||
// Check that the pkg5.repository file was created
|
||||
let pkg5_repo_path = repo_path.join("pkg5.repository");
|
||||
assert!(pkg5_repo_path.exists(), "pkg5.repository file should be created for backward compatibility");
|
||||
assert!(
|
||||
pkg5_repo_path.exists(),
|
||||
"pkg5.repository file should be created for backward compatibility"
|
||||
);
|
||||
|
||||
// Verify the content of the pkg5.repository file
|
||||
let pkg5_content = fs::read_to_string(&pkg5_repo_path).unwrap();
|
||||
|
|
@ -568,7 +591,10 @@ mod tests {
|
|||
println!("Publisher directory: {}", publisher_dir.display());
|
||||
println!("Publisher directory exists: {}", publisher_dir.exists());
|
||||
|
||||
assert!(publisher_dir.exists(), "Publisher directory should be created");
|
||||
assert!(
|
||||
publisher_dir.exists(),
|
||||
"Publisher directory should be created"
|
||||
);
|
||||
|
||||
let catalog_dir = publisher_dir.join("catalog");
|
||||
println!("Catalog directory: {}", catalog_dir.display());
|
||||
|
|
@ -733,7 +759,11 @@ mod tests {
|
|||
fs::write(&attrs_path, attrs_content).unwrap();
|
||||
|
||||
// Create mock catalog part files
|
||||
for part_name in ["catalog.base.C", "catalog.dependency.C", "catalog.summary.C"] {
|
||||
for part_name in [
|
||||
"catalog.base.C",
|
||||
"catalog.dependency.C",
|
||||
"catalog.summary.C",
|
||||
] {
|
||||
let part_path = catalog_dir.join(part_name);
|
||||
fs::write(&part_path, "{}").unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,38 +33,52 @@ pub struct AdviceReport {
|
|||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct AdviceOptions {
|
||||
pub max_depth: usize, // 0 = unlimited
|
||||
pub dependency_cap: usize, // 0 = unlimited per node
|
||||
pub max_depth: usize, // 0 = unlimited
|
||||
pub dependency_cap: usize, // 0 = unlimited per node
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Ctx {
|
||||
// caches
|
||||
catalog_cache: HashMap<String, Vec<(String, Fmri)>>, // stem -> [(publisher, fmri)]
|
||||
manifest_cache: HashMap<String, Manifest>, // fmri string -> manifest
|
||||
lock_cache: HashMap<String, Option<String>>, // stem -> incorporated release
|
||||
candidate_cache: HashMap<(String, Option<String>, Option<String>, Option<String>), Option<Fmri>>, // (stem, rel, branch, publisher)
|
||||
manifest_cache: HashMap<String, Manifest>, // fmri string -> manifest
|
||||
lock_cache: HashMap<String, Option<String>>, // stem -> incorporated release
|
||||
candidate_cache:
|
||||
HashMap<(String, Option<String>, Option<String>, Option<String>), Option<Fmri>>, // (stem, rel, branch, publisher)
|
||||
publisher_filter: Option<String>,
|
||||
cap: usize,
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
fn new(publisher_filter: Option<String>, cap: usize) -> Self {
|
||||
Self { publisher_filter, cap, ..Default::default() }
|
||||
Self {
|
||||
publisher_filter,
|
||||
cap,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advise_from_error(image: &Image, err: &SolverError, opts: AdviceOptions) -> Result<AdviceReport, AdviceError> {
|
||||
pub fn advise_from_error(
|
||||
image: &Image,
|
||||
err: &SolverError,
|
||||
opts: AdviceOptions,
|
||||
) -> Result<AdviceReport, AdviceError> {
|
||||
let mut report = AdviceReport::default();
|
||||
let Some(problem) = err.problem() else {
|
||||
return Ok(report);
|
||||
};
|
||||
|
||||
match &problem.kind {
|
||||
SolverProblemKind::NoCandidates { stem, release, branch } => {
|
||||
SolverProblemKind::NoCandidates {
|
||||
stem,
|
||||
release,
|
||||
branch,
|
||||
} => {
|
||||
// Advise directly on the missing root
|
||||
let mut ctx = Ctx::new(None, opts.dependency_cap);
|
||||
let details = build_missing_detail(image, &mut ctx, stem, release.as_deref(), branch.as_deref());
|
||||
let details =
|
||||
build_missing_detail(image, &mut ctx, stem, release.as_deref(), branch.as_deref());
|
||||
report.issues.push(AdviceIssue {
|
||||
path: vec![stem.clone()],
|
||||
stem: stem.clone(),
|
||||
|
|
@ -78,11 +92,23 @@ pub fn advise_from_error(image: &Image, err: &SolverError, opts: AdviceOptions)
|
|||
// Fall back to analyzing roots and traversing dependencies to find a missing candidate leaf.
|
||||
let mut ctx = Ctx::new(None, opts.dependency_cap);
|
||||
for root in &problem.roots {
|
||||
let root_fmri = match find_best_candidate(image, &mut ctx, &root.stem, root.version_req.as_deref(), root.branch.as_deref()) {
|
||||
let root_fmri = match find_best_candidate(
|
||||
image,
|
||||
&mut ctx,
|
||||
&root.stem,
|
||||
root.version_req.as_deref(),
|
||||
root.branch.as_deref(),
|
||||
) {
|
||||
Ok(Some(f)) => f,
|
||||
_ => {
|
||||
// Missing root candidate
|
||||
let details = build_missing_detail(image, &mut ctx, &root.stem, root.version_req.as_deref(), root.branch.as_deref());
|
||||
let details = build_missing_detail(
|
||||
image,
|
||||
&mut ctx,
|
||||
&root.stem,
|
||||
root.version_req.as_deref(),
|
||||
root.branch.as_deref(),
|
||||
);
|
||||
report.issues.push(AdviceIssue {
|
||||
path: vec![root.stem.clone()],
|
||||
stem: root.stem.clone(),
|
||||
|
|
@ -97,7 +123,16 @@ pub fn advise_from_error(image: &Image, err: &SolverError, opts: AdviceOptions)
|
|||
// Depth-first traversal looking for missing candidates
|
||||
let mut path = vec![root.stem.clone()];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
advise_recursive(image, &mut ctx, &root_fmri, &mut path, 1, opts.max_depth, &mut seen, &mut report)?;
|
||||
advise_recursive(
|
||||
image,
|
||||
&mut ctx,
|
||||
&root_fmri,
|
||||
&mut path,
|
||||
1,
|
||||
opts.max_depth,
|
||||
&mut seen,
|
||||
&mut report,
|
||||
)?;
|
||||
}
|
||||
Ok(report)
|
||||
}
|
||||
|
|
@ -114,30 +149,43 @@ fn advise_recursive(
|
|||
seen: &mut std::collections::HashSet<String>,
|
||||
report: &mut AdviceReport,
|
||||
) -> Result<(), AdviceError> {
|
||||
if max_depth != 0 && depth > max_depth { return Ok(()); }
|
||||
if max_depth != 0 && depth > max_depth {
|
||||
return Ok(());
|
||||
}
|
||||
let manifest = get_manifest_cached(image, ctx, fmri)?;
|
||||
|
||||
let mut processed = 0usize;
|
||||
for dep in manifest.dependencies.iter().filter(|d| d.dependency_type == "require" || d.dependency_type == "incorporate") {
|
||||
let Some(df) = &dep.fmri else { continue; };
|
||||
for dep in manifest
|
||||
.dependencies
|
||||
.iter()
|
||||
.filter(|d| d.dependency_type == "require" || d.dependency_type == "incorporate")
|
||||
{
|
||||
let Some(df) = &dep.fmri else {
|
||||
continue;
|
||||
};
|
||||
let dep_stem = df.stem().to_string();
|
||||
// Extract constraints from optional properties and, if absent, from the dependency FMRI version string
|
||||
let (mut rel, mut br) = extract_constraint(&dep.optional);
|
||||
let df_ver_str = df.version();
|
||||
if !df_ver_str.is_empty() {
|
||||
if rel.is_none() { rel = version_release(&df_ver_str); }
|
||||
if br.is_none() { br = version_branch(&df_ver_str); }
|
||||
if rel.is_none() {
|
||||
rel = version_release(&df_ver_str);
|
||||
}
|
||||
if br.is_none() {
|
||||
br = version_branch(&df_ver_str);
|
||||
}
|
||||
}
|
||||
// Mirror solver behavior: lock child to parent's branch when not explicitly constrained
|
||||
if br.is_none() {
|
||||
let parent_branch = fmri
|
||||
.version
|
||||
.as_ref()
|
||||
.and_then(|v| v.branch.clone());
|
||||
if let Some(pb) = parent_branch { br = Some(pb); }
|
||||
let parent_branch = fmri.version.as_ref().and_then(|v| v.branch.clone());
|
||||
if let Some(pb) = parent_branch {
|
||||
br = Some(pb);
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.cap != 0 && processed >= ctx.cap { break; }
|
||||
if ctx.cap != 0 && processed >= ctx.cap {
|
||||
break;
|
||||
}
|
||||
processed += 1;
|
||||
|
||||
match find_best_candidate(image, ctx, &dep_stem, rel.as_deref(), br.as_deref())? {
|
||||
|
|
@ -150,7 +198,8 @@ fn advise_recursive(
|
|||
}
|
||||
}
|
||||
None => {
|
||||
let details = build_missing_detail(image, ctx, &dep_stem, rel.as_deref(), br.as_deref());
|
||||
let details =
|
||||
build_missing_detail(image, ctx, &dep_stem, rel.as_deref(), br.as_deref());
|
||||
report.issues.push(AdviceIssue {
|
||||
path: path.clone(),
|
||||
stem: dep_stem.clone(),
|
||||
|
|
@ -177,32 +226,76 @@ fn extract_constraint(optional: &[Property]) -> (Option<String>, Option<String>)
|
|||
(release, branch)
|
||||
}
|
||||
|
||||
fn build_missing_detail(image: &Image, ctx: &mut Ctx, stem: &str, release: Option<&str>, branch: Option<&str>) -> String {
|
||||
fn build_missing_detail(
|
||||
image: &Image,
|
||||
ctx: &mut Ctx,
|
||||
stem: &str,
|
||||
release: Option<&str>,
|
||||
branch: Option<&str>,
|
||||
) -> String {
|
||||
let mut available: Vec<String> = Vec::new();
|
||||
if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) {
|
||||
for (pubname, fmri) in list {
|
||||
if let Some(ref pfilter) = ctx.publisher_filter { if &pubname != pfilter { continue; } }
|
||||
if fmri.stem() != stem { continue; }
|
||||
if let Some(ref pfilter) = ctx.publisher_filter {
|
||||
if &pubname != pfilter {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if fmri.stem() != stem {
|
||||
continue;
|
||||
}
|
||||
let ver = fmri.version();
|
||||
if ver.is_empty() { continue; }
|
||||
if ver.is_empty() {
|
||||
continue;
|
||||
}
|
||||
available.push(ver);
|
||||
}
|
||||
}
|
||||
available.sort();
|
||||
available.dedup();
|
||||
|
||||
let available_str = if available.is_empty() { "<none>".to_string() } else { available.join(", ") };
|
||||
let lock = get_incorporated_release_cached(image, ctx, stem).ok().flatten();
|
||||
let available_str = if available.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
available.join(", ")
|
||||
};
|
||||
let lock = get_incorporated_release_cached(image, ctx, stem)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
match (release, branch, lock.as_deref()) {
|
||||
(Some(r), Some(b), Some(lr)) => format!("Required release={}, branch={} not found. Image incorporation lock release={} may constrain candidates. Available versions: {}", r, b, lr, available_str),
|
||||
(Some(r), Some(b), None) => format!("Required release={}, branch={} not found. Available versions: {}", r, b, available_str),
|
||||
(Some(r), None, Some(lr)) => format!("Required release={} not found. Image incorporation lock release={} present. Available versions: {}", r, lr, available_str),
|
||||
(Some(r), None, None) => format!("Required release={} not found. Available versions: {}", r, available_str),
|
||||
(None, Some(b), Some(lr)) => format!("Required branch={} not found. Image incorporation lock release={} present. Available versions: {}", b, lr, available_str),
|
||||
(None, Some(b), None) => format!("Required branch={} not found. Available versions: {}", b, available_str),
|
||||
(None, None, Some(lr)) => format!("No candidates matched. Image incorporation lock release={} present. Available versions: {}", lr, available_str),
|
||||
(None, None, None) => format!("No candidates matched. Available versions: {}", available_str),
|
||||
(Some(r), Some(b), Some(lr)) => format!(
|
||||
"Required release={}, branch={} not found. Image incorporation lock release={} may constrain candidates. Available versions: {}",
|
||||
r, b, lr, available_str
|
||||
),
|
||||
(Some(r), Some(b), None) => format!(
|
||||
"Required release={}, branch={} not found. Available versions: {}",
|
||||
r, b, available_str
|
||||
),
|
||||
(Some(r), None, Some(lr)) => format!(
|
||||
"Required release={} not found. Image incorporation lock release={} present. Available versions: {}",
|
||||
r, lr, available_str
|
||||
),
|
||||
(Some(r), None, None) => format!(
|
||||
"Required release={} not found. Available versions: {}",
|
||||
r, available_str
|
||||
),
|
||||
(None, Some(b), Some(lr)) => format!(
|
||||
"Required branch={} not found. Image incorporation lock release={} present. Available versions: {}",
|
||||
b, lr, available_str
|
||||
),
|
||||
(None, Some(b), None) => format!(
|
||||
"Required branch={} not found. Available versions: {}",
|
||||
b, available_str
|
||||
),
|
||||
(None, None, Some(lr)) => format!(
|
||||
"No candidates matched. Image incorporation lock release={} present. Available versions: {}",
|
||||
lr, available_str
|
||||
),
|
||||
(None, None, None) => format!(
|
||||
"No candidates matched. Available versions: {}",
|
||||
available_str
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -219,20 +312,48 @@ fn find_best_candidate(
|
|||
req_branch.map(|s| s.to_string()),
|
||||
ctx.publisher_filter.clone(),
|
||||
);
|
||||
if let Some(cached) = ctx.candidate_cache.get(&key) { return Ok(cached.clone()); }
|
||||
if let Some(cached) = ctx.candidate_cache.get(&key) {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
|
||||
let lock_release = if req_release.is_none() { get_incorporated_release_cached(image, ctx, stem).ok().flatten() } else { None };
|
||||
let lock_release = if req_release.is_none() {
|
||||
get_incorporated_release_cached(image, ctx, stem)
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut candidates: Vec<(String, Fmri)> = Vec::new();
|
||||
for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? {
|
||||
if let Some(ref pfilter) = ctx.publisher_filter { if &pubf != pfilter { continue; } }
|
||||
if pfmri.stem() != stem { continue; }
|
||||
if let Some(ref pfilter) = ctx.publisher_filter {
|
||||
if &pubf != pfilter {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if pfmri.stem() != stem {
|
||||
continue;
|
||||
}
|
||||
let ver = pfmri.version();
|
||||
if ver.is_empty() { continue; }
|
||||
if ver.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let rel = version_release(&ver);
|
||||
let br = version_branch(&ver);
|
||||
if let Some(req_r) = req_release { if Some(req_r) != rel.as_deref() { continue; } } else if let Some(lock_r) = lock_release.as_deref() { if Some(lock_r) != rel.as_deref() { continue; } }
|
||||
if let Some(req_b) = req_branch { if Some(req_b) != br.as_deref() { continue; } }
|
||||
if let Some(req_r) = req_release {
|
||||
if Some(req_r) != rel.as_deref() {
|
||||
continue;
|
||||
}
|
||||
} else if let Some(lock_r) = lock_release.as_deref() {
|
||||
if Some(lock_r) != rel.as_deref() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(req_b) = req_branch {
|
||||
if Some(req_b) != br.as_deref() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
candidates.push((ver.clone(), pfmri.clone()));
|
||||
}
|
||||
|
||||
|
|
@ -247,7 +368,9 @@ fn version_release(version: &str) -> Option<String> {
|
|||
}
|
||||
|
||||
fn version_branch(version: &str) -> Option<String> {
|
||||
if let Some((_, rest)) = version.split_once(',') { return rest.split_once('-').map(|(b, _)| b.to_string()); }
|
||||
if let Some((_, rest)) = version.split_once(',') {
|
||||
return rest.split_once('-').map(|(b, _)| b.to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
|
@ -256,8 +379,13 @@ fn query_catalog_cached(
|
|||
ctx: &Ctx,
|
||||
stem: &str,
|
||||
) -> Result<Vec<(String, Fmri)>, AdviceError> {
|
||||
if let Some(v) = ctx.catalog_cache.get(stem) { return Ok(v.clone()); }
|
||||
let mut tmp = Ctx { catalog_cache: ctx.catalog_cache.clone(), ..Default::default() };
|
||||
if let Some(v) = ctx.catalog_cache.get(stem) {
|
||||
return Ok(v.clone());
|
||||
}
|
||||
let mut tmp = Ctx {
|
||||
catalog_cache: ctx.catalog_cache.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
query_catalog_cached_mut(image, &mut tmp, stem)
|
||||
}
|
||||
|
||||
|
|
@ -266,26 +394,48 @@ fn query_catalog_cached_mut(
|
|||
ctx: &mut Ctx,
|
||||
stem: &str,
|
||||
) -> Result<Vec<(String, Fmri)>, AdviceError> {
|
||||
if let Some(v) = ctx.catalog_cache.get(stem) { return Ok(v.clone()); }
|
||||
if let Some(v) = ctx.catalog_cache.get(stem) {
|
||||
return Ok(v.clone());
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let res = image.query_catalog(Some(stem)).map_err(|e| AdviceError{ message: format!("Failed to query catalog for {}: {}", stem, e) })?;
|
||||
for p in res { out.push((p.publisher, p.fmri)); }
|
||||
let res = image.query_catalog(Some(stem)).map_err(|e| AdviceError {
|
||||
message: format!("Failed to query catalog for {}: {}", stem, e),
|
||||
})?;
|
||||
for p in res {
|
||||
out.push((p.publisher, p.fmri));
|
||||
}
|
||||
ctx.catalog_cache.insert(stem.to_string(), out.clone());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn get_manifest_cached(image: &Image, ctx: &mut Ctx, fmri: &Fmri) -> Result<Manifest, AdviceError> {
|
||||
let key = fmri.to_string();
|
||||
if let Some(m) = ctx.manifest_cache.get(&key) { return Ok(m.clone()); }
|
||||
let manifest_opt = image.get_manifest_from_catalog(fmri).map_err(|e| AdviceError { message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e) })?;
|
||||
if let Some(m) = ctx.manifest_cache.get(&key) {
|
||||
return Ok(m.clone());
|
||||
}
|
||||
let manifest_opt = image
|
||||
.get_manifest_from_catalog(fmri)
|
||||
.map_err(|e| AdviceError {
|
||||
message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e),
|
||||
})?;
|
||||
let manifest = manifest_opt.unwrap_or_else(Manifest::new);
|
||||
ctx.manifest_cache.insert(key, manifest.clone());
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
fn get_incorporated_release_cached(image: &Image, ctx: &mut Ctx, stem: &str) -> Result<Option<String>, AdviceError> {
|
||||
if let Some(v) = ctx.lock_cache.get(stem) { return Ok(v.clone()); }
|
||||
let v = image.get_incorporated_release(stem).map_err(|e| AdviceError{ message: format!("Failed to read incorporation lock for {}: {}", stem, e) })?;
|
||||
fn get_incorporated_release_cached(
|
||||
image: &Image,
|
||||
ctx: &mut Ctx,
|
||||
stem: &str,
|
||||
) -> Result<Option<String>, AdviceError> {
|
||||
if let Some(v) = ctx.lock_cache.get(stem) {
|
||||
return Ok(v.clone());
|
||||
}
|
||||
let v = image
|
||||
.get_incorporated_release(stem)
|
||||
.map_err(|e| AdviceError {
|
||||
message: format!("Failed to read incorporation lock for {}: {}", stem, e),
|
||||
})?;
|
||||
ctx.lock_cache.insert(stem.to_string(), v.clone());
|
||||
Ok(v)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -71,7 +71,7 @@ mod tests {
|
|||
|
||||
// Create a temporary directory for the test
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let manifest_path = temp_dir.path().join("test_manifest.p5m"); // Changed extension to .p5m
|
||||
let manifest_path = temp_dir.path().join("test_manifest.p5m"); // Changed extension to .p5m
|
||||
|
||||
// Create a JSON manifest in the new format
|
||||
let json_manifest = r#"{
|
||||
|
|
@ -120,7 +120,7 @@ mod tests {
|
|||
Ok(manifest) => {
|
||||
println!("Manifest parsing succeeded");
|
||||
manifest
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Manifest parsing failed: {:?}", e);
|
||||
panic!("Failed to parse manifest: {:?}", e);
|
||||
|
|
@ -142,7 +142,10 @@ mod tests {
|
|||
assert_eq!(parsed_manifest.attributes[1].values[0], "true");
|
||||
|
||||
// Check third attribute
|
||||
assert_eq!(parsed_manifest.attributes[2].key, "org.opensolaris.consolidation");
|
||||
assert_eq!(
|
||||
parsed_manifest.attributes[2].key,
|
||||
"org.opensolaris.consolidation"
|
||||
);
|
||||
assert_eq!(parsed_manifest.attributes[2].values[0], "userland");
|
||||
|
||||
// Verify that properties is empty but exists
|
||||
|
|
|
|||
|
|
@ -803,8 +803,8 @@ fn emit_action_into_manifest(manifest: &mut Manifest, action_line: &str) -> Resu
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::actions::{Attr, File};
|
||||
use super::*;
|
||||
use crate::actions::{Attr, File};
|
||||
|
||||
#[test]
|
||||
fn add_default_set_attr() {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ use libips::image::{Image, ImageType};
|
|||
|
||||
fn should_run_network_tests() -> bool {
|
||||
// Even when ignored, provide an env switch to document intent
|
||||
env::var("IPS_E2E_NET").map(|v| v == "1" || v.to_lowercase() == "true").unwrap_or(false)
|
||||
env::var("IPS_E2E_NET")
|
||||
.map(|v| v == "1" || v.to_lowercase() == "true")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -38,7 +40,8 @@ fn e2e_download_and_build_catalog_openindiana() {
|
|||
let img_path = temp.path().join("image");
|
||||
|
||||
// Create the image
|
||||
let mut image = Image::create_image(&img_path, ImageType::Full).expect("failed to create image");
|
||||
let mut image =
|
||||
Image::create_image(&img_path, ImageType::Full).expect("failed to create image");
|
||||
|
||||
// Add OpenIndiana publisher
|
||||
let publisher = "openindiana.org";
|
||||
|
|
@ -52,12 +55,12 @@ fn e2e_download_and_build_catalog_openindiana() {
|
|||
.download_publisher_catalog(publisher)
|
||||
.expect("failed to download publisher catalog");
|
||||
|
||||
image.build_catalog().expect("failed to build merged catalog");
|
||||
image
|
||||
.build_catalog()
|
||||
.expect("failed to build merged catalog");
|
||||
|
||||
// Query catalog; we expect at least one package
|
||||
let packages = image
|
||||
.query_catalog(None)
|
||||
.expect("failed to query catalog");
|
||||
let packages = image.query_catalog(None).expect("failed to query catalog");
|
||||
|
||||
assert!(
|
||||
!packages.is_empty(),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use libips::actions::executors::InstallerError as LibInstallerError;
|
||||
use libips::fmri::FmriError;
|
||||
use libips::image::ImageError;
|
||||
use libips::solver::SolverError;
|
||||
use libips::actions::executors::InstallerError as LibInstallerError;
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
|
||||
|
|
@ -12,17 +12,11 @@ pub type Result<T> = std::result::Result<T, Pkg6Error>;
|
|||
#[derive(Debug, Error, Diagnostic)]
|
||||
pub enum Pkg6Error {
|
||||
#[error("I/O error: {0}")]
|
||||
#[diagnostic(
|
||||
code(pkg6::io_error),
|
||||
help("Check system resources and permissions")
|
||||
)]
|
||||
#[diagnostic(code(pkg6::io_error), help("Check system resources and permissions"))]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
#[diagnostic(
|
||||
code(pkg6::json_error),
|
||||
help("Check the JSON format and try again")
|
||||
)]
|
||||
#[diagnostic(code(pkg6::json_error), help("Check the JSON format and try again"))]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
|
||||
#[error("FMRI error: {0}")]
|
||||
|
|
|
|||
428
pkg6/src/main.rs
428
pkg6/src/main.rs
|
|
@ -3,8 +3,8 @@ use error::{Pkg6Error, Result};
|
|||
|
||||
use clap::{Parser, Subcommand};
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info};
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
|
|
@ -507,7 +507,11 @@ fn main() -> Result<()> {
|
|||
let cli = App::parse();
|
||||
|
||||
match &cli.command {
|
||||
Commands::Refresh { full, quiet, publishers } => {
|
||||
Commands::Refresh {
|
||||
full,
|
||||
quiet,
|
||||
publishers,
|
||||
} => {
|
||||
info!("Refreshing package catalog");
|
||||
debug!("Full refresh: {}", full);
|
||||
debug!("Quiet mode: {}", quiet);
|
||||
|
|
@ -526,7 +530,9 @@ fn main() -> Result<()> {
|
|||
error!("Failed to load image from {}: {}", image_path.display(), e);
|
||||
if !quiet {
|
||||
eprintln!("Failed to load image from {}: {}", image_path.display(), e);
|
||||
eprintln!("Make sure the path points to a valid image or use pkg6 image-create first");
|
||||
eprintln!(
|
||||
"Make sure the path points to a valid image or use pkg6 image-create first"
|
||||
);
|
||||
}
|
||||
return Err(e.into());
|
||||
}
|
||||
|
|
@ -546,8 +552,19 @@ fn main() -> Result<()> {
|
|||
println!("Refresh completed successfully");
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
Commands::Install { dry_run, verbose, quiet, concurrency, repo, accept, licenses, no_index, no_refresh, pkg_fmri_patterns } => {
|
||||
}
|
||||
Commands::Install {
|
||||
dry_run,
|
||||
verbose,
|
||||
quiet,
|
||||
concurrency,
|
||||
repo,
|
||||
accept,
|
||||
licenses,
|
||||
no_index,
|
||||
no_refresh,
|
||||
pkg_fmri_patterns,
|
||||
} => {
|
||||
info!("Installing packages: {:?}", pkg_fmri_patterns);
|
||||
debug!("Dry run: {}", dry_run);
|
||||
debug!("Verbose: {}", verbose);
|
||||
|
|
@ -561,7 +578,9 @@ fn main() -> Result<()> {
|
|||
|
||||
// Determine the image path using the -R argument or default rules
|
||||
let image_path = determine_image_path(cli.image_path.clone());
|
||||
if !quiet { println!("Using image at: {}", image_path.display()); }
|
||||
if !quiet {
|
||||
println!("Using image at: {}", image_path.display());
|
||||
}
|
||||
|
||||
// Load the image
|
||||
let image = match libips::image::Image::load(&image_path) {
|
||||
|
|
@ -576,12 +595,16 @@ fn main() -> Result<()> {
|
|||
// a full import or refresh automatically. Run `pkg6 refresh` explicitly
|
||||
// to update catalogs before installing if needed.
|
||||
if !*quiet {
|
||||
eprintln!("Install uses existing catalogs in redb; run 'pkg6 refresh' to update catalogs if needed.");
|
||||
eprintln!(
|
||||
"Install uses existing catalogs in redb; run 'pkg6 refresh' to update catalogs if needed."
|
||||
);
|
||||
}
|
||||
|
||||
// Build solver constraints from the provided pkg specs
|
||||
if pkg_fmri_patterns.is_empty() {
|
||||
if !quiet { eprintln!("No packages specified to install"); }
|
||||
if !quiet {
|
||||
eprintln!("No packages specified to install");
|
||||
}
|
||||
return Err(Pkg6Error::Other("no packages specified".to_string()));
|
||||
}
|
||||
let mut constraints: Vec<libips::solver::Constraint> = Vec::new();
|
||||
|
|
@ -601,44 +624,77 @@ fn main() -> Result<()> {
|
|||
} else {
|
||||
(name_part.to_string(), None)
|
||||
};
|
||||
constraints.push(libips::solver::Constraint { stem, version_req, preferred_publishers, branch: None });
|
||||
constraints.push(libips::solver::Constraint {
|
||||
stem,
|
||||
version_req,
|
||||
preferred_publishers,
|
||||
branch: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve install plan
|
||||
if !quiet { println!("Resolving dependencies..."); }
|
||||
if !quiet {
|
||||
println!("Resolving dependencies...");
|
||||
}
|
||||
let plan = match libips::solver::resolve_install(&image, &constraints) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
let mut printed_advice = false;
|
||||
if !*quiet {
|
||||
// Attempt to provide user-focused advice on how to resolve dependency issues
|
||||
let opts = libips::solver::advice::AdviceOptions { max_depth: 3, dependency_cap: 400 };
|
||||
let opts = libips::solver::advice::AdviceOptions {
|
||||
max_depth: 3,
|
||||
dependency_cap: 400,
|
||||
};
|
||||
match libips::solver::advice::advise_from_error(&image, &e, opts) {
|
||||
Ok(report) => {
|
||||
if !report.issues.is_empty() {
|
||||
printed_advice = true;
|
||||
eprintln!("\nAdvice: detected {} issue(s) preventing installation:", report.issues.len());
|
||||
eprintln!(
|
||||
"\nAdvice: detected {} issue(s) preventing installation:",
|
||||
report.issues.len()
|
||||
);
|
||||
for (i, iss) in report.issues.iter().enumerate() {
|
||||
let constraint_str = {
|
||||
let mut s = String::new();
|
||||
if let Some(r) = &iss.constraint_release { s.push_str(&format!("release={} ", r)); }
|
||||
if let Some(b) = &iss.constraint_branch { s.push_str(&format!("branch={}", b)); }
|
||||
if let Some(r) = &iss.constraint_release {
|
||||
s.push_str(&format!("release={} ", r));
|
||||
}
|
||||
if let Some(b) = &iss.constraint_branch {
|
||||
s.push_str(&format!("branch={}", b));
|
||||
}
|
||||
s.trim().to_string()
|
||||
};
|
||||
eprintln!(
|
||||
" {}. Missing viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}",
|
||||
i + 1,
|
||||
iss.stem,
|
||||
if iss.path.is_empty() { iss.stem.clone() } else { iss.path.join(" -> ") },
|
||||
if constraint_str.is_empty() { "<none>".to_string() } else { constraint_str },
|
||||
if iss.path.is_empty() {
|
||||
iss.stem.clone()
|
||||
} else {
|
||||
iss.path.join(" -> ")
|
||||
},
|
||||
if constraint_str.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
constraint_str
|
||||
},
|
||||
iss.details
|
||||
);
|
||||
}
|
||||
eprintln!("\nWhat you can try as a user:");
|
||||
eprintln!(" • Ensure your catalogs are up to date: 'pkg6 refresh'.");
|
||||
eprintln!(" • Verify that the required publishers are configured: 'pkg6 publisher'.");
|
||||
eprintln!(" • Some versions may be constrained by image incorporations; updating the image or selecting a compatible package set may help.");
|
||||
eprintln!(" • If the problem persists, report this to the repository maintainers with the above details.");
|
||||
eprintln!(
|
||||
" • Ensure your catalogs are up to date: 'pkg6 refresh'."
|
||||
);
|
||||
eprintln!(
|
||||
" • Verify that the required publishers are configured: 'pkg6 publisher'."
|
||||
);
|
||||
eprintln!(
|
||||
" • Some versions may be constrained by image incorporations; updating the image or selecting a compatible package set may help."
|
||||
);
|
||||
eprintln!(
|
||||
" • If the problem persists, report this to the repository maintainers with the above details."
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(advice_err) => {
|
||||
|
|
@ -657,19 +713,29 @@ fn main() -> Result<()> {
|
|||
}
|
||||
};
|
||||
|
||||
if !quiet { println!("Resolved {} package(s) to install", plan.add.len()); }
|
||||
if !quiet {
|
||||
println!("Resolved {} package(s) to install", plan.add.len());
|
||||
}
|
||||
|
||||
// Build and apply action plan
|
||||
if !quiet { println!("Building action plan..."); }
|
||||
if !quiet {
|
||||
println!("Building action plan...");
|
||||
}
|
||||
let ap = libips::image::action_plan::ActionPlan::from_install_plan(&plan);
|
||||
let quiet_mode = *quiet;
|
||||
let progress_cb: libips::actions::executors::ProgressCallback = Arc::new(move |evt| {
|
||||
if quiet_mode { return; }
|
||||
if quiet_mode {
|
||||
return;
|
||||
}
|
||||
match evt {
|
||||
libips::actions::executors::ProgressEvent::StartingPhase { phase, total } => {
|
||||
println!("Applying: {} (total {})...", phase, total);
|
||||
}
|
||||
libips::actions::executors::ProgressEvent::Progress { phase, current, total } => {
|
||||
libips::actions::executors::ProgressEvent::Progress {
|
||||
phase,
|
||||
current,
|
||||
total,
|
||||
} => {
|
||||
println!("Applying: {} {}/{}", phase, current, total);
|
||||
}
|
||||
libips::actions::executors::ProgressEvent::FinishedPhase { phase, total } => {
|
||||
|
|
@ -677,13 +743,21 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
});
|
||||
let apply_opts = libips::actions::executors::ApplyOptions { dry_run: *dry_run, progress: Some(progress_cb), progress_interval: 10 };
|
||||
if !quiet { println!("Applying action plan (dry-run: {})", dry_run); }
|
||||
let apply_opts = libips::actions::executors::ApplyOptions {
|
||||
dry_run: *dry_run,
|
||||
progress: Some(progress_cb),
|
||||
progress_interval: 10,
|
||||
};
|
||||
if !quiet {
|
||||
println!("Applying action plan (dry-run: {})", dry_run);
|
||||
}
|
||||
ap.apply(image.path(), &apply_opts)?;
|
||||
|
||||
// Update installed DB after success (skip on dry-run)
|
||||
if !*dry_run {
|
||||
if !quiet { println!("Recording installation in image database..."); }
|
||||
if !quiet {
|
||||
println!("Recording installation in image database...");
|
||||
}
|
||||
let total_pkgs = plan.add.len();
|
||||
let mut idx = 0usize;
|
||||
for rp in &plan.add {
|
||||
|
|
@ -705,21 +779,38 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
}
|
||||
if !quiet { println!("Installed {} package(s)", plan.add.len()); }
|
||||
if !quiet {
|
||||
println!("Installed {} package(s)", plan.add.len());
|
||||
}
|
||||
|
||||
// Dump installed database to make changes visible
|
||||
let installed = libips::image::installed::InstalledPackages::new(image.installed_db_path());
|
||||
let installed =
|
||||
libips::image::installed::InstalledPackages::new(image.installed_db_path());
|
||||
if let Err(e) = installed.dump_installed_table() {
|
||||
error!("Failed to dump installed database: {}", e);
|
||||
}
|
||||
} else if !quiet {
|
||||
println!("Dry-run completed: {} package(s) would be installed", plan.add.len());
|
||||
println!(
|
||||
"Dry-run completed: {} package(s) would be installed",
|
||||
plan.add.len()
|
||||
);
|
||||
}
|
||||
|
||||
info!("Installation completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::ExactInstall { dry_run, verbose, quiet, concurrency, repo, accept, licenses, no_index, no_refresh, pkg_fmri_patterns } => {
|
||||
}
|
||||
Commands::ExactInstall {
|
||||
dry_run,
|
||||
verbose,
|
||||
quiet,
|
||||
concurrency,
|
||||
repo,
|
||||
accept,
|
||||
licenses,
|
||||
no_index,
|
||||
no_refresh,
|
||||
pkg_fmri_patterns,
|
||||
} => {
|
||||
info!("Exact-installing packages: {:?}", pkg_fmri_patterns);
|
||||
debug!("Dry run: {}", dry_run);
|
||||
debug!("Verbose: {}", verbose);
|
||||
|
|
@ -734,8 +825,13 @@ fn main() -> Result<()> {
|
|||
// Stub implementation
|
||||
info!("Exact-installation completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::Uninstall { dry_run, verbose, quiet, pkg_fmri_patterns } => {
|
||||
}
|
||||
Commands::Uninstall {
|
||||
dry_run,
|
||||
verbose,
|
||||
quiet,
|
||||
pkg_fmri_patterns,
|
||||
} => {
|
||||
info!("Uninstalling packages: {:?}", pkg_fmri_patterns);
|
||||
debug!("Dry run: {}", dry_run);
|
||||
debug!("Verbose: {}", verbose);
|
||||
|
|
@ -744,8 +840,19 @@ fn main() -> Result<()> {
|
|||
// Stub implementation
|
||||
info!("Uninstallation completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::Update { dry_run, verbose, quiet, concurrency, repo, accept, licenses, no_index, no_refresh, pkg_fmri_patterns } => {
|
||||
}
|
||||
Commands::Update {
|
||||
dry_run,
|
||||
verbose,
|
||||
quiet,
|
||||
concurrency,
|
||||
repo,
|
||||
accept,
|
||||
licenses,
|
||||
no_index,
|
||||
no_refresh,
|
||||
pkg_fmri_patterns,
|
||||
} => {
|
||||
info!("Updating packages: {:?}", pkg_fmri_patterns);
|
||||
debug!("Dry run: {}", dry_run);
|
||||
debug!("Verbose: {}", verbose);
|
||||
|
|
@ -760,8 +867,14 @@ fn main() -> Result<()> {
|
|||
// Stub implementation
|
||||
info!("Update completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::List { verbose, quiet, all, output_format, pkg_fmri_patterns } => {
|
||||
}
|
||||
Commands::List {
|
||||
verbose,
|
||||
quiet,
|
||||
all,
|
||||
output_format,
|
||||
pkg_fmri_patterns,
|
||||
} => {
|
||||
info!("Listing packages: {:?}", pkg_fmri_patterns);
|
||||
debug!("Verbose: {}", verbose);
|
||||
debug!("Quiet: {}", quiet);
|
||||
|
|
@ -777,7 +890,9 @@ fn main() -> Result<()> {
|
|||
Ok(img) => img,
|
||||
Err(e) => {
|
||||
error!("Failed to load image from {}: {}", image_path.display(), e);
|
||||
error!("Make sure the path points to a valid image or use pkg6 image-create first");
|
||||
error!(
|
||||
"Make sure the path points to a valid image or use pkg6 image-create first"
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
|
@ -804,21 +919,27 @@ fn main() -> Result<()> {
|
|||
|
||||
match image.query_catalog(pattern) {
|
||||
Ok(packages) => {
|
||||
println!("PUBLISHER NAME VERSION STATE");
|
||||
println!("------------------------------------------------------------------------------------------------------------------------------------------------------");
|
||||
println!(
|
||||
"PUBLISHER NAME VERSION STATE"
|
||||
);
|
||||
println!(
|
||||
"------------------------------------------------------------------------------------------------------------------------------------------------------"
|
||||
);
|
||||
for pkg in packages {
|
||||
let state = if image.is_package_installed(&pkg.fmri).unwrap_or(false) {
|
||||
"installed"
|
||||
} else {
|
||||
"known"
|
||||
};
|
||||
println!("{:<40} {:<40} {:<30} {}",
|
||||
println!(
|
||||
"{:<40} {:<40} {:<30} {}",
|
||||
pkg.fmri.publisher.as_deref().unwrap_or("unknown"),
|
||||
pkg.fmri.name,
|
||||
pkg.fmri.version(),
|
||||
state);
|
||||
state
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to query catalog: {}", e);
|
||||
return Err(e.into());
|
||||
|
|
@ -829,16 +950,22 @@ fn main() -> Result<()> {
|
|||
info!("Listing installed packages");
|
||||
match image.query_installed_packages(pattern) {
|
||||
Ok(packages) => {
|
||||
println!("PUBLISHER NAME VERSION STATE");
|
||||
println!("------------------------------------------------------------------------------------------------------------------------------------------------------");
|
||||
println!(
|
||||
"PUBLISHER NAME VERSION STATE"
|
||||
);
|
||||
println!(
|
||||
"------------------------------------------------------------------------------------------------------------------------------------------------------"
|
||||
);
|
||||
for pkg in packages {
|
||||
println!("{:<40} {:<40} {:<30} {}",
|
||||
println!(
|
||||
"{:<40} {:<40} {:<30} {}",
|
||||
pkg.fmri.publisher.as_deref().unwrap_or("unknown"),
|
||||
pkg.fmri.name,
|
||||
pkg.fmri.version(),
|
||||
"installed");
|
||||
"installed"
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to query installed packages: {}", e);
|
||||
return Err(e.into());
|
||||
|
|
@ -848,8 +975,13 @@ fn main() -> Result<()> {
|
|||
|
||||
info!("List completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::Info { verbose, quiet, output_format, pkg_fmri_patterns } => {
|
||||
}
|
||||
Commands::Info {
|
||||
verbose,
|
||||
quiet,
|
||||
output_format,
|
||||
pkg_fmri_patterns,
|
||||
} => {
|
||||
info!("Showing info for packages: {:?}", pkg_fmri_patterns);
|
||||
debug!("Verbose: {}", verbose);
|
||||
debug!("Quiet: {}", quiet);
|
||||
|
|
@ -858,8 +990,13 @@ fn main() -> Result<()> {
|
|||
// Stub implementation
|
||||
info!("Info completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::Search { verbose, quiet, output_format, query } => {
|
||||
}
|
||||
Commands::Search {
|
||||
verbose,
|
||||
quiet,
|
||||
output_format,
|
||||
query,
|
||||
} => {
|
||||
info!("Searching for packages matching: {}", query);
|
||||
debug!("Verbose: {}", verbose);
|
||||
debug!("Quiet: {}", quiet);
|
||||
|
|
@ -868,8 +1005,12 @@ fn main() -> Result<()> {
|
|||
// Stub implementation
|
||||
info!("Search completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::Verify { verbose, quiet, pkg_fmri_patterns } => {
|
||||
}
|
||||
Commands::Verify {
|
||||
verbose,
|
||||
quiet,
|
||||
pkg_fmri_patterns,
|
||||
} => {
|
||||
info!("Verifying packages: {:?}", pkg_fmri_patterns);
|
||||
debug!("Verbose: {}", verbose);
|
||||
debug!("Quiet: {}", quiet);
|
||||
|
|
@ -877,8 +1018,13 @@ fn main() -> Result<()> {
|
|||
// Stub implementation
|
||||
info!("Verification completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::Fix { dry_run, verbose, quiet, pkg_fmri_patterns } => {
|
||||
}
|
||||
Commands::Fix {
|
||||
dry_run,
|
||||
verbose,
|
||||
quiet,
|
||||
pkg_fmri_patterns,
|
||||
} => {
|
||||
info!("Fixing packages: {:?}", pkg_fmri_patterns);
|
||||
debug!("Dry run: {}", dry_run);
|
||||
debug!("Verbose: {}", verbose);
|
||||
|
|
@ -887,8 +1033,12 @@ fn main() -> Result<()> {
|
|||
// Stub implementation
|
||||
info!("Fix completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::History { count, full, output_format } => {
|
||||
}
|
||||
Commands::History {
|
||||
count,
|
||||
full,
|
||||
output_format,
|
||||
} => {
|
||||
info!("Showing history");
|
||||
debug!("Count: {:?}", count);
|
||||
debug!("Full: {}", full);
|
||||
|
|
@ -897,8 +1047,13 @@ fn main() -> Result<()> {
|
|||
// Stub implementation
|
||||
info!("History completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::Contents { verbose, quiet, output_format, pkg_fmri_patterns } => {
|
||||
}
|
||||
Commands::Contents {
|
||||
verbose,
|
||||
quiet,
|
||||
output_format,
|
||||
pkg_fmri_patterns,
|
||||
} => {
|
||||
info!("Showing contents for packages: {:?}", pkg_fmri_patterns);
|
||||
debug!("Verbose: {}", verbose);
|
||||
debug!("Quiet: {}", quiet);
|
||||
|
|
@ -907,8 +1062,12 @@ fn main() -> Result<()> {
|
|||
// Stub implementation
|
||||
info!("Contents completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::SetPublisher { publisher, origin, mirror } => {
|
||||
}
|
||||
Commands::SetPublisher {
|
||||
publisher,
|
||||
origin,
|
||||
mirror,
|
||||
} => {
|
||||
info!("Setting publisher: {}", publisher);
|
||||
debug!("Origin: {:?}", origin);
|
||||
debug!("Mirror: {:?}", mirror);
|
||||
|
|
@ -922,7 +1081,9 @@ fn main() -> Result<()> {
|
|||
Ok(img) => img,
|
||||
Err(e) => {
|
||||
error!("Failed to load image from {}: {}", image_path.display(), e);
|
||||
error!("Make sure the path points to a valid image or use pkg6 image-create first");
|
||||
error!(
|
||||
"Make sure the path points to a valid image or use pkg6 image-create first"
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
|
@ -937,7 +1098,10 @@ fn main() -> Result<()> {
|
|||
if let Some(origin_url) = origin {
|
||||
// Add or update the publisher
|
||||
image.add_publisher(&publisher, &origin_url, mirrors, true)?;
|
||||
info!("Publisher {} configured with origin: {}", publisher, origin_url);
|
||||
info!(
|
||||
"Publisher {} configured with origin: {}",
|
||||
publisher, origin_url
|
||||
);
|
||||
|
||||
// Download the catalog
|
||||
image.download_publisher_catalog(&publisher)?;
|
||||
|
|
@ -955,13 +1119,15 @@ fn main() -> Result<()> {
|
|||
info!("Publisher {} set as default", publisher);
|
||||
} else {
|
||||
error!("Publisher {} not found and no origin provided", publisher);
|
||||
return Err(libips::image::ImageError::PublisherNotFound(publisher.clone()).into());
|
||||
return Err(
|
||||
libips::image::ImageError::PublisherNotFound(publisher.clone()).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Set-publisher completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
Commands::UnsetPublisher { publisher } => {
|
||||
info!("Unsetting publisher: {}", publisher);
|
||||
|
||||
|
|
@ -974,7 +1140,9 @@ fn main() -> Result<()> {
|
|||
Ok(img) => img,
|
||||
Err(e) => {
|
||||
error!("Failed to load image from {}: {}", image_path.display(), e);
|
||||
error!("Make sure the path points to a valid image or use pkg6 image-create first");
|
||||
error!(
|
||||
"Make sure the path points to a valid image or use pkg6 image-create first"
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
|
@ -993,8 +1161,12 @@ fn main() -> Result<()> {
|
|||
info!("Publisher {} removed successfully", publisher);
|
||||
info!("Unset-publisher completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::Publisher { verbose, output_format, publishers } => {
|
||||
}
|
||||
Commands::Publisher {
|
||||
verbose,
|
||||
output_format,
|
||||
publishers,
|
||||
} => {
|
||||
info!("Showing publisher information");
|
||||
|
||||
// Determine the image path using the -R argument or default rules
|
||||
|
|
@ -1006,7 +1178,9 @@ fn main() -> Result<()> {
|
|||
Ok(img) => img,
|
||||
Err(e) => {
|
||||
error!("Failed to load image from {}: {}", image_path.display(), e);
|
||||
error!("Make sure the path points to a valid image or use pkg6 image-create first");
|
||||
error!(
|
||||
"Make sure the path points to a valid image or use pkg6 image-create first"
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
|
@ -1076,7 +1250,10 @@ fn main() -> Result<()> {
|
|||
println!(" {}", mirror);
|
||||
}
|
||||
}
|
||||
println!(" Default: {}", if publisher.is_default { "Yes" } else { "No" });
|
||||
println!(
|
||||
" Default: {}",
|
||||
if publisher.is_default { "Yes" } else { "No" }
|
||||
);
|
||||
if let Some(catalog_dir) = &publisher.catalog_dir {
|
||||
println!(" Catalog directory: {}", catalog_dir);
|
||||
}
|
||||
|
|
@ -1084,7 +1261,7 @@ fn main() -> Result<()> {
|
|||
// Explicitly flush stdout after each publisher to ensure output is displayed
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
},
|
||||
}
|
||||
"json" => {
|
||||
// Display in JSON format
|
||||
// This format is useful for programmatic access to the publisher information
|
||||
|
|
@ -1095,7 +1272,7 @@ fn main() -> Result<()> {
|
|||
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
||||
println!("{}", json);
|
||||
let _ = std::io::stdout().flush();
|
||||
},
|
||||
}
|
||||
"tsv" => {
|
||||
// Display in TSV format (tab-separated values)
|
||||
// This format is useful for importing into spreadsheets or other data processing tools
|
||||
|
|
@ -1108,26 +1285,30 @@ fn main() -> Result<()> {
|
|||
let default = if publisher.is_default { "Yes" } else { "No" };
|
||||
let catalog_dir = publisher.catalog_dir.as_deref().unwrap_or("");
|
||||
|
||||
println!("{}\t{}\t{}\t{}\t{}",
|
||||
publisher.name,
|
||||
publisher.origin,
|
||||
mirrors,
|
||||
default,
|
||||
catalog_dir
|
||||
println!(
|
||||
"{}\t{}\t{}\t{}\t{}",
|
||||
publisher.name, publisher.origin, mirrors, default, catalog_dir
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
// Unsupported format
|
||||
return Err(Pkg6Error::UnsupportedOutputFormat(output_format_str.to_string()));
|
||||
return Err(Pkg6Error::UnsupportedOutputFormat(
|
||||
output_format_str.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
info!("Publisher completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Commands::ImageCreate { full_path, publisher, origin, image_type } => {
|
||||
}
|
||||
Commands::ImageCreate {
|
||||
full_path,
|
||||
publisher,
|
||||
origin,
|
||||
image_type,
|
||||
} => {
|
||||
info!("Creating image at: {}", full_path.display());
|
||||
debug!("Publisher: {:?}", publisher);
|
||||
debug!("Origin: {:?}", origin);
|
||||
|
|
@ -1148,21 +1329,36 @@ fn main() -> Result<()> {
|
|||
info!("Image created successfully at: {}", full_path.display());
|
||||
|
||||
// If publisher and origin are provided, only add the publisher; do not download/open catalogs here.
|
||||
if let (Some(publisher_name), Some(origin_url)) = (publisher.as_ref(), origin.as_ref()) {
|
||||
info!("Adding publisher {} with origin {}", publisher_name, origin_url);
|
||||
if let (Some(publisher_name), Some(origin_url)) = (publisher.as_ref(), origin.as_ref())
|
||||
{
|
||||
info!(
|
||||
"Adding publisher {} with origin {}",
|
||||
publisher_name, origin_url
|
||||
);
|
||||
|
||||
// Add the publisher
|
||||
image.add_publisher(publisher_name, origin_url, vec![], true)?;
|
||||
|
||||
info!("Publisher {} configured with origin: {}", publisher_name, origin_url);
|
||||
info!("Catalogs are not downloaded during image creation. Use 'pkg6 -R {} refresh {}' to download and open catalogs.", full_path.display(), publisher_name);
|
||||
info!(
|
||||
"Publisher {} configured with origin: {}",
|
||||
publisher_name, origin_url
|
||||
);
|
||||
info!(
|
||||
"Catalogs are not downloaded during image creation. Use 'pkg6 -R {} refresh {}' to download and open catalogs.",
|
||||
full_path.display(),
|
||||
publisher_name
|
||||
);
|
||||
} else {
|
||||
info!("No publisher configured. Use 'pkg6 set-publisher' to add a publisher.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
Commands::DebugDb { stats, dump_all, dump_table } => {
|
||||
}
|
||||
Commands::DebugDb {
|
||||
stats,
|
||||
dump_all,
|
||||
dump_table,
|
||||
} => {
|
||||
info!("Debug database command");
|
||||
debug!("Stats: {}", stats);
|
||||
debug!("Dump all: {}", dump_all);
|
||||
|
|
@ -1177,7 +1373,9 @@ fn main() -> Result<()> {
|
|||
Ok(img) => img,
|
||||
Err(e) => {
|
||||
error!("Failed to load image from {}: {}", image_path.display(), e);
|
||||
error!("Make sure the path points to a valid image or use pkg6 image-create first");
|
||||
error!(
|
||||
"Make sure the path points to a valid image or use pkg6 image-create first"
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
|
@ -1186,13 +1384,12 @@ fn main() -> Result<()> {
|
|||
let catalog = libips::image::catalog::ImageCatalog::new(
|
||||
image.catalog_dir(),
|
||||
image.catalog_db_path(),
|
||||
image.obsoleted_db_path()
|
||||
image.obsoleted_db_path(),
|
||||
);
|
||||
|
||||
// Create an installed packages object for the installed.redb database
|
||||
let installed = libips::image::installed::InstalledPackages::new(
|
||||
image.installed_db_path()
|
||||
);
|
||||
let installed =
|
||||
libips::image::installed::InstalledPackages::new(image.installed_db_path());
|
||||
|
||||
// Execute the requested debug command
|
||||
if *stats {
|
||||
|
|
@ -1200,13 +1397,19 @@ fn main() -> Result<()> {
|
|||
println!("=== CATALOG DATABASE ===");
|
||||
if let Err(e) = catalog.get_db_stats() {
|
||||
error!("Failed to get catalog database statistics: {}", e);
|
||||
return Err(Pkg6Error::Other(format!("Failed to get catalog database statistics: {}", e)));
|
||||
return Err(Pkg6Error::Other(format!(
|
||||
"Failed to get catalog database statistics: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
|
||||
println!("\n=== INSTALLED DATABASE ===");
|
||||
if let Err(e) = installed.get_db_stats() {
|
||||
error!("Failed to get installed database statistics: {}", e);
|
||||
return Err(Pkg6Error::Other(format!("Failed to get installed database statistics: {}", e)));
|
||||
return Err(Pkg6Error::Other(format!(
|
||||
"Failed to get installed database statistics: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1215,13 +1418,19 @@ fn main() -> Result<()> {
|
|||
println!("=== CATALOG DATABASE ===");
|
||||
if let Err(e) = catalog.dump_all_tables() {
|
||||
error!("Failed to dump catalog database tables: {}", e);
|
||||
return Err(Pkg6Error::Other(format!("Failed to dump catalog database tables: {}", e)));
|
||||
return Err(Pkg6Error::Other(format!(
|
||||
"Failed to dump catalog database tables: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
|
||||
println!("\n=== INSTALLED DATABASE ===");
|
||||
if let Err(e) = installed.dump_installed_table() {
|
||||
error!("Failed to dump installed database table: {}", e);
|
||||
return Err(Pkg6Error::Other(format!("Failed to dump installed database table: {}", e)));
|
||||
return Err(Pkg6Error::Other(format!(
|
||||
"Failed to dump installed database table: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1235,26 +1444,35 @@ fn main() -> Result<()> {
|
|||
println!("=== INSTALLED DATABASE ===");
|
||||
if let Err(e) = installed.dump_installed_table() {
|
||||
error!("Failed to dump installed table: {}", e);
|
||||
return Err(Pkg6Error::Other(format!("Failed to dump installed table: {}", e)));
|
||||
return Err(Pkg6Error::Other(format!(
|
||||
"Failed to dump installed table: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
},
|
||||
}
|
||||
"catalog" | "obsoleted" => {
|
||||
// Use the catalog database
|
||||
println!("=== CATALOG DATABASE ===");
|
||||
if let Err(e) = catalog.dump_table(table_name) {
|
||||
error!("Failed to dump table {}: {}", table_name, e);
|
||||
return Err(Pkg6Error::Other(format!("Failed to dump table {}: {}", table_name, e)));
|
||||
return Err(Pkg6Error::Other(format!(
|
||||
"Failed to dump table {}: {}",
|
||||
table_name, e
|
||||
)));
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
error!("Unknown table: {}", table_name);
|
||||
return Err(Pkg6Error::Other(format!("Unknown table: {}. Available tables: catalog, obsoleted, installed", table_name)));
|
||||
return Err(Pkg6Error::Other(format!(
|
||||
"Unknown table: {}. Available tables: catalog, obsoleted, installed",
|
||||
table_name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Debug database command completed successfully");
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::path::PathBuf;
|
||||
use crate::errors::DepotError;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, knuffel::Decode, Clone)]
|
||||
pub struct Config {
|
||||
|
|
@ -84,8 +84,9 @@ impl Config {
|
|||
pub fn load(path: Option<PathBuf>) -> crate::errors::Result<Self> {
|
||||
let path = path.unwrap_or_else(|| PathBuf::from("pkg6depotd.kdl"));
|
||||
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| DepotError::Config(format!("Failed to read config file {:?}: {}", path, e)))?;
|
||||
let content = fs::read_to_string(&path).map_err(|e| {
|
||||
DepotError::Config(format!("Failed to read config file {:?}: {}", path, e))
|
||||
})?;
|
||||
|
||||
knuffel::parse(path.to_str().unwrap_or("pkg6depotd.kdl"), &content)
|
||||
.map_err(|e| DepotError::Config(format!("Failed to parse config: {:?}", e)))
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
http::StatusCode,
|
||||
};
|
||||
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
pub enum DepotError {
|
||||
|
|
@ -31,8 +31,12 @@ pub enum DepotError {
|
|||
impl IntoResponse for DepotError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match &self {
|
||||
DepotError::Repo(libips::repository::RepositoryError::NotFound(_)) => (StatusCode::NOT_FOUND, self.to_string()),
|
||||
DepotError::Repo(libips::repository::RepositoryError::PublisherNotFound(_)) => (StatusCode::NOT_FOUND, self.to_string()),
|
||||
DepotError::Repo(libips::repository::RepositoryError::NotFound(_)) => {
|
||||
(StatusCode::NOT_FOUND, self.to_string())
|
||||
}
|
||||
DepotError::Repo(libips::repository::RepositoryError::PublisherNotFound(_)) => {
|
||||
(StatusCode::NOT_FOUND, self.to_string())
|
||||
}
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -14,9 +14,7 @@ struct HealthResponse {
|
|||
status: &'static str,
|
||||
}
|
||||
|
||||
pub async fn health(
|
||||
_state: State<Arc<DepotRepo>>,
|
||||
) -> impl IntoResponse {
|
||||
pub async fn health(_state: State<Arc<DepotRepo>>) -> impl IntoResponse {
|
||||
// Basic liveness/readiness for now. Future: include repo checks.
|
||||
(StatusCode::OK, Json(HealthResponse { status: "ok" }))
|
||||
}
|
||||
|
|
@ -33,11 +31,10 @@ struct AuthCheckResponse<'a> {
|
|||
/// Admin auth-check endpoint.
|
||||
/// For now, this is a minimal placeholder that only checks for the presence of a Bearer token.
|
||||
/// TODO: Validate JWT via OIDC JWKs using configured issuer/jwks_uri and required scopes.
|
||||
pub async fn auth_check(
|
||||
_state: State<Arc<DepotRepo>>,
|
||||
headers: HeaderMap,
|
||||
) -> Response {
|
||||
let auth = headers.get(axum::http::header::AUTHORIZATION).and_then(|v| v.to_str().ok());
|
||||
pub async fn auth_check(_state: State<Arc<DepotRepo>>, headers: HeaderMap) -> Response {
|
||||
let auth = headers
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok());
|
||||
let (authenticated, token_present) = match auth {
|
||||
Some(h) if h.to_ascii_lowercase().starts_with("bearer ") => (true, true),
|
||||
Some(_) => (false, true),
|
||||
|
|
@ -52,6 +49,10 @@ pub async fn auth_check(
|
|||
decision: if authenticated { "allow" } else { "deny" },
|
||||
};
|
||||
|
||||
let status = if authenticated { StatusCode::OK } else { StatusCode::UNAUTHORIZED };
|
||||
let status = if authenticated {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::UNAUTHORIZED
|
||||
};
|
||||
(status, Json(resp)).into_response()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use crate::errors::DepotError;
|
||||
use crate::repo::DepotRepo;
|
||||
use axum::http::header;
|
||||
use axum::{
|
||||
extract::{Path, State, Request},
|
||||
extract::{Path, Request, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use crate::repo::DepotRepo;
|
||||
use crate::errors::DepotError;
|
||||
use tower_http::services::ServeFile;
|
||||
use tower::ServiceExt;
|
||||
use axum::http::header;
|
||||
use tower_http::services::ServeFile;
|
||||
|
||||
pub async fn get_catalog_v1(
|
||||
State(repo): State<Arc<DepotRepo>>,
|
||||
|
|
@ -24,10 +24,13 @@ pub async fn get_catalog_v1(
|
|||
// Ensure correct content-type for JSON catalog artifacts regardless of file extension
|
||||
let is_catalog_json = filename == "catalog.attrs" || filename.starts_with("catalog.");
|
||||
if is_catalog_json {
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"));
|
||||
res.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
header::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
}
|
||||
Ok(res.into_response())
|
||||
},
|
||||
}
|
||||
Err(e) => Err(DepotError::Server(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,27 @@
|
|||
use axum::{
|
||||
extract::{Path, State, Request},
|
||||
response::{IntoResponse, Response},
|
||||
http::header,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tower_http::services::ServeFile;
|
||||
use tower::ServiceExt;
|
||||
use crate::repo::DepotRepo;
|
||||
use crate::errors::DepotError;
|
||||
use std::fs;
|
||||
use crate::repo::DepotRepo;
|
||||
use axum::{
|
||||
extract::{Path, Request, State},
|
||||
http::header,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use httpdate::fmt_http_date;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeFile;
|
||||
|
||||
pub async fn get_file(
|
||||
State(repo): State<Arc<DepotRepo>>,
|
||||
Path((publisher, _algo, digest)): Path<(String, String, String)>,
|
||||
req: Request,
|
||||
) -> Result<Response, DepotError> {
|
||||
let path = repo.get_file_path(&publisher, &digest)
|
||||
.ok_or_else(|| DepotError::Repo(libips::repository::RepositoryError::NotFound(digest.clone())))?;
|
||||
let path = repo.get_file_path(&publisher, &digest).ok_or_else(|| {
|
||||
DepotError::Repo(libips::repository::RepositoryError::NotFound(
|
||||
digest.clone(),
|
||||
))
|
||||
})?;
|
||||
|
||||
let service = ServeFile::new(path);
|
||||
let result = service.oneshot(req).await;
|
||||
|
|
@ -27,26 +30,42 @@ pub async fn get_file(
|
|||
Ok(mut res) => {
|
||||
// Add caching headers
|
||||
let max_age = repo.cache_max_age();
|
||||
res.headers_mut().insert(header::CACHE_CONTROL, header::HeaderValue::from_str(&format!("public, max-age={}", max_age)).unwrap());
|
||||
res.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
header::HeaderValue::from_str(&format!("public, max-age={}", max_age)).unwrap(),
|
||||
);
|
||||
// ETag from digest
|
||||
res.headers_mut().insert(header::ETAG, header::HeaderValue::from_str(&format!("\"{}\"", digest)).unwrap());
|
||||
res.headers_mut().insert(
|
||||
header::ETAG,
|
||||
header::HeaderValue::from_str(&format!("\"{}\"", digest)).unwrap(),
|
||||
);
|
||||
// Last-Modified from fs metadata
|
||||
if let Some(body_path) = res.extensions().get::<std::path::PathBuf>().cloned() {
|
||||
if let Ok(meta) = fs::metadata(&body_path) {
|
||||
if let Ok(mtime) = meta.modified() {
|
||||
let lm = fmt_http_date(mtime);
|
||||
res.headers_mut().insert(header::LAST_MODIFIED, header::HeaderValue::from_str(&lm).unwrap());
|
||||
res.headers_mut().insert(
|
||||
header::LAST_MODIFIED,
|
||||
header::HeaderValue::from_str(&lm).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: use now if extension not present (should rarely happen)
|
||||
if !res.headers().contains_key(header::LAST_MODIFIED) {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|_| SystemTime::now()).unwrap_or_else(SystemTime::now);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|_| SystemTime::now())
|
||||
.unwrap_or_else(SystemTime::now);
|
||||
let lm = fmt_http_date(now);
|
||||
res.headers_mut().insert(header::LAST_MODIFIED, header::HeaderValue::from_str(&lm).unwrap());
|
||||
res.headers_mut().insert(
|
||||
header::LAST_MODIFIED,
|
||||
header::HeaderValue::from_str(&lm).unwrap(),
|
||||
);
|
||||
}
|
||||
Ok(res.into_response())
|
||||
},
|
||||
}
|
||||
Err(e) => Err(DepotError::Server(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,33 @@
|
|||
use crate::errors::DepotError;
|
||||
use crate::repo::DepotRepo;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
http::header,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use crate::repo::DepotRepo;
|
||||
use crate::errors::DepotError;
|
||||
use libips::fmri::Fmri;
|
||||
use std::str::FromStr;
|
||||
use chrono::{Datelike, NaiveDateTime, TimeZone, Timelike, Utc};
|
||||
use libips::actions::Manifest;
|
||||
use chrono::{NaiveDateTime, Utc, TimeZone, Datelike, Timelike};
|
||||
use libips::actions::Property;
|
||||
use libips::fmri::Fmri;
|
||||
use std::fs;
|
||||
use std::io::Read as _;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn get_info(
|
||||
State(repo): State<Arc<DepotRepo>>,
|
||||
Path((publisher, fmri_str)): Path<(String, String)>,
|
||||
) -> Result<Response, DepotError> {
|
||||
let fmri = Fmri::from_str(&fmri_str).map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?;
|
||||
let fmri = Fmri::from_str(&fmri_str)
|
||||
.map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?;
|
||||
|
||||
let content = repo.get_manifest_text(&publisher, &fmri)?;
|
||||
|
||||
let manifest = match serde_json::from_str::<Manifest>(&content) {
|
||||
Ok(m) => m,
|
||||
Err(_) => Manifest::parse_string(content).map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?,
|
||||
Err(_) => Manifest::parse_string(content).map_err(|e| {
|
||||
DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string()))
|
||||
})?,
|
||||
};
|
||||
|
||||
let mut out = String::new();
|
||||
|
|
@ -46,17 +49,27 @@ pub async fn get_info(
|
|||
if let Some((rel_branch, ts)) = rest.split_once(':') {
|
||||
ts_str = Some(ts.to_string());
|
||||
if let Some((rel, br)) = rel_branch.split_once('-') {
|
||||
if !rel.is_empty() { build_release = Some(rel.to_string()); }
|
||||
if !br.is_empty() { branch = Some(br.to_string()); }
|
||||
if !rel.is_empty() {
|
||||
build_release = Some(rel.to_string());
|
||||
}
|
||||
if !br.is_empty() {
|
||||
branch = Some(br.to_string());
|
||||
}
|
||||
} else {
|
||||
// No branch
|
||||
if !rel_branch.is_empty() { build_release = Some(rel_branch.to_string()); }
|
||||
if !rel_branch.is_empty() {
|
||||
build_release = Some(rel_branch.to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No timestamp
|
||||
if let Some((rel, br)) = rest.split_once('-') {
|
||||
if !rel.is_empty() { build_release = Some(rel.to_string()); }
|
||||
if !br.is_empty() { branch = Some(br.to_string()); }
|
||||
if !rel.is_empty() {
|
||||
build_release = Some(rel.to_string());
|
||||
}
|
||||
if !br.is_empty() {
|
||||
branch = Some(br.to_string());
|
||||
}
|
||||
} else if !rest.is_empty() {
|
||||
build_release = Some(rest.to_string());
|
||||
}
|
||||
|
|
@ -64,8 +77,12 @@ pub async fn get_info(
|
|||
}
|
||||
|
||||
out.push_str(&format!("Version: {}\n", version_core));
|
||||
if let Some(rel) = build_release { out.push_str(&format!("Build Release: {}\n", rel)); }
|
||||
if let Some(br) = branch { out.push_str(&format!("Branch: {}\n", br)); }
|
||||
if let Some(rel) = build_release {
|
||||
out.push_str(&format!("Build Release: {}\n", rel));
|
||||
}
|
||||
if let Some(br) = branch {
|
||||
out.push_str(&format!("Branch: {}\n", br));
|
||||
}
|
||||
if let Some(ts) = ts_str.and_then(|s| format_packaging_date(&s)) {
|
||||
out.push_str(&format!("Packaging Date: {}\n", ts));
|
||||
}
|
||||
|
|
@ -89,7 +106,9 @@ pub async fn get_info(
|
|||
out.push_str("\nLicense:\n");
|
||||
let mut first = true;
|
||||
for license in &manifest.licenses {
|
||||
if !first { out.push('\n'); }
|
||||
if !first {
|
||||
out.push('\n');
|
||||
}
|
||||
first = false;
|
||||
|
||||
// Optional license name header for readability
|
||||
|
|
@ -105,20 +124,22 @@ pub async fn get_info(
|
|||
match resolve_license_text(&repo, &publisher, digest) {
|
||||
Some(text) => {
|
||||
out.push_str(&text);
|
||||
if !text.ends_with('\n') { out.push('\n'); }
|
||||
if !text.ends_with('\n') {
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Fallback: show the digest if content could not be resolved
|
||||
out.push_str(&format!("<license content unavailable for digest {}>\n", digest));
|
||||
out.push_str(&format!(
|
||||
"<license content unavailable for digest {}>\n",
|
||||
digest
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
[(header::CONTENT_TYPE, "text/plain")],
|
||||
out
|
||||
).into_response())
|
||||
Ok(([(header::CONTENT_TYPE, "text/plain")], out).into_response())
|
||||
}
|
||||
|
||||
// Try to read and decode the license text for a given digest from the repository.
|
||||
|
|
@ -152,7 +173,9 @@ fn resolve_license_text(repo: &DepotRepo, publisher: &str, digest: &str) -> Opti
|
|||
|
||||
let mut text = String::from_utf8_lossy(&data).to_string();
|
||||
if truncated {
|
||||
if !text.ends_with('\n') { text.push('\n'); }
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push_str("...[truncated]\n");
|
||||
}
|
||||
Some(text)
|
||||
|
|
@ -161,7 +184,7 @@ fn resolve_license_text(repo: &DepotRepo, publisher: &str, digest: &str) -> Opti
|
|||
fn find_attr(manifest: &Manifest, key: &str) -> Option<String> {
|
||||
for attr in &manifest.attributes {
|
||||
if attr.key == key {
|
||||
return attr.values.first().cloned();
|
||||
return attr.values.first().cloned();
|
||||
}
|
||||
}
|
||||
None
|
||||
|
|
@ -187,17 +210,32 @@ fn month_name(month: u32) -> &'static str {
|
|||
|
||||
fn format_packaging_date(ts: &str) -> Option<String> {
|
||||
// Expect formats like YYYYMMDDThhmmssZ or with fractional seconds before Z
|
||||
let clean_ts = if let Some((base, _frac)) = ts.split_once('.') { format!("{}Z", base) } else { ts.to_string() };
|
||||
let clean_ts = if let Some((base, _frac)) = ts.split_once('.') {
|
||||
format!("{}Z", base)
|
||||
} else {
|
||||
ts.to_string()
|
||||
};
|
||||
let ndt = NaiveDateTime::parse_from_str(&clean_ts, "%Y%m%dT%H%M%SZ").ok()?;
|
||||
let dt_utc = Utc.from_utc_datetime(&ndt);
|
||||
let month = month_name(dt_utc.month() as u32);
|
||||
let day = dt_utc.day();
|
||||
let year = dt_utc.year();
|
||||
let hour24 = dt_utc.hour();
|
||||
let (ampm, hour12) = if hour24 == 0 { ("AM", 12) } else if hour24 < 12 { ("AM", hour24) } else if hour24 == 12 { ("PM", 12) } else { ("PM", hour24 - 12) };
|
||||
let (ampm, hour12) = if hour24 == 0 {
|
||||
("AM", 12)
|
||||
} else if hour24 < 12 {
|
||||
("AM", hour24)
|
||||
} else if hour24 == 12 {
|
||||
("PM", 12)
|
||||
} else {
|
||||
("PM", hour24 - 12)
|
||||
};
|
||||
let minute = dt_utc.minute();
|
||||
let second = dt_utc.second();
|
||||
Some(format!("{} {:02}, {} at {:02}:{:02}:{:02} {}", month, day, year, hour12, minute, second, ampm))
|
||||
Some(format!(
|
||||
"{} {:02}, {} at {:02}:{:02}:{:02} {}",
|
||||
month, day, year, hour12, minute, second, ampm
|
||||
))
|
||||
}
|
||||
|
||||
// Sum pkg.size (uncompressed) and pkg.csize (compressed) over all file actions
|
||||
|
|
@ -208,9 +246,13 @@ fn compute_sizes(manifest: &Manifest) -> (u128, u128) {
|
|||
for file in &manifest.files {
|
||||
for Property { key, value } in &file.properties {
|
||||
if key == "pkg.size" {
|
||||
if let Ok(v) = value.parse::<u128>() { size = size.saturating_add(v); }
|
||||
if let Ok(v) = value.parse::<u128>() {
|
||||
size = size.saturating_add(v);
|
||||
}
|
||||
} else if key == "pkg.csize" {
|
||||
if let Ok(v) = value.parse::<u128>() { csize = csize.saturating_add(v); }
|
||||
if let Ok(v) = value.parse::<u128>() {
|
||||
csize = csize.saturating_add(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
use crate::errors::DepotError;
|
||||
use crate::repo::DepotRepo;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
http::header,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use crate::repo::DepotRepo;
|
||||
use crate::errors::DepotError;
|
||||
use libips::fmri::Fmri;
|
||||
use std::str::FromStr;
|
||||
use sha1::Digest as _;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn get_manifest(
|
||||
State(repo): State<Arc<DepotRepo>>,
|
||||
Path((publisher, fmri_str)): Path<(String, String)>,
|
||||
) -> Result<Response, DepotError> {
|
||||
let fmri = Fmri::from_str(&fmri_str).map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?;
|
||||
let fmri = Fmri::from_str(&fmri_str)
|
||||
.map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?;
|
||||
|
||||
let content = repo.get_manifest_text(&publisher, &fmri)?;
|
||||
// Compute weak ETag from SHA-1 of manifest content (legacy friendly)
|
||||
|
|
@ -28,5 +29,6 @@ pub async fn get_manifest(
|
|||
(header::ETAG, etag.as_str()),
|
||||
],
|
||||
content,
|
||||
).into_response())
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
pub mod versions;
|
||||
pub mod catalog;
|
||||
pub mod manifest;
|
||||
pub mod file;
|
||||
pub mod info;
|
||||
pub mod manifest;
|
||||
pub mod publisher;
|
||||
pub mod versions;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use crate::errors::DepotError;
|
||||
use crate::repo::DepotRepo;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
http::header,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use crate::repo::DepotRepo;
|
||||
use crate::errors::DepotError;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct P5iPublisherInfo {
|
||||
|
|
@ -43,10 +43,13 @@ async fn get_publisher_impl(
|
|||
) -> Result<Response, DepotError> {
|
||||
let repo_info = repo.get_info()?;
|
||||
|
||||
let pub_info = repo_info.publishers.into_iter().find(|p| p.name == publisher);
|
||||
let pub_info = repo_info
|
||||
.publishers
|
||||
.into_iter()
|
||||
.find(|p| p.name == publisher);
|
||||
|
||||
if let Some(p) = pub_info {
|
||||
let p5i = P5iFile {
|
||||
let p5i = P5iFile {
|
||||
packages: Vec::new(),
|
||||
publishers: vec![P5iPublisherInfo {
|
||||
alias: None,
|
||||
|
|
@ -56,12 +59,12 @@ async fn get_publisher_impl(
|
|||
}],
|
||||
version: 1,
|
||||
};
|
||||
let json = serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?;
|
||||
Ok((
|
||||
[(header::CONTENT_TYPE, "application/vnd.pkg5.info")],
|
||||
json
|
||||
).into_response())
|
||||
let json =
|
||||
serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?;
|
||||
Ok(([(header::CONTENT_TYPE, "application/vnd.pkg5.info")], json).into_response())
|
||||
} else {
|
||||
Err(DepotError::Repo(libips::repository::RepositoryError::PublisherNotFound(publisher)))
|
||||
Err(DepotError::Repo(
|
||||
libips::repository::RepositoryError::PublisherNotFound(publisher),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,12 +56,30 @@ pub async fn get_versions() -> impl IntoResponse {
|
|||
let response = VersionsResponse {
|
||||
server_version,
|
||||
operations: vec![
|
||||
SupportedOperation { op: Operation::Info, versions: vec![0] },
|
||||
SupportedOperation { op: Operation::Versions, versions: vec![0] },
|
||||
SupportedOperation { op: Operation::Catalog, versions: vec![1] },
|
||||
SupportedOperation { op: Operation::Manifest, versions: vec![0, 1] },
|
||||
SupportedOperation { op: Operation::File, versions: vec![0, 1] },
|
||||
SupportedOperation { op: Operation::Publisher, versions: vec![0, 1] },
|
||||
SupportedOperation {
|
||||
op: Operation::Info,
|
||||
versions: vec![0],
|
||||
},
|
||||
SupportedOperation {
|
||||
op: Operation::Versions,
|
||||
versions: vec![0],
|
||||
},
|
||||
SupportedOperation {
|
||||
op: Operation::Catalog,
|
||||
versions: vec![1],
|
||||
},
|
||||
SupportedOperation {
|
||||
op: Operation::Manifest,
|
||||
versions: vec![0, 1],
|
||||
},
|
||||
SupportedOperation {
|
||||
op: Operation::File,
|
||||
versions: vec![0, 1],
|
||||
},
|
||||
SupportedOperation {
|
||||
op: Operation::Publisher,
|
||||
versions: vec![0, 1],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
pub mod server;
|
||||
pub mod routes;
|
||||
pub mod admin;
|
||||
pub mod handlers;
|
||||
pub mod middleware;
|
||||
pub mod admin;
|
||||
pub mod routes;
|
||||
pub mod server;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,42 @@
|
|||
use crate::http::admin;
|
||||
use crate::http::handlers::{catalog, file, info, manifest, publisher, versions};
|
||||
use crate::repo::DepotRepo;
|
||||
use axum::{
|
||||
routing::{get, post, head},
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use crate::repo::DepotRepo;
|
||||
use crate::http::handlers::{versions, catalog, manifest, file, info, publisher};
|
||||
use crate::http::admin;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
pub fn app_router(state: Arc<DepotRepo>) -> Router {
|
||||
Router::new()
|
||||
.route("/versions/0/", get(versions::get_versions))
|
||||
.route("/{publisher}/catalog/1/{filename}", get(catalog::get_catalog_v1).head(catalog::get_catalog_v1))
|
||||
.route("/{publisher}/manifest/0/{fmri}", get(manifest::get_manifest).head(manifest::get_manifest))
|
||||
.route("/{publisher}/manifest/1/{fmri}", get(manifest::get_manifest).head(manifest::get_manifest))
|
||||
.route("/{publisher}/file/0/{algo}/{digest}", get(file::get_file).head(file::get_file))
|
||||
.route("/{publisher}/file/1/{algo}/{digest}", get(file::get_file).head(file::get_file))
|
||||
.route(
|
||||
"/{publisher}/catalog/1/{filename}",
|
||||
get(catalog::get_catalog_v1).head(catalog::get_catalog_v1),
|
||||
)
|
||||
.route(
|
||||
"/{publisher}/manifest/0/{fmri}",
|
||||
get(manifest::get_manifest).head(manifest::get_manifest),
|
||||
)
|
||||
.route(
|
||||
"/{publisher}/manifest/1/{fmri}",
|
||||
get(manifest::get_manifest).head(manifest::get_manifest),
|
||||
)
|
||||
.route(
|
||||
"/{publisher}/file/0/{algo}/{digest}",
|
||||
get(file::get_file).head(file::get_file),
|
||||
)
|
||||
.route(
|
||||
"/{publisher}/file/1/{algo}/{digest}",
|
||||
get(file::get_file).head(file::get_file),
|
||||
)
|
||||
.route("/{publisher}/info/0/{fmri}", get(info::get_info))
|
||||
.route("/{publisher}/publisher/0", get(publisher::get_publisher_v0))
|
||||
.route("/{publisher}/publisher/1", get(publisher::get_publisher_v1))
|
||||
// Admin API over HTTP
|
||||
.route("/admin/health", get(admin::health))
|
||||
.route("/admin/auth/check", post(admin::auth_check))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
use tokio::net::TcpListener;
|
||||
use axum::Router;
|
||||
use crate::errors::Result;
|
||||
use axum::Router;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
pub async fn run(router: Router, listener: TcpListener) -> Result<()> {
|
||||
let addr = listener.local_addr()?;
|
||||
tracing::info!("Listening on {}", addr);
|
||||
|
||||
axum::serve(listener, router).await.map_err(|e| crate::errors::DepotError::Server(e.to_string()))
|
||||
axum::serve(listener, router)
|
||||
.await
|
||||
.map_err(|e| crate::errors::DepotError::Server(e.to_string()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod daemon;
|
||||
pub mod errors;
|
||||
pub mod http;
|
||||
pub mod telemetry;
|
||||
pub mod repo;
|
||||
pub mod daemon;
|
||||
pub mod telemetry;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands};
|
||||
use config::Config;
|
||||
use miette::Result;
|
||||
use std::sync::Arc;
|
||||
use repo::DepotRepo;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn run() -> Result<()> {
|
||||
let args = Cli::parse();
|
||||
|
|
@ -57,13 +57,24 @@ pub async fn run() -> Result<()> {
|
|||
}
|
||||
|
||||
let router = http::routes::app_router(state);
|
||||
let bind_str = config.server.bind.first().cloned().unwrap_or_else(|| "0.0.0.0:8080".to_string());
|
||||
let addr: std::net::SocketAddr = bind_str.parse().map_err(crate::errors::DepotError::AddrParse)?;
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.map_err(crate::errors::DepotError::Io)?;
|
||||
let bind_str = config
|
||||
.server
|
||||
.bind
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "0.0.0.0:8080".to_string());
|
||||
let addr: std::net::SocketAddr = bind_str
|
||||
.parse()
|
||||
.map_err(crate::errors::DepotError::AddrParse)?;
|
||||
let listener = tokio::net::TcpListener::bind(addr)
|
||||
.await
|
||||
.map_err(crate::errors::DepotError::Io)?;
|
||||
|
||||
tracing::info!("Starting pkg6depotd on {}", bind_str);
|
||||
|
||||
http::server::run(router, listener).await.map_err(|e| miette::miette!(e))?;
|
||||
http::server::run(router, listener)
|
||||
.await
|
||||
.map_err(|e| miette::miette!(e))?;
|
||||
}
|
||||
Commands::ConfigTest => {
|
||||
println!("Configuration loaded successfully: {:?}", config);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use pkg6depotd::run;
|
||||
use miette::Result;
|
||||
use pkg6depotd::run;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use std::path::PathBuf;
|
||||
use libips::repository::{FileBackend, ReadableRepository};
|
||||
use crate::config::Config;
|
||||
use crate::errors::{Result, DepotError};
|
||||
use crate::errors::{DepotError, Result};
|
||||
use libips::fmri::Fmri;
|
||||
use libips::repository::{FileBackend, ReadableRepository};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub struct DepotRepo {
|
||||
|
|
@ -15,11 +15,12 @@ impl DepotRepo {
|
|||
pub fn new(config: &Config) -> Result<Self> {
|
||||
let root = config.repository.root.clone();
|
||||
let backend = FileBackend::open(&root).map_err(DepotError::Repo)?;
|
||||
let cache_max_age = config
|
||||
.server
|
||||
.cache_max_age
|
||||
.unwrap_or(3600);
|
||||
Ok(Self { backend: Mutex::new(backend), root, cache_max_age })
|
||||
let cache_max_age = config.server.cache_max_age.unwrap_or(3600);
|
||||
Ok(Self {
|
||||
backend: Mutex::new(backend),
|
||||
root,
|
||||
cache_max_age,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_catalog_path(&self, publisher: &str) -> PathBuf {
|
||||
|
|
@ -27,18 +28,27 @@ impl DepotRepo {
|
|||
}
|
||||
|
||||
pub fn get_file_path(&self, publisher: &str, hash: &str) -> Option<PathBuf> {
|
||||
let cand_pub = FileBackend::construct_file_path_with_publisher(&self.root, publisher, hash);
|
||||
if cand_pub.exists() { return Some(cand_pub); }
|
||||
let cand_pub = FileBackend::construct_file_path_with_publisher(&self.root, publisher, hash);
|
||||
if cand_pub.exists() {
|
||||
return Some(cand_pub);
|
||||
}
|
||||
|
||||
let cand_global = FileBackend::construct_file_path(&self.root, hash);
|
||||
if cand_global.exists() { return Some(cand_global); }
|
||||
let cand_global = FileBackend::construct_file_path(&self.root, hash);
|
||||
if cand_global.exists() {
|
||||
return Some(cand_global);
|
||||
}
|
||||
|
||||
None
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_manifest_text(&self, publisher: &str, fmri: &Fmri) -> Result<String> {
|
||||
let backend = self.backend.lock().map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?;
|
||||
backend.fetch_manifest_text(publisher, fmri).map_err(DepotError::Repo)
|
||||
let backend = self
|
||||
.backend
|
||||
.lock()
|
||||
.map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?;
|
||||
backend
|
||||
.fetch_manifest_text(publisher, fmri)
|
||||
.map_err(DepotError::Repo)
|
||||
}
|
||||
|
||||
pub fn get_manifest_path(&self, publisher: &str, fmri: &Fmri) -> Option<PathBuf> {
|
||||
|
|
@ -46,28 +56,54 @@ impl DepotRepo {
|
|||
if version.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let path = FileBackend::construct_manifest_path(&self.root, publisher, fmri.stem(), &version);
|
||||
if path.exists() { return Some(path); }
|
||||
let path =
|
||||
FileBackend::construct_manifest_path(&self.root, publisher, fmri.stem(), &version);
|
||||
if path.exists() {
|
||||
return Some(path);
|
||||
}
|
||||
// Fallbacks similar to lib logic
|
||||
let encoded_stem = url_encode_filename(fmri.stem());
|
||||
let encoded_version = url_encode_filename(&version);
|
||||
let alt1 = self.root.join("pkg").join(&encoded_stem).join(&encoded_version);
|
||||
if alt1.exists() { return Some(alt1); }
|
||||
let alt2 = self.root.join("publisher").join(publisher).join("pkg").join(&encoded_stem).join(&encoded_version);
|
||||
if alt2.exists() { return Some(alt2); }
|
||||
let alt1 = self
|
||||
.root
|
||||
.join("pkg")
|
||||
.join(&encoded_stem)
|
||||
.join(&encoded_version);
|
||||
if alt1.exists() {
|
||||
return Some(alt1);
|
||||
}
|
||||
let alt2 = self
|
||||
.root
|
||||
.join("publisher")
|
||||
.join(publisher)
|
||||
.join("pkg")
|
||||
.join(&encoded_stem)
|
||||
.join(&encoded_version);
|
||||
if alt2.exists() {
|
||||
return Some(alt2);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn cache_max_age(&self) -> u64 { self.cache_max_age }
|
||||
|
||||
pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result<PathBuf> {
|
||||
let backend = self.backend.lock().map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?;
|
||||
backend.get_catalog_file_path(publisher, filename).map_err(DepotError::Repo)
|
||||
pub fn cache_max_age(&self) -> u64 {
|
||||
self.cache_max_age
|
||||
}
|
||||
|
||||
pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result<PathBuf> {
|
||||
let backend = self
|
||||
.backend
|
||||
.lock()
|
||||
.map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?;
|
||||
backend
|
||||
.get_catalog_file_path(publisher, filename)
|
||||
.map_err(DepotError::Repo)
|
||||
}
|
||||
|
||||
pub fn get_info(&self) -> Result<libips::repository::RepositoryInfo> {
|
||||
let backend = self.backend.lock().map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?;
|
||||
let backend = self
|
||||
.backend
|
||||
.lock()
|
||||
.map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?;
|
||||
backend.get_info().map_err(DepotError::Repo)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
use crate::config::Config;
|
||||
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
pub fn init(_config: &Config) {
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use pkg6depotd::config::{Config, RepositoryConfig, ServerConfig};
|
||||
use pkg6depotd::repo::DepotRepo;
|
||||
use pkg6depotd::http;
|
||||
use libips::repository::{FileBackend, RepositoryVersion, WritableRepository};
|
||||
use libips::actions::{File as FileAction, Manifest};
|
||||
use libips::repository::{FileBackend, RepositoryVersion, WritableRepository};
|
||||
use pkg6depotd::config::{Config, RepositoryConfig, ServerConfig};
|
||||
use pkg6depotd::http;
|
||||
use pkg6depotd::repo::DepotRepo;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use tokio::net::TcpListener;
|
||||
use std::fs;
|
||||
|
||||
// Helper to setup a repo with a published package
|
||||
fn setup_repo(dir: &TempDir) -> PathBuf {
|
||||
|
|
@ -42,7 +42,7 @@ fn setup_repo(dir: &TempDir) -> PathBuf {
|
|||
values: vec!["pkg://test/example@1.0.0".to_string()],
|
||||
properties: HashMap::new(),
|
||||
});
|
||||
manifest.attributes.push(Attr {
|
||||
manifest.attributes.push(Attr {
|
||||
key: "pkg.summary".to_string(),
|
||||
values: vec!["Test Package".to_string()],
|
||||
properties: HashMap::new(),
|
||||
|
|
@ -98,7 +98,11 @@ async fn test_depot_server() {
|
|||
let base_url = format!("http://{}", addr);
|
||||
|
||||
// 1. Test Versions
|
||||
let resp = client.get(format!("{}/versions/0/", base_url)).send().await.unwrap();
|
||||
let resp = client
|
||||
.get(format!("{}/versions/0/", base_url))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
let text = resp.text().await.unwrap();
|
||||
assert!(text.contains("pkg-server pkg6depotd-0.5.1"));
|
||||
|
|
@ -111,7 +115,7 @@ async fn test_depot_server() {
|
|||
let catalog_v1_url = format!("{}/test/catalog/1/catalog.attrs", base_url);
|
||||
let resp = client.get(&catalog_v1_url).send().await.unwrap();
|
||||
if !resp.status().is_success() {
|
||||
println!("Catalog v1 failed: {:?}", resp);
|
||||
println!("Catalog v1 failed: {:?}", resp);
|
||||
}
|
||||
assert!(resp.status().is_success());
|
||||
let catalog_attrs = resp.text().await.unwrap();
|
||||
|
|
@ -144,13 +148,24 @@ async fn test_depot_server() {
|
|||
assert!(info_text.contains("Name: example"));
|
||||
assert!(info_text.contains("Summary: Test Package"));
|
||||
// Ensure FMRI format is correct: pkg://<publisher>/<name>@<version>
|
||||
assert!(info_text.contains("FMRI: pkg://test/example@1.0.0"), "Info FMRI was: {}", info_text);
|
||||
assert!(
|
||||
info_text.contains("FMRI: pkg://test/example@1.0.0"),
|
||||
"Info FMRI was: {}",
|
||||
info_text
|
||||
);
|
||||
|
||||
// 5. Test Publisher v1
|
||||
let pub_url = format!("{}/test/publisher/1", base_url);
|
||||
let resp = client.get(&pub_url).send().await.unwrap();
|
||||
assert!(resp.status().is_success());
|
||||
assert!(resp.headers().get("content-type").unwrap().to_str().unwrap().contains("application/vnd.pkg5.info"));
|
||||
assert!(
|
||||
resp.headers()
|
||||
.get("content-type")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains("application/vnd.pkg5.info")
|
||||
);
|
||||
let pub_json: serde_json::Value = resp.json().await.unwrap();
|
||||
assert_eq!(pub_json["version"], 1);
|
||||
assert_eq!(pub_json["publishers"][0]["name"], "test");
|
||||
|
|
@ -161,8 +176,8 @@ async fn test_depot_server() {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_ini_only_repo_serving_catalog() {
|
||||
use libips::repository::{WritableRepository, ReadableRepository};
|
||||
use libips::repository::BatchOptions;
|
||||
use libips::repository::{ReadableRepository, WritableRepository};
|
||||
use std::io::Write as _;
|
||||
|
||||
// Setup temp repo
|
||||
|
|
@ -190,18 +205,33 @@ async fn test_ini_only_repo_serving_catalog() {
|
|||
let mut manifest = Manifest::new();
|
||||
use libips::actions::Attr;
|
||||
use std::collections::HashMap;
|
||||
manifest.attributes.push(Attr { key: "pkg.fmri".to_string(), values: vec![format!("pkg://{}/example@1.0.0", publisher)], properties: HashMap::new() });
|
||||
manifest.attributes.push(Attr { key: "pkg.summary".to_string(), values: vec!["INI Repo Test Package".to_string()], properties: HashMap::new() });
|
||||
manifest.attributes.push(Attr {
|
||||
key: "pkg.fmri".to_string(),
|
||||
values: vec![format!("pkg://{}/example@1.0.0", publisher)],
|
||||
properties: HashMap::new(),
|
||||
});
|
||||
manifest.attributes.push(Attr {
|
||||
key: "pkg.summary".to_string(),
|
||||
values: vec!["INI Repo Test Package".to_string()],
|
||||
properties: HashMap::new(),
|
||||
});
|
||||
tx.update_manifest(manifest);
|
||||
tx.commit().unwrap();
|
||||
|
||||
// Rebuild catalog using batched API explicitly with small batch to exercise code path
|
||||
let opts = BatchOptions { batch_size: 1, flush_every_n: 1 };
|
||||
backend.rebuild_catalog_batched(publisher, true, opts).unwrap();
|
||||
let opts = BatchOptions {
|
||||
batch_size: 1,
|
||||
flush_every_n: 1,
|
||||
};
|
||||
backend
|
||||
.rebuild_catalog_batched(publisher, true, opts)
|
||||
.unwrap();
|
||||
|
||||
// Replace pkg6.repository with legacy pkg5.repository so FileBackend::open uses INI
|
||||
let pkg6_cfg = repo_path.join("pkg6.repository");
|
||||
if pkg6_cfg.exists() { fs::remove_file(&pkg6_cfg).unwrap(); }
|
||||
if pkg6_cfg.exists() {
|
||||
fs::remove_file(&pkg6_cfg).unwrap();
|
||||
}
|
||||
let mut ini = String::new();
|
||||
ini.push_str("[publisher]\n");
|
||||
ini.push_str(&format!("prefix = {}\n", publisher));
|
||||
|
|
@ -211,9 +241,23 @@ async fn test_ini_only_repo_serving_catalog() {
|
|||
|
||||
// Start depot server
|
||||
let config = Config {
|
||||
server: ServerConfig { bind: vec!["127.0.0.1:0".to_string()], workers: None, max_connections: None, reuseport: None, cache_max_age: Some(3600), tls_cert: None, tls_key: None },
|
||||
repository: RepositoryConfig { root: repo_path.clone(), mode: Some("readonly".to_string()) },
|
||||
telemetry: None, publishers: None, admin: None, oauth2: None,
|
||||
server: ServerConfig {
|
||||
bind: vec!["127.0.0.1:0".to_string()],
|
||||
workers: None,
|
||||
max_connections: None,
|
||||
reuseport: None,
|
||||
cache_max_age: Some(3600),
|
||||
tls_cert: None,
|
||||
tls_key: None,
|
||||
},
|
||||
repository: RepositoryConfig {
|
||||
root: repo_path.clone(),
|
||||
mode: Some("readonly".to_string()),
|
||||
},
|
||||
telemetry: None,
|
||||
publishers: None,
|
||||
admin: None,
|
||||
oauth2: None,
|
||||
};
|
||||
let repo = DepotRepo::new(&config).unwrap();
|
||||
let state = Arc::new(repo);
|
||||
|
|
@ -221,7 +265,9 @@ async fn test_ini_only_repo_serving_catalog() {
|
|||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
tokio::spawn(async move { http::server::run(router, listener).await.unwrap(); });
|
||||
tokio::spawn(async move {
|
||||
http::server::run(router, listener).await.unwrap();
|
||||
});
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let base_url = format!("http://{}", addr);
|
||||
|
|
@ -235,19 +281,48 @@ async fn test_ini_only_repo_serving_catalog() {
|
|||
assert!(body.contains("parts"));
|
||||
|
||||
// Also fetch individual catalog parts
|
||||
for part in ["catalog.base.C", "catalog.dependency.C", "catalog.summary.C"].iter() {
|
||||
for part in [
|
||||
"catalog.base.C",
|
||||
"catalog.dependency.C",
|
||||
"catalog.summary.C",
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
let url = format!("{}/{}/catalog/1/{}", base_url, publisher, part);
|
||||
let resp = client.get(&url).send().await.unwrap();
|
||||
assert!(resp.status().is_success(), "{} status: {:?}", part, resp.status());
|
||||
let ct = resp.headers().get("content-type").unwrap().to_str().unwrap().to_string();
|
||||
assert!(ct.contains("application/json"), "content-type for {} was {}", part, ct);
|
||||
assert!(
|
||||
resp.status().is_success(),
|
||||
"{} status: {:?}",
|
||||
part,
|
||||
resp.status()
|
||||
);
|
||||
let ct = resp
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(
|
||||
ct.contains("application/json"),
|
||||
"content-type for {} was {}",
|
||||
part,
|
||||
ct
|
||||
);
|
||||
let txt = resp.text().await.unwrap();
|
||||
assert!(!txt.is_empty(), "{} should not be empty", part);
|
||||
if *part == "catalog.base.C" {
|
||||
assert!(txt.contains(&publisher) && txt.contains("version"), "base part should contain publisher and version");
|
||||
assert!(
|
||||
txt.contains(&publisher) && txt.contains("version"),
|
||||
"base part should contain publisher and version"
|
||||
);
|
||||
} else {
|
||||
// dependency/summary may be empty for this test package; at least ensure signature is present
|
||||
assert!(txt.contains("_SIGNATURE"), "{} should contain a signature field", part);
|
||||
assert!(
|
||||
txt.contains("_SIGNATURE"),
|
||||
"{} should contain a signature field",
|
||||
part
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,8 +173,20 @@ mod e2e_tests {
|
|||
|
||||
// Check that the publisher was added
|
||||
assert!(repo_path.join("publisher").join("example.com").exists());
|
||||
assert!(repo_path.join("publisher").join("example.com").join("catalog").exists());
|
||||
assert!(repo_path.join("publisher").join("example.com").join("pkg").exists());
|
||||
assert!(
|
||||
repo_path
|
||||
.join("publisher")
|
||||
.join("example.com")
|
||||
.join("catalog")
|
||||
.exists()
|
||||
);
|
||||
assert!(
|
||||
repo_path
|
||||
.join("publisher")
|
||||
.join("example.com")
|
||||
.join("pkg")
|
||||
.exists()
|
||||
);
|
||||
|
||||
// Clean up
|
||||
cleanup_test_dir(&test_dir);
|
||||
|
|
@ -440,16 +452,21 @@ mod e2e_tests {
|
|||
);
|
||||
|
||||
let output = result.unwrap();
|
||||
let packages: serde_json::Value = serde_json::from_str(&output).expect("Failed to parse JSON output");
|
||||
let packages: serde_json::Value =
|
||||
serde_json::from_str(&output).expect("Failed to parse JSON output");
|
||||
|
||||
// The FMRI in the JSON is an object with scheme, publisher, name, and version fields
|
||||
// We need to extract these fields and construct the FMRI string
|
||||
let fmri_obj = &packages["packages"][0]["fmri"];
|
||||
let scheme = fmri_obj["scheme"].as_str().expect("Failed to get scheme");
|
||||
let publisher = fmri_obj["publisher"].as_str().expect("Failed to get publisher");
|
||||
let publisher = fmri_obj["publisher"]
|
||||
.as_str()
|
||||
.expect("Failed to get publisher");
|
||||
let name = fmri_obj["name"].as_str().expect("Failed to get name");
|
||||
let version_obj = &fmri_obj["version"];
|
||||
let release = version_obj["release"].as_str().expect("Failed to get release");
|
||||
let release = version_obj["release"]
|
||||
.as_str()
|
||||
.expect("Failed to get release");
|
||||
|
||||
// Construct the FMRI string in the format "pkg://publisher/name@version"
|
||||
let fmri = format!("{}://{}/{}", scheme, publisher, name);
|
||||
|
|
@ -466,7 +483,11 @@ mod e2e_tests {
|
|||
println!("Repo path: {}", repo_path.display());
|
||||
|
||||
// Check if the package exists in the repository
|
||||
let pkg_dir = repo_path.join("publisher").join("test").join("pkg").join("example");
|
||||
let pkg_dir = repo_path
|
||||
.join("publisher")
|
||||
.join("test")
|
||||
.join("pkg")
|
||||
.join("example");
|
||||
println!("Package directory: {}", pkg_dir.display());
|
||||
println!("Package directory exists: {}", pkg_dir.exists());
|
||||
|
||||
|
|
@ -482,11 +503,16 @@ mod e2e_tests {
|
|||
// Mark the package as obsoleted
|
||||
let result = run_pkg6repo(&[
|
||||
"obsolete-package",
|
||||
"-s", repo_path.to_str().unwrap(),
|
||||
"-p", "test",
|
||||
"-f", &fmri,
|
||||
"-m", "This package is obsoleted for testing purposes",
|
||||
"-r", "pkg://test/example2@1.0"
|
||||
"-s",
|
||||
repo_path.to_str().unwrap(),
|
||||
"-p",
|
||||
"test",
|
||||
"-f",
|
||||
&fmri,
|
||||
"-m",
|
||||
"This package is obsoleted for testing purposes",
|
||||
"-r",
|
||||
"pkg://test/example2@1.0",
|
||||
]);
|
||||
|
||||
// Print the result for debugging
|
||||
|
|
@ -513,7 +539,13 @@ mod e2e_tests {
|
|||
);
|
||||
|
||||
// List obsoleted packages
|
||||
let result = run_pkg6repo(&["list-obsoleted", "-s", repo_path.to_str().unwrap(), "-p", "test"]);
|
||||
let result = run_pkg6repo(&[
|
||||
"list-obsoleted",
|
||||
"-s",
|
||||
repo_path.to_str().unwrap(),
|
||||
"-p",
|
||||
"test",
|
||||
]);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to list obsoleted packages: {:?}",
|
||||
|
|
@ -529,9 +561,12 @@ mod e2e_tests {
|
|||
// Show details of the obsoleted package
|
||||
let result = run_pkg6repo(&[
|
||||
"show-obsoleted",
|
||||
"-s", repo_path.to_str().unwrap(),
|
||||
"-p", "test",
|
||||
"-f", &fmri
|
||||
"-s",
|
||||
repo_path.to_str().unwrap(),
|
||||
"-p",
|
||||
"test",
|
||||
"-f",
|
||||
&fmri,
|
||||
]);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
|
|
|
|||
|
|
@ -1273,7 +1273,7 @@ fn main() -> Result<()> {
|
|||
|
||||
info!("Repository imported successfully");
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
|
||||
Commands::ObsoletePackage {
|
||||
repo_uri_or_path,
|
||||
|
|
@ -1295,7 +1295,7 @@ fn main() -> Result<()> {
|
|||
&repo.path,
|
||||
publisher,
|
||||
parsed_fmri.stem(),
|
||||
&parsed_fmri.version()
|
||||
&parsed_fmri.version(),
|
||||
);
|
||||
|
||||
println!("Looking for manifest at: {}", manifest_path.display());
|
||||
|
|
@ -1336,7 +1336,7 @@ fn main() -> Result<()> {
|
|||
|
||||
info!("Package marked as obsoleted successfully: {}", parsed_fmri);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
|
||||
Commands::ListObsoleted {
|
||||
repo_uri_or_path,
|
||||
|
|
@ -1357,7 +1357,11 @@ fn main() -> Result<()> {
|
|||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||
|
||||
// List obsoleted packages with pagination
|
||||
obsoleted_manager.list_obsoleted_packages_paginated(publisher, page.clone(), page_size.clone())?
|
||||
obsoleted_manager.list_obsoleted_packages_paginated(
|
||||
publisher,
|
||||
page.clone(),
|
||||
page_size.clone(),
|
||||
)?
|
||||
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
||||
|
||||
// Determine the output format
|
||||
|
|
@ -1389,11 +1393,13 @@ fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
// Print pagination information
|
||||
println!("\nPage {} of {} (Total: {} packages)",
|
||||
println!(
|
||||
"\nPage {} of {} (Total: {} packages)",
|
||||
paginated_result.page,
|
||||
paginated_result.total_pages,
|
||||
paginated_result.total_count);
|
||||
},
|
||||
paginated_result.total_count
|
||||
);
|
||||
}
|
||||
"json" => {
|
||||
// Create a JSON representation of the obsoleted packages with pagination info
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -1405,7 +1411,11 @@ fn main() -> Result<()> {
|
|||
total_count: usize,
|
||||
}
|
||||
|
||||
let packages_str: Vec<String> = paginated_result.packages.iter().map(|f| f.to_string()).collect();
|
||||
let packages_str: Vec<String> = paginated_result
|
||||
.packages
|
||||
.iter()
|
||||
.map(|f| f.to_string())
|
||||
.collect();
|
||||
let paginated_output = PaginatedOutput {
|
||||
packages: packages_str,
|
||||
page: paginated_result.page,
|
||||
|
|
@ -1419,7 +1429,7 @@ fn main() -> Result<()> {
|
|||
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
||||
|
||||
println!("{}", json_output);
|
||||
},
|
||||
}
|
||||
"tsv" => {
|
||||
// Print headers if not omitted
|
||||
if !omit_headers {
|
||||
|
|
@ -1436,20 +1446,17 @@ fn main() -> Result<()> {
|
|||
None => String::new(),
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}\t{}\t{}",
|
||||
fmri.stem(),
|
||||
version_str,
|
||||
publisher_str
|
||||
);
|
||||
println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str);
|
||||
}
|
||||
|
||||
// Print pagination information
|
||||
println!("\nPAGE\t{}\nTOTAL_PAGES\t{}\nTOTAL_COUNT\t{}",
|
||||
println!(
|
||||
"\nPAGE\t{}\nTOTAL_PAGES\t{}\nTOTAL_COUNT\t{}",
|
||||
paginated_result.page,
|
||||
paginated_result.total_pages,
|
||||
paginated_result.total_count);
|
||||
},
|
||||
paginated_result.total_count
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
||||
output_format.to_string(),
|
||||
|
|
@ -1458,7 +1465,7 @@ fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
|
||||
Commands::ShowObsoleted {
|
||||
repo_uri_or_path,
|
||||
|
|
@ -1513,7 +1520,7 @@ fn main() -> Result<()> {
|
|||
|
||||
println!("Metadata Version: {}", metadata.metadata_version);
|
||||
println!("Content Hash: {}", metadata.content_hash);
|
||||
},
|
||||
}
|
||||
"json" => {
|
||||
// Create a JSON representation of the obsoleted package details
|
||||
let details_output = ObsoletedPackageDetailsOutput {
|
||||
|
|
@ -1531,7 +1538,7 @@ fn main() -> Result<()> {
|
|||
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
||||
|
||||
println!("{}", json_output);
|
||||
},
|
||||
}
|
||||
"tsv" => {
|
||||
println!("FMRI\t{}", metadata.fmri);
|
||||
println!("Status\t{}", metadata.status);
|
||||
|
|
@ -1549,7 +1556,7 @@ fn main() -> Result<()> {
|
|||
|
||||
println!("MetadataVersion\t{}", metadata.metadata_version);
|
||||
println!("ContentHash\t{}", metadata.content_hash);
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
||||
output_format.to_string(),
|
||||
|
|
@ -1558,7 +1565,7 @@ fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
|
||||
Commands::SearchObsoleted {
|
||||
repo_uri_or_path,
|
||||
|
|
@ -1568,7 +1575,10 @@ fn main() -> Result<()> {
|
|||
pattern,
|
||||
limit,
|
||||
} => {
|
||||
info!("Searching for obsoleted packages: {} (publisher: {})", pattern, publisher);
|
||||
info!(
|
||||
"Searching for obsoleted packages: {} (publisher: {})",
|
||||
pattern, publisher
|
||||
);
|
||||
|
||||
// Open the repository
|
||||
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
||||
|
|
@ -1579,7 +1589,8 @@ fn main() -> Result<()> {
|
|||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||
|
||||
// Search for obsoleted packages
|
||||
let mut packages = obsoleted_manager.search_obsoleted_packages(publisher, pattern)?;
|
||||
let mut packages =
|
||||
obsoleted_manager.search_obsoleted_packages(publisher, pattern)?;
|
||||
|
||||
// Apply limit if specified
|
||||
if let Some(max_results) = limit {
|
||||
|
|
@ -1616,10 +1627,11 @@ fn main() -> Result<()> {
|
|||
publisher_str
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
"json" => {
|
||||
// Create a JSON representation of the obsoleted packages
|
||||
let packages_str: Vec<String> = obsoleted_packages.iter().map(|f| f.to_string()).collect();
|
||||
let packages_str: Vec<String> =
|
||||
obsoleted_packages.iter().map(|f| f.to_string()).collect();
|
||||
let packages_output = ObsoletedPackagesOutput {
|
||||
packages: packages_str,
|
||||
};
|
||||
|
|
@ -1629,7 +1641,7 @@ fn main() -> Result<()> {
|
|||
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
||||
|
||||
println!("{}", json_output);
|
||||
},
|
||||
}
|
||||
"tsv" => {
|
||||
// Print headers if not omitted
|
||||
if !omit_headers {
|
||||
|
|
@ -1646,14 +1658,9 @@ fn main() -> Result<()> {
|
|||
None => String::new(),
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}\t{}\t{}",
|
||||
fmri.stem(),
|
||||
version_str,
|
||||
publisher_str
|
||||
);
|
||||
println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str);
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
||||
output_format.to_string(),
|
||||
|
|
@ -1662,7 +1669,7 @@ fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
|
||||
Commands::RestoreObsoleted {
|
||||
repo_uri_or_path,
|
||||
|
|
@ -1670,7 +1677,10 @@ fn main() -> Result<()> {
|
|||
fmri,
|
||||
no_rebuild,
|
||||
} => {
|
||||
info!("Restoring obsoleted package: {} (publisher: {})", fmri, publisher);
|
||||
info!(
|
||||
"Restoring obsoleted package: {} (publisher: {})",
|
||||
fmri, publisher
|
||||
);
|
||||
|
||||
// Parse the FMRI
|
||||
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
|
||||
|
|
@ -1710,7 +1720,7 @@ fn main() -> Result<()> {
|
|||
|
||||
info!("Package restored successfully: {}", parsed_fmri);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
|
||||
Commands::ExportObsoleted {
|
||||
repo_uri_or_path,
|
||||
|
|
@ -1739,7 +1749,7 @@ fn main() -> Result<()> {
|
|||
|
||||
info!("Exported {} obsoleted packages to {}", count, output_file);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
|
||||
Commands::ImportObsoleted {
|
||||
repo_uri_or_path,
|
||||
|
|
@ -1758,15 +1768,12 @@ fn main() -> Result<()> {
|
|||
|
||||
// Import the obsoleted packages
|
||||
let input_path = PathBuf::from(input_file);
|
||||
obsoleted_manager.import_obsoleted_packages(
|
||||
&input_path,
|
||||
publisher.as_deref(),
|
||||
)?
|
||||
obsoleted_manager.import_obsoleted_packages(&input_path, publisher.as_deref())?
|
||||
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
||||
|
||||
info!("Imported {} obsoleted packages", count);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
|
||||
Commands::CleanupObsoleted {
|
||||
repo_uri_or_path,
|
||||
|
|
@ -1775,11 +1782,15 @@ fn main() -> Result<()> {
|
|||
dry_run,
|
||||
} => {
|
||||
if *dry_run {
|
||||
info!("Dry run: Cleaning up obsoleted packages older than {} days for publisher: {}",
|
||||
ttl_days, publisher);
|
||||
info!(
|
||||
"Dry run: Cleaning up obsoleted packages older than {} days for publisher: {}",
|
||||
ttl_days, publisher
|
||||
);
|
||||
} else {
|
||||
info!("Cleaning up obsoleted packages older than {} days for publisher: {}",
|
||||
ttl_days, publisher);
|
||||
info!(
|
||||
"Cleaning up obsoleted packages older than {} days for publisher: {}",
|
||||
ttl_days, publisher
|
||||
);
|
||||
}
|
||||
|
||||
// Open the repository
|
||||
|
|
@ -1791,11 +1802,8 @@ fn main() -> Result<()> {
|
|||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||
|
||||
// Clean up the obsoleted packages
|
||||
obsoleted_manager.cleanup_obsoleted_packages_older_than_ttl(
|
||||
publisher,
|
||||
*ttl_days,
|
||||
*dry_run,
|
||||
)?
|
||||
obsoleted_manager
|
||||
.cleanup_obsoleted_packages_older_than_ttl(publisher, *ttl_days, *dry_run)?
|
||||
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
||||
|
||||
if *dry_run {
|
||||
|
|
|
|||
|
|
@ -222,7 +222,8 @@ impl Pkg5Importer {
|
|||
}
|
||||
|
||||
// Import packages and get counts
|
||||
let (regular_count, obsoleted_count) = self.import_packages(&source_path, &mut dest_repo, publisher_to_import)?;
|
||||
let (regular_count, obsoleted_count) =
|
||||
self.import_packages(&source_path, &mut dest_repo, publisher_to_import)?;
|
||||
let total_count = regular_count + obsoleted_count;
|
||||
|
||||
// Rebuild catalog and search index
|
||||
|
|
@ -349,8 +350,10 @@ impl Pkg5Importer {
|
|||
}
|
||||
|
||||
let total_package_count = regular_package_count + obsoleted_package_count;
|
||||
info!("Imported {} packages ({} regular, {} obsoleted)",
|
||||
total_package_count, regular_package_count, obsoleted_package_count);
|
||||
info!(
|
||||
"Imported {} packages ({} regular, {} obsoleted)",
|
||||
total_package_count, regular_package_count, obsoleted_package_count
|
||||
);
|
||||
Ok((regular_package_count, obsoleted_package_count))
|
||||
}
|
||||
|
||||
|
|
@ -439,8 +442,8 @@ impl Pkg5Importer {
|
|||
publisher,
|
||||
&fmri,
|
||||
&manifest_content,
|
||||
None, // No obsoleted_by information available
|
||||
None, // No deprecation message available
|
||||
None, // No obsoleted_by information available
|
||||
None, // No deprecation message available
|
||||
false, // Don't store the original manifest, use null hash instead
|
||||
)?;
|
||||
|
||||
|
|
@ -462,7 +465,8 @@ impl Pkg5Importer {
|
|||
// Debug the repository structure
|
||||
debug!(
|
||||
"Publisher directory: {}",
|
||||
libips::repository::FileBackend::construct_package_dir(&dest_repo.path, publisher, "").display()
|
||||
libips::repository::FileBackend::construct_package_dir(&dest_repo.path, publisher, "")
|
||||
.display()
|
||||
);
|
||||
|
||||
// Extract files referenced in the manifest
|
||||
|
|
|
|||
|
|
@ -82,15 +82,18 @@ impl std::fmt::Display for OutputFormat {
|
|||
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
#[error("pkgtree error: {message}")]
|
||||
#[diagnostic(code(ips::pkgtree_error), help("See logs with RUST_LOG=pkgtree=debug for more details."))]
|
||||
#[diagnostic(
|
||||
code(ips::pkgtree_error),
|
||||
help("See logs with RUST_LOG=pkgtree=debug for more details.")
|
||||
)]
|
||||
struct PkgTreeError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Edge {
|
||||
to: String, // target stem
|
||||
dep_type: String, // dependency type (e.g., require, incorporate, optional, etc.)
|
||||
to: String, // target stem
|
||||
dep_type: String, // dependency type (e.g., require, incorporate, optional, etc.)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
@ -101,7 +104,10 @@ struct Graph {
|
|||
|
||||
impl Graph {
|
||||
fn add_edge(&mut self, from: String, to: String, dep_type: String) {
|
||||
self.adj.entry(from).or_default().push(Edge { to, dep_type });
|
||||
self.adj
|
||||
.entry(from)
|
||||
.or_default()
|
||||
.push(Edge { to, dep_type });
|
||||
}
|
||||
|
||||
fn stems(&self) -> impl Iterator<Item = &String> {
|
||||
|
|
@ -127,8 +133,9 @@ fn main() -> Result<()> {
|
|||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
// Load image
|
||||
let image = Image::load(&cli.image_path)
|
||||
.map_err(|e| PkgTreeError { message: format!("Failed to load image at {:?}: {}", cli.image_path, e) })?;
|
||||
let image = Image::load(&cli.image_path).map_err(|e| PkgTreeError {
|
||||
message: format!("Failed to load image at {:?}: {}", cli.image_path, e),
|
||||
})?;
|
||||
|
||||
// Targeted analysis of solver error file has top priority if provided
|
||||
if let Some(err_path) = &cli.solver_error_file {
|
||||
|
|
@ -145,16 +152,27 @@ fn main() -> Result<()> {
|
|||
|
||||
// Dangling dependency scan has priority over graph mode
|
||||
if cli.find_dangling {
|
||||
run_dangling_scan(&image, cli.publisher.as_deref(), cli.package.as_deref(), cli.format)?;
|
||||
run_dangling_scan(
|
||||
&image,
|
||||
cli.publisher.as_deref(),
|
||||
cli.package.as_deref(),
|
||||
cli.format,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Graph mode
|
||||
// Query catalog (filtered if --package provided)
|
||||
let mut pkgs = if let Some(ref needle) = cli.package {
|
||||
image.query_catalog(Some(needle.as_str())).map_err(|e| PkgTreeError { message: format!("Failed to query catalog: {}", e) })?
|
||||
image
|
||||
.query_catalog(Some(needle.as_str()))
|
||||
.map_err(|e| PkgTreeError {
|
||||
message: format!("Failed to query catalog: {}", e),
|
||||
})?
|
||||
} else {
|
||||
image.query_catalog(None).map_err(|e| PkgTreeError { message: format!("Failed to query catalog: {}", e) })?
|
||||
image.query_catalog(None).map_err(|e| PkgTreeError {
|
||||
message: format!("Failed to query catalog: {}", e),
|
||||
})?
|
||||
};
|
||||
|
||||
// Filter by publisher if specified
|
||||
|
|
@ -203,13 +221,16 @@ fn main() -> Result<()> {
|
|||
|
||||
// If no nodes were added (e.g., filter too narrow), try building graph for all packages to support cycle analysis
|
||||
if graph.adj.is_empty() && filter_substr.is_some() {
|
||||
info!("No packages matched filter for dependency graph; analyzing full catalog for cycles/tree context.");
|
||||
info!(
|
||||
"No packages matched filter for dependency graph; analyzing full catalog for cycles/tree context."
|
||||
);
|
||||
for p in &pkgs {
|
||||
match image.get_manifest_from_catalog(&p.fmri) {
|
||||
Ok(Some(manifest)) => {
|
||||
let from_stem = p.fmri.stem().to_string();
|
||||
for dep in manifest.dependencies {
|
||||
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" {
|
||||
if dep.dependency_type != "require" && dep.dependency_type != "incorporate"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(dep_fmri) = dep.fmri {
|
||||
|
|
@ -227,7 +248,9 @@ fn main() -> Result<()> {
|
|||
let roots: Vec<String> = if let Some(ref needle) = filter_substr {
|
||||
let mut r = HashSet::new();
|
||||
for k in graph.adj.keys() {
|
||||
if k.contains(needle) { r.insert(k.clone()); }
|
||||
if k.contains(needle) {
|
||||
r.insert(k.clone());
|
||||
}
|
||||
}
|
||||
r.into_iter().collect()
|
||||
} else {
|
||||
|
|
@ -253,19 +276,47 @@ fn main() -> Result<()> {
|
|||
OutputFormat::Json => {
|
||||
use serde::Serialize;
|
||||
#[derive(Serialize)]
|
||||
struct JsonEdge { from: String, to: String, dep_type: String }
|
||||
struct JsonEdge {
|
||||
from: String,
|
||||
to: String,
|
||||
dep_type: String,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct JsonCycle { nodes: Vec<String>, edges: Vec<String> }
|
||||
struct JsonCycle {
|
||||
nodes: Vec<String>,
|
||||
edges: Vec<String>,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct Payload { edges: Vec<JsonEdge>, cycles: Vec<JsonCycle> }
|
||||
struct Payload {
|
||||
edges: Vec<JsonEdge>,
|
||||
cycles: Vec<JsonCycle>,
|
||||
}
|
||||
|
||||
let mut edges = Vec::new();
|
||||
for (from, es) in &graph.adj {
|
||||
for e in es { edges.push(JsonEdge{ from: from.clone(), to: e.to.clone(), dep_type: e.dep_type.clone() }); }
|
||||
for e in es {
|
||||
edges.push(JsonEdge {
|
||||
from: from.clone(),
|
||||
to: e.to.clone(),
|
||||
dep_type: e.dep_type.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let cycles_json = cycles.iter().map(|c| JsonCycle { nodes: c.nodes.clone(), edges: c.edges.clone() }).collect();
|
||||
let payload = Payload { edges, cycles: cycles_json };
|
||||
println!("{}", serde_json::to_string_pretty(&payload).into_diagnostic()?);
|
||||
let cycles_json = cycles
|
||||
.iter()
|
||||
.map(|c| JsonCycle {
|
||||
nodes: c.nodes.clone(),
|
||||
edges: c.edges.clone(),
|
||||
})
|
||||
.collect();
|
||||
let payload = Payload {
|
||||
edges,
|
||||
cycles: cycles_json,
|
||||
};
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&payload).into_diagnostic()?
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -285,7 +336,7 @@ struct AdviceIssue {
|
|||
path: Vec<String>, // path from root to the missing dependency stem
|
||||
stem: String, // the missing stem
|
||||
constraint: DepConstraint,
|
||||
details: String, // human description
|
||||
details: String, // human description
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -296,25 +347,42 @@ struct AdviceContext {
|
|||
catalog_cache: HashMap<String, Vec<(String, libips::fmri::Fmri)>>, // stem -> [(publisher, fmri)]
|
||||
manifest_cache: HashMap<String, libips::actions::Manifest>, // fmri string -> manifest
|
||||
lock_cache: HashMap<String, Option<String>>, // stem -> release lock
|
||||
candidate_cache: HashMap<(String, Option<String>, Option<String>, Option<String>), Option<libips::fmri::Fmri>>, // (stem, rel, branch, publisher)
|
||||
candidate_cache: HashMap<
|
||||
(String, Option<String>, Option<String>, Option<String>),
|
||||
Option<libips::fmri::Fmri>,
|
||||
>, // (stem, rel, branch, publisher)
|
||||
}
|
||||
|
||||
impl AdviceContext {
|
||||
fn new(publisher: Option<String>, advice_cap: usize) -> Self {
|
||||
AdviceContext { publisher, advice_cap, ..Default::default() }
|
||||
AdviceContext {
|
||||
publisher,
|
||||
advice_cap,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_advisor(image: &Image, ctx: &mut AdviceContext, root_stem: &str, max_depth: usize) -> Result<()> {
|
||||
fn run_advisor(
|
||||
image: &Image,
|
||||
ctx: &mut AdviceContext,
|
||||
root_stem: &str,
|
||||
max_depth: usize,
|
||||
) -> Result<()> {
|
||||
info!("Advisor analyzing installability for root: {}", root_stem);
|
||||
|
||||
// Find best candidate for root
|
||||
let root_fmri = match find_best_candidate(image, ctx, root_stem, None, None) {
|
||||
Ok(Some(fmri)) => fmri,
|
||||
Ok(None) => {
|
||||
println!("No candidates found for root package '{}'.\n- Suggestion: run 'pkg6 refresh' to update catalogs.\n- Ensure publisher{} contains the package.",
|
||||
root_stem,
|
||||
ctx.publisher.as_ref().map(|p| format!(" '{}')", p)).unwrap_or_else(|| "".to_string()));
|
||||
println!(
|
||||
"No candidates found for root package '{}'.\n- Suggestion: run 'pkg6 refresh' to update catalogs.\n- Ensure publisher{} contains the package.",
|
||||
root_stem,
|
||||
ctx.publisher
|
||||
.as_ref()
|
||||
.map(|p| format!(" '{}')", p))
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
|
|
@ -326,36 +394,81 @@ fn run_advisor(image: &Image, ctx: &mut AdviceContext, root_stem: &str, max_dept
|
|||
let mut issues: Vec<AdviceIssue> = Vec::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut path: Vec<String> = vec![root_stem.to_string()];
|
||||
advise_recursive(image, ctx, &root_fmri, &mut path, 1, max_depth, &mut seen, &mut issues)?;
|
||||
advise_recursive(
|
||||
image,
|
||||
ctx,
|
||||
&root_fmri,
|
||||
&mut path,
|
||||
1,
|
||||
max_depth,
|
||||
&mut seen,
|
||||
&mut issues,
|
||||
)?;
|
||||
|
||||
// Print summary
|
||||
if issues.is_empty() {
|
||||
println!("No immediate missing dependencies detected up to depth {} for root '{}'.\nIf installs still fail, try running with higher --advice-depth or check solver logs.", max_depth, root_stem);
|
||||
println!(
|
||||
"No immediate missing dependencies detected up to depth {} for root '{}'.\nIf installs still fail, try running with higher --advice-depth or check solver logs.",
|
||||
max_depth, root_stem
|
||||
);
|
||||
} else {
|
||||
println!("Found {} installability issue(s):", issues.len());
|
||||
for (i, iss) in issues.iter().enumerate() {
|
||||
let constraint_str = format!(
|
||||
"{}{}",
|
||||
iss.constraint.release.as_ref().map(|r| format!("release={} ", r)).unwrap_or_default(),
|
||||
iss.constraint.branch.as_ref().map(|b| format!("branch={}", b)).unwrap_or_default(),
|
||||
).trim().to_string();
|
||||
println!(" {}. {}\n - Path: {}\n - Constraint: {}\n - Details: {}",
|
||||
iss.constraint
|
||||
.release
|
||||
.as_ref()
|
||||
.map(|r| format!("release={} ", r))
|
||||
.unwrap_or_default(),
|
||||
iss.constraint
|
||||
.branch
|
||||
.as_ref()
|
||||
.map(|b| format!("branch={}", b))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.trim()
|
||||
.to_string();
|
||||
println!(
|
||||
" {}. {}\n - Path: {}\n - Constraint: {}\n - Details: {}",
|
||||
i + 1,
|
||||
format!("No viable candidates for '{}'", iss.stem),
|
||||
iss.path.join(" -> "),
|
||||
if constraint_str.is_empty() { "<none>".to_string() } else { constraint_str },
|
||||
if constraint_str.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
constraint_str
|
||||
},
|
||||
iss.details,
|
||||
);
|
||||
|
||||
// Suggestions
|
||||
println!(" - Suggestions:");
|
||||
println!(" • Add or publish a matching package for '{}'{}{}.",
|
||||
println!(
|
||||
" • Add or publish a matching package for '{}'{}{}.",
|
||||
iss.stem,
|
||||
iss.constraint.release.as_ref().map(|r| format!(" (release={})", r)).unwrap_or_default(),
|
||||
iss.constraint.branch.as_ref().map(|b| format!(" (branch={})", b)).unwrap_or_default());
|
||||
println!(" • Alternatively, relax the dependency constraint in the requiring package to match available releases.");
|
||||
if let Some(lock) = get_incorporated_release_cached(image, ctx, &iss.stem).ok().flatten() {
|
||||
println!(" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", iss.stem, lock);
|
||||
iss.constraint
|
||||
.release
|
||||
.as_ref()
|
||||
.map(|r| format!(" (release={})", r))
|
||||
.unwrap_or_default(),
|
||||
iss.constraint
|
||||
.branch
|
||||
.as_ref()
|
||||
.map(|b| format!(" (branch={})", b))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
println!(
|
||||
" • Alternatively, relax the dependency constraint in the requiring package to match available releases."
|
||||
);
|
||||
if let Some(lock) = get_incorporated_release_cached(image, ctx, &iss.stem)
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
println!(
|
||||
" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.",
|
||||
iss.stem, lock
|
||||
);
|
||||
}
|
||||
println!(" • Ensure catalogs are up to date: 'pkg6 refresh'.");
|
||||
}
|
||||
|
|
@ -374,7 +487,9 @@ fn advise_recursive(
|
|||
seen: &mut HashSet<String>,
|
||||
issues: &mut Vec<AdviceIssue>,
|
||||
) -> Result<()> {
|
||||
if max_depth != 0 && depth > max_depth { return Ok(()); }
|
||||
if max_depth != 0 && depth > max_depth {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Load manifest of the current FMRI (cached)
|
||||
let manifest = get_manifest_cached(image, ctx, fmri)?;
|
||||
|
|
@ -383,30 +498,61 @@ fn advise_recursive(
|
|||
let mut constrained = Vec::new();
|
||||
let mut unconstrained = Vec::new();
|
||||
for dep in manifest.dependencies {
|
||||
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { continue; }
|
||||
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" {
|
||||
continue;
|
||||
}
|
||||
let has_fmri = dep.fmri.is_some();
|
||||
if !has_fmri { continue; }
|
||||
if !has_fmri {
|
||||
continue;
|
||||
}
|
||||
let c = extract_constraint(&dep.optional);
|
||||
if c.release.is_some() || c.branch.is_some() { constrained.push((dep, c)); } else { unconstrained.push((dep, c)); }
|
||||
if c.release.is_some() || c.branch.is_some() {
|
||||
constrained.push((dep, c));
|
||||
} else {
|
||||
unconstrained.push((dep, c));
|
||||
}
|
||||
}
|
||||
for (dep, constraint) in constrained.into_iter().chain(unconstrained.into_iter()) {
|
||||
if ctx.advice_cap != 0 && processed >= ctx.advice_cap {
|
||||
debug!("Dependency processing for {} truncated at cap {}", fmri.stem(), ctx.advice_cap);
|
||||
debug!(
|
||||
"Dependency processing for {} truncated at cap {}",
|
||||
fmri.stem(),
|
||||
ctx.advice_cap
|
||||
);
|
||||
break;
|
||||
}
|
||||
processed += 1;
|
||||
|
||||
let dep_stem = dep.fmri.unwrap().stem().to_string();
|
||||
|
||||
debug!("Checking dependency to '{}' with constraint {:?}", dep_stem, (&constraint.release, &constraint.branch));
|
||||
debug!(
|
||||
"Checking dependency to '{}' with constraint {:?}",
|
||||
dep_stem,
|
||||
(&constraint.release, &constraint.branch)
|
||||
);
|
||||
|
||||
match find_best_candidate(image, ctx, &dep_stem, constraint.release.as_deref(), constraint.branch.as_deref())? {
|
||||
match find_best_candidate(
|
||||
image,
|
||||
ctx,
|
||||
&dep_stem,
|
||||
constraint.release.as_deref(),
|
||||
constraint.branch.as_deref(),
|
||||
)? {
|
||||
Some(next_fmri) => {
|
||||
// Continue recursion if not seen and depth allows
|
||||
if !seen.contains(&dep_stem) {
|
||||
seen.insert(dep_stem.clone());
|
||||
path.push(dep_stem.clone());
|
||||
advise_recursive(image, ctx, &next_fmri, path, depth + 1, max_depth, seen, issues)?;
|
||||
advise_recursive(
|
||||
image,
|
||||
ctx,
|
||||
&next_fmri,
|
||||
path,
|
||||
depth + 1,
|
||||
max_depth,
|
||||
seen,
|
||||
issues,
|
||||
)?;
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
|
@ -438,15 +584,28 @@ fn extract_constraint(optional: &[libips::actions::Property]) -> DepConstraint {
|
|||
DepConstraint { release, branch }
|
||||
}
|
||||
|
||||
fn build_missing_detail(image: &Image, ctx: &mut AdviceContext, stem: &str, constraint: &DepConstraint) -> String {
|
||||
fn build_missing_detail(
|
||||
image: &Image,
|
||||
ctx: &mut AdviceContext,
|
||||
stem: &str,
|
||||
constraint: &DepConstraint,
|
||||
) -> String {
|
||||
// List available releases/branches for informational purposes
|
||||
let mut available: Vec<String> = Vec::new();
|
||||
if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) {
|
||||
for (pubname, fmri) in list {
|
||||
if let Some(ref pfilter) = ctx.publisher { if &pubname != pfilter { continue; } }
|
||||
if fmri.stem() != stem { continue; }
|
||||
if let Some(ref pfilter) = ctx.publisher {
|
||||
if &pubname != pfilter {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if fmri.stem() != stem {
|
||||
continue;
|
||||
}
|
||||
let ver = fmri.version();
|
||||
if ver.is_empty() { continue; }
|
||||
if ver.is_empty() {
|
||||
continue;
|
||||
}
|
||||
available.push(ver);
|
||||
}
|
||||
}
|
||||
|
|
@ -460,17 +619,43 @@ fn build_missing_detail(image: &Image, ctx: &mut AdviceContext, stem: &str, cons
|
|||
available.join(", ")
|
||||
};
|
||||
|
||||
let lock = get_incorporated_release_cached(image, ctx, stem).ok().flatten();
|
||||
let lock = get_incorporated_release_cached(image, ctx, stem)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
match (&constraint.release, &constraint.branch, lock) {
|
||||
(Some(r), Some(b), Some(lr)) => format!("Required release={}, branch={} not found. Incorporation lock release={} may also constrain candidates. Available versions: {}", r, b, lr, available_str),
|
||||
(Some(r), Some(b), None) => format!("Required release={}, branch={} not found. Available versions: {}", r, b, available_str),
|
||||
(Some(r), None, Some(lr)) => format!("Required release={} not found. Incorporation lock release={} present. Available versions: {}", r, lr, available_str),
|
||||
(Some(r), None, None) => format!("Required release={} not found. Available versions: {}", r, available_str),
|
||||
(None, Some(b), Some(lr)) => format!("Required branch={} not found. Incorporation lock release={} present. Available versions: {}", b, lr, available_str),
|
||||
(None, Some(b), None) => format!("Required branch={} not found. Available versions: {}", b, available_str),
|
||||
(None, None, Some(lr)) => format!("No candidates matched. Incorporation lock release={} present. Available versions: {}", lr, available_str),
|
||||
(None, None, None) => format!("No candidates matched. Available versions: {}", available_str),
|
||||
(Some(r), Some(b), Some(lr)) => format!(
|
||||
"Required release={}, branch={} not found. Incorporation lock release={} may also constrain candidates. Available versions: {}",
|
||||
r, b, lr, available_str
|
||||
),
|
||||
(Some(r), Some(b), None) => format!(
|
||||
"Required release={}, branch={} not found. Available versions: {}",
|
||||
r, b, available_str
|
||||
),
|
||||
(Some(r), None, Some(lr)) => format!(
|
||||
"Required release={} not found. Incorporation lock release={} present. Available versions: {}",
|
||||
r, lr, available_str
|
||||
),
|
||||
(Some(r), None, None) => format!(
|
||||
"Required release={} not found. Available versions: {}",
|
||||
r, available_str
|
||||
),
|
||||
(None, Some(b), Some(lr)) => format!(
|
||||
"Required branch={} not found. Incorporation lock release={} present. Available versions: {}",
|
||||
b, lr, available_str
|
||||
),
|
||||
(None, Some(b), None) => format!(
|
||||
"Required branch={} not found. Available versions: {}",
|
||||
b, available_str
|
||||
),
|
||||
(None, None, Some(lr)) => format!(
|
||||
"No candidates matched. Incorporation lock release={} present. Available versions: {}",
|
||||
lr, available_str
|
||||
),
|
||||
(None, None, None) => format!(
|
||||
"No candidates matched. Available versions: {}",
|
||||
available_str
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -494,25 +679,47 @@ fn find_best_candidate(
|
|||
let mut candidates: Vec<(String, libips::fmri::Fmri)> = Vec::new();
|
||||
|
||||
// Prefer matching release from incorporation lock, unless explicit req_release provided
|
||||
let lock_release = if req_release.is_none() { get_incorporated_release_cached(image, ctx, stem).ok().flatten() } else { None };
|
||||
let lock_release = if req_release.is_none() {
|
||||
get_incorporated_release_cached(image, ctx, stem)
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? {
|
||||
if let Some(ref pfilter) = ctx.publisher { if &pubf != pfilter { continue; } }
|
||||
if pfmri.stem() != stem { continue; }
|
||||
if let Some(ref pfilter) = ctx.publisher {
|
||||
if &pubf != pfilter {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if pfmri.stem() != stem {
|
||||
continue;
|
||||
}
|
||||
let ver = pfmri.version();
|
||||
if ver.is_empty() { continue; }
|
||||
if ver.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse version string to extract release and branch heuristically: release,branch-rest
|
||||
let rel = version_release(&ver);
|
||||
let br = version_branch(&ver);
|
||||
|
||||
if let Some(req_r) = req_release {
|
||||
if Some(req_r) != rel.as_deref() { continue; }
|
||||
if Some(req_r) != rel.as_deref() {
|
||||
continue;
|
||||
}
|
||||
} else if let Some(lock_r) = lock_release.as_deref() {
|
||||
if Some(lock_r) != rel.as_deref() { continue; }
|
||||
if Some(lock_r) != rel.as_deref() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(req_b) = req_branch { if Some(req_b) != br.as_deref() { continue; } }
|
||||
if let Some(req_b) = req_branch {
|
||||
if Some(req_b) != br.as_deref() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
candidates.push((ver.clone(), pfmri.clone()));
|
||||
}
|
||||
|
|
@ -550,7 +757,10 @@ fn query_catalog_cached(
|
|||
// We don't have mutable borrow on ctx here; clone and return, caller will populate cache through a mutable wrapper.
|
||||
// To keep code simple, provide a small wrapper that fills the cache when needed.
|
||||
// We'll implement a separate function that has mutable ctx.
|
||||
let mut tmp_ctx = AdviceContext { catalog_cache: ctx.catalog_cache.clone(), ..Default::default() };
|
||||
let mut tmp_ctx = AdviceContext {
|
||||
catalog_cache: ctx.catalog_cache.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
query_catalog_cached_mut(image, &mut tmp_ctx, stem)
|
||||
}
|
||||
|
||||
|
|
@ -563,10 +773,9 @@ fn query_catalog_cached_mut(
|
|||
return Ok(v.clone());
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for p in image
|
||||
.query_catalog(Some(stem))
|
||||
.map_err(|e| PkgTreeError { message: format!("Failed to query catalog for {}: {}", stem, e) })?
|
||||
{
|
||||
for p in image.query_catalog(Some(stem)).map_err(|e| PkgTreeError {
|
||||
message: format!("Failed to query catalog for {}: {}", stem, e),
|
||||
})? {
|
||||
out.push((p.publisher, p.fmri));
|
||||
}
|
||||
ctx.catalog_cache.insert(stem.to_string(), out.clone());
|
||||
|
|
@ -584,7 +793,9 @@ fn get_manifest_cached(
|
|||
}
|
||||
let manifest_opt = image
|
||||
.get_manifest_from_catalog(fmri)
|
||||
.map_err(|e| PkgTreeError { message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e) })?;
|
||||
.map_err(|e| PkgTreeError {
|
||||
message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e),
|
||||
})?;
|
||||
let manifest = manifest_opt.unwrap_or_else(|| libips::actions::Manifest::new());
|
||||
ctx.manifest_cache.insert(key, manifest.clone());
|
||||
Ok(manifest)
|
||||
|
|
@ -595,7 +806,9 @@ fn get_incorporated_release_cached(
|
|||
ctx: &mut AdviceContext,
|
||||
stem: &str,
|
||||
) -> Result<Option<String>> {
|
||||
if let Some(v) = ctx.lock_cache.get(stem) { return Ok(v.clone()); }
|
||||
if let Some(v) = ctx.lock_cache.get(stem) {
|
||||
return Ok(v.clone());
|
||||
}
|
||||
let v = image.get_incorporated_release(stem)?;
|
||||
ctx.lock_cache.insert(stem.to_string(), v.clone());
|
||||
Ok(v)
|
||||
|
|
@ -607,7 +820,9 @@ fn print_trees(graph: &Graph, roots: &[String], max_depth: usize) {
|
|||
// Print a tree for each root
|
||||
let mut printed = HashSet::new();
|
||||
for r in roots {
|
||||
if printed.contains(r) { continue; }
|
||||
if printed.contains(r) {
|
||||
continue;
|
||||
}
|
||||
printed.insert(r.clone());
|
||||
println!("{}", r);
|
||||
let mut path = Vec::new();
|
||||
|
|
@ -625,7 +840,9 @@ fn print_tree_rec(
|
|||
path: &mut Vec<String>,
|
||||
_seen: &mut HashSet<String>,
|
||||
) {
|
||||
if max_depth != 0 && depth > max_depth { return; }
|
||||
if max_depth != 0 && depth > max_depth {
|
||||
return;
|
||||
}
|
||||
path.push(node.to_string());
|
||||
|
||||
if let Some(edges) = graph.adj.get(node) {
|
||||
|
|
@ -675,7 +892,11 @@ fn dfs_cycles(
|
|||
let mut cycle_edges = Vec::new();
|
||||
for i in pos..stack.len() {
|
||||
let from = &stack[i];
|
||||
let to2 = if i + 1 < stack.len() { &stack[i+1] } else { to };
|
||||
let to2 = if i + 1 < stack.len() {
|
||||
&stack[i + 1]
|
||||
} else {
|
||||
to
|
||||
};
|
||||
if let Some(es2) = graph.adj.get(from) {
|
||||
if let Some(edge) = es2.iter().find(|ed| &ed.to == to2) {
|
||||
cycle_edges.push(edge.dep_type.clone());
|
||||
|
|
@ -684,7 +905,10 @@ fn dfs_cycles(
|
|||
}
|
||||
}
|
||||
}
|
||||
cycles.push(Cycle { nodes: cycle_nodes, edges: cycle_edges });
|
||||
cycles.push(Cycle {
|
||||
nodes: cycle_nodes,
|
||||
edges: cycle_edges,
|
||||
});
|
||||
} else if !visited.contains(to) {
|
||||
dfs_cycles(graph, to, visited, stack, cycles);
|
||||
}
|
||||
|
|
@ -702,7 +926,7 @@ fn dedup_cycles(mut cycles: Vec<Cycle>) -> Vec<Cycle> {
|
|||
}
|
||||
// rotate to minimal node position (excluding the duplicate last element when comparing)
|
||||
if c.nodes.len() > 1 {
|
||||
let inner = &c.nodes[..c.nodes.len()-1];
|
||||
let inner = &c.nodes[..c.nodes.len() - 1];
|
||||
if let Some((min_idx, _)) = inner.iter().enumerate().min_by_key(|(_, n)| *n) {
|
||||
c.nodes.rotate_left(min_idx);
|
||||
c.edges.rotate_left(min_idx);
|
||||
|
|
@ -713,7 +937,12 @@ fn dedup_cycles(mut cycles: Vec<Cycle>) -> Vec<Cycle> {
|
|||
let mut seen = HashSet::new();
|
||||
cycles.retain(|c| {
|
||||
let key = c.nodes.join("->");
|
||||
if seen.contains(&key) { false } else { seen.insert(key); true }
|
||||
if seen.contains(&key) {
|
||||
false
|
||||
} else {
|
||||
seen.insert(key);
|
||||
true
|
||||
}
|
||||
});
|
||||
cycles
|
||||
}
|
||||
|
|
@ -730,7 +959,9 @@ fn print_cycles(cycles: &[Cycle]) {
|
|||
}
|
||||
|
||||
fn print_suggestions(cycles: &[Cycle], graph: &Graph) {
|
||||
if cycles.is_empty() { return; }
|
||||
if cycles.is_empty() {
|
||||
return;
|
||||
}
|
||||
println!("\nSuggestions to break cycles (heuristic):");
|
||||
for (i, c) in cycles.iter().enumerate() {
|
||||
// Prefer breaking an 'incorporate' edge if present, otherwise any edge
|
||||
|
|
@ -741,16 +972,30 @@ fn print_suggestions(cycles: &[Cycle], graph: &Graph) {
|
|||
if let Some(es) = graph.adj.get(from) {
|
||||
for e in es {
|
||||
if &e.to == to {
|
||||
if e.dep_type == "incorporate" { suggested = Some((from.clone(), to.clone())); break 'outer; }
|
||||
if suggested.is_none() { suggested = Some((from.clone(), to.clone())); }
|
||||
if e.dep_type == "incorporate" {
|
||||
suggested = Some((from.clone(), to.clone()));
|
||||
break 'outer;
|
||||
}
|
||||
if suggested.is_none() {
|
||||
suggested = Some((from.clone(), to.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((from, to)) = suggested {
|
||||
println!(" {}. Consider relaxing/removing edge {} -> {} (preferably if it's an incorporation).", i + 1, from, to);
|
||||
println!(
|
||||
" {}. Consider relaxing/removing edge {} -> {} (preferably if it's an incorporation).",
|
||||
i + 1,
|
||||
from,
|
||||
to
|
||||
);
|
||||
} else {
|
||||
println!(" {}. Consider relaxing one edge along the cycle: {}", i + 1, c.nodes.join(" -> "));
|
||||
println!(
|
||||
" {}. Consider relaxing one edge along the cycle: {}",
|
||||
i + 1,
|
||||
c.nodes.join(" -> ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -777,7 +1022,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------- Dangling dependency scan ----------
|
||||
fn run_dangling_scan(
|
||||
image: &Image,
|
||||
|
|
@ -786,9 +1030,9 @@ fn run_dangling_scan(
|
|||
format: OutputFormat,
|
||||
) -> Result<()> {
|
||||
// Query full catalog once
|
||||
let mut pkgs = image
|
||||
.query_catalog(None)
|
||||
.map_err(|e| PkgTreeError { message: format!("Failed to query catalog: {}", e) })?;
|
||||
let mut pkgs = image.query_catalog(None).map_err(|e| PkgTreeError {
|
||||
message: format!("Failed to query catalog: {}", e),
|
||||
})?;
|
||||
|
||||
// Build set of available non-obsolete stems AND an index of available (release, branch) pairs per stem,
|
||||
// honoring publisher filter
|
||||
|
|
@ -796,9 +1040,13 @@ fn run_dangling_scan(
|
|||
let mut available_index: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
|
||||
for p in &pkgs {
|
||||
if let Some(pubf) = publisher {
|
||||
if p.publisher != pubf { continue; }
|
||||
if p.publisher != pubf {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if p.obsolete {
|
||||
continue;
|
||||
}
|
||||
if p.obsolete { continue; }
|
||||
let stem = p.fmri.stem().to_string();
|
||||
available_stems.insert(stem.clone());
|
||||
let ver = p.fmri.version();
|
||||
|
|
@ -828,8 +1076,12 @@ fn run_dangling_scan(
|
|||
Ok(Some(man)) => {
|
||||
let mut missing_for_pkg: Vec<String> = Vec::new();
|
||||
for dep in man.dependencies {
|
||||
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { continue; }
|
||||
let Some(df) = dep.fmri else { continue; };
|
||||
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" {
|
||||
continue;
|
||||
}
|
||||
let Some(df) = dep.fmri else {
|
||||
continue;
|
||||
};
|
||||
let stem = df.stem().to_string();
|
||||
|
||||
// Extract version/branch constraints if any (from optional properties)
|
||||
|
|
@ -849,7 +1101,9 @@ fn run_dangling_scan(
|
|||
let satisfies = |stem: &str, rel: Option<&str>, br: Option<&str>| -> bool {
|
||||
if let Some(list) = available_index.get(stem) {
|
||||
if let (Some(rreq), Some(breq)) = (rel, br) {
|
||||
return list.iter().any(|(r, b)| r == rreq && b.as_deref() == Some(breq));
|
||||
return list
|
||||
.iter()
|
||||
.any(|(r, b)| r == rreq && b.as_deref() == Some(breq));
|
||||
} else if let Some(rreq) = rel {
|
||||
return list.iter().any(|(r, _)| r == rreq);
|
||||
} else if let Some(breq) = br {
|
||||
|
|
@ -868,14 +1122,24 @@ fn run_dangling_scan(
|
|||
if !satisfies(&stem, c.release.as_deref(), c.branch.as_deref()) {
|
||||
// Include constraint context in output for maintainers
|
||||
let mut ctx = String::new();
|
||||
if let Some(r) = &c.release { ctx.push_str(&format!("release={} ", r)); }
|
||||
if let Some(b) = &c.branch { ctx.push_str(&format!("branch={}", b)); }
|
||||
if let Some(r) = &c.release {
|
||||
ctx.push_str(&format!("release={} ", r));
|
||||
}
|
||||
if let Some(b) = &c.branch {
|
||||
ctx.push_str(&format!("branch={}", b));
|
||||
}
|
||||
let ctx = ctx.trim().to_string();
|
||||
if ctx.is_empty() { mark_missing = Some(stem.clone()); } else { mark_missing = Some(format!("{} [required {}]", stem, ctx)); }
|
||||
if ctx.is_empty() {
|
||||
mark_missing = Some(stem.clone());
|
||||
} else {
|
||||
mark_missing = Some(format!("{} [required {}]", stem, ctx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(m) = mark_missing { missing_for_pkg.push(m); }
|
||||
if let Some(m) = mark_missing {
|
||||
missing_for_pkg.push(m);
|
||||
}
|
||||
}
|
||||
if !missing_for_pkg.is_empty() {
|
||||
missing_for_pkg.sort();
|
||||
|
|
@ -898,13 +1162,18 @@ fn run_dangling_scan(
|
|||
if dangling.is_empty() {
|
||||
println!("No dangling dependencies detected.");
|
||||
} else {
|
||||
println!("Found {} package(s) with dangling dependencies:", dangling.len());
|
||||
println!(
|
||||
"Found {} package(s) with dangling dependencies:",
|
||||
dangling.len()
|
||||
);
|
||||
let mut keys: Vec<String> = dangling.keys().cloned().collect();
|
||||
keys.sort();
|
||||
for k in keys {
|
||||
println!("- {}:", k);
|
||||
if let Some(list) = dangling.get(&k) {
|
||||
for m in list { println!(" • {}", m); }
|
||||
for m in list {
|
||||
println!(" • {}", m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -912,10 +1181,16 @@ fn run_dangling_scan(
|
|||
OutputFormat::Json => {
|
||||
use serde::Serialize;
|
||||
#[derive(Serialize)]
|
||||
struct DanglingJson { package_fmri: String, missing_stems: Vec<String> }
|
||||
struct DanglingJson {
|
||||
package_fmri: String,
|
||||
missing_stems: Vec<String>,
|
||||
}
|
||||
let mut out: Vec<DanglingJson> = Vec::new();
|
||||
for (pkg, miss) in dangling.into_iter() {
|
||||
out.push(DanglingJson { package_fmri: pkg, missing_stems: miss });
|
||||
out.push(DanglingJson {
|
||||
package_fmri: pkg,
|
||||
missing_stems: miss,
|
||||
});
|
||||
}
|
||||
out.sort_by(|a, b| a.package_fmri.cmp(&b.package_fmri));
|
||||
println!("{}", serde_json::to_string_pretty(&out).into_diagnostic()?);
|
||||
|
|
@ -927,8 +1202,9 @@ fn run_dangling_scan(
|
|||
|
||||
// ---------- Targeted analysis: parse pkg6 solver error text ----------
|
||||
fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathBuf) -> Result<()> {
|
||||
let text = std::fs::read_to_string(err_path)
|
||||
.map_err(|e| PkgTreeError { message: format!("Failed to read solver error file {:?}: {}", err_path, e) })?;
|
||||
let text = std::fs::read_to_string(err_path).map_err(|e| PkgTreeError {
|
||||
message: format!("Failed to read solver error file {:?}: {}", err_path, e),
|
||||
})?;
|
||||
|
||||
// Build a stack based on indentation before the tree bullet "└─".
|
||||
let mut stack: Vec<String> = Vec::new();
|
||||
|
|
@ -943,14 +1219,22 @@ fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathB
|
|||
|
||||
// Extract node text after "└─ "
|
||||
let bullet = "└─ ";
|
||||
let start = match line.find(bullet) { Some(p) => p + bullet.len(), None => continue };
|
||||
let start = match line.find(bullet) {
|
||||
Some(p) => p + bullet.len(),
|
||||
None => continue,
|
||||
};
|
||||
let mut node_full = line[start..].trim().to_string();
|
||||
// Remove trailing diagnostic phrases for leaf line
|
||||
if let Some(pos) = node_full.find("for which no candidates were found") {
|
||||
node_full = node_full[..pos].trim().trim_end_matches(',').to_string();
|
||||
}
|
||||
|
||||
if level >= stack.len() { stack.push(node_full.clone()); } else { stack.truncate(level); stack.push(node_full.clone()); }
|
||||
if level >= stack.len() {
|
||||
stack.push(node_full.clone());
|
||||
} else {
|
||||
stack.truncate(level);
|
||||
stack.push(node_full.clone());
|
||||
}
|
||||
|
||||
if line.contains("for which no candidates were found") {
|
||||
failing_leaf = Some(node_full.clone());
|
||||
|
|
@ -961,7 +1245,9 @@ fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathB
|
|||
}
|
||||
|
||||
if failing_leaf.is_none() {
|
||||
println!("Could not find a 'for which no candidates were found' leaf in the provided solver error file.");
|
||||
println!(
|
||||
"Could not find a 'for which no candidates were found' leaf in the provided solver error file."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
|
@ -983,23 +1269,56 @@ fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathB
|
|||
println!("Found 1 installability issue (from solver error):");
|
||||
let constraint_str = format!(
|
||||
"{}{}",
|
||||
constraint.release.as_ref().map(|r| format!("release={} ", r)).unwrap_or_default(),
|
||||
constraint.branch.as_ref().map(|b| format!("branch={}", b)).unwrap_or_default(),
|
||||
).trim().to_string();
|
||||
println!(" 1. No viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}",
|
||||
constraint
|
||||
.release
|
||||
.as_ref()
|
||||
.map(|r| format!("release={} ", r))
|
||||
.unwrap_or_default(),
|
||||
constraint
|
||||
.branch
|
||||
.as_ref()
|
||||
.map(|b| format!("branch={}", b))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.trim()
|
||||
.to_string();
|
||||
println!(
|
||||
" 1. No viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}",
|
||||
stem,
|
||||
path_stems.join(" -> "),
|
||||
if constraint_str.is_empty() { "<none>".to_string() } else { constraint_str },
|
||||
if constraint_str.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
constraint_str
|
||||
},
|
||||
details,
|
||||
);
|
||||
println!(" - Suggestions:");
|
||||
println!(" • Add or publish a matching package for '{}'{}{}.",
|
||||
println!(
|
||||
" • Add or publish a matching package for '{}'{}{}.",
|
||||
stem,
|
||||
constraint.release.as_ref().map(|r| format!(" (release={})", r)).unwrap_or_default(),
|
||||
constraint.branch.as_ref().map(|b| format!(" (branch={})", b)).unwrap_or_default());
|
||||
println!(" • Alternatively, relax the dependency constraint in the requiring package to match available releases.");
|
||||
if let Some(lock) = get_incorporated_release_cached(image, &mut ctx, &stem).ok().flatten() {
|
||||
println!(" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", stem, lock);
|
||||
constraint
|
||||
.release
|
||||
.as_ref()
|
||||
.map(|r| format!(" (release={})", r))
|
||||
.unwrap_or_default(),
|
||||
constraint
|
||||
.branch
|
||||
.as_ref()
|
||||
.map(|b| format!(" (branch={})", b))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
println!(
|
||||
" • Alternatively, relax the dependency constraint in the requiring package to match available releases."
|
||||
);
|
||||
if let Some(lock) = get_incorporated_release_cached(image, &mut ctx, &stem)
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
println!(
|
||||
" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.",
|
||||
stem, lock
|
||||
);
|
||||
}
|
||||
println!(" • Ensure catalogs are up to date: 'pkg6 refresh'.");
|
||||
|
||||
|
|
@ -1010,31 +1329,45 @@ fn stem_from_node(node: &str) -> String {
|
|||
// Node may be like: "pkg://...@ver would require" or "archiver/gnu-tar branch=5.11, which ..." or just a stem
|
||||
let first = node.split_whitespace().next().unwrap_or("");
|
||||
if first.starts_with("pkg://") {
|
||||
if let Ok(fmri) = libips::fmri::Fmri::parse(first) { return fmri.stem().to_string(); }
|
||||
if let Ok(fmri) = libips::fmri::Fmri::parse(first) {
|
||||
return fmri.stem().to_string();
|
||||
}
|
||||
}
|
||||
// If it contains '@' (FMRI without scheme), parse via Fmri::parse
|
||||
if first.contains('@') {
|
||||
if let Ok(fmri) = libips::fmri::Fmri::parse(first) { return fmri.stem().to_string(); }
|
||||
if let Ok(fmri) = libips::fmri::Fmri::parse(first) {
|
||||
return fmri.stem().to_string();
|
||||
}
|
||||
}
|
||||
// Otherwise assume it's a stem token
|
||||
first.trim_end_matches(',').to_string()
|
||||
}
|
||||
|
||||
fn parse_leaf_node(node: &str) -> (String, DepConstraint) {
|
||||
let core = node.split("for which").next().unwrap_or(node).trim().trim_end_matches(',').to_string();
|
||||
let core = node
|
||||
.split("for which")
|
||||
.next()
|
||||
.unwrap_or(node)
|
||||
.trim()
|
||||
.trim_end_matches(',')
|
||||
.to_string();
|
||||
let mut release: Option<String> = None;
|
||||
let mut branch: Option<String> = None;
|
||||
|
||||
// Find release=
|
||||
if let Some(p) = core.find("release=") {
|
||||
let rest = &core[p + "release=".len()..];
|
||||
let end = rest.find(|c: char| c == ' ' || c == ',').unwrap_or(rest.len());
|
||||
let end = rest
|
||||
.find(|c: char| c == ' ' || c == ',')
|
||||
.unwrap_or(rest.len());
|
||||
release = Some(rest[..end].to_string());
|
||||
}
|
||||
// Find branch=
|
||||
if let Some(p) = core.find("branch=") {
|
||||
let rest = &core[p + "branch=".len()..];
|
||||
let end = rest.find(|c: char| c == ' ' || c == ',').unwrap_or(rest.len());
|
||||
let end = rest
|
||||
.find(|c: char| c == ' ' || c == ',')
|
||||
.unwrap_or(rest.len());
|
||||
branch = Some(rest[..end].to_string());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ mod sources;
|
|||
#[allow(clippy::result_large_err)]
|
||||
mod workspace;
|
||||
|
||||
use clap::ArgAction;
|
||||
use crate::workspace::Workspace;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use clap::ArgAction;
|
||||
use clap::{Parser, Subcommand};
|
||||
use specfile::macros;
|
||||
use specfile::parse;
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ use libips::actions::{ActionError, File as FileAction, Manifest};
|
|||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::env::{current_dir, set_current_dir};
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::fs::{File, create_dir_all};
|
||||
use std::io::Error as IOError;
|
||||
use std::io::copy;
|
||||
use std::io::prelude::*;
|
||||
use std::io::Error as IOError;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::result::Result as StdResult;
|
||||
|
|
|
|||
|
|
@ -43,34 +43,38 @@ impl MacroParser {
|
|||
for macro_pair in inner.clone().into_inner() {
|
||||
match macro_pair.as_rule() {
|
||||
Rule::macro_name => {
|
||||
replaced_line += self.get_variable(macro_pair.as_str())?;
|
||||
},
|
||||
replaced_line +=
|
||||
self.get_variable(macro_pair.as_str())?;
|
||||
}
|
||||
Rule::macro_parameter => {
|
||||
println!("macro parameter: {}", macro_pair.as_str())
|
||||
},
|
||||
println!(
|
||||
"macro parameter: {}",
|
||||
macro_pair.as_str()
|
||||
)
|
||||
}
|
||||
_ => panic!(
|
||||
"Unexpected macro match please update the code together with the peg grammar: {:?}",
|
||||
macro_pair.as_rule()
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => panic!(
|
||||
"Unexpected inner match please update the code together with the peg grammar: {:?}",
|
||||
inner.as_rule()
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Rule::EOI => (),
|
||||
Rule::text => {
|
||||
replaced_line += test_pair.as_str();
|
||||
replaced_line += " ";
|
||||
},
|
||||
}
|
||||
_ => panic!(
|
||||
"Unexpected match please update the code together with the peg grammar: {:?}",
|
||||
test_pair.as_rule()
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use anyhow::{bail, Result};
|
||||
use anyhow::{Result, bail};
|
||||
use semver::Version;
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
mod component;
|
||||
pub mod repology;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use lazy_static::lazy_static;
|
||||
use pest::iterators::Pairs;
|
||||
use pest::Parser;
|
||||
use pest::iterators::Pairs;
|
||||
use pest_derive::Parser;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -233,13 +233,16 @@ fn parse_makefile(pairs: Pairs<crate::Rule>, m: &mut Makefile) -> Result<()> {
|
|||
Rule::comment_string => (),
|
||||
Rule::include => {
|
||||
parse_include(p.into_inner(), m)?;
|
||||
},
|
||||
}
|
||||
Rule::target => (),
|
||||
Rule::define => {
|
||||
parse_define(p.into_inner(), m)?;
|
||||
}
|
||||
Rule::EOI => (),
|
||||
_ => panic!("unexpected rule {:?} inside makefile rule expected variable, define, comment, NEWLINE, include, target", p.as_rule()),
|
||||
_ => panic!(
|
||||
"unexpected rule {:?} inside makefile rule expected variable, define, comment, NEWLINE, include, target",
|
||||
p.as_rule()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,26 +292,23 @@ fn parse_variable(variable_pair: Pairs<crate::Rule>, m: &mut Makefile) -> Result
|
|||
Rule::variable_name => {
|
||||
var.0 = p.as_str().to_string();
|
||||
}
|
||||
Rule::variable_set => {
|
||||
var.1.mode = VariableMode::Set
|
||||
},
|
||||
Rule::variable_add => {
|
||||
var.1.mode = VariableMode::Add
|
||||
}
|
||||
Rule::variable_value => {
|
||||
match var.1.mode {
|
||||
VariableMode::Add => {
|
||||
if m.variables.contains_key(&var.0) {
|
||||
var.1 = m.variables.get(&var.0).unwrap().clone()
|
||||
}
|
||||
var.1.values.push(p.as_str().to_string());
|
||||
}
|
||||
VariableMode::Set => {
|
||||
var.1.values.push(p.as_str().to_string());
|
||||
Rule::variable_set => var.1.mode = VariableMode::Set,
|
||||
Rule::variable_add => var.1.mode = VariableMode::Add,
|
||||
Rule::variable_value => match var.1.mode {
|
||||
VariableMode::Add => {
|
||||
if m.variables.contains_key(&var.0) {
|
||||
var.1 = m.variables.get(&var.0).unwrap().clone()
|
||||
}
|
||||
var.1.values.push(p.as_str().to_string());
|
||||
}
|
||||
}
|
||||
_ => panic!("unexpected rule {:?} inside makefile rule expected variable_name, variable_set, variable_add, variable_value", p.as_rule()),
|
||||
VariableMode::Set => {
|
||||
var.1.values.push(p.as_str().to_string());
|
||||
}
|
||||
},
|
||||
_ => panic!(
|
||||
"unexpected rule {:?} inside makefile rule expected variable_name, variable_set, variable_add, variable_value",
|
||||
p.as_rule()
|
||||
),
|
||||
}
|
||||
}
|
||||
m.variables.insert(var.0, var.1);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue