From 00f58337a225e87684a252cfc4b2265df33e1dee Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 24 Jul 2024 15:09:06 -0700 Subject: [PATCH] new: Support pinning proto in `.prototools`. (#560) --- CHANGELOG.md | 4 ++ Cargo.lock | 40 +++++-------- Cargo.toml | 6 +- crates/cli/src/commands/bin.rs | 12 ++++ crates/cli/src/commands/install.rs | 1 + crates/cli/src/commands/outdated.rs | 4 ++ crates/cli/src/commands/status.rs | 4 ++ crates/cli/src/main_shim.rs | 5 +- crates/cli/src/session.rs | 3 + crates/cli/src/systems.rs | 78 +++++++++++++++++++++++++- crates/cli/tests/general_test.rs | 38 +++++++++++++ crates/core/src/proto_config.rs | 19 ++++++- crates/core/tests/proto_config_test.rs | 15 +++++ crates/shim/src/lib.rs | 10 ++-- 14 files changed, 201 insertions(+), 38 deletions(-) create mode 100644 crates/cli/tests/general_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f604c5e..fc913de7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ - Added `PROTO_NO_MODIFY_PROFILE` and `PROTO_NO_MODIFY_PATH` environment variables to `proto setup` (for automated workflows). - Updated `github://` plugin locators to support monorepos. Append the project name (that tags are prefixed with) to the path: `github://moonrepo/tools/node_tool` - Merged `proto use` and `proto install` commands. If no arguments are provided to `proto install`, it will install all configured tools. +- You can now pin a version of proto itself within `.prototools`, and proto shims will attempt to run proto using that configured version, instead of the global version. + ```toml + proto = "0.38.0" + ``` ## 0.38.4 diff --git a/Cargo.lock b/Cargo.lock index c87980537..5adefce91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,9 +404,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.9" +version = "4.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142" dependencies = [ "clap_builder", "clap_derive", @@ -414,9 +414,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.9" +version = "4.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac" dependencies = [ "anstream", "anstyle", @@ -426,9 +426,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.8" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b4be9c4c4b1f30b78d8a750e0822b6a6102d97e62061c583a6c1dea2dfb33ae" +checksum = "faa2032320fd6f50d22af510d204b2994eef49600dfbd0e771a166213844e4cd" dependencies = [ "clap", ] @@ -1962,13 +1962,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ + "hermit-abi", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2026,16 +2027,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "number_prefix" version = "0.4.0" @@ -3512,29 +3503,28 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.1" +version = "1.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3d955825d..72849520e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,8 @@ default-members = ["crates/cli"] [workspace.dependencies] anyhow = "1.0.86" async-trait = "0.1.81" -clap = "4.5.9" -clap_complete = "4.5.8" +clap = "4.5.10" +clap_complete = "4.5.9" dirs = "5.0.1" extism = "1.0.0" # Lower for consumers extism-pdk = "1.2.0" @@ -50,7 +50,7 @@ starbase_utils = { version = "0.8.3", default-features = false, features = [ "toml", ] } thiserror = "1.0.63" -tokio = { version = "1.38.1", features = ["full", "tracing"] } +tokio = { version = "1.39.1", features = ["full", "tracing"] } tracing = "0.1.40" uuid = { version = "1.10.0", features = ["v4"] } diff --git a/crates/cli/src/commands/bin.rs b/crates/cli/src/commands/bin.rs index 8589b8ca9..d575565e9 100644 --- a/crates/cli/src/commands/bin.rs +++ b/crates/cli/src/commands/bin.rs @@ -1,6 +1,7 @@ use crate::session::ProtoSession; use clap::Args; use proto_core::{detect_version, Id, UnresolvedVersionSpec}; +use proto_shim::{get_exe_file_name, locate_proto_exe}; use starbase::AppResult; #[derive(Args, Clone, Debug)] @@ -20,6 +21,17 @@ pub struct BinArgs { #[tracing::instrument(skip_all)] pub async fn bin(session: ProtoSession, args: BinArgs) -> AppResult { + if args.id == "proto" { + println!( + "{}", + locate_proto_exe("proto") + .unwrap_or(session.env.store.bin_dir.join(get_exe_file_name("proto"))) + .display() + ); + + return Ok(()); + } + let mut tool = session.load_tool(&args.id).await?; let version = detect_version(&tool, args.spec.clone()).await?; diff --git a/crates/cli/src/commands/install.rs b/crates/cli/src/commands/install.rs index a0c2f5928..80f450398 100644 --- a/crates/cli/src/commands/install.rs +++ b/crates/cli/src/commands/install.rs @@ -333,6 +333,7 @@ pub async fn install_all(session: &ProtoSession, args: InstallArgs) -> AppResult .get_merged_config_without_global()? }; let mut versions = config.versions.to_owned(); + versions.remove("proto"); for tool in &tools { if versions.contains_key(&tool.id) { diff --git a/crates/cli/src/commands/outdated.rs b/crates/cli/src/commands/outdated.rs index 2f96979f3..baaf104bf 100644 --- a/crates/cli/src/commands/outdated.rs +++ b/crates/cli/src/commands/outdated.rs @@ -84,6 +84,10 @@ pub async fn outdated(session: ProtoSession, args: OutdatedArgs) -> AppResult { if let Some(file_versions) = &file.config.versions { for (tool_id, config_version) in file_versions { + if tool_id == "proto" { + continue; + } + configured_tools.insert( tool_id.to_owned(), (config_version.to_owned(), file.path.to_owned()), diff --git a/crates/cli/src/commands/status.rs b/crates/cli/src/commands/status.rs index 8cafb32ac..724bb406f 100644 --- a/crates/cli/src/commands/status.rs +++ b/crates/cli/src/commands/status.rs @@ -53,6 +53,10 @@ pub async fn status(session: ProtoSession, args: StatusArgs) -> AppResult { if let Some(file_versions) = &file.config.versions { for (tool_id, config_version) in file_versions { + if tool_id == "proto" { + continue; + } + items.insert( tool_id.to_owned(), StatusItem { diff --git a/crates/cli/src/main_shim.rs b/crates/cli/src/main_shim.rs index bd8a30a67..2b20bafd4 100644 --- a/crates/cli/src/main_shim.rs +++ b/crates/cli/src/main_shim.rs @@ -165,6 +165,9 @@ pub fn main() -> Result<()> { debug(|| "Running proto shim".into()); + // Set the version variable so child processes utilize it + env::set_var("PROTO_VERSION", env!("CARGO_PKG_VERSION")); + // Extract arguments to pass-through let args = env::args_os().collect::>(); @@ -197,9 +200,9 @@ pub fn main() -> Result<()> { command.env("PROTO_SHIM_NAME", shim_name); command.env("PROTO_SHIM_PATH", exe_path); - // Must be the last line! debug(|| "Executing proto command".into()); debug(|| "This will replace the current process and stop debugging!".into()); + // Must be the last line! Ok(exec_command_and_replace(command)?) } diff --git a/crates/cli/src/session.rs b/crates/cli/src/session.rs index 4cedc9bd4..0de3f2d90 100644 --- a/crates/cli/src/session.rs +++ b/crates/cli/src/session.rs @@ -97,11 +97,14 @@ impl AppSession for ProtoSession { async fn startup(&mut self) -> AppResult { self.env = Arc::new(detect_proto_env()?); + sync_current_proto_tool(&self.env, &self.cli_version)?; + Ok(()) } async fn analyze(&mut self) -> AppResult { load_proto_configs(&self.env)?; + download_versioned_proto_tool(&self.env).await?; Ok(()) } diff --git a/crates/cli/src/systems.rs b/crates/cli/src/systems.rs index f73b0dd88..a36a49e3d 100644 --- a/crates/cli/src/systems.rs +++ b/crates/cli/src/systems.rs @@ -1,6 +1,8 @@ use crate::helpers::fetch_latest_version; use miette::IntoDiagnostic; -use proto_core::{is_offline, now, ProtoEnvironment}; +use proto_core::{is_offline, now, ProtoEnvironment, UnresolvedVersionSpec, PROTO_CONFIG_NAME}; +use proto_installer::{determine_triple, download_release, unpack_release}; +use proto_shim::get_exe_file_name; use semver::Version; use starbase::AppResult; use starbase_styles::color; @@ -8,7 +10,7 @@ use starbase_utils::fs; use std::env; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, instrument}; +use tracing::{debug, instrument, trace}; // STARTUP @@ -17,6 +19,32 @@ pub fn detect_proto_env() -> AppResult { ProtoEnvironment::new() } +#[instrument(skip_all)] +pub fn sync_current_proto_tool(env: &ProtoEnvironment, version: &str) -> AppResult { + let tool_dir = env.store.inventory_dir.join("proto").join(version); + + if tool_dir.exists() { + return Ok(()); + } + + let Ok(current_exe) = env::current_exe() else { + return Ok(()); + }; + + let exe_dir = current_exe.parent().unwrap_or(&env.store.bin_dir); + + for exe_name in [get_exe_file_name("proto"), get_exe_file_name("proto-shim")] { + let src_file = exe_dir.join(&exe_name); + let dst_file = tool_dir.join(&exe_name); + + if src_file.exists() && !dst_file.exists() { + fs::copy_file(src_file, dst_file)?; + } + } + + Ok(()) +} + // ANALYZE #[instrument(skip_all)] @@ -26,6 +54,52 @@ pub fn load_proto_configs(env: &ProtoEnvironment) -> AppResult { Ok(()) } +#[instrument(skip_all)] +pub async fn download_versioned_proto_tool(env: &ProtoEnvironment) -> AppResult { + if is_offline() { + return Ok(()); + } + + let config = env + .load_config_manager()? + .get_merged_config_without_global()?; + + if let Some(UnresolvedVersionSpec::Semantic(version)) = config.versions.get("proto") { + let version = version.to_string(); + let tool_dir = env.store.inventory_dir.join("proto").join(&version); + + if tool_dir.exists() { + return Ok(()); + } + + let triple_target = determine_triple()?; + + debug!( + version = &version, + install_dir = ?tool_dir, + "Downloading a versioned proto because it was configured in {}", + PROTO_CONFIG_NAME + ); + + unpack_release( + download_release( + &triple_target, + &version, + &env.store.temp_dir, + |downloaded_size, total_size| { + trace!("Downloaded {} of {} bytes", downloaded_size, total_size); + }, + ) + .await?, + &tool_dir, + &tool_dir, + false, + )?; + } + + Ok(()) +} + // EXECUTE #[instrument(skip_all)] diff --git a/crates/cli/tests/general_test.rs b/crates/cli/tests/general_test.rs new file mode 100644 index 000000000..a38c063eb --- /dev/null +++ b/crates/cli/tests/general_test.rs @@ -0,0 +1,38 @@ +mod utils; + +use utils::*; + +mod systems { + use super::*; + + #[test] + fn copies_current_bin_to_store() { + let sandbox = create_empty_proto_sandbox(); + + sandbox + .run_bin(|cmd| { + cmd.arg("bin").arg("proto"); + }) + .success(); + + assert!(sandbox + .path() + .join(".proto/tools/proto") + .join(env!("CARGO_PKG_VERSION")) + .exists()); + } + + #[test] + fn downloads_versioned_bin_to_store() { + let sandbox = create_empty_proto_sandbox(); + sandbox.create_file(".prototools", r#"proto = "0.30.0""#); + + sandbox + .run_bin(|cmd| { + cmd.arg("bin").arg("proto"); + }) + .success(); + + assert!(sandbox.path().join(".proto/tools/proto/0.30.0").exists()); + } +} diff --git a/crates/core/src/proto_config.rs b/crates/core/src/proto_config.rs index 27437c5bb..d1bf3d4e0 100644 --- a/crates/core/src/proto_config.rs +++ b/crates/core/src/proto_config.rs @@ -5,7 +5,7 @@ use rustc_hash::FxHashMap; use schematic::{ derive_enum, env, merge, Config, ConfigEnum, ConfigError, ConfigLoader, DefaultValueResult, Format, HandlerError, MergeResult, PartialConfig, ValidateError, ValidateErrorType, - ValidatorError, + ValidateResult, ValidatorError, }; use serde::{Deserialize, Serialize}; use starbase_styles::color; @@ -69,6 +69,21 @@ where Ok(Some(prev)) } +fn validate_reserved_words( + value: &BTreeMap, + _partial: &PartialProtoConfig, + _context: &(), + _finalize: bool, +) -> ValidateResult { + if value.contains_key("proto") { + return Err(ValidateError::new( + "proto is a reserved keyword, cannot use as a plugin identifier", + )); + } + + Ok(()) +} + derive_enum!( #[derive(ConfigEnum, Default)] pub enum DetectStrategy { @@ -168,7 +183,7 @@ pub struct ProtoConfig { #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub tools: BTreeMap, - #[setting(merge = merge::merge_btreemap)] + #[setting(merge = merge::merge_btreemap, validate = validate_reserved_words)] #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub plugins: BTreeMap, diff --git a/crates/core/tests/proto_config_test.rs b/crates/core/tests/proto_config_test.rs index 3c12ba852..b531ffe08 100644 --- a/crates/core/tests/proto_config_test.rs +++ b/crates/core/tests/proto_config_test.rs @@ -51,6 +51,21 @@ mod proto_config { handle_error(ProtoConfig::load_from(sandbox.path(), false).unwrap_err()); } + #[test] + #[should_panic(expected = "proto is a reserved keyword, cannot use as a plugin identifier")] + fn errors_for_reserved_plugin_words() { + let sandbox = create_empty_sandbox(); + sandbox.create_file( + ".prototools", + r#" +[plugins] +proto = "file://./file.toml" +"#, + ); + + handle_error(ProtoConfig::load_from(sandbox.path(), false).unwrap_err()); + } + #[test] fn can_set_settings() { let sandbox = create_empty_sandbox(); diff --git a/crates/shim/src/lib.rs b/crates/shim/src/lib.rs index b2ce56668..b7cc1fc05 100644 --- a/crates/shim/src/lib.rs +++ b/crates/shim/src/lib.rs @@ -13,7 +13,7 @@ pub use windows::*; use std::env; use std::path::PathBuf; -pub const SHIM_VERSION: u8 = 14; +pub const SHIM_VERSION: u8 = 15; pub fn locate_proto_exe(bin: &str) -> Option { let bin = get_exe_file_name(bin); @@ -48,10 +48,6 @@ pub fn locate_proto_exe(bin: &str) -> Option { } } - if let Ok(dir) = env::var("PROTO_LOOKUP_DIR") { - lookup_dirs.push(dir.into()); - } - if let Ok(dir) = env::var("PROTO_HOME") { let dir = PathBuf::from(dir); @@ -62,6 +58,10 @@ pub fn locate_proto_exe(bin: &str) -> Option { lookup_dirs.push(dir.join("bin")); } + if let Ok(dir) = env::var("PROTO_LOOKUP_DIR") { + lookup_dirs.push(dir.into()); + } + // Detect the currently running executable (proto), and then find // a proto-shim sibling in the same directory. This assumes both // binaries are the same version.