Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f9d3ede
fix: fall back to /usr/lib/os-release when /etc/os-release is absent
jussielo-amd Oct 14, 2026
838a521
fix: address review on os-release fallback
jussielo-amd Oct 14, 2026
5f2c3ff
fix: name the file a fallback read actually read, not always /etc/os-…
jussielo-amd Oct 14, 2026
988d4d0
style: apply cargo fmt
jussielo-amd Oct 15, 2026
45c537c
fix: delete dead openmpi::plan_for_os_release
jussielo-amd Oct 15, 2026
2dcb831
fix: make the simulated-host-root test compare against an independent…
jussielo-amd Oct 15, 2026
2b533f9
test: cover malformed-line fallback path naming at all four call sites
jussielo-amd Oct 15, 2026
83c528c
fix: match either os-release path in e2e failure filter, add examine-25
jussielo-amd Oct 15, 2026
192d61f
fix: narrow the WSL probe test's overclaimed parity with the Rust rea…
jussielo-amd Oct 15, 2026
c68afd1
refactor: factor the resolver closures and the unreadable-plan literal
jussielo-amd Oct 15, 2026
b6b0abd
fix: non-blocking doc/comment nits and the last two /etc-only readers
jussielo-amd Oct 15, 2026
565539a
refactor: fold repo_version resolution into unreadable_os_release_plan
jussielo-amd Oct 15, 2026
5c78fdb
fix: assert the exact OsRelease variant in the simulated-root test
jussielo-amd Oct 15, 2026
6731fe8
fix: drop filename prefixes from the WSL distro-too-old verify grep
jussielo-amd Oct 15, 2026
fce44bf
refactor: share the Unreadable message's path+error prefix
jussielo-amd Oct 15, 2026
856a293
fix: allow dead_code on install_hint_for_at to match its sibling
jussielo-amd Oct 15, 2026
31881ce
fix: make the WSL distro-too-old verify grep existence-only
jussielo-amd Oct 15, 2026
fe42a74
test: cover the Windows half of reaches_os_release_parse
jussielo-amd Oct 15, 2026
4b3d460
Merge remote-tracking branch 'origin/main' into fix/os-release-usr-li…
jussielo-amd Oct 15, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,9 @@ jobs:
# `is_wsl_host`, `/run/.containerenv` and `/proc/1/cgroup` via
# `probe_container`, and both `/etc/os-release` reads (the distro
# fields and the distro name, in
# `os_release_reads_follow_the_simulated_host_root`). The other routed
# `os_release_reads_follow_the_simulated_host_root` and, for the
# `/usr/lib/os-release` fallback, in
# `os_release_reads_fall_back_to_usr_lib_when_etc_is_absent`). The other routed
# reads (`/dev/dri`, `/sys/class/drm`, `/sys/module/amdgpu`, the other
# `/proc` files, the modprobe directories, the `/usr/lib/wsl/lib` and
# ROCDXG checks) have no planted-root unit assertion; they are covered
Expand Down
262 changes: 241 additions & 21 deletions apps/rocm/src/driver_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use rocm_core::{AppPaths, ExamineSummary, shell_command_for_host};
use serde::{Deserialize, Serialize};

use crate::cli_report;
use crate::runtime_install_finalize::{empty_as_unknown, read_os_release};
use crate::runtime_install_finalize::{empty_as_unknown, real_os_release};

pub(crate) fn install_driver(
paths: &AppPaths,
Expand All @@ -35,11 +35,11 @@ pub(crate) fn install_driver(
) -> std::result::Result<DriverInstallResult, DriverInstallError> {
let examine =
ExamineSummary::gather().map_err(|source| DriverInstallError::new(source, false))?;
let os_release = read_os_release().unwrap_or_default();
let os_release = real_os_release();
// The only place the real privilege level is read; every builder below takes
// it as a parameter so both branches stay testable on any host.
let plan =
build_driver_install_plan(&examine, &os_release, dkms, PrivilegeEscalation::detect());
build_driver_install_plan_read(&examine, &os_release, dkms, PrivilegeEscalation::detect());
let mut output = render_driver_install_plan(&plan, yes, dry_run);
if !yes || dry_run || !plan.supported || !plan.mutating {
return Ok(DriverInstallResult {
Expand Down Expand Up @@ -798,11 +798,59 @@ fn wsl_rocdxg_driver_plan(escalation: PrivilegeEscalation) -> DriverInstallPlan
}
}

/// [`build_driver_install_plan_at`], naming `/etc/os-release` in any
/// unreadable-line message. Only the unit tests fixturing bare
/// `os_release_text` (never the real host) use this one; the real call site
/// in [`install_driver`] knows the path it actually read and calls
/// [`build_driver_install_plan_read`] directly.
#[cfg(test)]
fn build_driver_install_plan(
examine: &ExamineSummary,
os_release_text: &str,
dkms: bool,
escalation: PrivilegeEscalation,
) -> DriverInstallPlan {
build_driver_install_plan_at(
examine,
"/etc/os-release",
os_release_text,
dkms,
escalation,
)
}

/// The plan for a release file that cannot be read as a distro at all — a
/// malformed line ([`build_driver_install_plan_at`]'s own early return) or a
/// file that exists but could not be read
/// ([`build_driver_install_plan_read`]'s `Unreadable` branch). Both name
/// `reason`, never a distro, and differ only in it: shared here, including
/// `repo_version`'s own resolution, so a new early branch — or a bumped
/// default — added to one is not silently absent from the other.
fn unreadable_os_release_plan(reason: String) -> DriverInstallPlan {
DriverInstallPlan {
supported: false,
mutating: false,
policy: "unreadable_os_release".to_owned(),
os_id: String::new(),
version_id: String::new(),
codename: String::new(),
repo_version: resolve_shell_default_template("${ROCM_CLI_AMDGPU_VERSION:-7.2.4}"),
reason,
preflight_checks: Vec::new(),
commands: Vec::new(),
checks: vec!["rocm examine".to_owned()],
reboot_required: false,
}
}

/// [`build_driver_install_plan`], naming `os_release_path` instead of always
/// `/etc/os-release` in an unreadable-line message.
fn build_driver_install_plan_at(
examine: &ExamineSummary,
os_release_path: &str,
os_release_text: &str,
dkms: bool,
escalation: PrivilegeEscalation,
) -> DriverInstallPlan {
// Resolve the AMD graphics version and amdgpu-install package release once,
// here at plan-build time, so the concrete values are baked into both the
Expand Down Expand Up @@ -841,24 +889,11 @@ fn build_driver_install_plan(
let os_release = match rocm_core::os_release::parse(os_release_text) {
Ok(fields) => fields,
Err(unreadable) => {
return DriverInstallPlan {
supported: false,
mutating: false,
policy: "unreadable_os_release".to_owned(),
os_id: String::new(),
version_id: String::new(),
codename: String::new(),
repo_version,
reason: format!(
"/etc/os-release {unreadable}; no driver commands were planned \
because the distro cannot be read. To fix it, {}.",
unreadable.fix()
),
preflight_checks: Vec::new(),
commands: Vec::new(),
checks: vec!["rocm examine".to_owned()],
reboot_required: false,
};
return unreadable_os_release_plan(format!(
"{os_release_path} {unreadable}; no driver commands were planned \
because the distro cannot be read. To fix it, {}.",
unreadable.fix()
));
}
};
let field = |key: &str| os_release.get(key).cloned();
Expand Down Expand Up @@ -971,6 +1006,36 @@ fn build_driver_install_plan(
}
}

/// [`build_driver_install_plan_at`] for `read`, naming the file actually read
/// in an unreadable-line message, and reporting a file that exists but could
/// not be read as itself rather than as an unknown distribution.
///
/// Without the latter, a host whose `/etc/os-release` is merely unreadable
/// (permission denied, a directory in its place) would plan nothing and say
/// "no commands were guessed for this distro" — the same message an actually
/// unsupported distribution gets — sending the user looking for a support
/// gap instead of a filesystem problem. The Windows and WSL branches never
/// reach the os-release parse at all, so they are unaffected either way and
/// are left to the normal call below.
fn build_driver_install_plan_read(
examine: &ExamineSummary,
read: &rocm_core::hardware_root::OsRelease,
dkms: bool,
escalation: PrivilegeEscalation,
) -> DriverInstallPlan {
let reaches_os_release_parse =
examine.os != "windows" && !examine.wsl.as_ref().is_some_and(|wsl| wsl.is_wsl);
if reaches_os_release_parse
&& let rocm_core::hardware_root::OsRelease::Unreadable { path, error } = read
{
return unreadable_os_release_plan(format!(
"{}; no driver commands were planned because the distro cannot be read.",
rocm_core::hardware_root::unreadable_prefix(path, error)
));
}
build_driver_install_plan_at(examine, read.path(), read.text(), dkms, escalation)
}

/// Select a driver install plan for a distro whose `/etc/os-release` `ID` is not
/// an AMD-documented distro, by falling back to its `ID_LIKE` base family.
///
Expand Down Expand Up @@ -1875,6 +1940,161 @@ mod tests {
);
}

/// `build_driver_install_plan_read` is the one seam `install_driver` hands
/// its `real_os_release()` result to. Both are now one-line delegations
/// to `hardware_root::read_os_release`, so reverting either back to a raw
/// `/etc/os-release` read is a type error here, not a silent regression.
/// This still pins the behaviour at the seam itself: an `OsRelease` that
/// says the fallback path was read plans exactly what a primary-path read
/// with the same content would.
#[test]
fn a_fallback_read_plans_exactly_like_a_primary_read() {
let _env = ScopedTestEnv::with_amd_overrides_cleared();
let examine = test_examine("linux", false);
let fallback = rocm_core::hardware_root::OsRelease::Found {
path: "/usr/lib/os-release",
text: "ID=ubuntu\nVERSION_ID=\"24.04\"\nVERSION_CODENAME=noble\n".to_owned(),
};
let primary = rocm_core::hardware_root::OsRelease::Found {
path: "/etc/os-release",
text: "ID=ubuntu\nVERSION_ID=\"24.04\"\nVERSION_CODENAME=noble\n".to_owned(),
};

let from_fallback =
build_driver_install_plan_read(&examine, &fallback, true, PrivilegeEscalation::Sudo);
let from_primary =
build_driver_install_plan_read(&examine, &primary, true, PrivilegeEscalation::Sudo);
assert_eq!(from_fallback.os_id, "ubuntu");
assert_eq!(
(
from_fallback.os_id.clone(),
from_fallback.version_id.clone(),
from_fallback.codename.clone(),
from_fallback.reason.clone(),
from_fallback.execution_commands(),
),
(
from_primary.os_id.clone(),
from_primary.version_id.clone(),
from_primary.codename.clone(),
from_primary.reason.clone(),
from_primary.execution_commands(),
),
);
}

/// A file that exists but could not be read plans nothing and names
/// itself, distinctly from both an unreadable *line* (which still comes
/// from a file that was read, see
/// [`an_unreadable_os_release_names_its_line_and_plans_nothing`]) and from
/// an unsupported distribution — which would send the user looking for a
/// support gap instead of a filesystem problem.
#[test]
fn an_unreadable_file_plans_nothing_and_names_itself() {
let _env = ScopedTestEnv::with_amd_overrides_cleared();
let read = rocm_core::hardware_root::OsRelease::Unreadable {
path: "/usr/lib/os-release",
error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
};
let plan = build_driver_install_plan_read(
&test_examine("linux", false),
&read,
true,
PrivilegeEscalation::Sudo,
);
assert_eq!(plan.policy, "unreadable_os_release");
assert!(!plan.supported);
assert!(plan.commands.is_empty(), "{:?}", plan.commands);
assert_eq!(
plan.reason,
"/usr/lib/os-release could not be read: denied; no driver commands were planned \
because the distro cannot be read."
);
}

/// The same malformed line as
/// [`an_unreadable_os_release_names_its_line_and_plans_nothing`], but read
/// through the fallback path: the reason must name `/usr/lib/os-release`,
/// not `/etc/os-release`. That test only exercises the bare,
/// hardcoded-`/etc` `build_driver_install_plan`, so a regression that
/// hardcoded the path back into `build_driver_install_plan_read` would
/// still pass it.
#[test]
fn a_fallback_reads_malformed_line_names_the_fallback_path() {
let _env = ScopedTestEnv::with_amd_overrides_cleared();
let read = rocm_core::hardware_root::OsRelease::Found {
path: "/usr/lib/os-release",
text: "ID=ubuntu\nVERSION_ID=\"24.04\"\nVERSION_CODENAME=noble\nunset ID\n".to_owned(),
};
let plan = build_driver_install_plan_read(
&test_examine("linux", false),
&read,
true,
PrivilegeEscalation::Sudo,
);
assert_eq!(
plan.reason,
"/usr/lib/os-release line 4 is not a plain assignment: \"unset ID\"; no driver \
commands were planned because the distro cannot be read. To fix it, remove \
that line or rewrite it as NAME=value."
);
assert!(!plan.supported);
assert!(plan.commands.is_empty(), "{:?}", plan.commands);
assert_eq!(plan.policy, "unreadable_os_release");
}

/// On WSL, `build_driver_install_plan_read`'s `Unreadable` branch must
/// not fire at all: WSL has its own guest-side os-release probe (see
/// `examine.rs`'s `WSL_REMOTE_PROBE`), so the local Rust-side read this
/// function receives is never authoritative for a WSL host, and the plan
/// must stay the ordinary `wsl_rocdxg` one — not `unreadable_os_release` —
/// regardless of what it says. Without the `reaches_os_release_parse`
/// guard this would plan nothing on a WSL host whose local read happens
/// to come back `Unreadable`, even though the guest-side probe may read
/// the distro just fine.
#[test]
fn a_wsl_host_ignores_an_unreadable_local_os_release_read() {
let _env = scoped_rocdxg_env();
let read = rocm_core::hardware_root::OsRelease::Unreadable {
path: "/usr/lib/os-release",
error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
};
let plan = build_driver_install_plan_read(
&test_examine("linux", true),
&read,
true,
PrivilegeEscalation::Sudo,
);
assert_eq!(plan.policy, "wsl_rocdxg");
assert!(plan.supported);
assert!(plan.mutating);
}

/// The Windows half of the same `reaches_os_release_parse` guard: a
/// native Windows host's driver install is `windows_validate_only`
/// regardless of what the local read says, since the Linux driver
/// install this plan guards is never attempted there. Without this half
/// of the guard, a Windows host whose `real_os_release()` happens to come
/// back `Unreadable` would wrongly take the Linux-only
/// `unreadable_os_release` policy instead.
#[test]
fn a_windows_host_ignores_an_unreadable_local_os_release_read() {
let _env = ScopedTestEnv::with_amd_overrides_cleared();
let read = rocm_core::hardware_root::OsRelease::Unreadable {
path: "/usr/lib/os-release",
error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
};
let plan = build_driver_install_plan_read(
&test_examine("windows", false),
&read,
true,
PrivilegeEscalation::Sudo,
);
assert_eq!(plan.policy, "windows_validate_only");
assert!(!plan.supported);
assert!(!plan.mutating);
}

/// With a duplicated key, the plan is built from the last assignment — the
/// one a shell keeps, and the one `rocm examine` reports, since both read
/// through `rocm_core::os_release`.
Expand Down
Loading
Loading