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 thiserror::Error;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::actions::{Link as LinkAction, Manifest};
|
|
||||||
use crate::actions::{Dir as DirAction, File as FileAction};
|
use crate::actions::{Dir as DirAction, File as FileAction};
|
||||||
|
use crate::actions::{Link as LinkAction, Manifest};
|
||||||
|
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
pub enum InstallerError {
|
pub enum InstallerError {
|
||||||
|
|
@ -23,16 +23,25 @@ pub enum InstallerError {
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Absolute paths are forbidden in actions: {path}")]
|
#[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 },
|
AbsolutePathForbidden { path: String },
|
||||||
|
|
||||||
#[error("Path escapes image root via traversal: {rel}")]
|
#[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 },
|
PathTraversalOutsideImage { rel: String },
|
||||||
|
|
||||||
#[error("Unsupported or not yet implemented action: {action} ({reason})")]
|
#[error("Unsupported or not yet implemented action: {action} ({reason})")]
|
||||||
#[diagnostic(code(ips::installer_error::unsupported_action))]
|
#[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 {
|
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 => {
|
Component::Prefix(_) | Component::RootDir => {
|
||||||
return Err(InstallerError::AbsolutePathForbidden {
|
return Err(InstallerError::AbsolutePathForbidden {
|
||||||
path: rel.to_string(),
|
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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("ApplyOptions")
|
f.debug_struct("ApplyOptions")
|
||||||
.field("dry_run", &self.dry_run)
|
.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)
|
.field("progress_interval", &self.progress_interval)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
@ -115,65 +127,154 @@ impl std::fmt::Debug for ApplyOptions {
|
||||||
|
|
||||||
impl Default for ApplyOptions {
|
impl Default for ApplyOptions {
|
||||||
fn default() -> Self {
|
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.
|
/// Progress event emitted by apply_manifest when a callback is provided.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum ProgressEvent {
|
pub enum ProgressEvent {
|
||||||
StartingPhase { phase: &'static str, total: usize },
|
StartingPhase {
|
||||||
Progress { phase: &'static str, current: usize, total: usize },
|
phase: &'static str,
|
||||||
FinishedPhase { phase: &'static str, total: usize },
|
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>;
|
pub type ProgressCallback = Arc<dyn Fn(ProgressEvent) + Send + Sync + 'static>;
|
||||||
|
|
||||||
/// Apply a manifest to the filesystem rooted at image_root.
|
/// 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).
|
/// 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>| {
|
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
|
// Directories first
|
||||||
let total_dirs = manifest.directories.len();
|
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;
|
let mut i = 0usize;
|
||||||
for d in &manifest.directories {
|
for d in &manifest.directories {
|
||||||
apply_dir(image_root, d, opts)?;
|
apply_dir(image_root, d, opts)?;
|
||||||
i += 1;
|
i += 1;
|
||||||
if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_dirs) {
|
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
|
// Files next
|
||||||
let total_files = manifest.files.len();
|
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;
|
i = 0;
|
||||||
for f_action in &manifest.files {
|
for f_action in &manifest.files {
|
||||||
apply_file(image_root, f_action, opts)?;
|
apply_file(image_root, f_action, opts)?;
|
||||||
i += 1;
|
i += 1;
|
||||||
if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_files) {
|
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
|
// Links
|
||||||
let total_links = manifest.links.len();
|
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;
|
i = 0;
|
||||||
for l in &manifest.links {
|
for l in &manifest.links {
|
||||||
apply_link(image_root, l, opts)?;
|
apply_link(image_root, l, opts)?;
|
||||||
i += 1;
|
i += 1;
|
||||||
if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_links) {
|
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.
|
// Other action kinds are ignored for now and left for future extension.
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -216,7 +317,11 @@ fn ensure_parent(image_root: &Path, p: &str, opts: &ApplyOptions) -> Result<(),
|
||||||
Ok(())
|
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)?;
|
let full = safe_join(image_root, &f.path)?;
|
||||||
|
|
||||||
// Ensure parent exists (directories should already be applied, but be robust)
|
// 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(())
|
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)?;
|
let link_path = safe_join(image_root, &l.path)?;
|
||||||
|
|
||||||
// Determine link type (default to symlink). If properties contain type=hard, create hard link.
|
// 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 {
|
} else {
|
||||||
// Symlink: require non-absolute target to avoid embedding full host paths
|
// Symlink: require non-absolute target to avoid embedding full host paths
|
||||||
if Path::new(&l.target).is_absolute() {
|
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)
|
// Create relative symlink as provided (do not convert to absolute to avoid embedding full paths)
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
|
|
|
||||||
|
|
@ -898,7 +898,10 @@ impl Manifest {
|
||||||
match serde_json::from_str::<Manifest>(&content) {
|
match serde_json::from_str::<Manifest>(&content) {
|
||||||
Ok(manifest) => Ok(manifest),
|
Ok(manifest) => Ok(manifest),
|
||||||
Err(err) => {
|
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
|
// If JSON parsing fails, fall back to string format
|
||||||
Manifest::parse_string(content)
|
Manifest::parse_string(content)
|
||||||
}
|
}
|
||||||
|
|
@ -933,17 +936,24 @@ impl Manifest {
|
||||||
property.key = prop.as_str().to_owned();
|
property.key = prop.as_str().to_owned();
|
||||||
}
|
}
|
||||||
Rule::property_value => {
|
Rule::property_value => {
|
||||||
let str_val: String = prop.as_str().to_owned();
|
let str_val: String =
|
||||||
property.value = str_val
|
prop.as_str().to_owned();
|
||||||
.replace(['\"', '\\'], "");
|
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);
|
act.properties.push(property);
|
||||||
}
|
}
|
||||||
Rule::EOI => (),
|
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);
|
m.add_action(act);
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,17 @@ use walkdir::WalkDir;
|
||||||
|
|
||||||
pub use crate::actions::Manifest;
|
pub use crate::actions::Manifest;
|
||||||
// Core typed 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::depend::{FileDep, GenerateOptions as DependGenerateOptions};
|
||||||
pub use crate::fmri::Fmri;
|
pub use crate::fmri::Fmri;
|
||||||
// For BaseMeta
|
// For BaseMeta
|
||||||
use crate::repository::file_backend::{FileBackend, Transaction};
|
use crate::repository::file_backend::{FileBackend, Transaction};
|
||||||
use crate::repository::{ReadableRepository, RepositoryError, RepositoryVersion, WritableRepository};
|
use crate::repository::{
|
||||||
|
ReadableRepository, RepositoryError, RepositoryVersion, WritableRepository,
|
||||||
|
};
|
||||||
use crate::transformer;
|
use crate::transformer;
|
||||||
pub use crate::transformer::TransformRule;
|
pub use crate::transformer::TransformRule;
|
||||||
|
|
||||||
|
|
@ -87,7 +92,10 @@ pub enum IpsError {
|
||||||
Io(String),
|
Io(String),
|
||||||
|
|
||||||
#[error("Unimplemented feature: {feature}")]
|
#[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 },
|
Unimplemented { feature: &'static str },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,19 +191,32 @@ impl ManifestBuilder {
|
||||||
let mut props = std::collections::HashMap::new();
|
let mut props = std::collections::HashMap::new();
|
||||||
props.insert(
|
props.insert(
|
||||||
"path".to_string(),
|
"path".to_string(),
|
||||||
Property { key: "path".to_string(), value: path.to_string() },
|
Property {
|
||||||
|
key: "path".to_string(),
|
||||||
|
value: path.to_string(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
props.insert(
|
props.insert(
|
||||||
"license".to_string(),
|
"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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a link action
|
/// Add a link action
|
||||||
pub fn add_link(&mut self, path: &str, target: &str) -> &mut Self {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +232,9 @@ impl ManifestBuilder {
|
||||||
}
|
}
|
||||||
/// Start a new empty builder
|
/// Start a new empty builder
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { manifest: Manifest::new() }
|
Self {
|
||||||
|
manifest: Manifest::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience: construct a Manifest directly by scanning a prototype directory.
|
/// Convenience: construct a Manifest directly by scanning a prototype directory.
|
||||||
|
|
@ -223,9 +246,9 @@ impl ManifestBuilder {
|
||||||
proto.display()
|
proto.display()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let root = proto
|
let root = proto.canonicalize().map_err(|e| {
|
||||||
.canonicalize()
|
IpsError::Io(format!("failed to canonicalize {}: {}", proto.display(), e))
|
||||||
.map_err(|e| IpsError::Io(format!("failed to canonicalize {}: {}", proto.display(), e)))?;
|
})?;
|
||||||
|
|
||||||
let mut m = Manifest::new();
|
let mut m = Manifest::new();
|
||||||
for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) {
|
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 {
|
if let Some(fmri) = meta.fmri {
|
||||||
push_attr("pkg.fmri", fmri.to_string());
|
push_attr("pkg.fmri", fmri.to_string());
|
||||||
}
|
}
|
||||||
if let Some(s) = meta.summary { push_attr("pkg.summary", s); }
|
if let Some(s) = meta.summary {
|
||||||
if let Some(c) = meta.classification { push_attr("info.classification", c); }
|
push_attr("pkg.summary", s);
|
||||||
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(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 {
|
if let Some(l) = meta.license {
|
||||||
// Represent base license via an attribute named 'license'; callers may add dedicated license actions separately
|
// Represent base license via an attribute named 'license'; callers may add dedicated license actions separately
|
||||||
self.manifest.attributes.push(Attr {
|
self.manifest.attributes.push(Attr {
|
||||||
|
|
@ -310,12 +341,16 @@ impl Repository {
|
||||||
pub fn open(path: &Path) -> Result<Self, IpsError> {
|
pub fn open(path: &Path) -> Result<Self, IpsError> {
|
||||||
// Validate by opening backend
|
// Validate by opening backend
|
||||||
let _ = FileBackend::open(path)?;
|
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> {
|
pub fn create(path: &Path) -> Result<Self, IpsError> {
|
||||||
let _ = FileBackend::create(path, RepositoryVersion::default())?;
|
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> {
|
pub fn has_publisher(&self, name: &str) -> Result<bool, IpsError> {
|
||||||
|
|
@ -330,7 +365,9 @@ impl Repository {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> &Path { &self.path }
|
pub fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// High-level publishing client for starting repository transactions.
|
/// High-level publishing client for starting repository transactions.
|
||||||
|
|
@ -352,14 +389,21 @@ pub struct PublisherClient {
|
||||||
|
|
||||||
impl PublisherClient {
|
impl PublisherClient {
|
||||||
pub fn new(repo: Repository, publisher: impl Into<String>) -> Self {
|
pub fn new(repo: Repository, publisher: impl Into<String>) -> Self {
|
||||||
Self { repo, publisher: publisher.into() }
|
Self {
|
||||||
|
repo,
|
||||||
|
publisher: publisher.into(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Begin a new transaction
|
/// Begin a new transaction
|
||||||
pub fn begin(&self) -> Result<Txn, IpsError> {
|
pub fn begin(&self) -> Result<Txn, IpsError> {
|
||||||
let backend = FileBackend::open(self.repo.path())?;
|
let backend = FileBackend::open(self.repo.path())?;
|
||||||
let tx = backend.begin_transaction()?; // returns Transaction bound to 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 {
|
impl Txn {
|
||||||
/// Add all files from the given payload/prototype directory
|
/// Add all files from the given payload/prototype directory
|
||||||
pub fn add_payload_dir(&mut self, dir: &Path) -> Result<(), IpsError> {
|
pub fn add_payload_dir(&mut self, dir: &Path) -> Result<(), IpsError> {
|
||||||
let root = dir
|
let root = dir.canonicalize().map_err(|e| {
|
||||||
.canonicalize()
|
IpsError::Io(format!("failed to canonicalize {}: {}", dir.display(), e))
|
||||||
.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()) {
|
for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) {
|
||||||
let p = entry.path();
|
let p = entry.path();
|
||||||
if p.is_file() {
|
if p.is_file() {
|
||||||
|
|
@ -451,7 +495,11 @@ pub struct DependencyGenerator;
|
||||||
impl DependencyGenerator {
|
impl DependencyGenerator {
|
||||||
/// Compute file-level dependencies for the given manifest, using `proto` as base for local file resolution.
|
/// 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.
|
/// 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() {
|
if opts.proto_dir.is_none() {
|
||||||
opts.proto_dir = Some(proto.to_path_buf());
|
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
|
/// Intentionally not implemented in this facade: mapping raw file dependencies to package FMRIs
|
||||||
/// requires repository/catalog context. Call `generate_with_repo` instead.
|
/// requires repository/catalog context. Call `generate_with_repo` instead.
|
||||||
pub fn generate(_proto: &Path, _manifest: &Manifest) -> Result<Manifest, IpsError> {
|
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.
|
/// Generate dependencies using a repository to resolve file-level deps into package FMRIs.
|
||||||
|
|
@ -562,10 +612,8 @@ impl Resolver {
|
||||||
if f.version.is_none() {
|
if f.version.is_none() {
|
||||||
// Query repository for this package name
|
// Query repository for this package name
|
||||||
let pkgs = repo.list_packages(publisher, Some(&f.name))?;
|
let pkgs = repo.list_packages(publisher, Some(&f.name))?;
|
||||||
let matches: Vec<&crate::repository::PackageInfo> = pkgs
|
let matches: Vec<&crate::repository::PackageInfo> =
|
||||||
.iter()
|
pkgs.iter().filter(|pi| pi.fmri.name == f.name).collect();
|
||||||
.filter(|pi| pi.fmri.name == f.name)
|
|
||||||
.collect();
|
|
||||||
if matches.len() == 1 {
|
if matches.len() == 1 {
|
||||||
let fmri = &matches[0].fmri;
|
let fmri = &matches[0].fmri;
|
||||||
if f.publisher.is_none() {
|
if f.publisher.is_none() {
|
||||||
|
|
@ -597,7 +645,6 @@ fn manifest_fmri(manifest: &Manifest) -> Option<Fmri> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Lint facade providing a typed, extensible rule engine with enable/disable controls.
|
/// Lint facade providing a typed, extensible rule engine with enable/disable controls.
|
||||||
///
|
///
|
||||||
/// Configure which rules to run, override severities, and pass rule-specific parameters.
|
/// 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 reference_repos: Vec<PathBuf>,
|
||||||
pub rulesets: Vec<String>,
|
pub rulesets: Vec<String>,
|
||||||
// Rule configurability
|
// Rule configurability
|
||||||
pub disabled_rules: Vec<String>, // rule IDs to disable
|
pub disabled_rules: Vec<String>, // rule IDs to disable
|
||||||
pub enabled_only: Option<Vec<String>>, // if Some, only these rule IDs run
|
pub enabled_only: Option<Vec<String>>, // if Some, only these rule IDs run
|
||||||
pub severity_overrides: std::collections::HashMap<String, lint::LintSeverity>,
|
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)
|
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)]
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
pub enum LintIssue {
|
pub enum LintIssue {
|
||||||
#[error("Manifest is missing pkg.fmri or it is invalid")]
|
#[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,
|
MissingOrInvalidFmri,
|
||||||
|
|
||||||
#[error("Manifest has multiple pkg.fmri attributes")]
|
#[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,
|
DuplicateFmri,
|
||||||
|
|
||||||
#[error("Manifest is missing pkg.summary")]
|
#[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,
|
MissingSummary,
|
||||||
|
|
||||||
#[error("Dependency is missing FMRI or name")]
|
#[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,
|
DependencyMissingFmri,
|
||||||
|
|
||||||
#[error("Dependency type is missing")]
|
#[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,
|
DependencyMissingType,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait LintRule {
|
pub trait LintRule {
|
||||||
fn id(&self) -> &'static str;
|
fn id(&self) -> &'static str;
|
||||||
fn description(&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.
|
/// 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.
|
/// The config carries enable/disable lists, severity overrides and rule-specific parameters for extensibility.
|
||||||
fn check(&self, manifest: &Manifest, config: &LintConfig) -> Vec<miette::Report>;
|
fn check(&self, manifest: &Manifest, config: &LintConfig) -> Vec<miette::Report>;
|
||||||
|
|
@ -670,8 +734,12 @@ pub mod lint {
|
||||||
|
|
||||||
struct RuleManifestFmri;
|
struct RuleManifestFmri;
|
||||||
impl LintRule for RuleManifestFmri {
|
impl LintRule for RuleManifestFmri {
|
||||||
fn id(&self) -> &'static str { "manifest.fmri" }
|
fn id(&self) -> &'static str {
|
||||||
fn description(&self) -> &'static str { "Validate pkg.fmri presence/uniqueness/parse" }
|
"manifest.fmri"
|
||||||
|
}
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Validate pkg.fmri presence/uniqueness/parse"
|
||||||
|
}
|
||||||
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
|
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
|
||||||
let mut diags = Vec::new();
|
let mut diags = Vec::new();
|
||||||
let mut fmri_attr_count = 0usize;
|
let mut fmri_attr_count = 0usize;
|
||||||
|
|
@ -679,13 +747,21 @@ pub mod lint {
|
||||||
for attr in &manifest.attributes {
|
for attr in &manifest.attributes {
|
||||||
if attr.key == "pkg.fmri" {
|
if attr.key == "pkg.fmri" {
|
||||||
fmri_attr_count += 1;
|
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) {
|
match (fmri_attr_count, fmri_text) {
|
||||||
(0, _) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)),
|
(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)),
|
(_, None) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)),
|
||||||
}
|
}
|
||||||
diags
|
diags
|
||||||
|
|
@ -694,29 +770,47 @@ pub mod lint {
|
||||||
|
|
||||||
struct RuleManifestSummary;
|
struct RuleManifestSummary;
|
||||||
impl LintRule for RuleManifestSummary {
|
impl LintRule for RuleManifestSummary {
|
||||||
fn id(&self) -> &'static str { "manifest.summary" }
|
fn id(&self) -> &'static str {
|
||||||
fn description(&self) -> &'static str { "Validate pkg.summary presence" }
|
"manifest.summary"
|
||||||
|
}
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Validate pkg.summary presence"
|
||||||
|
}
|
||||||
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
|
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
|
||||||
let mut diags = Vec::new();
|
let mut diags = Vec::new();
|
||||||
let has_summary = manifest
|
let has_summary = manifest
|
||||||
.attributes
|
.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.key == "pkg.summary" && a.values.iter().any(|v| !v.trim().is_empty()));
|
.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
|
diags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RuleDependencyFields;
|
struct RuleDependencyFields;
|
||||||
impl LintRule for RuleDependencyFields {
|
impl LintRule for RuleDependencyFields {
|
||||||
fn id(&self) -> &'static str { "depend.fields" }
|
fn id(&self) -> &'static str {
|
||||||
fn description(&self) -> &'static str { "Validate basic dependency fields" }
|
"depend.fields"
|
||||||
|
}
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Validate basic dependency fields"
|
||||||
|
}
|
||||||
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
|
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
|
||||||
let mut diags = Vec::new();
|
let mut diags = Vec::new();
|
||||||
for dep in &manifest.dependencies {
|
for dep in &manifest.dependencies {
|
||||||
let fmri_ok = dep.fmri.as_ref().map(|f| !f.name.trim().is_empty()).unwrap_or(false);
|
let fmri_ok = dep
|
||||||
if !fmri_ok { diags.push(miette::Report::new(LintIssue::DependencyMissingFmri)); }
|
.fmri
|
||||||
if dep.dependency_type.trim().is_empty() { diags.push(miette::Report::new(LintIssue::DependencyMissingType)); }
|
.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
|
diags
|
||||||
}
|
}
|
||||||
|
|
@ -735,7 +829,8 @@ pub mod lint {
|
||||||
let set: std::collections::HashSet<&str> = only.iter().map(|s| s.as_str()).collect();
|
let set: std::collections::HashSet<&str> = only.iter().map(|s| s.as_str()).collect();
|
||||||
return set.contains(rule_id);
|
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)
|
!disabled.contains(rule_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -751,7 +846,10 @@ pub mod lint {
|
||||||
/// assert!(diags.is_empty());
|
/// assert!(diags.is_empty());
|
||||||
/// # Ok::<(), ips::IpsError>(())
|
/// # 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();
|
let mut diags: Vec<miette::Report> = Vec::new();
|
||||||
for rule in default_rules().into_iter() {
|
for rule in default_rules().into_iter() {
|
||||||
if rule_enabled(rule.id(), config) {
|
if rule_enabled(rule.id(), config) {
|
||||||
|
|
@ -769,7 +867,11 @@ mod tests {
|
||||||
|
|
||||||
fn make_manifest_with_fmri(fmri_str: &str) -> Manifest {
|
fn make_manifest_with_fmri(fmri_str: &str) -> Manifest {
|
||||||
let mut m = Manifest::new();
|
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
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -799,14 +901,17 @@ mod tests {
|
||||||
let fmri = dep.fmri.as_ref().unwrap();
|
let fmri = dep.fmri.as_ref().unwrap();
|
||||||
assert_eq!(fmri.name, "pkgA");
|
assert_eq!(fmri.name, "pkgA");
|
||||||
assert_eq!(fmri.publisher.as_deref(), Some("pub"));
|
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");
|
assert_eq!(fmri.version.as_ref().unwrap().to_string(), "1.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolver_uses_repository_for_provider() {
|
fn resolver_uses_repository_for_provider() {
|
||||||
use crate::repository::file_backend::FileBackend;
|
|
||||||
use crate::repository::RepositoryVersion;
|
use crate::repository::RepositoryVersion;
|
||||||
|
use crate::repository::file_backend::FileBackend;
|
||||||
|
|
||||||
// Create a temporary repository and add a publisher
|
// Create a temporary repository and add a publisher
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|
@ -816,7 +921,11 @@ mod tests {
|
||||||
|
|
||||||
// Publish provider package pkgA@1.0
|
// Publish provider package pkgA@1.0
|
||||||
let mut provider = Manifest::new();
|
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();
|
let mut tx = backend.begin_transaction().unwrap();
|
||||||
tx.update_manifest(provider);
|
tx.update_manifest(provider);
|
||||||
tx.set_publisher("pub");
|
tx.set_publisher("pub");
|
||||||
|
|
@ -854,8 +963,16 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn lint_accepts_valid_manifest() {
|
fn lint_accepts_valid_manifest() {
|
||||||
let mut m = Manifest::new();
|
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 {
|
||||||
m.attributes.push(Attr { key: "pkg.summary".into(), values: vec!["A package".to_string()], properties: Default::default() });
|
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 cfg = LintConfig::default();
|
||||||
let diags = lint::lint_manifest(&m, &cfg).unwrap();
|
let diags = lint::lint_manifest(&m, &cfg).unwrap();
|
||||||
assert!(diags.is_empty(), "unexpected diags: {:?}", diags);
|
assert!(diags.is_empty(), "unexpected diags: {:?}", diags);
|
||||||
|
|
@ -865,14 +982,22 @@ mod tests {
|
||||||
fn lint_disable_summary_rule() {
|
fn lint_disable_summary_rule() {
|
||||||
// Manifest with valid fmri but missing summary
|
// Manifest with valid fmri but missing summary
|
||||||
let mut m = Manifest::new();
|
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
|
// Disable the summary rule; expect no diagnostics
|
||||||
let mut cfg = LintConfig::default();
|
let mut cfg = LintConfig::default();
|
||||||
cfg.disabled_rules = vec!["manifest.summary".to_string()];
|
cfg.disabled_rules = vec!["manifest.summary".to_string()];
|
||||||
let diags = lint::lint_manifest(&m, &cfg).unwrap();
|
let diags = lint::lint_manifest(&m, &cfg).unwrap();
|
||||||
// fmri is valid, dependencies empty, summary rule disabled => no diags
|
// 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]
|
#[test]
|
||||||
|
|
@ -889,14 +1014,29 @@ mod tests {
|
||||||
let m = b.build();
|
let m = b.build();
|
||||||
|
|
||||||
// Validate attributes include fmri and summary
|
// 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| {
|
||||||
assert!(m.attributes.iter().any(|a| a.key == "pkg.summary" && a.values.get(0).map(|v| v == "Summary").unwrap_or(false)));
|
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
|
// Validate license
|
||||||
assert_eq!(m.licenses.len(), 1);
|
assert_eq!(m.licenses.len(), 1);
|
||||||
let lic = &m.licenses[0];
|
let lic = &m.licenses[0];
|
||||||
assert_eq!(lic.properties.get("path").map(|p| p.value.as_str()), Some("LICENSE"));
|
assert_eq!(
|
||||||
assert_eq!(lic.properties.get("license").map(|p| p.value.as_str()), Some("MIT"));
|
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
|
// Validate link
|
||||||
assert_eq!(m.links.len(), 1);
|
assert_eq!(m.links.len(), 1);
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ use miette::Diagnostic;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::error::Error as StdError;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::error::Error as StdError;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
|
@ -27,10 +27,16 @@ pub struct DependError {
|
||||||
|
|
||||||
impl DependError {
|
impl DependError {
|
||||||
fn new(message: impl Into<String>) -> Self {
|
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 {
|
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).
|
/// 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 mut out = Vec::new();
|
||||||
let bypass = compile_bypass(&opts.bypass_patterns)?;
|
let bypass = compile_bypass(&opts.bypass_patterns)?;
|
||||||
|
|
||||||
for f in &manifest.files {
|
for f in &manifest.files {
|
||||||
// Determine installed path (manifests typically do not start with '/').
|
// 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) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,16 +158,28 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene
|
||||||
// Normalize /bin -> /usr/bin
|
// Normalize /bin -> /usr/bin
|
||||||
let interp_path = normalize_bin_path(&interp);
|
let interp_path = normalize_bin_path(&interp);
|
||||||
if !interp_path.starts_with('/') {
|
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 {
|
} else {
|
||||||
// Derive dir and base name
|
// Derive dir and base name
|
||||||
let (dir, base) = split_dir_base(&interp_path);
|
let (dir, base) = split_dir_base(&interp_path);
|
||||||
if let Some(dir) = dir {
|
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 Python interpreter, perform Python analysis
|
||||||
if interp_path.contains("python") {
|
if interp_path.contains("python") {
|
||||||
if let Some((maj, min)) = infer_python_version_from_paths(&installed_path, Some(&interp_path)) {
|
if let Some((maj, min)) =
|
||||||
let mut pydeps = process_python(&bytes, &installed_path, (maj, min), opts);
|
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);
|
out.append(&mut pydeps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +199,13 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene
|
||||||
if exec_path.starts_with('/') {
|
if exec_path.starts_with('/') {
|
||||||
let (dir, base) = split_dir_base(&exec_path);
|
let (dir, base) = split_dir_base(&exec_path);
|
||||||
if let Some(dir) = dir {
|
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
|
/// 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 out = Vec::new();
|
||||||
let mut token_count = 0;
|
let mut token_count = 0;
|
||||||
for p in provided {
|
for p in provided {
|
||||||
if p == PD_DEFAULT_RUNPATH {
|
if p == PD_DEFAULT_RUNPATH {
|
||||||
token_count += 1;
|
token_count += 1;
|
||||||
if 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);
|
out.extend_from_slice(defaults);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -208,7 +247,9 @@ fn insert_default_runpath(defaults: &[String], provided: &[String]) -> std::resu
|
||||||
fn compile_bypass(patterns: &[String]) -> Result<Vec<Regex>> {
|
fn compile_bypass(patterns: &[String]) -> Result<Vec<Regex>> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for p in patterns {
|
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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
@ -259,11 +300,18 @@ fn process_elf(bytes: &[u8], installed_path: &str, opts: &GenerateOptions) -> Ve
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If no override, prefer DT_RUNPATH if present else defaults
|
// 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
|
// 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
|
let expanded: Vec<String> = effective
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| p.replace("$ORIGIN", &origin))
|
.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
|
// Emit FileDep for each DT_NEEDED base name
|
||||||
for bn in needed.drain(..) {
|
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),
|
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 {
|
for fd in file_deps {
|
||||||
match &fd.kind {
|
match &fd.kind {
|
||||||
FileDepKind::Elf { base_name, run_paths, .. } => {
|
FileDepKind::Elf {
|
||||||
|
base_name,
|
||||||
|
run_paths,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
let mut providers: Vec<Fmri> = Vec::new();
|
let mut providers: Vec<Fmri> = Vec::new();
|
||||||
for dir in run_paths {
|
for dir in run_paths {
|
||||||
let full = normalize_join(dir, base_name);
|
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
|
// 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();
|
let mut providers: Vec<Fmri> = Vec::new();
|
||||||
for dir in run_paths {
|
for dir in run_paths {
|
||||||
let full = normalize_join(dir, base_name);
|
let full = normalize_join(dir, base_name);
|
||||||
|
|
@ -366,7 +428,11 @@ pub fn resolve_dependencies<R: ReadableRepository>(
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileDepKind::Python { base_names, run_paths, .. } => {
|
FileDepKind::Python {
|
||||||
|
base_names,
|
||||||
|
run_paths,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
let mut providers: Vec<Fmri> = Vec::new();
|
let mut providers: Vec<Fmri> = Vec::new();
|
||||||
for dir in run_paths {
|
for dir in run_paths {
|
||||||
for base in base_names {
|
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)
|
// Ask repo to show contents for all packages (files only)
|
||||||
let contents = repo
|
let contents = repo
|
||||||
.show_contents(publisher, None, Some(&["file".to_string()]))
|
.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>() {
|
let fmri = match pc.package_id.parse::<Fmri>() {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Skipping package with invalid FMRI {}: {}", pc.package_id, e);
|
warn!(
|
||||||
|
"Skipping package with invalid FMRI {}: {}",
|
||||||
|
pc.package_id, e
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(files) = pc.files {
|
if let Some(files) = pc.files {
|
||||||
for p in files {
|
for p in files {
|
||||||
// Ensure leading slash
|
// 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());
|
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)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Helpers for script processing ---
|
// --- Helpers for script processing ---
|
||||||
fn parse_shebang(bytes: &[u8]) -> Option<String> {
|
fn parse_shebang(bytes: &[u8]) -> Option<String> {
|
||||||
if bytes.len() < 2 || bytes[0] != b'#' || bytes[1] != b'!' {
|
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 {
|
fn looks_like_smf_manifest(bytes: &[u8]) -> bool {
|
||||||
// Very lightweight detection: SMF manifests are XML files with a <service_bundle ...> root
|
// 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.
|
// 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 ---
|
// --- 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
|
// 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 Ok(re) = Regex::new(r"^/usr/lib/python(\d+)\.(\d+)(/|$)") {
|
||||||
if let Some(c) = re.captures(installed_path) {
|
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 Ok(re) = Regex::new(r"python(\d+)\.(\d+)") {
|
||||||
if let Some(c) = re.captures(sb) {
|
if let Some(c) = re.captures(sb) {
|
||||||
if let (Some(ma), Some(mi)) = (c.get(1), c.get(2)) {
|
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));
|
return Some((maj, min));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -580,7 +659,12 @@ fn collect_python_imports(src: &str) -> Vec<String> {
|
||||||
mods
|
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 text = String::from_utf8_lossy(bytes);
|
||||||
let imports = collect_python_imports(&text);
|
let imports = collect_python_imports(&text);
|
||||||
if imports.is_empty() {
|
if imports.is_empty() {
|
||||||
|
|
@ -591,11 +675,21 @@ fn process_python(bytes: &[u8], installed_path: &str, version: (u8, u8), opts: &
|
||||||
for m in imports {
|
for m in imports {
|
||||||
let py = format!("{}.py", m);
|
let py = format!("{}.py", m);
|
||||||
let so = format!("{}.so", m);
|
let so = format!("{}.so", m);
|
||||||
if !base_names.contains(&py) { base_names.push(py); }
|
if !base_names.contains(&py) {
|
||||||
if !base_names.contains(&so) { base_names.push(so); }
|
base_names.push(py);
|
||||||
|
}
|
||||||
|
if !base_names.contains(&so) {
|
||||||
|
base_names.push(so);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let run_paths = compute_python_runpaths(version, opts);
|
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 ---
|
// --- SMF helpers ---
|
||||||
|
|
@ -608,7 +702,9 @@ fn extract_smf_execs(bytes: &[u8]) -> Vec<String> {
|
||||||
let m = cap.get(1).or_else(|| cap.get(2));
|
let m = cap.get(1).or_else(|| cap.get(2));
|
||||||
if let Some(v) = m {
|
if let Some(v) = m {
|
||||||
let val = v.as_str().to_string();
|
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 => {
|
x => {
|
||||||
return Err(DigestError::UnknownAlgorithm {
|
return Err(DigestError::UnknownAlgorithm {
|
||||||
algorithm: x.to_string(),
|
algorithm: x.to_string(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -152,7 +152,9 @@ pub enum DigestError {
|
||||||
#[error("hashing algorithm {algorithm:?} is not known by this library")]
|
#[error("hashing algorithm {algorithm:?} is not known by this library")]
|
||||||
#[diagnostic(
|
#[diagnostic(
|
||||||
code(ips::digest_error::unknown_algorithm),
|
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 },
|
UnknownAlgorithm { algorithm: String },
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::actions::executors::{apply_manifest, ApplyOptions, InstallerError};
|
|
||||||
use crate::actions::Manifest;
|
use crate::actions::Manifest;
|
||||||
|
use crate::actions::executors::{ApplyOptions, InstallerError, apply_manifest};
|
||||||
use crate::solver::InstallPlan;
|
use crate::solver::InstallPlan;
|
||||||
|
|
||||||
/// ActionPlan represents a merged list of actions across all manifests
|
/// ActionPlan represents a merged list of actions across all manifests
|
||||||
|
|
@ -50,12 +50,20 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn build_and_apply_empty_plan_dry_run() {
|
fn build_and_apply_empty_plan_dry_run() {
|
||||||
// Empty install plan should produce empty action plan and apply should be no-op.
|
// 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);
|
let ap = ActionPlan::from_install_plan(&plan);
|
||||||
assert!(ap.manifest.directories.is_empty());
|
assert!(ap.manifest.directories.is_empty());
|
||||||
assert!(ap.manifest.files.is_empty());
|
assert!(ap.manifest.files.is_empty());
|
||||||
assert!(ap.manifest.links.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");
|
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.
|
// Even if root doesn't exist, dry_run should not perform any IO and succeed.
|
||||||
let res = ap.apply(root, &opts);
|
let res = ap.apply(root, &opts);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
use crate::actions::{Manifest};
|
use crate::actions::Manifest;
|
||||||
use crate::fmri::Fmri;
|
use crate::fmri::Fmri;
|
||||||
use crate::repository::catalog::{CatalogManager, CatalogPart, PackageVersionEntry};
|
use crate::repository::catalog::{CatalogManager, CatalogPart, PackageVersionEntry};
|
||||||
|
use lz4::{Decoder as Lz4Decoder, EncoderBuilder as Lz4EncoderBuilder};
|
||||||
use miette::Diagnostic;
|
use miette::Diagnostic;
|
||||||
use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
|
use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::{Cursor, Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{info, warn, trace};
|
use tracing::{info, trace, warn};
|
||||||
use std::io::{Cursor, Read, Write};
|
|
||||||
use lz4::{Decoder as Lz4Decoder, EncoderBuilder as Lz4EncoderBuilder};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Table definition for the catalog database
|
/// Table definition for the catalog database
|
||||||
/// Key: stem@version
|
/// 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())
|
/// Value: version string as bytes (same format as Fmri::version())
|
||||||
pub const INCORPORATE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("incorporate");
|
pub const INCORPORATE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("incorporate");
|
||||||
|
|
||||||
|
|
||||||
/// Errors that can occur when working with the image catalog
|
/// Errors that can occur when working with the image catalog
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
pub enum CatalogError {
|
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
|
// Internal helpers for (de)compressing manifest JSON payloads stored in redb
|
||||||
fn is_likely_json(bytes: &[u8]) -> bool {
|
fn is_likely_json(bytes: &[u8]) -> bool {
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < bytes.len() && matches!(bytes[i], b' ' | b'\n' | b'\r' | b'\t') { i += 1; }
|
while i < bytes.len() && matches!(bytes[i], b' ' | b'\n' | b'\r' | b'\t') {
|
||||||
if i >= bytes.len() { return false; }
|
i += 1;
|
||||||
|
}
|
||||||
|
if i >= bytes.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
matches!(bytes[i], b'{' | b'[')
|
matches!(bytes[i], b'{' | b'[')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,35 +147,56 @@ impl ImageCatalog {
|
||||||
// Determine which table to dump and open the appropriate database
|
// Determine which table to dump and open the appropriate database
|
||||||
match table_name {
|
match table_name {
|
||||||
"catalog" => {
|
"catalog" => {
|
||||||
let db = Database::open(&self.db_path)
|
let db = Database::open(&self.db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", 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 tx = db.begin_read().map_err(|e| {
|
||||||
|
CatalogError::Database(format!("Failed to begin transaction: {}", e))
|
||||||
|
})?;
|
||||||
self.dump_catalog_table(&tx)?;
|
self.dump_catalog_table(&tx)?;
|
||||||
}
|
}
|
||||||
"obsoleted" => {
|
"obsoleted" => {
|
||||||
let db = Database::open(&self.obsoleted_db_path)
|
let db = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", 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 tx = db.begin_read().map_err(|e| {
|
||||||
|
CatalogError::Database(format!("Failed to begin transaction: {}", e))
|
||||||
|
})?;
|
||||||
self.dump_obsoleted_table(&tx)?;
|
self.dump_obsoleted_table(&tx)?;
|
||||||
}
|
}
|
||||||
"incorporate" => {
|
"incorporate" => {
|
||||||
let db = Database::open(&self.db_path)
|
let db = Database::open(&self.db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", 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 tx = db.begin_read().map_err(|e| {
|
||||||
|
CatalogError::Database(format!("Failed to begin transaction: {}", e))
|
||||||
|
})?;
|
||||||
// Simple dump of incorporate locks
|
// Simple dump of incorporate locks
|
||||||
if let Ok(table) = tx.open_table(INCORPORATE_TABLE) {
|
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)))? {
|
for entry in table.iter().map_err(|e| {
|
||||||
let (k, v) = entry.map_err(|e| CatalogError::Database(format!("Failed to read incorporate table entry: {}", 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 stem = k.value();
|
||||||
let ver = String::from_utf8_lossy(v.value());
|
let ver = String::from_utf8_lossy(v.value());
|
||||||
println!("{} -> {}", stem, ver);
|
println!("{} -> {}", stem, ver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err(CatalogError::Database(format!("Unknown table: {}", table_name))),
|
_ => {
|
||||||
|
return Err(CatalogError::Database(format!(
|
||||||
|
"Unknown table: {}",
|
||||||
|
table_name
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -181,17 +205,21 @@ impl ImageCatalog {
|
||||||
/// Dump the contents of all tables to stdout for debugging
|
/// Dump the contents of all tables to stdout for debugging
|
||||||
pub fn dump_all_tables(&self) -> Result<()> {
|
pub fn dump_all_tables(&self) -> Result<()> {
|
||||||
// Catalog DB
|
// Catalog DB
|
||||||
let db_cat = Database::open(&self.db_path)
|
let db_cat = Database::open(&self.db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||||
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)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
println!("=== CATALOG TABLE ===");
|
println!("=== CATALOG TABLE ===");
|
||||||
let _ = self.dump_catalog_table(&tx_cat);
|
let _ = self.dump_catalog_table(&tx_cat);
|
||||||
|
|
||||||
// Obsoleted DB
|
// Obsoleted DB
|
||||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||||
let tx_obs = db_obs.begin_read()
|
})?;
|
||||||
|
let tx_obs = db_obs
|
||||||
|
.begin_read()
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
println!("\n=== OBSOLETED TABLE ===");
|
println!("\n=== OBSOLETED TABLE ===");
|
||||||
let _ = self.dump_obsoleted_table(&tx_obs);
|
let _ = self.dump_obsoleted_table(&tx_obs);
|
||||||
|
|
@ -204,15 +232,24 @@ impl ImageCatalog {
|
||||||
match tx.open_table(CATALOG_TABLE) {
|
match tx.open_table(CATALOG_TABLE) {
|
||||||
Ok(table) => {
|
Ok(table) => {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? {
|
for entry_result in table.iter().map_err(|e| {
|
||||||
let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", 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();
|
let key_str = key.value();
|
||||||
|
|
||||||
// Try to deserialize the manifest (supports JSON or LZ4-compressed JSON)
|
// Try to deserialize the manifest (supports JSON or LZ4-compressed JSON)
|
||||||
match decode_manifest_bytes(value.value()) {
|
match decode_manifest_bytes(value.value()) {
|
||||||
Ok(manifest) => {
|
Ok(manifest) => {
|
||||||
// Extract the publisher from the FMRI attribute
|
// Extract the publisher from the FMRI attribute
|
||||||
let publisher = manifest.attributes.iter()
|
let publisher = manifest
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
.find(|attr| attr.key == "pkg.fmri")
|
.find(|attr| attr.key == "pkg.fmri")
|
||||||
.and_then(|attr| attr.values.get(0).cloned())
|
.and_then(|attr| attr.values.get(0).cloned())
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
@ -223,7 +260,7 @@ impl ImageCatalog {
|
||||||
println!(" Files: {}", manifest.files.len());
|
println!(" Files: {}", manifest.files.len());
|
||||||
println!(" Directories: {}", manifest.directories.len());
|
println!(" Directories: {}", manifest.directories.len());
|
||||||
println!(" Dependencies: {}", manifest.dependencies.len());
|
println!(" Dependencies: {}", manifest.dependencies.len());
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Key: {}", key_str);
|
println!("Key: {}", key_str);
|
||||||
println!(" Error deserializing manifest: {}", e);
|
println!(" Error deserializing manifest: {}", e);
|
||||||
|
|
@ -233,10 +270,13 @@ impl ImageCatalog {
|
||||||
}
|
}
|
||||||
println!("Total entries in catalog table: {}", count);
|
println!("Total entries in catalog table: {}", count);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error opening catalog table: {}", 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) {
|
match tx.open_table(OBSOLETED_TABLE) {
|
||||||
Ok(table) => {
|
Ok(table) => {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? {
|
for entry_result in table.iter().map_err(|e| {
|
||||||
let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", 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();
|
let key_str = key.value();
|
||||||
|
|
||||||
println!("Key: {}", key_str);
|
println!("Key: {}", key_str);
|
||||||
|
|
@ -255,27 +302,33 @@ impl ImageCatalog {
|
||||||
}
|
}
|
||||||
println!("Total entries in obsoleted table: {}", count);
|
println!("Total entries in obsoleted table: {}", count);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error opening obsoleted table: {}", 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
|
/// Get database statistics
|
||||||
pub fn get_db_stats(&self) -> Result<()> {
|
pub fn get_db_stats(&self) -> Result<()> {
|
||||||
// Open the catalog database
|
// Open the catalog database
|
||||||
let db_cat = Database::open(&self.db_path)
|
let db_cat = Database::open(&self.db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||||
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)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Open the obsoleted database
|
// Open the obsoleted database
|
||||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||||
let tx_obs = db_obs.begin_read()
|
})?;
|
||||||
|
let tx_obs = db_obs
|
||||||
|
.begin_read()
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Get table statistics
|
// Get table statistics
|
||||||
|
|
@ -284,23 +337,37 @@ impl ImageCatalog {
|
||||||
|
|
||||||
// Count catalog entries
|
// Count catalog entries
|
||||||
if let Ok(table) = tx_cat.open_table(CATALOG_TABLE) {
|
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)))? {
|
for result in table.iter().map_err(|e| {
|
||||||
let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", 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;
|
catalog_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count obsoleted entries (separate DB)
|
// Count obsoleted entries (separate DB)
|
||||||
if let Ok(table) = tx_obs.open_table(OBSOLETED_TABLE) {
|
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)))? {
|
for result in table.iter().map_err(|e| {
|
||||||
let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", 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;
|
obsoleted_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print statistics
|
// Print statistics
|
||||||
println!("Catalog database path: {}", self.db_path.display());
|
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!("Catalog directory: {}", self.catalog_dir.display());
|
||||||
println!("Table statistics:");
|
println!("Table statistics:");
|
||||||
println!(" Catalog table: {} entries", catalog_count);
|
println!(" Catalog table: {} entries", catalog_count);
|
||||||
|
|
@ -313,30 +380,43 @@ impl ImageCatalog {
|
||||||
/// Initialize the catalog database
|
/// Initialize the catalog database
|
||||||
pub fn init_db(&self) -> Result<()> {
|
pub fn init_db(&self) -> Result<()> {
|
||||||
// Ensure parent directories exist
|
// Ensure parent directories exist
|
||||||
if let Some(parent) = self.db_path.parent() { fs::create_dir_all(parent)?; }
|
if let Some(parent) = self.db_path.parent() {
|
||||||
if let Some(parent) = self.obsoleted_db_path.parent() { fs::create_dir_all(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
|
// Create/open catalog database and tables
|
||||||
let db_cat = Database::create(&self.db_path)
|
let db_cat = Database::create(&self.db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to create catalog database: {}", e)))?;
|
CatalogError::Database(format!("Failed to create catalog database: {}", e))
|
||||||
let tx_cat = db_cat.begin_write()
|
})?;
|
||||||
|
let tx_cat = db_cat
|
||||||
|
.begin_write()
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
tx_cat.open_table(CATALOG_TABLE)
|
tx_cat.open_table(CATALOG_TABLE).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to create catalog table: {}", 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.open_table(INCORPORATE_TABLE).map_err(|e| {
|
||||||
tx_cat.commit()
|
CatalogError::Database(format!("Failed to create incorporate table: {}", e))
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to commit catalog transaction: {}", e)))?;
|
})?;
|
||||||
|
tx_cat.commit().map_err(|e| {
|
||||||
|
CatalogError::Database(format!("Failed to commit catalog transaction: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Create/open obsoleted database and table
|
// Create/open obsoleted database and table
|
||||||
let db_obs = Database::create(&self.obsoleted_db_path)
|
let db_obs = Database::create(&self.obsoleted_db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to create obsoleted database: {}", e)))?;
|
CatalogError::Database(format!("Failed to create obsoleted database: {}", e))
|
||||||
let tx_obs = db_obs.begin_write()
|
})?;
|
||||||
|
let tx_obs = db_obs
|
||||||
|
.begin_write()
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
tx_obs.open_table(OBSOLETED_TABLE)
|
tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to create obsoleted table: {}", 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.commit().map_err(|e| {
|
||||||
|
CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -352,28 +432,37 @@ impl ImageCatalog {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the databases
|
// Open the databases
|
||||||
trace!("Opening databases at {:?} and {:?}", self.db_path, self.obsoleted_db_path);
|
trace!(
|
||||||
let db_cat = Database::open(&self.db_path)
|
"Opening databases at {:?} and {:?}",
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
self.db_path, self.obsoleted_db_path
|
||||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
);
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
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
|
// Begin writing transactions
|
||||||
trace!("Beginning write transactions");
|
trace!("Beginning write transactions");
|
||||||
let tx_cat = db_cat.begin_write()
|
let tx_cat = db_cat.begin_write().map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to begin catalog transaction: {}", 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_obs = db_obs.begin_write().map_err(|e| {
|
||||||
|
CatalogError::Database(format!("Failed to begin obsoleted transaction: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Open the catalog table
|
// Open the catalog table
|
||||||
trace!("Opening 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)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
||||||
|
|
||||||
// Open the obsoleted table
|
// Open the obsoleted table
|
||||||
trace!("Opening obsoleted table");
|
trace!("Opening obsoleted table");
|
||||||
let mut obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE)
|
let mut obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
CatalogError::Database(format!("Failed to open obsoleted table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Process each publisher
|
// Process each publisher
|
||||||
for publisher in publishers {
|
for publisher in publishers {
|
||||||
|
|
@ -383,28 +472,46 @@ impl ImageCatalog {
|
||||||
|
|
||||||
// Skip if the publisher catalog directory doesn't exist
|
// Skip if the publisher catalog directory doesn't exist
|
||||||
if !publisher_catalog_dir.exists() {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine where catalog parts live. Support both legacy nested layout
|
// Determine where catalog parts live. Support both legacy nested layout
|
||||||
// (publisher/<publisher>/catalog) and flat layout (directly under publisher dir).
|
// (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 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!("Creating catalog manager for publisher: {}", publisher);
|
||||||
trace!("Catalog parts directory: {:?}", catalog_parts_dir);
|
trace!("Catalog parts directory: {:?}", catalog_parts_dir);
|
||||||
|
|
||||||
// Check if the catalog parts directory exists (either layout)
|
// Check if the catalog parts directory exists (either layout)
|
||||||
if !catalog_parts_dir.exists() {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut catalog_manager = CatalogManager::new(catalog_parts_dir, publisher)
|
let mut catalog_manager =
|
||||||
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to create catalog manager: {}", e))))?;
|
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
|
// Get all catalog parts
|
||||||
trace!("Getting catalog parts for publisher: {}", publisher);
|
trace!("Getting catalog parts for publisher: {}", publisher);
|
||||||
|
|
@ -414,8 +521,12 @@ impl ImageCatalog {
|
||||||
// Load all catalog parts
|
// Load all catalog parts
|
||||||
for part_name in parts.keys() {
|
for part_name in parts.keys() {
|
||||||
trace!("Loading catalog part: {}", part_name);
|
trace!("Loading catalog part: {}", part_name);
|
||||||
catalog_manager.load_part(part_name)
|
catalog_manager.load_part(part_name).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to load catalog part: {}", 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
|
// New approach: Merge information across all catalog parts per stem@version, then process once
|
||||||
|
|
@ -425,7 +536,12 @@ impl ImageCatalog {
|
||||||
loaded_parts.push(part);
|
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
|
// Drop the tables to release the borrow on tx
|
||||||
|
|
@ -433,10 +549,12 @@ impl ImageCatalog {
|
||||||
drop(obsoleted_table);
|
drop(obsoleted_table);
|
||||||
|
|
||||||
// Commit the transactions
|
// Commit the transactions
|
||||||
tx_cat.commit()
|
tx_cat.commit().map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to commit catalog transaction: {}", 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_obs.commit().map_err(|e| {
|
||||||
|
CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
info!("Catalog built successfully");
|
info!("Catalog built successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -472,11 +590,18 @@ impl ImageCatalog {
|
||||||
|
|
||||||
// Process each package stem
|
// Process each package stem
|
||||||
for (stem, versions) in publisher_packages {
|
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
|
// Process each package version
|
||||||
for version_entry in versions {
|
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
|
// Create the FMRI
|
||||||
let version = if !version_entry.version.is_empty() {
|
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
|
// obsolete in an earlier part (present in obsoleted_table) and is NOT present
|
||||||
// in the catalog_table, skip importing it from this part.
|
// in the catalog_table, skip importing it from this part.
|
||||||
if !part_name.contains(".base") {
|
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 {
|
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 {
|
if was_obsoleted {
|
||||||
// Count as obsolete for progress accounting, even though we skip processing
|
// Count as obsolete for progress accounting, even though we skip processing
|
||||||
obsolete_count_incl_skipped += 1;
|
obsolete_count_incl_skipped += 1;
|
||||||
skipped_obsolete += 1;
|
skipped_obsolete += 1;
|
||||||
trace!(
|
trace!(
|
||||||
"Skipping {} from part {} because it is marked obsolete and not present in catalog",
|
"Skipping {} from part {} because it is marked obsolete and not present in catalog",
|
||||||
obsoleted_key,
|
obsoleted_key, part_name
|
||||||
part_name
|
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -523,11 +649,18 @@ impl ImageCatalog {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create or update the manifest
|
// 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
|
// Check if the package is obsolete
|
||||||
let is_obsolete = self.is_package_obsolete(&manifest);
|
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
|
// Serialize the manifest
|
||||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
||||||
|
|
@ -538,13 +671,23 @@ impl ImageCatalog {
|
||||||
let empty_bytes: &[u8] = &[0u8; 0];
|
let empty_bytes: &[u8] = &[0u8; 0];
|
||||||
obsoleted_table
|
obsoleted_table
|
||||||
.insert(obsoleted_key.as_str(), empty_bytes)
|
.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 {
|
} else {
|
||||||
// Store non-obsolete packages in the catalog table with stem@version as a key
|
// Store non-obsolete packages in the catalog table with stem@version as a key
|
||||||
let compressed = compress_json_lz4(&manifest_bytes)?;
|
let compressed = compress_json_lz4(&manifest_bytes)?;
|
||||||
catalog_table
|
catalog_table
|
||||||
.insert(catalog_key.as_str(), compressed.as_slice())
|
.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;
|
processed += 1;
|
||||||
|
|
@ -565,11 +708,7 @@ impl ImageCatalog {
|
||||||
// Final summary for this part/publisher
|
// Final summary for this part/publisher
|
||||||
info!(
|
info!(
|
||||||
"Finished import for publisher {}, part {}: {} versions processed ({} obsolete incl. skipped, {} skipped)",
|
"Finished import for publisher {}, part {}: {} versions processed ({} obsolete incl. skipped, {} skipped)",
|
||||||
publisher,
|
publisher, part_name, processed, obsolete_count_incl_skipped, skipped_obsolete
|
||||||
part_name,
|
|
||||||
processed,
|
|
||||||
obsolete_count_incl_skipped,
|
|
||||||
skipped_obsolete
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
trace!("No packages found for publisher: {}", publisher);
|
trace!("No packages found for publisher: {}", publisher);
|
||||||
|
|
@ -596,16 +735,19 @@ impl ImageCatalog {
|
||||||
for (stem, versions) in publisher_packages {
|
for (stem, versions) in publisher_packages {
|
||||||
let stem_map = merged.entry(stem.clone()).or_default();
|
let stem_map = merged.entry(stem.clone()).or_default();
|
||||||
for v in versions {
|
for v in versions {
|
||||||
let entry = stem_map
|
let entry =
|
||||||
.entry(v.version.clone())
|
stem_map
|
||||||
.or_insert(PackageVersionEntry {
|
.entry(v.version.clone())
|
||||||
version: v.version.clone(),
|
.or_insert(PackageVersionEntry {
|
||||||
actions: None,
|
version: v.version.clone(),
|
||||||
signature_sha1: None,
|
actions: None,
|
||||||
});
|
signature_sha1: None,
|
||||||
|
});
|
||||||
// Merge signature if not yet set
|
// Merge signature if not yet set
|
||||||
if entry.signature_sha1.is_none() {
|
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
|
// Merge actions, de-duplicating
|
||||||
if let Some(actions) = &v.actions {
|
if let Some(actions) = &v.actions {
|
||||||
|
|
@ -649,40 +791,55 @@ impl ImageCatalog {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build/update manifest with merged actions
|
// 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
|
// Obsolete decision based on merged actions in manifest
|
||||||
let is_obsolete = self.is_package_obsolete(&manifest);
|
let is_obsolete = self.is_package_obsolete(&manifest);
|
||||||
if is_obsolete { obsolete_count += 1; }
|
if is_obsolete {
|
||||||
|
obsolete_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Serialize and write
|
// Serialize and write
|
||||||
if is_obsolete {
|
if is_obsolete {
|
||||||
// Compute full FMRI for obsoleted key
|
// Compute full FMRI for obsoleted key
|
||||||
let version_obj = if !entry.version.is_empty() {
|
let version_obj = if !entry.version.is_empty() {
|
||||||
match crate::fmri::Version::parse(&entry.version) { Ok(v) => Some(v), Err(_) => None }
|
match crate::fmri::Version::parse(&entry.version) {
|
||||||
} else { None };
|
Ok(v) => Some(v),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let fmri = Fmri::with_publisher(publisher, stem, version_obj);
|
let fmri = Fmri::with_publisher(publisher, stem, version_obj);
|
||||||
let obsoleted_key = fmri.to_string();
|
let obsoleted_key = fmri.to_string();
|
||||||
let empty_bytes: &[u8] = &[0u8; 0];
|
let empty_bytes: &[u8] = &[0u8; 0];
|
||||||
obsoleted_table
|
obsoleted_table
|
||||||
.insert(obsoleted_key.as_str(), empty_bytes)
|
.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 {
|
} else {
|
||||||
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
||||||
let compressed = compress_json_lz4(&manifest_bytes)?;
|
let compressed = compress_json_lz4(&manifest_bytes)?;
|
||||||
catalog_table
|
catalog_table
|
||||||
.insert(catalog_key.as_str(), compressed.as_slice())
|
.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;
|
processed += 1;
|
||||||
if processed % progress_step == 0 {
|
if processed % progress_step == 0 {
|
||||||
info!(
|
info!(
|
||||||
"Import progress (publisher {}, merged): {}/{} versions processed ({} obsolete)",
|
"Import progress (publisher {}, merged): {}/{} versions processed ({} obsolete)",
|
||||||
publisher,
|
publisher, processed, total_versions, obsolete_count
|
||||||
processed,
|
|
||||||
total_versions,
|
|
||||||
obsolete_count
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -691,9 +848,7 @@ impl ImageCatalog {
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Finished merged import for publisher {}: {} versions processed ({} obsolete)",
|
"Finished merged import for publisher {}: {} versions processed ({} obsolete)",
|
||||||
publisher,
|
publisher, processed, obsolete_count
|
||||||
processed,
|
|
||||||
obsolete_count
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -728,11 +883,12 @@ impl ImageCatalog {
|
||||||
|
|
||||||
// Remove quotes if present
|
// Remove quotes if present
|
||||||
if value.starts_with('"') && value.ends_with('"') {
|
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
|
// 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 {
|
if let Some(index) = attr_index {
|
||||||
manifest.attributes[index].values = vec![value.to_string()];
|
manifest.attributes[index].values = vec![value.to_string()];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -758,10 +914,14 @@ impl ImageCatalog {
|
||||||
match k {
|
match k {
|
||||||
"type" => dep_type = v.to_string(),
|
"type" => dep_type = v.to_string(),
|
||||||
"predicate" => {
|
"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" => {
|
"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" => {
|
||||||
root_image = v.to_string();
|
root_image = v.to_string();
|
||||||
|
|
@ -792,9 +952,10 @@ impl ImageCatalog {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Map the FmriError to a CatalogError
|
// Map the FmriError to a CatalogError
|
||||||
return Err(CatalogError::Repository(
|
return Err(CatalogError::Repository(
|
||||||
crate::repository::RepositoryError::Other(
|
crate::repository::RepositoryError::Other(format!(
|
||||||
format!("Invalid version format: {}", e)
|
"Invalid version format: {}",
|
||||||
)
|
e
|
||||||
|
)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -812,7 +973,10 @@ impl ImageCatalog {
|
||||||
/// Ensure the manifest has the correct FMRI attribute
|
/// Ensure the manifest has the correct FMRI attribute
|
||||||
fn ensure_fmri_attribute(&self, manifest: &mut Manifest, fmri: &Fmri) {
|
fn ensure_fmri_attribute(&self, manifest: &mut Manifest, fmri: &Fmri) {
|
||||||
// Check if the manifest already has an FMRI attribute
|
// 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 not, add it
|
||||||
if !has_fmri {
|
if !has_fmri {
|
||||||
|
|
@ -833,30 +997,40 @@ impl ImageCatalog {
|
||||||
/// Query the catalog for packages matching a pattern
|
/// Query the catalog for packages matching a pattern
|
||||||
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
|
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
|
||||||
// Open the catalog database
|
// Open the catalog database
|
||||||
let db_cat = Database::open(&self.db_path)
|
let db_cat = Database::open(&self.db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||||
|
})?;
|
||||||
// Begin a read transaction
|
// 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)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Open the catalog table
|
// 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)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
||||||
|
|
||||||
// Open the obsoleted database
|
// Open the obsoleted database
|
||||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||||
let tx_obs = db_obs.begin_read()
|
})?;
|
||||||
|
let tx_obs = db_obs
|
||||||
|
.begin_read()
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE)
|
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
CatalogError::Database(format!("Failed to open obsoleted table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
// Process the catalog table (non-obsolete packages)
|
// Process the catalog table (non-obsolete packages)
|
||||||
// Iterate through all entries in the table
|
// 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)))? {
|
for entry_result in catalog_table.iter().map_err(|e| {
|
||||||
let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", 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();
|
let key_str = key.value();
|
||||||
|
|
||||||
// Skip if the key doesn't match the pattern
|
// Skip if the key doesn't match the pattern
|
||||||
|
|
@ -880,7 +1054,9 @@ impl ImageCatalog {
|
||||||
let manifest: Manifest = decode_manifest_bytes(value.value())?;
|
let manifest: Manifest = decode_manifest_bytes(value.value())?;
|
||||||
|
|
||||||
// Extract the publisher from the FMRI attribute
|
// Extract the publisher from the FMRI attribute
|
||||||
let publisher = manifest.attributes.iter()
|
let publisher = manifest
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
.find(|attr| attr.key == "pkg.fmri")
|
.find(|attr| attr.key == "pkg.fmri")
|
||||||
.map(|attr| {
|
.map(|attr| {
|
||||||
if let Some(fmri_str) = attr.values.get(0) {
|
if let Some(fmri_str) = attr.values.get(0) {
|
||||||
|
|
@ -918,8 +1094,12 @@ impl ImageCatalog {
|
||||||
|
|
||||||
// Process the obsoleted table (obsolete packages)
|
// Process the obsoleted table (obsolete packages)
|
||||||
// Iterate through all entries in the table
|
// 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)))? {
|
for entry_result in obsoleted_table.iter().map_err(|e| {
|
||||||
let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", 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();
|
let key_str = key.value();
|
||||||
|
|
||||||
// Skip if the key doesn't match the pattern
|
// Skip if the key doesn't match the pattern
|
||||||
|
|
@ -933,7 +1113,10 @@ impl ImageCatalog {
|
||||||
match Fmri::parse(key_str) {
|
match Fmri::parse(key_str) {
|
||||||
Ok(fmri) => {
|
Ok(fmri) => {
|
||||||
// Extract the publisher
|
// 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)
|
// Add to results (obsolete)
|
||||||
results.push(PackageInfo {
|
results.push(PackageInfo {
|
||||||
|
|
@ -941,9 +1124,12 @@ impl ImageCatalog {
|
||||||
obsolete: true,
|
obsolete: true,
|
||||||
publisher,
|
publisher,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
Err(e) => {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -955,14 +1141,17 @@ impl ImageCatalog {
|
||||||
/// Get a manifest from the catalog
|
/// Get a manifest from the catalog
|
||||||
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
|
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
|
||||||
// Open the catalog database
|
// Open the catalog database
|
||||||
let db_cat = Database::open(&self.db_path)
|
let db_cat = Database::open(&self.db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
CatalogError::Database(format!("Failed to open catalog database: {}", e))
|
||||||
|
})?;
|
||||||
// Begin a read transaction
|
// 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)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Open the catalog table
|
// 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)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
||||||
|
|
||||||
// Create the key for the catalog table (stem@version)
|
// Create the key for the catalog table (stem@version)
|
||||||
|
|
@ -974,12 +1163,15 @@ impl ImageCatalog {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not found in catalog DB, check obsoleted DB
|
// If not found in catalog DB, check obsoleted DB
|
||||||
let db_obs = Database::open(&self.obsoleted_db_path)
|
let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
CatalogError::Database(format!("Failed to open obsoleted database: {}", e))
|
||||||
let tx_obs = db_obs.begin_read()
|
})?;
|
||||||
|
let tx_obs = db_obs
|
||||||
|
.begin_read()
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE)
|
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
CatalogError::Database(format!("Failed to open obsoleted table: {}", e))
|
||||||
|
})?;
|
||||||
let obsoleted_key = fmri.to_string();
|
let obsoleted_key = fmri.to_string();
|
||||||
if let Ok(Some(_)) = obsoleted_table.get(obsoleted_key.as_str()) {
|
if let Ok(Some(_)) = obsoleted_table.get(obsoleted_key.as_str()) {
|
||||||
let mut manifest = Manifest::new();
|
let mut manifest = Manifest::new();
|
||||||
|
|
|
||||||
|
|
@ -83,22 +83,32 @@ impl InstalledPackages {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||||
|
|
||||||
// Begin a read transaction
|
// 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)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Open the installed table
|
// Open the installed table
|
||||||
match tx.open_table(INSTALLED_TABLE) {
|
match tx.open_table(INSTALLED_TABLE) {
|
||||||
Ok(table) => {
|
Ok(table) => {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for entry_result in table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? {
|
for entry_result in table.iter().map_err(|e| {
|
||||||
let (key, value) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", 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();
|
let key_str = key.value();
|
||||||
|
|
||||||
// Try to deserialize the manifest
|
// Try to deserialize the manifest
|
||||||
match serde_json::from_slice::<Manifest>(value.value()) {
|
match serde_json::from_slice::<Manifest>(value.value()) {
|
||||||
Ok(manifest) => {
|
Ok(manifest) => {
|
||||||
// Extract the publisher from the FMRI attribute
|
// Extract the publisher from the FMRI attribute
|
||||||
let publisher = manifest.attributes.iter()
|
let publisher = manifest
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
.find(|attr| attr.key == "pkg.fmri")
|
.find(|attr| attr.key == "pkg.fmri")
|
||||||
.and_then(|attr| attr.values.get(0).cloned())
|
.and_then(|attr| attr.values.get(0).cloned())
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
@ -109,7 +119,7 @@ impl InstalledPackages {
|
||||||
println!(" Files: {}", manifest.files.len());
|
println!(" Files: {}", manifest.files.len());
|
||||||
println!(" Directories: {}", manifest.directories.len());
|
println!(" Directories: {}", manifest.directories.len());
|
||||||
println!(" Dependencies: {}", manifest.dependencies.len());
|
println!(" Dependencies: {}", manifest.dependencies.len());
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Key: {}", key_str);
|
println!("Key: {}", key_str);
|
||||||
println!(" Error deserializing manifest: {}", e);
|
println!(" Error deserializing manifest: {}", e);
|
||||||
|
|
@ -119,10 +129,13 @@ impl InstalledPackages {
|
||||||
}
|
}
|
||||||
println!("Total entries in installed table: {}", count);
|
println!("Total entries in installed table: {}", count);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error opening installed table: {}", 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)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||||
|
|
||||||
// Begin a read transaction
|
// 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)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Get table statistics
|
// Get table statistics
|
||||||
|
|
@ -142,8 +156,15 @@ impl InstalledPackages {
|
||||||
|
|
||||||
// Count installed entries
|
// Count installed entries
|
||||||
if let Ok(table) = tx.open_table(INSTALLED_TABLE) {
|
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)))? {
|
for result in table.iter().map_err(|e| {
|
||||||
let _ = result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", 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;
|
installed_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,14 +190,17 @@ impl InstalledPackages {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to create database: {}", e)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to create database: {}", e)))?;
|
||||||
|
|
||||||
// Create tables
|
// Create tables
|
||||||
let tx = db.begin_write()
|
let tx = db
|
||||||
|
.begin_write()
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
tx.open_table(INSTALLED_TABLE)
|
tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to create installed table: {}", e)))?;
|
InstalledError::Database(format!("Failed to create installed table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
tx.commit()
|
tx.commit().map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
|
InstalledError::Database(format!("Failed to commit transaction: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +212,8 @@ impl InstalledPackages {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||||
|
|
||||||
// Begin a writing transaction
|
// 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)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Create the key (full FMRI including publisher)
|
// 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
|
// Use a block scope to ensure the table is dropped before committing the transaction
|
||||||
{
|
{
|
||||||
// Open the installed table
|
// Open the installed table
|
||||||
let mut installed_table = tx.open_table(INSTALLED_TABLE)
|
let mut installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Insert the package into the installed table
|
// Insert the package into the installed table
|
||||||
installed_table.insert(key.as_str(), manifest_bytes.as_slice())
|
installed_table
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to insert into installed table: {}", e)))?;
|
.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
|
// The table is dropped at the end of this block, releasing its borrow of tx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit the transaction
|
// Commit the transaction
|
||||||
tx.commit()
|
tx.commit().map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
|
InstalledError::Database(format!("Failed to commit transaction: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
info!("Added package to installed database: {}", key);
|
info!("Added package to installed database: {}", key);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -225,7 +258,8 @@ impl InstalledPackages {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||||
|
|
||||||
// Begin a writing transaction
|
// 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)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Create the key (full FMRI including publisher)
|
// 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
|
// Use a block scope to ensure the table is dropped before committing the transaction
|
||||||
{
|
{
|
||||||
// Open the installed table
|
// Open the installed table
|
||||||
let mut installed_table = tx.open_table(INSTALLED_TABLE)
|
let mut installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Check if the package exists
|
// Check if the package exists
|
||||||
if let Ok(None) = installed_table.get(key.as_str()) {
|
if let Ok(None) = installed_table.get(key.as_str()) {
|
||||||
|
|
@ -243,15 +278,17 @@ impl InstalledPackages {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the package from the installed table
|
// Remove the package from the installed table
|
||||||
installed_table.remove(key.as_str())
|
installed_table.remove(key.as_str()).map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to remove from installed table: {}", 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
|
// The table is dropped at the end of this block, releasing its borrow of tx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit the transaction
|
// Commit the transaction
|
||||||
tx.commit()
|
tx.commit().map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
|
InstalledError::Database(format!("Failed to commit transaction: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
info!("Removed package from installed database: {}", key);
|
info!("Removed package from installed database: {}", key);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -264,21 +301,30 @@ impl InstalledPackages {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||||
|
|
||||||
// Begin a read transaction
|
// 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)))?;
|
.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
|
// Use a block scope to ensure the table is dropped when no longer needed
|
||||||
let results = {
|
let results = {
|
||||||
// Open the installed table
|
// Open the installed table
|
||||||
let installed_table = tx.open_table(INSTALLED_TABLE)
|
let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
// Process the installed table
|
// Process the installed table
|
||||||
// Iterate through all entries in the 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)))? {
|
for entry_result in installed_table.iter().map_err(|e| {
|
||||||
let (key, _) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", 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();
|
let key_str = key.value();
|
||||||
|
|
||||||
// Skip if the key doesn't match the pattern
|
// Skip if the key doesn't match the pattern
|
||||||
|
|
@ -292,13 +338,13 @@ impl InstalledPackages {
|
||||||
let fmri = Fmri::from_str(key_str)?;
|
let fmri = Fmri::from_str(key_str)?;
|
||||||
|
|
||||||
// Get the publisher (handling the Option<String>)
|
// 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
|
// Add to results
|
||||||
results.push(InstalledPackageInfo {
|
results.push(InstalledPackageInfo { fmri, publisher });
|
||||||
fmri,
|
|
||||||
publisher,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results
|
results
|
||||||
|
|
@ -315,7 +361,8 @@ impl InstalledPackages {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||||
|
|
||||||
// Begin a read transaction
|
// 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)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Create the key (full FMRI including publisher)
|
// 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
|
// Use a block scope to ensure the table is dropped when no longer needed
|
||||||
let manifest_option = {
|
let manifest_option = {
|
||||||
// Open the installed table
|
// Open the installed table
|
||||||
let installed_table = tx.open_table(INSTALLED_TABLE)
|
let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Try to get the manifest from the installed table
|
// Try to get the manifest from the installed table
|
||||||
if let Ok(Some(bytes)) = installed_table.get(key.as_str()) {
|
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)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
|
||||||
|
|
||||||
// Begin a read transaction
|
// 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)))?;
|
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
|
||||||
|
|
||||||
// Create the key (full FMRI including publisher)
|
// 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
|
// Use a block scope to ensure the table is dropped when no longer needed
|
||||||
let is_installed = {
|
let is_installed = {
|
||||||
// Open the installed table
|
// Open the installed table
|
||||||
let installed_table = tx.open_table(INSTALLED_TABLE)
|
let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
|
InstalledError::Database(format!("Failed to open installed table: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Check if the package exists
|
// Check if the package exists
|
||||||
if let Ok(Some(_)) = installed_table.get(key.as_str()) {
|
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
|
// Verify that the package is in the results
|
||||||
assert_eq!(packages.len(), 1);
|
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");
|
assert_eq!(packages[0].publisher, "test");
|
||||||
|
|
||||||
// Get the manifest from the installed packages database
|
// Get the manifest from the installed packages database
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,18 @@ mod tests;
|
||||||
|
|
||||||
use miette::Diagnostic;
|
use miette::Diagnostic;
|
||||||
use properties::*;
|
use properties::*;
|
||||||
|
use redb::{Database, ReadableDatabase, ReadableTable};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use thiserror::Error;
|
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
|
// Export the catalog module
|
||||||
pub mod catalog;
|
pub mod catalog;
|
||||||
use catalog::{ImageCatalog, PackageInfo, INCORPORATE_TABLE};
|
use catalog::{INCORPORATE_TABLE, ImageCatalog, PackageInfo};
|
||||||
|
|
||||||
// Export the installed packages module
|
// Export the installed packages module
|
||||||
pub mod installed;
|
pub mod installed;
|
||||||
|
|
@ -150,7 +150,13 @@ impl Image {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a publisher to the 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
|
// Check if publisher already exists
|
||||||
if self.publishers.iter().any(|p| p.name == name) {
|
if self.publishers.iter().any(|p| p.name == name) {
|
||||||
// Update existing publisher
|
// Update existing publisher
|
||||||
|
|
@ -304,7 +310,10 @@ impl Image {
|
||||||
fs::create_dir_all(&metadata_dir).map_err(|e| {
|
fs::create_dir_all(&metadata_dir).map_err(|e| {
|
||||||
ImageError::IO(std::io::Error::new(
|
ImageError::IO(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
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| {
|
fs::create_dir_all(&manifest_dir).map_err(|e| {
|
||||||
ImageError::IO(std::io::Error::new(
|
ImageError::IO(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
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| {
|
fs::create_dir_all(&catalog_dir).map_err(|e| {
|
||||||
ImageError::IO(std::io::Error::new(
|
ImageError::IO(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
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
|
// Create the installed packages database
|
||||||
let installed = InstalledPackages::new(&db_path);
|
let installed = InstalledPackages::new(&db_path);
|
||||||
installed.init_db().map_err(|e| {
|
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
|
/// 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
|
// Precheck incorporation dependencies: fail if any stem already has a lock
|
||||||
for d in &manifest.dependencies {
|
for d in &manifest.dependencies {
|
||||||
if d.dependency_type == "incorporate" {
|
if d.dependency_type == "incorporate" {
|
||||||
|
|
@ -351,7 +373,8 @@ impl Image {
|
||||||
let stem = df.stem();
|
let stem = df.stem();
|
||||||
if let Some(_) = self.get_incorporated_release(stem)? {
|
if let Some(_) = self.get_incorporated_release(stem)? {
|
||||||
return Err(ImageError::Database(format!(
|
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
|
// Add to installed database
|
||||||
let installed = InstalledPackages::new(self.installed_db_path());
|
let installed = InstalledPackages::new(self.installed_db_path());
|
||||||
installed.add_package(fmri, manifest).map_err(|e| {
|
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
|
// Write incorporation locks for any incorporate dependencies
|
||||||
|
|
@ -385,23 +411,35 @@ impl Image {
|
||||||
pub fn uninstall_package(&self, fmri: &crate::fmri::Fmri) -> Result<()> {
|
pub fn uninstall_package(&self, fmri: &crate::fmri::Fmri) -> Result<()> {
|
||||||
let installed = InstalledPackages::new(self.installed_db_path());
|
let installed = InstalledPackages::new(self.installed_db_path());
|
||||||
installed.remove_package(fmri).map_err(|e| {
|
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
|
/// 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());
|
let installed = InstalledPackages::new(self.installed_db_path());
|
||||||
installed.query_packages(pattern).map_err(|e| {
|
installed
|
||||||
ImageError::Database(format!("Failed to query installed packages: {}", e))
|
.query_packages(pattern)
|
||||||
})
|
.map_err(|e| ImageError::Database(format!("Failed to query installed packages: {}", e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a manifest from the installed packages database
|
/// 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());
|
let installed = InstalledPackages::new(self.installed_db_path());
|
||||||
installed.get_manifest(fmri).map_err(|e| {
|
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:
|
/// and stored under a flattened path:
|
||||||
/// manifests/<publisher>/<encoded_stem>@<encoded_version>.p5m
|
/// manifests/<publisher>/<encoded_stem>@<encoded_version>.p5m
|
||||||
/// Missing publisher will fall back to the image default publisher, then "unknown".
|
/// 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
|
// Determine publisher name
|
||||||
let pub_name = if let Some(p) = &fmri.publisher {
|
let pub_name = if let Some(p) = &fmri.publisher {
|
||||||
p.clone()
|
p.clone()
|
||||||
|
|
@ -438,7 +480,9 @@ impl Image {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for b in s.bytes() {
|
for b in s.bytes() {
|
||||||
match b {
|
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('+'),
|
b' ' => out.push('+'),
|
||||||
_ => {
|
_ => {
|
||||||
out.push('%');
|
out.push('%');
|
||||||
|
|
@ -484,7 +528,11 @@ impl Image {
|
||||||
|
|
||||||
/// Initialize the catalog database
|
/// Initialize the catalog database
|
||||||
pub fn init_catalog_db(&self) -> Result<()> {
|
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| {
|
catalog.init_db().map_err(|e| {
|
||||||
ImageError::Database(format!("Failed to initialize catalog database: {}", e))
|
ImageError::Database(format!("Failed to initialize catalog database: {}", e))
|
||||||
})
|
})
|
||||||
|
|
@ -526,7 +574,8 @@ impl Image {
|
||||||
self.publishers.iter().collect()
|
self.publishers.iter().collect()
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, filter publishers by name
|
// Otherwise, filter publishers by name
|
||||||
self.publishers.iter()
|
self.publishers
|
||||||
|
.iter()
|
||||||
.filter(|p| publishers.contains(&p.name))
|
.filter(|p| publishers.contains(&p.name))
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
@ -541,19 +590,25 @@ impl Image {
|
||||||
for publisher in &publishers_to_refresh {
|
for publisher in &publishers_to_refresh {
|
||||||
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
|
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
|
||||||
if publisher_catalog_dir.exists() {
|
if publisher_catalog_dir.exists() {
|
||||||
fs::remove_dir_all(&publisher_catalog_dir)
|
fs::remove_dir_all(&publisher_catalog_dir).map_err(|e| {
|
||||||
.map_err(|e| ImageError::IO(std::io::Error::new(
|
ImageError::IO(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
std::io::ErrorKind::Other,
|
||||||
format!("Failed to remove catalog directory for publisher {}: {}",
|
format!(
|
||||||
publisher.name, e)
|
"Failed to remove catalog directory for publisher {}: {}",
|
||||||
)))?;
|
publisher.name, e
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
fs::create_dir_all(&publisher_catalog_dir)
|
fs::create_dir_all(&publisher_catalog_dir).map_err(|e| {
|
||||||
.map_err(|e| ImageError::IO(std::io::Error::new(
|
ImageError::IO(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
std::io::ErrorKind::Other,
|
||||||
format!("Failed to create catalog directory for publisher {}: {}",
|
format!(
|
||||||
publisher.name, e)
|
"Failed to create catalog directory for publisher {}: {}",
|
||||||
)))?;
|
publisher.name, e
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -574,23 +629,29 @@ impl Image {
|
||||||
self.init_catalog_db()?;
|
self.init_catalog_db()?;
|
||||||
|
|
||||||
// Get publisher names
|
// Get publisher names
|
||||||
let publisher_names: Vec<String> = self.publishers.iter()
|
let publisher_names: Vec<String> = self.publishers.iter().map(|p| p.name.clone()).collect();
|
||||||
.map(|p| p.name.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Create the catalog and build it
|
// Create the catalog and build it
|
||||||
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path());
|
let catalog = ImageCatalog::new(
|
||||||
catalog.build_catalog(&publisher_names).map_err(|e| {
|
self.catalog_dir(),
|
||||||
ImageError::Database(format!("Failed to build catalog: {}", e))
|
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
|
/// Query the catalog for packages matching a pattern
|
||||||
pub fn query_catalog(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
|
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());
|
let catalog = ImageCatalog::new(
|
||||||
catalog.query_packages(pattern).map_err(|e| {
|
self.catalog_dir(),
|
||||||
ImageError::Database(format!("Failed to query catalog: {}", e))
|
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.
|
/// 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>> {
|
pub fn get_incorporated_release(&self, stem: &str) -> Result<Option<String>> {
|
||||||
let db = Database::open(self.catalog_db_path())
|
let db = Database::open(self.catalog_db_path())
|
||||||
.map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?;
|
.map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||||
let tx = db.begin_read()
|
let tx = db.begin_read().map_err(|e| {
|
||||||
.map_err(|e| ImageError::Database(format!("Failed to begin read transaction: {}", e)))?;
|
ImageError::Database(format!("Failed to begin read transaction: {}", e))
|
||||||
|
})?;
|
||||||
match tx.open_table(INCORPORATE_TABLE) {
|
match tx.open_table(INCORPORATE_TABLE) {
|
||||||
Ok(table) => {
|
Ok(table) => match table.get(stem) {
|
||||||
match table.get(stem) {
|
Ok(Some(val)) => Ok(Some(String::from_utf8_lossy(val.value()).to_string())),
|
||||||
Ok(Some(val)) => Ok(Some(String::from_utf8_lossy(val.value()).to_string())),
|
Ok(None) => Ok(None),
|
||||||
Ok(None) => Ok(None),
|
Err(e) => Err(ImageError::Database(format!(
|
||||||
Err(e) => Err(ImageError::Database(format!("Failed to read incorporate lock: {}", e))),
|
"Failed to read incorporate lock: {}",
|
||||||
}
|
e
|
||||||
}
|
))),
|
||||||
|
},
|
||||||
Err(_) => Ok(None),
|
Err(_) => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -617,26 +680,39 @@ impl Image {
|
||||||
pub fn add_incorporation_lock(&self, stem: &str, release: &str) -> Result<()> {
|
pub fn add_incorporation_lock(&self, stem: &str, release: &str) -> Result<()> {
|
||||||
let db = Database::open(self.catalog_db_path())
|
let db = Database::open(self.catalog_db_path())
|
||||||
.map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?;
|
.map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?;
|
||||||
let tx = db.begin_write()
|
let tx = db.begin_write().map_err(|e| {
|
||||||
.map_err(|e| ImageError::Database(format!("Failed to begin write transaction: {}", e)))?;
|
ImageError::Database(format!("Failed to begin write transaction: {}", e))
|
||||||
|
})?;
|
||||||
{
|
{
|
||||||
let mut table = tx.open_table(INCORPORATE_TABLE)
|
let mut table = tx.open_table(INCORPORATE_TABLE).map_err(|e| {
|
||||||
.map_err(|e| ImageError::Database(format!("Failed to open incorporate table: {}", e)))?;
|
ImageError::Database(format!("Failed to open incorporate table: {}", e))
|
||||||
|
})?;
|
||||||
if let Ok(Some(_)) = table.get(stem) {
|
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())
|
table.insert(stem, release.as_bytes()).map_err(|e| {
|
||||||
.map_err(|e| ImageError::Database(format!("Failed to insert incorporate lock: {}", e)))?;
|
ImageError::Database(format!("Failed to insert incorporate lock: {}", e))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
tx.commit()
|
tx.commit().map_err(|e| {
|
||||||
.map_err(|e| ImageError::Database(format!("Failed to commit incorporate lock: {}", e)))?
|
ImageError::Database(format!("Failed to commit incorporate lock: {}", e))
|
||||||
;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a manifest from the catalog
|
/// Get a manifest from the catalog
|
||||||
pub fn get_manifest_from_catalog(&self, fmri: &crate::fmri::Fmri) -> Result<Option<crate::actions::Manifest>> {
|
pub fn get_manifest_from_catalog(
|
||||||
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path());
|
&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| {
|
catalog.get_manifest(fmri).map_err(|e| {
|
||||||
ImageError::Database(format!("Failed to get manifest from catalog: {}", 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
|
/// This bypasses the local catalog database and retrieves the full manifest from
|
||||||
/// the configured publisher origin (REST for http/https origins; File backend for
|
/// the configured publisher origin (REST for http/https origins; File backend for
|
||||||
/// file:// origins). A versioned FMRI is required.
|
/// 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
|
// Determine publisher: use FMRI's publisher if present, otherwise default publisher
|
||||||
let publisher_name = if let Some(p) = &fmri.publisher {
|
let publisher_name = if let Some(p) = &fmri.publisher {
|
||||||
p.clone()
|
p.clone()
|
||||||
|
|
@ -671,13 +750,15 @@ impl Image {
|
||||||
let path_str = origin.trim_start_matches("file://");
|
let path_str = origin.trim_start_matches("file://");
|
||||||
let path = PathBuf::from(path_str);
|
let path = PathBuf::from(path_str);
|
||||||
let mut repo = FileBackend::open(&path)?;
|
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 {
|
} else {
|
||||||
let mut repo = RestBackend::open(origin)?;
|
let mut repo = RestBackend::open(origin)?;
|
||||||
// Optionally set a per-publisher cache directory (used by other REST ops)
|
// Optionally set a per-publisher cache directory (used by other REST ops)
|
||||||
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
|
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
|
||||||
repo.set_local_cache_path(&publisher_catalog_dir)?;
|
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());
|
println!("Catalog dir: {:?}", image.catalog_dir());
|
||||||
|
|
||||||
// Add a publisher
|
// 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
|
// Print the publishers
|
||||||
println!("Publishers: {:?}", image.publishers());
|
println!("Publishers: {:?}", image.publishers());
|
||||||
|
|
@ -59,7 +61,10 @@ fn test_catalog_methods() {
|
||||||
"updates": {},
|
"updates": {},
|
||||||
"version": 1
|
"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);
|
println!("catalog.attrs content: {}", attrs_content);
|
||||||
fs::write(publisher_dir.join("catalog.attrs"), attrs_content).unwrap();
|
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);
|
println!("base catalog part content: {}", base_content);
|
||||||
fs::write(publisher_dir.join("base"), base_content).unwrap();
|
fs::write(publisher_dir.join("base"), base_content).unwrap();
|
||||||
|
|
||||||
// Verify that the files were written correctly
|
// Verify that the files were written correctly
|
||||||
println!("Checking if catalog.attrs exists: {}", publisher_dir.join("catalog.attrs").exists());
|
println!(
|
||||||
println!("Checking if base catalog part exists: {}", publisher_dir.join("base").exists());
|
"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
|
// Build the catalog
|
||||||
println!("Building catalog...");
|
println!("Building catalog...");
|
||||||
|
|
@ -109,7 +123,7 @@ fn test_catalog_methods() {
|
||||||
Ok(pkgs) => {
|
Ok(pkgs) => {
|
||||||
println!("Found {} packages", pkgs.len());
|
println!("Found {} packages", pkgs.len());
|
||||||
pkgs
|
pkgs
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Failed to query catalog: {:?}", e);
|
println!("Failed to query catalog: {:?}", e);
|
||||||
panic!("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
|
// 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
|
// 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
|
// Verify that one package is not marked as obsolete
|
||||||
let non_obsolete_packages: Vec<_> = packages.iter().filter(|p| !p.obsolete).collect();
|
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();
|
let mut image = Image::create_image(&image_path, ImageType::Full).unwrap();
|
||||||
|
|
||||||
// Add two publishers
|
// Add two publishers
|
||||||
image.add_publisher("test1", "http://example.com/repo1", vec![], true).unwrap();
|
image
|
||||||
image.add_publisher("test2", "http://example.com/repo2", vec![], false).unwrap();
|
.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
|
// Create the catalog directory structure for both publishers
|
||||||
let catalog_dir = image.catalog_dir();
|
let catalog_dir = image.catalog_dir();
|
||||||
|
|
@ -177,8 +198,16 @@ fn test_refresh_catalogs_directory_clearing() {
|
||||||
// Create marker files in both publisher directories
|
// Create marker files in both publisher directories
|
||||||
let marker_file1 = publisher1_dir.join("marker");
|
let marker_file1 = publisher1_dir.join("marker");
|
||||||
let marker_file2 = publisher2_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(
|
||||||
fs::write(&marker_file2, "This file should be removed during full refresh").unwrap();
|
&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_file1.exists());
|
||||||
assert!(marker_file2.exists());
|
assert!(marker_file2.exists());
|
||||||
|
|
||||||
|
|
@ -195,7 +224,11 @@ fn test_refresh_catalogs_directory_clearing() {
|
||||||
assert!(marker_file2.exists());
|
assert!(marker_file2.exists());
|
||||||
|
|
||||||
// Create a new marker file for publisher1
|
// 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());
|
assert!(marker_file1.exists());
|
||||||
|
|
||||||
// Directly test the directory clearing functionality for all publishers
|
// Directly test the directory clearing functionality for all publishers
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@
|
||||||
|
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
|
pub mod api;
|
||||||
|
pub mod depend;
|
||||||
pub mod digest;
|
pub mod digest;
|
||||||
pub mod fmri;
|
pub mod fmri;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
pub mod payload;
|
pub mod payload;
|
||||||
pub mod repository;
|
|
||||||
pub mod publisher;
|
pub mod publisher;
|
||||||
pub mod transformer;
|
pub mod repository;
|
||||||
pub mod solver;
|
pub mod solver;
|
||||||
pub mod depend;
|
|
||||||
pub mod api;
|
|
||||||
mod test_json_manifest;
|
mod test_json_manifest;
|
||||||
|
pub mod transformer;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod publisher_tests;
|
mod publisher_tests;
|
||||||
|
|
@ -69,91 +69,101 @@ set name=pkg.summary value=\"'XZ Utils - loss-less file compression application
|
||||||
);
|
);
|
||||||
|
|
||||||
let test_results = vec![
|
let test_results = vec![
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("pkg.fmri"),
|
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(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("com.oracle.info.name"),
|
key: String::from("com.oracle.info.name"),
|
||||||
values: vec![String::from("nginx"), String::from("test")],
|
values: vec![String::from("nginx"), String::from("test")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("userland.info.git-remote"),
|
key: String::from("userland.info.git-remote"),
|
||||||
values: vec![String::from("git://github.com/OpenIndiana/oi-userland.git")],
|
values: vec![String::from("git://github.com/OpenIndiana/oi-userland.git")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("userland.info.git-branch"),
|
key: String::from("userland.info.git-branch"),
|
||||||
values: vec![String::from("HEAD")],
|
values: vec![String::from("HEAD")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("userland.info.git-rev"),
|
key: String::from("userland.info.git-rev"),
|
||||||
values: vec![String::from("1665491ba61bd494bf73e2916cd2250f3024260e")],
|
values: vec![String::from("1665491ba61bd494bf73e2916cd2250f3024260e")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("pkg.summary"),
|
key: String::from("pkg.summary"),
|
||||||
values: vec![String::from("Nginx Webserver")],
|
values: vec![String::from("Nginx Webserver")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("info.classification"),
|
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(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("info.upstream-url"),
|
key: String::from("info.upstream-url"),
|
||||||
values: vec![String::from("http://nginx.net/")],
|
values: vec![String::from("http://nginx.net/")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("info.source-url"),
|
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(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("org.opensolaris.consolidation"),
|
key: String::from("org.opensolaris.consolidation"),
|
||||||
values: vec![String::from("userland")],
|
values: vec![String::from("userland")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("com.oracle.info.version"),
|
key: String::from("com.oracle.info.version"),
|
||||||
values: vec![String::from("1.18.0")],
|
values: vec![String::from("1.18.0")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("pkg.summary"),
|
key: String::from("pkg.summary"),
|
||||||
values: vec![String::from("provided mouse accessibility enhancements")],
|
values: vec![String::from("provided mouse accessibility enhancements")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("info.upstream"),
|
key: String::from("info.upstream"),
|
||||||
values: vec![String::from("X.Org Foundation")],
|
values: vec![String::from("X.Org Foundation")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("pkg.description"),
|
key: String::from("pkg.description"),
|
||||||
values: vec![String::from("Latvian language support's extra files")],
|
values: vec![String::from("Latvian language support's extra files")],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("variant.arch"),
|
key: String::from("variant.arch"),
|
||||||
values: vec![String::from("i386")],
|
values: vec![String::from("i386")],
|
||||||
properties: optional_hash,
|
properties: optional_hash,
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("info.source-url"),
|
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(),
|
properties: HashMap::new(),
|
||||||
},
|
},
|
||||||
Attr{
|
Attr {
|
||||||
key: String::from("pkg.summary"),
|
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(),
|
properties: HashMap::new(),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let res = Manifest::parse_string(manifest_string);
|
let res = Manifest::parse_string(manifest_string);
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@
|
||||||
// MPL was not distributed with this file, You can
|
// MPL was not distributed with this file, You can
|
||||||
// obtain one at https://mozilla.org/MPL/2.0/.
|
// obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use miette::Diagnostic;
|
use miette::Diagnostic;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::actions::{File as FileAction, Manifest, Transform as TransformAction};
|
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::file_backend::{FileBackend, Transaction};
|
||||||
|
use crate::repository::{ReadableRepository, RepositoryError, WritableRepository};
|
||||||
use crate::transformer;
|
use crate::transformer;
|
||||||
|
|
||||||
/// Error type for high-level publishing operations
|
/// Error type for high-level publishing operations
|
||||||
|
|
@ -30,7 +30,10 @@ pub enum PublisherError {
|
||||||
Io(String),
|
Io(String),
|
||||||
|
|
||||||
#[error("invalid root path: {0}")]
|
#[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),
|
InvalidRoot(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +54,12 @@ impl PublisherClient {
|
||||||
/// Open an existing repository located at `path` with a selected `publisher`.
|
/// 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> {
|
pub fn open<P: AsRef<Path>>(path: P, publisher: impl Into<String>) -> Result<Self> {
|
||||||
let backend = FileBackend::open(path)?;
|
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.
|
/// 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()));
|
return Err(PublisherError::InvalidRoot(root.display().to_string()));
|
||||||
}
|
}
|
||||||
let mut manifest = Manifest::new();
|
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
|
// Ensure a transaction is open
|
||||||
if self.tx.is_none() {
|
if self.tx.is_none() {
|
||||||
self.open_transaction()?;
|
self.open_transaction()?;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ mod tests {
|
||||||
let repo_path = tmp.path().to_path_buf();
|
let repo_path = tmp.path().to_path_buf();
|
||||||
|
|
||||||
// Initialize repository
|
// 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");
|
backend.add_publisher("test").expect("add publisher");
|
||||||
|
|
||||||
// Prepare a prototype directory with a nested file
|
// Prepare a prototype directory with a nested file
|
||||||
|
|
@ -36,16 +37,27 @@ mod tests {
|
||||||
// Use PublisherClient to publish
|
// Use PublisherClient to publish
|
||||||
let mut client = PublisherClient::open(&repo_path, "test").expect("open client");
|
let mut client = PublisherClient::open(&repo_path, "test").expect("open client");
|
||||||
client.open_transaction().expect("open tx");
|
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");
|
client.publish(manifest, true).expect("publish");
|
||||||
|
|
||||||
// Verify the manifest exists at the default path for unknown version
|
// Verify the manifest exists at the default path for unknown version
|
||||||
let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest");
|
let manifest_path =
|
||||||
assert!(manifest_path.exists(), "manifest not found at {}", manifest_path.display());
|
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
|
// Verify at least one file was stored under publisher/test/file
|
||||||
let file_root = repo_path.join("publisher").join("test").join("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;
|
let mut any_file = false;
|
||||||
if let Ok(entries) = fs::read_dir(&file_root) {
|
if let Ok(entries) = fs::read_dir(&file_root) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
|
|
@ -62,14 +74,15 @@ mod tests {
|
||||||
} else if path.is_file() {
|
} else if path.is_file() {
|
||||||
any_file = true;
|
any_file = true;
|
||||||
}
|
}
|
||||||
if any_file { break; }
|
if any_file {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert!(any_file, "no stored file found in file store");
|
assert!(any_file, "no stored file found in file store");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod transform_rule_integration_tests {
|
mod transform_rule_integration_tests {
|
||||||
use crate::actions::Manifest;
|
use crate::actions::Manifest;
|
||||||
|
|
@ -85,7 +98,8 @@ mod transform_rule_integration_tests {
|
||||||
// Setup repository and publisher
|
// Setup repository and publisher
|
||||||
let tmp = TempDir::new().expect("tempdir");
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
let repo_path = tmp.path().to_path_buf();
|
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");
|
backend.add_publisher("test").expect("add publisher");
|
||||||
|
|
||||||
// Prototype directory with a file
|
// Prototype directory with a file
|
||||||
|
|
@ -102,18 +116,33 @@ mod transform_rule_integration_tests {
|
||||||
|
|
||||||
// Use PublisherClient to load rules, build manifest and publish
|
// Use PublisherClient to load rules, build manifest and publish
|
||||||
let mut client = PublisherClient::open(&repo_path, "test").expect("open client");
|
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");
|
assert!(loaded >= 1, "expected at least one rule loaded");
|
||||||
client.open_transaction().expect("open tx");
|
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");
|
client.publish(manifest, false).expect("publish");
|
||||||
|
|
||||||
// Read stored manifest and verify attribute
|
// Read stored manifest and verify attribute
|
||||||
let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest");
|
let manifest_path =
|
||||||
assert!(manifest_path.exists(), "manifest missing: {}", manifest_path.display());
|
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 json = fs::read_to_string(&manifest_path).expect("read manifest");
|
||||||
let parsed: Manifest = serde_json::from_str(&json).expect("parse manifest json");
|
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"));
|
let has_summary = parsed
|
||||||
assert!(has_summary, "pkg.summary attribute added via rules not found");
|
.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<()> {
|
fn atomic_write_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
|
||||||
let parent = path.parent().unwrap_or(Path::new("."));
|
let parent = path.parent().unwrap_or(Path::new("."));
|
||||||
fs::create_dir_all(parent)
|
fs::create_dir_all(parent).map_err(|e| RepositoryError::DirectoryCreateError {
|
||||||
.map_err(|e| RepositoryError::DirectoryCreateError { path: parent.to_path_buf(), source: e })?;
|
path: parent.to_path_buf(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
|
|
||||||
let tmp: PathBuf = path.with_extension("tmp");
|
let tmp: PathBuf = path.with_extension("tmp");
|
||||||
{
|
{
|
||||||
let mut f = std::fs::File::create(&tmp)
|
let mut f = std::fs::File::create(&tmp).map_err(|e| RepositoryError::FileWriteError {
|
||||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
path: tmp.clone(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
f.write_all(bytes)
|
f.write_all(bytes)
|
||||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
.map_err(|e| RepositoryError::FileWriteError {
|
||||||
f.flush()
|
path: tmp.clone(),
|
||||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
source: e,
|
||||||
|
})?;
|
||||||
|
f.flush().map_err(|e| RepositoryError::FileWriteError {
|
||||||
|
path: tmp.clone(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
fs::rename(&tmp, path)
|
fs::rename(&tmp, path).map_err(|e| RepositoryError::FileWriteError {
|
||||||
.map_err(|e| RepositoryError::FileWriteError { path: path.to_path_buf(), source: e })?
|
path: path.to_path_buf(),
|
||||||
;
|
source: e,
|
||||||
|
})?;
|
||||||
Ok(())
|
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> {
|
pub(crate) fn write_catalog_attrs(path: &Path, attrs: &mut CatalogAttrs) -> Result<String> {
|
||||||
// Compute signature over content without _SIGNATURE
|
// Compute signature over content without _SIGNATURE
|
||||||
attrs.signature = None;
|
attrs.signature = None;
|
||||||
let bytes_without_sig = serde_json::to_vec(&attrs)
|
let bytes_without_sig = serde_json::to_vec(&attrs).map_err(|e| {
|
||||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)))?;
|
RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e))
|
||||||
|
})?;
|
||||||
let sig = sha1_hex(&bytes_without_sig);
|
let sig = sha1_hex(&bytes_without_sig);
|
||||||
let mut sig_map = std::collections::HashMap::new();
|
let mut sig_map = std::collections::HashMap::new();
|
||||||
sig_map.insert("sha-1".to_string(), sig);
|
sig_map.insert("sha-1".to_string(), sig);
|
||||||
attrs.signature = Some(sig_map);
|
attrs.signature = Some(sig_map);
|
||||||
|
|
||||||
let final_bytes = serde_json::to_vec(&attrs)
|
let final_bytes = serde_json::to_vec(&attrs).map_err(|e| {
|
||||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)))?;
|
RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e))
|
||||||
|
})?;
|
||||||
debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog.attrs");
|
debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog.attrs");
|
||||||
atomic_write_bytes(path, &final_bytes)?;
|
atomic_write_bytes(path, &final_bytes)?;
|
||||||
// safe to unwrap as signature was just inserted
|
// 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))]
|
#[instrument(level = "debug", skip(part))]
|
||||||
pub(crate) fn write_catalog_part(path: &Path, part: &mut CatalogPart) -> Result<String> {
|
pub(crate) fn write_catalog_part(path: &Path, part: &mut CatalogPart) -> Result<String> {
|
||||||
// Compute signature over content without _SIGNATURE
|
// Compute signature over content without _SIGNATURE
|
||||||
part.signature = None;
|
part.signature = None;
|
||||||
let bytes_without_sig = serde_json::to_vec(&part)
|
let bytes_without_sig = serde_json::to_vec(&part).map_err(|e| {
|
||||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)))?;
|
RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e))
|
||||||
|
})?;
|
||||||
let sig = sha1_hex(&bytes_without_sig);
|
let sig = sha1_hex(&bytes_without_sig);
|
||||||
let mut sig_map = std::collections::HashMap::new();
|
let mut sig_map = std::collections::HashMap::new();
|
||||||
sig_map.insert("sha-1".to_string(), sig);
|
sig_map.insert("sha-1".to_string(), sig);
|
||||||
part.signature = Some(sig_map);
|
part.signature = Some(sig_map);
|
||||||
|
|
||||||
let final_bytes = serde_json::to_vec(&part)
|
let final_bytes = serde_json::to_vec(&part).map_err(|e| {
|
||||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)))?;
|
RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e))
|
||||||
|
})?;
|
||||||
debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog part");
|
debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog part");
|
||||||
atomic_write_bytes(path, &final_bytes)?;
|
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))]
|
#[instrument(level = "debug", skip(log))]
|
||||||
pub(crate) fn write_update_log(path: &Path, log: &mut UpdateLog) -> Result<String> {
|
pub(crate) fn write_update_log(path: &Path, log: &mut UpdateLog) -> Result<String> {
|
||||||
// Compute signature over content without _SIGNATURE
|
// Compute signature over content without _SIGNATURE
|
||||||
log.signature = None;
|
log.signature = None;
|
||||||
let bytes_without_sig = serde_json::to_vec(&log)
|
let bytes_without_sig = serde_json::to_vec(&log).map_err(|e| {
|
||||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?;
|
RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e))
|
||||||
|
})?;
|
||||||
let sig = sha1_hex(&bytes_without_sig);
|
let sig = sha1_hex(&bytes_without_sig);
|
||||||
let mut sig_map = std::collections::HashMap::new();
|
let mut sig_map = std::collections::HashMap::new();
|
||||||
sig_map.insert("sha-1".to_string(), sig);
|
sig_map.insert("sha-1".to_string(), sig);
|
||||||
log.signature = Some(sig_map);
|
log.signature = Some(sig_map);
|
||||||
|
|
||||||
let final_bytes = serde_json::to_vec(&log)
|
let final_bytes = serde_json::to_vec(&log).map_err(|e| {
|
||||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?;
|
RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e))
|
||||||
|
})?;
|
||||||
debug!(path = %path.display(), bytes = final_bytes.len(), "writing update log");
|
debug!(path = %path.display(), bytes = final_bytes.len(), "writing update log");
|
||||||
atomic_write_bytes(path, &final_bytes)?;
|
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/.
|
// obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
use super::{RepositoryError, Result};
|
use super::{RepositoryError, Result};
|
||||||
use flate2::write::GzEncoder;
|
|
||||||
use flate2::Compression as GzipCompression;
|
use flate2::Compression as GzipCompression;
|
||||||
|
use flate2::write::GzEncoder;
|
||||||
use lz4::EncoderBuilder;
|
use lz4::EncoderBuilder;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -25,11 +25,11 @@ use crate::digest::Digest;
|
||||||
use crate::fmri::Fmri;
|
use crate::fmri::Fmri;
|
||||||
use crate::payload::{Payload, PayloadCompressionAlgorithm};
|
use crate::payload::{Payload, PayloadCompressionAlgorithm};
|
||||||
|
|
||||||
use super::{
|
|
||||||
PackageContents, PackageInfo, PublisherInfo, ReadableRepository, RepositoryConfig,
|
|
||||||
RepositoryInfo, RepositoryVersion, WritableRepository, REPOSITORY_CONFIG_FILENAME,
|
|
||||||
};
|
|
||||||
use super::catalog_writer;
|
use super::catalog_writer;
|
||||||
|
use super::{
|
||||||
|
PackageContents, PackageInfo, PublisherInfo, REPOSITORY_CONFIG_FILENAME, ReadableRepository,
|
||||||
|
RepositoryConfig, RepositoryInfo, RepositoryVersion, WritableRepository,
|
||||||
|
};
|
||||||
use ini::Ini;
|
use ini::Ini;
|
||||||
|
|
||||||
// Define a struct to hold the content vectors for each package
|
// 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
|
/// Uses RefCell for interior mutability to allow mutation through immutable references
|
||||||
catalog_manager: Option<std::cell::RefCell<crate::repository::catalog::CatalogManager>>,
|
catalog_manager: Option<std::cell::RefCell<crate::repository::catalog::CatalogManager>>,
|
||||||
/// Manager for obsoleted packages
|
/// 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
|
/// Format a SystemTime as an ISO 8601 timestamp string
|
||||||
|
|
@ -342,20 +343,16 @@ impl Transaction {
|
||||||
// Check if the temp file already exists
|
// Check if the temp file already exists
|
||||||
if temp_file_path.exists() {
|
if temp_file_path.exists() {
|
||||||
// If it exists, remove it to avoid any issues with existing content
|
// If it exists, remove it to avoid any issues with existing content
|
||||||
fs::remove_file(&temp_file_path).map_err(|e| {
|
fs::remove_file(&temp_file_path).map_err(|e| RepositoryError::FileWriteError {
|
||||||
RepositoryError::FileWriteError {
|
path: temp_file_path.clone(),
|
||||||
path: temp_file_path.clone(),
|
source: e,
|
||||||
source: e,
|
|
||||||
}
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the file content
|
// Read the file content
|
||||||
let file_content = fs::read(file_path).map_err(|e| {
|
let file_content = fs::read(file_path).map_err(|e| RepositoryError::FileReadError {
|
||||||
RepositoryError::FileReadError {
|
path: file_path.to_path_buf(),
|
||||||
path: file_path.to_path_buf(),
|
source: e,
|
||||||
source: e,
|
|
||||||
}
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Create a payload with the hash information if it doesn't exist
|
// Create a payload with the hash information if it doesn't exist
|
||||||
|
|
@ -493,7 +490,8 @@ impl Transaction {
|
||||||
// Copy files to their final location
|
// Copy files to their final location
|
||||||
for (source_path, hash) in self.files {
|
for (source_path, hash) in self.files {
|
||||||
// Create the destination path using the helper function with publisher
|
// 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
|
// Create parent directories if they don't exist
|
||||||
if let Some(parent) = dest_path.parent() {
|
if let Some(parent) = dest_path.parent() {
|
||||||
|
|
@ -567,7 +565,8 @@ impl Transaction {
|
||||||
// Construct the manifest path using the helper method
|
// Construct the manifest path using the helper method
|
||||||
let pkg_manifest_path = if package_version.is_empty() {
|
let pkg_manifest_path = if package_version.is_empty() {
|
||||||
// If no version was provided, store as a default manifest file
|
// 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 {
|
} else {
|
||||||
FileBackend::construct_manifest_path(
|
FileBackend::construct_manifest_path(
|
||||||
&self.repo,
|
&self.repo,
|
||||||
|
|
@ -667,13 +666,15 @@ impl ReadableRepository for FileBackend {
|
||||||
let config5_path = path.join("pkg5.repository");
|
let config5_path = path.join("pkg5.repository");
|
||||||
|
|
||||||
let config: RepositoryConfig = if config6_path.exists() {
|
let config: RepositoryConfig = if config6_path.exists() {
|
||||||
let config_data = fs::read_to_string(&config6_path)
|
let config_data = fs::read_to_string(&config6_path).map_err(|e| {
|
||||||
.map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config6_path.display(), e)))?;
|
RepositoryError::ConfigReadError(format!("{}: {}", config6_path.display(), e))
|
||||||
|
})?;
|
||||||
serde_json::from_str(&config_data)?
|
serde_json::from_str(&config_data)?
|
||||||
} else if config5_path.exists() {
|
} else if config5_path.exists() {
|
||||||
// Minimal mapping for legacy INI: take publishers only from INI; do not scan disk.
|
// Minimal mapping for legacy INI: take publishers only from INI; do not scan disk.
|
||||||
let ini = Ini::load_from_file(&config5_path)
|
let ini = Ini::load_from_file(&config5_path).map_err(|e| {
|
||||||
.map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config5_path.display(), e)))?;
|
RepositoryError::ConfigReadError(format!("{}: {}", config5_path.display(), e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Default repository version for legacy format is v4
|
// Default repository version for legacy format is v4
|
||||||
let mut cfg = RepositoryConfig::default();
|
let mut cfg = RepositoryConfig::default();
|
||||||
|
|
@ -829,7 +830,10 @@ impl ReadableRepository for FileBackend {
|
||||||
pattern: Option<&str>,
|
pattern: Option<&str>,
|
||||||
action_types: Option<&[String]>,
|
action_types: Option<&[String]>,
|
||||||
) -> Result<Vec<PackageContents>> {
|
) -> 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
|
// Use a HashMap to store package information
|
||||||
let mut packages = HashMap::new();
|
let mut packages = HashMap::new();
|
||||||
|
|
||||||
|
|
@ -889,7 +893,9 @@ impl ReadableRepository for FileBackend {
|
||||||
|
|
||||||
// Check if the file starts with a valid manifest marker
|
// Check if the file starts with a valid manifest marker
|
||||||
if bytes_read == 0
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -901,7 +907,9 @@ impl ReadableRepository for FileBackend {
|
||||||
let mut pkg_id = String::new();
|
let mut pkg_id = String::new();
|
||||||
|
|
||||||
for attr in &manifest.attributes {
|
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];
|
let fmri = &attr.values[0];
|
||||||
|
|
||||||
// Parse the FMRI using our Fmri type
|
// Parse the FMRI using our Fmri type
|
||||||
|
|
@ -913,14 +921,22 @@ impl ReadableRepository for FileBackend {
|
||||||
match Regex::new(pat) {
|
match Regex::new(pat) {
|
||||||
Ok(regex) => {
|
Ok(regex) => {
|
||||||
// Use regex matching
|
// Use regex matching
|
||||||
if !regex.is_match(parsed_fmri.stem()) {
|
if !regex.is_match(
|
||||||
|
parsed_fmri.stem(),
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Log the error but fall back to the simple string contains
|
// Log the error but fall back to the simple string contains
|
||||||
error!("FileBackend::show_contents: Error compiling regex pattern '{}': {}", pat, err);
|
error!(
|
||||||
if !parsed_fmri.stem().contains(pat) {
|
"FileBackend::show_contents: Error compiling regex pattern '{}': {}",
|
||||||
|
pat, err
|
||||||
|
);
|
||||||
|
if !parsed_fmri
|
||||||
|
.stem()
|
||||||
|
.contains(pat)
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -970,7 +986,9 @@ impl ReadableRepository for FileBackend {
|
||||||
.contains(&"file".to_string())
|
.contains(&"file".to_string())
|
||||||
{
|
{
|
||||||
for file in &manifest.files {
|
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())
|
.contains(&"dir".to_string())
|
||||||
{
|
{
|
||||||
for dir in &manifest.directories {
|
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())
|
.contains(&"link".to_string())
|
||||||
{
|
{
|
||||||
for link in &manifest.links {
|
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 {
|
for depend in &manifest.dependencies {
|
||||||
if let Some(fmri) = &depend.fmri {
|
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())
|
.contains(&"license".to_string())
|
||||||
{
|
{
|
||||||
for license in &manifest.licenses {
|
for license in &manifest.licenses {
|
||||||
if let Some(path_prop) = license.properties.get("path") {
|
if let Some(path_prop) =
|
||||||
content_vectors.licenses.push(path_prop.value.clone());
|
license.properties.get("path")
|
||||||
} else if let Some(license_prop) = license.properties.get("license") {
|
{
|
||||||
content_vectors.licenses.push(license_prop.value.clone());
|
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 {
|
} 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) => {
|
Err(err) => {
|
||||||
// Log the error but fall back to the simple string contains
|
// 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)
|
if !parsed_fmri.stem().contains(pat)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1323,16 +1360,30 @@ impl ReadableRepository for FileBackend {
|
||||||
|
|
||||||
// If destination already exists and matches digest, do nothing
|
// If destination already exists and matches digest, do nothing
|
||||||
if dest.exists() {
|
if dest.exists() {
|
||||||
let bytes = fs::read(dest).map_err(|e| RepositoryError::FileReadError { path: dest.to_path_buf(), source: e })?;
|
let bytes = fs::read(dest).map_err(|e| RepositoryError::FileReadError {
|
||||||
match crate::digest::Digest::from_bytes(&bytes, algo.clone(), crate::digest::DigestSource::PrimaryPayloadHash) {
|
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(()),
|
Ok(comp) if comp.hash == hash => return Ok(()),
|
||||||
_ => { /* fall through to overwrite */ }
|
_ => { /* fall through to overwrite */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read source content and verify digest
|
// Read source content and verify digest
|
||||||
let bytes = fs::read(&source_path).map_err(|e| RepositoryError::FileReadError { path: source_path.clone(), source: e })?;
|
let bytes = fs::read(&source_path).map_err(|e| RepositoryError::FileReadError {
|
||||||
match crate::digest::Digest::from_bytes(&bytes, algo, crate::digest::DigestSource::PrimaryPayloadHash) {
|
path: source_path.clone(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
|
match crate::digest::Digest::from_bytes(
|
||||||
|
&bytes,
|
||||||
|
algo,
|
||||||
|
crate::digest::DigestSource::PrimaryPayloadHash,
|
||||||
|
) {
|
||||||
Ok(comp) => {
|
Ok(comp) => {
|
||||||
if comp.hash != hash {
|
if comp.hash != hash {
|
||||||
return Err(RepositoryError::DigestError(format!(
|
return Err(RepositoryError::DigestError(format!(
|
||||||
|
|
@ -1363,7 +1414,9 @@ impl ReadableRepository for FileBackend {
|
||||||
// Require a concrete version
|
// Require a concrete version
|
||||||
let version = fmri.version();
|
let version = fmri.version();
|
||||||
if version.is_empty() {
|
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
|
// Preferred path: publisher-scoped manifest path
|
||||||
|
|
@ -1375,7 +1428,11 @@ impl ReadableRepository for FileBackend {
|
||||||
// Fallbacks: global pkg layout without publisher
|
// Fallbacks: global pkg layout without publisher
|
||||||
let encoded_stem = Self::url_encode(fmri.stem());
|
let encoded_stem = Self::url_encode(fmri.stem());
|
||||||
let encoded_version = Self::url_encode(&version);
|
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() {
|
if alt1.exists() {
|
||||||
return crate::actions::Manifest::parse_file(&alt1).map_err(RepositoryError::from);
|
return crate::actions::Manifest::parse_file(&alt1).map_err(RepositoryError::from);
|
||||||
}
|
}
|
||||||
|
|
@ -1744,7 +1801,10 @@ impl FileBackend {
|
||||||
locale: &str,
|
locale: &str,
|
||||||
fmri: &crate::fmri::Fmri,
|
fmri: &crate::fmri::Fmri,
|
||||||
op_type: crate::repository::catalog::CatalogOperationType,
|
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>,
|
signature_sha1: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let catalog_dir = Self::construct_catalog_path(&self.path, publisher);
|
let catalog_dir = Self::construct_catalog_path(&self.path, publisher);
|
||||||
|
|
@ -1816,19 +1876,29 @@ impl FileBackend {
|
||||||
// Require a concrete version
|
// Require a concrete version
|
||||||
let version = fmri.version();
|
let version = fmri.version();
|
||||||
if version.is_empty() {
|
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
|
// Preferred path: publisher-scoped manifest path
|
||||||
let path = Self::construct_manifest_path(&self.path, publisher, fmri.stem(), &version);
|
let path = Self::construct_manifest_path(&self.path, publisher, fmri.stem(), &version);
|
||||||
if path.exists() {
|
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
|
// Fallbacks: global pkg layout without publisher
|
||||||
let encoded_stem = Self::url_encode(fmri.stem());
|
let encoded_stem = Self::url_encode(fmri.stem());
|
||||||
let encoded_version = Self::url_encode(&version);
|
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() {
|
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
|
let alt2 = self
|
||||||
.path
|
.path
|
||||||
|
|
@ -1838,9 +1908,15 @@ impl FileBackend {
|
||||||
.join(&encoded_stem)
|
.join(&encoded_stem)
|
||||||
.join(&encoded_version);
|
.join(&encoded_version);
|
||||||
if alt2.exists() {
|
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
|
/// Fetch catalog file path
|
||||||
pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result<PathBuf> {
|
pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result<PathBuf> {
|
||||||
|
|
@ -1880,8 +1956,7 @@ impl FileBackend {
|
||||||
.set("check-certificate-revocation", "False");
|
.set("check-certificate-revocation", "False");
|
||||||
|
|
||||||
// Add CONFIGURATION section with version
|
// Add CONFIGURATION section with version
|
||||||
conf.with_section(Some("CONFIGURATION"))
|
conf.with_section(Some("CONFIGURATION")).set("version", "4");
|
||||||
.set("version", "4");
|
|
||||||
|
|
||||||
// Write the INI file
|
// Write the INI file
|
||||||
conf.write_to_file(legacy_config_path)?;
|
conf.write_to_file(legacy_config_path)?;
|
||||||
|
|
@ -1939,10 +2014,7 @@ impl FileBackend {
|
||||||
/// Helper method to construct a catalog path consistently
|
/// Helper method to construct a catalog path consistently
|
||||||
///
|
///
|
||||||
/// Format: base_path/publisher/publisher_name/catalog
|
/// Format: base_path/publisher/publisher_name/catalog
|
||||||
pub fn construct_catalog_path(
|
pub fn construct_catalog_path(base_path: &Path, publisher: &str) -> PathBuf {
|
||||||
base_path: &Path,
|
|
||||||
publisher: &str,
|
|
||||||
) -> PathBuf {
|
|
||||||
base_path.join("publisher").join(publisher).join("catalog")
|
base_path.join("publisher").join(publisher).join("catalog")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1963,23 +2035,20 @@ impl FileBackend {
|
||||||
/// Helper method to construct a package directory path consistently
|
/// Helper method to construct a package directory path consistently
|
||||||
///
|
///
|
||||||
/// Format: base_path/publisher/publisher_name/pkg/url_encoded_stem
|
/// Format: base_path/publisher/publisher_name/pkg/url_encoded_stem
|
||||||
pub fn construct_package_dir(
|
pub fn construct_package_dir(base_path: &Path, publisher: &str, stem: &str) -> PathBuf {
|
||||||
base_path: &Path,
|
|
||||||
publisher: &str,
|
|
||||||
stem: &str,
|
|
||||||
) -> PathBuf {
|
|
||||||
let encoded_stem = Self::url_encode(stem);
|
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
|
/// Helper method to construct a file path consistently
|
||||||
///
|
///
|
||||||
/// Format: base_path/file/XX/hash
|
/// Format: base_path/file/XX/hash
|
||||||
/// Where XX is the first two characters of the hash
|
/// Where XX is the first two characters of the hash
|
||||||
pub fn construct_file_path(
|
pub fn construct_file_path(base_path: &Path, hash: &str) -> PathBuf {
|
||||||
base_path: &Path,
|
|
||||||
hash: &str,
|
|
||||||
) -> PathBuf {
|
|
||||||
if hash.len() < 2 {
|
if hash.len() < 2 {
|
||||||
// Fallback for very short hashes (shouldn't happen with SHA256)
|
// Fallback for very short hashes (shouldn't happen with SHA256)
|
||||||
base_path.join("file").join(hash)
|
base_path.join("file").join(hash)
|
||||||
|
|
@ -1988,10 +2057,7 @@ impl FileBackend {
|
||||||
let first_two = &hash[0..2];
|
let first_two = &hash[0..2];
|
||||||
|
|
||||||
// Create the path: $REPO/file/XX/XXYY...
|
// Create the path: $REPO/file/XX/XXYY...
|
||||||
base_path
|
base_path.join("file").join(first_two).join(hash)
|
||||||
.join("file")
|
|
||||||
.join(first_two)
|
|
||||||
.join(hash)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2006,7 +2072,11 @@ impl FileBackend {
|
||||||
) -> PathBuf {
|
) -> PathBuf {
|
||||||
if hash.len() < 2 {
|
if hash.len() < 2 {
|
||||||
// Fallback for very short hashes (shouldn't happen with SHA256)
|
// 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 {
|
} else {
|
||||||
// Extract the first two characters from the hash
|
// Extract the first two characters from the hash
|
||||||
let first_two = &hash[0..2];
|
let first_two = &hash[0..2];
|
||||||
|
|
@ -2094,7 +2164,10 @@ impl FileBackend {
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Log the error but fall back to the simple string contains
|
// 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) {
|
if !parsed_fmri.stem().contains(pat) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -2113,8 +2186,12 @@ impl FileBackend {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if the package is obsoleted
|
// Check if the package is obsoleted
|
||||||
let is_obsoleted = if let Some(obsoleted_manager) = &self.obsoleted_manager {
|
let is_obsoleted = if let Some(obsoleted_manager) =
|
||||||
obsoleted_manager.borrow().is_obsoleted(publisher, &final_fmri)
|
&self.obsoleted_manager
|
||||||
|
{
|
||||||
|
obsoleted_manager
|
||||||
|
.borrow()
|
||||||
|
.is_obsoleted(publisher, &final_fmri)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
@ -2122,9 +2199,7 @@ impl FileBackend {
|
||||||
// Only add the package if it's not obsoleted
|
// Only add the package if it's not obsoleted
|
||||||
if !is_obsoleted {
|
if !is_obsoleted {
|
||||||
// Create a PackageInfo struct and add it to the list
|
// Create a PackageInfo struct and add it to the list
|
||||||
packages.push(PackageInfo {
|
packages.push(PackageInfo { fmri: final_fmri });
|
||||||
fmri: final_fmri,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Found the package info, no need to check other attributes
|
// Found the package info, no need to check other attributes
|
||||||
|
|
@ -2245,7 +2320,11 @@ impl FileBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the manifest content for hash calculation
|
// 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
|
// Parse the manifest using parse_file which handles JSON correctly
|
||||||
let manifest = Manifest::parse_file(&manifest_path)?;
|
let manifest = Manifest::parse_file(&manifest_path)?;
|
||||||
|
|
@ -2334,7 +2413,12 @@ impl FileBackend {
|
||||||
processed_in_batch += 1;
|
processed_in_batch += 1;
|
||||||
if processed_in_batch >= opts.batch_size {
|
if processed_in_batch >= opts.batch_size {
|
||||||
batch_no += 1;
|
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;
|
processed_in_batch = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2407,7 +2491,8 @@ impl FileBackend {
|
||||||
for (fmri, actions, signature) in dependency_entries {
|
for (fmri, actions, signature) in dependency_entries {
|
||||||
dependency_part.add_package(publisher, &fmri, actions, Some(signature));
|
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");
|
debug!("Wrote dependency part file");
|
||||||
|
|
||||||
// Summary part
|
// Summary part
|
||||||
|
|
@ -2417,7 +2502,8 @@ impl FileBackend {
|
||||||
for (fmri, actions, signature) in summary_entries {
|
for (fmri, actions, signature) in summary_entries {
|
||||||
summary_part.add_package(publisher, &fmri, actions, Some(signature));
|
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");
|
debug!("Wrote summary part file");
|
||||||
|
|
||||||
// Update part signatures in attrs (written after parts)
|
// Update part signatures in attrs (written after parts)
|
||||||
|
|
@ -2495,25 +2581,42 @@ impl FileBackend {
|
||||||
|
|
||||||
// Ensure catalog dir exists
|
// Ensure catalog dir exists
|
||||||
let catalog_dir = Self::construct_catalog_path(&self.path, publisher);
|
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
|
// Serialize JSON
|
||||||
let json = serde_json::to_vec_pretty(log)
|
let json = serde_json::to_vec_pretty(log).map_err(|e| {
|
||||||
.map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?;
|
RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Write atomically
|
// Write atomically
|
||||||
let target = catalog_dir.join(log_filename);
|
let target = catalog_dir.join(log_filename);
|
||||||
let tmp = target.with_extension("tmp");
|
let tmp = target.with_extension("tmp");
|
||||||
{
|
{
|
||||||
let mut f = std::fs::File::create(&tmp)
|
let mut f =
|
||||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
std::fs::File::create(&tmp).map_err(|e| RepositoryError::FileWriteError {
|
||||||
|
path: tmp.clone(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
use std::io::Write as _;
|
use std::io::Write as _;
|
||||||
f.write_all(&json)
|
f.write_all(&json)
|
||||||
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
.map_err(|e| RepositoryError::FileWriteError {
|
||||||
f.flush().map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?;
|
path: tmp.clone(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
|
f.flush().map_err(|e| RepositoryError::FileWriteError {
|
||||||
|
path: tmp.clone(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
std::fs::rename(&tmp, &target)
|
std::fs::rename(&tmp, &target).map_err(|e| RepositoryError::FileWriteError {
|
||||||
.map_err(|e| RepositoryError::FileWriteError { path: target.clone(), source: e })?;
|
path: target.clone(),
|
||||||
|
source: e,
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -2536,7 +2639,8 @@ impl FileBackend {
|
||||||
) -> Result<std::cell::RefMut<'_, crate::repository::catalog::CatalogManager>> {
|
) -> Result<std::cell::RefMut<'_, crate::repository::catalog::CatalogManager>> {
|
||||||
if self.catalog_manager.is_none() {
|
if self.catalog_manager.is_none() {
|
||||||
let publisher_dir = self.path.join("publisher");
|
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);
|
let refcell = std::cell::RefCell::new(manager);
|
||||||
self.catalog_manager = Some(refcell);
|
self.catalog_manager = Some(refcell);
|
||||||
}
|
}
|
||||||
|
|
@ -2669,18 +2773,17 @@ impl FileBackend {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let directories =
|
let directories = if !manifest.directories.is_empty() {
|
||||||
if !manifest.directories.is_empty() {
|
Some(
|
||||||
Some(
|
manifest
|
||||||
manifest
|
.directories
|
||||||
.directories
|
.iter()
|
||||||
.iter()
|
.map(|d| d.path.clone())
|
||||||
.map(|d| d.path.clone())
|
.collect(),
|
||||||
.collect(),
|
)
|
||||||
)
|
} else {
|
||||||
} else {
|
None
|
||||||
None
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let links = if !manifest.links.is_empty() {
|
let links = if !manifest.links.is_empty() {
|
||||||
Some(
|
Some(
|
||||||
|
|
@ -2694,22 +2797,20 @@ impl FileBackend {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let dependencies =
|
let dependencies = if !manifest.dependencies.is_empty()
|
||||||
if !manifest.dependencies.is_empty() {
|
{
|
||||||
Some(
|
Some(
|
||||||
manifest
|
manifest
|
||||||
.dependencies
|
.dependencies
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|d| {
|
.filter_map(|d| {
|
||||||
d.fmri
|
d.fmri.as_ref().map(|f| f.to_string())
|
||||||
.as_ref()
|
})
|
||||||
.map(|f| f.to_string())
|
.collect(),
|
||||||
})
|
)
|
||||||
.collect(),
|
} else {
|
||||||
)
|
None
|
||||||
} else {
|
};
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let licenses = if !manifest.licenses.is_empty() {
|
let licenses = if !manifest.licenses.is_empty() {
|
||||||
Some(
|
Some(
|
||||||
|
|
@ -2746,7 +2847,10 @@ impl FileBackend {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the package to the index
|
// 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
|
// Found the package info, no need to check other attributes
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -216,8 +216,8 @@ impl From<bincode::error::EncodeError> for RepositoryError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub mod catalog;
|
pub mod catalog;
|
||||||
pub(crate) mod file_backend;
|
|
||||||
mod catalog_writer;
|
mod catalog_writer;
|
||||||
|
pub(crate) mod file_backend;
|
||||||
mod obsoleted;
|
mod obsoleted;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
mod rest_backend;
|
mod rest_backend;
|
||||||
|
|
@ -231,7 +231,7 @@ pub use catalog::{
|
||||||
};
|
};
|
||||||
pub use file_backend::FileBackend;
|
pub use file_backend::FileBackend;
|
||||||
pub use obsoleted::{ObsoletedPackageManager, ObsoletedPackageMetadata};
|
pub use obsoleted::{ObsoletedPackageManager, ObsoletedPackageMetadata};
|
||||||
pub use progress::{ProgressInfo, ProgressReporter, NoopProgressReporter};
|
pub use progress::{NoopProgressReporter, ProgressInfo, ProgressReporter};
|
||||||
pub use rest_backend::RestBackend;
|
pub use rest_backend::RestBackend;
|
||||||
|
|
||||||
/// Repository configuration filename
|
/// Repository configuration filename
|
||||||
|
|
@ -248,7 +248,10 @@ pub struct BatchOptions {
|
||||||
|
|
||||||
impl Default for BatchOptions {
|
impl Default for BatchOptions {
|
||||||
fn default() -> Self {
|
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.
|
/// Fetch a content payload identified by digest into the destination path.
|
||||||
/// Implementations should download/copy the payload to a temporary path,
|
/// Implementations should download/copy the payload to a temporary path,
|
||||||
/// verify integrity, and atomically move into `dest`.
|
/// verify integrity, and atomically move into `dest`.
|
||||||
fn fetch_payload(
|
fn fetch_payload(&mut self, publisher: &str, digest: &str, dest: &Path) -> Result<()>;
|
||||||
&mut self,
|
|
||||||
publisher: &str,
|
|
||||||
digest: &str,
|
|
||||||
dest: &Path,
|
|
||||||
) -> Result<()>;
|
|
||||||
|
|
||||||
/// Fetch a package manifest by FMRI from the repository.
|
/// Fetch a package manifest by FMRI from the repository.
|
||||||
/// Implementations should retrieve and parse the manifest for the given
|
/// 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 reqwest::blocking::Client;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::catalog::CatalogManager;
|
||||||
use super::{
|
use super::{
|
||||||
NoopProgressReporter, PackageContents, PackageInfo, ProgressInfo, ProgressReporter,
|
NoopProgressReporter, PackageContents, PackageInfo, ProgressInfo, ProgressReporter,
|
||||||
PublisherInfo, ReadableRepository, RepositoryConfig, RepositoryError, RepositoryInfo,
|
PublisherInfo, ReadableRepository, RepositoryConfig, RepositoryError, RepositoryInfo,
|
||||||
RepositoryVersion, Result, WritableRepository,
|
RepositoryVersion, Result, WritableRepository,
|
||||||
};
|
};
|
||||||
use super::catalog::CatalogManager;
|
|
||||||
|
|
||||||
/// Repository implementation that uses a REST API to interact with a remote repository.
|
/// 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
|
// 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
|
// Create catalog directory
|
||||||
let catalog_dir = publisher_dir.join("catalog");
|
let catalog_dir = publisher_dir.join("catalog");
|
||||||
|
|
@ -144,7 +147,10 @@ impl WritableRepository for RestBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the directory was created
|
// 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());
|
debug!("Created publisher directory: {}", publisher_dir.display());
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -259,7 +265,9 @@ impl WritableRepository for RestBackend {
|
||||||
|
|
||||||
// Check if we have a local cache path
|
// Check if we have a local cache path
|
||||||
if cloned_self.local_cache_path.is_none() {
|
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
|
// Filter publishers if specified
|
||||||
|
|
@ -336,21 +344,26 @@ impl ReadableRepository for RestBackend {
|
||||||
match response.json::<Value>() {
|
match response.json::<Value>() {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
// Extract publisher information
|
// 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 {
|
for (name, _) in publishers {
|
||||||
debug!("Found publisher: {}", name);
|
debug!("Found publisher: {}", name);
|
||||||
config.publishers.push(name.clone());
|
config.publishers.push(name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to parse publisher information: {}", e);
|
warn!("Failed to parse publisher information: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Failed to fetch publisher information: HTTP status {}", response.status());
|
warn!(
|
||||||
|
"Failed to fetch publisher information: HTTP status {}",
|
||||||
|
response.status()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to connect to repository: {}", e);
|
warn!("Failed to connect to repository: {}", e);
|
||||||
}
|
}
|
||||||
|
|
@ -536,12 +549,7 @@ impl ReadableRepository for RestBackend {
|
||||||
Ok(package_contents)
|
Ok(package_contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_payload(
|
fn fetch_payload(&mut self, publisher: &str, digest: &str, dest: &Path) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
publisher: &str,
|
|
||||||
digest: &str,
|
|
||||||
dest: &Path,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Determine hash and algorithm from the provided digest string
|
// Determine hash and algorithm from the provided digest string
|
||||||
let mut hash = digest.to_string();
|
let mut hash = digest.to_string();
|
||||||
let mut algo: Option<crate::digest::DigestAlgorithm> = None;
|
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()));
|
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![
|
let candidates = vec![
|
||||||
format!("{}/file/{}/{}", self.uri, shard, hash),
|
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
|
// Ensure destination directory exists
|
||||||
|
|
@ -571,11 +586,17 @@ impl ReadableRepository for RestBackend {
|
||||||
for url in candidates {
|
for url in candidates {
|
||||||
match self.client.get(&url).send() {
|
match self.client.get(&url).send() {
|
||||||
Ok(resp) if resp.status().is_success() => {
|
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
|
// Verify digest if algorithm is known
|
||||||
if let Some(alg) = algo.clone() {
|
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) => {
|
Ok(comp) => {
|
||||||
if comp.hash != hash {
|
if comp.hash != hash {
|
||||||
return Err(RepositoryError::DigestError(format!(
|
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(
|
fn fetch_manifest(
|
||||||
|
|
@ -636,14 +659,18 @@ impl RestBackend {
|
||||||
// Require versioned FMRI
|
// Require versioned FMRI
|
||||||
let version = fmri.version();
|
let version = fmri.version();
|
||||||
if version.is_empty() {
|
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
|
// URL-encode helper
|
||||||
let url_encode = |s: &str| -> String {
|
let url_encode = |s: &str| -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for b in s.bytes() {
|
for b in s.bytes() {
|
||||||
match b {
|
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('+'),
|
b' ' => out.push('+'),
|
||||||
_ => {
|
_ => {
|
||||||
out.push('%');
|
out.push('%');
|
||||||
|
|
@ -658,16 +685,24 @@ impl RestBackend {
|
||||||
let encoded_version = url_encode(&version);
|
let encoded_version = url_encode(&version);
|
||||||
let candidates = vec![
|
let candidates = vec![
|
||||||
format!("{}/manifest/0/{}", self.uri, encoded_fmri),
|
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
|
// Fallbacks to direct file-style paths if server exposes static files
|
||||||
format!("{}/pkg/{}/{}", self.uri, encoded_stem, encoded_version),
|
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;
|
let mut last_err: Option<String> = None;
|
||||||
for url in candidates {
|
for url in candidates {
|
||||||
match self.client.get(&url).send() {
|
match self.client.get(&url).send() {
|
||||||
Ok(resp) if resp.status().is_success() => {
|
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);
|
return Ok(text);
|
||||||
}
|
}
|
||||||
Ok(resp) => {
|
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.
|
/// 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<()> {
|
pub fn initialize(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> {
|
||||||
// Check if we have a local cache path
|
// Check if we have a local cache path
|
||||||
if self.local_cache_path.is_none() {
|
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
|
// Download catalogs for all publishers
|
||||||
|
|
@ -743,7 +782,11 @@ impl RestBackend {
|
||||||
// Check if we have a local cache path
|
// Check if we have a local cache path
|
||||||
let cache_path = match &self.local_cache_path {
|
let cache_path = match &self.local_cache_path {
|
||||||
Some(path) => 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
|
// 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
|
// Get or create the catalog manager pointing at the per-publisher directory directly
|
||||||
if !self.catalog_managers.contains_key(publisher) {
|
if !self.catalog_managers.contains_key(publisher) {
|
||||||
let catalog_manager = CatalogManager::new(cache_path, 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())
|
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
|
// Prepare candidate URLs to support both modern and legacy pkg5 depotd layouts
|
||||||
let mut urls: Vec<String> = vec![
|
let mut urls: Vec<String> = vec![
|
||||||
format!("{}/catalog/1/{}", self.uri, file_name),
|
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" {
|
if file_name == "catalog.attrs" {
|
||||||
// Some older depots expose catalog.attrs at the root or under publisher path
|
// Some older depots expose catalog.attrs at the root or under publisher path
|
||||||
urls.insert(1, format!("{}/catalog.attrs", self.uri));
|
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!(
|
debug!(
|
||||||
|
|
@ -855,10 +905,7 @@ impl RestBackend {
|
||||||
"Failed to download '{}' from any known endpoint: {}",
|
"Failed to download '{}' from any known endpoint: {}",
|
||||||
file_name, s
|
file_name, s
|
||||||
),
|
),
|
||||||
None => format!(
|
None => format!("Failed to download '{}' from any known endpoint", file_name),
|
||||||
"Failed to download '{}' from any known endpoint",
|
|
||||||
file_name
|
|
||||||
),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -890,7 +937,11 @@ impl RestBackend {
|
||||||
// Check if we have a local cache path
|
// Check if we have a local cache path
|
||||||
let cache_path = match &self.local_cache_path {
|
let cache_path = match &self.local_cache_path {
|
||||||
Some(path) => 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
|
// Ensure the per-publisher directory (local cache path) exists
|
||||||
|
|
@ -913,19 +964,23 @@ impl RestBackend {
|
||||||
|
|
||||||
// Store the file directly under the per-publisher directory
|
// Store the file directly under the per-publisher directory
|
||||||
let file_path = cache_path.join(file_name);
|
let file_path = cache_path.join(file_name);
|
||||||
let mut file = File::create(&file_path)
|
let mut file = File::create(&file_path).map_err(|e| {
|
||||||
.map_err(|e| {
|
// Report failure
|
||||||
// Report failure
|
progress.finish(&progress_info);
|
||||||
progress.finish(&progress_info);
|
RepositoryError::FileWriteError {
|
||||||
RepositoryError::FileWriteError { path: file_path.clone(), source: e }
|
path: file_path.clone(),
|
||||||
})?;
|
source: e,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
file.write_all(&content)
|
file.write_all(&content).map_err(|e| {
|
||||||
.map_err(|e| {
|
// Report failure
|
||||||
// Report failure
|
progress.finish(&progress_info);
|
||||||
progress.finish(&progress_info);
|
RepositoryError::FileWriteError {
|
||||||
RepositoryError::FileWriteError { path: file_path.clone(), source: e }
|
path: file_path.clone(),
|
||||||
})?;
|
source: e,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
debug!("Stored catalog file: {}", file_path.display());
|
debug!("Stored catalog file: {}", file_path.display());
|
||||||
|
|
||||||
|
|
@ -969,26 +1024,29 @@ impl RestBackend {
|
||||||
let progress_reporter = progress.unwrap_or(&NoopProgressReporter);
|
let progress_reporter = progress.unwrap_or(&NoopProgressReporter);
|
||||||
|
|
||||||
// Create progress info for the overall operation
|
// 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
|
// Notify that we're starting the download
|
||||||
progress_reporter.start(&overall_progress);
|
progress_reporter.start(&overall_progress);
|
||||||
|
|
||||||
// First download catalog.attrs to get the list of available parts
|
// 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
|
// Parse the catalog.attrs file to get the list of parts
|
||||||
let attrs_content = fs::read_to_string(&attrs_path)
|
let attrs_content = fs::read_to_string(&attrs_path).map_err(|e| {
|
||||||
.map_err(|e| {
|
progress_reporter.finish(&overall_progress);
|
||||||
progress_reporter.finish(&overall_progress);
|
RepositoryError::FileReadError {
|
||||||
RepositoryError::FileReadError { path: attrs_path.clone(), source: e }
|
path: attrs_path.clone(),
|
||||||
})?;
|
source: e,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
let attrs: Value = serde_json::from_str(&attrs_content)
|
let attrs: Value = serde_json::from_str(&attrs_content).map_err(|e| {
|
||||||
.map_err(|e| {
|
progress_reporter.finish(&overall_progress);
|
||||||
progress_reporter.finish(&overall_progress);
|
RepositoryError::JsonParseError(format!("Failed to parse catalog.attrs: {}", e))
|
||||||
RepositoryError::JsonParseError(format!("Failed to parse catalog.attrs: {}", e))
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
// Get the list of parts
|
// Get the list of parts
|
||||||
let parts = attrs["parts"].as_object().ok_or_else(|| {
|
let parts = attrs["parts"].as_object().ok_or_else(|| {
|
||||||
|
|
@ -1006,7 +1064,8 @@ impl RestBackend {
|
||||||
debug!("Downloading catalog part: {}", part_name);
|
debug!("Downloading catalog part: {}", part_name);
|
||||||
|
|
||||||
// Update progress with current part
|
// 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));
|
.with_context(format!("Downloading part: {}", part_name));
|
||||||
progress_reporter.update(&overall_progress);
|
progress_reporter.update(&overall_progress);
|
||||||
|
|
||||||
|
|
@ -1091,7 +1150,11 @@ impl RestBackend {
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// * `Result<()>` - Ok if the catalog was refreshed successfully, Err otherwise
|
/// * `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)
|
self.download_catalog(publisher, progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ mod tests {
|
||||||
use crate::actions::Manifest;
|
use crate::actions::Manifest;
|
||||||
use crate::fmri::Fmri;
|
use crate::fmri::Fmri;
|
||||||
use crate::repository::{
|
use crate::repository::{
|
||||||
CatalogManager, FileBackend, ProgressInfo, ProgressReporter,
|
CatalogManager, FileBackend, ProgressInfo, ProgressReporter, REPOSITORY_CONFIG_FILENAME,
|
||||||
ReadableRepository, RepositoryError, RepositoryVersion, RestBackend, Result, WritableRepository,
|
ReadableRepository, RepositoryError, RepositoryVersion, RestBackend, Result,
|
||||||
REPOSITORY_CONFIG_FILENAME,
|
WritableRepository,
|
||||||
};
|
};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -210,8 +210,14 @@ mod tests {
|
||||||
assert!(FileBackend::construct_package_dir(&repo_path, "example.com", "").exists());
|
assert!(FileBackend::construct_package_dir(&repo_path, "example.com", "").exists());
|
||||||
|
|
||||||
// Check that the pub.p5i file was created for backward compatibility
|
// 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");
|
let pub_p5i_path = repo_path
|
||||||
assert!(pub_p5i_path.exists(), "pub.p5i file should be created for backward compatibility");
|
.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
|
// Verify the content of the pub.p5i file
|
||||||
let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap();
|
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
|
// Add a package to the part using the stored publisher
|
||||||
let fmri = Fmri::parse("pkg://test/example@1.0.0").unwrap();
|
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
|
// Save the part
|
||||||
catalog_manager.save_part("test_part").unwrap();
|
catalog_manager.save_part("test_part").unwrap();
|
||||||
|
|
@ -286,7 +294,13 @@ mod tests {
|
||||||
publish_package(&mut repo, &manifest_path, &prototype_dir, "test").unwrap();
|
publish_package(&mut repo, &manifest_path, &prototype_dir, "test").unwrap();
|
||||||
|
|
||||||
// Check that the files were published in the publisher-specific directory
|
// 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
|
// Get repository information
|
||||||
let repo_info = repo.get_info().unwrap();
|
let repo_info = repo.get_info().unwrap();
|
||||||
|
|
@ -364,9 +378,11 @@ mod tests {
|
||||||
|
|
||||||
// Check for specific files
|
// Check for specific files
|
||||||
assert!(files.iter().any(|f| f.contains("usr/bin/hello")));
|
assert!(files.iter().any(|f| f.contains("usr/bin/hello")));
|
||||||
assert!(files
|
assert!(
|
||||||
.iter()
|
files
|
||||||
.any(|f| f.contains("usr/share/doc/example/README.txt")));
|
.iter()
|
||||||
|
.any(|f| f.contains("usr/share/doc/example/README.txt"))
|
||||||
|
);
|
||||||
assert!(files.iter().any(|f| f.contains("etc/config/example.conf")));
|
assert!(files.iter().any(|f| f.contains("etc/config/example.conf")));
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
|
|
@ -428,7 +444,8 @@ mod tests {
|
||||||
let hash = repo.store_file(&test_file_path, "test").unwrap();
|
let hash = repo.store_file(&test_file_path, "test").unwrap();
|
||||||
|
|
||||||
// Check if the file was stored in the correct directory structure
|
// 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
|
// Verify that the file exists at the expected path
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -480,7 +497,10 @@ mod tests {
|
||||||
|
|
||||||
// Check that the pub.p5i file was created for the new publisher
|
// 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");
|
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
|
// Verify the content of the pub.p5i file
|
||||||
let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap();
|
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
|
// Check that the pkg5.repository file was created
|
||||||
let pkg5_repo_path = repo_path.join("pkg5.repository");
|
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
|
// Verify the content of the pkg5.repository file
|
||||||
let pkg5_content = fs::read_to_string(&pkg5_repo_path).unwrap();
|
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: {}", publisher_dir.display());
|
||||||
println!("Publisher directory exists: {}", publisher_dir.exists());
|
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");
|
let catalog_dir = publisher_dir.join("catalog");
|
||||||
println!("Catalog directory: {}", catalog_dir.display());
|
println!("Catalog directory: {}", catalog_dir.display());
|
||||||
|
|
@ -733,7 +759,11 @@ mod tests {
|
||||||
fs::write(&attrs_path, attrs_content).unwrap();
|
fs::write(&attrs_path, attrs_content).unwrap();
|
||||||
|
|
||||||
// Create mock catalog part files
|
// 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);
|
let part_path = catalog_dir.join(part_name);
|
||||||
fs::write(&part_path, "{}").unwrap();
|
fs::write(&part_path, "{}").unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,38 +33,52 @@ pub struct AdviceReport {
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct AdviceOptions {
|
pub struct AdviceOptions {
|
||||||
pub max_depth: usize, // 0 = unlimited
|
pub max_depth: usize, // 0 = unlimited
|
||||||
pub dependency_cap: usize, // 0 = unlimited per node
|
pub dependency_cap: usize, // 0 = unlimited per node
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Ctx {
|
struct Ctx {
|
||||||
// caches
|
// caches
|
||||||
catalog_cache: HashMap<String, Vec<(String, Fmri)>>, // stem -> [(publisher, fmri)]
|
catalog_cache: HashMap<String, Vec<(String, Fmri)>>, // stem -> [(publisher, fmri)]
|
||||||
manifest_cache: HashMap<String, Manifest>, // fmri string -> manifest
|
manifest_cache: HashMap<String, Manifest>, // fmri string -> manifest
|
||||||
lock_cache: HashMap<String, Option<String>>, // stem -> incorporated release
|
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)
|
candidate_cache:
|
||||||
|
HashMap<(String, Option<String>, Option<String>, Option<String>), Option<Fmri>>, // (stem, rel, branch, publisher)
|
||||||
publisher_filter: Option<String>,
|
publisher_filter: Option<String>,
|
||||||
cap: usize,
|
cap: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ctx {
|
impl Ctx {
|
||||||
fn new(publisher_filter: Option<String>, cap: usize) -> Self {
|
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 mut report = AdviceReport::default();
|
||||||
let Some(problem) = err.problem() else {
|
let Some(problem) = err.problem() else {
|
||||||
return Ok(report);
|
return Ok(report);
|
||||||
};
|
};
|
||||||
|
|
||||||
match &problem.kind {
|
match &problem.kind {
|
||||||
SolverProblemKind::NoCandidates { stem, release, branch } => {
|
SolverProblemKind::NoCandidates {
|
||||||
|
stem,
|
||||||
|
release,
|
||||||
|
branch,
|
||||||
|
} => {
|
||||||
// Advise directly on the missing root
|
// Advise directly on the missing root
|
||||||
let mut ctx = Ctx::new(None, opts.dependency_cap);
|
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 {
|
report.issues.push(AdviceIssue {
|
||||||
path: vec![stem.clone()],
|
path: vec![stem.clone()],
|
||||||
stem: 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.
|
// Fall back to analyzing roots and traversing dependencies to find a missing candidate leaf.
|
||||||
let mut ctx = Ctx::new(None, opts.dependency_cap);
|
let mut ctx = Ctx::new(None, opts.dependency_cap);
|
||||||
for root in &problem.roots {
|
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,
|
Ok(Some(f)) => f,
|
||||||
_ => {
|
_ => {
|
||||||
// Missing root candidate
|
// 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 {
|
report.issues.push(AdviceIssue {
|
||||||
path: vec![root.stem.clone()],
|
path: vec![root.stem.clone()],
|
||||||
stem: 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
|
// Depth-first traversal looking for missing candidates
|
||||||
let mut path = vec![root.stem.clone()];
|
let mut path = vec![root.stem.clone()];
|
||||||
let mut seen = std::collections::HashSet::new();
|
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)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
@ -114,30 +149,43 @@ fn advise_recursive(
|
||||||
seen: &mut std::collections::HashSet<String>,
|
seen: &mut std::collections::HashSet<String>,
|
||||||
report: &mut AdviceReport,
|
report: &mut AdviceReport,
|
||||||
) -> Result<(), AdviceError> {
|
) -> 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 manifest = get_manifest_cached(image, ctx, fmri)?;
|
||||||
|
|
||||||
let mut processed = 0usize;
|
let mut processed = 0usize;
|
||||||
for dep in manifest.dependencies.iter().filter(|d| d.dependency_type == "require" || d.dependency_type == "incorporate") {
|
for dep in manifest
|
||||||
let Some(df) = &dep.fmri else { continue; };
|
.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();
|
let dep_stem = df.stem().to_string();
|
||||||
// Extract constraints from optional properties and, if absent, from the dependency FMRI version 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 (mut rel, mut br) = extract_constraint(&dep.optional);
|
||||||
let df_ver_str = df.version();
|
let df_ver_str = df.version();
|
||||||
if !df_ver_str.is_empty() {
|
if !df_ver_str.is_empty() {
|
||||||
if rel.is_none() { rel = version_release(&df_ver_str); }
|
if rel.is_none() {
|
||||||
if br.is_none() { br = version_branch(&df_ver_str); }
|
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
|
// Mirror solver behavior: lock child to parent's branch when not explicitly constrained
|
||||||
if br.is_none() {
|
if br.is_none() {
|
||||||
let parent_branch = fmri
|
let parent_branch = fmri.version.as_ref().and_then(|v| v.branch.clone());
|
||||||
.version
|
if let Some(pb) = parent_branch {
|
||||||
.as_ref()
|
br = Some(pb);
|
||||||
.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;
|
processed += 1;
|
||||||
|
|
||||||
match find_best_candidate(image, ctx, &dep_stem, rel.as_deref(), br.as_deref())? {
|
match find_best_candidate(image, ctx, &dep_stem, rel.as_deref(), br.as_deref())? {
|
||||||
|
|
@ -150,7 +198,8 @@ fn advise_recursive(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
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 {
|
report.issues.push(AdviceIssue {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
stem: dep_stem.clone(),
|
stem: dep_stem.clone(),
|
||||||
|
|
@ -177,32 +226,76 @@ fn extract_constraint(optional: &[Property]) -> (Option<String>, Option<String>)
|
||||||
(release, branch)
|
(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();
|
let mut available: Vec<String> = Vec::new();
|
||||||
if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) {
|
if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) {
|
||||||
for (pubname, fmri) in list {
|
for (pubname, fmri) in list {
|
||||||
if let Some(ref pfilter) = ctx.publisher_filter { if &pubname != pfilter { continue; } }
|
if let Some(ref pfilter) = ctx.publisher_filter {
|
||||||
if fmri.stem() != stem { continue; }
|
if &pubname != pfilter {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fmri.stem() != stem {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let ver = fmri.version();
|
let ver = fmri.version();
|
||||||
if ver.is_empty() { continue; }
|
if ver.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
available.push(ver);
|
available.push(ver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
available.sort();
|
available.sort();
|
||||||
available.dedup();
|
available.dedup();
|
||||||
|
|
||||||
let available_str = if available.is_empty() { "<none>".to_string() } else { available.join(", ") };
|
let available_str = if available.is_empty() {
|
||||||
let lock = get_incorporated_release_cached(image, ctx, stem).ok().flatten();
|
"<none>".to_string()
|
||||||
|
} else {
|
||||||
|
available.join(", ")
|
||||||
|
};
|
||||||
|
let lock = get_incorporated_release_cached(image, ctx, stem)
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
match (release, branch, lock.as_deref()) {
|
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), Some(lr)) => format!(
|
||||||
(Some(r), Some(b), None) => format!("Required release={}, branch={} not found. Available versions: {}", r, b, available_str),
|
"Required release={}, branch={} not found. Image incorporation lock release={} may constrain candidates. Available versions: {}",
|
||||||
(Some(r), None, Some(lr)) => format!("Required release={} not found. Image incorporation lock release={} present. Available versions: {}", r, lr, available_str),
|
r, b, 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),
|
(Some(r), Some(b), None) => format!(
|
||||||
(None, Some(b), None) => format!("Required branch={} not found. Available versions: {}", b, available_str),
|
"Required release={}, branch={} not found. Available versions: {}",
|
||||||
(None, None, Some(lr)) => format!("No candidates matched. Image incorporation lock release={} present. Available versions: {}", lr, available_str),
|
r, b, available_str
|
||||||
(None, None, None) => format!("No candidates matched. Available versions: {}", 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()),
|
req_branch.map(|s| s.to_string()),
|
||||||
ctx.publisher_filter.clone(),
|
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();
|
let mut candidates: Vec<(String, Fmri)> = Vec::new();
|
||||||
for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? {
|
for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? {
|
||||||
if let Some(ref pfilter) = ctx.publisher_filter { if &pubf != pfilter { continue; } }
|
if let Some(ref pfilter) = ctx.publisher_filter {
|
||||||
if pfmri.stem() != stem { continue; }
|
if &pubf != pfilter {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pfmri.stem() != stem {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let ver = pfmri.version();
|
let ver = pfmri.version();
|
||||||
if ver.is_empty() { continue; }
|
if ver.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let rel = version_release(&ver);
|
let rel = version_release(&ver);
|
||||||
let br = version_branch(&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_r) = req_release {
|
||||||
if let Some(req_b) = req_branch { if Some(req_b) != br.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 let Some(req_b) = req_branch {
|
||||||
|
if Some(req_b) != br.as_deref() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
candidates.push((ver.clone(), pfmri.clone()));
|
candidates.push((ver.clone(), pfmri.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,7 +368,9 @@ fn version_release(version: &str) -> Option<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn version_branch(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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,8 +379,13 @@ fn query_catalog_cached(
|
||||||
ctx: &Ctx,
|
ctx: &Ctx,
|
||||||
stem: &str,
|
stem: &str,
|
||||||
) -> Result<Vec<(String, Fmri)>, AdviceError> {
|
) -> 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) {
|
||||||
let mut tmp = Ctx { catalog_cache: ctx.catalog_cache.clone(), ..Default::default() };
|
return Ok(v.clone());
|
||||||
|
}
|
||||||
|
let mut tmp = Ctx {
|
||||||
|
catalog_cache: ctx.catalog_cache.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
query_catalog_cached_mut(image, &mut tmp, stem)
|
query_catalog_cached_mut(image, &mut tmp, stem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,26 +394,48 @@ fn query_catalog_cached_mut(
|
||||||
ctx: &mut Ctx,
|
ctx: &mut Ctx,
|
||||||
stem: &str,
|
stem: &str,
|
||||||
) -> Result<Vec<(String, Fmri)>, AdviceError> {
|
) -> 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 mut out = Vec::new();
|
||||||
let res = image.query_catalog(Some(stem)).map_err(|e| AdviceError{ message: format!("Failed to query catalog for {}: {}", stem, e) })?;
|
let res = image.query_catalog(Some(stem)).map_err(|e| AdviceError {
|
||||||
for p in res { out.push((p.publisher, p.fmri)); }
|
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());
|
ctx.catalog_cache.insert(stem.to_string(), out.clone());
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_manifest_cached(image: &Image, ctx: &mut Ctx, fmri: &Fmri) -> Result<Manifest, AdviceError> {
|
fn get_manifest_cached(image: &Image, ctx: &mut Ctx, fmri: &Fmri) -> Result<Manifest, AdviceError> {
|
||||||
let key = fmri.to_string();
|
let key = fmri.to_string();
|
||||||
if let Some(m) = ctx.manifest_cache.get(&key) { return Ok(m.clone()); }
|
if let Some(m) = ctx.manifest_cache.get(&key) {
|
||||||
let manifest_opt = image.get_manifest_from_catalog(fmri).map_err(|e| AdviceError { message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e) })?;
|
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);
|
let manifest = manifest_opt.unwrap_or_else(Manifest::new);
|
||||||
ctx.manifest_cache.insert(key, manifest.clone());
|
ctx.manifest_cache.insert(key, manifest.clone());
|
||||||
Ok(manifest)
|
Ok(manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_incorporated_release_cached(image: &Image, ctx: &mut Ctx, stem: &str) -> Result<Option<String>, AdviceError> {
|
fn get_incorporated_release_cached(
|
||||||
if let Some(v) = ctx.lock_cache.get(stem) { return Ok(v.clone()); }
|
image: &Image,
|
||||||
let v = image.get_incorporated_release(stem).map_err(|e| AdviceError{ message: format!("Failed to read incorporation lock for {}: {}", stem, e) })?;
|
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());
|
ctx.lock_cache.insert(stem.to_string(), v.clone());
|
||||||
Ok(v)
|
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
|
// Create a temporary directory for the test
|
||||||
let temp_dir = tempdir().unwrap();
|
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
|
// Create a JSON manifest in the new format
|
||||||
let json_manifest = r#"{
|
let json_manifest = r#"{
|
||||||
|
|
@ -120,7 +120,7 @@ mod tests {
|
||||||
Ok(manifest) => {
|
Ok(manifest) => {
|
||||||
println!("Manifest parsing succeeded");
|
println!("Manifest parsing succeeded");
|
||||||
manifest
|
manifest
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Manifest parsing failed: {:?}", e);
|
println!("Manifest parsing failed: {:?}", e);
|
||||||
panic!("Failed to parse manifest: {:?}", e);
|
panic!("Failed to parse manifest: {:?}", e);
|
||||||
|
|
@ -142,7 +142,10 @@ mod tests {
|
||||||
assert_eq!(parsed_manifest.attributes[1].values[0], "true");
|
assert_eq!(parsed_manifest.attributes[1].values[0], "true");
|
||||||
|
|
||||||
// Check third attribute
|
// 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");
|
assert_eq!(parsed_manifest.attributes[2].values[0], "userland");
|
||||||
|
|
||||||
// Verify that properties is empty but exists
|
// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::actions::{Attr, File};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::actions::{Attr, File};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn add_default_set_attr() {
|
fn add_default_set_attr() {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ use libips::image::{Image, ImageType};
|
||||||
|
|
||||||
fn should_run_network_tests() -> bool {
|
fn should_run_network_tests() -> bool {
|
||||||
// Even when ignored, provide an env switch to document intent
|
// 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]
|
#[test]
|
||||||
|
|
@ -38,7 +40,8 @@ fn e2e_download_and_build_catalog_openindiana() {
|
||||||
let img_path = temp.path().join("image");
|
let img_path = temp.path().join("image");
|
||||||
|
|
||||||
// Create the 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
|
// Add OpenIndiana publisher
|
||||||
let publisher = "openindiana.org";
|
let publisher = "openindiana.org";
|
||||||
|
|
@ -52,12 +55,12 @@ fn e2e_download_and_build_catalog_openindiana() {
|
||||||
.download_publisher_catalog(publisher)
|
.download_publisher_catalog(publisher)
|
||||||
.expect("failed to download publisher catalog");
|
.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
|
// Query catalog; we expect at least one package
|
||||||
let packages = image
|
let packages = image.query_catalog(None).expect("failed to query catalog");
|
||||||
.query_catalog(None)
|
|
||||||
.expect("failed to query catalog");
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!packages.is_empty(),
|
!packages.is_empty(),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
use libips::actions::executors::InstallerError as LibInstallerError;
|
||||||
use libips::fmri::FmriError;
|
use libips::fmri::FmriError;
|
||||||
use libips::image::ImageError;
|
use libips::image::ImageError;
|
||||||
use libips::solver::SolverError;
|
use libips::solver::SolverError;
|
||||||
use libips::actions::executors::InstallerError as LibInstallerError;
|
|
||||||
use miette::Diagnostic;
|
use miette::Diagnostic;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
@ -12,17 +12,11 @@ pub type Result<T> = std::result::Result<T, Pkg6Error>;
|
||||||
#[derive(Debug, Error, Diagnostic)]
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
pub enum Pkg6Error {
|
pub enum Pkg6Error {
|
||||||
#[error("I/O error: {0}")]
|
#[error("I/O error: {0}")]
|
||||||
#[diagnostic(
|
#[diagnostic(code(pkg6::io_error), help("Check system resources and permissions"))]
|
||||||
code(pkg6::io_error),
|
|
||||||
help("Check system resources and permissions")
|
|
||||||
)]
|
|
||||||
IoError(#[from] std::io::Error),
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("JSON error: {0}")]
|
#[error("JSON error: {0}")]
|
||||||
#[diagnostic(
|
#[diagnostic(code(pkg6::json_error), help("Check the JSON format and try again"))]
|
||||||
code(pkg6::json_error),
|
|
||||||
help("Check the JSON format and try again")
|
|
||||||
)]
|
|
||||||
JsonError(#[from] serde_json::Error),
|
JsonError(#[from] serde_json::Error),
|
||||||
|
|
||||||
#[error("FMRI error: {0}")]
|
#[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 clap::{Parser, Subcommand};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
use tracing_subscriber::filter::LevelFilter;
|
use tracing_subscriber::filter::LevelFilter;
|
||||||
|
|
@ -507,7 +507,11 @@ fn main() -> Result<()> {
|
||||||
let cli = App::parse();
|
let cli = App::parse();
|
||||||
|
|
||||||
match &cli.command {
|
match &cli.command {
|
||||||
Commands::Refresh { full, quiet, publishers } => {
|
Commands::Refresh {
|
||||||
|
full,
|
||||||
|
quiet,
|
||||||
|
publishers,
|
||||||
|
} => {
|
||||||
info!("Refreshing package catalog");
|
info!("Refreshing package catalog");
|
||||||
debug!("Full refresh: {}", full);
|
debug!("Full refresh: {}", full);
|
||||||
debug!("Quiet mode: {}", quiet);
|
debug!("Quiet mode: {}", quiet);
|
||||||
|
|
@ -526,7 +530,9 @@ fn main() -> Result<()> {
|
||||||
error!("Failed to load image from {}: {}", image_path.display(), e);
|
error!("Failed to load image from {}: {}", image_path.display(), e);
|
||||||
if !quiet {
|
if !quiet {
|
||||||
eprintln!("Failed to load image from {}: {}", image_path.display(), e);
|
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());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
|
@ -546,8 +552,19 @@ fn main() -> Result<()> {
|
||||||
println!("Refresh completed successfully");
|
println!("Refresh completed successfully");
|
||||||
}
|
}
|
||||||
Ok(())
|
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);
|
info!("Installing packages: {:?}", pkg_fmri_patterns);
|
||||||
debug!("Dry run: {}", dry_run);
|
debug!("Dry run: {}", dry_run);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
|
|
@ -561,7 +578,9 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
// Determine the image path using the -R argument or default rules
|
// Determine the image path using the -R argument or default rules
|
||||||
let image_path = determine_image_path(cli.image_path.clone());
|
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
|
// Load the image
|
||||||
let image = match libips::image::Image::load(&image_path) {
|
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
|
// a full import or refresh automatically. Run `pkg6 refresh` explicitly
|
||||||
// to update catalogs before installing if needed.
|
// to update catalogs before installing if needed.
|
||||||
if !*quiet {
|
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
|
// Build solver constraints from the provided pkg specs
|
||||||
if pkg_fmri_patterns.is_empty() {
|
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()));
|
return Err(Pkg6Error::Other("no packages specified".to_string()));
|
||||||
}
|
}
|
||||||
let mut constraints: Vec<libips::solver::Constraint> = Vec::new();
|
let mut constraints: Vec<libips::solver::Constraint> = Vec::new();
|
||||||
|
|
@ -601,44 +624,77 @@ fn main() -> Result<()> {
|
||||||
} else {
|
} else {
|
||||||
(name_part.to_string(), None)
|
(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
|
// Resolve install plan
|
||||||
if !quiet { println!("Resolving dependencies..."); }
|
if !quiet {
|
||||||
|
println!("Resolving dependencies...");
|
||||||
|
}
|
||||||
let plan = match libips::solver::resolve_install(&image, &constraints) {
|
let plan = match libips::solver::resolve_install(&image, &constraints) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let mut printed_advice = false;
|
let mut printed_advice = false;
|
||||||
if !*quiet {
|
if !*quiet {
|
||||||
// Attempt to provide user-focused advice on how to resolve dependency issues
|
// 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) {
|
match libips::solver::advice::advise_from_error(&image, &e, opts) {
|
||||||
Ok(report) => {
|
Ok(report) => {
|
||||||
if !report.issues.is_empty() {
|
if !report.issues.is_empty() {
|
||||||
printed_advice = true;
|
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() {
|
for (i, iss) in report.issues.iter().enumerate() {
|
||||||
let constraint_str = {
|
let constraint_str = {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
if let Some(r) = &iss.constraint_release { s.push_str(&format!("release={} ", r)); }
|
if let Some(r) = &iss.constraint_release {
|
||||||
if let Some(b) = &iss.constraint_branch { s.push_str(&format!("branch={}", b)); }
|
s.push_str(&format!("release={} ", r));
|
||||||
|
}
|
||||||
|
if let Some(b) = &iss.constraint_branch {
|
||||||
|
s.push_str(&format!("branch={}", b));
|
||||||
|
}
|
||||||
s.trim().to_string()
|
s.trim().to_string()
|
||||||
};
|
};
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" {}. Missing viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}",
|
" {}. Missing viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}",
|
||||||
i + 1,
|
i + 1,
|
||||||
iss.stem,
|
iss.stem,
|
||||||
if iss.path.is_empty() { iss.stem.clone() } else { iss.path.join(" -> ") },
|
if iss.path.is_empty() {
|
||||||
if constraint_str.is_empty() { "<none>".to_string() } else { constraint_str },
|
iss.stem.clone()
|
||||||
|
} else {
|
||||||
|
iss.path.join(" -> ")
|
||||||
|
},
|
||||||
|
if constraint_str.is_empty() {
|
||||||
|
"<none>".to_string()
|
||||||
|
} else {
|
||||||
|
constraint_str
|
||||||
|
},
|
||||||
iss.details
|
iss.details
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
eprintln!("\nWhat you can try as a user:");
|
eprintln!("\nWhat you can try as a user:");
|
||||||
eprintln!(" • Ensure your catalogs are up to date: 'pkg6 refresh'.");
|
eprintln!(
|
||||||
eprintln!(" • Verify that the required publishers are configured: 'pkg6 publisher'.");
|
" • Ensure your catalogs are up to date: 'pkg6 refresh'."
|
||||||
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!(
|
||||||
|
" • 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) => {
|
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
|
// 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 ap = libips::image::action_plan::ActionPlan::from_install_plan(&plan);
|
||||||
let quiet_mode = *quiet;
|
let quiet_mode = *quiet;
|
||||||
let progress_cb: libips::actions::executors::ProgressCallback = Arc::new(move |evt| {
|
let progress_cb: libips::actions::executors::ProgressCallback = Arc::new(move |evt| {
|
||||||
if quiet_mode { return; }
|
if quiet_mode {
|
||||||
|
return;
|
||||||
|
}
|
||||||
match evt {
|
match evt {
|
||||||
libips::actions::executors::ProgressEvent::StartingPhase { phase, total } => {
|
libips::actions::executors::ProgressEvent::StartingPhase { phase, total } => {
|
||||||
println!("Applying: {} (total {})...", 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);
|
println!("Applying: {} {}/{}", phase, current, total);
|
||||||
}
|
}
|
||||||
libips::actions::executors::ProgressEvent::FinishedPhase { phase, 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 };
|
let apply_opts = libips::actions::executors::ApplyOptions {
|
||||||
if !quiet { println!("Applying action plan (dry-run: {})", dry_run); }
|
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)?;
|
ap.apply(image.path(), &apply_opts)?;
|
||||||
|
|
||||||
// Update installed DB after success (skip on dry-run)
|
// Update installed DB after success (skip on dry-run)
|
||||||
if !*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 total_pkgs = plan.add.len();
|
||||||
let mut idx = 0usize;
|
let mut idx = 0usize;
|
||||||
for rp in &plan.add {
|
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
|
// 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() {
|
if let Err(e) = installed.dump_installed_table() {
|
||||||
error!("Failed to dump installed database: {}", e);
|
error!("Failed to dump installed database: {}", e);
|
||||||
}
|
}
|
||||||
} else if !quiet {
|
} 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");
|
info!("Installation completed successfully");
|
||||||
Ok(())
|
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);
|
info!("Exact-installing packages: {:?}", pkg_fmri_patterns);
|
||||||
debug!("Dry run: {}", dry_run);
|
debug!("Dry run: {}", dry_run);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
|
|
@ -734,8 +825,13 @@ fn main() -> Result<()> {
|
||||||
// Stub implementation
|
// Stub implementation
|
||||||
info!("Exact-installation completed successfully");
|
info!("Exact-installation completed successfully");
|
||||||
Ok(())
|
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);
|
info!("Uninstalling packages: {:?}", pkg_fmri_patterns);
|
||||||
debug!("Dry run: {}", dry_run);
|
debug!("Dry run: {}", dry_run);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
|
|
@ -744,8 +840,19 @@ fn main() -> Result<()> {
|
||||||
// Stub implementation
|
// Stub implementation
|
||||||
info!("Uninstallation completed successfully");
|
info!("Uninstallation completed successfully");
|
||||||
Ok(())
|
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);
|
info!("Updating packages: {:?}", pkg_fmri_patterns);
|
||||||
debug!("Dry run: {}", dry_run);
|
debug!("Dry run: {}", dry_run);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
|
|
@ -760,8 +867,14 @@ fn main() -> Result<()> {
|
||||||
// Stub implementation
|
// Stub implementation
|
||||||
info!("Update completed successfully");
|
info!("Update completed successfully");
|
||||||
Ok(())
|
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);
|
info!("Listing packages: {:?}", pkg_fmri_patterns);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
debug!("Quiet: {}", quiet);
|
debug!("Quiet: {}", quiet);
|
||||||
|
|
@ -777,7 +890,9 @@ fn main() -> Result<()> {
|
||||||
Ok(img) => img,
|
Ok(img) => img,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load image from {}: {}", image_path.display(), 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());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -804,21 +919,27 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
match image.query_catalog(pattern) {
|
match image.query_catalog(pattern) {
|
||||||
Ok(packages) => {
|
Ok(packages) => {
|
||||||
println!("PUBLISHER NAME VERSION STATE");
|
println!(
|
||||||
println!("------------------------------------------------------------------------------------------------------------------------------------------------------");
|
"PUBLISHER NAME VERSION STATE"
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"------------------------------------------------------------------------------------------------------------------------------------------------------"
|
||||||
|
);
|
||||||
for pkg in packages {
|
for pkg in packages {
|
||||||
let state = if image.is_package_installed(&pkg.fmri).unwrap_or(false) {
|
let state = if image.is_package_installed(&pkg.fmri).unwrap_or(false) {
|
||||||
"installed"
|
"installed"
|
||||||
} else {
|
} else {
|
||||||
"known"
|
"known"
|
||||||
};
|
};
|
||||||
println!("{:<40} {:<40} {:<30} {}",
|
println!(
|
||||||
|
"{:<40} {:<40} {:<30} {}",
|
||||||
pkg.fmri.publisher.as_deref().unwrap_or("unknown"),
|
pkg.fmri.publisher.as_deref().unwrap_or("unknown"),
|
||||||
pkg.fmri.name,
|
pkg.fmri.name,
|
||||||
pkg.fmri.version(),
|
pkg.fmri.version(),
|
||||||
state);
|
state
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to query catalog: {}", e);
|
error!("Failed to query catalog: {}", e);
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
|
|
@ -829,16 +950,22 @@ fn main() -> Result<()> {
|
||||||
info!("Listing installed packages");
|
info!("Listing installed packages");
|
||||||
match image.query_installed_packages(pattern) {
|
match image.query_installed_packages(pattern) {
|
||||||
Ok(packages) => {
|
Ok(packages) => {
|
||||||
println!("PUBLISHER NAME VERSION STATE");
|
println!(
|
||||||
println!("------------------------------------------------------------------------------------------------------------------------------------------------------");
|
"PUBLISHER NAME VERSION STATE"
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"------------------------------------------------------------------------------------------------------------------------------------------------------"
|
||||||
|
);
|
||||||
for pkg in packages {
|
for pkg in packages {
|
||||||
println!("{:<40} {:<40} {:<30} {}",
|
println!(
|
||||||
|
"{:<40} {:<40} {:<30} {}",
|
||||||
pkg.fmri.publisher.as_deref().unwrap_or("unknown"),
|
pkg.fmri.publisher.as_deref().unwrap_or("unknown"),
|
||||||
pkg.fmri.name,
|
pkg.fmri.name,
|
||||||
pkg.fmri.version(),
|
pkg.fmri.version(),
|
||||||
"installed");
|
"installed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to query installed packages: {}", e);
|
error!("Failed to query installed packages: {}", e);
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
|
|
@ -848,8 +975,13 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
info!("List completed successfully");
|
info!("List completed successfully");
|
||||||
Ok(())
|
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);
|
info!("Showing info for packages: {:?}", pkg_fmri_patterns);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
debug!("Quiet: {}", quiet);
|
debug!("Quiet: {}", quiet);
|
||||||
|
|
@ -858,8 +990,13 @@ fn main() -> Result<()> {
|
||||||
// Stub implementation
|
// Stub implementation
|
||||||
info!("Info completed successfully");
|
info!("Info completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Commands::Search { verbose, quiet, output_format, query } => {
|
Commands::Search {
|
||||||
|
verbose,
|
||||||
|
quiet,
|
||||||
|
output_format,
|
||||||
|
query,
|
||||||
|
} => {
|
||||||
info!("Searching for packages matching: {}", query);
|
info!("Searching for packages matching: {}", query);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
debug!("Quiet: {}", quiet);
|
debug!("Quiet: {}", quiet);
|
||||||
|
|
@ -868,8 +1005,12 @@ fn main() -> Result<()> {
|
||||||
// Stub implementation
|
// Stub implementation
|
||||||
info!("Search completed successfully");
|
info!("Search completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Commands::Verify { verbose, quiet, pkg_fmri_patterns } => {
|
Commands::Verify {
|
||||||
|
verbose,
|
||||||
|
quiet,
|
||||||
|
pkg_fmri_patterns,
|
||||||
|
} => {
|
||||||
info!("Verifying packages: {:?}", pkg_fmri_patterns);
|
info!("Verifying packages: {:?}", pkg_fmri_patterns);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
debug!("Quiet: {}", quiet);
|
debug!("Quiet: {}", quiet);
|
||||||
|
|
@ -877,8 +1018,13 @@ fn main() -> Result<()> {
|
||||||
// Stub implementation
|
// Stub implementation
|
||||||
info!("Verification completed successfully");
|
info!("Verification completed successfully");
|
||||||
Ok(())
|
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);
|
info!("Fixing packages: {:?}", pkg_fmri_patterns);
|
||||||
debug!("Dry run: {}", dry_run);
|
debug!("Dry run: {}", dry_run);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
|
|
@ -887,8 +1033,12 @@ fn main() -> Result<()> {
|
||||||
// Stub implementation
|
// Stub implementation
|
||||||
info!("Fix completed successfully");
|
info!("Fix completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Commands::History { count, full, output_format } => {
|
Commands::History {
|
||||||
|
count,
|
||||||
|
full,
|
||||||
|
output_format,
|
||||||
|
} => {
|
||||||
info!("Showing history");
|
info!("Showing history");
|
||||||
debug!("Count: {:?}", count);
|
debug!("Count: {:?}", count);
|
||||||
debug!("Full: {}", full);
|
debug!("Full: {}", full);
|
||||||
|
|
@ -897,8 +1047,13 @@ fn main() -> Result<()> {
|
||||||
// Stub implementation
|
// Stub implementation
|
||||||
info!("History completed successfully");
|
info!("History completed successfully");
|
||||||
Ok(())
|
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);
|
info!("Showing contents for packages: {:?}", pkg_fmri_patterns);
|
||||||
debug!("Verbose: {}", verbose);
|
debug!("Verbose: {}", verbose);
|
||||||
debug!("Quiet: {}", quiet);
|
debug!("Quiet: {}", quiet);
|
||||||
|
|
@ -907,8 +1062,12 @@ fn main() -> Result<()> {
|
||||||
// Stub implementation
|
// Stub implementation
|
||||||
info!("Contents completed successfully");
|
info!("Contents completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Commands::SetPublisher { publisher, origin, mirror } => {
|
Commands::SetPublisher {
|
||||||
|
publisher,
|
||||||
|
origin,
|
||||||
|
mirror,
|
||||||
|
} => {
|
||||||
info!("Setting publisher: {}", publisher);
|
info!("Setting publisher: {}", publisher);
|
||||||
debug!("Origin: {:?}", origin);
|
debug!("Origin: {:?}", origin);
|
||||||
debug!("Mirror: {:?}", mirror);
|
debug!("Mirror: {:?}", mirror);
|
||||||
|
|
@ -922,7 +1081,9 @@ fn main() -> Result<()> {
|
||||||
Ok(img) => img,
|
Ok(img) => img,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load image from {}: {}", image_path.display(), 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());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -937,7 +1098,10 @@ fn main() -> Result<()> {
|
||||||
if let Some(origin_url) = origin {
|
if let Some(origin_url) = origin {
|
||||||
// Add or update the publisher
|
// Add or update the publisher
|
||||||
image.add_publisher(&publisher, &origin_url, mirrors, true)?;
|
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
|
// Download the catalog
|
||||||
image.download_publisher_catalog(&publisher)?;
|
image.download_publisher_catalog(&publisher)?;
|
||||||
|
|
@ -955,13 +1119,15 @@ fn main() -> Result<()> {
|
||||||
info!("Publisher {} set as default", publisher);
|
info!("Publisher {} set as default", publisher);
|
||||||
} else {
|
} else {
|
||||||
error!("Publisher {} not found and no origin provided", publisher);
|
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");
|
info!("Set-publisher completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Commands::UnsetPublisher { publisher } => {
|
Commands::UnsetPublisher { publisher } => {
|
||||||
info!("Unsetting publisher: {}", publisher);
|
info!("Unsetting publisher: {}", publisher);
|
||||||
|
|
||||||
|
|
@ -974,7 +1140,9 @@ fn main() -> Result<()> {
|
||||||
Ok(img) => img,
|
Ok(img) => img,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load image from {}: {}", image_path.display(), 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());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -993,8 +1161,12 @@ fn main() -> Result<()> {
|
||||||
info!("Publisher {} removed successfully", publisher);
|
info!("Publisher {} removed successfully", publisher);
|
||||||
info!("Unset-publisher completed successfully");
|
info!("Unset-publisher completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Commands::Publisher { verbose, output_format, publishers } => {
|
Commands::Publisher {
|
||||||
|
verbose,
|
||||||
|
output_format,
|
||||||
|
publishers,
|
||||||
|
} => {
|
||||||
info!("Showing publisher information");
|
info!("Showing publisher information");
|
||||||
|
|
||||||
// Determine the image path using the -R argument or default rules
|
// Determine the image path using the -R argument or default rules
|
||||||
|
|
@ -1006,7 +1178,9 @@ fn main() -> Result<()> {
|
||||||
Ok(img) => img,
|
Ok(img) => img,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load image from {}: {}", image_path.display(), 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());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1076,7 +1250,10 @@ fn main() -> Result<()> {
|
||||||
println!(" {}", mirror);
|
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 {
|
if let Some(catalog_dir) = &publisher.catalog_dir {
|
||||||
println!(" Catalog directory: {}", 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
|
// Explicitly flush stdout after each publisher to ensure output is displayed
|
||||||
let _ = std::io::stdout().flush();
|
let _ = std::io::stdout().flush();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"json" => {
|
"json" => {
|
||||||
// Display in JSON format
|
// Display in JSON format
|
||||||
// This format is useful for programmatic access to the publisher information
|
// 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));
|
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
||||||
println!("{}", json);
|
println!("{}", json);
|
||||||
let _ = std::io::stdout().flush();
|
let _ = std::io::stdout().flush();
|
||||||
},
|
}
|
||||||
"tsv" => {
|
"tsv" => {
|
||||||
// Display in TSV format (tab-separated values)
|
// Display in TSV format (tab-separated values)
|
||||||
// This format is useful for importing into spreadsheets or other data processing tools
|
// 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 default = if publisher.is_default { "Yes" } else { "No" };
|
||||||
let catalog_dir = publisher.catalog_dir.as_deref().unwrap_or("");
|
let catalog_dir = publisher.catalog_dir.as_deref().unwrap_or("");
|
||||||
|
|
||||||
println!("{}\t{}\t{}\t{}\t{}",
|
println!(
|
||||||
publisher.name,
|
"{}\t{}\t{}\t{}\t{}",
|
||||||
publisher.origin,
|
publisher.name, publisher.origin, mirrors, default, catalog_dir
|
||||||
mirrors,
|
|
||||||
default,
|
|
||||||
catalog_dir
|
|
||||||
);
|
);
|
||||||
let _ = std::io::stdout().flush();
|
let _ = std::io::stdout().flush();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Unsupported format
|
// Unsupported format
|
||||||
return Err(Pkg6Error::UnsupportedOutputFormat(output_format_str.to_string()));
|
return Err(Pkg6Error::UnsupportedOutputFormat(
|
||||||
|
output_format_str.to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Publisher completed successfully");
|
info!("Publisher completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Commands::ImageCreate { full_path, publisher, origin, image_type } => {
|
Commands::ImageCreate {
|
||||||
|
full_path,
|
||||||
|
publisher,
|
||||||
|
origin,
|
||||||
|
image_type,
|
||||||
|
} => {
|
||||||
info!("Creating image at: {}", full_path.display());
|
info!("Creating image at: {}", full_path.display());
|
||||||
debug!("Publisher: {:?}", publisher);
|
debug!("Publisher: {:?}", publisher);
|
||||||
debug!("Origin: {:?}", origin);
|
debug!("Origin: {:?}", origin);
|
||||||
|
|
@ -1148,21 +1329,36 @@ fn main() -> Result<()> {
|
||||||
info!("Image created successfully at: {}", full_path.display());
|
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 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()) {
|
if let (Some(publisher_name), Some(origin_url)) = (publisher.as_ref(), origin.as_ref())
|
||||||
info!("Adding publisher {} with origin {}", publisher_name, origin_url);
|
{
|
||||||
|
info!(
|
||||||
|
"Adding publisher {} with origin {}",
|
||||||
|
publisher_name, origin_url
|
||||||
|
);
|
||||||
|
|
||||||
// Add the publisher
|
// Add the publisher
|
||||||
image.add_publisher(publisher_name, origin_url, vec![], true)?;
|
image.add_publisher(publisher_name, origin_url, vec![], true)?;
|
||||||
|
|
||||||
info!("Publisher {} configured with origin: {}", publisher_name, origin_url);
|
info!(
|
||||||
info!("Catalogs are not downloaded during image creation. Use 'pkg6 -R {} refresh {}' to download and open catalogs.", full_path.display(), publisher_name);
|
"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 {
|
} else {
|
||||||
info!("No publisher configured. Use 'pkg6 set-publisher' to add a publisher.");
|
info!("No publisher configured. Use 'pkg6 set-publisher' to add a publisher.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Commands::DebugDb { stats, dump_all, dump_table } => {
|
Commands::DebugDb {
|
||||||
|
stats,
|
||||||
|
dump_all,
|
||||||
|
dump_table,
|
||||||
|
} => {
|
||||||
info!("Debug database command");
|
info!("Debug database command");
|
||||||
debug!("Stats: {}", stats);
|
debug!("Stats: {}", stats);
|
||||||
debug!("Dump all: {}", dump_all);
|
debug!("Dump all: {}", dump_all);
|
||||||
|
|
@ -1177,7 +1373,9 @@ fn main() -> Result<()> {
|
||||||
Ok(img) => img,
|
Ok(img) => img,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load image from {}: {}", image_path.display(), 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());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1186,13 +1384,12 @@ fn main() -> Result<()> {
|
||||||
let catalog = libips::image::catalog::ImageCatalog::new(
|
let catalog = libips::image::catalog::ImageCatalog::new(
|
||||||
image.catalog_dir(),
|
image.catalog_dir(),
|
||||||
image.catalog_db_path(),
|
image.catalog_db_path(),
|
||||||
image.obsoleted_db_path()
|
image.obsoleted_db_path(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create an installed packages object for the installed.redb database
|
// Create an installed packages object for the installed.redb database
|
||||||
let installed = libips::image::installed::InstalledPackages::new(
|
let installed =
|
||||||
image.installed_db_path()
|
libips::image::installed::InstalledPackages::new(image.installed_db_path());
|
||||||
);
|
|
||||||
|
|
||||||
// Execute the requested debug command
|
// Execute the requested debug command
|
||||||
if *stats {
|
if *stats {
|
||||||
|
|
@ -1200,13 +1397,19 @@ fn main() -> Result<()> {
|
||||||
println!("=== CATALOG DATABASE ===");
|
println!("=== CATALOG DATABASE ===");
|
||||||
if let Err(e) = catalog.get_db_stats() {
|
if let Err(e) = catalog.get_db_stats() {
|
||||||
error!("Failed to get catalog database statistics: {}", e);
|
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 ===");
|
println!("\n=== INSTALLED DATABASE ===");
|
||||||
if let Err(e) = installed.get_db_stats() {
|
if let Err(e) = installed.get_db_stats() {
|
||||||
error!("Failed to get installed database statistics: {}", e);
|
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 ===");
|
println!("=== CATALOG DATABASE ===");
|
||||||
if let Err(e) = catalog.dump_all_tables() {
|
if let Err(e) = catalog.dump_all_tables() {
|
||||||
error!("Failed to dump catalog database tables: {}", e);
|
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 ===");
|
println!("\n=== INSTALLED DATABASE ===");
|
||||||
if let Err(e) = installed.dump_installed_table() {
|
if let Err(e) = installed.dump_installed_table() {
|
||||||
error!("Failed to dump installed database table: {}", e);
|
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 ===");
|
println!("=== INSTALLED DATABASE ===");
|
||||||
if let Err(e) = installed.dump_installed_table() {
|
if let Err(e) = installed.dump_installed_table() {
|
||||||
error!("Failed to dump installed table: {}", e);
|
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" => {
|
"catalog" | "obsoleted" => {
|
||||||
// Use the catalog database
|
// Use the catalog database
|
||||||
println!("=== CATALOG DATABASE ===");
|
println!("=== CATALOG DATABASE ===");
|
||||||
if let Err(e) = catalog.dump_table(table_name) {
|
if let Err(e) = catalog.dump_table(table_name) {
|
||||||
error!("Failed to dump table {}: {}", table_name, e);
|
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);
|
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");
|
info!("Debug database command completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
use crate::errors::DepotError;
|
use crate::errors::DepotError;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, knuffel::Decode, Clone)]
|
#[derive(Debug, knuffel::Decode, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
|
@ -84,8 +84,9 @@ impl Config {
|
||||||
pub fn load(path: Option<PathBuf>) -> crate::errors::Result<Self> {
|
pub fn load(path: Option<PathBuf>) -> crate::errors::Result<Self> {
|
||||||
let path = path.unwrap_or_else(|| PathBuf::from("pkg6depotd.kdl"));
|
let path = path.unwrap_or_else(|| PathBuf::from("pkg6depotd.kdl"));
|
||||||
|
|
||||||
let content = fs::read_to_string(&path)
|
let content = fs::read_to_string(&path).map_err(|e| {
|
||||||
.map_err(|e| DepotError::Config(format!("Failed to read config file {:?}: {}", path, e)))?;
|
DepotError::Config(format!("Failed to read config file {:?}: {}", path, e))
|
||||||
|
})?;
|
||||||
|
|
||||||
knuffel::parse(path.to_str().unwrap_or("pkg6depotd.kdl"), &content)
|
knuffel::parse(path.to_str().unwrap_or("pkg6depotd.kdl"), &content)
|
||||||
.map_err(|e| DepotError::Config(format!("Failed to parse config: {:?}", e)))
|
.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 miette::Diagnostic;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use axum::{
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
http::StatusCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
pub enum DepotError {
|
pub enum DepotError {
|
||||||
|
|
@ -31,8 +31,12 @@ pub enum DepotError {
|
||||||
impl IntoResponse for DepotError {
|
impl IntoResponse for DepotError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let (status, message) = match &self {
|
let (status, message) = match &self {
|
||||||
DepotError::Repo(libips::repository::RepositoryError::NotFound(_)) => (StatusCode::NOT_FOUND, self.to_string()),
|
DepotError::Repo(libips::repository::RepositoryError::NotFound(_)) => {
|
||||||
DepotError::Repo(libips::repository::RepositoryError::PublisherNotFound(_)) => (StatusCode::NOT_FOUND, self.to_string()),
|
(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()),
|
_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
|
Json,
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
Json,
|
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -14,9 +14,7 @@ struct HealthResponse {
|
||||||
status: &'static str,
|
status: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn health(
|
pub async fn health(_state: State<Arc<DepotRepo>>) -> impl IntoResponse {
|
||||||
_state: State<Arc<DepotRepo>>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
// Basic liveness/readiness for now. Future: include repo checks.
|
// Basic liveness/readiness for now. Future: include repo checks.
|
||||||
(StatusCode::OK, Json(HealthResponse { status: "ok" }))
|
(StatusCode::OK, Json(HealthResponse { status: "ok" }))
|
||||||
}
|
}
|
||||||
|
|
@ -33,11 +31,10 @@ struct AuthCheckResponse<'a> {
|
||||||
/// Admin auth-check endpoint.
|
/// Admin auth-check endpoint.
|
||||||
/// For now, this is a minimal placeholder that only checks for the presence of a Bearer token.
|
/// 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.
|
/// TODO: Validate JWT via OIDC JWKs using configured issuer/jwks_uri and required scopes.
|
||||||
pub async fn auth_check(
|
pub async fn auth_check(_state: State<Arc<DepotRepo>>, headers: HeaderMap) -> Response {
|
||||||
_state: State<Arc<DepotRepo>>,
|
let auth = headers
|
||||||
headers: HeaderMap,
|
.get(axum::http::header::AUTHORIZATION)
|
||||||
) -> Response {
|
.and_then(|v| v.to_str().ok());
|
||||||
let auth = headers.get(axum::http::header::AUTHORIZATION).and_then(|v| v.to_str().ok());
|
|
||||||
let (authenticated, token_present) = match auth {
|
let (authenticated, token_present) = match auth {
|
||||||
Some(h) if h.to_ascii_lowercase().starts_with("bearer ") => (true, true),
|
Some(h) if h.to_ascii_lowercase().starts_with("bearer ") => (true, true),
|
||||||
Some(_) => (false, true),
|
Some(_) => (false, true),
|
||||||
|
|
@ -52,6 +49,10 @@ pub async fn auth_check(
|
||||||
decision: if authenticated { "allow" } else { "deny" },
|
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()
|
(status, Json(resp)).into_response()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
|
use crate::errors::DepotError;
|
||||||
|
use crate::repo::DepotRepo;
|
||||||
|
use axum::http::header;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State, Request},
|
extract::{Path, Request, State},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::repo::DepotRepo;
|
|
||||||
use crate::errors::DepotError;
|
|
||||||
use tower_http::services::ServeFile;
|
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
use axum::http::header;
|
use tower_http::services::ServeFile;
|
||||||
|
|
||||||
pub async fn get_catalog_v1(
|
pub async fn get_catalog_v1(
|
||||||
State(repo): State<Arc<DepotRepo>>,
|
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
|
// Ensure correct content-type for JSON catalog artifacts regardless of file extension
|
||||||
let is_catalog_json = filename == "catalog.attrs" || filename.starts_with("catalog.");
|
let is_catalog_json = filename == "catalog.attrs" || filename.starts_with("catalog.");
|
||||||
if is_catalog_json {
|
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())
|
Ok(res.into_response())
|
||||||
},
|
}
|
||||||
Err(e) => Err(DepotError::Server(e.to_string())),
|
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 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 httpdate::fmt_http_date;
|
||||||
|
use std::fs;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use tower::ServiceExt;
|
||||||
|
use tower_http::services::ServeFile;
|
||||||
|
|
||||||
pub async fn get_file(
|
pub async fn get_file(
|
||||||
State(repo): State<Arc<DepotRepo>>,
|
State(repo): State<Arc<DepotRepo>>,
|
||||||
Path((publisher, _algo, digest)): Path<(String, String, String)>,
|
Path((publisher, _algo, digest)): Path<(String, String, String)>,
|
||||||
req: Request,
|
req: Request,
|
||||||
) -> Result<Response, DepotError> {
|
) -> Result<Response, DepotError> {
|
||||||
let path = repo.get_file_path(&publisher, &digest)
|
let path = repo.get_file_path(&publisher, &digest).ok_or_else(|| {
|
||||||
.ok_or_else(|| DepotError::Repo(libips::repository::RepositoryError::NotFound(digest.clone())))?;
|
DepotError::Repo(libips::repository::RepositoryError::NotFound(
|
||||||
|
digest.clone(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
let service = ServeFile::new(path);
|
let service = ServeFile::new(path);
|
||||||
let result = service.oneshot(req).await;
|
let result = service.oneshot(req).await;
|
||||||
|
|
@ -27,26 +30,42 @@ pub async fn get_file(
|
||||||
Ok(mut res) => {
|
Ok(mut res) => {
|
||||||
// Add caching headers
|
// Add caching headers
|
||||||
let max_age = repo.cache_max_age();
|
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
|
// 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
|
// Last-Modified from fs metadata
|
||||||
if let Some(body_path) = res.extensions().get::<std::path::PathBuf>().cloned() {
|
if let Some(body_path) = res.extensions().get::<std::path::PathBuf>().cloned() {
|
||||||
if let Ok(meta) = fs::metadata(&body_path) {
|
if let Ok(meta) = fs::metadata(&body_path) {
|
||||||
if let Ok(mtime) = meta.modified() {
|
if let Ok(mtime) = meta.modified() {
|
||||||
let lm = fmt_http_date(mtime);
|
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)
|
// Fallback: use now if extension not present (should rarely happen)
|
||||||
if !res.headers().contains_key(header::LAST_MODIFIED) {
|
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);
|
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())
|
Ok(res.into_response())
|
||||||
},
|
}
|
||||||
Err(e) => Err(DepotError::Server(e.to_string())),
|
Err(e) => Err(DepotError::Server(e.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,33 @@
|
||||||
|
use crate::errors::DepotError;
|
||||||
|
use crate::repo::DepotRepo;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{IntoResponse, Response},
|
|
||||||
http::header,
|
http::header,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use chrono::{Datelike, NaiveDateTime, TimeZone, Timelike, Utc};
|
||||||
use crate::repo::DepotRepo;
|
|
||||||
use crate::errors::DepotError;
|
|
||||||
use libips::fmri::Fmri;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use libips::actions::Manifest;
|
use libips::actions::Manifest;
|
||||||
use chrono::{NaiveDateTime, Utc, TimeZone, Datelike, Timelike};
|
|
||||||
use libips::actions::Property;
|
use libips::actions::Property;
|
||||||
|
use libips::fmri::Fmri;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Read as _;
|
use std::io::Read as _;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub async fn get_info(
|
pub async fn get_info(
|
||||||
State(repo): State<Arc<DepotRepo>>,
|
State(repo): State<Arc<DepotRepo>>,
|
||||||
Path((publisher, fmri_str)): Path<(String, String)>,
|
Path((publisher, fmri_str)): Path<(String, String)>,
|
||||||
) -> Result<Response, DepotError> {
|
) -> 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 content = repo.get_manifest_text(&publisher, &fmri)?;
|
||||||
|
|
||||||
let manifest = match serde_json::from_str::<Manifest>(&content) {
|
let manifest = match serde_json::from_str::<Manifest>(&content) {
|
||||||
Ok(m) => m,
|
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();
|
let mut out = String::new();
|
||||||
|
|
@ -46,17 +49,27 @@ pub async fn get_info(
|
||||||
if let Some((rel_branch, ts)) = rest.split_once(':') {
|
if let Some((rel_branch, ts)) = rest.split_once(':') {
|
||||||
ts_str = Some(ts.to_string());
|
ts_str = Some(ts.to_string());
|
||||||
if let Some((rel, br)) = rel_branch.split_once('-') {
|
if let Some((rel, br)) = rel_branch.split_once('-') {
|
||||||
if !rel.is_empty() { build_release = Some(rel.to_string()); }
|
if !rel.is_empty() {
|
||||||
if !br.is_empty() { branch = Some(br.to_string()); }
|
build_release = Some(rel.to_string());
|
||||||
|
}
|
||||||
|
if !br.is_empty() {
|
||||||
|
branch = Some(br.to_string());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No branch
|
// 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 {
|
} else {
|
||||||
// No timestamp
|
// No timestamp
|
||||||
if let Some((rel, br)) = rest.split_once('-') {
|
if let Some((rel, br)) = rest.split_once('-') {
|
||||||
if !rel.is_empty() { build_release = Some(rel.to_string()); }
|
if !rel.is_empty() {
|
||||||
if !br.is_empty() { branch = Some(br.to_string()); }
|
build_release = Some(rel.to_string());
|
||||||
|
}
|
||||||
|
if !br.is_empty() {
|
||||||
|
branch = Some(br.to_string());
|
||||||
|
}
|
||||||
} else if !rest.is_empty() {
|
} else if !rest.is_empty() {
|
||||||
build_release = Some(rest.to_string());
|
build_release = Some(rest.to_string());
|
||||||
}
|
}
|
||||||
|
|
@ -64,8 +77,12 @@ pub async fn get_info(
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push_str(&format!("Version: {}\n", version_core));
|
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(rel) = build_release {
|
||||||
if let Some(br) = branch { out.push_str(&format!("Branch: {}\n", br)); }
|
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)) {
|
if let Some(ts) = ts_str.and_then(|s| format_packaging_date(&s)) {
|
||||||
out.push_str(&format!("Packaging Date: {}\n", ts));
|
out.push_str(&format!("Packaging Date: {}\n", ts));
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +106,9 @@ pub async fn get_info(
|
||||||
out.push_str("\nLicense:\n");
|
out.push_str("\nLicense:\n");
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
for license in &manifest.licenses {
|
for license in &manifest.licenses {
|
||||||
if !first { out.push('\n'); }
|
if !first {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
first = false;
|
first = false;
|
||||||
|
|
||||||
// Optional license name header for readability
|
// Optional license name header for readability
|
||||||
|
|
@ -105,20 +124,22 @@ pub async fn get_info(
|
||||||
match resolve_license_text(&repo, &publisher, digest) {
|
match resolve_license_text(&repo, &publisher, digest) {
|
||||||
Some(text) => {
|
Some(text) => {
|
||||||
out.push_str(&text);
|
out.push_str(&text);
|
||||||
if !text.ends_with('\n') { out.push('\n'); }
|
if !text.ends_with('\n') {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Fallback: show the digest if content could not be resolved
|
// 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((
|
Ok(([(header::CONTENT_TYPE, "text/plain")], out).into_response())
|
||||||
[(header::CONTENT_TYPE, "text/plain")],
|
|
||||||
out
|
|
||||||
).into_response())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to read and decode the license text for a given digest from the repository.
|
// 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();
|
let mut text = String::from_utf8_lossy(&data).to_string();
|
||||||
if truncated {
|
if truncated {
|
||||||
if !text.ends_with('\n') { text.push('\n'); }
|
if !text.ends_with('\n') {
|
||||||
|
text.push('\n');
|
||||||
|
}
|
||||||
text.push_str("...[truncated]\n");
|
text.push_str("...[truncated]\n");
|
||||||
}
|
}
|
||||||
Some(text)
|
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> {
|
fn find_attr(manifest: &Manifest, key: &str) -> Option<String> {
|
||||||
for attr in &manifest.attributes {
|
for attr in &manifest.attributes {
|
||||||
if attr.key == key {
|
if attr.key == key {
|
||||||
return attr.values.first().cloned();
|
return attr.values.first().cloned();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
@ -187,17 +210,32 @@ fn month_name(month: u32) -> &'static str {
|
||||||
|
|
||||||
fn format_packaging_date(ts: &str) -> Option<String> {
|
fn format_packaging_date(ts: &str) -> Option<String> {
|
||||||
// Expect formats like YYYYMMDDThhmmssZ or with fractional seconds before Z
|
// 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 ndt = NaiveDateTime::parse_from_str(&clean_ts, "%Y%m%dT%H%M%SZ").ok()?;
|
||||||
let dt_utc = Utc.from_utc_datetime(&ndt);
|
let dt_utc = Utc.from_utc_datetime(&ndt);
|
||||||
let month = month_name(dt_utc.month() as u32);
|
let month = month_name(dt_utc.month() as u32);
|
||||||
let day = dt_utc.day();
|
let day = dt_utc.day();
|
||||||
let year = dt_utc.year();
|
let year = dt_utc.year();
|
||||||
let hour24 = dt_utc.hour();
|
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 minute = dt_utc.minute();
|
||||||
let second = dt_utc.second();
|
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
|
// 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 file in &manifest.files {
|
||||||
for Property { key, value } in &file.properties {
|
for Property { key, value } in &file.properties {
|
||||||
if key == "pkg.size" {
|
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" {
|
} 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::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{IntoResponse, Response},
|
|
||||||
http::header,
|
http::header,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
|
||||||
use crate::repo::DepotRepo;
|
|
||||||
use crate::errors::DepotError;
|
|
||||||
use libips::fmri::Fmri;
|
use libips::fmri::Fmri;
|
||||||
use std::str::FromStr;
|
|
||||||
use sha1::Digest as _;
|
use sha1::Digest as _;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub async fn get_manifest(
|
pub async fn get_manifest(
|
||||||
State(repo): State<Arc<DepotRepo>>,
|
State(repo): State<Arc<DepotRepo>>,
|
||||||
Path((publisher, fmri_str)): Path<(String, String)>,
|
Path((publisher, fmri_str)): Path<(String, String)>,
|
||||||
) -> Result<Response, DepotError> {
|
) -> 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 content = repo.get_manifest_text(&publisher, &fmri)?;
|
||||||
// Compute weak ETag from SHA-1 of manifest content (legacy friendly)
|
// 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()),
|
(header::ETAG, etag.as_str()),
|
||||||
],
|
],
|
||||||
content,
|
content,
|
||||||
).into_response())
|
)
|
||||||
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod versions;
|
|
||||||
pub mod catalog;
|
pub mod catalog;
|
||||||
pub mod manifest;
|
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
|
pub mod manifest;
|
||||||
pub mod publisher;
|
pub mod publisher;
|
||||||
|
pub mod versions;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
|
use crate::errors::DepotError;
|
||||||
|
use crate::repo::DepotRepo;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{IntoResponse, Response},
|
|
||||||
http::header,
|
http::header,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
|
||||||
use crate::repo::DepotRepo;
|
|
||||||
use crate::errors::DepotError;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct P5iPublisherInfo {
|
struct P5iPublisherInfo {
|
||||||
|
|
@ -43,10 +43,13 @@ async fn get_publisher_impl(
|
||||||
) -> Result<Response, DepotError> {
|
) -> Result<Response, DepotError> {
|
||||||
let repo_info = repo.get_info()?;
|
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 {
|
if let Some(p) = pub_info {
|
||||||
let p5i = P5iFile {
|
let p5i = P5iFile {
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
publishers: vec![P5iPublisherInfo {
|
publishers: vec![P5iPublisherInfo {
|
||||||
alias: None,
|
alias: None,
|
||||||
|
|
@ -56,12 +59,12 @@ async fn get_publisher_impl(
|
||||||
}],
|
}],
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?;
|
let json =
|
||||||
Ok((
|
serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?;
|
||||||
[(header::CONTENT_TYPE, "application/vnd.pkg5.info")],
|
Ok(([(header::CONTENT_TYPE, "application/vnd.pkg5.info")], json).into_response())
|
||||||
json
|
|
||||||
).into_response())
|
|
||||||
} else {
|
} 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 {
|
let response = VersionsResponse {
|
||||||
server_version,
|
server_version,
|
||||||
operations: vec![
|
operations: vec![
|
||||||
SupportedOperation { op: Operation::Info, versions: vec![0] },
|
SupportedOperation {
|
||||||
SupportedOperation { op: Operation::Versions, versions: vec![0] },
|
op: Operation::Info,
|
||||||
SupportedOperation { op: Operation::Catalog, versions: vec![1] },
|
versions: vec![0],
|
||||||
SupportedOperation { op: Operation::Manifest, versions: vec![0, 1] },
|
},
|
||||||
SupportedOperation { op: Operation::File, versions: vec![0, 1] },
|
SupportedOperation {
|
||||||
SupportedOperation { op: Operation::Publisher, versions: vec![0, 1] },
|
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 admin;
|
||||||
pub mod routes;
|
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod middleware;
|
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::{
|
use axum::{
|
||||||
routing::{get, post, head},
|
|
||||||
Router,
|
Router,
|
||||||
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::repo::DepotRepo;
|
use tower_http::trace::TraceLayer;
|
||||||
use crate::http::handlers::{versions, catalog, manifest, file, info, publisher};
|
|
||||||
use crate::http::admin;
|
|
||||||
|
|
||||||
pub fn app_router(state: Arc<DepotRepo>) -> Router {
|
pub fn app_router(state: Arc<DepotRepo>) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/versions/0/", get(versions::get_versions))
|
.route("/versions/0/", get(versions::get_versions))
|
||||||
.route("/{publisher}/catalog/1/{filename}", get(catalog::get_catalog_v1).head(catalog::get_catalog_v1))
|
.route(
|
||||||
.route("/{publisher}/manifest/0/{fmri}", get(manifest::get_manifest).head(manifest::get_manifest))
|
"/{publisher}/catalog/1/{filename}",
|
||||||
.route("/{publisher}/manifest/1/{fmri}", get(manifest::get_manifest).head(manifest::get_manifest))
|
get(catalog::get_catalog_v1).head(catalog::get_catalog_v1),
|
||||||
.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}/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}/info/0/{fmri}", get(info::get_info))
|
||||||
.route("/{publisher}/publisher/0", get(publisher::get_publisher_v0))
|
.route("/{publisher}/publisher/0", get(publisher::get_publisher_v0))
|
||||||
.route("/{publisher}/publisher/1", get(publisher::get_publisher_v1))
|
.route("/{publisher}/publisher/1", get(publisher::get_publisher_v1))
|
||||||
// Admin API over HTTP
|
// Admin API over HTTP
|
||||||
.route("/admin/health", get(admin::health))
|
.route("/admin/health", get(admin::health))
|
||||||
.route("/admin/auth/check", post(admin::auth_check))
|
.route("/admin/auth/check", post(admin::auth_check))
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
use tokio::net::TcpListener;
|
|
||||||
use axum::Router;
|
|
||||||
use crate::errors::Result;
|
use crate::errors::Result;
|
||||||
|
use axum::Router;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
pub async fn run(router: Router, listener: TcpListener) -> Result<()> {
|
pub async fn run(router: Router, listener: TcpListener) -> Result<()> {
|
||||||
let addr = listener.local_addr()?;
|
let addr = listener.local_addr()?;
|
||||||
tracing::info!("Listening on {}", 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 cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod daemon;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod telemetry;
|
|
||||||
pub mod repo;
|
pub mod repo;
|
||||||
pub mod daemon;
|
pub mod telemetry;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands};
|
use cli::{Cli, Commands};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use miette::Result;
|
use miette::Result;
|
||||||
use std::sync::Arc;
|
|
||||||
use repo::DepotRepo;
|
use repo::DepotRepo;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub async fn run() -> Result<()> {
|
pub async fn run() -> Result<()> {
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
|
|
@ -57,13 +57,24 @@ pub async fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let router = http::routes::app_router(state);
|
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 bind_str = config
|
||||||
let addr: std::net::SocketAddr = bind_str.parse().map_err(crate::errors::DepotError::AddrParse)?;
|
.server
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.map_err(crate::errors::DepotError::Io)?;
|
.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);
|
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 => {
|
Commands::ConfigTest => {
|
||||||
println!("Configuration loaded successfully: {:?}", config);
|
println!("Configuration loaded successfully: {:?}", config);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use pkg6depotd::run;
|
|
||||||
use miette::Result;
|
use miette::Result;
|
||||||
|
use pkg6depotd::run;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
use libips::repository::{FileBackend, ReadableRepository};
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::errors::{Result, DepotError};
|
use crate::errors::{DepotError, Result};
|
||||||
use libips::fmri::Fmri;
|
use libips::fmri::Fmri;
|
||||||
|
use libips::repository::{FileBackend, ReadableRepository};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
pub struct DepotRepo {
|
pub struct DepotRepo {
|
||||||
|
|
@ -15,11 +15,12 @@ impl DepotRepo {
|
||||||
pub fn new(config: &Config) -> Result<Self> {
|
pub fn new(config: &Config) -> Result<Self> {
|
||||||
let root = config.repository.root.clone();
|
let root = config.repository.root.clone();
|
||||||
let backend = FileBackend::open(&root).map_err(DepotError::Repo)?;
|
let backend = FileBackend::open(&root).map_err(DepotError::Repo)?;
|
||||||
let cache_max_age = config
|
let cache_max_age = config.server.cache_max_age.unwrap_or(3600);
|
||||||
.server
|
Ok(Self {
|
||||||
.cache_max_age
|
backend: Mutex::new(backend),
|
||||||
.unwrap_or(3600);
|
root,
|
||||||
Ok(Self { backend: Mutex::new(backend), root, cache_max_age })
|
cache_max_age,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_catalog_path(&self, publisher: &str) -> PathBuf {
|
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> {
|
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);
|
let cand_pub = FileBackend::construct_file_path_with_publisher(&self.root, publisher, hash);
|
||||||
if cand_pub.exists() { return Some(cand_pub); }
|
if cand_pub.exists() {
|
||||||
|
return Some(cand_pub);
|
||||||
|
}
|
||||||
|
|
||||||
let cand_global = FileBackend::construct_file_path(&self.root, hash);
|
let cand_global = FileBackend::construct_file_path(&self.root, hash);
|
||||||
if cand_global.exists() { return Some(cand_global); }
|
if cand_global.exists() {
|
||||||
|
return Some(cand_global);
|
||||||
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_manifest_text(&self, publisher: &str, fmri: &Fmri) -> Result<String> {
|
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)))?;
|
let backend = self
|
||||||
backend.fetch_manifest_text(publisher, fmri).map_err(DepotError::Repo)
|
.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> {
|
pub fn get_manifest_path(&self, publisher: &str, fmri: &Fmri) -> Option<PathBuf> {
|
||||||
|
|
@ -46,28 +56,54 @@ impl DepotRepo {
|
||||||
if version.is_empty() {
|
if version.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let path = FileBackend::construct_manifest_path(&self.root, publisher, fmri.stem(), &version);
|
let path =
|
||||||
if path.exists() { return Some(path); }
|
FileBackend::construct_manifest_path(&self.root, publisher, fmri.stem(), &version);
|
||||||
|
if path.exists() {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
// Fallbacks similar to lib logic
|
// Fallbacks similar to lib logic
|
||||||
let encoded_stem = url_encode_filename(fmri.stem());
|
let encoded_stem = url_encode_filename(fmri.stem());
|
||||||
let encoded_version = url_encode_filename(&version);
|
let encoded_version = url_encode_filename(&version);
|
||||||
let alt1 = self.root.join("pkg").join(&encoded_stem).join(&encoded_version);
|
let alt1 = self
|
||||||
if alt1.exists() { return Some(alt1); }
|
.root
|
||||||
let alt2 = self.root.join("publisher").join(publisher).join("pkg").join(&encoded_stem).join(&encoded_version);
|
.join("pkg")
|
||||||
if alt2.exists() { return Some(alt2); }
|
.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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cache_max_age(&self) -> u64 { self.cache_max_age }
|
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_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> {
|
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)
|
backend.get_info().map_err(DepotError::Repo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
pub fn init(_config: &Config) {
|
pub fn init(_config: &Config) {
|
||||||
let env_filter = EnvFilter::try_from_default_env()
|
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::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::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
// Helper to setup a repo with a published package
|
// Helper to setup a repo with a published package
|
||||||
fn setup_repo(dir: &TempDir) -> PathBuf {
|
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()],
|
values: vec!["pkg://test/example@1.0.0".to_string()],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
});
|
});
|
||||||
manifest.attributes.push(Attr {
|
manifest.attributes.push(Attr {
|
||||||
key: "pkg.summary".to_string(),
|
key: "pkg.summary".to_string(),
|
||||||
values: vec!["Test Package".to_string()],
|
values: vec!["Test Package".to_string()],
|
||||||
properties: HashMap::new(),
|
properties: HashMap::new(),
|
||||||
|
|
@ -98,7 +98,11 @@ async fn test_depot_server() {
|
||||||
let base_url = format!("http://{}", addr);
|
let base_url = format!("http://{}", addr);
|
||||||
|
|
||||||
// 1. Test Versions
|
// 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());
|
assert!(resp.status().is_success());
|
||||||
let text = resp.text().await.unwrap();
|
let text = resp.text().await.unwrap();
|
||||||
assert!(text.contains("pkg-server pkg6depotd-0.5.1"));
|
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 catalog_v1_url = format!("{}/test/catalog/1/catalog.attrs", base_url);
|
||||||
let resp = client.get(&catalog_v1_url).send().await.unwrap();
|
let resp = client.get(&catalog_v1_url).send().await.unwrap();
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
println!("Catalog v1 failed: {:?}", resp);
|
println!("Catalog v1 failed: {:?}", resp);
|
||||||
}
|
}
|
||||||
assert!(resp.status().is_success());
|
assert!(resp.status().is_success());
|
||||||
let catalog_attrs = resp.text().await.unwrap();
|
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("Name: example"));
|
||||||
assert!(info_text.contains("Summary: Test Package"));
|
assert!(info_text.contains("Summary: Test Package"));
|
||||||
// Ensure FMRI format is correct: pkg://<publisher>/<name>@<version>
|
// 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
|
// 5. Test Publisher v1
|
||||||
let pub_url = format!("{}/test/publisher/1", base_url);
|
let pub_url = format!("{}/test/publisher/1", base_url);
|
||||||
let resp = client.get(&pub_url).send().await.unwrap();
|
let resp = client.get(&pub_url).send().await.unwrap();
|
||||||
assert!(resp.status().is_success());
|
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();
|
let pub_json: serde_json::Value = resp.json().await.unwrap();
|
||||||
assert_eq!(pub_json["version"], 1);
|
assert_eq!(pub_json["version"], 1);
|
||||||
assert_eq!(pub_json["publishers"][0]["name"], "test");
|
assert_eq!(pub_json["publishers"][0]["name"], "test");
|
||||||
|
|
@ -161,8 +176,8 @@ async fn test_depot_server() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_ini_only_repo_serving_catalog() {
|
async fn test_ini_only_repo_serving_catalog() {
|
||||||
use libips::repository::{WritableRepository, ReadableRepository};
|
|
||||||
use libips::repository::BatchOptions;
|
use libips::repository::BatchOptions;
|
||||||
|
use libips::repository::{ReadableRepository, WritableRepository};
|
||||||
use std::io::Write as _;
|
use std::io::Write as _;
|
||||||
|
|
||||||
// Setup temp repo
|
// Setup temp repo
|
||||||
|
|
@ -190,18 +205,33 @@ async fn test_ini_only_repo_serving_catalog() {
|
||||||
let mut manifest = Manifest::new();
|
let mut manifest = Manifest::new();
|
||||||
use libips::actions::Attr;
|
use libips::actions::Attr;
|
||||||
use std::collections::HashMap;
|
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 {
|
||||||
manifest.attributes.push(Attr { key: "pkg.summary".to_string(), values: vec!["INI Repo Test Package".to_string()], properties: HashMap::new() });
|
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.update_manifest(manifest);
|
||||||
tx.commit().unwrap();
|
tx.commit().unwrap();
|
||||||
|
|
||||||
// Rebuild catalog using batched API explicitly with small batch to exercise code path
|
// Rebuild catalog using batched API explicitly with small batch to exercise code path
|
||||||
let opts = BatchOptions { batch_size: 1, flush_every_n: 1 };
|
let opts = BatchOptions {
|
||||||
backend.rebuild_catalog_batched(publisher, true, opts).unwrap();
|
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
|
// Replace pkg6.repository with legacy pkg5.repository so FileBackend::open uses INI
|
||||||
let pkg6_cfg = repo_path.join("pkg6.repository");
|
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();
|
let mut ini = String::new();
|
||||||
ini.push_str("[publisher]\n");
|
ini.push_str("[publisher]\n");
|
||||||
ini.push_str(&format!("prefix = {}\n", publisher));
|
ini.push_str(&format!("prefix = {}\n", publisher));
|
||||||
|
|
@ -211,9 +241,23 @@ async fn test_ini_only_repo_serving_catalog() {
|
||||||
|
|
||||||
// Start depot server
|
// Start depot server
|
||||||
let config = Config {
|
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 },
|
server: ServerConfig {
|
||||||
repository: RepositoryConfig { root: repo_path.clone(), mode: Some("readonly".to_string()) },
|
bind: vec!["127.0.0.1:0".to_string()],
|
||||||
telemetry: None, publishers: None, admin: None, oauth2: None,
|
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 repo = DepotRepo::new(&config).unwrap();
|
||||||
let state = Arc::new(repo);
|
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 listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
let addr = listener.local_addr().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 client = reqwest::Client::new();
|
||||||
let base_url = format!("http://{}", addr);
|
let base_url = format!("http://{}", addr);
|
||||||
|
|
@ -235,19 +281,48 @@ async fn test_ini_only_repo_serving_catalog() {
|
||||||
assert!(body.contains("parts"));
|
assert!(body.contains("parts"));
|
||||||
|
|
||||||
// Also fetch individual catalog 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 url = format!("{}/{}/catalog/1/{}", base_url, publisher, part);
|
||||||
let resp = client.get(&url).send().await.unwrap();
|
let resp = client.get(&url).send().await.unwrap();
|
||||||
assert!(resp.status().is_success(), "{} status: {:?}", part, resp.status());
|
assert!(
|
||||||
let ct = resp.headers().get("content-type").unwrap().to_str().unwrap().to_string();
|
resp.status().is_success(),
|
||||||
assert!(ct.contains("application/json"), "content-type for {} was {}", part, ct);
|
"{} 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();
|
let txt = resp.text().await.unwrap();
|
||||||
assert!(!txt.is_empty(), "{} should not be empty", part);
|
assert!(!txt.is_empty(), "{} should not be empty", part);
|
||||||
if *part == "catalog.base.C" {
|
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 {
|
} else {
|
||||||
// dependency/summary may be empty for this test package; at least ensure signature is present
|
// 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
|
// Check that the publisher was added
|
||||||
assert!(repo_path.join("publisher").join("example.com").exists());
|
assert!(repo_path.join("publisher").join("example.com").exists());
|
||||||
assert!(repo_path.join("publisher").join("example.com").join("catalog").exists());
|
assert!(
|
||||||
assert!(repo_path.join("publisher").join("example.com").join("pkg").exists());
|
repo_path
|
||||||
|
.join("publisher")
|
||||||
|
.join("example.com")
|
||||||
|
.join("catalog")
|
||||||
|
.exists()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
repo_path
|
||||||
|
.join("publisher")
|
||||||
|
.join("example.com")
|
||||||
|
.join("pkg")
|
||||||
|
.exists()
|
||||||
|
);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
cleanup_test_dir(&test_dir);
|
cleanup_test_dir(&test_dir);
|
||||||
|
|
@ -440,16 +452,21 @@ mod e2e_tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let output = result.unwrap();
|
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
|
// 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
|
// We need to extract these fields and construct the FMRI string
|
||||||
let fmri_obj = &packages["packages"][0]["fmri"];
|
let fmri_obj = &packages["packages"][0]["fmri"];
|
||||||
let scheme = fmri_obj["scheme"].as_str().expect("Failed to get scheme");
|
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 name = fmri_obj["name"].as_str().expect("Failed to get name");
|
||||||
let version_obj = &fmri_obj["version"];
|
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"
|
// Construct the FMRI string in the format "pkg://publisher/name@version"
|
||||||
let fmri = format!("{}://{}/{}", scheme, publisher, name);
|
let fmri = format!("{}://{}/{}", scheme, publisher, name);
|
||||||
|
|
@ -466,7 +483,11 @@ mod e2e_tests {
|
||||||
println!("Repo path: {}", repo_path.display());
|
println!("Repo path: {}", repo_path.display());
|
||||||
|
|
||||||
// Check if the package exists in the repository
|
// 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: {}", pkg_dir.display());
|
||||||
println!("Package directory exists: {}", pkg_dir.exists());
|
println!("Package directory exists: {}", pkg_dir.exists());
|
||||||
|
|
||||||
|
|
@ -482,11 +503,16 @@ mod e2e_tests {
|
||||||
// Mark the package as obsoleted
|
// Mark the package as obsoleted
|
||||||
let result = run_pkg6repo(&[
|
let result = run_pkg6repo(&[
|
||||||
"obsolete-package",
|
"obsolete-package",
|
||||||
"-s", repo_path.to_str().unwrap(),
|
"-s",
|
||||||
"-p", "test",
|
repo_path.to_str().unwrap(),
|
||||||
"-f", &fmri,
|
"-p",
|
||||||
"-m", "This package is obsoleted for testing purposes",
|
"test",
|
||||||
"-r", "pkg://test/example2@1.0"
|
"-f",
|
||||||
|
&fmri,
|
||||||
|
"-m",
|
||||||
|
"This package is obsoleted for testing purposes",
|
||||||
|
"-r",
|
||||||
|
"pkg://test/example2@1.0",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Print the result for debugging
|
// Print the result for debugging
|
||||||
|
|
@ -513,7 +539,13 @@ mod e2e_tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
// List obsoleted packages
|
// 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!(
|
assert!(
|
||||||
result.is_ok(),
|
result.is_ok(),
|
||||||
"Failed to list obsoleted packages: {:?}",
|
"Failed to list obsoleted packages: {:?}",
|
||||||
|
|
@ -529,9 +561,12 @@ mod e2e_tests {
|
||||||
// Show details of the obsoleted package
|
// Show details of the obsoleted package
|
||||||
let result = run_pkg6repo(&[
|
let result = run_pkg6repo(&[
|
||||||
"show-obsoleted",
|
"show-obsoleted",
|
||||||
"-s", repo_path.to_str().unwrap(),
|
"-s",
|
||||||
"-p", "test",
|
repo_path.to_str().unwrap(),
|
||||||
"-f", &fmri
|
"-p",
|
||||||
|
"test",
|
||||||
|
"-f",
|
||||||
|
&fmri,
|
||||||
]);
|
]);
|
||||||
assert!(
|
assert!(
|
||||||
result.is_ok(),
|
result.is_ok(),
|
||||||
|
|
|
||||||
|
|
@ -1273,7 +1273,7 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
info!("Repository imported successfully");
|
info!("Repository imported successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::ObsoletePackage {
|
Commands::ObsoletePackage {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
|
|
@ -1295,7 +1295,7 @@ fn main() -> Result<()> {
|
||||||
&repo.path,
|
&repo.path,
|
||||||
publisher,
|
publisher,
|
||||||
parsed_fmri.stem(),
|
parsed_fmri.stem(),
|
||||||
&parsed_fmri.version()
|
&parsed_fmri.version(),
|
||||||
);
|
);
|
||||||
|
|
||||||
println!("Looking for manifest at: {}", manifest_path.display());
|
println!("Looking for manifest at: {}", manifest_path.display());
|
||||||
|
|
@ -1336,7 +1336,7 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
info!("Package marked as obsoleted successfully: {}", parsed_fmri);
|
info!("Package marked as obsoleted successfully: {}", parsed_fmri);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::ListObsoleted {
|
Commands::ListObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
|
|
@ -1357,7 +1357,11 @@ fn main() -> Result<()> {
|
||||||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||||
|
|
||||||
// List obsoleted packages with pagination
|
// 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
|
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
||||||
|
|
||||||
// Determine the output format
|
// Determine the output format
|
||||||
|
|
@ -1389,11 +1393,13 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print pagination information
|
// Print pagination information
|
||||||
println!("\nPage {} of {} (Total: {} packages)",
|
println!(
|
||||||
|
"\nPage {} of {} (Total: {} packages)",
|
||||||
paginated_result.page,
|
paginated_result.page,
|
||||||
paginated_result.total_pages,
|
paginated_result.total_pages,
|
||||||
paginated_result.total_count);
|
paginated_result.total_count
|
||||||
},
|
);
|
||||||
|
}
|
||||||
"json" => {
|
"json" => {
|
||||||
// Create a JSON representation of the obsoleted packages with pagination info
|
// Create a JSON representation of the obsoleted packages with pagination info
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -1405,7 +1411,11 @@ fn main() -> Result<()> {
|
||||||
total_count: usize,
|
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 {
|
let paginated_output = PaginatedOutput {
|
||||||
packages: packages_str,
|
packages: packages_str,
|
||||||
page: paginated_result.page,
|
page: paginated_result.page,
|
||||||
|
|
@ -1419,7 +1429,7 @@ fn main() -> Result<()> {
|
||||||
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
||||||
|
|
||||||
println!("{}", json_output);
|
println!("{}", json_output);
|
||||||
},
|
}
|
||||||
"tsv" => {
|
"tsv" => {
|
||||||
// Print headers if not omitted
|
// Print headers if not omitted
|
||||||
if !omit_headers {
|
if !omit_headers {
|
||||||
|
|
@ -1436,20 +1446,17 @@ fn main() -> Result<()> {
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str);
|
||||||
"{}\t{}\t{}",
|
|
||||||
fmri.stem(),
|
|
||||||
version_str,
|
|
||||||
publisher_str
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print pagination information
|
// 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.page,
|
||||||
paginated_result.total_pages,
|
paginated_result.total_pages,
|
||||||
paginated_result.total_count);
|
paginated_result.total_count
|
||||||
},
|
);
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
||||||
output_format.to_string(),
|
output_format.to_string(),
|
||||||
|
|
@ -1458,7 +1465,7 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::ShowObsoleted {
|
Commands::ShowObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
|
|
@ -1513,7 +1520,7 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
println!("Metadata Version: {}", metadata.metadata_version);
|
println!("Metadata Version: {}", metadata.metadata_version);
|
||||||
println!("Content Hash: {}", metadata.content_hash);
|
println!("Content Hash: {}", metadata.content_hash);
|
||||||
},
|
}
|
||||||
"json" => {
|
"json" => {
|
||||||
// Create a JSON representation of the obsoleted package details
|
// Create a JSON representation of the obsoleted package details
|
||||||
let details_output = ObsoletedPackageDetailsOutput {
|
let details_output = ObsoletedPackageDetailsOutput {
|
||||||
|
|
@ -1531,7 +1538,7 @@ fn main() -> Result<()> {
|
||||||
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
||||||
|
|
||||||
println!("{}", json_output);
|
println!("{}", json_output);
|
||||||
},
|
}
|
||||||
"tsv" => {
|
"tsv" => {
|
||||||
println!("FMRI\t{}", metadata.fmri);
|
println!("FMRI\t{}", metadata.fmri);
|
||||||
println!("Status\t{}", metadata.status);
|
println!("Status\t{}", metadata.status);
|
||||||
|
|
@ -1549,7 +1556,7 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
println!("MetadataVersion\t{}", metadata.metadata_version);
|
println!("MetadataVersion\t{}", metadata.metadata_version);
|
||||||
println!("ContentHash\t{}", metadata.content_hash);
|
println!("ContentHash\t{}", metadata.content_hash);
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
||||||
output_format.to_string(),
|
output_format.to_string(),
|
||||||
|
|
@ -1558,7 +1565,7 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::SearchObsoleted {
|
Commands::SearchObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
|
|
@ -1568,7 +1575,10 @@ fn main() -> Result<()> {
|
||||||
pattern,
|
pattern,
|
||||||
limit,
|
limit,
|
||||||
} => {
|
} => {
|
||||||
info!("Searching for obsoleted packages: {} (publisher: {})", pattern, publisher);
|
info!(
|
||||||
|
"Searching for obsoleted packages: {} (publisher: {})",
|
||||||
|
pattern, publisher
|
||||||
|
);
|
||||||
|
|
||||||
// Open the repository
|
// Open the repository
|
||||||
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
||||||
|
|
@ -1579,7 +1589,8 @@ fn main() -> Result<()> {
|
||||||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||||
|
|
||||||
// Search for obsoleted packages
|
// 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
|
// Apply limit if specified
|
||||||
if let Some(max_results) = limit {
|
if let Some(max_results) = limit {
|
||||||
|
|
@ -1616,10 +1627,11 @@ fn main() -> Result<()> {
|
||||||
publisher_str
|
publisher_str
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"json" => {
|
"json" => {
|
||||||
// Create a JSON representation of the obsoleted packages
|
// 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 {
|
let packages_output = ObsoletedPackagesOutput {
|
||||||
packages: packages_str,
|
packages: packages_str,
|
||||||
};
|
};
|
||||||
|
|
@ -1629,7 +1641,7 @@ fn main() -> Result<()> {
|
||||||
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
|
||||||
|
|
||||||
println!("{}", json_output);
|
println!("{}", json_output);
|
||||||
},
|
}
|
||||||
"tsv" => {
|
"tsv" => {
|
||||||
// Print headers if not omitted
|
// Print headers if not omitted
|
||||||
if !omit_headers {
|
if !omit_headers {
|
||||||
|
|
@ -1646,14 +1658,9 @@ fn main() -> Result<()> {
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str);
|
||||||
"{}\t{}\t{}",
|
|
||||||
fmri.stem(),
|
|
||||||
version_str,
|
|
||||||
publisher_str
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
||||||
output_format.to_string(),
|
output_format.to_string(),
|
||||||
|
|
@ -1662,7 +1669,7 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::RestoreObsoleted {
|
Commands::RestoreObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
|
|
@ -1670,7 +1677,10 @@ fn main() -> Result<()> {
|
||||||
fmri,
|
fmri,
|
||||||
no_rebuild,
|
no_rebuild,
|
||||||
} => {
|
} => {
|
||||||
info!("Restoring obsoleted package: {} (publisher: {})", fmri, publisher);
|
info!(
|
||||||
|
"Restoring obsoleted package: {} (publisher: {})",
|
||||||
|
fmri, publisher
|
||||||
|
);
|
||||||
|
|
||||||
// Parse the FMRI
|
// Parse the FMRI
|
||||||
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
|
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
|
||||||
|
|
@ -1710,7 +1720,7 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
info!("Package restored successfully: {}", parsed_fmri);
|
info!("Package restored successfully: {}", parsed_fmri);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::ExportObsoleted {
|
Commands::ExportObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
|
|
@ -1739,7 +1749,7 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
info!("Exported {} obsoleted packages to {}", count, output_file);
|
info!("Exported {} obsoleted packages to {}", count, output_file);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::ImportObsoleted {
|
Commands::ImportObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
|
|
@ -1758,15 +1768,12 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
// Import the obsoleted packages
|
// Import the obsoleted packages
|
||||||
let input_path = PathBuf::from(input_file);
|
let input_path = PathBuf::from(input_file);
|
||||||
obsoleted_manager.import_obsoleted_packages(
|
obsoleted_manager.import_obsoleted_packages(&input_path, publisher.as_deref())?
|
||||||
&input_path,
|
|
||||||
publisher.as_deref(),
|
|
||||||
)?
|
|
||||||
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
||||||
|
|
||||||
info!("Imported {} obsoleted packages", count);
|
info!("Imported {} obsoleted packages", count);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::CleanupObsoleted {
|
Commands::CleanupObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
|
|
@ -1775,11 +1782,15 @@ fn main() -> Result<()> {
|
||||||
dry_run,
|
dry_run,
|
||||||
} => {
|
} => {
|
||||||
if *dry_run {
|
if *dry_run {
|
||||||
info!("Dry run: Cleaning up obsoleted packages older than {} days for publisher: {}",
|
info!(
|
||||||
ttl_days, publisher);
|
"Dry run: Cleaning up obsoleted packages older than {} days for publisher: {}",
|
||||||
|
ttl_days, publisher
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
info!("Cleaning up obsoleted packages older than {} days for publisher: {}",
|
info!(
|
||||||
ttl_days, publisher);
|
"Cleaning up obsoleted packages older than {} days for publisher: {}",
|
||||||
|
ttl_days, publisher
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the repository
|
// Open the repository
|
||||||
|
|
@ -1791,11 +1802,8 @@ fn main() -> Result<()> {
|
||||||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||||
|
|
||||||
// Clean up the obsoleted packages
|
// Clean up the obsoleted packages
|
||||||
obsoleted_manager.cleanup_obsoleted_packages_older_than_ttl(
|
obsoleted_manager
|
||||||
publisher,
|
.cleanup_obsoleted_packages_older_than_ttl(publisher, *ttl_days, *dry_run)?
|
||||||
*ttl_days,
|
|
||||||
*dry_run,
|
|
||||||
)?
|
|
||||||
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
||||||
|
|
||||||
if *dry_run {
|
if *dry_run {
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,8 @@ impl Pkg5Importer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import packages and get counts
|
// 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;
|
let total_count = regular_count + obsoleted_count;
|
||||||
|
|
||||||
// Rebuild catalog and search index
|
// Rebuild catalog and search index
|
||||||
|
|
@ -349,8 +350,10 @@ impl Pkg5Importer {
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_package_count = regular_package_count + obsoleted_package_count;
|
let total_package_count = regular_package_count + obsoleted_package_count;
|
||||||
info!("Imported {} packages ({} regular, {} obsoleted)",
|
info!(
|
||||||
total_package_count, regular_package_count, obsoleted_package_count);
|
"Imported {} packages ({} regular, {} obsoleted)",
|
||||||
|
total_package_count, regular_package_count, obsoleted_package_count
|
||||||
|
);
|
||||||
Ok((regular_package_count, obsoleted_package_count))
|
Ok((regular_package_count, obsoleted_package_count))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,8 +442,8 @@ impl Pkg5Importer {
|
||||||
publisher,
|
publisher,
|
||||||
&fmri,
|
&fmri,
|
||||||
&manifest_content,
|
&manifest_content,
|
||||||
None, // No obsoleted_by information available
|
None, // No obsoleted_by information available
|
||||||
None, // No deprecation message available
|
None, // No deprecation message available
|
||||||
false, // Don't store the original manifest, use null hash instead
|
false, // Don't store the original manifest, use null hash instead
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
@ -462,7 +465,8 @@ impl Pkg5Importer {
|
||||||
// Debug the repository structure
|
// Debug the repository structure
|
||||||
debug!(
|
debug!(
|
||||||
"Publisher directory: {}",
|
"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
|
// Extract files referenced in the manifest
|
||||||
|
|
|
||||||
|
|
@ -82,15 +82,18 @@ impl std::fmt::Display for OutputFormat {
|
||||||
|
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
#[error("pkgtree error: {message}")]
|
#[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 {
|
struct PkgTreeError {
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Edge {
|
struct Edge {
|
||||||
to: String, // target stem
|
to: String, // target stem
|
||||||
dep_type: String, // dependency type (e.g., require, incorporate, optional, etc.)
|
dep_type: String, // dependency type (e.g., require, incorporate, optional, etc.)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
|
|
@ -101,7 +104,10 @@ struct Graph {
|
||||||
|
|
||||||
impl Graph {
|
impl Graph {
|
||||||
fn add_edge(&mut self, from: String, to: String, dep_type: String) {
|
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> {
|
fn stems(&self) -> impl Iterator<Item = &String> {
|
||||||
|
|
@ -127,8 +133,9 @@ fn main() -> Result<()> {
|
||||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||||
|
|
||||||
// Load image
|
// Load image
|
||||||
let image = Image::load(&cli.image_path)
|
let image = Image::load(&cli.image_path).map_err(|e| PkgTreeError {
|
||||||
.map_err(|e| PkgTreeError { message: format!("Failed to load image at {:?}: {}", cli.image_path, e) })?;
|
message: format!("Failed to load image at {:?}: {}", cli.image_path, e),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Targeted analysis of solver error file has top priority if provided
|
// Targeted analysis of solver error file has top priority if provided
|
||||||
if let Some(err_path) = &cli.solver_error_file {
|
if let Some(err_path) = &cli.solver_error_file {
|
||||||
|
|
@ -145,16 +152,27 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
// Dangling dependency scan has priority over graph mode
|
// Dangling dependency scan has priority over graph mode
|
||||||
if cli.find_dangling {
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Graph mode
|
// Graph mode
|
||||||
// Query catalog (filtered if --package provided)
|
// Query catalog (filtered if --package provided)
|
||||||
let mut pkgs = if let Some(ref needle) = cli.package {
|
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 {
|
} 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
|
// 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 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() {
|
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 {
|
for p in &pkgs {
|
||||||
match image.get_manifest_from_catalog(&p.fmri) {
|
match image.get_manifest_from_catalog(&p.fmri) {
|
||||||
Ok(Some(manifest)) => {
|
Ok(Some(manifest)) => {
|
||||||
let from_stem = p.fmri.stem().to_string();
|
let from_stem = p.fmri.stem().to_string();
|
||||||
for dep in manifest.dependencies {
|
for dep in manifest.dependencies {
|
||||||
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" {
|
if dep.dependency_type != "require" && dep.dependency_type != "incorporate"
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(dep_fmri) = dep.fmri {
|
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 roots: Vec<String> = if let Some(ref needle) = filter_substr {
|
||||||
let mut r = HashSet::new();
|
let mut r = HashSet::new();
|
||||||
for k in graph.adj.keys() {
|
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()
|
r.into_iter().collect()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -253,19 +276,47 @@ fn main() -> Result<()> {
|
||||||
OutputFormat::Json => {
|
OutputFormat::Json => {
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct JsonEdge { from: String, to: String, dep_type: String }
|
struct JsonEdge {
|
||||||
|
from: String,
|
||||||
|
to: String,
|
||||||
|
dep_type: String,
|
||||||
|
}
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct JsonCycle { nodes: Vec<String>, edges: Vec<String> }
|
struct JsonCycle {
|
||||||
|
nodes: Vec<String>,
|
||||||
|
edges: Vec<String>,
|
||||||
|
}
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Payload { edges: Vec<JsonEdge>, cycles: Vec<JsonCycle> }
|
struct Payload {
|
||||||
|
edges: Vec<JsonEdge>,
|
||||||
|
cycles: Vec<JsonCycle>,
|
||||||
|
}
|
||||||
|
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for (from, es) in &graph.adj {
|
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 cycles_json = cycles
|
||||||
let payload = Payload { edges, cycles: cycles_json };
|
.iter()
|
||||||
println!("{}", serde_json::to_string_pretty(&payload).into_diagnostic()?);
|
.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
|
path: Vec<String>, // path from root to the missing dependency stem
|
||||||
stem: String, // the missing stem
|
stem: String, // the missing stem
|
||||||
constraint: DepConstraint,
|
constraint: DepConstraint,
|
||||||
details: String, // human description
|
details: String, // human description
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -296,25 +347,42 @@ struct AdviceContext {
|
||||||
catalog_cache: HashMap<String, Vec<(String, libips::fmri::Fmri)>>, // stem -> [(publisher, fmri)]
|
catalog_cache: HashMap<String, Vec<(String, libips::fmri::Fmri)>>, // stem -> [(publisher, fmri)]
|
||||||
manifest_cache: HashMap<String, libips::actions::Manifest>, // fmri string -> manifest
|
manifest_cache: HashMap<String, libips::actions::Manifest>, // fmri string -> manifest
|
||||||
lock_cache: HashMap<String, Option<String>>, // stem -> release lock
|
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 {
|
impl AdviceContext {
|
||||||
fn new(publisher: Option<String>, advice_cap: usize) -> Self {
|
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);
|
info!("Advisor analyzing installability for root: {}", root_stem);
|
||||||
|
|
||||||
// Find best candidate for root
|
// Find best candidate for root
|
||||||
let root_fmri = match find_best_candidate(image, ctx, root_stem, None, None) {
|
let root_fmri = match find_best_candidate(image, ctx, root_stem, None, None) {
|
||||||
Ok(Some(fmri)) => fmri,
|
Ok(Some(fmri)) => fmri,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
println!("No candidates found for root package '{}'.\n- Suggestion: run 'pkg6 refresh' to update catalogs.\n- Ensure publisher{} contains the package.",
|
println!(
|
||||||
root_stem,
|
"No candidates found for root package '{}'.\n- Suggestion: run 'pkg6 refresh' to update catalogs.\n- Ensure publisher{} contains the package.",
|
||||||
ctx.publisher.as_ref().map(|p| format!(" '{}')", p)).unwrap_or_else(|| "".to_string()));
|
root_stem,
|
||||||
|
ctx.publisher
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| format!(" '{}')", p))
|
||||||
|
.unwrap_or_else(|| "".to_string())
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e),
|
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 issues: Vec<AdviceIssue> = Vec::new();
|
||||||
let mut seen: HashSet<String> = HashSet::new();
|
let mut seen: HashSet<String> = HashSet::new();
|
||||||
let mut path: Vec<String> = vec![root_stem.to_string()];
|
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
|
// Print summary
|
||||||
if issues.is_empty() {
|
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 {
|
} else {
|
||||||
println!("Found {} installability issue(s):", issues.len());
|
println!("Found {} installability issue(s):", issues.len());
|
||||||
for (i, iss) in issues.iter().enumerate() {
|
for (i, iss) in issues.iter().enumerate() {
|
||||||
let constraint_str = format!(
|
let constraint_str = format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
iss.constraint.release.as_ref().map(|r| format!("release={} ", r)).unwrap_or_default(),
|
iss.constraint
|
||||||
iss.constraint.branch.as_ref().map(|b| format!("branch={}", b)).unwrap_or_default(),
|
.release
|
||||||
).trim().to_string();
|
.as_ref()
|
||||||
println!(" {}. {}\n - Path: {}\n - Constraint: {}\n - Details: {}",
|
.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,
|
i + 1,
|
||||||
format!("No viable candidates for '{}'", iss.stem),
|
format!("No viable candidates for '{}'", iss.stem),
|
||||||
iss.path.join(" -> "),
|
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,
|
iss.details,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Suggestions
|
// Suggestions
|
||||||
println!(" - Suggestions:");
|
println!(" - Suggestions:");
|
||||||
println!(" • Add or publish a matching package for '{}'{}{}.",
|
println!(
|
||||||
|
" • Add or publish a matching package for '{}'{}{}.",
|
||||||
iss.stem,
|
iss.stem,
|
||||||
iss.constraint.release.as_ref().map(|r| format!(" (release={})", r)).unwrap_or_default(),
|
iss.constraint
|
||||||
iss.constraint.branch.as_ref().map(|b| format!(" (branch={})", b)).unwrap_or_default());
|
.release
|
||||||
println!(" • Alternatively, relax the dependency constraint in the requiring package to match available releases.");
|
.as_ref()
|
||||||
if let Some(lock) = get_incorporated_release_cached(image, ctx, &iss.stem).ok().flatten() {
|
.map(|r| format!(" (release={})", r))
|
||||||
println!(" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", iss.stem, lock);
|
.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'.");
|
println!(" • Ensure catalogs are up to date: 'pkg6 refresh'.");
|
||||||
}
|
}
|
||||||
|
|
@ -374,7 +487,9 @@ fn advise_recursive(
|
||||||
seen: &mut HashSet<String>,
|
seen: &mut HashSet<String>,
|
||||||
issues: &mut Vec<AdviceIssue>,
|
issues: &mut Vec<AdviceIssue>,
|
||||||
) -> Result<()> {
|
) -> 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)
|
// Load manifest of the current FMRI (cached)
|
||||||
let manifest = get_manifest_cached(image, ctx, fmri)?;
|
let manifest = get_manifest_cached(image, ctx, fmri)?;
|
||||||
|
|
@ -383,30 +498,61 @@ fn advise_recursive(
|
||||||
let mut constrained = Vec::new();
|
let mut constrained = Vec::new();
|
||||||
let mut unconstrained = Vec::new();
|
let mut unconstrained = Vec::new();
|
||||||
for dep in manifest.dependencies {
|
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();
|
let has_fmri = dep.fmri.is_some();
|
||||||
if !has_fmri { continue; }
|
if !has_fmri {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let c = extract_constraint(&dep.optional);
|
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()) {
|
for (dep, constraint) in constrained.into_iter().chain(unconstrained.into_iter()) {
|
||||||
if ctx.advice_cap != 0 && processed >= ctx.advice_cap {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
processed += 1;
|
processed += 1;
|
||||||
|
|
||||||
let dep_stem = dep.fmri.unwrap().stem().to_string();
|
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) => {
|
Some(next_fmri) => {
|
||||||
// Continue recursion if not seen and depth allows
|
// Continue recursion if not seen and depth allows
|
||||||
if !seen.contains(&dep_stem) {
|
if !seen.contains(&dep_stem) {
|
||||||
seen.insert(dep_stem.clone());
|
seen.insert(dep_stem.clone());
|
||||||
path.push(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();
|
path.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -438,15 +584,28 @@ fn extract_constraint(optional: &[libips::actions::Property]) -> DepConstraint {
|
||||||
DepConstraint { release, branch }
|
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
|
// List available releases/branches for informational purposes
|
||||||
let mut available: Vec<String> = Vec::new();
|
let mut available: Vec<String> = Vec::new();
|
||||||
if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) {
|
if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) {
|
||||||
for (pubname, fmri) in list {
|
for (pubname, fmri) in list {
|
||||||
if let Some(ref pfilter) = ctx.publisher { if &pubname != pfilter { continue; } }
|
if let Some(ref pfilter) = ctx.publisher {
|
||||||
if fmri.stem() != stem { continue; }
|
if &pubname != pfilter {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fmri.stem() != stem {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let ver = fmri.version();
|
let ver = fmri.version();
|
||||||
if ver.is_empty() { continue; }
|
if ver.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
available.push(ver);
|
available.push(ver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -460,17 +619,43 @@ fn build_missing_detail(image: &Image, ctx: &mut AdviceContext, stem: &str, cons
|
||||||
available.join(", ")
|
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) {
|
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), Some(lr)) => format!(
|
||||||
(Some(r), Some(b), None) => format!("Required release={}, branch={} not found. Available versions: {}", r, b, available_str),
|
"Required release={}, branch={} not found. Incorporation lock release={} may also constrain candidates. Available versions: {}",
|
||||||
(Some(r), None, Some(lr)) => format!("Required release={} not found. Incorporation lock release={} present. Available versions: {}", r, lr, available_str),
|
r, b, 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),
|
(Some(r), Some(b), None) => format!(
|
||||||
(None, Some(b), None) => format!("Required branch={} not found. Available versions: {}", b, available_str),
|
"Required release={}, branch={} not found. Available versions: {}",
|
||||||
(None, None, Some(lr)) => format!("No candidates matched. Incorporation lock release={} present. Available versions: {}", lr, available_str),
|
r, b, available_str
|
||||||
(None, None, None) => format!("No candidates matched. Available versions: {}", 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();
|
let mut candidates: Vec<(String, libips::fmri::Fmri)> = Vec::new();
|
||||||
|
|
||||||
// Prefer matching release from incorporation lock, unless explicit req_release provided
|
// 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)? {
|
for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? {
|
||||||
if let Some(ref pfilter) = ctx.publisher { if &pubf != pfilter { continue; } }
|
if let Some(ref pfilter) = ctx.publisher {
|
||||||
if pfmri.stem() != stem { continue; }
|
if &pubf != pfilter {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pfmri.stem() != stem {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let ver = pfmri.version();
|
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
|
// Parse version string to extract release and branch heuristically: release,branch-rest
|
||||||
let rel = version_release(&ver);
|
let rel = version_release(&ver);
|
||||||
let br = version_branch(&ver);
|
let br = version_branch(&ver);
|
||||||
|
|
||||||
if let Some(req_r) = req_release {
|
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() {
|
} 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()));
|
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.
|
// 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.
|
// To keep code simple, provide a small wrapper that fills the cache when needed.
|
||||||
// We'll implement a separate function that has mutable ctx.
|
// 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)
|
query_catalog_cached_mut(image, &mut tmp_ctx, stem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,10 +773,9 @@ fn query_catalog_cached_mut(
|
||||||
return Ok(v.clone());
|
return Ok(v.clone());
|
||||||
}
|
}
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for p in image
|
for p in image.query_catalog(Some(stem)).map_err(|e| PkgTreeError {
|
||||||
.query_catalog(Some(stem))
|
message: format!("Failed to query catalog for {}: {}", stem, e),
|
||||||
.map_err(|e| PkgTreeError { message: format!("Failed to query catalog for {}: {}", stem, e) })?
|
})? {
|
||||||
{
|
|
||||||
out.push((p.publisher, p.fmri));
|
out.push((p.publisher, p.fmri));
|
||||||
}
|
}
|
||||||
ctx.catalog_cache.insert(stem.to_string(), out.clone());
|
ctx.catalog_cache.insert(stem.to_string(), out.clone());
|
||||||
|
|
@ -584,7 +793,9 @@ fn get_manifest_cached(
|
||||||
}
|
}
|
||||||
let manifest_opt = image
|
let manifest_opt = image
|
||||||
.get_manifest_from_catalog(fmri)
|
.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());
|
let manifest = manifest_opt.unwrap_or_else(|| libips::actions::Manifest::new());
|
||||||
ctx.manifest_cache.insert(key, manifest.clone());
|
ctx.manifest_cache.insert(key, manifest.clone());
|
||||||
Ok(manifest)
|
Ok(manifest)
|
||||||
|
|
@ -595,7 +806,9 @@ fn get_incorporated_release_cached(
|
||||||
ctx: &mut AdviceContext,
|
ctx: &mut AdviceContext,
|
||||||
stem: &str,
|
stem: &str,
|
||||||
) -> Result<Option<String>> {
|
) -> 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)?;
|
let v = image.get_incorporated_release(stem)?;
|
||||||
ctx.lock_cache.insert(stem.to_string(), v.clone());
|
ctx.lock_cache.insert(stem.to_string(), v.clone());
|
||||||
Ok(v)
|
Ok(v)
|
||||||
|
|
@ -607,7 +820,9 @@ fn print_trees(graph: &Graph, roots: &[String], max_depth: usize) {
|
||||||
// Print a tree for each root
|
// Print a tree for each root
|
||||||
let mut printed = HashSet::new();
|
let mut printed = HashSet::new();
|
||||||
for r in roots {
|
for r in roots {
|
||||||
if printed.contains(r) { continue; }
|
if printed.contains(r) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
printed.insert(r.clone());
|
printed.insert(r.clone());
|
||||||
println!("{}", r);
|
println!("{}", r);
|
||||||
let mut path = Vec::new();
|
let mut path = Vec::new();
|
||||||
|
|
@ -625,7 +840,9 @@ fn print_tree_rec(
|
||||||
path: &mut Vec<String>,
|
path: &mut Vec<String>,
|
||||||
_seen: &mut HashSet<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());
|
path.push(node.to_string());
|
||||||
|
|
||||||
if let Some(edges) = graph.adj.get(node) {
|
if let Some(edges) = graph.adj.get(node) {
|
||||||
|
|
@ -675,7 +892,11 @@ fn dfs_cycles(
|
||||||
let mut cycle_edges = Vec::new();
|
let mut cycle_edges = Vec::new();
|
||||||
for i in pos..stack.len() {
|
for i in pos..stack.len() {
|
||||||
let from = &stack[i];
|
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(es2) = graph.adj.get(from) {
|
||||||
if let Some(edge) = es2.iter().find(|ed| &ed.to == to2) {
|
if let Some(edge) = es2.iter().find(|ed| &ed.to == to2) {
|
||||||
cycle_edges.push(edge.dep_type.clone());
|
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) {
|
} else if !visited.contains(to) {
|
||||||
dfs_cycles(graph, to, visited, stack, cycles);
|
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)
|
// rotate to minimal node position (excluding the duplicate last element when comparing)
|
||||||
if c.nodes.len() > 1 {
|
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) {
|
if let Some((min_idx, _)) = inner.iter().enumerate().min_by_key(|(_, n)| *n) {
|
||||||
c.nodes.rotate_left(min_idx);
|
c.nodes.rotate_left(min_idx);
|
||||||
c.edges.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();
|
let mut seen = HashSet::new();
|
||||||
cycles.retain(|c| {
|
cycles.retain(|c| {
|
||||||
let key = c.nodes.join("->");
|
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
|
cycles
|
||||||
}
|
}
|
||||||
|
|
@ -730,7 +959,9 @@ fn print_cycles(cycles: &[Cycle]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_suggestions(cycles: &[Cycle], graph: &Graph) {
|
fn print_suggestions(cycles: &[Cycle], graph: &Graph) {
|
||||||
if cycles.is_empty() { return; }
|
if cycles.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
println!("\nSuggestions to break cycles (heuristic):");
|
println!("\nSuggestions to break cycles (heuristic):");
|
||||||
for (i, c) in cycles.iter().enumerate() {
|
for (i, c) in cycles.iter().enumerate() {
|
||||||
// Prefer breaking an 'incorporate' edge if present, otherwise any edge
|
// 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) {
|
if let Some(es) = graph.adj.get(from) {
|
||||||
for e in es {
|
for e in es {
|
||||||
if &e.to == to {
|
if &e.to == to {
|
||||||
if e.dep_type == "incorporate" { suggested = Some((from.clone(), to.clone())); break 'outer; }
|
if e.dep_type == "incorporate" {
|
||||||
if suggested.is_none() { suggested = Some((from.clone(), to.clone())); }
|
suggested = Some((from.clone(), to.clone()));
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
if suggested.is_none() {
|
||||||
|
suggested = Some((from.clone(), to.clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((from, to)) = suggested {
|
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 {
|
} 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 ----------
|
// ---------- Dangling dependency scan ----------
|
||||||
fn run_dangling_scan(
|
fn run_dangling_scan(
|
||||||
image: &Image,
|
image: &Image,
|
||||||
|
|
@ -786,9 +1030,9 @@ fn run_dangling_scan(
|
||||||
format: OutputFormat,
|
format: OutputFormat,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Query full catalog once
|
// Query full catalog once
|
||||||
let mut pkgs = image
|
let mut pkgs = image.query_catalog(None).map_err(|e| PkgTreeError {
|
||||||
.query_catalog(None)
|
message: format!("Failed to query catalog: {}", e),
|
||||||
.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,
|
// Build set of available non-obsolete stems AND an index of available (release, branch) pairs per stem,
|
||||||
// honoring publisher filter
|
// honoring publisher filter
|
||||||
|
|
@ -796,9 +1040,13 @@ fn run_dangling_scan(
|
||||||
let mut available_index: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
|
let mut available_index: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
|
||||||
for p in &pkgs {
|
for p in &pkgs {
|
||||||
if let Some(pubf) = publisher {
|
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();
|
let stem = p.fmri.stem().to_string();
|
||||||
available_stems.insert(stem.clone());
|
available_stems.insert(stem.clone());
|
||||||
let ver = p.fmri.version();
|
let ver = p.fmri.version();
|
||||||
|
|
@ -828,8 +1076,12 @@ fn run_dangling_scan(
|
||||||
Ok(Some(man)) => {
|
Ok(Some(man)) => {
|
||||||
let mut missing_for_pkg: Vec<String> = Vec::new();
|
let mut missing_for_pkg: Vec<String> = Vec::new();
|
||||||
for dep in man.dependencies {
|
for dep in man.dependencies {
|
||||||
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { continue; }
|
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" {
|
||||||
let Some(df) = dep.fmri else { continue; };
|
continue;
|
||||||
|
}
|
||||||
|
let Some(df) = dep.fmri else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let stem = df.stem().to_string();
|
let stem = df.stem().to_string();
|
||||||
|
|
||||||
// Extract version/branch constraints if any (from optional properties)
|
// 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 {
|
let satisfies = |stem: &str, rel: Option<&str>, br: Option<&str>| -> bool {
|
||||||
if let Some(list) = available_index.get(stem) {
|
if let Some(list) = available_index.get(stem) {
|
||||||
if let (Some(rreq), Some(breq)) = (rel, br) {
|
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 {
|
} else if let Some(rreq) = rel {
|
||||||
return list.iter().any(|(r, _)| r == rreq);
|
return list.iter().any(|(r, _)| r == rreq);
|
||||||
} else if let Some(breq) = br {
|
} 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()) {
|
if !satisfies(&stem, c.release.as_deref(), c.branch.as_deref()) {
|
||||||
// Include constraint context in output for maintainers
|
// Include constraint context in output for maintainers
|
||||||
let mut ctx = String::new();
|
let mut ctx = String::new();
|
||||||
if let Some(r) = &c.release { ctx.push_str(&format!("release={} ", r)); }
|
if let Some(r) = &c.release {
|
||||||
if let Some(b) = &c.branch { ctx.push_str(&format!("branch={}", b)); }
|
ctx.push_str(&format!("release={} ", r));
|
||||||
|
}
|
||||||
|
if let Some(b) = &c.branch {
|
||||||
|
ctx.push_str(&format!("branch={}", b));
|
||||||
|
}
|
||||||
let ctx = ctx.trim().to_string();
|
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() {
|
if !missing_for_pkg.is_empty() {
|
||||||
missing_for_pkg.sort();
|
missing_for_pkg.sort();
|
||||||
|
|
@ -898,13 +1162,18 @@ fn run_dangling_scan(
|
||||||
if dangling.is_empty() {
|
if dangling.is_empty() {
|
||||||
println!("No dangling dependencies detected.");
|
println!("No dangling dependencies detected.");
|
||||||
} else {
|
} 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();
|
let mut keys: Vec<String> = dangling.keys().cloned().collect();
|
||||||
keys.sort();
|
keys.sort();
|
||||||
for k in keys {
|
for k in keys {
|
||||||
println!("- {}:", k);
|
println!("- {}:", k);
|
||||||
if let Some(list) = dangling.get(&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 => {
|
OutputFormat::Json => {
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
#[derive(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();
|
let mut out: Vec<DanglingJson> = Vec::new();
|
||||||
for (pkg, miss) in dangling.into_iter() {
|
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));
|
out.sort_by(|a, b| a.package_fmri.cmp(&b.package_fmri));
|
||||||
println!("{}", serde_json::to_string_pretty(&out).into_diagnostic()?);
|
println!("{}", serde_json::to_string_pretty(&out).into_diagnostic()?);
|
||||||
|
|
@ -927,8 +1202,9 @@ fn run_dangling_scan(
|
||||||
|
|
||||||
// ---------- Targeted analysis: parse pkg6 solver error text ----------
|
// ---------- Targeted analysis: parse pkg6 solver error text ----------
|
||||||
fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathBuf) -> Result<()> {
|
fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathBuf) -> Result<()> {
|
||||||
let text = std::fs::read_to_string(err_path)
|
let text = std::fs::read_to_string(err_path).map_err(|e| PkgTreeError {
|
||||||
.map_err(|e| PkgTreeError { message: format!("Failed to read solver error file {:?}: {}", err_path, e) })?;
|
message: format!("Failed to read solver error file {:?}: {}", err_path, e),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Build a stack based on indentation before the tree bullet "└─".
|
// Build a stack based on indentation before the tree bullet "└─".
|
||||||
let mut stack: Vec<String> = Vec::new();
|
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 "└─ "
|
// Extract node text after "└─ "
|
||||||
let bullet = "└─ ";
|
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();
|
let mut node_full = line[start..].trim().to_string();
|
||||||
// Remove trailing diagnostic phrases for leaf line
|
// Remove trailing diagnostic phrases for leaf line
|
||||||
if let Some(pos) = node_full.find("for which no candidates were found") {
|
if let Some(pos) = node_full.find("for which no candidates were found") {
|
||||||
node_full = node_full[..pos].trim().trim_end_matches(',').to_string();
|
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") {
|
if line.contains("for which no candidates were found") {
|
||||||
failing_leaf = Some(node_full.clone());
|
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() {
|
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(());
|
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):");
|
println!("Found 1 installability issue (from solver error):");
|
||||||
let constraint_str = format!(
|
let constraint_str = format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
constraint.release.as_ref().map(|r| format!("release={} ", r)).unwrap_or_default(),
|
constraint
|
||||||
constraint.branch.as_ref().map(|b| format!("branch={}", b)).unwrap_or_default(),
|
.release
|
||||||
).trim().to_string();
|
.as_ref()
|
||||||
println!(" 1. No viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}",
|
.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,
|
stem,
|
||||||
path_stems.join(" -> "),
|
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,
|
details,
|
||||||
);
|
);
|
||||||
println!(" - Suggestions:");
|
println!(" - Suggestions:");
|
||||||
println!(" • Add or publish a matching package for '{}'{}{}.",
|
println!(
|
||||||
|
" • Add or publish a matching package for '{}'{}{}.",
|
||||||
stem,
|
stem,
|
||||||
constraint.release.as_ref().map(|r| format!(" (release={})", r)).unwrap_or_default(),
|
constraint
|
||||||
constraint.branch.as_ref().map(|b| format!(" (branch={})", b)).unwrap_or_default());
|
.release
|
||||||
println!(" • Alternatively, relax the dependency constraint in the requiring package to match available releases.");
|
.as_ref()
|
||||||
if let Some(lock) = get_incorporated_release_cached(image, &mut ctx, &stem).ok().flatten() {
|
.map(|r| format!(" (release={})", r))
|
||||||
println!(" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", stem, lock);
|
.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'.");
|
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
|
// 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("");
|
let first = node.split_whitespace().next().unwrap_or("");
|
||||||
if first.starts_with("pkg://") {
|
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 it contains '@' (FMRI without scheme), parse via Fmri::parse
|
||||||
if first.contains('@') {
|
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
|
// Otherwise assume it's a stem token
|
||||||
first.trim_end_matches(',').to_string()
|
first.trim_end_matches(',').to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_leaf_node(node: &str) -> (String, DepConstraint) {
|
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 release: Option<String> = None;
|
||||||
let mut branch: Option<String> = None;
|
let mut branch: Option<String> = None;
|
||||||
|
|
||||||
// Find release=
|
// Find release=
|
||||||
if let Some(p) = core.find("release=") {
|
if let Some(p) = core.find("release=") {
|
||||||
let rest = &core[p + "release=".len()..];
|
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());
|
release = Some(rest[..end].to_string());
|
||||||
}
|
}
|
||||||
// Find branch=
|
// Find branch=
|
||||||
if let Some(p) = core.find("branch=") {
|
if let Some(p) = core.find("branch=") {
|
||||||
let rest = &core[p + "branch=".len()..];
|
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());
|
branch = Some(rest[..end].to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ mod sources;
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
mod workspace;
|
mod workspace;
|
||||||
|
|
||||||
use clap::ArgAction;
|
|
||||||
use crate::workspace::Workspace;
|
use crate::workspace::Workspace;
|
||||||
use anyhow::anyhow;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use clap::ArgAction;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use specfile::macros;
|
use specfile::macros;
|
||||||
use specfile::parse;
|
use specfile::parse;
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ use libips::actions::{ActionError, File as FileAction, Manifest};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::env::{current_dir, set_current_dir};
|
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::copy;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::io::Error as IOError;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::result::Result as StdResult;
|
use std::result::Result as StdResult;
|
||||||
|
|
|
||||||
|
|
@ -43,34 +43,38 @@ impl MacroParser {
|
||||||
for macro_pair in inner.clone().into_inner() {
|
for macro_pair in inner.clone().into_inner() {
|
||||||
match macro_pair.as_rule() {
|
match macro_pair.as_rule() {
|
||||||
Rule::macro_name => {
|
Rule::macro_name => {
|
||||||
replaced_line += self.get_variable(macro_pair.as_str())?;
|
replaced_line +=
|
||||||
},
|
self.get_variable(macro_pair.as_str())?;
|
||||||
|
}
|
||||||
Rule::macro_parameter => {
|
Rule::macro_parameter => {
|
||||||
println!("macro parameter: {}", macro_pair.as_str())
|
println!(
|
||||||
},
|
"macro parameter: {}",
|
||||||
|
macro_pair.as_str()
|
||||||
|
)
|
||||||
|
}
|
||||||
_ => panic!(
|
_ => panic!(
|
||||||
"Unexpected macro match please update the code together with the peg grammar: {:?}",
|
"Unexpected macro match please update the code together with the peg grammar: {:?}",
|
||||||
macro_pair.as_rule()
|
macro_pair.as_rule()
|
||||||
)
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => panic!(
|
_ => panic!(
|
||||||
"Unexpected inner match please update the code together with the peg grammar: {:?}",
|
"Unexpected inner match please update the code together with the peg grammar: {:?}",
|
||||||
inner.as_rule()
|
inner.as_rule()
|
||||||
)
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Rule::EOI => (),
|
Rule::EOI => (),
|
||||||
Rule::text => {
|
Rule::text => {
|
||||||
replaced_line += test_pair.as_str();
|
replaced_line += test_pair.as_str();
|
||||||
replaced_line += " ";
|
replaced_line += " ";
|
||||||
},
|
}
|
||||||
_ => panic!(
|
_ => panic!(
|
||||||
"Unexpected match please update the code together with the peg grammar: {:?}",
|
"Unexpected match please update the code together with the peg grammar: {:?}",
|
||||||
test_pair.as_rule()
|
test_pair.as_rule()
|
||||||
)
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result, bail};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
mod component;
|
mod component;
|
||||||
pub mod repology;
|
pub mod repology;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use pest::iterators::Pairs;
|
|
||||||
use pest::Parser;
|
use pest::Parser;
|
||||||
|
use pest::iterators::Pairs;
|
||||||
use pest_derive::Parser;
|
use pest_derive::Parser;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -233,13 +233,16 @@ fn parse_makefile(pairs: Pairs<crate::Rule>, m: &mut Makefile) -> Result<()> {
|
||||||
Rule::comment_string => (),
|
Rule::comment_string => (),
|
||||||
Rule::include => {
|
Rule::include => {
|
||||||
parse_include(p.into_inner(), m)?;
|
parse_include(p.into_inner(), m)?;
|
||||||
},
|
}
|
||||||
Rule::target => (),
|
Rule::target => (),
|
||||||
Rule::define => {
|
Rule::define => {
|
||||||
parse_define(p.into_inner(), m)?;
|
parse_define(p.into_inner(), m)?;
|
||||||
}
|
}
|
||||||
Rule::EOI => (),
|
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 => {
|
Rule::variable_name => {
|
||||||
var.0 = p.as_str().to_string();
|
var.0 = p.as_str().to_string();
|
||||||
}
|
}
|
||||||
Rule::variable_set => {
|
Rule::variable_set => var.1.mode = VariableMode::Set,
|
||||||
var.1.mode = VariableMode::Set
|
Rule::variable_add => var.1.mode = VariableMode::Add,
|
||||||
},
|
Rule::variable_value => match var.1.mode {
|
||||||
Rule::variable_add => {
|
VariableMode::Add => {
|
||||||
var.1.mode = VariableMode::Add
|
if m.variables.contains_key(&var.0) {
|
||||||
}
|
var.1 = m.variables.get(&var.0).unwrap().clone()
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
var.1.values.push(p.as_str().to_string());
|
||||||
}
|
}
|
||||||
}
|
VariableMode::Set => {
|
||||||
_ => panic!("unexpected rule {:?} inside makefile rule expected variable_name, variable_set, variable_add, variable_value", p.as_rule()),
|
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);
|
m.variables.insert(var.0, var.1);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue