Skip to content

fix(uninstall): undo the installer's PATH change and remove the running exe - #636

Merged
rominf merged 2 commits into
mainfrom
fix/uninstall-remove-running-exe-and-path
Oct 15, 2026
Merged

rominf merged 2 commits into
mainfrom
fix/uninstall-remove-running-exe-and-path

Conversation

@rominf

@rominf rominf commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

rocm uninstall left two parts of ROCm CLI behind:

  • Windows: it skipped the running rocm.exe, so the program that ran the uninstall stayed in the install folder.
  • All platforms: the installer's PATH change survived. That is the marked block install.sh appends to a shell profile, or the user Path entry install.ps1 adds.

#616 (open) adds uninstall guidance to the README; this PR is the code change. It does not depend on #616.

Changes

  • Installers record their PATH change. They write it to <install dir>/.rocm-cli-path-edit: profile and path-expr lines from install.sh, a user-path line from install.ps1. Only a change the installer made itself is recorded, never a PATH line the user wrote. install.sh adds lines instead of overwriting, so a reinstall from a different shell keeps the earlier entry.
  • Uninstall undoes it.
    • It removes the marked profile block. The profile reads back byte for byte as it was before the install, a profile kept as a symlink stays a symlink, and non-UTF-8 profiles are handled.
    • On Windows it removes the user Path entry from HKCU\Environment, keeps the value's REG_SZ/REG_EXPAND_SZ type, and broadcasts WM_SETTINGCHANGE.
    • It also finds marked blocks left by installs from before the record.
  • Order and failure handling. The PATH change is undone first, before anything is removed. If it can't be undone, or a profile the installer recorded can't be read, uninstall stops with nothing else removed, so you can fix it and retry. Each step is printed as it finishes, so a later failure still shows what was already done.
  • Profile rewrite. The edited profile is written to a new file created with the original's permissions, never wider, then renamed into place.
  • Installer record. install.sh records a profile only when it holds a closed marked block for this folder. install.ps1 writes its record before changing the user Path.
  • When the change is kept. It is left in place, and the review says so, when other programs live in the install folder (they may rely on it), and with --keep-binaries. The record stays too, so a later uninstall still knows about it. With --keep-binaries the review says so; in a shared folder it names the record it keeps.
  • Running rocm.exe. It is deleted with self-replace (Apache-2.0, Windows-only dependency). The crate moves the file out of the install folder and deletes it once the process exits.

Stopping managed servers and the automation helper before removal is left to #299, which implements it. This PR doesn't depend on #299: Windows allows renaming a running executable, so the self-delete works without stopping those processes first.

Known limits

  • Shared default folder. ~/.local/bin is often shared with other tools (uv, pipx, …). In that case the PATH change is kept and the review names those tools. On many default Linux installs, uninstall will therefore report the change rather than undo it.
  • Windows installs from before this change have no record. Uninstall can't tell an entry the installer added from one the user added, so it leaves the entry and says so.

Tests

Lifecycle scenarios in install_lifecycle.feature:

Scenario Platform What it checks
lifecycle-09 Linux Uninstall restores the profile exactly; the record is gone.
lifecycle-22 Windows The rocm.exe that ran uninstall is gone. Before this change it was skipped.
lifecycle-23 Linux (new) A shared folder keeps the change and its record, and the output says why.
lifecycle-24 Windows (new) The install folder is taken back off the user PATH; another %VAR% entry and the REG_EXPAND_SZ type are kept. @serial, like lifecycle-13.
lifecycle-25 Linux (new) --keep-binaries keeps the change and says so. This only guards against a future regression: the old code never touched PATH.
lifecycle-26 Linux (new) A PATH undo that fails removes nothing (binaries, record and isolated state all stay), and a retry completes.
lifecycle-27 Linux (new) A profile picked with ROCM_CLI_SHELL_PROFILE, in an install folder reached through a link, is found and restored only through the installer's record.

The XDG purge step used to check a subdirectory that never existed. It now seeds a file in each directory and asserts the directories are gone.

Unit tests cover:

  • profile block removal (bash, fish, CRLF, non-UTF-8, the user's own line, another install's block, an unterminated block);
  • record parsing;
  • shared-folder detection;
  • symlinked home and symlinked profile;
  • an unreadable profile;
  • abort ordering and retry;
  • --keep-binaries against an installed folder holding a record and a marked profile;
  • the replacement's permissions at creation, and a stale replacement;
  • non-UTF-8 record, profile path, install folder and file names;
  • a folder that can't be listed;
  • streaming output on a late failure;
  • the running exe matched case-insensitively;
  • the Windows path-list edit.

Verification

Run locally on Linux (WSL2):
On the current head (rebased onto 287e08a8):

  • cargo test -p rocm --bin rocm (1196 passed) and -p rocm-core --lib (666 passed).
  • Both CI clippy invocations, for Linux and for the Windows target; feature_naming; cargo check --test e2e; manifest --check.
  • E2E_INCLUDE_LIFECYCLE=1 E2E_ONLY_LIFECYCLE=1 cargo xtask e2e: 13/13 Linux lifecycle scenarios pass (01–09, 23, 25–27), run on the head before the final rebase.
  • tpn --check, check-crate-edges, shellcheck on install.sh, and a pwsh parse of the registry scripts pass.

The full default e2e suite, scripts/smoke_local.py and cargo xtask coverage were run on the earlier head, not re-run after the review fixes; CI runs them.

Not verified locally: the Windows code paths (registry edit, self-delete, the case-insensitive exe match, lifecycle-22/24 including the expandable-entry check), which were only type- and lint-checked with cargo clippy --target x86_64-pc-windows-gnu -- -D warnings. Deliberate type errors injected into the cfg(windows) code failed that check, so it really compiles them. They execute in the Windows lifecycle CI lane.

@juhovainio juhovainio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed this (diff-only, didn't run the e2e suite myself) — no blocking issues, it does what it says: undoes the installer's PATH edit and removes the running exe on uninstall. Left a handful of inline notes, mostly about edge-case consistency rather than correctness:

  • A couple of spots where a failure partway through execute_plan behaves differently depending on which step hit it — worth making consistent.
  • install.ps1's PATH-record write and the record read in uninstall.rs have narrower safety margins than their Unix/byte-level counterparts elsewhere in this same change.
  • Some test duplication in lifecycle_steps.rs that could use a shared helper.
  • A possible Windows CI flakiness risk around the registry-mutating scenarios (not new, but this PR adds a second one).
  • A duplicate atomic-write helper and a small messaging gap for --keep-binaries, both just worth being aware of.

Also worth a quick look: the PR description attributes this to #616, but that issue doesn't actually describe the PATH-record/undo mechanism or exe removal — might be worth tightening that reference.

On the positive side: the byte-level profile editing (handles non-UTF-8 and CRLF), the symlinked-$HOME handling, and the then_xdg_gone fix (the old assertion was checking a path that never existed, so it always passed trivially — the new one actually tests something) all stood out as solid, deliberate work. Docs look fully in sync with the implementation.

None of this blocks merging as far as I can tell from the diff — just flagging for your call.

)),
Ok(false) => {}
Err(error) => {
unreadable = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a Unix profile candidate fails to read here (e.g. permission denied), unreadable gets set and a warning is queued, but the function just keeps going: it doesn't add this profile to edits, and leaves plan.path_record as None. A few lines up, execute_plan's own undo loop treats a comparable failure very differently — it bails out and keeps the binaries around for a retry. Here, the uninstall proceeds anyway, the binaries get removed, and the stale .rocm-cli-path-edit record is left on disk with nothing left around to ever retry it (no rocm binary remains to rerun rocm uninstall). Doesn't corrupt anything and a warning is printed, but it's inconsistent with the "a failure to undo stops uninstall" guarantee the module doc describes, and permission-denied profiles are realistic enough to be worth handling the same way as the execute-time case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. If the installer recorded writing to a profile, uninstall now plans to undo it even when the profile cannot be read. The review warns, and uninstall stops before removing anything. A profile that is only scanned for, not recorded, still just warns. Tests: a_recorded_profile_that_cannot_be_read_stops_before_anything_goes and an_unrecorded_profile_that_cannot_be_read_is_a_warning_not_an_absence.

Comment thread apps/rocm/src/uninstall.rs Outdated
}

if let Some(exe) = &plan.self_delete {
lines.push(delete_running_executable(exe)?);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bare ? — if delete_running_executable fails, every "removed X" line collected earlier in this same call is dropped instead of printed. Two branches above, the PATH-undo failure path explicitly reprints lines before bailing for exactly this reason. Low real-world frequency since self-replace is normally reliable, but worth the same treatment for consistency: print what was collected so far before returning the error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed for every failure path: each line is now printed as its step finishes, so no failure can drop them (6670305f). Test: a_failure_after_the_path_undo_still_reports_what_was_done.

Comment thread install.ps1
# user PATH stays the user's.
$recordPath = Join-Path $Path ".rocm-cli-path-edit"
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($recordPath, "user-path $Path`n", $utf8NoBom)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overwrites .rocm-cli-path-edit with WriteAllText (truncate), whereas install.sh's record_path_edit appends with a dedup guard. A crash between the registry write above and this write leaves PATH modified with no record — though that degrades to the same "pre-change install, no record" case the PR's own "Known limits" section already discloses, so it's a narrow timing window reaching an already-acknowledged state rather than a new failure mode. Flagging for awareness; not blocking.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reordered: the record is now written before SetEnvironmentVariable. An interrupted install now leaves at worst a record of an entry that is not there. Uninstall reports that entry as no longer on the user PATH.

Comment thread apps/rocm/src/uninstall.rs Outdated
home: Option<&Path>,
) {
let record_path = bin_dir.join(PATH_EDIT_RECORD);
let record = match fs::read_to_string(&record_path) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs::read_to_string here will fail on a non-UTF-8 byte in the record (possible if $HOME contains non-UTF-8 bytes — install.sh writes it unescaped), silently treating the whole record as unreadable and leaving that install's PATH edit unaddressed. The profile-editing path elsewhere in this file (remove_profile_block_bytes) is deliberately byte-level for exactly this reason; this read isn't. Fails safe, narrow precondition, but worth the same tolerance for consistency.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The record is parsed as bytes, and its targets are kept as OsString. The folder spellings derived from it are compared as bytes too, so a non-UTF-8 HOME or install folder also matches. Tests: a_record_naming_a_profile_that_is_not_utf8_is_still_followed and an_install_folder_whose_path_is_not_utf8_is_still_found.

}

#[when("the user uninstalls from the installed binary keeping the binaries")]
async fn when_uninstall_keep_binaries(world: &mut E2eWorld) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when_uninstall, when_uninstall_keep, and this new when_uninstall_keep_binaries are three near-identical Command::new(&rocm)... blocks differing only in the args slice. A shared run_uninstall(world, extra_args) helper would cut roughly 45 duplicated lines. Separately, then_profile_kept (line 1286) duplicates then_single_path_marker's exact marker-count check under a different name. Cosmetic, not a correctness issue — just flagging for a cleanup pass.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: a shared run_uninstall(world, extra_args) helper, and both marker steps now use assert_single_path_marker.

And the record of the installer's PATH change is still in the install directory

@id:lifecycle-windows-uninstall-restores-user-path @lifecycle @requires-os:windows
Scenario: lifecycle-24 - Windows - uninstall takes the install directory back off the user PATH

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scenario and the pre-existing lifecycle-13 both capture/mutate/restore the single real HKCU\Environment\Path value on the Windows runner, with no serialization guard visible here. max_concurrent_scenarios defaults to 64 unless the lane has a GPU or shared-cache dirs configured. This PR doubles the collision surface on what looks like an existing (not new) hazard — couldn't confirm from here whether the actual Windows CI lane forces these serial. Worth checking if this (or lifecycle-13) ever flakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: the hosted Windows lane runs up to 64 scenarios at once. lifecycle-13 and lifecycle-24 are now @serial. Capturing and restoring the Path also keeps its registry type now; before, an expandable Path was flattened to REG_SZ.


/// Replace `path`'s contents in one step, keeping its permissions, so an
/// interrupted write never leaves a truncated shell profile behind.
fn write_replacing(path: &Path, contents: &[u8]) -> Result<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a second, independent atomic-replace-file implementation (temp-file + fs::rename, preserving source permissions) alongside the existing write_file_atomically family in therock.rs, which a prior commit (911a399) introduced specifically to consolidate atomic-write logic across the codebase. It does have a real justification the existing helpers lack — preserving the destination file's permission bits matters when rewriting a live shell profile, and the existing helpers don't do that. Just flagging so the duplication is a known, deliberate tradeoff rather than something that drifts further next time someone needs atomic writes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping it separate on purpose. It must keep the profile's permissions, edit through a symlinked profile, and now create the new file with its final mode. write_file_atomically does none of these, and neither does #610's shared version. If #610 lands with a keep-permissions option, I will switch to it.

Comment thread apps/rocm/src/main.rs
#[arg(long)]
dry_run: bool,
/// Keep installed rocm-cli binaries.
/// Keep installed rocm-cli binaries, and the installer's PATH change.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--keep-binaries keeps the PATH record too (confirmed by the unit test that plan_path_edits is simply never invoked under this flag), but unlike the shared-install-folder case — which prints an explicit "PATH change left in place: ..." message — there's no equivalent message here telling the user the PATH change was left alone. Purely a messaging completeness gap, not a behavior bug.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The note now reads "binary removal disabled by --keep-binaries, so any PATH change the installer made stays too". The unit test and lifecycle-25 assert it together with the state that is kept.

juhovainio
juhovainio previously approved these changes Oct 12, 2026
jussielo-amd
jussielo-amd previously approved these changes Oct 12, 2026

@jussielo-amd jussielo-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff against origin/main (three-dot merge-base) on branch pr-636-review. No prior reviews or review comments existed on this PR before this one.

This is a careful, well-scoped fix. rocm uninstall previously left two things behind: the running rocm.exe on Windows (unlinkable while running) and the installer's PATH edit (shell-profile block on Linux, user PATH registry entry on Windows). The PR adds crates/rocm-core/src/user_path.rs for the Windows HKCU\Environment Path manipulation (preserves REG_SZ/REG_EXPAND_SZ type, broadcasts WM_SETTINGCHANGE) and substantially extends apps/rocm/src/uninstall.rs to record and undo the PATH edit, with self-replace handling the Windows self-delete.

What I checked:

  • Full diff read through user_path.rs, uninstall.rs (~960 lines), the main.rs plan-building changes, install.sh/install.ps1 record-writing, and the Gherkin/step-definition additions in tests/e2e-cucumber.
  • Built and ran the new unit tests locally: all 5 rocm-core user_path tests and all 23 apps/rocm uninstall::tests pass (cargo test -p rocm-core user_path, cargo test -p rocm --bin rocm uninstall::).
  • cargo clippy -p rocm -p rocm-core --all-targets -- -D warnings is clean.
  • cargo xtask check-crate-edges passes (no new first-party edge introduced; self-replace is a Windows-only third-party dep, properly added to MANIFEST.md/THIRD_PARTY_NOTICES.txt/Cargo.lock).
  • Ordering/safety logic verified by reading and by test: PATH edits are undone before binaries are removed, and a failed PATH undo leaves the binaries and the .rocm-cli-path-edit record in place for a retry (apps/rocm/src/uninstall.rs execute_plan), exercised by a_path_change_that_cannot_be_undone_stops_before_the_binaries_go and the matching lifecycle-linux-uninstall-path-failure-keeps-binaries e2e scenario.
  • Verified a user's own PATH line is never touched (only the installer's marked block, matched byte-for-byte with quote-boundary checks in remove_profile_block_bytes) and that a profile reached via a symlink is edited at its real target while the link itself is preserved (a_linked_profile_is_edited_where_it_lives_and_stays_a_link).
  • Docs (README.md, docs/manual-testing.md, docs/testing.md, docs/architecture.md, docs/engine-plugins.md) were updated in the same change to match the new behavior, including correctly reclassifying uninstall.rs's extraction pattern now that it owns PathEdit.
  • BDD coverage for the new user-observable behavior is thorough: @id:lifecycle-linux-uninstall-full-purge (updated), plus four new scenarios (lifecycle-23 through lifecycle-26) covering shared-folder PATH retention, Windows user-PATH removal, --keep-binaries retention, and the undo-failure/retry path. The pre-existing XDG-purge check bug (asserting a rocm-cli subdirectory that never existed) is also fixed as called out in the commit message.
  • Leak scan across the diff is clean (the only hits were the word "private" in docs/architecture.md describing Rust visibility, not an actual leak).

I didn't find any blocking issues. A couple of very minor, non-blocking observations for awareness, not requesting changes: other_entries in uninstall.rs treats any non-rocm-cli file in the install folder as "another program" sharing it, which is a conservative/safe default but could occasionally over-trigger on stray files; and the Windows "on PATH but not recorded" informational message reads the current process's PATH env var rather than the persisted registry value, which only affects that FYI wording, not the actual undo logic.

Approving.

@jussielo-amd jussielo-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up to my earlier approval on this PR: a second, independent pass surfaced issues I missed the first time, and after re-verifying each directly against the code (and reproducing two of them), I'm retracting the approval. Sorry for the churn — the latest review should be treated as the current state.

Two confirmed blockers, both in newly-added code in apps/rocm/src/uninstall.rs:

  1. Lost progress output on any failure other than the PATH-edit-undo one. execute_plan (lines 94-133) accumulates every "removed ..." line into a local Vec<String> and only returns it to the caller on full success; uninstall() then does for line in execute_plan(&plan)? { println!("{line}"); }. Only the PATH-edit failure arm (lines 103-118) explicitly reprints the accumulated lines before bailing. Every other failure path — the non-binary removal loop (97-101), the binary removal loop (120-124), install-record removal (125-128), and the Windows self-delete (delete_running_executable(exe)? at 130-131) — propagates its error straight through ?, which drops the Vec<String> (and therefore every already-reported-success line, including a successful PATH undo) with nothing printed. I reproduced this: I added a temporary test that plans a real shell-profile PATH edit (which succeeds, restoring the profile) followed by a binary removal that's forced to fail (read-only bin directory), and confirmed the returned error contains no mention of the PATH-lines removal that had, in fact, already happened — there is no way for the caller to learn about it. Pre-PR, each line was println!'d immediately as each item was removed, so this is a real regression, not pre-existing behavior.

  2. The "binaries were left in place" message doesn't account for already-removed state, and no scenario checks what actually happens to it. The non-binary actions (config/data/cache, lines 97-101) run before the PATH-edit-undo is attempted (103-118). On a plain rocm uninstall --yes with no --keep-* flags, if the PATH edit then fails, config/data/cache have already been permanently deleted, but the bail message only says "The ROCm CLI binaries were left in place." The new lifecycle-26 scenario (tests/e2e-cucumber/features/install_lifecycle.feature) exercising this exact failure path checks that the binaries, profile, and record are preserved, but never asserts what happened to the isolated config/data/cache state, so this interaction is untested either way.

A security-relevant finding I independently reproduced, also in new code: write_replacing (apps/rocm/src/uninstall.rs lines 478-492), used to rewrite a shell profile after stripping the rocm-cli block, creates the replacement at default/umask permissions via fs::write and only applies the original file's mode afterward via fs::set_permissions. I verified with a standalone repro that a 0600 original file is briefly written back as 0644 before the mode fix lands — a narrow but real window where a profile holding a secret (e.g. an exported API key) is world/group-readable to another local user on a shared box.

A few lower-severity, non-blocking items worth tightening in the same area, for awareness rather than as hard blockers: other_entries (used to decide whether another program shares the install folder) re-derives "is this ours" from a hardcoded filename-prefix heuristic (is_rocm_install_entry_name) instead of consulting the authoritative .rocm-cli-manifest that install.sh/install.ps1 already write with the literal list of installed paths — a future binary name that doesn't fit the heuristic, or a third-party file that happens to, is misclassified either way; the "is this the running exe" check in main.rs (~17866, *path == current_exe) is a strict PathBuf equality, while this PR's own e2e step then_running_rocm_gone (tests/e2e-cucumber/tests/e2e/lifecycle_steps.rs) deliberately avoids that same comparison and matches by filename suffix instead, with a comment citing Windows long-vs-8.3-short-path spelling risk — the production check doesn't share that hedge, and a miss there would hit the output-loss bug above; and user_path::remove_user_path_entry's registry read-modify-write has no compare-and-swap (though this mirrors the same unguarded pattern install.ps1's own Add-InstallDirToUserPath already uses for writing the entry, so it's not a new category of risk this PR introduces).

Everything from my first review (test runs, clippy, crate-edges check, doc sync, leak scan) still stands — those were accurate, this is additive.

@siloteemu siloteemu left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Automated review · pr-review-watcher · 68dd067

This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.

Review — needs work

Full review of the whole change.
Implements what its description claims; it names #616, an open docs PR, as the intended behaviour, and leaves stopping background processes to #299. No ticket named.

Blocking

  • new · A failure after the PATH undo no longer reports what was already changed — apps/rocm/src/uninstall.rs:82-84, :94-134
    • On prw-base, uninstall() printed removed … after each remove_path. Now execute_plan saves every line and prints them only if the whole plan succeeds. The one exception is the PATH-undo error branch (:107-109), which prints them before failing.
    • The ? on the config/data/cache loop (:98), the binary loop (:121), the record removal (:126) and delete_running_executable (:131) all throw the saved lines away.
    • So when a binary removal fails (for example a running rocmd.exe on Windows), the user sees only failed to remove …. They are not told that their shell profile or user PATH was already edited, or that config, data and cache were already deleted.
    • Nothing tests a failure after the PATH step.
    • Confidence 85 · logic · Fix: print each line as it is produced, as the base code did, or print the saved lines on every error exit.
  • new · keeping_the_binaries_keeps_the_path_change cannot fail on --keep-binaries — apps/rocm/src/uninstall.rs:686-704, apps/rocm/src/main.rs:17851
    • I replaced if options.keep_binaries { with if false { in build_uninstall_plan, and the test still passed.
    • The reason: the test binary sits in a cargo target/ layout, so is_dev_binary_layout skips binary and PATH planning however the flag is set. The record and profile checks see nothing either way.
    • The PR description lists --keep-binaries under "Unit tests cover", which this test does not.
    • lifecycle-25 does guard the flag, on the Linux @lifecycle lane.
    • Confidence 100 · logic · Fix: test --keep-binaries against a fixture install folder that holds a record and a marked profile (for example by factoring the gate so it takes bin_dir). Otherwise delete the test and its line in the description, and rely on lifecycle-25.
  • new · No test depends on what install.sh writes into the record — install.sh (the record_path_edit calls after append_path_snippet), tests/e2e-cucumber/tests/e2e/lifecycle_steps.rs:390, :449, apps/rocm/src/uninstall.rs:231-251
    • The Linux lifecycle scenarios always write the block to $HOME/.bashrc, and the install folder is <root>/install/bin, which is outside HOME.
    • Uninstall always checks .bashrc as a known profile, and always works out the literal path spelling itself. So neither the profile line nor the path-expr line from install.sh is ever needed. Scenarios 09, 23, 25 and 26 only check that the file exists.
    • If either key in install.sh were misspelled or dropped, every scenario would stay green. Two user-visible cases have no scenario, as AGENTS.md §3 requires:
      • a profile picked through ROCM_CLI_SHELL_PROFILE, which only the record can find;
      • a block spelled $HOME/… that uninstall cannot work out for itself.
    • The unit tests write records by hand, so they cover the parsing side only.
    • Confidence 85 · logic · Fix: add a lifecycle scenario that installs with ROCM_CLI_SHELL_PROFILE pointing at a profile outside the known list, then uninstalls without that variable and checks the profile is restored.
  • new · lifecycle-24 would pass even if the rest of the user PATH were destroyed — tests/e2e-cucumber/tests/e2e/lifecycle_steps.rs:1322-1342, crates/rocm-core/src/user_path.rs:40-171
    • The step checks only two things: the install folder is no longer in the user Path, and the output says from your user PATH.
    • It would stay green if remove_user_path_entry emptied the value, dropped other entries, or rewrote REG_EXPAND_SZ as REG_SZ. The code promises to preserve all three ("keeps the value's registry type"), and the PR description says so too.
    • The registry function has no other test. The #[cfg(test)] tests in user_path.rs cover only the string helper, and the Windows branch of plan_path_edits never runs on Linux.
    • Confidence 85 · logic · Fix: before the install, put an extra entry (one with %VAR%) on the user Path. After uninstall, assert that it is still there, and that the value type is unchanged.
  • new · The branch conflicts with the base — crates/rocm-core/Cargo.toml, docs/architecture.md
    • git merge-tree prw-base HEAD reports content conflicts in both files. The base is 19 commits ahead: it added a different windows-sys feature and rewrote the architecture paragraphs this change edits.
    • After rebasing:
      • re-add Win32_UI_WindowsAndMessaging;
      • regenerate the lock, manifest and notices;
      • re-check how the base's architecture text describes uninstall.rs, since this change adds an owned type (PathEdit).
    • Confidence 100 · mechanical · Fix: rebase onto the current base and resolve.

Non-blocking

  • new · The new section header names something no scenario covers — tests/e2e-cucumber/features/install_lifecycle.feature:279
    • # ── Uninstall: PATH changes and background processes ── sits over four scenarios, and none of them concerns background processes.
    • Confidence 85 · mechanical · Fix: drop "and background processes".
  • new · other_entries errs toward removing the PATH change — apps/rocm/src/uninstall.rs:416-428
    • Three cases are treated as "no other programs here", so the PATH change is undone under a program that may still need it:
      • read_dir fails (it returns an empty list);
      • an entry cannot be read (.flatten() drops it);
      • a file name is not UTF-8 (filter_map(to_str) drops it).
    • The same function handles unreadable profiles and the record file the other way, keeping the change.
    • Confidence 70 · logic · Fix: use to_string_lossy, and on a read error keep the change and add a warning.
  • new · The install.sh comment says more than its check does — install.sh ("Only the installer's own block is recorded")
    • The guard is a grep for the start marker, which also matches a block written for a different install folder.
    • append_path_snippet already reports unchanged in that case, so the record can name a profile and path-expr that are not this install's. Uninstall copes: it reports "no longer has the rocm-cli PATH lines".
    • Confidence 70 · mechanical · Fix: match the block for this folder's spelling, or reword the comment.

Decisions for the author

  • Order of removal — tradeoff
    • Config, data and cache are deleted before the PATH undo. When the undo fails, the error message and the README mention only that the binaries were kept.
    • On Windows, a binary removal that fails (for example a running rocmd.exe) now comes after the user PATH entry is already gone. rocm.exe is then left off PATH. A retry still works through the full path, because the record outlives the failure.
    • Undoing PATH first and deleting state last would make every failure fully retryable, but would delete state later.
  • The record stays in a shared folder that has no uninstaller left — tradeoff
    • The comment at uninstall.rs:184-185 says the record stays "so a later uninstall still knows". But the same run removes rocm and rocmd, so the record is only read again after a reinstall. Until then it is a stray file in the user's ~/.local/bin.
    • The alternative is to remove it and rely on the scan of known profiles, which on Linux does not need the record.
  • Windows upgrades from installs that predate this change — tradeoff
    • install.ps1 records the entry only when it adds it. Rerunning the installer to upgrade an older install, as the README suggests, never records the entry, so uninstall never removes it. On Linux, the marked block is found anyway.
    • The PR states this limit. Confirm it is acceptable for the most common existing Windows users.

Positive signals

  • Shell profiles are edited as bytes, with an atomic replace that keeps permissions, and a symlinked profile is edited where it points. Non-UTF-8 and dotfile-managed profiles therefore keep everything outside the block.
  • The fixed XDG purge step replaces a check that could never fail: on prw-base, it tested a rocm-cli subdirectory that was never created.

Deployment notes

  • Adds a Windows-only dependency, self-replace 1.5 (Apache-2.0), and the Win32_UI_WindowsAndMessaging feature of windows-sys in rocm-core.
  • Both installers now write .rocm-cli-path-edit into the install folder. Older installers do not write it.

What this covered

Read every changed file in full, together with the install.sh and install.ps1 PATH functions, build_uninstall_plan and its helpers, storage.rs's use of UninstallPlan, and the CI lanes that run the lifecycle features. Range: merge base 0d85577…68dd067e, compared with prw-base 8e2e890.

What ran:

  • The rocm uninstall unit tests (40 passed).
  • One mutation of the --keep-binaries gate.
  • A read-only git merge-tree against the base.
  • CI on 68dd067: every check passed or was skipped.

Not run:

  • The Windows code paths (registry edit, self-delete) and the lifecycle e2e suite, which needs packaging and a Windows host. Those findings come from reading the code.
  • cargo xtask manifest --check and tpn --check.

The design review (Step 7) was done by the driver from the workers' investigation, not by separate design workers. The four review passes, history, prior changes on earlier PRs, code comments, and the instruction file (AGENTS.md) all ran as independent workers.

This review was not reconciled against any discussion on the PR.

…ng exe

`rocm uninstall` left two things of ROCm CLI behind:

- On Windows it skipped the running rocm.exe, so the program that ran
  uninstall stayed in the install folder.
- On every platform the installer's PATH change survived: the marked
  block install.sh appends to a shell profile, and the user Path entry
  install.ps1 adds.

The installers now record the PATH change they made in
.rocm-cli-path-edit, and uninstall undoes it: the marked profile block
is removed (the profile reads back as it did before the install, and a
profile kept as a link stays a link), or the entry is taken off the
HKCU user Path with its registry type kept. A PATH line the user wrote
is never touched, and the change stays when other programs share the
install folder or with --keep-binaries. Marked blocks from installs
that predate the record, or from an earlier install under another
shell, are found and removed too. The PATH change is undone before the
binaries go, so a failure leaves rocm and the record in place to retry.

The running rocm.exe is deleted with self-replace, which moves it out
of the install folder and deletes it once the process exits.

Lifecycle scenarios cover the profile restore, the shared folder,
--keep-binaries, the failed-undo retry, the Windows user Path, and the
running rocm.exe. The XDG purge check now asserts the directories
themselves are gone; it used to check a subdirectory that never existed.

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
… runs

Review follow-up.

Order and reporting:
- The installer's PATH change is now undone before anything is removed,
  so a change that cannot be undone leaves the config, data, cache, and
  binaries all in place, and the message says nothing else was removed.
- Each step is printed as it finishes. A later failure (a binary Windows
  will not unlink, say) no longer hides the PATH undo and the removals
  that already happened.
- A profile the installer recorded writing to that cannot be read is now
  planned as an undo, so the uninstall stops there before removing
  anything, instead of removing the binaries and stranding the record.

Correctness:
- The replacement profile is created with the original's permissions,
  never wider, instead of at the umask default and narrowed afterwards.
  A replacement left by an interrupted run no longer blocks the next.
- The record and the folder spellings are read as bytes, so a home or
  profile path that is not UTF-8 is still matched.
- Another program in the install folder is detected even when its name
  is not UTF-8, and a folder that cannot be listed keeps the PATH change.
- The running rocm.exe is matched case-insensitively.
- --keep-binaries now says the PATH change stays with the binaries, and
  a shared folder names the record it keeps.
- install.sh records a profile only when it holds a closed marked block
  for this folder. install.ps1 writes its record before changing the
  user Path, so an interrupted install never leaves it unrecorded.

Tests:
- The --keep-binaries unit test now plans against an installed folder
  holding a record and a marked profile; it could not fail before.
- lifecycle-24 adds an expandable %VARIABLE% entry to the user Path and
  checks uninstall keeps it and the REG_EXPAND_SZ type. The harness now
  captures and restores the user Path with its registry type, and the
  two scenarios that change it are @serial.
- lifecycle-26 checks the isolated state survives the failed undo.
- lifecycle-27 installs through a linked folder into a profile chosen
  with ROCM_CLI_SHELL_PROFILE, so only the installer's record can lead
  uninstall to the block.

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
@rominf
rominf force-pushed the fix/uninstall-remove-running-exe-and-path branch from 68dd067 to 6670305 Compare October 14, 2026 11:24
@rominf

rominf commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main 287e08a8; the head is now 6670305f. Main had moved code this PR edits, so two of its edits are re-applied in main's new form. Both PRs added a feature to crates/rocm-core/Cargo.toml, and both features are kept. #624/#653 rewrote docs/architecture.md, and this PR's two edits there now sit in main's per-file list. The PR's other added and removed lines are unchanged by the rebase. The review fixes are in 6670305f; each new test fails when its fix is reverted.

@jussielo-amd

  • Lost progress output: fixed. Each step is printed as it finishes, so a late failure no longer hides the earlier ones. Test: a_failure_after_the_path_undo_still_reports_what_was_done.
  • "Binaries were left in place" after state was already gone: fixed by the order. The PATH change is now undone before anything is removed, and the message says "Nothing else was removed". The unit test and lifecycle-26 now assert that config, data and cache survive.
  • Profile briefly at 0644: fixed. The replacement is created with the original's mode (create_new), never wider. A leftover replacement from an interrupted run no longer blocks the next one. Tests: the_replacement_profile_is_never_more_readable_than_the_original and a_replacement_left_by_an_interrupted_uninstall_does_not_block_the_next.
  • other_entries heuristic vs .rocm-cli-manifest: keeping the heuristic. Uninstall removes files by the same name rule, and the sharing check has to agree with what it removes. Moving both to the manifest can be a follow-up.
  • Running-exe equality: now case-insensitive on Windows. Test: the_running_executable_is_matched_whatever_case_windows_reports_it_in.
  • Registry read-modify-write: a registry value has no compare-and-swap, and the installer has the same window. Leaving it.
  • Process PATH in the "not recorded" note: the note only describes the situation and changes nothing. Leaving it.

@siloteemu (automated review)

  • Output lost after the PATH undo: fixed, as above.
  • The --keep-binaries test could not fail: the binary-removal decision is now a separate function. The test runs a plan against an installed folder that holds a record and a marked profile, with and without the flag. It now fails if the flag check is made to never fire.
  • Nothing depended on what install.sh records: new lifecycle-27 (@id:lifecycle-linux-uninstall-follows-the-installer-record). The install folder is reached through a link, the profile is chosen with ROCM_CLI_SHELL_PROFILE, and uninstall runs without that variable. Misspelling either record key makes it fail.
  • lifecycle-24 would not catch the rest of the user PATH being lost: it now adds a %VAR% entry as REG_EXPAND_SZ after the install. It checks that the entry survives and the value keeps its type. The harness now captures and restores the Path with its registry type; before, it flattened an expandable Path to REG_SZ. lifecycle-13 and lifecycle-24 are @serial.
  • Conflicts: rebased.
  • "Background processes" section header: renamed to "Uninstall: the installer's PATH change".
  • other_entries erred toward removal: a folder or entry that cannot be read now keeps the PATH change and adds a warning. Non-UTF-8 names count as other programs.
  • The install.sh comment said more than its check did: the installer now records a profile only when it holds a closed marked block for this folder, which matches the comment.
  • Order of removal: the PATH change goes first, as suggested.
  • Record left in a shared folder: kept on purpose. On Windows it is the only proof that the installer added the entry. The review note now names it.
  • Windows installs from before the record: an accepted limit, stated in the description.

@juhovainio: replied inline. The description no longer claims #616 describes this behaviour.

Two smaller fixes came out of the Windows clippy run: OsStr was unused on Windows, and a test helper was dead code there.

Verified locally on the rebased head:

  • fmt;
  • rocm --bin (1196 passed), rocm-core (666 passed);
  • feature_naming, cargo check --test e2e, xtask manifest --check;
  • clippy -D warnings, both invocations, for Linux and for the Windows target;
  • the Linux lifecycle scenarios: 13 of 13 (01–09, 23, 25, 26, 27), run on the head before the final rebase.

Deliberate type errors injected into the cfg(windows) code failed the Windows clippy run. That shows the Windows check really compiles it. Not verified locally: the Windows runtime behaviour (registry edit, self-delete, lifecycle-22 and lifecycle-24). The Windows lane is its first run.

@rominf
rominf dismissed stale reviews from juhovainio, jussielo-amd, jussielo-amd, and siloteemu October 14, 2026 11:26

Stale: approved 68dd067; the head is now 6670305. Since then: rebase onto 287e08a, plus 6670305, which changes the order of the PATH undo, the output streaming, profile permissions, record parsing, and adds lifecycle-27. All verified locally; a fresh review is needed to merge.

@jussielo-amd jussielo-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 6670305f against the two confirmed blockers and the security finding from my prior review, plus the automated review's blockers.

Verified directly against the code (not just the reply text):

  • Lost progress output: execute_plan now takes a report: &mut dyn FnMut(String) callback and calls it per-step as each edit/removal completes, instead of buffering into a Vec that only got printed on full success. The PATH-undo loop runs first, before any removal. New test a_failure_after_the_path_undo_still_reports_what_was_done forces a late binary-removal failure and asserts the PATH-undo and config-removal lines are still present in the output — confirmed this actually exercises the code path.
  • Permission window on profile rewrite: create_replacement now opens the temp file with create_new and sets the target mode directly via OpenOptionsExt::mode, so the file never exists at umask-derived permissions before narrowing. the_replacement_profile_is_never_more_readable_than_the_original pins this at the unit level, and a_replacement_left_by_an_interrupted_uninstall_does_not_block_the_next covers the stale-tempfile case.
  • --keep-binaries test that couldn't fail: now calls plan_installed_binaries directly against a layout holding a real record and marked profile, rather than going through is_dev_binary_layout's bypass. Asserts the flag both keeps the PATH edit/record and reports it in the skip note. Confirmed the test topology no longer depends on the dev-layout short-circuit the finding identified.
  • Branch conflicts with base: rebased onto 287e08a8; windows-sys's feature list carries both this PR's Win32_UI_WindowsAndMessaging and whatever main had added, and docs/architecture.md reads clean.
  • Spot-checked the other siloteemu/juhovainio items addressed in the reply (case-insensitive running-exe match, install.sh record ordering before SetEnvironmentVariable, byte-level record parsing, other_entries erring toward keeping the PATH change on an unreadable entry, lifecycle-27 and the @serial tags) — each matches what the reply describes.

The remaining items (manifest-vs-heuristic for other_entries, registry read-modify-write, the informational PATH note reading the process env) are reasonable trade-offs with stated rationale, not blockers.

CI is green across the board; only the MI300X hardware lane is still pending.

No further blocking issues from me. Approving.

@rominf
rominf added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 12342b3 Oct 15, 2026
30 of 31 checks passed
@rominf
rominf deleted the fix/uninstall-remove-running-exe-and-path branch October 15, 2026 09:03
r0x0r added a commit that referenced this pull request Oct 15, 2026
#636 rewrote the e2e user-PATH restore to go through the registry
directly so it keeps the value's REG_EXPAND_SZ/REG_SZ kind, while this
branch had refactored the old `[Environment]::SetEnvironmentVariable`
restore into a `write_user_path` helper. Kept #636's `restore_user_path`
and `capture_raw_user_path` -- they are strictly more correct -- and kept
`write_user_path` as a stage-only helper for `stage_user_path_for_test`,
which deliberately wants the installer's own write path. Retargeted the
staging capture at `capture_raw_user_path` for #636's new
`Option<CapturedUserPath>` field type.

Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
fredespi added a commit that referenced this pull request Oct 15, 2026
Keep main's installer PATH undo and running-executable deletion (#636) together with the stop-before-remove gate: the stop pass and its gate run first, then execute_plan_with undoes the PATH changes and removes entries, reporting removed / failed and possibly partial / not attempted on a failure. Our lifecycle scenarios keep numbers 10-29; main's new ones become 30-34.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants