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)
|
||||||
}
|
}
|
||||||
|
|
@ -638,31 +685,48 @@ 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);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -46,7 +46,7 @@ pub type Result<T> = std::result::Result<T, InstalledError>;
|
||||||
pub struct InstalledPackageInfo {
|
pub struct InstalledPackageInfo {
|
||||||
/// The FMRI of the package
|
/// The FMRI of the package
|
||||||
pub fmri: Fmri,
|
pub fmri: Fmri,
|
||||||
|
|
||||||
/// The publisher of the package
|
/// The publisher of the package
|
||||||
pub publisher: String,
|
pub publisher: String,
|
||||||
}
|
}
|
||||||
|
|
@ -68,48 +68,58 @@ impl InstalledPackages {
|
||||||
// To fix this issue, we use block scopes {} around table operations to ensure that the table
|
// To fix this issue, we use block scopes {} around table operations to ensure that the table
|
||||||
// objects are dropped (and their borrows released) before committing the transaction.
|
// objects are dropped (and their borrows released) before committing the transaction.
|
||||||
// This pattern is used in all methods that commit transactions after table operations.
|
// This pattern is used in all methods that commit transactions after table operations.
|
||||||
|
|
||||||
/// Create a new installed packages database
|
/// Create a new installed packages database
|
||||||
pub fn new<P: AsRef<Path>>(db_path: P) -> Self {
|
pub fn new<P: AsRef<Path>>(db_path: P) -> Self {
|
||||||
InstalledPackages {
|
InstalledPackages {
|
||||||
db_path: db_path.as_ref().to_path_buf(),
|
db_path: db_path.as_ref().to_path_buf(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dump the contents of the installed table to stdout for debugging
|
/// Dump the contents of the installed table to stdout for debugging
|
||||||
pub fn dump_installed_table(&self) -> Result<()> {
|
pub fn dump_installed_table(&self) -> Result<()> {
|
||||||
// Open the database
|
// Open the database
|
||||||
let db = Database::open(&self.db_path)
|
let db = Database::open(&self.db_path)
|
||||||
.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());
|
||||||
|
|
||||||
println!("Key: {}", key_str);
|
println!("Key: {}", key_str);
|
||||||
println!(" FMRI: {}", publisher);
|
println!(" FMRI: {}", publisher);
|
||||||
println!(" Attributes: {}", manifest.attributes.len());
|
println!(" Attributes: {}", manifest.attributes.len());
|
||||||
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,214 +129,252 @@ 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
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get database statistics
|
/// Get database statistics
|
||||||
pub fn get_db_stats(&self) -> Result<()> {
|
pub fn get_db_stats(&self) -> Result<()> {
|
||||||
// Open the database
|
// Open the database
|
||||||
let db = Database::open(&self.db_path)
|
let db = Database::open(&self.db_path)
|
||||||
.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
|
||||||
let mut installed_count = 0;
|
let mut installed_count = 0;
|
||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print statistics
|
// Print statistics
|
||||||
println!("Database path: {}", self.db_path.display());
|
println!("Database path: {}", self.db_path.display());
|
||||||
println!("Table statistics:");
|
println!("Table statistics:");
|
||||||
println!(" Installed table: {} entries", installed_count);
|
println!(" Installed table: {} entries", installed_count);
|
||||||
println!("Total entries: {}", installed_count);
|
println!("Total entries: {}", installed_count);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the installed packages database
|
/// Initialize the installed packages database
|
||||||
pub fn init_db(&self) -> Result<()> {
|
pub fn init_db(&self) -> Result<()> {
|
||||||
// Create a parent directory if it doesn't exist
|
// Create a parent directory if it doesn't exist
|
||||||
if let Some(parent) = self.db_path.parent() {
|
if let Some(parent) = self.db_path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open or create the database
|
// Open or create the database
|
||||||
let db = Database::create(&self.db_path)
|
let db = Database::create(&self.db_path)
|
||||||
.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()
|
|
||||||
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
|
tx.commit().map_err(|e| {
|
||||||
|
InstalledError::Database(format!("Failed to commit transaction: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a package to the installed packages database
|
/// Add a package to the installed packages database
|
||||||
pub fn add_package(&self, fmri: &Fmri, manifest: &Manifest) -> Result<()> {
|
pub fn add_package(&self, fmri: &Fmri, manifest: &Manifest) -> Result<()> {
|
||||||
// Open the database
|
// Open the database
|
||||||
let db = Database::open(&self.db_path)
|
let db = Database::open(&self.db_path)
|
||||||
.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)
|
||||||
let key = fmri.to_string();
|
let key = fmri.to_string();
|
||||||
|
|
||||||
// Serialize the manifest
|
// Serialize the manifest
|
||||||
let manifest_bytes = serde_json::to_vec(manifest)?;
|
let manifest_bytes = serde_json::to_vec(manifest)?;
|
||||||
|
|
||||||
// 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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a package from the installed packages database
|
/// Remove a package from the installed packages database
|
||||||
pub fn remove_package(&self, fmri: &Fmri) -> Result<()> {
|
pub fn remove_package(&self, fmri: &Fmri) -> Result<()> {
|
||||||
// Open the database
|
// Open the database
|
||||||
let db = Database::open(&self.db_path)
|
let db = Database::open(&self.db_path)
|
||||||
.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)
|
||||||
let key = fmri.to_string();
|
let key = fmri.to_string();
|
||||||
|
|
||||||
// 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()) {
|
||||||
return Err(InstalledError::PackageNotFound(key));
|
return Err(InstalledError::PackageNotFound(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query the installed packages database for packages matching a pattern
|
/// Query the installed packages database for packages matching a pattern
|
||||||
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<InstalledPackageInfo>> {
|
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<InstalledPackageInfo>> {
|
||||||
// Open the database
|
// Open the database
|
||||||
let db = Database::open(&self.db_path)
|
let db = Database::open(&self.db_path)
|
||||||
.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
|
||||||
if let Some(pattern) = pattern {
|
if let Some(pattern) = pattern {
|
||||||
if !key_str.contains(pattern) {
|
if !key_str.contains(pattern) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the key to get the FMRI
|
// Parse the key to get the FMRI
|
||||||
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
|
||||||
// The table is dropped at the end of this block
|
// The table is dropped at the end of this block
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a manifest from the installed packages database
|
/// Get a manifest from the installed packages database
|
||||||
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
|
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
|
||||||
// Open the database
|
// Open the database
|
||||||
let db = Database::open(&self.db_path)
|
let db = Database::open(&self.db_path)
|
||||||
.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)
|
||||||
let key = fmri.to_string();
|
let key = fmri.to_string();
|
||||||
|
|
||||||
// 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()) {
|
||||||
Some(serde_json::from_slice(bytes.value())?)
|
Some(serde_json::from_slice(bytes.value())?)
|
||||||
|
|
@ -335,29 +383,31 @@ impl InstalledPackages {
|
||||||
}
|
}
|
||||||
// The table is dropped at the end of this block
|
// The table is dropped at the end of this block
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(manifest_option)
|
Ok(manifest_option)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a package is installed
|
/// Check if a package is installed
|
||||||
pub fn is_installed(&self, fmri: &Fmri) -> Result<bool> {
|
pub fn is_installed(&self, fmri: &Fmri) -> Result<bool> {
|
||||||
// Open the database
|
// Open the database
|
||||||
let db = Database::open(&self.db_path)
|
let db = Database::open(&self.db_path)
|
||||||
.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)
|
||||||
let key = fmri.to_string();
|
let key = fmri.to_string();
|
||||||
|
|
||||||
// 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()) {
|
||||||
true
|
true
|
||||||
|
|
@ -366,7 +416,7 @@ impl InstalledPackages {
|
||||||
}
|
}
|
||||||
// The table is dropped at the end of this block
|
// The table is dropped at the end of this block
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(is_installed)
|
Ok(is_installed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,67 +10,70 @@ fn test_installed_packages() {
|
||||||
// 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 image_path = temp_dir.path().join("image");
|
let image_path = temp_dir.path().join("image");
|
||||||
|
|
||||||
// Create the image
|
// Create the image
|
||||||
let image = Image::create_image(&image_path, ImageType::Full).unwrap();
|
let image = Image::create_image(&image_path, ImageType::Full).unwrap();
|
||||||
|
|
||||||
// Verify that the installed packages database was initialized
|
// Verify that the installed packages database was initialized
|
||||||
assert!(image.installed_db_path().exists());
|
assert!(image.installed_db_path().exists());
|
||||||
|
|
||||||
// Create a test manifest
|
// Create a test manifest
|
||||||
let mut manifest = Manifest::new();
|
let mut manifest = Manifest::new();
|
||||||
|
|
||||||
// Add some attributes to the manifest
|
// Add some attributes to the manifest
|
||||||
let mut attr = Attr::default();
|
let mut attr = Attr::default();
|
||||||
attr.key = "pkg.fmri".to_string();
|
attr.key = "pkg.fmri".to_string();
|
||||||
attr.values = vec!["pkg://test/example/package@1.0".to_string()];
|
attr.values = vec!["pkg://test/example/package@1.0".to_string()];
|
||||||
manifest.attributes.push(attr);
|
manifest.attributes.push(attr);
|
||||||
|
|
||||||
let mut attr = Attr::default();
|
let mut attr = Attr::default();
|
||||||
attr.key = "pkg.summary".to_string();
|
attr.key = "pkg.summary".to_string();
|
||||||
attr.values = vec!["Example package".to_string()];
|
attr.values = vec!["Example package".to_string()];
|
||||||
manifest.attributes.push(attr);
|
manifest.attributes.push(attr);
|
||||||
|
|
||||||
let mut attr = Attr::default();
|
let mut attr = Attr::default();
|
||||||
attr.key = "pkg.description".to_string();
|
attr.key = "pkg.description".to_string();
|
||||||
attr.values = vec!["An example package for testing".to_string()];
|
attr.values = vec!["An example package for testing".to_string()];
|
||||||
manifest.attributes.push(attr);
|
manifest.attributes.push(attr);
|
||||||
|
|
||||||
// Create an FMRI for the package
|
// Create an FMRI for the package
|
||||||
let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap();
|
let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap();
|
||||||
|
|
||||||
// Install the package
|
// Install the package
|
||||||
image.install_package(&fmri, &manifest).unwrap();
|
image.install_package(&fmri, &manifest).unwrap();
|
||||||
|
|
||||||
// Verify that the package is installed
|
// Verify that the package is installed
|
||||||
assert!(image.is_package_installed(&fmri).unwrap());
|
assert!(image.is_package_installed(&fmri).unwrap());
|
||||||
|
|
||||||
// Query the installed packages
|
// Query the installed packages
|
||||||
let packages = image.query_installed_packages(None).unwrap();
|
let packages = image.query_installed_packages(None).unwrap();
|
||||||
|
|
||||||
// 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
|
||||||
let installed_manifest = image.get_manifest_from_installed(&fmri).unwrap().unwrap();
|
let installed_manifest = image.get_manifest_from_installed(&fmri).unwrap().unwrap();
|
||||||
|
|
||||||
// Verify that the manifest is correct
|
// Verify that the manifest is correct
|
||||||
assert_eq!(installed_manifest.attributes.len(), 3);
|
assert_eq!(installed_manifest.attributes.len(), 3);
|
||||||
|
|
||||||
// Uninstall the package
|
// Uninstall the package
|
||||||
image.uninstall_package(&fmri).unwrap();
|
image.uninstall_package(&fmri).unwrap();
|
||||||
|
|
||||||
// Verify that the package is no longer installed
|
// Verify that the package is no longer installed
|
||||||
assert!(!image.is_package_installed(&fmri).unwrap());
|
assert!(!image.is_package_installed(&fmri).unwrap());
|
||||||
|
|
||||||
// Query the installed packages again
|
// Query the installed packages again
|
||||||
let packages = image.query_installed_packages(None).unwrap();
|
let packages = image.query_installed_packages(None).unwrap();
|
||||||
|
|
||||||
// Verify that there are no packages
|
// Verify that there are no packages
|
||||||
assert_eq!(packages.len(), 0);
|
assert_eq!(packages.len(), 0);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
temp_dir.close().unwrap();
|
temp_dir.close().unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -80,42 +83,42 @@ fn test_installed_packages_key_format() {
|
||||||
// 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 db_path = temp_dir.path().join("installed.redb");
|
let db_path = temp_dir.path().join("installed.redb");
|
||||||
|
|
||||||
// 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().unwrap();
|
installed.init_db().unwrap();
|
||||||
|
|
||||||
// Create a test manifest
|
// Create a test manifest
|
||||||
let mut manifest = Manifest::new();
|
let mut manifest = Manifest::new();
|
||||||
|
|
||||||
// Add some attributes to the manifest
|
// Add some attributes to the manifest
|
||||||
let mut attr = Attr::default();
|
let mut attr = Attr::default();
|
||||||
attr.key = "pkg.fmri".to_string();
|
attr.key = "pkg.fmri".to_string();
|
||||||
attr.values = vec!["pkg://test/example/package@1.0".to_string()];
|
attr.values = vec!["pkg://test/example/package@1.0".to_string()];
|
||||||
manifest.attributes.push(attr);
|
manifest.attributes.push(attr);
|
||||||
|
|
||||||
// Create an FMRI for the package
|
// Create an FMRI for the package
|
||||||
let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap();
|
let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap();
|
||||||
|
|
||||||
// Add the package to the database
|
// Add the package to the database
|
||||||
installed.add_package(&fmri, &manifest).unwrap();
|
installed.add_package(&fmri, &manifest).unwrap();
|
||||||
|
|
||||||
// Open the database directly to check the key format
|
// Open the database directly to check the key format
|
||||||
let db = Database::open(&db_path).unwrap();
|
let db = Database::open(&db_path).unwrap();
|
||||||
let tx = db.begin_read().unwrap();
|
let tx = db.begin_read().unwrap();
|
||||||
let table = tx.open_table(installed::INSTALLED_TABLE).unwrap();
|
let table = tx.open_table(installed::INSTALLED_TABLE).unwrap();
|
||||||
|
|
||||||
// Iterate through the keys
|
// Iterate through the keys
|
||||||
let mut keys = Vec::new();
|
let mut keys = Vec::new();
|
||||||
for entry in table.iter().unwrap() {
|
for entry in table.iter().unwrap() {
|
||||||
let (key, _) = entry.unwrap();
|
let (key, _) = entry.unwrap();
|
||||||
keys.push(key.value().to_string());
|
keys.push(key.value().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that there is one key and it has the correct format
|
// Verify that there is one key and it has the correct format
|
||||||
assert_eq!(keys.len(), 1);
|
assert_eq!(keys.len(), 1);
|
||||||
assert_eq!(keys[0], "pkg://test/example/package@1.0");
|
assert_eq!(keys[0], "pkg://test/example/package@1.0");
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
temp_dir.close().unwrap();
|
temp_dir.close().unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -49,28 +49,28 @@ pub enum ImageError {
|
||||||
help("Provide a valid path for the image")
|
help("Provide a valid path for the image")
|
||||||
)]
|
)]
|
||||||
InvalidPath(String),
|
InvalidPath(String),
|
||||||
|
|
||||||
#[error("Repository error: {0}")]
|
#[error("Repository error: {0}")]
|
||||||
#[diagnostic(
|
#[diagnostic(
|
||||||
code(ips::image_error::repository),
|
code(ips::image_error::repository),
|
||||||
help("Check the repository configuration and try again")
|
help("Check the repository configuration and try again")
|
||||||
)]
|
)]
|
||||||
Repository(#[from] RepositoryError),
|
Repository(#[from] RepositoryError),
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
#[diagnostic(
|
#[diagnostic(
|
||||||
code(ips::image_error::database),
|
code(ips::image_error::database),
|
||||||
help("Check the database configuration and try again")
|
help("Check the database configuration and try again")
|
||||||
)]
|
)]
|
||||||
Database(String),
|
Database(String),
|
||||||
|
|
||||||
#[error("Publisher not found: {0}")]
|
#[error("Publisher not found: {0}")]
|
||||||
#[diagnostic(
|
#[diagnostic(
|
||||||
code(ips::image_error::publisher_not_found),
|
code(ips::image_error::publisher_not_found),
|
||||||
help("Check the publisher name and try again")
|
help("Check the publisher name and try again")
|
||||||
)]
|
)]
|
||||||
PublisherNotFound(String),
|
PublisherNotFound(String),
|
||||||
|
|
||||||
#[error("No publishers configured")]
|
#[error("No publishers configured")]
|
||||||
#[diagnostic(
|
#[diagnostic(
|
||||||
code(ips::image_error::no_publishers),
|
code(ips::image_error::no_publishers),
|
||||||
|
|
@ -148,9 +148,15 @@ impl Image {
|
||||||
publishers: vec![],
|
publishers: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
|
|
@ -159,7 +165,7 @@ impl Image {
|
||||||
publisher.origin = origin.to_string();
|
publisher.origin = origin.to_string();
|
||||||
publisher.mirrors = mirrors;
|
publisher.mirrors = mirrors;
|
||||||
publisher.is_default = is_default;
|
publisher.is_default = is_default;
|
||||||
|
|
||||||
// If this publisher is now the default, make sure no other publisher is default
|
// If this publisher is now the default, make sure no other publisher is default
|
||||||
if is_default {
|
if is_default {
|
||||||
for other_publisher in &mut self.publishers {
|
for other_publisher in &mut self.publishers {
|
||||||
|
|
@ -168,7 +174,7 @@ impl Image {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -180,43 +186,43 @@ impl Image {
|
||||||
mirrors,
|
mirrors,
|
||||||
is_default,
|
is_default,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If this publisher is the default, make sure no other publisher is default
|
// If this publisher is the default, make sure no other publisher is default
|
||||||
if is_default {
|
if is_default {
|
||||||
for publisher in &mut self.publishers {
|
for publisher in &mut self.publishers {
|
||||||
publisher.is_default = false;
|
publisher.is_default = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.publishers.push(publisher);
|
self.publishers.push(publisher);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the image to persist the changes
|
// Save the image to persist the changes
|
||||||
self.save()?;
|
self.save()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a publisher from the image
|
/// Remove a publisher from the image
|
||||||
pub fn remove_publisher(&mut self, name: &str) -> Result<()> {
|
pub fn remove_publisher(&mut self, name: &str) -> Result<()> {
|
||||||
let initial_len = self.publishers.len();
|
let initial_len = self.publishers.len();
|
||||||
self.publishers.retain(|p| p.name != name);
|
self.publishers.retain(|p| p.name != name);
|
||||||
|
|
||||||
if self.publishers.len() == initial_len {
|
if self.publishers.len() == initial_len {
|
||||||
return Err(ImageError::PublisherNotFound(name.to_string()));
|
return Err(ImageError::PublisherNotFound(name.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we removed the default publisher, set the first remaining publisher as default
|
// If we removed the default publisher, set the first remaining publisher as default
|
||||||
if self.publishers.iter().all(|p| !p.is_default) && !self.publishers.is_empty() {
|
if self.publishers.iter().all(|p| !p.is_default) && !self.publishers.is_empty() {
|
||||||
self.publishers[0].is_default = true;
|
self.publishers[0].is_default = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the image to persist the changes
|
// Save the image to persist the changes
|
||||||
self.save()?;
|
self.save()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the default publisher
|
/// Get the default publisher
|
||||||
pub fn default_publisher(&self) -> Result<&Publisher> {
|
pub fn default_publisher(&self) -> Result<&Publisher> {
|
||||||
// Find the default publisher
|
// Find the default publisher
|
||||||
|
|
@ -225,15 +231,15 @@ impl Image {
|
||||||
return Ok(publisher);
|
return Ok(publisher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no publisher is marked as default, return the first one
|
// If no publisher is marked as default, return the first one
|
||||||
if !self.publishers.is_empty() {
|
if !self.publishers.is_empty() {
|
||||||
return Ok(&self.publishers[0]);
|
return Ok(&self.publishers[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(ImageError::NoPublishers)
|
Err(ImageError::NoPublishers)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a publisher by name
|
/// Get a publisher by name
|
||||||
pub fn get_publisher(&self, name: &str) -> Result<&Publisher> {
|
pub fn get_publisher(&self, name: &str) -> Result<&Publisher> {
|
||||||
for publisher in &self.publishers {
|
for publisher in &self.publishers {
|
||||||
|
|
@ -241,10 +247,10 @@ impl Image {
|
||||||
return Ok(publisher);
|
return Ok(publisher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(ImageError::PublisherNotFound(name.to_string()))
|
Err(ImageError::PublisherNotFound(name.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all publishers
|
/// Get all publishers
|
||||||
pub fn publishers(&self) -> &[Publisher] {
|
pub fn publishers(&self) -> &[Publisher] {
|
||||||
&self.publishers
|
&self.publishers
|
||||||
|
|
@ -272,27 +278,27 @@ impl Image {
|
||||||
pub fn image_json_path(&self) -> PathBuf {
|
pub fn image_json_path(&self) -> PathBuf {
|
||||||
self.metadata_dir().join("pkg6.image.json")
|
self.metadata_dir().join("pkg6.image.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the installed packages database
|
/// Returns the path to the installed packages database
|
||||||
pub fn installed_db_path(&self) -> PathBuf {
|
pub fn installed_db_path(&self) -> PathBuf {
|
||||||
self.metadata_dir().join("installed.redb")
|
self.metadata_dir().join("installed.redb")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the manifest directory
|
/// Returns the path to the manifest directory
|
||||||
pub fn manifest_dir(&self) -> PathBuf {
|
pub fn manifest_dir(&self) -> PathBuf {
|
||||||
self.metadata_dir().join("manifests")
|
self.metadata_dir().join("manifests")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the catalog directory
|
/// Returns the path to the catalog directory
|
||||||
pub fn catalog_dir(&self) -> PathBuf {
|
pub fn catalog_dir(&self) -> PathBuf {
|
||||||
self.metadata_dir().join("catalog")
|
self.metadata_dir().join("catalog")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the catalog database
|
/// Returns the path to the catalog database
|
||||||
pub fn catalog_db_path(&self) -> PathBuf {
|
pub fn catalog_db_path(&self) -> PathBuf {
|
||||||
self.metadata_dir().join("catalog.redb")
|
self.metadata_dir().join("catalog.redb")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the obsoleted packages database (separate DB)
|
/// Returns the path to the obsoleted packages database (separate DB)
|
||||||
pub fn obsoleted_db_path(&self) -> PathBuf {
|
pub fn obsoleted_db_path(&self) -> PathBuf {
|
||||||
self.metadata_dir().join("obsoleted.redb")
|
self.metadata_dir().join("obsoleted.redb")
|
||||||
|
|
@ -304,46 +310,62 @@ 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
|
||||||
|
),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the manifest directory if it doesn't exist
|
/// Creates the manifest directory if it doesn't exist
|
||||||
pub fn create_manifest_dir(&self) -> Result<()> {
|
pub fn create_manifest_dir(&self) -> Result<()> {
|
||||||
let manifest_dir = self.manifest_dir();
|
let manifest_dir = self.manifest_dir();
|
||||||
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
|
||||||
|
),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the catalog directory if it doesn't exist
|
/// Creates the catalog directory if it doesn't exist
|
||||||
pub fn create_catalog_dir(&self) -> Result<()> {
|
pub fn create_catalog_dir(&self) -> Result<()> {
|
||||||
let catalog_dir = self.catalog_dir();
|
let catalog_dir = self.catalog_dir();
|
||||||
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
|
||||||
|
),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the installed packages database
|
/// Initialize the installed packages database
|
||||||
pub fn init_installed_db(&self) -> Result<()> {
|
pub fn init_installed_db(&self) -> Result<()> {
|
||||||
let db_path = self.installed_db_path();
|
let db_path = self.installed_db_path();
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -380,31 +406,43 @@ impl Image {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a package from the installed packages database
|
/// Remove a package from the installed packages database
|
||||||
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
|
||||||
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a package is installed
|
/// Check if a package is installed
|
||||||
pub fn is_package_installed(&self, fmri: &crate::fmri::Fmri) -> Result<bool> {
|
pub fn is_package_installed(&self, fmri: &crate::fmri::Fmri) -> Result<bool> {
|
||||||
let installed = InstalledPackages::new(self.installed_db_path());
|
let installed = InstalledPackages::new(self.installed_db_path());
|
||||||
|
|
@ -412,14 +450,18 @@ impl Image {
|
||||||
ImageError::Database(format!("Failed to check if package is installed: {}", e))
|
ImageError::Database(format!("Failed to check if package is installed: {}", e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save a manifest into the metadata manifests directory for this image.
|
/// Save a manifest into the metadata manifests directory for this image.
|
||||||
///
|
///
|
||||||
/// The original, unprocessed manifest text is downloaded from the repository
|
/// The original, unprocessed manifest text is downloaded from the repository
|
||||||
/// 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('%');
|
||||||
|
|
@ -481,31 +525,35 @@ impl Image {
|
||||||
|
|
||||||
Ok(file_path)
|
Ok(file_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download catalogs from all configured publishers and build the merged catalog
|
/// Download catalogs from all configured publishers and build the merged catalog
|
||||||
pub fn download_catalogs(&self) -> Result<()> {
|
pub fn download_catalogs(&self) -> Result<()> {
|
||||||
// Create catalog directory if it doesn't exist
|
// Create catalog directory if it doesn't exist
|
||||||
self.create_catalog_dir()?;
|
self.create_catalog_dir()?;
|
||||||
|
|
||||||
// Download catalogs for each publisher
|
// Download catalogs for each publisher
|
||||||
for publisher in &self.publishers {
|
for publisher in &self.publishers {
|
||||||
self.download_publisher_catalog(&publisher.name)?;
|
self.download_publisher_catalog(&publisher.name)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the merged catalog
|
// Build the merged catalog
|
||||||
self.build_catalog()?;
|
self.build_catalog()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh catalogs for specified publishers or all publishers if none specified
|
/// Refresh catalogs for specified publishers or all publishers if none specified
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|
@ -519,78 +567,91 @@ impl Image {
|
||||||
pub fn refresh_catalogs(&self, publishers: &[String], full: bool) -> Result<()> {
|
pub fn refresh_catalogs(&self, publishers: &[String], full: bool) -> Result<()> {
|
||||||
// Create catalog directory if it doesn't exist
|
// Create catalog directory if it doesn't exist
|
||||||
self.create_catalog_dir()?;
|
self.create_catalog_dir()?;
|
||||||
|
|
||||||
// Determine which publishers to refresh
|
// Determine which publishers to refresh
|
||||||
let publishers_to_refresh: Vec<&Publisher> = if publishers.is_empty() {
|
let publishers_to_refresh: Vec<&Publisher> = if publishers.is_empty() {
|
||||||
// If no publishers specified, refresh all
|
// If no publishers specified, refresh all
|
||||||
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()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if we have any publishers to refresh
|
// Check if we have any publishers to refresh
|
||||||
if publishers_to_refresh.is_empty() {
|
if publishers_to_refresh.is_empty() {
|
||||||
return Err(ImageError::NoPublishers);
|
return Err(ImageError::NoPublishers);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If full refresh is requested, clear the catalog directory for each publisher
|
// If full refresh is requested, clear the catalog directory for each publisher
|
||||||
if full {
|
if full {
|
||||||
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
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download catalogs for each publisher
|
// Download catalogs for each publisher
|
||||||
for publisher in publishers_to_refresh {
|
for publisher in publishers_to_refresh {
|
||||||
self.download_publisher_catalog(&publisher.name)?;
|
self.download_publisher_catalog(&publisher.name)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the merged catalog
|
// Build the merged catalog
|
||||||
self.build_catalog()?;
|
self.build_catalog()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the merged catalog from downloaded catalogs
|
/// Build the merged catalog from downloaded catalogs
|
||||||
pub fn build_catalog(&self) -> Result<()> {
|
pub fn build_catalog(&self) -> Result<()> {
|
||||||
// Initialize the catalog database if it doesn't exist
|
// Initialize the catalog database if it doesn't exist
|
||||||
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,91 +680,109 @@ 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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a full manifest for the given FMRI directly from its repository origin.
|
/// Fetch a full manifest for the given FMRI directly from its repository origin.
|
||||||
///
|
///
|
||||||
/// 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()
|
||||||
} else {
|
} else {
|
||||||
self.default_publisher()?.name.clone()
|
self.default_publisher()?.name.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Look up publisher configuration
|
// Look up publisher configuration
|
||||||
let publisher = self.get_publisher(&publisher_name)?;
|
let publisher = self.get_publisher(&publisher_name)?;
|
||||||
let origin = &publisher.origin;
|
let origin = &publisher.origin;
|
||||||
|
|
||||||
// Require a concrete version in the FMRI
|
// Require a concrete version in the FMRI
|
||||||
if fmri.version().is_empty() {
|
if fmri.version().is_empty() {
|
||||||
return Err(ImageError::Repository(RepositoryError::Other(
|
return Err(ImageError::Repository(RepositoryError::Other(
|
||||||
"FMRI must include a version to fetch manifest".to_string(),
|
"FMRI must include a version to fetch manifest".to_string(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choose backend based on origin scheme
|
// Choose backend based on origin scheme
|
||||||
if origin.starts_with("file://") {
|
if origin.starts_with("file://") {
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download catalog for a specific publisher
|
/// Download catalog for a specific publisher
|
||||||
pub fn download_publisher_catalog(&self, publisher_name: &str) -> Result<()> {
|
pub fn download_publisher_catalog(&self, publisher_name: &str) -> Result<()> {
|
||||||
// Get the publisher
|
// Get the publisher
|
||||||
let publisher = self.get_publisher(publisher_name)?;
|
let publisher = self.get_publisher(publisher_name)?;
|
||||||
|
|
||||||
// Create a REST backend for the publisher
|
// Create a REST backend for the publisher
|
||||||
let mut repo = RestBackend::open(&publisher.origin)?;
|
let mut repo = RestBackend::open(&publisher.origin)?;
|
||||||
|
|
||||||
// Set local cache path to the catalog directory for this publisher
|
// Set local cache path to the catalog directory for this publisher
|
||||||
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
|
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
|
||||||
fs::create_dir_all(&publisher_catalog_dir)?;
|
fs::create_dir_all(&publisher_catalog_dir)?;
|
||||||
repo.set_local_cache_path(&publisher_catalog_dir)?;
|
repo.set_local_cache_path(&publisher_catalog_dir)?;
|
||||||
|
|
||||||
// Download the catalog
|
// Download the catalog
|
||||||
repo.download_catalog(&publisher.name, None)?;
|
repo.download_catalog(&publisher.name, None)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new image with the basic directory structure
|
/// Create a new image with the basic directory structure
|
||||||
///
|
///
|
||||||
/// This method only creates the image structure without adding publishers or downloading catalogs.
|
/// This method only creates the image structure without adding publishers or downloading catalogs.
|
||||||
/// Publisher addition and catalog downloading should be handled separately.
|
/// Publisher addition and catalog downloading should be handled separately.
|
||||||
///
|
///
|
||||||
|
|
@ -715,21 +796,21 @@ impl Image {
|
||||||
ImageType::Full => Image::new_full(path.as_ref().to_path_buf()),
|
ImageType::Full => Image::new_full(path.as_ref().to_path_buf()),
|
||||||
ImageType::Partial => Image::new_partial(path.as_ref().to_path_buf()),
|
ImageType::Partial => Image::new_partial(path.as_ref().to_path_buf()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the directory structure
|
// Create the directory structure
|
||||||
image.create_metadata_dir()?;
|
image.create_metadata_dir()?;
|
||||||
image.create_manifest_dir()?;
|
image.create_manifest_dir()?;
|
||||||
image.create_catalog_dir()?;
|
image.create_catalog_dir()?;
|
||||||
|
|
||||||
// Initialize the installed packages database
|
// Initialize the installed packages database
|
||||||
image.init_installed_db()?;
|
image.init_installed_db()?;
|
||||||
|
|
||||||
// Initialize the catalog database
|
// Initialize the catalog database
|
||||||
image.init_catalog_db()?;
|
image.init_catalog_db()?;
|
||||||
|
|
||||||
// Save the image
|
// Save the image
|
||||||
image.save()?;
|
image.save()?;
|
||||||
|
|
||||||
Ok(image)
|
Ok(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -749,14 +830,14 @@ impl Image {
|
||||||
/// Loads an image from the specified path
|
/// Loads an image from the specified path
|
||||||
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
// Check for both full and partial image JSON files
|
// Check for both full and partial image JSON files
|
||||||
let full_image = Image::new_full(path);
|
let full_image = Image::new_full(path);
|
||||||
let partial_image = Image::new_partial(path);
|
let partial_image = Image::new_partial(path);
|
||||||
|
|
||||||
let full_json_path = full_image.image_json_path();
|
let full_json_path = full_image.image_json_path();
|
||||||
let partial_json_path = partial_image.image_json_path();
|
let partial_json_path = partial_image.image_json_path();
|
||||||
|
|
||||||
// Determine which JSON file exists
|
// Determine which JSON file exists
|
||||||
let json_path = if full_json_path.exists() {
|
let json_path = if full_json_path.exists() {
|
||||||
full_json_path
|
full_json_path
|
||||||
|
|
@ -764,18 +845,18 @@ impl Image {
|
||||||
partial_json_path
|
partial_json_path
|
||||||
} else {
|
} else {
|
||||||
return Err(ImageError::InvalidPath(format!(
|
return Err(ImageError::InvalidPath(format!(
|
||||||
"Image JSON file not found at either {:?} or {:?}",
|
"Image JSON file not found at either {:?} or {:?}",
|
||||||
full_json_path, partial_json_path
|
full_json_path, partial_json_path
|
||||||
)));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let file = File::open(&json_path).map_err(|e| {
|
let file = File::open(&json_path).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 open image JSON file at {:?}: {}", json_path, e),
|
format!("Failed to open image JSON file at {:?}: {}", json_path, e),
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
serde_json::from_reader(file).map_err(ImageError::Json)
|
serde_json::from_reader(file).map_err(ImageError::Json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ fn test_image_catalog() {
|
||||||
// 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 image_path = temp_dir.path().join("image");
|
let image_path = temp_dir.path().join("image");
|
||||||
|
|
||||||
// Create the image
|
// Create the image
|
||||||
let image = Image::create_image(&image_path, ImageType::Full).unwrap();
|
let image = Image::create_image(&image_path, ImageType::Full).unwrap();
|
||||||
|
|
||||||
// Verify that the catalog database was initialized
|
// Verify that the catalog database was initialized
|
||||||
assert!(image.catalog_db_path().exists());
|
assert!(image.catalog_db_path().exists());
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
temp_dir.close().unwrap();
|
temp_dir.close().unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -23,28 +23,30 @@ fn test_catalog_methods() {
|
||||||
// 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 image_path = temp_dir.path().join("image");
|
let image_path = temp_dir.path().join("image");
|
||||||
|
|
||||||
// Create the image
|
// Create the image
|
||||||
let mut image = Image::create_image(&image_path, ImageType::Full).unwrap();
|
let mut image = Image::create_image(&image_path, ImageType::Full).unwrap();
|
||||||
|
|
||||||
// Print the image type and paths
|
// Print the image type and paths
|
||||||
println!("Image type: {:?}", image.image_type());
|
println!("Image type: {:?}", image.image_type());
|
||||||
println!("Image path: {:?}", image.path());
|
println!("Image path: {:?}", image.path());
|
||||||
println!("Metadata dir: {:?}", image.metadata_dir());
|
println!("Metadata dir: {:?}", image.metadata_dir());
|
||||||
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());
|
||||||
|
|
||||||
// Create the catalog directory structure
|
// Create the catalog directory structure
|
||||||
let catalog_dir = image.catalog_dir();
|
let catalog_dir = image.catalog_dir();
|
||||||
let publisher_dir = catalog_dir.join("test");
|
let publisher_dir = catalog_dir.join("test");
|
||||||
println!("Publisher dir: {:?}", publisher_dir);
|
println!("Publisher dir: {:?}", publisher_dir);
|
||||||
fs::create_dir_all(&publisher_dir).unwrap();
|
fs::create_dir_all(&publisher_dir).unwrap();
|
||||||
|
|
||||||
// Create a simple catalog.attrs file
|
// Create a simple catalog.attrs file
|
||||||
let attrs_content = r#"{
|
let attrs_content = r#"{
|
||||||
"created": "2025-08-04T23:01:00Z",
|
"created": "2025-08-04T23:01:00Z",
|
||||||
|
|
@ -59,10 +61,13 @@ 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();
|
||||||
|
|
||||||
// Create a simple base catalog part
|
// Create a simple base catalog part
|
||||||
let base_content = r#"{
|
let base_content = r#"{
|
||||||
"test": {
|
"test": {
|
||||||
|
|
@ -88,68 +93,80 @@ 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...");
|
||||||
match image.build_catalog() {
|
match image.build_catalog() {
|
||||||
Ok(_) => println!("Catalog built successfully"),
|
Ok(_) => println!("Catalog built successfully"),
|
||||||
Err(e) => println!("Failed to build catalog: {:?}", e),
|
Err(e) => println!("Failed to build catalog: {:?}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query the catalog
|
// Query the catalog
|
||||||
println!("Querying catalog...");
|
println!("Querying catalog...");
|
||||||
let packages = match image.query_catalog(None) {
|
let packages = match image.query_catalog(None) {
|
||||||
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify that both non-obsolete and obsolete packages are in the results
|
// Verify that both non-obsolete and obsolete packages are in the results
|
||||||
assert_eq!(packages.len(), 2);
|
assert_eq!(packages.len(), 2);
|
||||||
|
|
||||||
// Verify that one package is marked as obsolete
|
// Verify that one package is marked as obsolete
|
||||||
let obsolete_packages: Vec<_> = packages.iter().filter(|p| p.obsolete).collect();
|
let obsolete_packages: Vec<_> = packages.iter().filter(|p| p.obsolete).collect();
|
||||||
assert_eq!(obsolete_packages.len(), 1);
|
assert_eq!(obsolete_packages.len(), 1);
|
||||||
assert_eq!(obsolete_packages[0].fmri.stem(), "example/obsolete");
|
assert_eq!(obsolete_packages[0].fmri.stem(), "example/obsolete");
|
||||||
|
|
||||||
// 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();
|
||||||
assert_eq!(non_obsolete_packages.len(), 1);
|
assert_eq!(non_obsolete_packages.len(), 1);
|
||||||
assert_eq!(non_obsolete_packages[0].fmri.stem(), "example/package");
|
assert_eq!(non_obsolete_packages[0].fmri.stem(), "example/package");
|
||||||
|
|
||||||
// Get the manifest for the non-obsolete package
|
// Get the manifest for the non-obsolete package
|
||||||
let fmri = &non_obsolete_packages[0].fmri;
|
let fmri = &non_obsolete_packages[0].fmri;
|
||||||
let manifest = image.get_manifest_from_catalog(fmri).unwrap();
|
let manifest = image.get_manifest_from_catalog(fmri).unwrap();
|
||||||
assert!(manifest.is_some());
|
assert!(manifest.is_some());
|
||||||
|
|
||||||
// Get the manifest for the obsolete package
|
// Get the manifest for the obsolete package
|
||||||
let fmri = &obsolete_packages[0].fmri;
|
let fmri = &obsolete_packages[0].fmri;
|
||||||
let manifest = image.get_manifest_from_catalog(fmri).unwrap();
|
let manifest = image.get_manifest_from_catalog(fmri).unwrap();
|
||||||
assert!(manifest.is_some());
|
assert!(manifest.is_some());
|
||||||
|
|
||||||
// Verify that the obsolete package's manifest has the obsolete attribute
|
// Verify that the obsolete package's manifest has the obsolete attribute
|
||||||
let manifest = manifest.unwrap();
|
let manifest = manifest.unwrap();
|
||||||
let is_obsolete = manifest.attributes.iter().any(|attr| {
|
let is_obsolete = manifest.attributes.iter().any(|attr| {
|
||||||
attr.key == "pkg.obsolete" && attr.values.get(0).map_or(false, |v| v == "true")
|
attr.key == "pkg.obsolete" && attr.values.get(0).map_or(false, |v| v == "true")
|
||||||
});
|
});
|
||||||
assert!(is_obsolete);
|
assert!(is_obsolete);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
temp_dir.close().unwrap();
|
temp_dir.close().unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -159,45 +176,61 @@ fn test_refresh_catalogs_directory_clearing() {
|
||||||
// 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 image_path = temp_dir.path().join("image");
|
let image_path = temp_dir.path().join("image");
|
||||||
|
|
||||||
// Create the image
|
// Create the image
|
||||||
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();
|
||||||
let publisher1_dir = catalog_dir.join("test1");
|
let publisher1_dir = catalog_dir.join("test1");
|
||||||
let publisher2_dir = catalog_dir.join("test2");
|
let publisher2_dir = catalog_dir.join("test2");
|
||||||
fs::create_dir_all(&publisher1_dir).unwrap();
|
fs::create_dir_all(&publisher1_dir).unwrap();
|
||||||
fs::create_dir_all(&publisher2_dir).unwrap();
|
fs::create_dir_all(&publisher2_dir).unwrap();
|
||||||
|
|
||||||
// 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());
|
||||||
|
|
||||||
// Directly test the directory clearing functionality for a specific publisher
|
// Directly test the directory clearing functionality for a specific publisher
|
||||||
// This simulates the behavior of refresh_catalogs with full=true for a specific publisher
|
// This simulates the behavior of refresh_catalogs with full=true for a specific publisher
|
||||||
if publisher1_dir.exists() {
|
if publisher1_dir.exists() {
|
||||||
fs::remove_dir_all(&publisher1_dir).unwrap();
|
fs::remove_dir_all(&publisher1_dir).unwrap();
|
||||||
}
|
}
|
||||||
fs::create_dir_all(&publisher1_dir).unwrap();
|
fs::create_dir_all(&publisher1_dir).unwrap();
|
||||||
|
|
||||||
// Verify that the marker file for publisher1 was removed
|
// Verify that the marker file for publisher1 was removed
|
||||||
assert!(!marker_file1.exists());
|
assert!(!marker_file1.exists());
|
||||||
// Verify that the marker file for publisher2 still exists
|
// Verify that the marker file for publisher2 still exists
|
||||||
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
|
||||||
// This simulates the behavior of refresh_catalogs with full=true for all publishers
|
// This simulates the behavior of refresh_catalogs with full=true for all publishers
|
||||||
for publisher in &image.publishers {
|
for publisher in &image.publishers {
|
||||||
|
|
@ -207,11 +240,11 @@ fn test_refresh_catalogs_directory_clearing() {
|
||||||
}
|
}
|
||||||
fs::create_dir_all(&publisher_dir).unwrap();
|
fs::create_dir_all(&publisher_dir).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that both marker files were removed
|
// Verify that both marker files were removed
|
||||||
assert!(!marker_file1.exists());
|
assert!(!marker_file1.exists());
|
||||||
assert!(!marker_file2.exists());
|
assert!(!marker_file2.exists());
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
temp_dir.close().unwrap();
|
temp_dir.close().unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -597,14 +596,14 @@ impl Transaction {
|
||||||
if config_path.exists() {
|
if config_path.exists() {
|
||||||
let config_content = fs::read_to_string(&config_path)?;
|
let config_content = fs::read_to_string(&config_path)?;
|
||||||
let config: RepositoryConfig = serde_json::from_str(&config_content)?;
|
let config: RepositoryConfig = serde_json::from_str(&config_content)?;
|
||||||
|
|
||||||
// Check if this publisher was just added in this transaction
|
// Check if this publisher was just added in this transaction
|
||||||
let publisher_dir = self.repo.join("publisher").join(&publisher);
|
let publisher_dir = self.repo.join("publisher").join(&publisher);
|
||||||
let pub_p5i_path = publisher_dir.join("pub.p5i");
|
let pub_p5i_path = publisher_dir.join("pub.p5i");
|
||||||
|
|
||||||
if !pub_p5i_path.exists() {
|
if !pub_p5i_path.exists() {
|
||||||
debug!("Creating pub.p5i file for publisher: {}", publisher);
|
debug!("Creating pub.p5i file for publisher: {}", publisher);
|
||||||
|
|
||||||
// Create the pub.p5i file
|
// Create the pub.p5i file
|
||||||
let repo = FileBackend {
|
let repo = FileBackend {
|
||||||
path: self.repo.clone(),
|
path: self.repo.clone(),
|
||||||
|
|
@ -612,7 +611,7 @@ impl Transaction {
|
||||||
catalog_manager: None,
|
catalog_manager: None,
|
||||||
obsoleted_manager: None,
|
obsoleted_manager: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
repo.create_pub_p5i_file(&publisher)?;
|
repo.create_pub_p5i_file(&publisher)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -1520,10 +1577,10 @@ impl WritableRepository for FileBackend {
|
||||||
let config_path = self.path.join(REPOSITORY_CONFIG_FILENAME);
|
let config_path = self.path.join(REPOSITORY_CONFIG_FILENAME);
|
||||||
let config_data = serde_json::to_string_pretty(&self.config)?;
|
let config_data = serde_json::to_string_pretty(&self.config)?;
|
||||||
fs::write(config_path, config_data)?;
|
fs::write(config_path, config_data)?;
|
||||||
|
|
||||||
// Save the legacy INI format for backward compatibility
|
// Save the legacy INI format for backward compatibility
|
||||||
self.save_legacy_config()?;
|
self.save_legacy_config()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
@ -1865,32 +1941,31 @@ impl FileBackend {
|
||||||
pub fn save_legacy_config(&self) -> Result<()> {
|
pub fn save_legacy_config(&self) -> Result<()> {
|
||||||
let legacy_config_path = self.path.join("pkg5.repository");
|
let legacy_config_path = self.path.join("pkg5.repository");
|
||||||
let mut conf = Ini::new();
|
let mut conf = Ini::new();
|
||||||
|
|
||||||
// Add publisher section with default publisher
|
// Add publisher section with default publisher
|
||||||
if let Some(default_publisher) = &self.config.default_publisher {
|
if let Some(default_publisher) = &self.config.default_publisher {
|
||||||
conf.with_section(Some("publisher"))
|
conf.with_section(Some("publisher"))
|
||||||
.set("prefix", default_publisher);
|
.set("prefix", default_publisher);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add repository section with version and default values
|
// Add repository section with version and default values
|
||||||
conf.with_section(Some("repository"))
|
conf.with_section(Some("repository"))
|
||||||
.set("version", "4")
|
.set("version", "4")
|
||||||
.set("trust-anchor-directory", "/etc/certs/CA/")
|
.set("trust-anchor-directory", "/etc/certs/CA/")
|
||||||
.set("signature-required-names", "[]")
|
.set("signature-required-names", "[]")
|
||||||
.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)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a pub.p5i file for a publisher for backward compatibility
|
/// Create a pub.p5i file for a publisher for backward compatibility
|
||||||
///
|
///
|
||||||
/// Format: base_path/publisher/publisher_name/pub.p5i
|
/// Format: base_path/publisher/publisher_name/pub.p5i
|
||||||
fn create_pub_p5i_file(&self, publisher: &str) -> Result<()> {
|
fn create_pub_p5i_file(&self, publisher: &str) -> Result<()> {
|
||||||
// Define the structure for the pub.p5i file
|
// Define the structure for the pub.p5i file
|
||||||
|
|
@ -1937,17 +2012,14 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method to construct a manifest path consistently
|
/// Helper method to construct a manifest path consistently
|
||||||
///
|
///
|
||||||
/// Format: base_path/publisher/publisher_name/pkg/stem/encoded_version
|
/// Format: base_path/publisher/publisher_name/pkg/stem/encoded_version
|
||||||
pub fn construct_manifest_path(
|
pub fn construct_manifest_path(
|
||||||
base_path: &Path,
|
base_path: &Path,
|
||||||
|
|
@ -1959,27 +2031,24 @@ impl FileBackend {
|
||||||
let encoded_version = Self::url_encode(version);
|
let encoded_version = Self::url_encode(version);
|
||||||
pkg_dir.join(encoded_version)
|
pkg_dir.join(encoded_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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,15 +2057,12 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method to construct a file path consistently with publisher
|
/// Helper method to construct a file path consistently with publisher
|
||||||
///
|
///
|
||||||
/// Format: base_path/publisher/publisher_name/file/XX/hash
|
/// Format: base_path/publisher/publisher_name/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_with_publisher(
|
pub fn construct_file_path_with_publisher(
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -2111,20 +2184,22 @@ impl FileBackend {
|
||||||
} else {
|
} else {
|
||||||
parsed_fmri.clone()
|
parsed_fmri.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -2186,7 +2261,7 @@ impl FileBackend {
|
||||||
opts: crate::repository::BatchOptions,
|
opts: crate::repository::BatchOptions,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("Rebuilding catalog (batched) for publisher: {}", publisher);
|
info!("Rebuilding catalog (batched) for publisher: {}", publisher);
|
||||||
|
|
||||||
// Create the catalog directory for the publisher if it doesn't exist
|
// Create the catalog directory for the publisher if it doesn't exist
|
||||||
let catalog_dir = Self::construct_catalog_path(&self.path, publisher);
|
let catalog_dir = Self::construct_catalog_path(&self.path, publisher);
|
||||||
debug!("Publisher catalog directory: {}", catalog_dir.display());
|
debug!("Publisher catalog directory: {}", catalog_dir.display());
|
||||||
|
|
@ -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,29 +2581,46 @@ 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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the file path for a given hash using the new directory structure with publisher
|
/// Generate the file path for a given hash using the new directory structure with publisher
|
||||||
/// This is a wrapper around the construct_file_path_with_publisher helper method
|
/// This is a wrapper around the construct_file_path_with_publisher helper method
|
||||||
fn generate_file_path_with_publisher(&self, publisher: &str, hash: &str) -> PathBuf {
|
fn generate_file_path_with_publisher(&self, publisher: &str, hash: &str) -> PathBuf {
|
||||||
|
|
@ -2528,7 +2631,7 @@ impl FileBackend {
|
||||||
///
|
///
|
||||||
/// This method returns a mutable reference to the catalog manager.
|
/// This method returns a mutable reference to the catalog manager.
|
||||||
/// It uses interior mutability with RefCell to allow mutation through an immutable reference.
|
/// It uses interior mutability with RefCell to allow mutation through an immutable reference.
|
||||||
///
|
///
|
||||||
/// The catalog manager is specific to the given publisher.
|
/// The catalog manager is specific to the given publisher.
|
||||||
pub fn get_catalog_manager(
|
pub fn get_catalog_manager(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -2544,7 +2648,7 @@ impl FileBackend {
|
||||||
// This is safe because we just checked that catalog_manager is Some
|
// This is safe because we just checked that catalog_manager is Some
|
||||||
Ok(self.catalog_manager.as_ref().unwrap().borrow_mut())
|
Ok(self.catalog_manager.as_ref().unwrap().borrow_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get or initialize the obsoleted package manager
|
/// Get or initialize the obsoleted package manager
|
||||||
///
|
///
|
||||||
/// This method returns a mutable reference to the obsoleted package manager.
|
/// This method returns a mutable reference to the obsoleted package manager.
|
||||||
|
|
@ -2597,7 +2701,7 @@ impl FileBackend {
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
{
|
{
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
// Try to read the first few bytes of the file to check if it's a manifest file
|
// Try to read the first few bytes of the file to check if it's a manifest file
|
||||||
let mut file = match fs::File::open(&path) {
|
let mut file = match fs::File::open(&path) {
|
||||||
|
|
@ -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,8 +2847,11 @@ 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
|
|
@ -63,13 +63,13 @@ pub trait ProgressReporter {
|
||||||
pub struct ProgressInfo {
|
pub struct ProgressInfo {
|
||||||
/// The name of the operation being performed
|
/// The name of the operation being performed
|
||||||
pub operation: String,
|
pub operation: String,
|
||||||
|
|
||||||
/// The current progress value (e.g., bytes downloaded, files processed)
|
/// The current progress value (e.g., bytes downloaded, files processed)
|
||||||
pub current: Option<u64>,
|
pub current: Option<u64>,
|
||||||
|
|
||||||
/// The total expected value (e.g., total bytes, total files)
|
/// The total expected value (e.g., total bytes, total files)
|
||||||
pub total: Option<u64>,
|
pub total: Option<u64>,
|
||||||
|
|
||||||
/// Additional context about the operation (e.g., current file name)
|
/// Additional context about the operation (e.g., current file name)
|
||||||
pub context: Option<String>,
|
pub context: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -139,18 +139,18 @@ impl ProgressInfo {
|
||||||
impl fmt::Display for ProgressInfo {
|
impl fmt::Display for ProgressInfo {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", self.operation)?;
|
write!(f, "{}", self.operation)?;
|
||||||
|
|
||||||
if let (Some(current), Some(total)) = (self.current, self.total) {
|
if let (Some(current), Some(total)) = (self.current, self.total) {
|
||||||
let percentage = (current as f64 / total as f64) * 100.0;
|
let percentage = (current as f64 / total as f64) * 100.0;
|
||||||
write!(f, " {:.1}% ({}/{})", percentage, current, total)?;
|
write!(f, " {:.1}% ({}/{})", percentage, current, total)?;
|
||||||
} else if let Some(current) = self.current {
|
} else if let Some(current) = self.current {
|
||||||
write!(f, " {}", current)?;
|
write!(f, " {}", current)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(context) = &self.context {
|
if let Some(context) = &self.context {
|
||||||
write!(f, " - {}", context)?;
|
write!(f, " - {}", context)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,4 +165,4 @@ impl ProgressReporter for NoopProgressReporter {
|
||||||
fn start(&self, _info: &ProgressInfo) {}
|
fn start(&self, _info: &ProgressInfo) {}
|
||||||
fn update(&self, _info: &ProgressInfo) {}
|
fn update(&self, _info: &ProgressInfo) {}
|
||||||
fn finish(&self, _info: &ProgressInfo) {}
|
fn finish(&self, _info: &ProgressInfo) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
|
@ -125,27 +125,33 @@ impl WritableRepository for RestBackend {
|
||||||
println!("Creating publisher directory...");
|
println!("Creating publisher directory...");
|
||||||
let publisher_dir = cache_path.join("publisher").join(publisher);
|
let publisher_dir = cache_path.join("publisher").join(publisher);
|
||||||
println!("Publisher directory path: {}", publisher_dir.display());
|
println!("Publisher directory path: {}", publisher_dir.display());
|
||||||
|
|
||||||
match fs::create_dir_all(&publisher_dir) {
|
match fs::create_dir_all(&publisher_dir) {
|
||||||
Ok(_) => println!("Successfully created publisher directory"),
|
Ok(_) => println!("Successfully created publisher directory"),
|
||||||
Err(e) => println!("Failed to create publisher directory: {}", e),
|
Err(e) => println!("Failed to create publisher directory: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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");
|
||||||
println!("Catalog directory path: {}", catalog_dir.display());
|
println!("Catalog directory path: {}", catalog_dir.display());
|
||||||
|
|
||||||
match fs::create_dir_all(&catalog_dir) {
|
match fs::create_dir_all(&catalog_dir) {
|
||||||
Ok(_) => println!("Successfully created catalog directory"),
|
Ok(_) => println!("Successfully created catalog directory"),
|
||||||
Err(e) => println!("Failed to create catalog directory: {}", e),
|
Err(e) => println!("Failed to create catalog directory: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
println!("No local cache path set, skipping directory creation");
|
println!("No local cache path set, skipping directory creation");
|
||||||
|
|
@ -256,10 +262,12 @@ impl WritableRepository for RestBackend {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
catalog_managers: HashMap::new(),
|
catalog_managers: HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -316,18 +324,18 @@ impl ReadableRepository for RestBackend {
|
||||||
/// Open an existing repository
|
/// Open an existing repository
|
||||||
fn open<P: AsRef<Path>>(uri: P) -> Result<Self> {
|
fn open<P: AsRef<Path>>(uri: P) -> Result<Self> {
|
||||||
let uri_str = uri.as_ref().to_string_lossy().to_string();
|
let uri_str = uri.as_ref().to_string_lossy().to_string();
|
||||||
|
|
||||||
// Create an HTTP client
|
// Create an HTTP client
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
// Fetch the repository configuration from the remote server
|
// Fetch the repository configuration from the remote server
|
||||||
// We'll try to get the publisher information using the publisher endpoint
|
// We'll try to get the publisher information using the publisher endpoint
|
||||||
let url = format!("{}/publisher/0", uri_str);
|
let url = format!("{}/publisher/0", uri_str);
|
||||||
|
|
||||||
debug!("Fetching repository configuration from: {}", url);
|
debug!("Fetching repository configuration from: {}", url);
|
||||||
|
|
||||||
let mut config = RepositoryConfig::default();
|
let mut config = RepositoryConfig::default();
|
||||||
|
|
||||||
// Try to fetch publisher information
|
// Try to fetch publisher information
|
||||||
match client.get(&url).send() {
|
match client.get(&url).send() {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
|
|
@ -336,31 +344,36 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we couldn't get any publishers, add a default one
|
// If we couldn't get any publishers, add a default one
|
||||||
if config.publishers.is_empty() {
|
if config.publishers.is_empty() {
|
||||||
config.publishers.push("openindiana.org".to_string());
|
config.publishers.push("openindiana.org".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the repository instance
|
// Create the repository instance
|
||||||
Ok(RestBackend {
|
Ok(RestBackend {
|
||||||
uri: uri_str,
|
uri: uri_str,
|
||||||
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
|
@ -698,15 +735,15 @@ impl RestBackend {
|
||||||
/// Returns an error if the directory could not be created.
|
/// Returns an error if the directory could not be created.
|
||||||
pub fn set_local_cache_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
|
pub fn set_local_cache_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
|
||||||
self.local_cache_path = Some(path.as_ref().to_path_buf());
|
self.local_cache_path = Some(path.as_ref().to_path_buf());
|
||||||
|
|
||||||
// Create the directory if it doesn't exist
|
// Create the directory if it doesn't exist
|
||||||
if let Some(path) = &self.local_cache_path {
|
if let Some(path) = &self.local_cache_path {
|
||||||
fs::create_dir_all(path)?;
|
fs::create_dir_all(path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes the repository by downloading catalog files for all publishers.
|
/// Initializes the repository by downloading catalog files for all publishers.
|
||||||
///
|
///
|
||||||
/// This method should be called after setting the local cache path with
|
/// This method should be called after setting the local cache path with
|
||||||
|
|
@ -729,21 +766,27 @@ 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
|
||||||
self.download_all_catalogs(progress)?;
|
self.download_all_catalogs(progress)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the catalog manager for a publisher
|
/// Get the catalog manager for a publisher
|
||||||
fn get_catalog_manager(&mut self, publisher: &str) -> Result<&mut CatalogManager> {
|
fn get_catalog_manager(&mut self, publisher: &str) -> Result<&mut CatalogManager> {
|
||||||
// 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,12 +796,13 @@ 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())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads a catalog file from the remote server.
|
/// Downloads a catalog file from the remote server.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|
@ -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,13 +905,10 @@ 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
|
|
||||||
),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download and store a catalog file
|
/// Download and store a catalog file
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|
@ -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());
|
||||||
|
|
||||||
|
|
@ -935,7 +990,7 @@ impl RestBackend {
|
||||||
|
|
||||||
Ok(file_path)
|
Ok(file_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads all catalog files for a specific publisher.
|
/// Downloads all catalog files for a specific publisher.
|
||||||
///
|
///
|
||||||
/// This method downloads the catalog.attrs file first to determine what catalog parts
|
/// This method downloads the catalog.attrs file first to determine what catalog parts
|
||||||
|
|
@ -967,73 +1022,77 @@ impl RestBackend {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Use a no-op reporter if none was provided
|
// Use a no-op reporter if none was provided
|
||||||
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)
|
})?;
|
||||||
.map_err(|e| {
|
|
||||||
progress_reporter.finish(&overall_progress);
|
let attrs: Value = serde_json::from_str(&attrs_content).map_err(|e| {
|
||||||
RepositoryError::JsonParseError(format!("Failed to parse catalog.attrs: {}", e))
|
progress_reporter.finish(&overall_progress);
|
||||||
})?;
|
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(|| {
|
||||||
progress_reporter.finish(&overall_progress);
|
progress_reporter.finish(&overall_progress);
|
||||||
RepositoryError::JsonParseError("Missing 'parts' field in catalog.attrs".to_string())
|
RepositoryError::JsonParseError("Missing 'parts' field in catalog.attrs".to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Update progress with total number of parts
|
// Update progress with total number of parts
|
||||||
let total_parts = parts.len() as u64 + 1; // +1 for catalog.attrs
|
let total_parts = parts.len() as u64 + 1; // +1 for catalog.attrs
|
||||||
overall_progress = overall_progress.with_total(total_parts).with_current(1);
|
overall_progress = overall_progress.with_total(total_parts).with_current(1);
|
||||||
progress_reporter.update(&overall_progress);
|
progress_reporter.update(&overall_progress);
|
||||||
|
|
||||||
// Download each part
|
// Download each part
|
||||||
for (i, part_name) in parts.keys().enumerate() {
|
for (i, part_name) in parts.keys().enumerate() {
|
||||||
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);
|
||||||
|
|
||||||
self.download_and_store_catalog_file(publisher, part_name, progress)?;
|
self.download_and_store_catalog_file(publisher, part_name, progress)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the catalog manager for this publisher
|
// Get the catalog manager for this publisher
|
||||||
let catalog_manager = self.get_catalog_manager(publisher)?;
|
let catalog_manager = self.get_catalog_manager(publisher)?;
|
||||||
|
|
||||||
// Update progress for loading parts
|
// Update progress for loading parts
|
||||||
overall_progress = overall_progress.with_context("Loading catalog parts".to_string());
|
overall_progress = overall_progress.with_context("Loading catalog parts".to_string());
|
||||||
progress_reporter.update(&overall_progress);
|
progress_reporter.update(&overall_progress);
|
||||||
|
|
||||||
// Load the catalog parts
|
// Load the catalog parts
|
||||||
for part_name in parts.keys() {
|
for part_name in parts.keys() {
|
||||||
catalog_manager.load_part(part_name)?;
|
catalog_manager.load_part(part_name)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report completion
|
// Report completion
|
||||||
overall_progress = overall_progress.with_current(total_parts);
|
overall_progress = overall_progress.with_current(total_parts);
|
||||||
progress_reporter.finish(&overall_progress);
|
progress_reporter.finish(&overall_progress);
|
||||||
|
|
||||||
info!("Downloaded catalog for publisher: {}", publisher);
|
info!("Downloaded catalog for publisher: {}", publisher);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download catalogs for all publishers
|
/// Download catalogs for all publishers
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|
@ -1046,19 +1105,19 @@ impl RestBackend {
|
||||||
pub fn download_all_catalogs(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> {
|
pub fn download_all_catalogs(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> {
|
||||||
// Use a no-op reporter if none was provided
|
// Use a no-op reporter if none was provided
|
||||||
let progress_reporter = progress.unwrap_or(&NoopProgressReporter);
|
let progress_reporter = progress.unwrap_or(&NoopProgressReporter);
|
||||||
|
|
||||||
// Clone the publishers list to avoid borrowing issues
|
// Clone the publishers list to avoid borrowing issues
|
||||||
let publishers = self.config.publishers.clone();
|
let publishers = self.config.publishers.clone();
|
||||||
let total_publishers = publishers.len() as u64;
|
let total_publishers = publishers.len() as u64;
|
||||||
|
|
||||||
// Create progress info for the overall operation
|
// Create progress info for the overall operation
|
||||||
let mut overall_progress = ProgressInfo::new("Downloading all catalogs")
|
let mut overall_progress = ProgressInfo::new("Downloading all catalogs")
|
||||||
.with_total(total_publishers)
|
.with_total(total_publishers)
|
||||||
.with_current(0);
|
.with_current(0);
|
||||||
|
|
||||||
// Notify that we're starting the download
|
// Notify that we're starting the download
|
||||||
progress_reporter.start(&overall_progress);
|
progress_reporter.start(&overall_progress);
|
||||||
|
|
||||||
// Download catalogs for each publisher
|
// Download catalogs for each publisher
|
||||||
for (i, publisher) in publishers.iter().enumerate() {
|
for (i, publisher) in publishers.iter().enumerate() {
|
||||||
// Update progress with current publisher
|
// Update progress with current publisher
|
||||||
|
|
@ -1066,21 +1125,21 @@ impl RestBackend {
|
||||||
.with_current(i as u64)
|
.with_current(i as u64)
|
||||||
.with_context(format!("Publisher: {}", publisher));
|
.with_context(format!("Publisher: {}", publisher));
|
||||||
progress_reporter.update(&overall_progress);
|
progress_reporter.update(&overall_progress);
|
||||||
|
|
||||||
// Download catalog for this publisher
|
// Download catalog for this publisher
|
||||||
self.download_catalog(publisher, progress)?;
|
self.download_catalog(publisher, progress)?;
|
||||||
|
|
||||||
// Update progress after completing this publisher
|
// Update progress after completing this publisher
|
||||||
overall_progress = overall_progress.with_current(i as u64 + 1);
|
overall_progress = overall_progress.with_current(i as u64 + 1);
|
||||||
progress_reporter.update(&overall_progress);
|
progress_reporter.update(&overall_progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report completion
|
// Report completion
|
||||||
progress_reporter.finish(&overall_progress);
|
progress_reporter.finish(&overall_progress);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh the catalog for a publisher
|
/// Refresh the catalog for a publisher
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -208,15 +208,21 @@ mod tests {
|
||||||
assert!(repo.config.publishers.contains(&"example.com".to_string()));
|
assert!(repo.config.publishers.contains(&"example.com".to_string()));
|
||||||
assert!(FileBackend::construct_catalog_path(&repo_path, "example.com").exists());
|
assert!(FileBackend::construct_catalog_path(&repo_path, "example.com").exists());
|
||||||
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();
|
||||||
let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap();
|
let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap();
|
||||||
|
|
||||||
// Check the structure of the pub.p5i file
|
// Check the structure of the pub.p5i file
|
||||||
assert_eq!(pub_p5i_json["version"], 1);
|
assert_eq!(pub_p5i_json["version"], 1);
|
||||||
assert!(pub_p5i_json["packages"].is_array());
|
assert!(pub_p5i_json["packages"].is_array());
|
||||||
|
|
@ -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!(
|
||||||
|
|
@ -448,7 +465,7 @@ mod tests {
|
||||||
// Clean up
|
// Clean up
|
||||||
cleanup_test_dir(&test_dir);
|
cleanup_test_dir(&test_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_transaction_pub_p5i_creation() {
|
fn test_transaction_pub_p5i_creation() {
|
||||||
// Run the setup script to prepare the test environment
|
// Run the setup script to prepare the test environment
|
||||||
|
|
@ -463,39 +480,42 @@ mod tests {
|
||||||
|
|
||||||
// Create a new publisher through a transaction
|
// Create a new publisher through a transaction
|
||||||
let publisher = "transaction_test";
|
let publisher = "transaction_test";
|
||||||
|
|
||||||
// Start a transaction
|
// Start a transaction
|
||||||
let mut transaction = repo.begin_transaction().unwrap();
|
let mut transaction = repo.begin_transaction().unwrap();
|
||||||
|
|
||||||
// Set the publisher for the transaction
|
// Set the publisher for the transaction
|
||||||
transaction.set_publisher(publisher);
|
transaction.set_publisher(publisher);
|
||||||
|
|
||||||
// Add a simple manifest to the transaction
|
// Add a simple manifest to the transaction
|
||||||
let manifest_path = manifest_dir.join("example.p5m");
|
let manifest_path = manifest_dir.join("example.p5m");
|
||||||
let manifest = Manifest::parse_file(&manifest_path).unwrap();
|
let manifest = Manifest::parse_file(&manifest_path).unwrap();
|
||||||
transaction.update_manifest(manifest);
|
transaction.update_manifest(manifest);
|
||||||
|
|
||||||
// Commit the transaction
|
// Commit the transaction
|
||||||
transaction.commit().unwrap();
|
transaction.commit().unwrap();
|
||||||
|
|
||||||
// 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();
|
||||||
let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap();
|
let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap();
|
||||||
|
|
||||||
// Check the structure of the pub.p5i file
|
// Check the structure of the pub.p5i file
|
||||||
assert_eq!(pub_p5i_json["version"], 1);
|
assert_eq!(pub_p5i_json["version"], 1);
|
||||||
assert!(pub_p5i_json["packages"].is_array());
|
assert!(pub_p5i_json["packages"].is_array());
|
||||||
assert!(pub_p5i_json["publishers"].is_array());
|
assert!(pub_p5i_json["publishers"].is_array());
|
||||||
assert_eq!(pub_p5i_json["publishers"][0]["name"], publisher);
|
assert_eq!(pub_p5i_json["publishers"][0]["name"], publisher);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
cleanup_test_dir(&test_dir);
|
cleanup_test_dir(&test_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_legacy_pkg5_repository_creation() {
|
fn test_legacy_pkg5_repository_creation() {
|
||||||
// Create a test directory
|
// Create a test directory
|
||||||
|
|
@ -508,20 +528,23 @@ mod tests {
|
||||||
// Add a publisher
|
// Add a publisher
|
||||||
let publisher = "openindiana.org";
|
let publisher = "openindiana.org";
|
||||||
repo.add_publisher(publisher).unwrap();
|
repo.add_publisher(publisher).unwrap();
|
||||||
|
|
||||||
// Set as default publisher
|
// Set as default publisher
|
||||||
repo.set_default_publisher(publisher).unwrap();
|
repo.set_default_publisher(publisher).unwrap();
|
||||||
|
|
||||||
// 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();
|
||||||
|
|
||||||
// Print the content for debugging
|
// Print the content for debugging
|
||||||
println!("pkg5.repository content:\n{}", pkg5_content);
|
println!("pkg5.repository content:\n{}", pkg5_content);
|
||||||
|
|
||||||
// Check that the file contains the expected sections and values
|
// Check that the file contains the expected sections and values
|
||||||
assert!(pkg5_content.contains("[publisher]"));
|
assert!(pkg5_content.contains("[publisher]"));
|
||||||
assert!(pkg5_content.contains("prefix=openindiana.org"));
|
assert!(pkg5_content.contains("prefix=openindiana.org"));
|
||||||
|
|
@ -531,55 +554,58 @@ mod tests {
|
||||||
assert!(pkg5_content.contains("signature-required-names=[]"));
|
assert!(pkg5_content.contains("signature-required-names=[]"));
|
||||||
assert!(pkg5_content.contains("check-certificate-revocation=False"));
|
assert!(pkg5_content.contains("check-certificate-revocation=False"));
|
||||||
assert!(pkg5_content.contains("[CONFIGURATION]"));
|
assert!(pkg5_content.contains("[CONFIGURATION]"));
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
cleanup_test_dir(&test_dir);
|
cleanup_test_dir(&test_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rest_repository_local_functionality() {
|
fn test_rest_repository_local_functionality() {
|
||||||
use crate::repository::RestBackend;
|
use crate::repository::RestBackend;
|
||||||
|
|
||||||
// Create a test directory
|
// Create a test directory
|
||||||
let test_dir = create_test_dir("rest_repository");
|
let test_dir = create_test_dir("rest_repository");
|
||||||
let cache_path = test_dir.join("cache");
|
let cache_path = test_dir.join("cache");
|
||||||
|
|
||||||
println!("Test directory: {}", test_dir.display());
|
println!("Test directory: {}", test_dir.display());
|
||||||
println!("Cache path: {}", cache_path.display());
|
println!("Cache path: {}", cache_path.display());
|
||||||
|
|
||||||
// Create a REST repository
|
// Create a REST repository
|
||||||
let uri = "http://pkg.opensolaris.org/release";
|
let uri = "http://pkg.opensolaris.org/release";
|
||||||
let mut repo = RestBackend::open(uri).unwrap();
|
let mut repo = RestBackend::open(uri).unwrap();
|
||||||
|
|
||||||
// Set the local cache path
|
// Set the local cache path
|
||||||
repo.set_local_cache_path(&cache_path).unwrap();
|
repo.set_local_cache_path(&cache_path).unwrap();
|
||||||
|
|
||||||
println!("Local cache path set to: {:?}", repo.local_cache_path);
|
println!("Local cache path set to: {:?}", repo.local_cache_path);
|
||||||
|
|
||||||
// Add a publisher
|
// Add a publisher
|
||||||
let publisher = "openindiana.org";
|
let publisher = "openindiana.org";
|
||||||
repo.add_publisher(publisher).unwrap();
|
repo.add_publisher(publisher).unwrap();
|
||||||
|
|
||||||
println!("Publisher added: {}", publisher);
|
println!("Publisher added: {}", publisher);
|
||||||
println!("Publishers in config: {:?}", repo.config.publishers);
|
println!("Publishers in config: {:?}", repo.config.publishers);
|
||||||
|
|
||||||
// Verify that the directory structure was created correctly
|
// Verify that the directory structure was created correctly
|
||||||
let publisher_dir = cache_path.join("publisher").join(publisher);
|
let publisher_dir = cache_path.join("publisher").join(publisher);
|
||||||
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());
|
||||||
println!("Catalog directory exists: {}", catalog_dir.exists());
|
println!("Catalog directory exists: {}", catalog_dir.exists());
|
||||||
|
|
||||||
assert!(catalog_dir.exists(), "Catalog directory should be created");
|
assert!(catalog_dir.exists(), "Catalog directory should be created");
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
cleanup_test_dir(&test_dir);
|
cleanup_test_dir(&test_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A test progress reporter that records all progress events
|
/// A test progress reporter that records all progress events
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct TestProgressReporter {
|
struct TestProgressReporter {
|
||||||
|
|
@ -590,7 +616,7 @@ mod tests {
|
||||||
/// Records of all finish events
|
/// Records of all finish events
|
||||||
finish_events: Arc<Mutex<Vec<ProgressInfo>>>,
|
finish_events: Arc<Mutex<Vec<ProgressInfo>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestProgressReporter {
|
impl TestProgressReporter {
|
||||||
/// Create a new test progress reporter
|
/// Create a new test progress reporter
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
|
@ -600,116 +626,116 @@ mod tests {
|
||||||
finish_events: Arc::new(Mutex::new(Vec::new())),
|
finish_events: Arc::new(Mutex::new(Vec::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the number of start events recorded
|
/// Get the number of start events recorded
|
||||||
fn start_count(&self) -> usize {
|
fn start_count(&self) -> usize {
|
||||||
self.start_events.lock().unwrap().len()
|
self.start_events.lock().unwrap().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the number of update events recorded
|
/// Get the number of update events recorded
|
||||||
fn update_count(&self) -> usize {
|
fn update_count(&self) -> usize {
|
||||||
self.update_events.lock().unwrap().len()
|
self.update_events.lock().unwrap().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the number of finish events recorded
|
/// Get the number of finish events recorded
|
||||||
fn finish_count(&self) -> usize {
|
fn finish_count(&self) -> usize {
|
||||||
self.finish_events.lock().unwrap().len()
|
self.finish_events.lock().unwrap().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a clone of all start events
|
/// Get a clone of all start events
|
||||||
fn get_start_events(&self) -> Vec<ProgressInfo> {
|
fn get_start_events(&self) -> Vec<ProgressInfo> {
|
||||||
self.start_events.lock().unwrap().clone()
|
self.start_events.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a clone of all update events
|
/// Get a clone of all update events
|
||||||
fn get_update_events(&self) -> Vec<ProgressInfo> {
|
fn get_update_events(&self) -> Vec<ProgressInfo> {
|
||||||
self.update_events.lock().unwrap().clone()
|
self.update_events.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a clone of all finish events
|
/// Get a clone of all finish events
|
||||||
fn get_finish_events(&self) -> Vec<ProgressInfo> {
|
fn get_finish_events(&self) -> Vec<ProgressInfo> {
|
||||||
self.finish_events.lock().unwrap().clone()
|
self.finish_events.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProgressReporter for TestProgressReporter {
|
impl ProgressReporter for TestProgressReporter {
|
||||||
fn start(&self, info: &ProgressInfo) {
|
fn start(&self, info: &ProgressInfo) {
|
||||||
let mut events = self.start_events.lock().unwrap();
|
let mut events = self.start_events.lock().unwrap();
|
||||||
events.push(info.clone());
|
events.push(info.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&self, info: &ProgressInfo) {
|
fn update(&self, info: &ProgressInfo) {
|
||||||
let mut events = self.update_events.lock().unwrap();
|
let mut events = self.update_events.lock().unwrap();
|
||||||
events.push(info.clone());
|
events.push(info.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&self, info: &ProgressInfo) {
|
fn finish(&self, info: &ProgressInfo) {
|
||||||
let mut events = self.finish_events.lock().unwrap();
|
let mut events = self.finish_events.lock().unwrap();
|
||||||
events.push(info.clone());
|
events.push(info.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_progress_reporter() {
|
fn test_progress_reporter() {
|
||||||
// Create a test progress reporter
|
// Create a test progress reporter
|
||||||
let reporter = TestProgressReporter::new();
|
let reporter = TestProgressReporter::new();
|
||||||
|
|
||||||
// Create some progress info
|
// Create some progress info
|
||||||
let info1 = ProgressInfo::new("Test operation 1");
|
let info1 = ProgressInfo::new("Test operation 1");
|
||||||
let info2 = ProgressInfo::new("Test operation 2")
|
let info2 = ProgressInfo::new("Test operation 2")
|
||||||
.with_current(50)
|
.with_current(50)
|
||||||
.with_total(100);
|
.with_total(100);
|
||||||
|
|
||||||
// Report some progress
|
// Report some progress
|
||||||
reporter.start(&info1);
|
reporter.start(&info1);
|
||||||
reporter.update(&info2);
|
reporter.update(&info2);
|
||||||
reporter.finish(&info1);
|
reporter.finish(&info1);
|
||||||
|
|
||||||
// Check that the events were recorded
|
// Check that the events were recorded
|
||||||
assert_eq!(reporter.start_count(), 1);
|
assert_eq!(reporter.start_count(), 1);
|
||||||
assert_eq!(reporter.update_count(), 1);
|
assert_eq!(reporter.update_count(), 1);
|
||||||
assert_eq!(reporter.finish_count(), 1);
|
assert_eq!(reporter.finish_count(), 1);
|
||||||
|
|
||||||
// Check the content of the events
|
// Check the content of the events
|
||||||
let start_events = reporter.get_start_events();
|
let start_events = reporter.get_start_events();
|
||||||
let update_events = reporter.get_update_events();
|
let update_events = reporter.get_update_events();
|
||||||
let finish_events = reporter.get_finish_events();
|
let finish_events = reporter.get_finish_events();
|
||||||
|
|
||||||
assert_eq!(start_events[0].operation, "Test operation 1");
|
assert_eq!(start_events[0].operation, "Test operation 1");
|
||||||
assert_eq!(update_events[0].operation, "Test operation 2");
|
assert_eq!(update_events[0].operation, "Test operation 2");
|
||||||
assert_eq!(update_events[0].current, Some(50));
|
assert_eq!(update_events[0].current, Some(50));
|
||||||
assert_eq!(update_events[0].total, Some(100));
|
assert_eq!(update_events[0].total, Some(100));
|
||||||
assert_eq!(finish_events[0].operation, "Test operation 1");
|
assert_eq!(finish_events[0].operation, "Test operation 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rest_backend_with_progress() {
|
fn test_rest_backend_with_progress() {
|
||||||
// This test is a mock test that doesn't actually connect to a remote server
|
// This test is a mock test that doesn't actually connect to a remote server
|
||||||
// It just verifies that the progress reporting mechanism works correctly
|
// It just verifies that the progress reporting mechanism works correctly
|
||||||
|
|
||||||
// Create a test directory
|
// Create a test directory
|
||||||
let test_dir = create_test_dir("rest_progress");
|
let test_dir = create_test_dir("rest_progress");
|
||||||
let cache_path = test_dir.join("cache");
|
let cache_path = test_dir.join("cache");
|
||||||
|
|
||||||
// Create a REST repository
|
// Create a REST repository
|
||||||
let uri = "http://pkg.opensolaris.org/release";
|
let uri = "http://pkg.opensolaris.org/release";
|
||||||
let mut repo = RestBackend::create(uri, RepositoryVersion::V4).unwrap();
|
let mut repo = RestBackend::create(uri, RepositoryVersion::V4).unwrap();
|
||||||
|
|
||||||
// Set the local cache path
|
// Set the local cache path
|
||||||
repo.set_local_cache_path(&cache_path).unwrap();
|
repo.set_local_cache_path(&cache_path).unwrap();
|
||||||
|
|
||||||
// Create a test progress reporter
|
// Create a test progress reporter
|
||||||
let reporter = TestProgressReporter::new();
|
let reporter = TestProgressReporter::new();
|
||||||
|
|
||||||
// Add a publisher
|
// Add a publisher
|
||||||
let publisher = "test";
|
let publisher = "test";
|
||||||
repo.add_publisher(publisher).unwrap();
|
repo.add_publisher(publisher).unwrap();
|
||||||
|
|
||||||
// Create a mock catalog.attrs file
|
// Create a mock catalog.attrs file
|
||||||
let publisher_dir = cache_path.join("publisher").join(publisher);
|
let publisher_dir = cache_path.join("publisher").join(publisher);
|
||||||
let catalog_dir = publisher_dir.join("catalog");
|
let catalog_dir = publisher_dir.join("catalog");
|
||||||
fs::create_dir_all(&catalog_dir).unwrap();
|
fs::create_dir_all(&catalog_dir).unwrap();
|
||||||
|
|
||||||
let attrs_content = r#"{
|
let attrs_content = r#"{
|
||||||
"created": "20250803T124900Z",
|
"created": "20250803T124900Z",
|
||||||
"last-modified": "20250803T124900Z",
|
"last-modified": "20250803T124900Z",
|
||||||
|
|
@ -728,35 +754,39 @@ mod tests {
|
||||||
},
|
},
|
||||||
"version": 1
|
"version": 1
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
let attrs_path = catalog_dir.join("catalog.attrs");
|
let attrs_path = catalog_dir.join("catalog.attrs");
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock the download_catalog_file method to avoid actual HTTP requests
|
// Mock the download_catalog_file method to avoid actual HTTP requests
|
||||||
// This is done by creating the files before calling download_catalog
|
// This is done by creating the files before calling download_catalog
|
||||||
|
|
||||||
// Create a simple progress update to ensure update events are recorded
|
// Create a simple progress update to ensure update events are recorded
|
||||||
let progress_info = ProgressInfo::new("Test update")
|
let progress_info = ProgressInfo::new("Test update")
|
||||||
.with_current(1)
|
.with_current(1)
|
||||||
.with_total(2);
|
.with_total(2);
|
||||||
reporter.update(&progress_info);
|
reporter.update(&progress_info);
|
||||||
|
|
||||||
// Call download_catalog with the progress reporter
|
// Call download_catalog with the progress reporter
|
||||||
// This will fail because we're not actually connecting to a server,
|
// This will fail because we're not actually connecting to a server,
|
||||||
// but we can still verify that the progress reporter was called
|
// but we can still verify that the progress reporter was called
|
||||||
let _ = repo.download_catalog(publisher, Some(&reporter));
|
let _ = repo.download_catalog(publisher, Some(&reporter));
|
||||||
|
|
||||||
// Check that the progress reporter was called
|
// Check that the progress reporter was called
|
||||||
assert!(reporter.start_count() > 0, "No start events recorded");
|
assert!(reporter.start_count() > 0, "No start events recorded");
|
||||||
assert!(reporter.update_count() > 0, "No update events recorded");
|
assert!(reporter.update_count() > 0, "No update events recorded");
|
||||||
assert!(reporter.finish_count() > 0, "No finish events recorded");
|
assert!(reporter.finish_count() > 0, "No finish events recorded");
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
cleanup_test_dir(&test_dir);
|
cleanup_test_dir(&test_dir);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -23,7 +23,7 @@ mod tests {
|
||||||
// Instead of using JSON, let's create a string format manifest
|
// Instead of using JSON, let's create a string format manifest
|
||||||
// that the parser can handle
|
// that the parser can handle
|
||||||
let manifest_string = "set name=pkg.fmri value=pkg://test/example@1.0.0\n";
|
let manifest_string = "set name=pkg.fmri value=pkg://test/example@1.0.0\n";
|
||||||
|
|
||||||
// Write the string to a file
|
// Write the string to a file
|
||||||
let mut file = File::create(&manifest_path).unwrap();
|
let mut file = File::create(&manifest_path).unwrap();
|
||||||
file.write_all(manifest_string.as_bytes()).unwrap();
|
file.write_all(manifest_string.as_bytes()).unwrap();
|
||||||
|
|
@ -68,10 +68,10 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_new_json_format() {
|
fn test_parse_new_json_format() {
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
@ -129,22 +129,25 @@ mod tests {
|
||||||
|
|
||||||
// Verify that the parsed manifest has the expected attributes
|
// Verify that the parsed manifest has the expected attributes
|
||||||
assert_eq!(parsed_manifest.attributes.len(), 3);
|
assert_eq!(parsed_manifest.attributes.len(), 3);
|
||||||
|
|
||||||
// Check first attribute
|
// Check first attribute
|
||||||
assert_eq!(parsed_manifest.attributes[0].key, "pkg.fmri");
|
assert_eq!(parsed_manifest.attributes[0].key, "pkg.fmri");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed_manifest.attributes[0].values[0],
|
parsed_manifest.attributes[0].values[0],
|
||||||
"pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3,5.11-2014.0.1.1:20250628T100651Z"
|
"pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3,5.11-2014.0.1.1:20250628T100651Z"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check second attribute
|
// Check second attribute
|
||||||
assert_eq!(parsed_manifest.attributes[1].key, "pkg.obsolete");
|
assert_eq!(parsed_manifest.attributes[1].key, "pkg.obsolete");
|
||||||
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
|
||||||
for attr in &parsed_manifest.attributes {
|
for attr in &parsed_manifest.attributes {
|
||||||
assert!(attr.properties.is_empty());
|
assert!(attr.properties.is_empty());
|
||||||
|
|
|
||||||
|
|
@ -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}")]
|
||||||
|
|
@ -84,4 +78,4 @@ impl From<&str> for Pkg6Error {
|
||||||
fn from(s: &str) -> Self {
|
fn from(s: &str) -> Self {
|
||||||
Pkg6Error::Other(s.to_string())
|
Pkg6Error::Other(s.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
568
pkg6/src/main.rs
568
pkg6/src/main.rs
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,7 @@ pub struct Cli {
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_daemon: bool,
|
pub no_daemon: bool,
|
||||||
|
|
||||||
#[arg(long, value_name = "FILE")]
|
#[arg(long, value_name = "FILE")]
|
||||||
pub pid_file: Option<PathBuf>,
|
pub pid_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -83,10 +83,11 @@ pub struct Oauth2Config {
|
||||||
impl Config {
|
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 {
|
||||||
|
|
@ -22,7 +22,7 @@ pub enum DepotError {
|
||||||
#[error("Server error: {0}")]
|
#[error("Server error: {0}")]
|
||||||
#[diagnostic(code(ips::depot_error::server))]
|
#[diagnostic(code(ips::depot_error::server))]
|
||||||
Server(String),
|
Server(String),
|
||||||
|
|
||||||
#[error("Repository error: {0}")]
|
#[error("Repository error: {0}")]
|
||||||
#[diagnostic(code(ips::depot_error::repo))]
|
#[diagnostic(code(ips::depot_error::repo))]
|
||||||
Repo(#[from] libips::repository::RepositoryError),
|
Repo(#[from] libips::repository::RepositoryError),
|
||||||
|
|
@ -31,11 +31,15 @@ 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()),
|
||||||
};
|
};
|
||||||
|
|
||||||
(status, message).into_response()
|
(status, message).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>>,
|
||||||
|
|
@ -18,16 +18,19 @@ pub async fn get_catalog_v1(
|
||||||
|
|
||||||
let service = ServeFile::new(path);
|
let service = ServeFile::new(path);
|
||||||
let result = service.oneshot(req).await;
|
let result = service.oneshot(req).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(mut res) => {
|
Ok(mut res) => {
|
||||||
// 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,52 +1,71 @@
|
||||||
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;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
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,35 +1,38 @@
|
||||||
|
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();
|
||||||
out.push_str(&format!("Name: {}\n", fmri.name));
|
out.push_str(&format!("Name: {}\n", fmri.name));
|
||||||
|
|
||||||
if let Some(summary) = find_attr(&manifest, "pkg.summary") {
|
if let Some(summary) = find_attr(&manifest, "pkg.summary") {
|
||||||
out.push_str(&format!("Summary: {}\n", summary));
|
out.push_str(&format!("Summary: {}\n", summary));
|
||||||
}
|
}
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
@ -83,13 +100,15 @@ pub async fn get_info(
|
||||||
} else {
|
} else {
|
||||||
out.push_str(&format!("FMRI: pkg://{}/{}@{}\n", publisher, name, version));
|
out.push_str(&format!("FMRI: pkg://{}/{}@{}\n", publisher, name, version));
|
||||||
}
|
}
|
||||||
|
|
||||||
// License
|
// License
|
||||||
// Print actual license text content from repository instead of hash.
|
// Print actual license text content from repository instead of hash.
|
||||||
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,32 +1,34 @@
|
||||||
|
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)
|
||||||
let mut hasher = sha1::Sha1::new();
|
let mut hasher = sha1::Sha1::new();
|
||||||
hasher.update(content.as_bytes());
|
hasher.update(content.as_bytes());
|
||||||
let etag = format!("\"{}\"", format!("{:x}", hasher.finalize()));
|
let etag = format!("\"{}\"", format!("{:x}", hasher.finalize()));
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
[
|
[
|
||||||
(header::CONTENT_TYPE, "text/plain"),
|
(header::CONTENT_TYPE, "text/plain"),
|
||||||
(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 {
|
||||||
|
|
@ -42,11 +42,14 @@ async fn get_publisher_impl(
|
||||||
Path(publisher): Path<String>,
|
Path(publisher): Path<String>,
|
||||||
) -> 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,14 +56,32 @@ 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],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
response.to_string()
|
response.to_string()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,25 +1,25 @@
|
||||||
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();
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
// For M1, let's just create a dummy default if not found/failed for testing purposes
|
// For M1, let's just create a dummy default if not found/failed for testing purposes
|
||||||
// In a real scenario we'd want to be more specific about errors.
|
// In a real scenario we'd want to be more specific about errors.
|
||||||
|
|
||||||
let config = Config::load(args.config.clone()).unwrap_or_else(|e| {
|
let config = Config::load(args.config.clone()).unwrap_or_else(|e| {
|
||||||
eprintln!("Failed to load config: {}. Using default.", e);
|
eprintln!("Failed to load config: {}. Using default.", e);
|
||||||
Config {
|
Config {
|
||||||
|
|
@ -45,7 +45,7 @@ pub async fn run() -> Result<()> {
|
||||||
|
|
||||||
// Init telemetry
|
// Init telemetry
|
||||||
telemetry::init(&config);
|
telemetry::init(&config);
|
||||||
|
|
||||||
// Init repo
|
// Init repo
|
||||||
let repo = DepotRepo::new(&config).map_err(|e| miette::miette!(e))?;
|
let repo = DepotRepo::new(&config).map_err(|e| miette::miette!(e))?;
|
||||||
let state = Arc::new(repo);
|
let state = Arc::new(repo);
|
||||||
|
|
@ -55,15 +55,26 @@ pub async fn run() -> Result<()> {
|
||||||
if !args.no_daemon {
|
if !args.no_daemon {
|
||||||
daemon::daemonize().map_err(|e| miette::miette!(e))?;
|
daemon::daemonize().map_err(|e| miette::miette!(e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
}
|
||||||
if cand_global.exists() { return Some(cand_global); }
|
|
||||||
|
let cand_global = FileBackend::construct_file_path(&self.root, hash);
|
||||||
None
|
if cand_global.exists() {
|
||||||
|
return Some(cand_global);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
@ -10,6 +10,6 @@ pub fn init(_config: &Config) {
|
||||||
.with(tracing_subscriber::fmt::layer());
|
.with(tracing_subscriber::fmt::layer());
|
||||||
|
|
||||||
// TODO: Add OTLP layer if configured in _config
|
// TODO: Add OTLP layer if configured in _config
|
||||||
|
|
||||||
registry.init();
|
registry.init();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -15,44 +15,44 @@ fn setup_repo(dir: &TempDir) -> PathBuf {
|
||||||
let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap();
|
let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap();
|
||||||
let publisher = "test";
|
let publisher = "test";
|
||||||
backend.add_publisher(publisher).unwrap();
|
backend.add_publisher(publisher).unwrap();
|
||||||
|
|
||||||
// Create a transaction to publish a package
|
// Create a transaction to publish a package
|
||||||
let mut tx = backend.begin_transaction().unwrap();
|
let mut tx = backend.begin_transaction().unwrap();
|
||||||
tx.set_publisher(publisher);
|
tx.set_publisher(publisher);
|
||||||
|
|
||||||
// Create content
|
// Create content
|
||||||
let content_dir = dir.path().join("content");
|
let content_dir = dir.path().join("content");
|
||||||
fs::create_dir_all(&content_dir).unwrap();
|
fs::create_dir_all(&content_dir).unwrap();
|
||||||
let file_path = content_dir.join("hello.txt");
|
let file_path = content_dir.join("hello.txt");
|
||||||
fs::write(&file_path, "Hello IPS").unwrap();
|
fs::write(&file_path, "Hello IPS").unwrap();
|
||||||
|
|
||||||
// Add file
|
// Add file
|
||||||
let mut fa = FileAction::read_from_path(&file_path).unwrap();
|
let mut fa = FileAction::read_from_path(&file_path).unwrap();
|
||||||
fa.path = "hello.txt".to_string(); // relative path in package
|
fa.path = "hello.txt".to_string(); // relative path in package
|
||||||
tx.add_file(fa, &file_path).unwrap();
|
tx.add_file(fa, &file_path).unwrap();
|
||||||
|
|
||||||
// Update manifest
|
// Update manifest
|
||||||
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 {
|
manifest.attributes.push(Attr {
|
||||||
key: "pkg.fmri".to_string(),
|
key: "pkg.fmri".to_string(),
|
||||||
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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.update_manifest(manifest);
|
tx.update_manifest(manifest);
|
||||||
tx.commit().unwrap();
|
tx.commit().unwrap();
|
||||||
|
|
||||||
backend.rebuild(Some(publisher), false, false).unwrap();
|
backend.rebuild(Some(publisher), false, false).unwrap();
|
||||||
|
|
||||||
repo_path
|
repo_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ async fn test_depot_server() {
|
||||||
// Setup
|
// Setup
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let repo_path = setup_repo(&temp_dir);
|
let repo_path = setup_repo(&temp_dir);
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
server: ServerConfig {
|
server: ServerConfig {
|
||||||
bind: vec!["127.0.0.1:0".to_string()],
|
bind: vec!["127.0.0.1:0".to_string()],
|
||||||
|
|
@ -81,24 +81,28 @@ async fn test_depot_server() {
|
||||||
admin: None,
|
admin: None,
|
||||||
oauth2: 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);
|
||||||
let router = http::routes::app_router(state);
|
let router = http::routes::app_router(state);
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
// Spawn server
|
// Spawn server
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
http::server::run(router, listener).await.unwrap();
|
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);
|
||||||
|
|
||||||
// 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"));
|
||||||
|
|
@ -106,12 +110,12 @@ async fn test_depot_server() {
|
||||||
assert!(text.contains("manifest 0 1"));
|
assert!(text.contains("manifest 0 1"));
|
||||||
|
|
||||||
// 2. Test Catalog
|
// 2. Test Catalog
|
||||||
|
|
||||||
// Test Catalog v1
|
// Test Catalog v1
|
||||||
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();
|
||||||
|
|
@ -128,7 +132,7 @@ async fn test_depot_server() {
|
||||||
let manifest_text = resp.text().await.unwrap();
|
let manifest_text = resp.text().await.unwrap();
|
||||||
assert!(manifest_text.contains("pkg.fmri"));
|
assert!(manifest_text.contains("pkg.fmri"));
|
||||||
assert!(manifest_text.contains("example@1.0.0"));
|
assert!(manifest_text.contains("example@1.0.0"));
|
||||||
|
|
||||||
// v1
|
// v1
|
||||||
let manifest_v1_url = format!("{}/test/manifest/1/{}", base_url, fmri_arg);
|
let manifest_v1_url = format!("{}/test/manifest/1/{}", base_url, fmri_arg);
|
||||||
let resp = client.get(&manifest_v1_url).send().await.unwrap();
|
let resp = client.get(&manifest_v1_url).send().await.unwrap();
|
||||||
|
|
@ -144,25 +148,36 @@ 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");
|
||||||
|
|
||||||
// 6. Test File
|
// 6. Test File
|
||||||
// We assume file exists if manifest works.
|
// We assume file exists if manifest works.
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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);
|
||||||
|
|
@ -388,7 +400,7 @@ mod e2e_tests {
|
||||||
// Clean up
|
// Clean up
|
||||||
cleanup_test_dir(&test_dir);
|
cleanup_test_dir(&test_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_e2e_obsoleted_packages() {
|
fn test_e2e_obsoleted_packages() {
|
||||||
// Run the setup script to prepare the test environment
|
// Run the setup script to prepare the test environment
|
||||||
|
|
@ -438,38 +450,47 @@ mod e2e_tests {
|
||||||
"Failed to list packages: {:?}",
|
"Failed to list packages: {:?}",
|
||||||
result.err()
|
result.err()
|
||||||
);
|
);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
// Add version if available
|
// Add version if available
|
||||||
let fmri = if !release.is_empty() {
|
let fmri = if !release.is_empty() {
|
||||||
format!("{}@{}", fmri, release)
|
format!("{}@{}", fmri, release)
|
||||||
} else {
|
} else {
|
||||||
fmri
|
fmri
|
||||||
};
|
};
|
||||||
|
|
||||||
// Print the FMRI and repo path for debugging
|
// Print the FMRI and repo path for debugging
|
||||||
println!("FMRI: {}", fmri);
|
println!("FMRI: {}", fmri);
|
||||||
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());
|
||||||
|
|
||||||
// List files in the package directory if it exists
|
// List files in the package directory if it exists
|
||||||
if pkg_dir.exists() {
|
if pkg_dir.exists() {
|
||||||
println!("Files in package directory:");
|
println!("Files in package directory:");
|
||||||
|
|
@ -478,26 +499,31 @@ mod e2e_tests {
|
||||||
println!(" {}", entry.path().display());
|
println!(" {}", entry.path().display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
println!("Result: {:?}", result);
|
println!("Result: {:?}", result);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
result.is_ok(),
|
result.is_ok(),
|
||||||
"Failed to mark package as obsoleted: {:?}",
|
"Failed to mark package as obsoleted: {:?}",
|
||||||
result.err()
|
result.err()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify the package is no longer in the main repository
|
// Verify the package is no longer in the main repository
|
||||||
let result = run_pkg6repo(&["list", "-s", repo_path.to_str().unwrap()]);
|
let result = run_pkg6repo(&["list", "-s", repo_path.to_str().unwrap()]);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -505,40 +531,49 @@ mod e2e_tests {
|
||||||
"Failed to list packages: {:?}",
|
"Failed to list packages: {:?}",
|
||||||
result.err()
|
result.err()
|
||||||
);
|
);
|
||||||
|
|
||||||
let output = result.unwrap();
|
let output = result.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
!output.contains("example"),
|
!output.contains("example"),
|
||||||
"Package still found in repository after being marked as obsoleted"
|
"Package still found in repository after being marked as obsoleted"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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: {:?}",
|
||||||
result.err()
|
result.err()
|
||||||
);
|
);
|
||||||
|
|
||||||
let output = result.unwrap();
|
let output = result.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("example"),
|
output.contains("example"),
|
||||||
"Obsoleted package not found in obsoleted packages list"
|
"Obsoleted package not found in obsoleted packages list"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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(),
|
||||||
"Failed to show obsoleted package details: {:?}",
|
"Failed to show obsoleted package details: {:?}",
|
||||||
result.err()
|
result.err()
|
||||||
);
|
);
|
||||||
|
|
||||||
let output = result.unwrap();
|
let output = result.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("Status: obsolete"),
|
output.contains("Status: obsolete"),
|
||||||
|
|
@ -552,8 +587,8 @@ mod e2e_tests {
|
||||||
output.contains("pkg://test/example2@1.0"),
|
output.contains("pkg://test/example2@1.0"),
|
||||||
"Replacement package not found in details"
|
"Replacement package not found in details"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
cleanup_test_dir(&test_dir);
|
cleanup_test_dir(&test_dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -334,170 +334,170 @@ enum Commands {
|
||||||
#[clap(short = 'p', long)]
|
#[clap(short = 'p', long)]
|
||||||
publisher: Option<String>,
|
publisher: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Mark a package as obsoleted
|
/// Mark a package as obsoleted
|
||||||
ObsoletePackage {
|
ObsoletePackage {
|
||||||
/// Path or URI of the repository
|
/// Path or URI of the repository
|
||||||
#[clap(short = 's')]
|
#[clap(short = 's')]
|
||||||
repo_uri_or_path: String,
|
repo_uri_or_path: String,
|
||||||
|
|
||||||
/// Publisher of the package
|
/// Publisher of the package
|
||||||
#[clap(short = 'p')]
|
#[clap(short = 'p')]
|
||||||
publisher: String,
|
publisher: String,
|
||||||
|
|
||||||
/// FMRI of the package to mark as obsoleted
|
/// FMRI of the package to mark as obsoleted
|
||||||
#[clap(short = 'f')]
|
#[clap(short = 'f')]
|
||||||
fmri: String,
|
fmri: String,
|
||||||
|
|
||||||
/// Optional deprecation message explaining why the package is obsoleted
|
/// Optional deprecation message explaining why the package is obsoleted
|
||||||
#[clap(short = 'm', long = "message")]
|
#[clap(short = 'm', long = "message")]
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
|
|
||||||
/// Optional list of packages that replace this obsoleted package
|
/// Optional list of packages that replace this obsoleted package
|
||||||
#[clap(short = 'r', long = "replaced-by")]
|
#[clap(short = 'r', long = "replaced-by")]
|
||||||
replaced_by: Option<Vec<String>>,
|
replaced_by: Option<Vec<String>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// List obsoleted packages in a repository
|
/// List obsoleted packages in a repository
|
||||||
ListObsoleted {
|
ListObsoleted {
|
||||||
/// Path or URI of the repository
|
/// Path or URI of the repository
|
||||||
#[clap(short = 's')]
|
#[clap(short = 's')]
|
||||||
repo_uri_or_path: String,
|
repo_uri_or_path: String,
|
||||||
|
|
||||||
/// Output format
|
/// Output format
|
||||||
#[clap(short = 'F')]
|
#[clap(short = 'F')]
|
||||||
format: Option<String>,
|
format: Option<String>,
|
||||||
|
|
||||||
/// Omit headers
|
/// Omit headers
|
||||||
#[clap(short = 'H')]
|
#[clap(short = 'H')]
|
||||||
omit_headers: bool,
|
omit_headers: bool,
|
||||||
|
|
||||||
/// Publisher to list obsoleted packages for
|
/// Publisher to list obsoleted packages for
|
||||||
#[clap(short = 'p')]
|
#[clap(short = 'p')]
|
||||||
publisher: String,
|
publisher: String,
|
||||||
|
|
||||||
/// Page number (1-based, defaults to 1)
|
/// Page number (1-based, defaults to 1)
|
||||||
#[clap(long = "page")]
|
#[clap(long = "page")]
|
||||||
page: Option<usize>,
|
page: Option<usize>,
|
||||||
|
|
||||||
/// Number of packages per page (defaults to 100, 0 for all)
|
/// Number of packages per page (defaults to 100, 0 for all)
|
||||||
#[clap(long = "page-size")]
|
#[clap(long = "page-size")]
|
||||||
page_size: Option<usize>,
|
page_size: Option<usize>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Show details of an obsoleted package
|
/// Show details of an obsoleted package
|
||||||
ShowObsoleted {
|
ShowObsoleted {
|
||||||
/// Path or URI of the repository
|
/// Path or URI of the repository
|
||||||
#[clap(short = 's')]
|
#[clap(short = 's')]
|
||||||
repo_uri_or_path: String,
|
repo_uri_or_path: String,
|
||||||
|
|
||||||
/// Output format
|
/// Output format
|
||||||
#[clap(short = 'F')]
|
#[clap(short = 'F')]
|
||||||
format: Option<String>,
|
format: Option<String>,
|
||||||
|
|
||||||
/// Publisher of the package
|
/// Publisher of the package
|
||||||
#[clap(short = 'p')]
|
#[clap(short = 'p')]
|
||||||
publisher: String,
|
publisher: String,
|
||||||
|
|
||||||
/// FMRI of the obsoleted package to show
|
/// FMRI of the obsoleted package to show
|
||||||
#[clap(short = 'f')]
|
#[clap(short = 'f')]
|
||||||
fmri: String,
|
fmri: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Search for obsoleted packages
|
/// Search for obsoleted packages
|
||||||
SearchObsoleted {
|
SearchObsoleted {
|
||||||
/// Path or URI of the repository
|
/// Path or URI of the repository
|
||||||
#[clap(short = 's')]
|
#[clap(short = 's')]
|
||||||
repo_uri_or_path: String,
|
repo_uri_or_path: String,
|
||||||
|
|
||||||
/// Output format
|
/// Output format
|
||||||
#[clap(short = 'F')]
|
#[clap(short = 'F')]
|
||||||
format: Option<String>,
|
format: Option<String>,
|
||||||
|
|
||||||
/// Omit headers
|
/// Omit headers
|
||||||
#[clap(short = 'H')]
|
#[clap(short = 'H')]
|
||||||
omit_headers: bool,
|
omit_headers: bool,
|
||||||
|
|
||||||
/// Publisher to search obsoleted packages for
|
/// Publisher to search obsoleted packages for
|
||||||
#[clap(short = 'p')]
|
#[clap(short = 'p')]
|
||||||
publisher: String,
|
publisher: String,
|
||||||
|
|
||||||
/// Search pattern (supports glob patterns)
|
/// Search pattern (supports glob patterns)
|
||||||
#[clap(short = 'q')]
|
#[clap(short = 'q')]
|
||||||
pattern: String,
|
pattern: String,
|
||||||
|
|
||||||
/// Maximum number of results to return
|
/// Maximum number of results to return
|
||||||
#[clap(short = 'n', long = "limit")]
|
#[clap(short = 'n', long = "limit")]
|
||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Restore an obsoleted package to the main repository
|
/// Restore an obsoleted package to the main repository
|
||||||
RestoreObsoleted {
|
RestoreObsoleted {
|
||||||
/// Path or URI of the repository
|
/// Path or URI of the repository
|
||||||
#[clap(short = 's')]
|
#[clap(short = 's')]
|
||||||
repo_uri_or_path: String,
|
repo_uri_or_path: String,
|
||||||
|
|
||||||
/// Publisher of the package
|
/// Publisher of the package
|
||||||
#[clap(short = 'p')]
|
#[clap(short = 'p')]
|
||||||
publisher: String,
|
publisher: String,
|
||||||
|
|
||||||
/// FMRI of the obsoleted package to restore
|
/// FMRI of the obsoleted package to restore
|
||||||
#[clap(short = 'f')]
|
#[clap(short = 'f')]
|
||||||
fmri: String,
|
fmri: String,
|
||||||
|
|
||||||
/// Skip rebuilding the catalog after restoration
|
/// Skip rebuilding the catalog after restoration
|
||||||
#[clap(long = "no-rebuild")]
|
#[clap(long = "no-rebuild")]
|
||||||
no_rebuild: bool,
|
no_rebuild: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Export obsoleted packages to a file
|
/// Export obsoleted packages to a file
|
||||||
ExportObsoleted {
|
ExportObsoleted {
|
||||||
/// Path or URI of the repository
|
/// Path or URI of the repository
|
||||||
#[clap(short = 's')]
|
#[clap(short = 's')]
|
||||||
repo_uri_or_path: String,
|
repo_uri_or_path: String,
|
||||||
|
|
||||||
/// Publisher to export obsoleted packages for
|
/// Publisher to export obsoleted packages for
|
||||||
#[clap(short = 'p')]
|
#[clap(short = 'p')]
|
||||||
publisher: String,
|
publisher: String,
|
||||||
|
|
||||||
/// Output file path
|
/// Output file path
|
||||||
#[clap(short = 'o')]
|
#[clap(short = 'o')]
|
||||||
output_file: String,
|
output_file: String,
|
||||||
|
|
||||||
/// Optional search pattern to filter packages
|
/// Optional search pattern to filter packages
|
||||||
#[clap(short = 'q')]
|
#[clap(short = 'q')]
|
||||||
pattern: Option<String>,
|
pattern: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Import obsoleted packages from a file
|
/// Import obsoleted packages from a file
|
||||||
ImportObsoleted {
|
ImportObsoleted {
|
||||||
/// Path or URI of the repository
|
/// Path or URI of the repository
|
||||||
#[clap(short = 's')]
|
#[clap(short = 's')]
|
||||||
repo_uri_or_path: String,
|
repo_uri_or_path: String,
|
||||||
|
|
||||||
/// Input file path
|
/// Input file path
|
||||||
#[clap(short = 'i')]
|
#[clap(short = 'i')]
|
||||||
input_file: String,
|
input_file: String,
|
||||||
|
|
||||||
/// Override publisher (use this instead of the one in the export file)
|
/// Override publisher (use this instead of the one in the export file)
|
||||||
#[clap(short = 'p')]
|
#[clap(short = 'p')]
|
||||||
publisher: Option<String>,
|
publisher: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Clean up obsoleted packages older than a specified TTL (time-to-live)
|
/// Clean up obsoleted packages older than a specified TTL (time-to-live)
|
||||||
CleanupObsoleted {
|
CleanupObsoleted {
|
||||||
/// Path or URI of the repository
|
/// Path or URI of the repository
|
||||||
#[clap(short = 's')]
|
#[clap(short = 's')]
|
||||||
repo_uri_or_path: String,
|
repo_uri_or_path: String,
|
||||||
|
|
||||||
/// Publisher to clean up obsoleted packages for
|
/// Publisher to clean up obsoleted packages for
|
||||||
#[clap(short = 'p')]
|
#[clap(short = 'p')]
|
||||||
publisher: String,
|
publisher: String,
|
||||||
|
|
||||||
/// TTL in days
|
/// TTL in days
|
||||||
#[clap(short = 't', long = "ttl-days", default_value = "90")]
|
#[clap(short = 't', long = "ttl-days", default_value = "90")]
|
||||||
ttl_days: u32,
|
ttl_days: u32,
|
||||||
|
|
||||||
/// Perform a dry run (don't actually remove packages)
|
/// Perform a dry run (don't actually remove packages)
|
||||||
#[clap(short = 'n', long = "dry-run")]
|
#[clap(short = 'n', long = "dry-run")]
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
|
|
@ -1273,8 +1273,8 @@ 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,
|
||||||
publisher,
|
publisher,
|
||||||
|
|
@ -1283,41 +1283,41 @@ fn main() -> Result<()> {
|
||||||
replaced_by,
|
replaced_by,
|
||||||
} => {
|
} => {
|
||||||
info!("Marking package as obsoleted: {}", fmri);
|
info!("Marking package as obsoleted: {}", fmri);
|
||||||
|
|
||||||
// Open the repository
|
// Open the repository
|
||||||
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
||||||
|
|
||||||
// Parse the FMRI
|
// Parse the FMRI
|
||||||
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
|
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
|
||||||
|
|
||||||
// Get the manifest for the package using the helper method
|
// Get the manifest for the package using the helper method
|
||||||
let manifest_path = FileBackend::construct_manifest_path(
|
let manifest_path = FileBackend::construct_manifest_path(
|
||||||
&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());
|
||||||
println!("Publisher: {}", publisher);
|
println!("Publisher: {}", publisher);
|
||||||
println!("Stem: {}", parsed_fmri.stem());
|
println!("Stem: {}", parsed_fmri.stem());
|
||||||
println!("Version: {}", parsed_fmri.version());
|
println!("Version: {}", parsed_fmri.version());
|
||||||
|
|
||||||
if !manifest_path.exists() {
|
if !manifest_path.exists() {
|
||||||
return Err(Pkg6RepoError::from(format!(
|
return Err(Pkg6RepoError::from(format!(
|
||||||
"Package not found: {}",
|
"Package not found: {}",
|
||||||
parsed_fmri
|
parsed_fmri
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the manifest content
|
// Read the manifest content
|
||||||
let manifest_content = std::fs::read_to_string(&manifest_path)?;
|
let manifest_content = std::fs::read_to_string(&manifest_path)?;
|
||||||
|
|
||||||
// Create a new scope for the obsoleted_manager to ensure it's dropped before we call repo.rebuild()
|
// Create a new scope for the obsoleted_manager to ensure it's dropped before we call repo.rebuild()
|
||||||
{
|
{
|
||||||
// Get the obsoleted package manager
|
// Get the obsoleted package manager
|
||||||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||||
|
|
||||||
// Store the obsoleted package
|
// Store the obsoleted package
|
||||||
obsoleted_manager.store_obsoleted_package(
|
obsoleted_manager.store_obsoleted_package(
|
||||||
publisher,
|
publisher,
|
||||||
|
|
@ -1327,17 +1327,17 @@ fn main() -> Result<()> {
|
||||||
message.clone(),
|
message.clone(),
|
||||||
)?;
|
)?;
|
||||||
} // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
} // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
||||||
|
|
||||||
// Remove the original package from the repository
|
// Remove the original package from the repository
|
||||||
std::fs::remove_file(&manifest_path)?;
|
std::fs::remove_file(&manifest_path)?;
|
||||||
|
|
||||||
// Rebuild the catalog to reflect the changes
|
// Rebuild the catalog to reflect the changes
|
||||||
repo.rebuild(Some(publisher), false, false)?;
|
repo.rebuild(Some(publisher), false, false)?;
|
||||||
|
|
||||||
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,
|
||||||
format,
|
format,
|
||||||
|
|
@ -1347,39 +1347,43 @@ fn main() -> Result<()> {
|
||||||
page_size,
|
page_size,
|
||||||
} => {
|
} => {
|
||||||
info!("Listing obsoleted packages for publisher: {}", publisher);
|
info!("Listing obsoleted packages for publisher: {}", 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)?;
|
||||||
|
|
||||||
// Get the obsoleted packages in a new scope to avoid borrowing issues
|
// Get the obsoleted packages in a new scope to avoid borrowing issues
|
||||||
let paginated_result = {
|
let paginated_result = {
|
||||||
// Get the obsoleted package manager
|
// Get the obsoleted package manager
|
||||||
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
|
||||||
let output_format = format.as_deref().unwrap_or("table");
|
let output_format = format.as_deref().unwrap_or("table");
|
||||||
|
|
||||||
match output_format {
|
match output_format {
|
||||||
"table" => {
|
"table" => {
|
||||||
// Print headers if not omitted
|
// Print headers if not omitted
|
||||||
if !omit_headers {
|
if !omit_headers {
|
||||||
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
|
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print packages
|
// Print packages
|
||||||
for fmri in &paginated_result.packages {
|
for fmri in &paginated_result.packages {
|
||||||
// Format version and publisher, handling optional fields
|
// Format version and publisher, handling optional fields
|
||||||
let version_str = fmri.version();
|
let version_str = fmri.version();
|
||||||
|
|
||||||
let publisher_str = match &fmri.publisher {
|
let publisher_str = match &fmri.publisher {
|
||||||
Some(publisher) => publisher.clone(),
|
Some(publisher) => publisher.clone(),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{:<30} {:<15} {:<10}",
|
"{:<30} {:<15} {:<10}",
|
||||||
fmri.stem(),
|
fmri.stem(),
|
||||||
|
|
@ -1387,13 +1391,15 @@ fn main() -> Result<()> {
|
||||||
publisher_str
|
publisher_str
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print pagination information
|
// Print pagination information
|
||||||
println!("\nPage {} of {} (Total: {} packages)",
|
println!(
|
||||||
paginated_result.page,
|
"\nPage {} of {} (Total: {} packages)",
|
||||||
paginated_result.total_pages,
|
paginated_result.page,
|
||||||
paginated_result.total_count);
|
paginated_result.total_pages,
|
||||||
},
|
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)]
|
||||||
|
|
@ -1404,8 +1410,12 @@ fn main() -> Result<()> {
|
||||||
total_pages: usize,
|
total_pages: usize,
|
||||||
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,
|
||||||
|
|
@ -1413,53 +1423,50 @@ fn main() -> Result<()> {
|
||||||
total_pages: paginated_result.total_pages,
|
total_pages: paginated_result.total_pages,
|
||||||
total_count: paginated_result.total_count,
|
total_count: paginated_result.total_count,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Serialize to pretty-printed JSON
|
// Serialize to pretty-printed JSON
|
||||||
let json_output = serde_json::to_string_pretty(&paginated_output)
|
let json_output = serde_json::to_string_pretty(&paginated_output)
|
||||||
.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 {
|
||||||
println!("NAME\tVERSION\tPUBLISHER");
|
println!("NAME\tVERSION\tPUBLISHER");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print packages as tab-separated values
|
// Print packages as tab-separated values
|
||||||
for fmri in &paginated_result.packages {
|
for fmri in &paginated_result.packages {
|
||||||
// Format version and publisher, handling optional fields
|
// Format version and publisher, handling optional fields
|
||||||
let version_str = fmri.version();
|
let version_str = fmri.version();
|
||||||
|
|
||||||
let publisher_str = match &fmri.publisher {
|
let publisher_str = match &fmri.publisher {
|
||||||
Some(publisher) => publisher.clone(),
|
Some(publisher) => publisher.clone(),
|
||||||
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!(
|
||||||
paginated_result.page,
|
"\nPAGE\t{}\nTOTAL_PAGES\t{}\nTOTAL_COUNT\t{}",
|
||||||
paginated_result.total_pages,
|
paginated_result.page,
|
||||||
paginated_result.total_count);
|
paginated_result.total_pages,
|
||||||
},
|
paginated_result.total_count
|
||||||
|
);
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
return Err(Pkg6RepoError::UnsupportedOutputFormat(
|
||||||
output_format.to_string(),
|
output_format.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::ShowObsoleted {
|
Commands::ShowObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
format,
|
format,
|
||||||
|
|
@ -1467,18 +1474,18 @@ fn main() -> Result<()> {
|
||||||
fmri,
|
fmri,
|
||||||
} => {
|
} => {
|
||||||
info!("Showing details of obsoleted package: {}", fmri);
|
info!("Showing details of obsoleted package: {}", fmri);
|
||||||
|
|
||||||
// Open the repository
|
// Open the repository
|
||||||
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
||||||
|
|
||||||
// Parse the FMRI
|
// Parse the FMRI
|
||||||
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
|
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
|
||||||
|
|
||||||
// Get the obsoleted package metadata in a new scope to avoid borrowing issues
|
// Get the obsoleted package metadata in a new scope to avoid borrowing issues
|
||||||
let metadata = {
|
let metadata = {
|
||||||
// Get the obsoleted package manager
|
// Get the obsoleted package manager
|
||||||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||||
|
|
||||||
// Get the obsoleted package metadata
|
// Get the obsoleted package metadata
|
||||||
match obsoleted_manager.get_obsoleted_package_metadata(publisher, &parsed_fmri)? {
|
match obsoleted_manager.get_obsoleted_package_metadata(publisher, &parsed_fmri)? {
|
||||||
Some(metadata) => metadata,
|
Some(metadata) => metadata,
|
||||||
|
|
@ -1490,30 +1497,30 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}; // 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
|
||||||
let output_format = format.as_deref().unwrap_or("table");
|
let output_format = format.as_deref().unwrap_or("table");
|
||||||
|
|
||||||
match output_format {
|
match output_format {
|
||||||
"table" => {
|
"table" => {
|
||||||
println!("FMRI: {}", metadata.fmri);
|
println!("FMRI: {}", metadata.fmri);
|
||||||
println!("Status: {}", metadata.status);
|
println!("Status: {}", metadata.status);
|
||||||
println!("Obsolescence Date: {}", metadata.obsolescence_date);
|
println!("Obsolescence Date: {}", metadata.obsolescence_date);
|
||||||
|
|
||||||
if let Some(msg) = &metadata.deprecation_message {
|
if let Some(msg) = &metadata.deprecation_message {
|
||||||
println!("Deprecation Message: {}", msg);
|
println!("Deprecation Message: {}", msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(replacements) = &metadata.obsoleted_by {
|
if let Some(replacements) = &metadata.obsoleted_by {
|
||||||
println!("Replaced By:");
|
println!("Replaced By:");
|
||||||
for replacement in replacements {
|
for replacement in replacements {
|
||||||
println!(" {}", replacement);
|
println!(" {}", replacement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
@ -1525,41 +1532,41 @@ fn main() -> Result<()> {
|
||||||
metadata_version: metadata.metadata_version,
|
metadata_version: metadata.metadata_version,
|
||||||
content_hash: metadata.content_hash,
|
content_hash: metadata.content_hash,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Serialize to pretty-printed JSON
|
// Serialize to pretty-printed JSON
|
||||||
let json_output = serde_json::to_string_pretty(&details_output)
|
let json_output = serde_json::to_string_pretty(&details_output)
|
||||||
.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);
|
||||||
println!("ObsolescenceDate\t{}", metadata.obsolescence_date);
|
println!("ObsolescenceDate\t{}", metadata.obsolescence_date);
|
||||||
|
|
||||||
if let Some(msg) = &metadata.deprecation_message {
|
if let Some(msg) = &metadata.deprecation_message {
|
||||||
println!("DeprecationMessage\t{}", msg);
|
println!("DeprecationMessage\t{}", msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(replacements) = &metadata.obsoleted_by {
|
if let Some(replacements) = &metadata.obsoleted_by {
|
||||||
for (i, replacement) in replacements.iter().enumerate() {
|
for (i, replacement) in replacements.iter().enumerate() {
|
||||||
println!("ReplacedBy{}\t{}", i + 1, replacement);
|
println!("ReplacedBy{}\t{}", i + 1, replacement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::SearchObsoleted {
|
Commands::SearchObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
format,
|
format,
|
||||||
|
|
@ -1568,47 +1575,51 @@ 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)?;
|
||||||
|
|
||||||
// Get the obsoleted packages in a new scope to avoid borrowing issues
|
// Get the obsoleted packages in a new scope to avoid borrowing issues
|
||||||
let obsoleted_packages = {
|
let obsoleted_packages = {
|
||||||
// Get the obsoleted package manager
|
// Get the obsoleted package manager
|
||||||
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 {
|
||||||
packages.truncate(*max_results);
|
packages.truncate(*max_results);
|
||||||
}
|
}
|
||||||
|
|
||||||
packages
|
packages
|
||||||
}; // 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
|
||||||
let output_format = format.as_deref().unwrap_or("table");
|
let output_format = format.as_deref().unwrap_or("table");
|
||||||
|
|
||||||
match output_format {
|
match output_format {
|
||||||
"table" => {
|
"table" => {
|
||||||
// Print headers if not omitted
|
// Print headers if not omitted
|
||||||
if !omit_headers {
|
if !omit_headers {
|
||||||
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
|
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print packages
|
// Print packages
|
||||||
for fmri in obsoleted_packages {
|
for fmri in obsoleted_packages {
|
||||||
// Format version and publisher, handling optional fields
|
// Format version and publisher, handling optional fields
|
||||||
let version_str = fmri.version();
|
let version_str = fmri.version();
|
||||||
|
|
||||||
let publisher_str = match &fmri.publisher {
|
let publisher_str = match &fmri.publisher {
|
||||||
Some(publisher) => publisher.clone(),
|
Some(publisher) => publisher.clone(),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{:<30} {:<15} {:<10}",
|
"{:<30} {:<15} {:<10}",
|
||||||
fmri.stem(),
|
fmri.stem(),
|
||||||
|
|
@ -1616,102 +1627,101 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Serialize to pretty-printed JSON
|
// Serialize to pretty-printed JSON
|
||||||
let json_output = serde_json::to_string_pretty(&packages_output)
|
let json_output = serde_json::to_string_pretty(&packages_output)
|
||||||
.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 {
|
||||||
println!("NAME\tVERSION\tPUBLISHER");
|
println!("NAME\tVERSION\tPUBLISHER");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print packages as tab-separated values
|
// Print packages as tab-separated values
|
||||||
for fmri in obsoleted_packages {
|
for fmri in obsoleted_packages {
|
||||||
// Format version and publisher, handling optional fields
|
// Format version and publisher, handling optional fields
|
||||||
let version_str = fmri.version();
|
let version_str = fmri.version();
|
||||||
|
|
||||||
let publisher_str = match &fmri.publisher {
|
let publisher_str = match &fmri.publisher {
|
||||||
Some(publisher) => publisher.clone(),
|
Some(publisher) => publisher.clone(),
|
||||||
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(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
|
|
||||||
Commands::RestoreObsoleted {
|
Commands::RestoreObsoleted {
|
||||||
repo_uri_or_path,
|
repo_uri_or_path,
|
||||||
publisher,
|
publisher,
|
||||||
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)?;
|
||||||
|
|
||||||
// Open the repository
|
// Open the repository
|
||||||
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
||||||
|
|
||||||
// Get the manifest content and remove the obsoleted package
|
// Get the manifest content and remove the obsoleted package
|
||||||
let manifest_content = {
|
let manifest_content = {
|
||||||
// Get the obsoleted package manager
|
// Get the obsoleted package manager
|
||||||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||||
|
|
||||||
// Get the manifest content and remove the obsoleted package
|
// Get the manifest content and remove the obsoleted package
|
||||||
obsoleted_manager.get_and_remove_obsoleted_package(publisher, &parsed_fmri)?
|
obsoleted_manager.get_and_remove_obsoleted_package(publisher, &parsed_fmri)?
|
||||||
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
||||||
|
|
||||||
// Parse the manifest
|
// Parse the manifest
|
||||||
let manifest = libips::actions::Manifest::parse_string(manifest_content)?;
|
let manifest = libips::actions::Manifest::parse_string(manifest_content)?;
|
||||||
|
|
||||||
// Begin a transaction
|
// Begin a transaction
|
||||||
let mut transaction = repo.begin_transaction()?;
|
let mut transaction = repo.begin_transaction()?;
|
||||||
|
|
||||||
// Set the publisher for the transaction
|
// Set the publisher for the transaction
|
||||||
transaction.set_publisher(publisher);
|
transaction.set_publisher(publisher);
|
||||||
|
|
||||||
// Update the manifest in the transaction
|
// Update the manifest in the transaction
|
||||||
transaction.update_manifest(manifest);
|
transaction.update_manifest(manifest);
|
||||||
|
|
||||||
// Commit the transaction
|
// Commit the transaction
|
||||||
transaction.commit()?;
|
transaction.commit()?;
|
||||||
|
|
||||||
// Rebuild the catalog if not disabled
|
// Rebuild the catalog if not disabled
|
||||||
if !no_rebuild {
|
if !no_rebuild {
|
||||||
info!("Rebuilding catalog...");
|
info!("Rebuilding catalog...");
|
||||||
repo.rebuild(Some(publisher), false, false)?;
|
repo.rebuild(Some(publisher), false, false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
publisher,
|
publisher,
|
||||||
|
|
@ -1719,15 +1729,15 @@ fn main() -> Result<()> {
|
||||||
pattern,
|
pattern,
|
||||||
} => {
|
} => {
|
||||||
info!("Exporting obsoleted packages for publisher: {}", publisher);
|
info!("Exporting obsoleted packages for publisher: {}", 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)?;
|
||||||
|
|
||||||
// Export the obsoleted packages
|
// Export the obsoleted packages
|
||||||
let count = {
|
let count = {
|
||||||
// Get the obsoleted package manager
|
// Get the obsoleted package manager
|
||||||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||||
|
|
||||||
// Export the obsoleted packages
|
// Export the obsoleted packages
|
||||||
let output_path = PathBuf::from(output_file);
|
let output_path = PathBuf::from(output_file);
|
||||||
obsoleted_manager.export_obsoleted_packages(
|
obsoleted_manager.export_obsoleted_packages(
|
||||||
|
|
@ -1736,38 +1746,35 @@ fn main() -> Result<()> {
|
||||||
&output_path,
|
&output_path,
|
||||||
)?
|
)?
|
||||||
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
|
||||||
|
|
||||||
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,
|
||||||
input_file,
|
input_file,
|
||||||
publisher,
|
publisher,
|
||||||
} => {
|
} => {
|
||||||
info!("Importing obsoleted packages from {}", input_file);
|
info!("Importing obsoleted packages from {}", input_file);
|
||||||
|
|
||||||
// Open the repository
|
// Open the repository
|
||||||
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
||||||
|
|
||||||
// Import the obsoleted packages
|
// Import the obsoleted packages
|
||||||
let count = {
|
let count = {
|
||||||
// Get the obsoleted package manager
|
// Get the obsoleted package manager
|
||||||
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
let obsoleted_manager = repo.get_obsoleted_manager()?;
|
||||||
|
|
||||||
// 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,
|
||||||
publisher,
|
publisher,
|
||||||
|
|
@ -1775,35 +1782,36 @@ 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
|
||||||
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
let mut repo = FileBackend::open(repo_uri_or_path)?;
|
||||||
|
|
||||||
// Clean up the obsoleted packages
|
// Clean up the obsoleted packages
|
||||||
let count = {
|
let count = {
|
||||||
// Get the obsoleted package manager
|
// Get the obsoleted package manager
|
||||||
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 {
|
||||||
info!("Dry run: Would remove {} obsoleted packages", count);
|
info!("Dry run: Would remove {} obsoleted packages", count);
|
||||||
} else {
|
} else {
|
||||||
info!("Successfully removed {} obsoleted packages", count);
|
info!("Successfully removed {} obsoleted packages", count);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -235,7 +236,7 @@ impl Pkg5Importer {
|
||||||
info!(" Total packages processed: {}", total_count);
|
info!(" Total packages processed: {}", total_count);
|
||||||
info!(" Regular packages imported: {}", regular_count);
|
info!(" Regular packages imported: {}", regular_count);
|
||||||
info!(" Obsoleted packages stored: {}", obsoleted_count);
|
info!(" Obsoleted packages stored: {}", obsoleted_count);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,7 +268,7 @@ impl Pkg5Importer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Imports packages from the source repository
|
/// Imports packages from the source repository
|
||||||
///
|
///
|
||||||
/// Returns a tuple of (regular_package_count, obsoleted_package_count)
|
/// Returns a tuple of (regular_package_count, obsoleted_package_count)
|
||||||
fn import_packages(
|
fn import_packages(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -349,13 +350,15 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Imports a specific package version
|
/// Imports a specific package version
|
||||||
///
|
///
|
||||||
/// Returns a boolean indicating whether the package was obsoleted
|
/// Returns a boolean indicating whether the package was obsoleted
|
||||||
fn import_package_version(
|
fn import_package_version(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -389,7 +392,7 @@ impl Pkg5Importer {
|
||||||
// Check if this is an obsoleted package
|
// Check if this is an obsoleted package
|
||||||
let mut is_obsoleted = false;
|
let mut is_obsoleted = false;
|
||||||
let mut fmri_str = String::new();
|
let mut fmri_str = String::new();
|
||||||
|
|
||||||
// Extract the FMRI from the manifest
|
// Extract the FMRI from the manifest
|
||||||
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() {
|
||||||
|
|
@ -397,7 +400,7 @@ impl Pkg5Importer {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for pkg.obsolete attribute
|
// Check for pkg.obsolete attribute
|
||||||
for attr in &manifest.attributes {
|
for attr in &manifest.attributes {
|
||||||
if attr.key == "pkg.obsolete" && !attr.values.is_empty() {
|
if attr.key == "pkg.obsolete" && !attr.values.is_empty() {
|
||||||
|
|
@ -408,11 +411,11 @@ impl Pkg5Importer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is an obsoleted package, store it in the obsoleted directory
|
// If this is an obsoleted package, store it in the obsoleted directory
|
||||||
if is_obsoleted && !fmri_str.is_empty() {
|
if is_obsoleted && !fmri_str.is_empty() {
|
||||||
debug!("Handling obsoleted package: {}", fmri_str);
|
debug!("Handling obsoleted package: {}", fmri_str);
|
||||||
|
|
||||||
// Parse the FMRI
|
// Parse the FMRI
|
||||||
let fmri = match Fmri::parse(&fmri_str) {
|
let fmri = match Fmri::parse(&fmri_str) {
|
||||||
Ok(fmri) => fmri,
|
Ok(fmri) => fmri,
|
||||||
|
|
@ -424,10 +427,10 @@ impl Pkg5Importer {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the obsoleted package manager
|
// Get the obsoleted package manager
|
||||||
let obsoleted_manager = dest_repo.get_obsoleted_manager()?;
|
let obsoleted_manager = dest_repo.get_obsoleted_manager()?;
|
||||||
|
|
||||||
// Store the obsoleted package with null hash (don't store the original manifest)
|
// Store the obsoleted package with null hash (don't store the original manifest)
|
||||||
// This saves storage space for obsoleted packages that don't provide any useful
|
// This saves storage space for obsoleted packages that don't provide any useful
|
||||||
// information beyond the fact that they are obsoleted. When a client requests
|
// information beyond the fact that they are obsoleted. When a client requests
|
||||||
|
|
@ -439,18 +442,18 @@ 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
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
info!("Stored obsoleted package: {}", fmri);
|
info!("Stored obsoleted package: {}", fmri);
|
||||||
return Ok(true); // Return true to indicate this was an obsoleted package
|
return Ok(true); // Return true to indicate this was an obsoleted package
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-obsoleted packages, proceed with normal import
|
// For non-obsoleted packages, proceed with normal import
|
||||||
debug!("Processing regular (non-obsoleted) package");
|
debug!("Processing regular (non-obsoleted) package");
|
||||||
|
|
||||||
// Begin a transaction
|
// Begin a transaction
|
||||||
debug!("Beginning transaction");
|
debug!("Beginning transaction");
|
||||||
let mut transaction = dest_repo.begin_transaction()?;
|
let mut transaction = dest_repo.begin_transaction()?;
|
||||||
|
|
@ -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
|
||||||
|
|
@ -486,10 +490,10 @@ impl Pkg5Importer {
|
||||||
let first_two = &hash[0..2];
|
let first_two = &hash[0..2];
|
||||||
let next_two = &hash[2..4];
|
let next_two = &hash[2..4];
|
||||||
let file_path_new = file_dir.join(first_two).join(next_two).join(&hash);
|
let file_path_new = file_dir.join(first_two).join(next_two).join(&hash);
|
||||||
|
|
||||||
// Fall back to the old one-level hierarchy if the file doesn't exist in the new structure
|
// Fall back to the old one-level hierarchy if the file doesn't exist in the new structure
|
||||||
let file_path_old = file_dir.join(first_two).join(&hash);
|
let file_path_old = file_dir.join(first_two).join(&hash);
|
||||||
|
|
||||||
// Use the path that exists
|
// Use the path that exists
|
||||||
let file_path = if file_path_new.exists() {
|
let file_path = if file_path_new.exists() {
|
||||||
file_path_new
|
file_path_new
|
||||||
|
|
|
||||||
|
|
@ -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