Skip to content
Closed
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
7 changes: 7 additions & 0 deletions src/anolisa/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- `anolisa install` no longer refuses raw-backend installs on hosts without
RPM tooling (e.g. Ubuntu/Debian): the system-RPM presence check is skipped
where RPM is not the host's package authority, instead of failing with
"rpm not found on PATH". Rpm-family hosts keep the check unchanged.

## [0.2.15] - 2026-07-30

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,17 +438,24 @@ pub(crate) fn plan_component(
let (target, native_package): (ProviderTarget, Option<String>) = match &active_binding {
Some(binding) => target_for_active_record(binding, &family, args, &component),
None if family == "raw" => {
// The I3 presence probe consults the rpmdb, which only exists
// where RPM is the host's package authority; on a deb-family
// host (e.g. Ubuntu) the check is vacuous and demanding rpm/dnf
// would block every raw install. Skipping yields NotProbed,
// which the planner does not read as "present".
let native_package = match scope {
InstallationScope::System => Some(system_probe_package(
args,
&layout,
&env,
&repo_config,
&component,
query,
&command,
)?),
InstallationScope::User { .. } => None,
InstallationScope::System if system_rpm_probe_applies(env.os_id.as_deref()) => {
Some(system_probe_package(
args,
&layout,
&env,
&repo_config,
&component,
query,
&command,
)?)
}
_ => None,
};
(
ProviderTarget::Owned {
Expand Down Expand Up @@ -875,6 +882,22 @@ fn resolve_owned_artifact(
.map_err(|err| err.with_command(command))
}

/// Whether a fresh raw system-scope install must run the system-RPM
/// presence probe (planner rule I3).
///
/// The probe protects unobserved system RPMs, which can only exist where
/// RPM is the host's package authority. A host that positively identifies
/// as another package family (deb: Ubuntu, Debian, ...) has no rpmdb to
/// consult, so the probe is skipped instead of demanding rpm/dnf the
/// distro does not ship. Rpm-family and unrecognized hosts keep the probe,
/// so a genuinely rpm-based host with broken tooling still fails closed.
pub(crate) fn system_rpm_probe_applies(os_id: Option<&str>) -> bool {
match os_id.and_then(anolisa_env::pkg_base_from_id) {
Some(base) => base == "rpm",
None => true,
}
}

/// RPM package name a raw install probes for the planner's I3 rule.
fn system_probe_package(
args: &InstallArgs,
Expand Down Expand Up @@ -1749,6 +1772,20 @@ mod tests {
}
}

#[test]
fn system_rpm_probe_follows_host_package_family() {
// rpm-family hosts must keep the I3 probe.
assert!(system_rpm_probe_applies(Some("alinux")));
assert!(system_rpm_probe_applies(Some("anolis")));
assert!(system_rpm_probe_applies(Some("fedora")));
// deb-family hosts have no rpmdb; the probe is vacuous there.
assert!(!system_rpm_probe_applies(Some("ubuntu")));
assert!(!system_rpm_probe_applies(Some("debian")));
// Unrecognized or undetected hosts fail closed and keep the probe.
assert!(system_rpm_probe_applies(Some("opensuse-leap")));
assert!(system_rpm_probe_applies(None));
}

#[test]
fn delegated_install_reports_execution_and_finalization() {
let (_tmp, mut ctx) = system_ctx_with_configured_rpm_repo(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,13 @@ fn delegated_install_requires_configured_rpm_backend() {
fn system_install_without_rpm_tooling_warns_and_exits() {
// System scope, fresh state: with rpm/dnf absent the probe cannot prove
// the component is not an unobserved system RPM (I3), so install refuses
// rather than silently placing raw files over one.
// rather than silently placing raw files over one. Only hosts whose
// package authority is RPM run the probe; a deb-family host asserts the
// inverse in `deb_host_missing_rpm_tooling_does_not_block_raw_install`.
let host_os_id = anolisa_env::EnvService::detect().os_id;
if !system_rpm_probe_applies(host_os_id.as_deref()) {
return;
}
let (_tmp, ctx) = system_ctx_with_raw_repo(false);
let q = FakeQuery {
command_missing: true,
Expand All @@ -687,6 +693,38 @@ fn system_install_without_rpm_tooling_warns_and_exits() {
);
}

#[test]
fn deb_host_missing_rpm_tooling_does_not_block_raw_install() {
// A deb-family host (e.g. Ubuntu) has no rpmdb, so the I3 presence
// probe is vacuous: missing rpm/dnf must not block a raw install.
// Runs only on such hosts; rpm-authority hosts assert the inverse in
// `system_install_without_rpm_tooling_warns_and_exits`.
let host_os_id = anolisa_env::EnvService::detect().os_id;
if system_rpm_probe_applies(host_os_id.as_deref()) {
return;
}
let tmp = tempdir().expect("tmpdir");
let prefix = tmp.path().join("sys");
let repo_url = write_local_repo(&tmp.path().join("repo"));
let mut a = args("agentsight");
a.repo = Some(repo_url);
let ctx = ctx_with_prefix(false, Some(prefix));
let q = FakeQuery {
command_missing: true,
..Default::default()
};

handle_one_with_query("agentsight".to_string(), a, &ctx, &q)
.expect("raw install must not require rpm tooling on a non-rpm host");

assert!(
load_store(&ctx)
.find(ObjectKind::Component, "agentsight")
.is_some(),
"raw install must be recorded"
);
}

#[test]
fn explicit_rpm_without_tooling_warns_and_exits() {
let (_tmp, ctx) = system_ctx_with_raw_repo(false);
Expand Down
Loading