From ea97f9d90b7775ede753766e9ed0bc18607fe390 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 22 Feb 2025 10:08:30 -0800 Subject: [PATCH] new: Enable shims for proto itself. (#725) --- CHANGELOG.md | 17 +++ crates/cli/src/commands/activate.rs | 2 + crates/cli/src/commands/bin.rs | 14 +- crates/cli/src/commands/run.rs | 145 ++++++++++++------- crates/cli/src/session.rs | 18 +-- crates/cli/src/systems.rs | 37 +---- crates/cli/src/utils/tool_record.rs | 1 + crates/cli/src/workflows/install_workflow.rs | 7 +- crates/cli/tests/general_test.rs | 21 --- crates/cli/tests/install_uninstall_test.rs | 36 ++++- crates/cli/tests/pin_test.rs | 68 +++++++++ crates/cli/tests/run_test.rs | 88 +++++++++++ crates/core/src/config.rs | 2 +- 13 files changed, 309 insertions(+), 147 deletions(-) delete mode 100644 crates/cli/tests/general_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a55bfe83..4ad1e4eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,23 @@ - [Rust](https://github.com/moonrepo/plugins/blob/master/tools/rust/CHANGELOG.md) - [Schema (TOML, JSON, YAML)](https://github.com/moonrepo/plugins/blob/master/tools/internal-schema/CHANGELOG.md) +## Unreleased + +#### 🚀 Updates + +- Added shim support to the internal `proto` tool, allowing the proto version to be pinned in `.prototools`, and the version to dynamically be detected at runtime. This enables a specific proto version to be used per project. +- Updated `proto install` to now install proto if a version has been defined. + +#### 🧩 Plugins + +- Updated `proto_tool` to v0.5.1. + - Now supports shims. + +#### ⚙️ Internal + +- Updated Rust to v1.85. +- Updated dependencies. + ## 0.46.1 #### 🐞 Fixes diff --git a/crates/cli/src/commands/activate.rs b/crates/cli/src/commands/activate.rs index 3b562f608..aef1ea7c0 100644 --- a/crates/cli/src/commands/activate.rs +++ b/crates/cli/src/commands/activate.rs @@ -143,6 +143,8 @@ pub async fn activate(session: ProtoSession, args: ActivateArgs) -> AppResult { { info.env .insert("PROTO_VERSION".into(), Some(version.to_string())); + info.env + .insert("PROTO_PROTO_VERSION".into(), Some(version.to_string())); info.paths.push( session diff --git a/crates/cli/src/commands/bin.rs b/crates/cli/src/commands/bin.rs index 2699efbda..8c8501560 100644 --- a/crates/cli/src/commands/bin.rs +++ b/crates/cli/src/commands/bin.rs @@ -1,7 +1,6 @@ use crate::session::ProtoSession; use clap::Args; -use proto_core::{Id, PROTO_PLUGIN_KEY, ToolSpec, detect_version_with_spec}; -use proto_shim::{get_exe_file_name, locate_proto_exe}; +use proto_core::{Id, ToolSpec, detect_version_with_spec}; use starbase::AppResult; #[derive(Args, Clone, Debug)] @@ -21,17 +20,6 @@ pub struct BinArgs { #[tracing::instrument(skip_all)] pub async fn bin(session: ProtoSession, args: BinArgs) -> AppResult { - if args.id == PROTO_PLUGIN_KEY { - session.console.out.write_line( - locate_proto_exe("proto") - .unwrap_or(session.env.store.bin_dir.join(get_exe_file_name("proto"))) - .display() - .to_string(), - )?; - - return Ok(None); - } - let mut tool = session .load_tool(&args.id, args.spec.clone().and_then(|spec| spec.backend)) .await?; diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index 9e41452c4..99400b60a 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -3,9 +3,9 @@ use crate::error::ProtoCliError; use crate::session::ProtoSession; use clap::Args; use miette::IntoDiagnostic; -use proto_core::{Id, Tool, ToolSpec, detect_version_with_spec}; +use proto_core::{Id, PROTO_PLUGIN_KEY, Tool, ToolSpec, detect_version_with_spec}; use proto_pdk_api::{ExecutableConfig, RunHook, RunHookResult}; -use proto_shim::exec_command_and_replace; +use proto_shim::{exec_command_and_replace, locate_proto_exe}; use starbase::AppResult; use starbase_utils::fs; use std::env; @@ -37,8 +37,20 @@ pub struct RunArgs { passthrough: Vec, } +fn should_use_global_proto(tool: &Tool) -> miette::Result { + Ok(tool.id == PROTO_PLUGIN_KEY + && !tool + .proto + .load_config()? + .versions + .contains_key(PROTO_PLUGIN_KEY)) +} + fn is_trying_to_self_upgrade(tool: &Tool, args: &[String]) -> bool { - if tool.metadata.self_upgrade_commands.is_empty() { + if tool.id == PROTO_PLUGIN_KEY + || tool.metadata.self_upgrade_commands.is_empty() + || args.is_empty() + { return false; } @@ -119,13 +131,16 @@ fn create_command, A: AsRef>( exe_config: &ExecutableConfig, args: I, ) -> miette::Result { - let exe_path = exe_config.exe_path.as_ref().unwrap(); + let exe_path = exe_config + .exe_path + .as_ref() + .expect("Could not determine executable path."); let args = args .into_iter() .map(|arg| arg.as_ref().to_os_string()) .collect::>(); - let command = if let Some(parent_exe_path) = &exe_config.parent_exe_name { + let mut command = if let Some(parent_exe_path) = &exe_config.parent_exe_name { let mut exe_args = vec![exe_path.as_os_str().to_os_string()]; exe_args.extend(args); @@ -148,6 +163,17 @@ fn create_command, A: AsRef>( create_process_command(exe_path, args) }; + for (key, value) in tool.proto.load_config()?.get_env_vars(Some(&tool.id))? { + match value { + Some(value) => { + command.env(key, value); + } + None => { + command.env_remove(key); + } + }; + } + Ok(command) } @@ -156,6 +182,7 @@ pub async fn run(session: ProtoSession, args: RunArgs) -> AppResult { let mut tool = session .load_tool(&args.id, args.spec.clone().and_then(|spec| spec.backend)) .await?; + let mut use_global_proto = should_use_global_proto(&tool)?; // Avoid running the tool's native self-upgrade as it conflicts with proto if is_trying_to_self_upgrade(&tool, &args.passthrough) { @@ -166,14 +193,61 @@ pub async fn run(session: ProtoSession, args: RunArgs) -> AppResult { .into()); } - let spec = detect_version_with_spec(&tool, args.spec.clone()).await?; + // Detect a version to run with + let spec = if use_global_proto { + args.spec + .clone() + .unwrap_or_else(|| ToolSpec::parse("*").unwrap()) + } else { + detect_version_with_spec(&tool, args.spec.clone()).await? + }; // Check if installed or need to install - if !tool.is_setup_with_spec(&spec).await? { + if tool.is_setup_with_spec(&spec).await? { + if tool.id == PROTO_PLUGIN_KEY { + use_global_proto = false; + } + } else { let config = tool.proto.load_config()?; let resolved_version = tool.get_resolved_version(); - if !config.settings.auto_install { + // Auto-install the missing tool + if config.settings.auto_install { + session.console.out.write_line(format!( + "Auto-install is enabled, attempting to install {} {}", + tool.get_name(), + resolved_version, + ))?; + + install_one( + session.clone(), + InstallArgs { + internal: true, + spec: Some(ToolSpec { + backend: spec.backend, + req: resolved_version.to_unresolved_spec(), + res: Some(resolved_version.clone()), + }), + ..Default::default() + }, + tool.id.clone(), + ) + .await?; + + session.console.out.write_line(format!( + "{} {} has been installed, continuing execution...", + tool.get_name(), + resolved_version, + ))?; + } + // If this is the proto tool running, continue instead of failing + else if use_global_proto { + debug!( + "No proto version detected or located, falling back to the global proto binary!" + ); + } + // Otherwise fail with a not installed error + else { let command = format!("proto install {} {}", tool.id, resolved_version); if let Ok(source) = env::var(format!("{}_DETECTED_FROM", tool.get_env_var_prefix())) { @@ -193,42 +267,18 @@ pub async fn run(session: ProtoSession, args: RunArgs) -> AppResult { } .into()); } - - // Install the tool - session.console.out.write_line(format!( - "Auto-install is enabled, attempting to install {} {}", - tool.get_name(), - resolved_version, - ))?; - - install_one( - session.clone(), - InstallArgs { - internal: true, - spec: Some(ToolSpec { - backend: spec.backend, - req: resolved_version.to_unresolved_spec(), - res: Some(resolved_version.clone()), - }), - ..Default::default() - }, - tool.id.clone(), - ) - .await?; - - session.console.out.write_line(format!( - "{} {} has been installed, continuing execution...", - tool.get_name(), - resolved_version, - ))?; } // Determine the binary path to execute - let exe_config = get_executable(&tool, &args).await?; - let exe_path = exe_config - .exe_path - .as_ref() - .expect("Could not determine executable path."); + let exe_config = if use_global_proto { + ExecutableConfig { + exe_path: locate_proto_exe("proto"), + primary: true, + ..Default::default() + } + } else { + get_executable(&tool, &args).await? + }; // Run before hook let hook_result = if tool.plugin.has_func("pre_run").await { @@ -258,17 +308,6 @@ pub async fn run(session: ProtoSession, args: RunArgs) -> AppResult { // Create and run the command let mut command = create_command(&tool, &exe_config, &args.passthrough)?; - for (key, value) in tool.proto.load_config()?.get_env_vars(Some(&tool.id))? { - match value { - Some(value) => { - command.env(key, value); - } - None => { - command.env_remove(key); - } - }; - } - if let Some(hook_args) = hook_result.args { command.args(hook_args); } @@ -284,7 +323,7 @@ pub async fn run(session: ProtoSession, args: RunArgs) -> AppResult { ) .env( format!("{}_BIN", tool.get_env_var_prefix()), - exe_path.to_string_lossy().to_string(), + exe_config.exe_path.as_ref().unwrap(), ); // Update the last used timestamp diff --git a/crates/cli/src/session.rs b/crates/cli/src/session.rs index 20f0ddcea..3c870f638 100644 --- a/crates/cli/src/session.rs +++ b/crates/cli/src/session.rs @@ -6,11 +6,9 @@ use crate::utils::progress_instance::ProgressInstance; use crate::utils::tool_record::ToolRecord; use async_trait::async_trait; use miette::IntoDiagnostic; -use proto_core::registry::ProtoRegistry; use proto_core::{ - Backend, ConfigMode, Id, PROTO_PLUGIN_KEY, ProtoConfig, ProtoEnvironment, SCHEMA_PLUGIN_KEY, - Tool, ToolSpec, UnresolvedVersionSpec, load_schema_plugin_with_proto, load_tool, - load_tool_from_locator, + Backend, ConfigMode, Id, ProtoConfig, ProtoEnvironment, SCHEMA_PLUGIN_KEY, ToolSpec, + UnresolvedVersionSpec, load_schema_plugin_with_proto, load_tool, registry::ProtoRegistry, }; use rustc_hash::FxHashSet; use semver::Version; @@ -162,7 +160,7 @@ impl ProtoSession { } // These shouldn't be treated as a "normal plugin" - if id == SCHEMA_PLUGIN_KEY || id == PROTO_PLUGIN_KEY { + if id == SCHEMA_PLUGIN_KEY { continue; } @@ -218,15 +216,6 @@ impl ProtoSession { self.load_tools_with_options(options).await } - pub async fn load_proto_tool(&self) -> miette::Result { - load_tool_from_locator( - Id::new(PROTO_PLUGIN_KEY)?, - &self.env, - self.env.load_config()?.builtin_proto_plugin(), - ) - .await - } - pub async fn render_progress_loader(&self) -> miette::Result { use iocraft::prelude::element; @@ -270,7 +259,6 @@ impl AppSession for ProtoSession { async fn analyze(&mut self) -> AppResult { load_proto_configs(&self.env)?; - download_versioned_proto_tool(self).await?; Ok(None) } diff --git a/crates/cli/src/systems.rs b/crates/cli/src/systems.rs index a74ecaf62..ade0ea711 100644 --- a/crates/cli/src/systems.rs +++ b/crates/cli/src/systems.rs @@ -1,11 +1,6 @@ use crate::app::{App as CLI, Commands}; use crate::helpers::fetch_latest_version; -use crate::session::ProtoSession; -use proto_core::flow::install::InstallOptions; -use proto_core::{ - ConfigMode, PROTO_CONFIG_NAME, PROTO_PLUGIN_KEY, ProtoEnvironment, UnresolvedVersionSpec, - is_offline, now, -}; +use proto_core::{ConfigMode, ProtoEnvironment, is_offline, now}; use proto_shim::get_exe_file_name; use semver::Version; use starbase_styles::color; @@ -55,36 +50,6 @@ pub fn load_proto_configs(env: &ProtoEnvironment) -> miette::Result<()> { Ok(()) } -#[instrument(skip_all)] -pub async fn download_versioned_proto_tool(session: &ProtoSession) -> miette::Result<()> { - let config = session - .env - .load_config_manager()? - .get_merged_config_without_global()?; - - if let Some(spec) = config.versions.get(PROTO_PLUGIN_KEY) { - // Only support fully-qualified versions as we need to prepend the - // tool directory into PATH, which doesn't support requirements - if !matches!(spec.req, UnresolvedVersionSpec::Semantic(_)) { - return Ok(()); - } - - let mut tool = session.load_proto_tool().await?; - - if !tool.is_installed() { - debug!( - version = spec.to_string(), - "Downloading a versioned proto because it was configured in {}", PROTO_CONFIG_NAME - ); - - tool.setup_with_spec(spec, InstallOptions::default()) - .await?; - } - } - - Ok(()) -} - // EXECUTE #[instrument(skip_all)] diff --git a/crates/cli/src/utils/tool_record.rs b/crates/cli/src/utils/tool_record.rs index e626a2603..c33e599a5 100644 --- a/crates/cli/src/utils/tool_record.rs +++ b/crates/cli/src/utils/tool_record.rs @@ -6,6 +6,7 @@ use proto_core::{ use std::collections::BTreeMap; use std::path::PathBuf; +#[derive(Debug)] pub struct ToolRecord { pub tool: Tool, pub config: ProtoToolConfig, diff --git a/crates/cli/src/workflows/install_workflow.rs b/crates/cli/src/workflows/install_workflow.rs index c5cec59f6..057f944dd 100644 --- a/crates/cli/src/workflows/install_workflow.rs +++ b/crates/cli/src/workflows/install_workflow.rs @@ -7,7 +7,7 @@ use crate::utils::tool_record::ToolRecord; use iocraft::element; use miette::IntoDiagnostic; use proto_core::flow::install::{InstallOptions, InstallPhase}; -use proto_core::{Id, PROTO_PLUGIN_KEY, PinLocation, ToolSpec}; +use proto_core::{Id, PinLocation, ToolSpec}; use proto_pdk_api::{ InstallHook, InstallStrategy, Switch, SyncShellProfileInput, SyncShellProfileOutput, }; @@ -278,11 +278,6 @@ impl InstallWorkflow { spec: &ToolSpec, arg_pin_to: &Option, ) -> miette::Result { - // Don't pin the proto tool itself as it's internal only - if self.tool.id.as_str() == PROTO_PLUGIN_KEY { - return Ok(false); - } - let config = self.tool.proto.load_config()?; let mut pin_to = PinLocation::Local; let mut pin = false; diff --git a/crates/cli/tests/general_test.rs b/crates/cli/tests/general_test.rs deleted file mode 100644 index c1265caed..000000000 --- a/crates/cli/tests/general_test.rs +++ /dev/null @@ -1,21 +0,0 @@ -mod utils; - -use utils::*; - -mod systems { - use super::*; - - #[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/cli/tests/install_uninstall_test.rs b/crates/cli/tests/install_uninstall_test.rs index 63a19d016..5f932f466 100644 --- a/crates/cli/tests/install_uninstall_test.rs +++ b/crates/cli/tests/install_uninstall_test.rs @@ -60,6 +60,38 @@ mod install_uninstall { assert!(sandbox.path().join(".proto/tools/node/16.20.2").exists()); } + #[test] + fn installs_and_uninstalls_proto() { + let sandbox = create_empty_proto_sandbox(); + let tool_dir = sandbox.path().join(".proto/tools/proto/0.45.0"); + + assert!(!tool_dir.exists()); + + // Install + let assert = sandbox + .run_bin(|cmd| { + cmd.arg("install").arg("proto").arg("0.45.0"); + }) + .success(); + + assert!(tool_dir.exists()); + + assert.stdout(predicate::str::contains("proto 0.45.0 has been installed")); + + // Uninstall + let assert = sandbox + .run_bin(|cmd| { + cmd.arg("uninstall").arg("proto").arg("0.45.0").arg("--yes"); + }) + .success(); + + assert!(!tool_dir.exists()); + + assert.stdout(predicate::str::contains( + "proto 0.45.0 has been uninstalled!", + )); + } + #[test] fn installs_and_uninstalls_tool() { let sandbox = create_empty_proto_sandbox(); @@ -87,7 +119,7 @@ mod install_uninstall { // Uninstall let assert = sandbox .run_bin(|cmd| { - cmd.arg("uninstall").arg("node").arg("19.0.0"); + cmd.arg("uninstall").arg("node").arg("19.0.0").arg("--yes"); }) .success(); @@ -274,7 +306,7 @@ mod install_uninstall { // Uninstall sandbox .run_bin(|cmd| { - cmd.arg("uninstall").arg("node").arg("19.0.0"); + cmd.arg("uninstall").arg("node").arg("19.0.0").arg("--yes"); }) .success(); diff --git a/crates/cli/tests/pin_test.rs b/crates/cli/tests/pin_test.rs index 1fda2a4fe..61d11d04e 100644 --- a/crates/cli/tests/pin_test.rs +++ b/crates/cli/tests/pin_test.rs @@ -137,6 +137,26 @@ npm = "9.0.0" "npm = \"6.14.18\"\n" ) } + + #[test] + fn can_set_proto() { + let sandbox = create_empty_proto_sandbox(); + let version_file = sandbox.path().join(".prototools"); + + assert!(!version_file.exists()); + + sandbox + .run_bin(|cmd| { + cmd.arg("pin").arg("proto").arg("0.45.0"); + }) + .success(); + + assert!(version_file.exists()); + assert_eq!( + fs::read_to_string(version_file).unwrap(), + "proto = \"0.45.0\"\n" + ) + } } mod pin_global { @@ -269,6 +289,30 @@ mod pin_global { assert!(!link.exists()); } + + #[test] + fn can_set_proto() { + let sandbox = create_empty_proto_sandbox(); + let version_file = sandbox.path().join(".proto/.prototools"); + + assert!(!version_file.exists()); + + sandbox + .run_bin(|cmd| { + cmd.arg("pin") + .arg("proto") + .arg("0.45.0") + .arg("--to") + .arg("global"); + }) + .success(); + + assert!(version_file.exists()); + assert_eq!( + fs::read_to_string(version_file).unwrap(), + "proto = \"0.45.0\"\n" + ) + } } mod pin_user { @@ -297,4 +341,28 @@ mod pin_user { "node = \"19.0.0\"\n" ) } + + #[test] + fn can_set_proto() { + let sandbox = create_empty_proto_sandbox(); + let version_file = sandbox.path().join(".home/.prototools"); + + assert!(!version_file.exists()); + + sandbox + .run_bin(|cmd| { + cmd.arg("pin") + .arg("proto") + .arg("0.45.0") + .arg("--to") + .arg("home"); + }) + .success(); + + assert!(version_file.exists()); + assert_eq!( + fs::read_to_string(version_file).unwrap(), + "proto = \"0.45.0\"\n" + ) + } } diff --git a/crates/cli/tests/run_test.rs b/crates/cli/tests/run_test.rs index 1bc81c25e..1afe2b0c1 100644 --- a/crates/cli/tests/run_test.rs +++ b/crates/cli/tests/run_test.rs @@ -408,4 +408,92 @@ FOURTH = "ignores-$FIRST-$PARENT" assert_snapshot!(assert.output_standardized()); } } + + mod proto { + use super::*; + + #[test] + fn runs_the_global_exe_if_nothing_installed() { + let sandbox = create_empty_proto_sandbox(); + + let assert = sandbox + .run_bin(|cmd| { + cmd.arg("run").arg("proto").arg("--").arg("--version"); + }) + .success(); + + assert.stdout(predicate::str::contains("0.45.0").not()); + } + + #[test] + fn runs_the_installed_exe() { + let sandbox = create_empty_proto_sandbox(); + + sandbox + .run_bin(|cmd| { + cmd.arg("install").arg("proto").arg("0.45.0"); + }) + .success(); + + let assert = sandbox + .run_bin(|cmd| { + cmd.arg("run") + .arg("proto") + .arg("0.45.0") + .arg("--") + .arg("--version"); + }) + .success(); + + assert.stdout(predicate::str::contains("0.45.0")); + } + + #[test] + fn runs_using_version_detection() { + let sandbox = create_empty_proto_sandbox(); + + sandbox + .run_bin(|cmd| { + cmd.arg("install").arg("proto").arg("0.45.0"); + }) + .success(); + + // Env var + let assert = sandbox + .run_bin(|cmd| { + cmd.env("PROTO_PROTO_VERSION", "0.45.0") + .arg("run") + .arg("proto") + .arg("--") + .arg("--version"); + }) + .success(); + + assert.stdout(predicate::str::contains("0.45.0")); + + // Local version + sandbox.create_file(".prototools", "proto = \"0.45.0\""); + + let assert = sandbox + .run_bin(|cmd| { + cmd.arg("run").arg("proto").arg("--").arg("--version"); + }) + .success(); + + assert.stdout(predicate::str::contains("0.45.0")); + + fs::remove_file(sandbox.path().join(".prototools")).unwrap(); + + // Global version + sandbox.create_file(".proto/.prototools", "proto = \"0.45.0\""); + + let assert = sandbox + .run_bin(|cmd| { + cmd.arg("run").arg("proto").arg("--").arg("--version"); + }) + .success(); + + assert.stdout(predicate::str::contains("0.45.0")); + } + } } diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index c74e1cf38..578be1920 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -329,7 +329,7 @@ impl ProtoConfig { pub fn builtin_proto_plugin(&self) -> PluginLocator { PluginLocator::Url(Box::new(UrlLocator { - url: "https://github.com/moonrepo/plugins/releases/download/proto_tool-v0.5.0/proto_tool.wasm".into() + url: "https://github.com/moonrepo/plugins/releases/download/proto_tool-v0.5.1/proto_tool.wasm".into() })) }