diff --git a/MANIFEST.md b/MANIFEST.md index 0c78300f..4357b8fc 100644 --- a/MANIFEST.md +++ b/MANIFEST.md @@ -671,6 +671,15 @@ official uv GitHub releases at `https://github.com/astral-sh/uv/releases/`. The binary is cached in the rocm-cli managed data directory and reused for subsequent operations. The version may be pinned via `ROCM_CLI_UV_VERSION`. +`uv`'s own content-addressed package cache is also kept in the managed data +directory (at `/uv-cache`), so that it shares a filesystem with the +environments `uv` populates and packages can be hardlinked into them instead of +copied. This cache holds every wheel `uv` downloads — the ROCm SDK and the +torch stack included — so it is typically the largest directory rocm-cli +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`. + ### Lemonade Embeddable Runtime When `rocm engines install lemonade` is run, the CLI downloads a prebuilt diff --git a/apps/rocm/src/comfyui.rs b/apps/rocm/src/comfyui.rs index 0d21a491..fdc1d6ea 100644 --- a/apps/rocm/src/comfyui.rs +++ b/apps/rocm/src/comfyui.rs @@ -348,6 +348,7 @@ pub(crate) fn install( let uv = ensure_uv_binary(paths) .context("failed to acquire uv binary for ComfyUI dependency install")?; run_uv_logged_command( + paths, &uv, uv_install_args(&runtime.python, &packages), Some(&runtime_env), @@ -1400,6 +1401,7 @@ fn uv_install_args(venv_python: &Path, packages: &[String]) -> Vec { } fn run_uv_logged_command( + paths: &AppPaths, uv: &Path, args: Vec, runtime_env: Option<&ComfyUiRuntimeEnvironment>, @@ -1421,7 +1423,7 @@ fn run_uv_logged_command( .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - for (key, value) in uv_command_env() { + for (key, value) in uv_command_env(paths) { command.env(key, value); } if let Some(runtime_env) = runtime_env { diff --git a/apps/rocm/src/main.rs b/apps/rocm/src/main.rs index b0a3fd13..3bac5ca9 100644 --- a/apps/rocm/src/main.rs +++ b/apps/rocm/src/main.rs @@ -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, write_all_tcp_stream, + shell_command_for_host, uv_cache_source, write_all_tcp_stream, }; use rocm_engine_protocol::{ DEFAULT_LOG_TAIL_LINES, DetectRequest, DetectResponse, DevicePolicy, @@ -435,10 +435,12 @@ rocm logs --search error timeout")] /// Keep saved settings. #[arg(long)] keep_config: bool, - /// Keep app data such as logs, services, and engines. + /// Keep app data such as logs, services, engines, and the uv package cache + /// (often the largest directory rocm-cli manages). #[arg(long)] keep_data: bool, - /// Keep caches. + /// Keep caches under the cache directory. Does not cover the uv package cache, + /// which lives under the data directory; use --keep-data for that. #[arg(long)] keep_cache: bool, /// Allow removing development binaries inside the current build tree. @@ -888,6 +890,7 @@ fn main() -> Result<()> { .and_then(|paths| logging::init(&paths)); maybe_migrate_legacy_dashboard_config(); + maybe_notice_legacy_uv_cache(); let raw_args: Vec = std::env::args().skip(1).collect(); if raw_args.is_empty() { @@ -918,6 +921,54 @@ fn main() -> Result<()> { dispatch(Cli::parse()) } +/// Legacy `uv` cache location, used before the cache was colocated with the managed +/// data directory. Kept relative so the check works on every platform's home dir. +const LEGACY_UV_CACHE_RELATIVE: [&str; 2] = [".cache", "uv"]; + +/// One-shot notice that a pre-colocation `uv` cache is still occupying space at the +/// default `uv` location. Nothing is migrated or deleted: the cache is +/// content-addressed and may be shared with unrelated `uv` projects on the machine, so +/// removing it is the user's call. Silent when the managed cache does not exist yet +/// (nothing has moved) or when an override is in effect. +fn maybe_notice_legacy_uv_cache() { + let Ok(paths) = AppPaths::discover() else { + return; + }; + let cache = uv_cache_source(&paths); + if cache.is_override() { + return; + } + // Only worth mentioning once the managed cache is actually in use; otherwise the + // legacy directory is simply the cache still being used by other tools. + if !cache.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_CACHE_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 cache for other uv projects. + let marker = paths.data_dir.join(".legacy-uv-cache-notice"); + if marker.exists() { + return; + } + eprintln!( + "rocm: the uv cache now lives at {}; the previous cache at {} is no longer used by rocm-cli and can be removed if no other uv project needs it", + cache.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. diff --git a/apps/rocm/src/therock.rs b/apps/rocm/src/therock.rs index 204a15ae..fdb79247 100644 --- a/apps/rocm/src/therock.rs +++ b/apps/rocm/src/therock.rs @@ -14,7 +14,9 @@ use rocm_core::{ uv_venv_args, verify_rsa_pkcs1_sha256_signature, }; #[cfg(test)] -use rocm_core::{generate_rsa_signing_keypair, sign_rsa_pkcs1_sha256_signature}; +use rocm_core::{ + generate_rsa_signing_keypair, managed_uv_cache_dir, sign_rsa_pkcs1_sha256_signature, +}; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt::Write as _; @@ -905,7 +907,7 @@ fn install_wheel_runtime( "Creating Python environment at {}.", install_root.display() )); - ensure_uv_venv(&uv, &python_launcher.executable, &install_root)?; + ensure_uv_venv(paths, &uv, &python_launcher.executable, &install_root)?; let env_python = venv_python_path(&install_root); progress_line(format!( @@ -920,6 +922,7 @@ fn install_wheel_runtime( } install_args.extend(therock_pip_package_specs(&resolution.package_versions)); run_uv_progress_command( + paths, &uv, install_args .iter() @@ -2214,7 +2217,12 @@ fn extract_tarball(archive_path: &Path, target_dir: &Path) -> Result<()> { ) } -fn ensure_uv_venv(uv: &Path, python_launcher: &Path, install_root: &Path) -> Result<()> { +fn ensure_uv_venv( + paths: &AppPaths, + uv: &Path, + python_launcher: &Path, + install_root: &Path, +) -> Result<()> { let env_python = venv_python_path(install_root); if env_python.is_file() { if run_command( @@ -2241,7 +2249,7 @@ fn ensure_uv_venv(uv: &Path, python_launcher: &Path, install_root: &Path) -> Res .map(String::as_str) .collect::>() .as_slice(), - &uv_command_env(), + &uv_command_env(paths), "create managed TheRock runtime virtual environment", )?; if !env_python.is_file() { @@ -2626,10 +2634,15 @@ fn run_command_with_env( bail!("{context_text}: {detail}") } -fn run_uv_progress_command(uv: &Path, args: &[&str], context_text: &str) -> Result<()> { +fn run_uv_progress_command( + paths: &AppPaths, + uv: &Path, + args: &[&str], + context_text: &str, +) -> Result<()> { let mut command = Command::new(uv); command.args(args); - for (key, value) in &uv_command_env() { + for (key, value) in &uv_command_env(paths) { command.env(key, value); } let status = command @@ -2733,7 +2746,7 @@ fn ensure_managed_python(paths: &AppPaths) -> Result { progress_line(format!("Installing Python {version} via uv...")); let status = Command::new(&uv) .args(["python", "install", &version]) - .envs(uv_command_env()) + .envs(uv_command_env(paths)) .stdin(Stdio::null()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) @@ -2746,7 +2759,7 @@ fn ensure_managed_python(paths: &AppPaths) -> Result { progress_line(format!("Finding Python {version}...")); let output = Command::new(&uv) .args(["python", "find", &version]) - .envs(uv_command_env()) + .envs(uv_command_env(paths)) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -3395,12 +3408,32 @@ mod tests { } #[test] - fn managed_uv_cache_defaults_inside_generated_runtime_folder() { + fn managed_uv_cache_sits_under_the_data_dir_for_generated_runtime_folders() { let (_root, paths) = test_paths("managed-uv-cache"); let runtime_key = "release-wheel-gfx120x-all-7-14-0"; let install_root = managed_runtime_root(&paths, "wheel", runtime_key); - // uv caches live beside the venv; verify the wheel root path structure assert!(install_root.starts_with(&paths.data_dir)); + // Without --prefix the generated runtime folder is itself under the data dir, so + // the uv cache shares a filesystem with the environment it populates. + assert!(managed_uv_cache_dir(&paths.data_dir).starts_with(&paths.data_dir)); + } + + #[test] + fn uv_cache_does_not_follow_a_prefix_install_root() { + // Documents a known gap rather than an intended behavior: `--prefix` relocates + // install_root only, while the uv cache stays keyed off the data dir. When the two + // land on different filesystems uv falls back to copying. Tracked separately; see + // the `--prefix` non-goal on the PR that introduced the colocation. + let (_root, paths) = test_paths("prefix-uv-cache"); + let prefix_root = PathBuf::from("/mnt/elsewhere/envs/my-env"); + let cache = managed_uv_cache_dir(&paths.data_dir); + + assert!( + !cache.starts_with(&prefix_root), + "cache {} unexpectedly followed the --prefix root", + cache.display() + ); + assert!(cache.starts_with(&paths.data_dir)); } #[test] diff --git a/crates/rocm-core/src/lib.rs b/crates/rocm-core/src/lib.rs index 92e46e08..302387d6 100644 --- a/crates/rocm-core/src/lib.rs +++ b/crates/rocm-core/src/lib.rs @@ -49,22 +49,24 @@ use runtime::home_rocm_dir; 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_tools_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_runtime_cache_dir, 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, }; pub use uv::{ - DEFAULT_UV_TIMEOUT_SECS, ensure_uv_binary, uv_binary_name, 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, 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, }; pub const DEFAULT_LOCAL_PORT: u16 = 11_435; diff --git a/crates/rocm-core/src/runtime.rs b/crates/rocm-core/src/runtime.rs index 940f9e97..e66668cb 100644 --- a/crates/rocm-core/src/runtime.rs +++ b/crates/rocm-core/src/runtime.rs @@ -226,6 +226,12 @@ pub fn managed_pip_cache_dir(root: &Path) -> PathBuf { normalize_runtime_path_for_host(root).join("pip-cache") } +/// `uv`'s content-addressed cache, kept under the managed root so it shares a filesystem +/// with the environments `uv` populates and hardlinking keeps working (see issue #160). +pub fn managed_uv_cache_dir(root: &Path) -> PathBuf { + normalize_runtime_path_for_host(root).join("uv-cache") +} + pub fn managed_logs_dir(root: &Path) -> PathBuf { normalize_runtime_path_for_host(root).join("logs") } diff --git a/crates/rocm-core/src/uv.rs b/crates/rocm-core/src/uv.rs index 7d6dbd3f..b9ab379e 100644 --- a/crates/rocm-core/src/uv.rs +++ b/crates/rocm-core/src/uv.rs @@ -13,11 +13,14 @@ use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; +use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Duration; -use crate::runtime::{managed_tools_dir, runtime_is_windows, runtime_os_name}; +use crate::runtime::{ + managed_tools_dir, managed_uv_cache_dir, runtime_is_windows, runtime_os_name, +}; use crate::{AppPaths, download_file_to_path, unix_time_millis}; /// Default network timeout, in seconds, applied to `uv` HTTP operations. @@ -58,13 +61,113 @@ pub fn uv_http_timeout_secs() -> u64 { .unwrap_or(DEFAULT_UV_TIMEOUT_SECS) } -/// Environment pairs to apply when spawning `uv` so network behavior is configured -/// consistently (uv reads `UV_HTTP_TIMEOUT` rather than accepting a `--timeout` flag). -pub fn uv_command_env() -> Vec<(String, String)> { - vec![( - "UV_HTTP_TIMEOUT".to_owned(), - uv_http_timeout_secs().to_string(), - )] +/// Environment variable `uv` reads to locate its content-addressed cache. +pub const UV_CACHE_DIR_ENV: &str = "UV_CACHE_DIR"; + +/// Environment variable used to place the `uv` cache explicitly. +/// +/// Namespaced like the other rocm-cli knobs in this module so that a `UV_CACHE_DIR` a +/// developer exported for unrelated Python work is distinguishable from a deliberate +/// choice about rocm-cli. +pub const UV_CACHE_DIR_OVERRIDE_ENV: &str = "ROCM_CLI_UV_CACHE_DIR"; + +/// Where the `uv` cache for a spawned command comes from. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UvCacheSource { + /// Derived from the managed data directory. + Managed(PathBuf), + /// Set explicitly via [`UV_CACHE_DIR_OVERRIDE_ENV`]. + Override(PathBuf), + /// Inherited from an ambient [`UV_CACHE_DIR_ENV`] in the environment. + Inherited(PathBuf), +} + +impl UvCacheSource { + /// The cache directory this source resolves to. + pub fn path(&self) -> &Path { + match self { + Self::Managed(path) | Self::Override(path) | Self::Inherited(path) => path, + } + } + + /// Whether the managed colocation was bypassed, so callers can report it. + pub const fn is_override(&self) -> bool { + !matches!(self, Self::Managed(_)) + } +} + +/// Resolve the `uv` cache directory from the managed paths and the two override +/// variables. +/// +/// Precedence: [`UV_CACHE_DIR_OVERRIDE_ENV`] (a deliberate rocm-cli choice), then an +/// ambient [`UV_CACHE_DIR_ENV`] (kept so the e2e harness can share one cache across +/// scenarios), then the managed location beside the environments `uv` populates. +/// +/// Blank and whitespace-only values are ignored in both cases, matching `uv_version` and +/// `env_secs` in this module — `UV_CACHE_DIR=" "` is a leftover, not a choice. +pub fn uv_cache_source(paths: &AppPaths) -> UvCacheSource { + resolve_uv_cache_source( + paths, + std::env::var_os(UV_CACHE_DIR_OVERRIDE_ENV).as_deref(), + std::env::var_os(UV_CACHE_DIR_ENV).as_deref(), + ) +} + +fn resolve_uv_cache_source( + paths: &AppPaths, + override_dir: Option<&OsStr>, + inherited_dir: Option<&OsStr>, +) -> UvCacheSource { + if let Some(value) = meaningful_cache_dir(override_dir) { + return UvCacheSource::Override(value); + } + if let Some(value) = meaningful_cache_dir(inherited_dir) { + return UvCacheSource::Inherited(value); + } + UvCacheSource::Managed(managed_uv_cache_dir(&paths.data_dir)) +} + +/// A cache path is meaningful only when it is present and not blank. `OsStr` has no +/// `trim`, so trimming is done on the lossy view and the original value is kept when it +/// survives — a path is never silently rewritten. +fn meaningful_cache_dir(value: Option<&OsStr>) -> Option { + let value = value?; + let trimmed = value.to_string_lossy(); + let trimmed = trimmed.trim(); + if trimmed.is_empty() { + return None; + } + Some(PathBuf::from(trimmed)) +} + +/// Environment pairs to apply when spawning `uv`. +/// +/// Network behavior is configured consistently (uv reads `UV_HTTP_TIMEOUT` rather than +/// accepting a `--timeout` flag) and the cache lives beside the managed environments it +/// populates. +/// +/// Without a cache inside the managed root, `uv` caches under `$HOME/.cache/uv`; when +/// that is on a different filesystem from the data directory, `uv` cannot hardlink and +/// silently copies every file, so each environment carries a full duplicate of the SDK +/// and torch stack. +/// +/// Note this colocates with the *data directory*, not with a `--prefix` install root; see +/// the `--prefix` caveat in `docs/manual-testing.md`. +pub fn uv_command_env(paths: &AppPaths) -> Vec<(String, String)> { + uv_command_env_for_cache(uv_cache_source(paths)) +} + +fn uv_command_env_for_cache(cache: UvCacheSource) -> Vec<(String, String)> { + vec![ + ( + "UV_HTTP_TIMEOUT".to_owned(), + uv_http_timeout_secs().to_string(), + ), + ( + UV_CACHE_DIR_ENV.to_owned(), + cache.path().to_string_lossy().into_owned(), + ), + ] } /// Arguments for `uv venv`, creating an environment at `env_root` using `python`. @@ -367,6 +470,115 @@ mod tests { ); } + fn test_paths(root: &str) -> AppPaths { + AppPaths { + config_dir: PathBuf::from(root).join("config"), + data_dir: PathBuf::from(root), + cache_dir: PathBuf::from(root).join("cache"), + } + } + + fn cache_dir_in(env: &[(String, String)]) -> Option { + env.iter() + .find(|(key, _)| key == UV_CACHE_DIR_ENV) + .map(|(_, value)| value.clone()) + } + + #[test] + fn command_env_cache_dir_is_derived_from_data_dir() { + let paths = test_paths("/managed/root"); + let env = uv_command_env_for_cache(resolve_uv_cache_source(&paths, None, None)); + assert_eq!( + cache_dir_in(&env), + Some( + managed_uv_cache_dir(&paths.data_dir) + .to_string_lossy() + .into_owned() + ) + ); + assert!(env.iter().any(|(key, _)| key == "UV_HTTP_TIMEOUT")); + } + + #[test] + fn command_env_keeps_an_inherited_cache_dir() { + // The e2e harness sets UV_CACHE_DIR to share one cache across scenarios; a user can + // do the same. Such a choice must be inherited, not overridden. + let paths = test_paths("/managed/root"); + let source = resolve_uv_cache_source(&paths, None, Some(OsStr::new("/shared/uv-cache"))); + assert_eq!( + source, + UvCacheSource::Inherited(PathBuf::from("/shared/uv-cache")) + ); + assert_eq!( + cache_dir_in(&uv_command_env_for_cache(source)), + Some("/shared/uv-cache".to_owned()) + ); + } + + #[test] + fn namespaced_override_wins_over_an_ambient_uv_cache_dir() { + // ROCM_CLI_UV_CACHE_DIR is the rocm-cli knob; a bare UV_CACHE_DIR may just be + // exported for unrelated Python work, so the namespaced one takes precedence. + let paths = test_paths("/managed/root"); + let source = resolve_uv_cache_source( + &paths, + Some(OsStr::new("/chosen/uv-cache")), + Some(OsStr::new("/ambient/uv-cache")), + ); + assert_eq!( + source, + UvCacheSource::Override(PathBuf::from("/chosen/uv-cache")) + ); + assert!(source.is_override()); + } + + #[test] + fn blank_cache_overrides_fall_back_to_the_managed_location() { + // The boundary the env read actually has to survive: unset, empty, and + // whitespace-only all mean "no choice was made". + let paths = test_paths("/managed/root"); + let managed = UvCacheSource::Managed(managed_uv_cache_dir(&paths.data_dir)); + for blank in ["", " ", "\t\n"] { + assert_eq!( + resolve_uv_cache_source(&paths, Some(OsStr::new(blank)), None), + managed, + "blank ROCM_CLI_UV_CACHE_DIR {blank:?} should not count as an override" + ); + assert_eq!( + resolve_uv_cache_source(&paths, None, Some(OsStr::new(blank))), + managed, + "blank UV_CACHE_DIR {blank:?} should not count as an override" + ); + } + assert!(!managed.is_override()); + } + + #[test] + fn surrounding_whitespace_is_trimmed_from_a_cache_override() { + let paths = test_paths("/managed/root"); + assert_eq!( + resolve_uv_cache_source(&paths, Some(OsStr::new(" /chosen/uv-cache \n")), None), + UvCacheSource::Override(PathBuf::from("/chosen/uv-cache")) + ); + } + + #[test] + fn managed_cache_dir_tracks_rocm_cli_data_dir() { + // AppPaths::with_managed_root is how ROCM_CLI_DATA_DIR reaches the cache, so + // exercise that path rather than two hand-built AppPaths. + let moved = test_paths("/home/user/.rocm").with_managed_root("/mnt/big/rocm", false); + let source = resolve_uv_cache_source(&moved, None, None); + assert_eq!( + source, + UvCacheSource::Managed(managed_uv_cache_dir(&moved.data_dir)) + ); + assert!( + source.path().starts_with("/mnt/big/rocm"), + "cache {} should sit under the relocated data dir", + source.path().display() + ); + } + #[test] fn slug_sanitizes_unexpected_characters() { assert_eq!(slug("0.8.4"), "0.8.4"); diff --git a/docs/manual-testing.md b/docs/manual-testing.md index a4c82958..4add0a28 100644 --- a/docs/manual-testing.md +++ b/docs/manual-testing.md @@ -45,6 +45,14 @@ pip create it when downloads start. If you omit `--prefix`, rocm-cli should choose a managed runtime folder and still place the pip cache inside that runtime folder at `\pip-cache`. +The `uv` package cache is separate from that pip cache and does **not** follow +`--prefix`. It lives at `\uv-cache` so it shares a filesystem with the +managed environments and `uv` can hardlink into them. With `--prefix` pointing at +a different filesystem from `ROCM_CLI_DATA_DIR`, `uv` falls back to copying +packages; set `ROCM_CLI_UV_CACHE_DIR` to a folder on the prefix filesystem to +restore hardlinking. Making `--prefix` do this automatically is tracked +separately. + ## 1. First-Time Setup Start rocm-cli: diff --git a/engines/vllm/src/lib.rs b/engines/vllm/src/lib.rs index ff083318..25faae88 100644 --- a/engines/vllm/src/lib.rs +++ b/engines/vllm/src/lib.rs @@ -939,7 +939,7 @@ fn install_vllm_with_uv(python: &Path, reinstall: bool) -> Result<()> { args.push(index_url.clone()); let output = ProcessCommand::new(&uv) .args(args) - .envs(uv_command_env()) + .envs(uv_command_env(&paths)) .output() .context("failed to launch uv pip install for vLLM")?; if output.status.success() {