From e89a14c38da191d212980d2d23b482fee24424a8 Mon Sep 17 00:00:00 2001 From: Brad Heller Date: Wed, 19 Nov 2025 16:31:40 +0000 Subject: [PATCH 1/4] chore: Add download caching to local runs --- crates/config/src/lib.rs | 14 +++++++++++++ crates/tower-cmd/src/run.rs | 1 + crates/tower-runtime/src/lib.rs | 3 +++ crates/tower-runtime/src/local.rs | 2 +- crates/tower-uv/src/lib.rs | 34 +++++++++++++++++++++++++------ 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 187775b6..3110dab2 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use serde::{Deserialize, Serialize}; use tower_api::apis::configuration::Configuration; use url::Url; @@ -20,6 +21,9 @@ pub struct Config { #[serde(skip_serializing, skip_deserializing)] pub session: Option, + + // cache_dir is the directory that we should cache uv artifacts within. + pub cache_dir: PathBuf, } impl Config { @@ -29,6 +33,7 @@ impl Config { tower_url: default_tower_url(), json: false, session: None, + cache_dir: default_cache_dir(), } } @@ -45,6 +50,7 @@ impl Config { tower_url, json: false, session: None, + cache_dir: default_cache_dir(), } } @@ -72,6 +78,7 @@ impl Config { tower_url: sess.tower_url.clone(), json: self.json, session: Some(sess), + cache_dir: default_cache_dir(), } } @@ -200,3 +207,10 @@ impl From<&Config> for Configuration { config.make_api_configuration() } } + +pub fn default_cache_dir() -> PathBuf { + let dir = dirs::data_local_dir().unwrap(); + let path = dir.join("tower").join("cache"); + std::fs::create_dir_all(&path).unwrap(); + path +} diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index a993a7a3..35f5697f 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -161,6 +161,7 @@ where secrets, params, env_vars, + Some(config.cache_dir), ) .await?; diff --git a/crates/tower-runtime/src/lib.rs b/crates/tower-runtime/src/lib.rs index 7cf8a86a..195c3e26 100644 --- a/crates/tower-runtime/src/lib.rs +++ b/crates/tower-runtime/src/lib.rs @@ -80,6 +80,7 @@ impl AppLauncher { secrets: HashMap, parameters: HashMap, env_vars: HashMap, + cache_dir: Option, ) -> Result<(), Error> { let cwd = package.unpacked_path.clone().unwrap().to_path_buf(); @@ -92,6 +93,7 @@ impl AppLauncher { parameters, package, env_vars, + cache_dir, }; // NOTE: This is a really awful hack to force any existing app to drop itself. Not certain @@ -134,6 +136,7 @@ pub struct StartOptions { pub parameters: HashMap, pub env_vars: HashMap, pub output_sender: OutputSender, + pub cache_dir: Option, } pub struct ExecuteOptions { diff --git a/crates/tower-runtime/src/local.rs b/crates/tower-runtime/src/local.rs index 6fe330b8..916d019d 100644 --- a/crates/tower-runtime/src/local.rs +++ b/crates/tower-runtime/src/local.rs @@ -178,7 +178,7 @@ async fn execute_local_app( let _ = sx.send(wait_for_process(ctx.clone(), &cancel_token, child).await); } else { - let uv = Uv::new().await?; + let uv = Uv::new(opts.cache_dir).await?; let env_vars = make_env_vars( &ctx, &environment, diff --git a/crates/tower-uv/src/lib.rs b/crates/tower-uv/src/lib.rs index beb668b5..5c62d0f9 100644 --- a/crates/tower-uv/src/lib.rs +++ b/crates/tower-uv/src/lib.rs @@ -94,14 +94,17 @@ async fn test_uv_path(path: &PathBuf) -> Result<(), Error> { pub struct Uv { pub uv_path: PathBuf, + + // cache_dir is the directory that dependencies should be cached in. + cache_dir: Option, } impl Uv { - pub async fn new() -> Result { + pub async fn new(cache_dir: Option) -> Result { match install::find_or_setup_uv().await { Ok(uv_path) => { test_uv_path(&uv_path).await?; - Ok(Uv { uv_path }) + Ok(Uv { uv_path, cache_dir }) } Err(e) => { debug!("Error setting up UV: {:?}", e); @@ -117,15 +120,22 @@ impl Uv { ) -> Result { debug!("Executing UV ({:?}) venv in {:?}", &self.uv_path, cwd); - let child = Command::new(&self.uv_path) - .kill_on_drop(true) + let mut cmd = Command::new(&self.uv_path); + cmd.kill_on_drop(true) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .current_dir(cwd) .arg("venv") - .envs(env_vars) - .spawn()?; + .envs(env_vars); + + if let Some(dir) = &self.cache_dir { + cmd.arg("--cache-dir").arg(dir); + } + + let child = cmd.spawn()?; + + Ok(child) } @@ -156,6 +166,10 @@ impl Uv { cmd.process_group(0); } + if let Some(dir) = &self.cache_dir { + cmd.arg("--cache-dir").arg(dir); + } + let child = cmd.spawn()?; Ok(child) @@ -185,6 +199,10 @@ impl Uv { cmd.process_group(0); } + if let Some(dir) = &self.cache_dir { + cmd.arg("--cache-dir").arg(dir); + } + let child = cmd.spawn()?; Ok(child) @@ -228,6 +246,10 @@ impl Uv { cmd.process_group(0); } + if let Some(dir) = &self.cache_dir { + cmd.arg("--cache-dir").arg(dir); + } + let child = cmd.spawn()?; Ok(child) } From af31b3bb2f0ac3766f1c09f97c13c58139779db4 Mon Sep 17 00:00:00 2001 From: Brad Heller Date: Wed, 19 Nov 2025 17:27:32 +0000 Subject: [PATCH 2/4] Add protected mode, to disable access to core enviro This prevents users from using binaries, etc., that are already installed on the environment. --- crates/tower-runtime/src/local.rs | 5 ++++- crates/tower-uv/src/lib.rs | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/tower-runtime/src/local.rs b/crates/tower-runtime/src/local.rs index 916d019d..5119b086 100644 --- a/crates/tower-runtime/src/local.rs +++ b/crates/tower-runtime/src/local.rs @@ -178,7 +178,10 @@ async fn execute_local_app( let _ = sx.send(wait_for_process(ctx.clone(), &cancel_token, child).await); } else { - let uv = Uv::new(opts.cache_dir).await?; + // we put Uv in to protected mode when there's no caching configured/enabled. + let protected_mode = opts.cache_dir.is_none(); + + let uv = Uv::new(opts.cache_dir, protected_mode).await?; let env_vars = make_env_vars( &ctx, &environment, diff --git a/crates/tower-uv/src/lib.rs b/crates/tower-uv/src/lib.rs index 5c62d0f9..01f22d18 100644 --- a/crates/tower-uv/src/lib.rs +++ b/crates/tower-uv/src/lib.rs @@ -97,14 +97,19 @@ pub struct Uv { // cache_dir is the directory that dependencies should be cached in. cache_dir: Option, + + // protected_mode is a flag that indicates whether the UV instance is in protected mode. + // In protected mode, the UV instance do things like clear the environment variables before + // use, etc. + protected_mode: bool, } impl Uv { - pub async fn new(cache_dir: Option) -> Result { + pub async fn new(cache_dir: Option, protected_mode: bool) -> Result { match install::find_or_setup_uv().await { Ok(uv_path) => { test_uv_path(&uv_path).await?; - Ok(Uv { uv_path, cache_dir }) + Ok(Uv { uv_path, cache_dir, protected_mode }) } Err(e) => { debug!("Error setting up UV: {:?}", e); @@ -238,14 +243,18 @@ impl Uv { .arg("--no-progress") .arg("run") .arg(program) - .env_clear() .envs(env_vars); + #[cfg(unix)] { cmd.process_group(0); } + if self.protected_mode { + cmd.env_clear(); + } + if let Some(dir) = &self.cache_dir { cmd.arg("--cache-dir").arg(dir); } From c8657111ddb4f48c7cc27835b05e126e56932304 Mon Sep 17 00:00:00 2001 From: Brad Heller Date: Wed, 19 Nov 2025 17:38:34 +0000 Subject: [PATCH 3/4] Some fixes for tests, etc. --- crates/config/src/lib.rs | 14 +++++++------- crates/tower-cmd/src/run.rs | 2 +- crates/tower-runtime/tests/local_test.rs | 6 ++++++ crates/tower-uv/tests/install_test.rs | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 3110dab2..4d8a9b18 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -23,7 +23,7 @@ pub struct Config { pub session: Option, // cache_dir is the directory that we should cache uv artifacts within. - pub cache_dir: PathBuf, + pub cache_dir: Option, } impl Config { @@ -33,7 +33,7 @@ impl Config { tower_url: default_tower_url(), json: false, session: None, - cache_dir: default_cache_dir(), + cache_dir: Some(default_cache_dir()), } } @@ -50,7 +50,7 @@ impl Config { tower_url, json: false, session: None, - cache_dir: default_cache_dir(), + cache_dir: Some(default_cache_dir()), } } @@ -78,7 +78,7 @@ impl Config { tower_url: sess.tower_url.clone(), json: self.json, session: Some(sess), - cache_dir: default_cache_dir(), + cache_dir: Some(default_cache_dir()), } } @@ -208,9 +208,9 @@ impl From<&Config> for Configuration { } } +// default_cache_dir gets the path the default cache location for dependencies, etc. Note +// that you don't have to create underlying directory, uv will do that automagically for us. pub fn default_cache_dir() -> PathBuf { let dir = dirs::data_local_dir().unwrap(); - let path = dir.join("tower").join("cache"); - std::fs::create_dir_all(&path).unwrap(); - path + dir.join("tower").join("cache") } diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index 35f5697f..7bee38d5 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -161,7 +161,7 @@ where secrets, params, env_vars, - Some(config.cache_dir), + config.cache_dir, ) .await?; diff --git a/crates/tower-runtime/tests/local_test.rs b/crates/tower-runtime/tests/local_test.rs index 8105834e..65b6a24b 100644 --- a/crates/tower-runtime/tests/local_test.rs +++ b/crates/tower-runtime/tests/local_test.rs @@ -56,6 +56,7 @@ async fn test_running_hello_world() { secrets: HashMap::new(), parameters: HashMap::new(), env_vars: HashMap::new(), + cache_dir: Some(config::default_cache_dir()) }; // Start the app using the LocalApp runtime @@ -100,6 +101,7 @@ async fn test_running_use_faker() { secrets: HashMap::new(), parameters: HashMap::new(), env_vars: HashMap::new(), + cache_dir: Some(config::default_cache_dir()), }; // Start the app using the LocalApp runtime @@ -151,6 +153,7 @@ async fn test_running_legacy_app() { secrets: HashMap::new(), parameters: HashMap::new(), env_vars: HashMap::new(), + cache_dir: Some(config::default_cache_dir()) }; // Start the app using the LocalApp runtime @@ -210,6 +213,9 @@ async fn test_running_app_with_secret() { secrets: secrets, parameters: HashMap::new(), env_vars: HashMap::new(), + + // NOTE: No cache dir indicates that we want to run in protected mode. + cache_dir: None, }; // Start the app using the LocalApp runtime diff --git a/crates/tower-uv/tests/install_test.rs b/crates/tower-uv/tests/install_test.rs index bfdb0393..1cb036d7 100644 --- a/crates/tower-uv/tests/install_test.rs +++ b/crates/tower-uv/tests/install_test.rs @@ -7,6 +7,6 @@ async fn test_installing_uv() { let _ = tokio::fs::remove_dir_all(&default_uv_bin_dir).await; // Now if we instantiate a Uv instance, it should install the `uv` binary. - let uv = Uv::new().await.expect("Failed to create a Uv instance"); + let uv = Uv::new(None, false).await.expect("Failed to create a Uv instance"); assert!(uv.is_valid().await); } From ddee5839b2b26fc3059cb9e36e0ad34ef60066b5 Mon Sep 17 00:00:00 2001 From: Brad Heller Date: Wed, 19 Nov 2025 17:56:33 +0000 Subject: [PATCH 4/4] Final fix for broken test Need to add envs after we clear_env! --- crates/tower-uv/src/lib.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/tower-uv/src/lib.rs b/crates/tower-uv/src/lib.rs index 01f22d18..f625307f 100644 --- a/crates/tower-uv/src/lib.rs +++ b/crates/tower-uv/src/lib.rs @@ -140,8 +140,6 @@ impl Uv { let child = cmd.spawn()?; - - Ok(child) } @@ -242,9 +240,7 @@ impl Uv { .arg("never") .arg("--no-progress") .arg("run") - .arg(program) - .envs(env_vars); - + .arg(program); #[cfg(unix)] { @@ -255,6 +251,9 @@ impl Uv { cmd.env_clear(); } + // Need to do this after env_clear intentionally. + cmd.envs(env_vars); + if let Some(dir) = &self.cache_dir { cmd.arg("--cache-dir").arg(dir); }