diff --git a/Cargo.lock b/Cargo.lock index df78fba0..88900214 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,7 +480,7 @@ dependencies = [ [[package]] name = "config" -version = "0.3.35" +version = "0.3.36" dependencies = [ "chrono", "clap", @@ -586,7 +586,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto" -version = "0.3.35" +version = "0.3.36" dependencies = [ "aes-gcm", "base64", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "testutils" -version = "0.3.35" +version = "0.3.36" dependencies = [ "pem", "rsa", @@ -3484,7 +3484,7 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tower" -version = "0.3.35" +version = "0.3.36" dependencies = [ "tokio", "tower-api", @@ -3509,7 +3509,7 @@ dependencies = [ [[package]] name = "tower-api" -version = "0.3.35" +version = "0.3.36" dependencies = [ "reqwest", "serde", @@ -3521,7 +3521,7 @@ dependencies = [ [[package]] name = "tower-cmd" -version = "0.3.35" +version = "0.3.36" dependencies = [ "axum", "bytes", @@ -3591,7 +3591,7 @@ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-package" -version = "0.3.35" +version = "0.3.36" dependencies = [ "async-compression", "config", @@ -3610,7 +3610,7 @@ dependencies = [ [[package]] name = "tower-runtime" -version = "0.3.35" +version = "0.3.36" dependencies = [ "chrono", "config", @@ -3631,7 +3631,7 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tower-telemetry" -version = "0.3.35" +version = "0.3.36" dependencies = [ "tracing", "tracing-appender", @@ -3640,7 +3640,7 @@ dependencies = [ [[package]] name = "tower-uv" -version = "0.3.35" +version = "0.3.36" dependencies = [ "async-compression", "async_zip", @@ -3654,7 +3654,7 @@ dependencies = [ [[package]] name = "tower-version" -version = "0.3.35" +version = "0.3.36" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 4e81a450..e7affdf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.3.35" +version = "0.3.36" description = "Tower is the best way to host Python data apps in production" rust-version = "1.81" authors = ["Brad Heller "] diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 187775b6..4d8a9b18 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: Option, } impl Config { @@ -29,6 +33,7 @@ impl Config { tower_url: default_tower_url(), json: false, session: None, + cache_dir: Some(default_cache_dir()), } } @@ -45,6 +50,7 @@ impl Config { tower_url, json: false, session: None, + cache_dir: Some(default_cache_dir()), } } @@ -72,6 +78,7 @@ impl Config { tower_url: sess.tower_url.clone(), json: self.json, session: Some(sess), + cache_dir: Some(default_cache_dir()), } } @@ -200,3 +207,10 @@ impl From<&Config> for Configuration { config.make_api_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(); + dir.join("tower").join("cache") +} diff --git a/crates/tower-cmd/src/run.rs b/crates/tower-cmd/src/run.rs index a993a7a3..7bee38d5 100644 --- a/crates/tower-cmd/src/run.rs +++ b/crates/tower-cmd/src/run.rs @@ -161,6 +161,7 @@ where secrets, params, env_vars, + 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..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().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-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/src/lib.rs b/crates/tower-uv/src/lib.rs index beb668b5..f625307f 100644 --- a/crates/tower-uv/src/lib.rs +++ b/crates/tower-uv/src/lib.rs @@ -94,14 +94,22 @@ 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, + + // 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() -> 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 }) + Ok(Uv { uv_path, cache_dir, protected_mode }) } Err(e) => { debug!("Error setting up UV: {:?}", e); @@ -117,15 +125,20 @@ 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 +169,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 +202,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) @@ -219,15 +240,24 @@ impl Uv { .arg("never") .arg("--no-progress") .arg("run") - .arg(program) - .env_clear() - .envs(env_vars); - + .arg(program); + #[cfg(unix)] { cmd.process_group(0); } + if self.protected_mode { + 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); + } + let child = cmd.spawn()?; Ok(child) } 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); } diff --git a/pyproject.toml b/pyproject.toml index f24d527c..60851996 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "tower" -version = "0.3.35" +version = "0.3.36" description = "Tower CLI and runtime environment for Tower." authors = [{ name = "Tower Computing Inc.", email = "brad@tower.dev" }] readme = "README.md" diff --git a/uv.lock b/uv.lock index e2f07d96..84f16e1b 100644 --- a/uv.lock +++ b/uv.lock @@ -2744,7 +2744,7 @@ wheels = [ [[package]] name = "tower" -version = "0.3.35" +version = "0.3.36" source = { editable = "." } dependencies = [ { name = "attrs" },