Skip to content
Open
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 MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,13 @@ manages, on the order of several GB per SDK version installed. It is removed by
`rocm uninstall` unless `--keep-data` is passed, and its location can be
overridden with `ROCM_CLI_UV_CACHE_DIR`.

When a managed Python interpreter is needed, `uv python install` downloads a
standalone CPython build. That download is also kept in the managed data
directory (at `<data-dir>/uv-python`) rather than `uv`'s own default of
`$HOME/.local/share/uv/python/`. It is removed by `rocm uninstall` unless
`--keep-data` is passed, and its location can be overridden with
`ROCM_CLI_UV_PYTHON_INSTALL_DIR`.

### Lemonade Embeddable Runtime

When `rocm engines install lemonade` is run, the CLI downloads a prebuilt
Expand Down
98 changes: 93 additions & 5 deletions apps/rocm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use rocm_core::{
read_tcp_stream_to_string, resolve_builtin_model_recipe, resolve_model_recipe,
runtime_install_root_is_protected, runtime_path_is_same_or_inside,
runtime_python_activation_hint, runtime_python_env_bin_dir, runtime_python_executable_in_env,
shell_command_for_host, uv_cache_source, write_all_tcp_stream,
shell_command_for_host, uv_cache_source, uv_python_install_dir_source, write_all_tcp_stream,
};
use rocm_engine_protocol::{
DEFAULT_LOG_TAIL_LINES, DetectRequest, DetectResponse, DevicePolicy,
Expand Down Expand Up @@ -481,12 +481,13 @@ rocm logs --search error timeout")]
/// Keep saved settings.
#[arg(long)]
keep_config: bool,
/// Keep app data such as logs, services, engines, and the uv package cache
/// (often the largest directory rocm-cli manages).
/// Keep app data such as logs, services, engines, the uv package cache, and the
/// uv-managed Python interpreter (often the largest directories rocm-cli manages).
#[arg(long)]
keep_data: bool,
/// Keep caches under the cache directory. Does not cover the uv package cache,
/// which lives under the data directory; use --keep-data for that.
/// Keep caches under the cache directory. Does not cover the uv package cache or
/// the uv-managed Python interpreter, which live under the data directory; use
/// --keep-data for those.
#[arg(long)]
keep_cache: bool,
/// Allow removing development binaries inside the current build tree.
Expand Down Expand Up @@ -1019,6 +1020,7 @@ fn main() -> Result<()> {

maybe_migrate_legacy_dashboard_config();
maybe_notice_legacy_uv_cache();
maybe_notice_legacy_uv_python_install_dir();

let raw_args: Vec<String> = std::env::args().skip(1).collect();
if raw_args.is_empty() {
Expand Down Expand Up @@ -1097,6 +1099,53 @@ fn maybe_notice_legacy_uv_cache() {
let _ = fs::write(&marker, b"");
}

/// Legacy `uv`-managed Python install location, used before it was colocated with the
/// managed data directory. Kept relative so the check works on every platform's home dir.
const LEGACY_UV_PYTHON_INSTALL_DIR_RELATIVE: [&str; 4] = [".local", "share", "uv", "python"];

/// One-shot notice that pre-colocation `uv`-managed Python interpreters are still
/// occupying space at the default `uv` location. Nothing is migrated or deleted: removing
/// it is the user's call. Silent when the managed dir does not exist yet (nothing has
/// moved) or when an override is in effect.
fn maybe_notice_legacy_uv_python_install_dir() {
let Ok(paths) = AppPaths::discover() else {
return;
};
let install_dir = uv_python_install_dir_source(&paths);
if install_dir.is_override() {
return;
}
// Only worth mentioning once the managed dir is actually in use; otherwise the legacy
// directory is simply still being used by other tools.
if !install_dir.path().is_dir() {
return;
}
let Some(home) = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
else {
return;
};
let legacy = LEGACY_UV_PYTHON_INSTALL_DIR_RELATIVE
.iter()
.fold(home, |dir, part| dir.join(part));
if !legacy.is_dir() {
return;
}
// One-shot: a standing reminder on every invocation would be noise, and the user may
// reasonably decide to keep the legacy interpreters for other uv projects.
let marker = paths.data_dir.join(".legacy-uv-python-install-dir-notice");
if marker.exists() {
return;
}
eprintln!(
"rocm: the uv-managed Python interpreter now lives at {}; the previous location at {} is no longer used by rocm-cli and can be removed if no other uv project needs it",
install_dir.path().display(),
legacy.display()
);
let _ = fs::write(&marker, b"");
}

/// One-shot, best-effort migration of a legacy rocm-dash `config.toml` into the
/// unified `config.json`. Prints a notice when a migration runs;
/// never fails startup if the legacy file is malformed.
Expand Down Expand Up @@ -15769,6 +15818,20 @@ fn shared_cache_candidates() -> Vec<(PathBuf, &'static str)> {
));
}

// Same reasoning for the standalone interpreters `uv python install` downloads. The
// managed location sits under the data directory and is filtered out by
// `shared_cache_notes_for`; this only surfaces when it has been pointed elsewhere.
let uv_python = env_path("UV_PYTHON_INSTALL_DIR").or_else(|| {
rocm_core::runtime_home_dir()
.map(|home| home.join(".local").join("share").join("uv").join("python"))
});
if let Some(path) = uv_python {
candidates.push((
path,
"uv-managed Python interpreters are shared with other uv projects on this computer and are",
));
}

let hf_cache = env_path("HF_HOME")
.map(|home| home.join("hub"))
.or_else(|| env_path("HUGGINGFACE_HUB_CACHE"))
Expand Down Expand Up @@ -17217,6 +17280,31 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}

/// The uv-managed Python interpreters are now the other large thing rocm-cli causes
/// to be downloaded, so uninstall must account for them the same way it does the uv
/// cache. Drives the real candidate list rather than a hand-made one, so deleting the
/// production code fails this test.
#[test]
fn shared_cache_candidates_include_the_uv_python_install_dir() {
let lock = super::BUILTIN_ENGINE_ENV_LOCK.get_or_init(|| Mutex::new(()));
let _guard = lock.lock().expect("env lock poisoned");
let overridden = std::env::temp_dir().join(format!("rocm-uv-py-{}", std::process::id()));
let _scoped = super::ScopedEnvVar::set_path("UV_PYTHON_INSTALL_DIR", &overridden);

let candidates = super::shared_cache_candidates();
let entry = candidates
.iter()
.find(|(path, _)| path == &overridden)
.unwrap_or_else(|| {
panic!("UV_PYTHON_INSTALL_DIR missing from uninstall candidates: {candidates:?}")
});
assert!(
entry.1.contains("uv-managed Python"),
"unexpected description: {}",
entry.1
);
}

/// A cache that does not exist is not worth mentioning.
#[test]
fn shared_cache_notes_ignore_missing_paths() {
Expand Down
20 changes: 19 additions & 1 deletion apps/rocm/src/therock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use rocm_core::{
};
#[cfg(test)]
use rocm_core::{
generate_rsa_signing_keypair, managed_uv_cache_dir, sign_rsa_pkcs1_sha256_signature,
generate_rsa_signing_keypair, managed_uv_cache_dir, managed_uv_python_install_dir,
sign_rsa_pkcs1_sha256_signature,
};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
Expand Down Expand Up @@ -3642,6 +3643,23 @@ mod tests {
assert!(cache.starts_with(&paths.data_dir));
}

#[test]
fn uv_python_install_dir_does_not_follow_a_prefix_install_root() {
// Same documented non-goal as `uv_cache_does_not_follow_a_prefix_install_root`:
// `--prefix` moves install_root only, while the interpreters stay keyed off the
// data dir. Pins the claim `docs/manual-testing.md` makes about `uv-python`.
let (_root, paths) = test_paths("prefix-uv-python");
let prefix_root = PathBuf::from("/mnt/elsewhere/envs/my-env");
let install_dir = managed_uv_python_install_dir(&paths.data_dir);

assert!(
!install_dir.starts_with(&prefix_root),
"python install dir {} unexpectedly followed the --prefix root",
install_dir.display()
);
assert!(install_dir.starts_with(&paths.data_dir));
}

#[test]
fn managed_python_defaults_to_312() {
assert_eq!(DEFAULT_MANAGED_PYTHON_VERSION, "3.12");
Expand Down
32 changes: 17 additions & 15 deletions crates/rocm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,25 @@ pub use runtime::{
RuntimeHost, RuntimePlatform, current_executable_path, default_cache_dir, default_config_dir,
default_data_dir, default_interactive_shell_program, managed_logs_dir, managed_pip_cache_dir,
managed_runtime_cache_dir, managed_runtime_data_root, managed_tools_dir, managed_uv_cache_dir,
normalize_runtime_path_for_host, normalize_runtime_path_for_storage,
normalize_runtime_path_text_for_host, normalize_runtime_path_text_for_platform,
normalize_runtime_path_text_for_storage, platform_binary_name, prepend_runtime_path,
runtime_directory_label, runtime_drive_root_for_key, runtime_drive_roots, runtime_exe_suffix,
runtime_home_dir, runtime_install_root_is_protected, runtime_is_linux, runtime_is_windows,
runtime_os_name, runtime_path_for_child, runtime_path_for_windows_child,
runtime_path_is_same_or_inside, runtime_path_list_join, runtime_path_list_split,
runtime_path_sort_key, runtime_path_text_is_absolute_for_host,
runtime_path_text_is_absolute_for_platform, runtime_paths_equivalent,
runtime_python_activation_hint, runtime_python_activation_script, runtime_python_bin_dir_name,
runtime_python_env_bin_dir, runtime_python_executable_in_env, runtime_python_executable_name,
runtime_rocm_library_filename, shell_command_for_host,
managed_uv_python_install_dir, normalize_runtime_path_for_host,
normalize_runtime_path_for_storage, normalize_runtime_path_text_for_host,
normalize_runtime_path_text_for_platform, normalize_runtime_path_text_for_storage,
platform_binary_name, prepend_runtime_path, runtime_directory_label,
runtime_drive_root_for_key, runtime_drive_roots, runtime_exe_suffix, runtime_home_dir,
runtime_install_root_is_protected, runtime_is_linux, runtime_is_windows, runtime_os_name,
runtime_path_for_child, runtime_path_for_windows_child, runtime_path_is_same_or_inside,
runtime_path_list_join, runtime_path_list_split, runtime_path_sort_key,
runtime_path_text_is_absolute_for_host, runtime_path_text_is_absolute_for_platform,
runtime_paths_equivalent, runtime_python_activation_hint, runtime_python_activation_script,
runtime_python_bin_dir_name, runtime_python_env_bin_dir, runtime_python_executable_in_env,
runtime_python_executable_name, runtime_rocm_library_filename, shell_command_for_host,
};
pub use uv::{
DEFAULT_UV_TIMEOUT_SECS, UV_CACHE_DIR_ENV, UV_CACHE_DIR_OVERRIDE_ENV, UvCacheSource,
ensure_uv_binary, uv_binary_name, uv_cache_source, uv_command_env, uv_http_timeout_secs,
uv_pip_freeze_args, uv_pip_install_base, uv_venv_args,
DEFAULT_UV_TIMEOUT_SECS, UV_CACHE_DIR_ENV, UV_CACHE_DIR_OVERRIDE_ENV,
UV_PYTHON_INSTALL_DIR_ENV, UV_PYTHON_INSTALL_DIR_OVERRIDE_ENV, UvCacheSource,
UvPythonInstallDirSource, ensure_uv_binary, uv_binary_name, uv_cache_source, uv_command_env,
uv_http_timeout_secs, uv_pip_freeze_args, uv_pip_install_base, uv_python_install_dir_source,
uv_venv_args,
};

pub const DEFAULT_LOCAL_PORT: u16 = 11_435;
Expand Down
9 changes: 9 additions & 0 deletions crates/rocm-core/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@ pub fn managed_uv_cache_dir(root: &Path) -> PathBuf {
normalize_runtime_path_for_host(root).join("uv-cache")
}

/// Standalone CPython interpreters downloaded by `uv python install`, kept under the managed
/// root for the same reason as `managed_uv_cache_dir`.
///
/// Without this, `uv` falls back to `$HOME/.local/share/uv/python/`, leaking outside the
/// managed root.
pub fn managed_uv_python_install_dir(root: &Path) -> PathBuf {
normalize_runtime_path_for_host(root).join("uv-python")
}

pub fn managed_logs_dir(root: &Path) -> PathBuf {
normalize_runtime_path_for_host(root).join("logs")
}
Expand Down
Loading