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
9 changes: 9 additions & 0 deletions MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<data-dir>/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
Expand Down
4 changes: 3 additions & 1 deletion apps/rocm/src/comfyui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -1400,6 +1401,7 @@ fn uv_install_args(venv_python: &Path, packages: &[String]) -> Vec<String> {
}

fn run_uv_logged_command(
paths: &AppPaths,
uv: &Path,
args: Vec<String>,
runtime_env: Option<&ComfyUiRuntimeEnvironment>,
Expand All @@ -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 {
Expand Down
57 changes: 54 additions & 3 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, 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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<String> = std::env::args().skip(1).collect();
if raw_args.is_empty() {
Expand Down Expand Up @@ -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.
Expand Down
53 changes: 43 additions & 10 deletions apps/rocm/src/therock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 _;
Expand Down Expand Up @@ -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!(
Expand All @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -2241,7 +2249,7 @@ fn ensure_uv_venv(uv: &Path, python_launcher: &Path, install_root: &Path) -> Res
.map(String::as_str)
.collect::<Vec<_>>()
.as_slice(),
&uv_command_env(),
&uv_command_env(paths),
"create managed TheRock runtime virtual environment",
)?;
if !env_python.is_file() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2733,7 +2746,7 @@ fn ensure_managed_python(paths: &AppPaths) -> Result<PythonLauncher> {
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())
Expand All @@ -2746,7 +2759,7 @@ fn ensure_managed_python(paths: &AppPaths) -> Result<PythonLauncher> {
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())
Expand Down Expand Up @@ -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]
Expand Down
30 changes: 16 additions & 14 deletions crates/rocm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions crates/rocm-core/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
Loading
Loading