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
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ repository.
| security-framework | 2.11.1 | MIT OR Apache-2.0 |
| security-framework | 3.7.0 | MIT OR Apache-2.0 |
| security-framework-sys | 2.17.0 | MIT OR Apache-2.0 |
| self-replace | 1.5.0 | Apache-2.0 |
| semver | 1.0.28 | MIT OR Apache-2.0 |
| serde | 1.0.228 | MIT OR Apache-2.0 |
| serde_core | 1.0.228 | MIT OR Apache-2.0 |
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ add a demo. Until the demo-gifs workflow has run once, these links 404.

The installer downloads a prebuilt bundle, verifies its SHA-256 checksum,
installs the `rocm` and `rocmd` binaries into `~/.local/bin`, and adds that
directory to your shell `PATH`. Rerun it any time to upgrade.
directory to your shell `PATH`. Rerun it any time to upgrade. `rocm uninstall`
removes the binaries, and undoes the `PATH` change too when no other program
lives in that directory.

### Linux and WSL (x86_64)

Expand Down Expand Up @@ -946,7 +948,20 @@ rocm logs [--service <service-id>] [--search TERM ...]

rocm uninstall [--yes] [--dry-run]
[--keep-binaries] [--keep-config] [--keep-data] [--keep-cache]
```
[--force-dev-binaries]
```

`rocm uninstall` lists what it will do and asks before doing it (`--yes` skips
the question, `--dry-run` only lists). It removes the config, data, and cache
directories and the installed binaries, including the running `rocm.exe` on
Windows. It also undoes the `PATH` change the installer made: the marked block
in your shell profile, or the user `PATH` entry the Windows installer recorded
adding. That change stays when other programs live in the same folder, since
they may rely on it (the review names them), and with `--keep-binaries`. A
`PATH` line you wrote yourself is never touched. The `PATH` change is undone
first: if it cannot be, nothing is removed, so you can fix the cause and run
`rocm uninstall` again. Each step is reported as it finishes, so if a later
step fails, the output still shows what was already removed.

On Linux, if a config, data, or cache folder is a link to somewhere else (for
example a cache moved to a bigger disk), `rocm uninstall` removes the link and
Expand Down
1 change: 1 addition & 0 deletions THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5846,6 +5846,7 @@ The following component(s) are licensed under Apache License 2.0:
- security-framework-sys 2.17.0 <https://github.com/kornelski/rust-security-framework>
- security-framework 2.11.1 <https://github.com/kornelski/rust-security-framework>
- security-framework 3.7.0 <https://github.com/kornelski/rust-security-framework>
- self-replace 1.5.0 <https://github.com/mitsuhiko/self-replace>
- serde_with 3.21.0 <https://github.com/jonasbb/serde_with/>
- serde_with_macros 3.21.0 <https://github.com/jonasbb/serde_with/>
- signal-hook-mio 0.2.5 <https://github.com/vorner/signal-hook>
Expand Down
1 change: 1 addition & 0 deletions apps/rocm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ ureq = { version = "2.12", features = ["native-certs"] }
libc.workspace = true

[target.'cfg(windows)'.dependencies]
self-replace = "1.5"
windows-native-keyring-store = "1.1"
windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem"] }

Expand Down
128 changes: 96 additions & 32 deletions apps/rocm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,14 +648,20 @@ rocm logs --search error timeout")]
command: BenchCommand,
},
/// Remove ROCm CLI-managed files from this computer.
///
/// Removes the config, data, and cache directories and the installed
/// binaries, including the running rocm.exe on Windows. It also undoes the
/// PATH change the installer made (its marked shell-profile block, or the
/// user PATH entry it recorded adding on Windows), unless other programs
/// share the install folder.
Uninstall {
/// Do not ask for interactive confirmation.
#[arg(long)]
yes: bool,
/// Show what would be removed without deleting anything.
#[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.

#[arg(long)]
keep_binaries: bool,
/// Keep saved settings.
Expand Down Expand Up @@ -13595,38 +13601,24 @@ struct UninstallPlan {
actions: Vec<UninstallPlanEntry>,
skipped: Vec<String>,
warnings: Vec<String>,
/// Installer PATH changes `rocm uninstall` undoes.
path_edits: Vec<uninstall::PathEdit>,
/// The installer's record of its PATH change, removed once that change is
/// undone.
path_record: Option<PathBuf>,
/// The running executable, on Windows, which is deleted as the command exits.
self_delete: Option<PathBuf>,
}

fn build_uninstall_plan(paths: &AppPaths, options: &UninstallOptions) -> Result<UninstallPlan> {
let mut plan = UninstallPlan::default();

if options.keep_binaries {
plan.skipped
.push("binary removal disabled by --keep-binaries".to_owned());
} else {
let current_exe =
daemon_binary_path().context("failed to discover current rocm executable")?;
if is_dev_binary_layout(&current_exe) && !options.force_dev_binaries {
plan.skipped.push(format!(
"binary removal skipped because {} looks like a cargo target build; pass --force-dev-binaries to remove sibling debug/release binaries",
current_exe.display()
));
} else {
for path in collect_installed_binary_candidates(&current_exe)? {
if rocm_core::runtime_is_windows() && path == current_exe {
plan.skipped.push(format!(
"skipping running executable on Windows: {}",
path.display()
));
continue;
}
plan.actions.push(UninstallPlanEntry {
kind: "binary",
path,
});
}
}
}
plan_installed_binaries(
&mut plan,
options,
|| daemon_binary_path().context("failed to discover current rocm executable"),
rocm_core::runtime_home_dir().as_deref(),
)?;

for (keep, kind, path) in [
(options.keep_config, "config", paths.config_dir.clone()),
Expand Down Expand Up @@ -13713,6 +13705,76 @@ fn build_uninstall_plan(paths: &AppPaths, options: &UninstallOptions) -> Result<
Ok(plan)
}

/// Plan removing the installed binaries beside the running one, which
/// `current_exe` names, and undoing the installer's PATH change for their
/// folder. `--keep-binaries` keeps both, and so does a cargo build's folder.
fn plan_installed_binaries(
plan: &mut UninstallPlan,
options: &UninstallOptions,
current_exe: impl FnOnce() -> Result<PathBuf>,
home: Option<&Path>,
) -> Result<()> {
if options.keep_binaries {
// The PATH change only serves the binaries, so it stays with them.
plan.skipped.push(
"binary removal disabled by --keep-binaries, so any PATH change the installer made \
stays too"
.to_owned(),
);
return Ok(());
}
let current_exe = current_exe()?;
if is_dev_binary_layout(&current_exe) && !options.force_dev_binaries {
plan.skipped.push(format!(
"binary removal skipped because {} looks like a cargo target build; pass --force-dev-binaries to remove sibling debug/release binaries",
current_exe.display()
));
return Ok(());
}
let binaries = collect_installed_binary_candidates(&current_exe)?;
for path in &binaries {
// Windows will not unlink a running image; it is deleted as the
// command exits instead.
if is_running_executable(path, &current_exe, cfg!(windows)) {
plan.self_delete = Some(path.clone());
continue;
}
// The PATH record goes only once its change is undone; see
// `uninstall::plan_path_edits`.
if path.file_name().and_then(|name| name.to_str()) == Some(uninstall::PATH_EDIT_RECORD) {
continue;
}
plan.actions.push(UninstallPlanEntry {
kind: "binary",
path: path.clone(),
});
}
if let Some(bin_dir) = current_exe.parent() {
uninstall::plan_path_edits(plan, bin_dir, &binaries, home);
}
Ok(())
}

/// Whether `path`, an entry of the running executable's folder, is the running
/// executable itself, which only Windows has to treat specially. Windows names
/// are case-insensitive, and the running image may report its own name in
/// another case, or in its 8.3 short form (`ROCM.EXE`), than the folder listing
/// does; missing it would plan an ordinary removal that Windows refuses.
fn is_running_executable(path: &Path, current_exe: &Path, windows: bool) -> bool {
if !windows {
return false;
}
path == current_exe
|| (path.parent() == current_exe.parent()
&& path
.file_name()
.zip(current_exe.file_name())
.is_some_and(|(name, exe)| {
name.to_string_lossy()
.eq_ignore_ascii_case(&exe.to_string_lossy())
}))
}

pub(crate) fn render_uninstall_dry_run(paths: &AppPaths) -> Result<String> {
let options = UninstallOptions {
dry_run: true,
Expand All @@ -13726,14 +13788,16 @@ fn render_uninstall_plan(plan: &UninstallPlan, options: &UninstallOptions) -> St
let mut output = String::new();
let _ = writeln!(output, "Uninstall review");
let _ = writeln!(output);
if plan.actions.is_empty() {
let removals = uninstall::removal_count(plan);
if uninstall::plan_is_empty(plan) {
let _ = writeln!(output, "Nothing managed by rocm-cli would be removed.");
} else {
let _ = writeln!(output, "{} item(s) would be removed:", plan.actions.len());
} else if removals > 0 {
let _ = writeln!(output, "{removals} item(s) would be removed:");
for entry in &plan.actions {
let _ = writeln!(output, " - {}: {}", entry.kind, entry.path.display());
}
}
uninstall::render_extra_plan(&mut output, plan);
if !plan.warnings.is_empty() {
let _ = writeln!(output);
let _ = writeln!(output, "Please review:");
Expand Down Expand Up @@ -13863,7 +13927,7 @@ fn collect_installed_binary_candidates(current_exe: &Path) -> Result<Vec<PathBuf
}

fn is_rocm_install_entry_name(name: &str) -> bool {
if name == ".rocm-cli-manifest" {
if name == ".rocm-cli-manifest" || name == uninstall::PATH_EDIT_RECORD {
return true;
}
let normalized = name.strip_suffix(".exe").unwrap_or(name);
Expand Down
Loading
Loading