Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/pkg-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,8 @@ path = "../../tests/test_upgrade_apply.rs"
[[test]]
name = "test_curated_registry"
path = "../../tests/test_curated_registry.rs"

[[test]]
name = "test_cli_no_deps"
path = "../../tests/test_cli_no_deps.rs"

90 changes: 54 additions & 36 deletions crates/pkg-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ enum Commands {
#[arg(long = "ignore-missing-libs")]
ignore_missing_libs: bool,

/// Skip resolving and installing declared dependencies
#[arg(long = "no-deps", alias = "skip-deps")]
no_deps: bool,

/// Interactively select from matching package candidates
#[arg(short = 'i', long = "interactive")]
interactive: bool,
Expand Down Expand Up @@ -348,6 +352,7 @@ async fn run() -> Result<()> {
yes,
ignore_missing_libs,
interactive,
no_deps,
} => {
let config_path = engine.layout().base_dir().join("repositories.toml");
let config = if config_path.exists() {
Expand Down Expand Up @@ -639,7 +644,7 @@ async fn run() -> Result<()> {
// The planner will validate the resulting installed snapshot
// again, so a failed dependency can never be reported as a
// successful install.
if !dry_run {
if !dry_run && !no_deps {
if let Err(e) = install_declared_dependencies(
&engine,
&artifact_path,
Expand Down Expand Up @@ -684,27 +689,29 @@ async fn run() -> Result<()> {
}

let mut detected_deps = Vec::new();
for dep in &preflight.package.dependencies {
let dep_name = dep.name.as_str();
if preflight
.missing_libraries
.iter()
.any(|m| matches_missing_library(dep_name, m))
{
if let Ok(RemoteResolution::Exact(p)) = engine
.resolve_dependency_package(
dep_name,
preferred_repo.as_deref(),
preferred_format.as_deref(),
config.as_ref(),
)
if !no_deps {
for dep in &preflight.package.dependencies {
let dep_name = dep.name.as_str();
if preflight
.missing_libraries
.iter()
.any(|m| matches_missing_library(dep_name, m))
{
if !detected_deps.iter().any(
|d: &pkg_core::domain::package::RemotePackage| {
d.name == p.name
},
) {
detected_deps.push(p);
if let Ok(RemoteResolution::Exact(p)) = engine
.resolve_dependency_package(
dep_name,
preferred_repo.as_deref(),
preferred_format.as_deref(),
config.as_ref(),
)
{
if !detected_deps.iter().any(
|d: &pkg_core::domain::package::RemotePackage| {
d.name == p.name
},
) {
detected_deps.push(p);
}
}
}
}
Expand Down Expand Up @@ -740,6 +747,7 @@ async fn run() -> Result<()> {
false,
InstallOptions {
allow_missing_libraries: false,
skip_dependencies: false,
},
) {
Ok(_) => {
Expand Down Expand Up @@ -820,6 +828,7 @@ async fn run() -> Result<()> {
dry_run,
InstallOptions {
allow_missing_libraries: allow_missing,
skip_dependencies: no_deps,
},
);

Expand Down Expand Up @@ -1262,7 +1271,9 @@ async fn run() -> Result<()> {
r.id, r.distro, r.format, status, desc
);
}
println!("\nTip: Run `pkg repo add <id>` to enable an official repository.");
println!(
"\nTip: Run `pkg repo add <id>` to enable an official repository."
);
}
return Ok(());
}
Expand Down Expand Up @@ -2434,6 +2445,10 @@ async fn mcp_call_tool(
.get("allow_missing_libs")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let no_deps = arguments
.get("no_deps")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let config_path = engine.layout().base_dir().join("repositories.toml");
let config = config_path
.is_file()
Expand All @@ -2453,22 +2468,24 @@ async fn mcp_call_tool(
}
};
let artifact = download_with_progress(engine, &remote, true).await?;
install_declared_dependencies(
engine,
&artifact,
profile,
Some(&remote.repository_id),
Some(&remote.format),
config.as_ref(),
true,
true,
&[],
1,
)
.await?;
if !no_deps {
install_declared_dependencies(
engine,
&artifact,
profile,
Some(&remote.repository_id),
Some(&remote.format),
config.as_ref(),
true,
true,
&[],
1,
)
.await?;
}
artifact
};
if std::path::Path::new(target).is_file() {
if !no_deps && std::path::Path::new(target).is_file() {
install_declared_dependencies(
engine,
&artifact,
Expand All @@ -2489,6 +2506,7 @@ async fn mcp_call_tool(
false,
InstallOptions {
allow_missing_libraries: allow_missing,
skip_dependencies: no_deps,
},
)?)?)
}
Expand Down
36 changes: 32 additions & 4 deletions crates/pkg-core/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub enum PackageInfo {
pub struct InstallOptions {
/// If true, missing shared libraries detected in ELF binaries will not abort installation.
pub allow_missing_libraries: bool,
/// If true, package dependency resolution is skipped.
pub skip_dependencies: bool,
}

/// Report returned by preflight inspection of an uninstalled package artifact.
Expand Down Expand Up @@ -422,13 +424,31 @@ impl Engine {
profile: &str,
replaced_packages: &[crate::domain::package::PackageName],
) -> Result<InstallPlan> {
Planner::plan_install_with_replacements(
self.plan_install_with_options_replacing(
artifact_path,
profile,
replaced_packages,
&InstallOptions::default(),
)
}

/// Plans an install while treating the listed installed package names
/// as replacements in the same upgrade transaction and applying the specified options.
pub fn plan_install_with_options_replacing(
&self,
artifact_path: &Path,
profile: &str,
replaced_packages: &[crate::domain::package::PackageName],
options: &InstallOptions,
) -> Result<InstallPlan> {
Planner::plan_install_with_replacements_and_options(
artifact_path,
&self.layout,
&self.db,
profile,
true,
replaced_packages,
options,
)
}

Expand Down Expand Up @@ -498,7 +518,12 @@ impl Engine {
replaced_packages: &[crate::domain::package::PackageName],
) -> Result<InstallPlan> {
if is_dry_run {
return self.plan_install_with_replacements(artifact_path, profile, replaced_packages);
return self.plan_install_with_options_replacing(
artifact_path,
profile,
replaced_packages,
&options,
);
}

// Acquire process lock to prevent concurrent state mutations (INV-012)
Expand All @@ -508,13 +533,14 @@ impl Engine {
let _ = Recovery::reconcile(&self.layout, &self.db)?;

// Plan installation
let mut plan = Planner::plan_install_with_replacements(
let mut plan = Planner::plan_install_with_replacements_and_options(
artifact_path,
&self.layout,
&self.db,
profile,
false,
replaced_packages,
&options,
)?;
let old_pkg = self.db.get_package(profile, plan.package.name.as_str())?;
let old_binaries = self
Expand Down Expand Up @@ -1616,7 +1642,9 @@ impl Engine {
Ok(Ok((repo, packages))) => successes.push((repo, packages)),
Ok(Err(err)) => failures.push(err),
Err(join_err) => {
failures.push(Error::Internal(format!("Repository task join error: {join_err}")));
failures.push(Error::Internal(format!(
"Repository task join error: {join_err}"
)));
}
}
}
Expand Down
1 change: 0 additions & 1 deletion crates/pkg-core/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ impl HostFacts {
}
}


#[cfg(test)]
mod tests {
use super::*;
Expand Down
27 changes: 24 additions & 3 deletions crates/pkg-core/src/planner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ impl Planner {
profile: &str,
is_dry_run: bool,
replaced_packages: &[PackageName],
) -> Result<InstallPlan> {
Self::plan_install_with_replacements_and_options(
artifact_path,
layout,
db,
profile,
is_dry_run,
replaced_packages,
&crate::engine::InstallOptions::default(),
)
}

/// Plans an install while treating the listed installed package names as
/// replacements in the same transaction and respecting customized install options.
pub fn plan_install_with_replacements_and_options(
artifact_path: &Path,
layout: &StoreLayout,
db: &StateDatabase,
profile: &str,
is_dry_run: bool,
replaced_packages: &[PackageName],
options: &crate::engine::InstallOptions,
) -> Result<InstallPlan> {
StoreLayout::validate_profile(profile)?;
let format = crate::format::detect_format(artifact_path)?;
Expand All @@ -58,10 +80,9 @@ impl Planner {
}

// Resolve declared package/capability constraints before producing an
// install plan. The resolver receives only immutable snapshots and
// evidence derived from the host and pkg-owned installed payloads.
// install plan, unless dependency resolution is explicitly skipped.
let mut resolved_dependencies = Vec::new();
if !package.constraints.is_empty() {
if !options.skip_dependencies && !package.constraints.is_empty() {
let mut host_evidence = HostEvidence::detect(&host);
let profile_lib_dir = layout.profile_lib_dir(profile);
if profile_lib_dir.is_dir()
Expand Down
18 changes: 14 additions & 4 deletions crates/pkg-core/src/repository/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ impl CuratedRegistry {
.timeout(std::time::Duration::from_secs(4))
.build();
if let Ok(client) = client {
if let Ok(resp) = client.get("https://pkg.atlantic.sh/repositories").send().await {
if let Ok(resp) = client
.get("https://pkg.atlantic.sh/repositories")
.send()
.await
{
if resp.status().is_success() {
if let Ok(bytes) = resp.bytes().await {
if let Ok(registry) = serde_json::from_slice::<CuratedRegistry>(&bytes) {
Expand All @@ -188,7 +192,9 @@ impl CuratedRegistry {
/// Finds a repository by ID (case-insensitive).
#[must_use]
pub fn find_by_id(&self, id: &str) -> Option<&CuratedRepository> {
self.repositories.iter().find(|r| r.id.eq_ignore_ascii_case(id))
self.repositories
.iter()
.find(|r| r.id.eq_ignore_ascii_case(id))
}

/// Selects the repositories that should be enabled by default for a host distribution.
Expand Down Expand Up @@ -340,7 +346,9 @@ impl RepositoriesConfig {
pub fn default_for_host_with_registry(registry: &CuratedRegistry) -> Self {
let (distro_id, distro_id_like) = crate::host::HostFacts::detect_distro();
let metadata = crate::host::HostFacts::release_metadata();
let codename = metadata.get("os_release_VERSION_CODENAME").map(|s| s.as_str());
let codename = metadata
.get("os_release_VERSION_CODENAME")
.map(|s| s.as_str());
let version_id = metadata.get("os_release_VERSION_ID").map(|s| s.as_str());

let mut matched = if let Some(ref d) = distro_id {
Expand All @@ -360,7 +368,9 @@ impl RepositoriesConfig {
}

let repos = matched.into_iter().map(RepositoryConfig::from).collect();
RepositoriesConfig { repositories: repos }
RepositoriesConfig {
repositories: repos,
}
}

/// Loads the repository configuration from the specified TOML file.
Expand Down
2 changes: 1 addition & 1 deletion crates/pkg-core/src/repository/deb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub async fn update_debian_repository(
);
let checksums = parse_release_checksums(&release)?;
let host_arch = get_debian_architecture();
let fetches = futures::stream::iter(components.to_vec().into_iter().map(|component| {
let fetches = futures::stream::iter(components.iter().cloned().map(|component| {
let client = &client;
let checksums = &checksums;
async move {
Expand Down
Loading
Loading