From 41275f4ea5673cc98dffbb6d23fea47d6e343c16 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 21 Aug 2024 12:59:07 -0700 Subject: [PATCH] internal: Split up code into flows. (#597) --- Cargo.lock | 75 +- Cargo.toml | 10 +- crates/cli/src/commands/activate.rs | 11 +- crates/cli/src/commands/bin.rs | 11 +- crates/cli/src/commands/clean.rs | 4 +- crates/cli/src/commands/plugin/info.rs | 44 +- crates/cli/src/commands/run.rs | 18 +- crates/cli/tests/plugins_test.rs | 10 +- crates/core/src/flow/install.rs | 479 ++++++++- crates/core/src/flow/link.rs | 170 +++ crates/core/src/flow/locate.rs | 323 ++++++ crates/core/src/flow/mod.rs | 4 + crates/core/src/flow/resolve.rs | 258 +++++ crates/core/src/flow/setup.rs | 156 +++ crates/core/src/layout/store.rs | 4 +- crates/core/src/tool.rs | 1347 +----------------------- crates/pdk-test-utils/src/macros.rs | 6 +- plugins/Cargo.lock | 166 +-- 18 files changed, 1620 insertions(+), 1476 deletions(-) create mode 100644 crates/core/src/flow/link.rs create mode 100644 crates/core/src/flow/locate.rs create mode 100644 crates/core/src/flow/resolve.rs create mode 100644 crates/core/src/flow/setup.rs diff --git a/Cargo.lock b/Cargo.lock index 272806e25..4c94e6531 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,9 +426,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.16" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c677cd0126f3026d8b093fa29eae5d812fde5c05bc66dbb29d0374eea95113a" +checksum = "2aedc27e53da9ff495f5da6f4325390e71f46f886022b618303042e8ccf4bcac" dependencies = [ "clap", ] @@ -2602,9 +2602,9 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.12.5" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "base64 0.22.1", "bytes", @@ -2644,7 +2644,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg", + "windows-registry", ] [[package]] @@ -2801,9 +2801,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.15" +version = "2.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c478f373151538826ed50feaceeef7095ad435065a48153af789005fd5e44c0d" +checksum = "aeb7ac86243095b70a7920639507b71d51a63390d1ba26c4f60a552fbb914a37" dependencies = [ "sdd", ] @@ -2830,9 +2830,9 @@ dependencies = [ [[package]] name = "schematic" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14bf79931c9c99d9bcdc29a448836dbcea9ad5991285cc83dd4c9f46cf771ac2" +checksum = "a27ae5ae2fb243bce05160f4983d7e258978843e5b9dbb21375c4311cec68300" dependencies = [ "garde", "indexmap 2.4.0", @@ -2852,9 +2852,9 @@ dependencies = [ [[package]] name = "schematic_macros" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f037e36d93185ba8e25049754095e4aabb985ed5a8157da369d40be60d9fa" +checksum = "b67892882d3a0f4e4186f4805a00bb23f978847438ed2efb1b615eb847c8c9cc" dependencies = [ "convert_case", "darling", @@ -2865,9 +2865,9 @@ dependencies = [ [[package]] name = "schematic_types" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce97b2ad673e2183ec94cce86b039e692ecdf1b3b8a1195c90ad31f8adbbd0d" +checksum = "653e1eacf66aa291a836204f0bbe8c2ae7adbc943b3e3ef8f4a2abfac77d12dd" dependencies = [ "indexmap 2.4.0", "semver", @@ -3300,6 +3300,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "sysinfo" @@ -3316,20 +3319,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -3454,9 +3457,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", @@ -4460,7 +4463,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ "windows-implement", "windows-interface", - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -4486,6 +4489,17 @@ dependencies = [ "syn 2.0.74", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -4495,6 +4509,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index dabed8294..713bb2238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ default-members = ["crates/cli"] anyhow = "1.0.86" async-trait = "0.1.81" clap = "4.5.16" -clap_complete = "4.5.16" +clap_complete = "4.5.20" dirs = "5.0.1" extism = "1.0.0" # Lower for consumers extism-pdk = "1.2.1" @@ -16,14 +16,14 @@ indexmap = "2.4.0" miette = "7.2.0" once_cell = "1.19.0" regex = { version = "1.10.6", default-features = false, features = ["std"] } -reqwest = { version = "0.12.5", default-features = false, features = [ +reqwest = { version = "0.12.7", default-features = false, features = [ "charset", "http2", "macos-system-configuration", ] } rustc-hash = "2.0.0" -scc = "2.1.15" -schematic = { version = "0.17.2", default-features = false } +scc = "2.1.16" +schematic = { version = "0.17.3", default-features = false } semver = { version = "1.0.23", features = ["serde"] } serde = { version = "1.0.208", features = ["derive"] } serde_json = "1.0.125" @@ -50,7 +50,7 @@ starbase_utils = { version = "0.8.7", default-features = false, features = [ "toml", ] } thiserror = "1.0.63" -tokio = { version = "1.39.2", features = ["full", "tracing"] } +tokio = { version = "1.39.3", features = ["full", "tracing"] } tracing = "0.1.40" uuid = { version = "1.10.0", features = ["v4"] } diff --git a/crates/cli/src/commands/activate.rs b/crates/cli/src/commands/activate.rs index 20cdbc622..a189380d2 100644 --- a/crates/cli/src/commands/activate.rs +++ b/crates/cli/src/commands/activate.rs @@ -107,16 +107,13 @@ pub async fn activate(session: ProtoSession, args: ActivateArgs) -> AppResult { // Resolve the version and locate executables if tool.is_setup(&version).await? { - tool.locate_exes_dir().await?; - tool.locate_globals_dirs().await?; - // Higher priority over globals - if let Some(exe_dir) = tool.get_exes_dir() { - item.add_path(exe_dir); + if let Some(exes_dir) = tool.locate_exes_dir().await? { + item.add_path(&exes_dir); } - for global_dir in tool.get_globals_dirs() { - item.add_path(global_dir); + for globals_dir in tool.locate_globals_dirs().await? { + item.add_path(&globals_dir); } } diff --git a/crates/cli/src/commands/bin.rs b/crates/cli/src/commands/bin.rs index 43dbabd72..b4a8b9d65 100644 --- a/crates/cli/src/commands/bin.rs +++ b/crates/cli/src/commands/bin.rs @@ -36,10 +36,11 @@ pub async fn bin(session: ProtoSession, args: BinArgs) -> AppResult { let version = detect_version(&tool, args.spec.clone()).await?; tool.resolve_version(&version, true).await?; - tool.create_executables(args.shim, args.bin).await?; if args.bin { - for bin in tool.get_bin_locations().await? { + tool.symlink_bins(true).await?; + + for bin in tool.resolve_bin_locations().await? { if bin.primary { println!("{}", bin.path.display()); return Ok(()); @@ -48,7 +49,9 @@ pub async fn bin(session: ProtoSession, args: BinArgs) -> AppResult { } if args.shim { - for shim in tool.get_shim_locations().await? { + tool.generate_shims(true).await?; + + for shim in tool.resolve_shim_locations().await? { if shim.primary { println!("{}", shim.path.display()); return Ok(()); @@ -56,7 +59,7 @@ pub async fn bin(session: ProtoSession, args: BinArgs) -> AppResult { } } - println!("{}", tool.get_exe_path()?.display()); + println!("{}", tool.locate_exe_file().await?.display()); Ok(()) } diff --git a/crates/cli/src/commands/clean.rs b/crates/cli/src/commands/clean.rs index fbb1e5fbf..7f06e9e89 100644 --- a/crates/cli/src/commands/clean.rs +++ b/crates/cli/src/commands/clean.rs @@ -251,12 +251,12 @@ pub async fn purge_tool(session: &ProtoSession, id: &Id, yes: bool) -> miette::R fs::remove_dir_all(inventory_dir)?; // Delete binaries - for bin in tool.get_bin_locations().await? { + for bin in tool.resolve_bin_locations().await? { session.env.store.unlink_bin(&bin.path)?; } // Delete shims - for shim in tool.get_shim_locations().await? { + for shim in tool.resolve_shim_locations().await? { session.env.store.remove_shim(&shim.path)?; } diff --git a/crates/cli/src/commands/plugin/info.rs b/crates/cli/src/commands/plugin/info.rs index c31360a66..34212c176 100644 --- a/crates/cli/src/commands/plugin/info.rs +++ b/crates/cli/src/commands/plugin/info.rs @@ -2,8 +2,8 @@ use crate::printer::{format_env_var, format_value, Printer}; use crate::session::ProtoSession; use clap::Args; use proto_core::{ - detect_version, EnvVar, ExecutableLocation, Id, PluginLocator, ProtoToolConfig, ToolManifest, - UnresolvedVersionSpec, + detect_version, flow::locate::ExecutableLocation, EnvVar, Id, PluginLocator, ProtoToolConfig, + ToolManifest, UnresolvedVersionSpec, }; use proto_pdk_api::ToolMetadataOutput; use serde::Serialize; @@ -16,8 +16,8 @@ use std::path::PathBuf; pub struct PluginInfo { bins: Vec, config: ProtoToolConfig, + exe_file: PathBuf, exes_dir: Option, - exe_path: PathBuf, globals_dirs: Vec, globals_prefix: Option, id: Id, @@ -46,23 +46,20 @@ pub async fn info(session: ProtoSession, args: InfoPluginArgs) -> AppResult { .unwrap_or_else(|_| UnresolvedVersionSpec::parse("*").unwrap()); tool.resolve_version(&version, false).await?; - tool.create_executables(false, false).await?; - tool.locate_exes_dir().await?; - tool.locate_globals_dirs().await?; let mut config = session.env.load_config()?.to_owned(); let tool_config = config.tools.remove(&tool.id).unwrap_or_default(); - let bins = tool.get_bin_locations().await?; - let shims = tool.get_shim_locations().await?; + let bins = tool.resolve_bin_locations().await?; + let shims = tool.resolve_shim_locations().await?; if args.json { let info = PluginInfo { bins, config: tool_config, - exes_dir: tool.get_exes_dir().map(|dir| dir.to_path_buf()), - exe_path: tool.get_exe_path()?.to_path_buf(), - globals_dirs: tool.get_globals_dirs().to_owned(), - globals_prefix: tool.get_globals_prefix().map(|p| p.to_owned()), + exe_file: tool.locate_exe_file().await?, + exes_dir: tool.locate_exes_dir().await?, + globals_dirs: tool.locate_globals_dirs().await?, + globals_prefix: tool.locate_globals_prefix().await?, inventory_dir: tool.get_inventory_dir(), shims, id: tool.id, @@ -77,11 +74,6 @@ pub async fn info(session: ProtoSession, args: InfoPluginArgs) -> AppResult { return Ok(()); } - let mut version_resolver = tool - .load_version_resolver(&UnresolvedVersionSpec::default()) - .await?; - version_resolver.aliases.extend(tool_config.aliases.clone()); - let mut printer = Printer::new(); printer.header(&tool.id, &tool.metadata.name); @@ -101,22 +93,32 @@ pub async fn info(session: ProtoSession, args: InfoPluginArgs) -> AppResult { // INVENTORY + let exe_file = tool.locate_exe_file().await?; + let exes_dir = tool.locate_exes_dir().await?; + let globals_dirs = tool.locate_globals_dir().await?; + let globals_prefix = tool.locate_globals_prefix().await?; + + let mut version_resolver = tool + .load_version_resolver(&UnresolvedVersionSpec::default()) + .await?; + version_resolver.aliases.extend(tool_config.aliases.clone()); + printer.named_section("Inventory", |p| { p.entry("Store", color::path(tool.get_inventory_dir())); - p.entry("Executable", color::path(tool.get_exe_path()?)); + p.entry("Executable", color::path(exe_file)); - if let Some(dir) = tool.get_exes_dir() { + if let Some(dir) = exes_dir { p.entry("Executables directory", color::path(dir)); } - if let Some(prefix) = tool.get_globals_prefix() { + if let Some(prefix) = globals_prefix { p.entry("Global packages prefix", color::property(prefix)); } p.entry_list( "Global packages directories", - tool.get_globals_dirs().iter().map(color::path), + globals_dirs.iter().map(color::path), Some(color::failure("None")), ); diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index d700e36a8..1d9fb6e01 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -56,8 +56,8 @@ async fn get_executable(tool: &Tool, args: &RunArgs) -> miette::Result miette::Result AppResult { // Run before hook let hook_result = if tool.plugin.has_func("pre_run").await { - tool.locate_globals_dirs().await?; - - let globals_dir = tool.get_globals_dir(); - let globals_prefix = tool.get_globals_prefix(); + let globals_dir = tool.locate_globals_dir().await?; + let globals_prefix = tool.locate_globals_prefix().await?; tool.plugin .call_func_with( "pre_run", RunHook { context: tool.create_context(), - globals_dir: globals_dir.map(|dir| tool.to_virtual_path(dir)), - globals_prefix: globals_prefix.map(|p| p.to_owned()), + globals_dir: globals_dir.map(|dir| tool.to_virtual_path(&dir)), + globals_prefix, passthrough_args: args.passthrough.clone(), }, ) diff --git a/crates/cli/tests/plugins_test.rs b/crates/cli/tests/plugins_test.rs index 191162077..66856868b 100644 --- a/crates/cli/tests/plugins_test.rs +++ b/crates/cli/tests/plugins_test.rs @@ -39,10 +39,16 @@ where let base_dir = proto.store.inventory_dir.join("moon/1.0.0"); if cfg!(windows) { - assert_eq!(tool.get_exe_path().unwrap(), &base_dir.join("moon.exe")); + assert_eq!( + &tool.locate_exe_file().await.unwrap(), + &base_dir.join("moon.exe") + ); assert!(proto.store.shims_dir.join("moon.exe").exists()); } else { - assert_eq!(tool.get_exe_path().unwrap(), &base_dir.join("moon")); + assert_eq!( + &tool.locate_exe_file().await.unwrap(), + &base_dir.join("moon") + ); assert!(proto.store.shims_dir.join("moon").exists()); } } diff --git a/crates/core/src/flow/install.rs b/crates/core/src/flow/install.rs index 67cb4211f..7f2317e50 100644 --- a/crates/core/src/flow/install.rs +++ b/crates/core/src/flow/install.rs @@ -1,4 +1,14 @@ -pub use starbase_utils::net::OnChunkFn; +use crate::checksum::verify_checksum; +use crate::error::ProtoError; +use crate::helpers::{extract_filename_from_url, is_archive_file, is_offline}; +use crate::tool::Tool; +use proto_pdk_api::*; +use proto_shim::*; +use starbase_archive::Archiver; +use starbase_utils::net::DownloadOptions; +use starbase_utils::{fs, net}; +use std::path::Path; +use tracing::{debug, instrument}; #[derive(Default)] pub enum InstallStrategy { @@ -15,6 +25,7 @@ pub enum InstallPhase { Unpack, } +pub use starbase_utils::net::OnChunkFn; pub type OnPhaseFn = Box; #[derive(Default)] @@ -23,3 +34,469 @@ pub struct InstallOptions { pub on_phase_change: Option, pub strategy: InstallStrategy, } + +impl Tool { + /// Return true if the tool has been installed. This is less accurate than `is_setup`, + /// as it only checks for the existence of the inventory directory. + pub fn is_installed(&self) -> bool { + let dir = self.get_product_dir(); + + self.version + .as_ref() + // Canary can be overwritten so treat as not-installed + .is_some_and(|v| { + !v.is_latest() + && !v.is_canary() + && self.inventory.manifest.installed_versions.contains(v) + }) + && dir.exists() + && !fs::is_dir_locked(dir) + } + + /// Verify the downloaded file using the checksum strategy for the tool. + /// Common strategies are SHA256 and MD5. + #[instrument(skip(self))] + pub async fn verify_checksum( + &self, + checksum_file: &Path, + download_file: &Path, + checksum_public_key: Option<&str>, + ) -> miette::Result { + debug!( + tool = self.id.as_str(), + download_file = ?download_file, + checksum_file = ?checksum_file, + "Verifying checksum of downloaded file", + ); + + // Allow plugin to provide their own checksum verification method + let verified = if self.plugin.has_func("verify_checksum").await { + let output: VerifyChecksumOutput = self + .plugin + .call_func_with( + "verify_checksum", + VerifyChecksumInput { + checksum_file: self.to_virtual_path(checksum_file), + download_file: self.to_virtual_path(download_file), + context: self.create_context(), + }, + ) + .await?; + + output.verified + + // Otherwise attempt to verify it ourselves + } else { + verify_checksum(download_file, checksum_file, checksum_public_key)? + }; + + if verified { + debug!( + tool = self.id.as_str(), + "Successfully verified, checksum matches" + ); + + return Ok(true); + } + + Err(ProtoError::InvalidChecksum { + checksum: checksum_file.to_path_buf(), + download: download_file.to_path_buf(), + } + .into()) + } + + #[instrument(skip(self))] + pub async fn build_from_source(&self, _install_dir: &Path) -> miette::Result<()> { + debug!( + tool = self.id.as_str(), + "Installing tool by building from source" + ); + + if !self.plugin.has_func("build_instructions").await { + return Err(ProtoError::UnsupportedBuildFromSource { + tool: self.get_name().to_owned(), + } + .into()); + } + + // let temp_dir = self.get_temp_dir(); + + // let options: BuildInstructionsOutput = self.plugin.cache_func_with( + // "build_instructions", + // BuildInstructionsInput { + // context: self.create_context(), + // }, + // )?; + + // match &options.source { + // // Should this do anything? + // SourceLocation::None => { + // return Ok(()); + // } + + // // Download from archive + // SourceLocation::Archive { url: archive_url } => { + // let download_file = temp_dir.join(extract_filename_from_url(archive_url)?); + + // debug!( + // tool = self.id.as_str(), + // archive_url, + // download_file = ?download_file, + // install_dir = ?install_dir, + // "Attempting to download and unpack sources", + // ); + + // net::download_from_url_with_client( + // archive_url, + // &download_file, + // self.proto.get_plugin_loader()?.get_client()?, + // ) + // .await?; + + // Archiver::new(install_dir, &download_file).unpack_from_ext()?; + // } + + // // Clone from Git repository + // SourceLocation::Git { + // url: repo_url, + // reference: ref_name, + // submodules, + // } => { + // debug!( + // tool = self.id.as_str(), + // repo_url, + // ref_name, + // install_dir = ?install_dir, + // "Attempting to clone a Git repository", + // ); + + // let run_git = |args: &[&str]| -> miette::Result<()> { + // let status = Command::new("git") + // .args(args) + // .current_dir(install_dir) + // .spawn() + // .into_diagnostic()? + // .wait() + // .into_diagnostic()?; + + // if !status.success() { + // return Err(ProtoError::BuildFailed { + // tool: self.get_name().to_owned(), + // url: repo_url.clone(), + // status: format!("exit code {}", status), + // } + // .into()); + // } + + // Ok(()) + // }; + + // // TODO, pull if already cloned + + // fs::create_dir_all(install_dir)?; + + // run_git(&[ + // "clone", + // if *submodules { + // "--recurse-submodules" + // } else { + // "" + // }, + // repo_url, + // ".", + // ])?; + + // run_git(&["checkout", ref_name])?; + // } + // }; + + Ok(()) + } + + /// Download the tool (as an archive) from its distribution registry + /// into the `~/.proto/tools/` folder, and optionally verify checksums. + #[instrument(skip(self, options))] + pub async fn install_from_prebuilt( + &self, + install_dir: &Path, + mut options: InstallOptions, + ) -> miette::Result<()> { + debug!( + tool = self.id.as_str(), + "Installing tool by downloading a pre-built archive" + ); + + let client = self.proto.get_plugin_loader()?.get_client()?; + let temp_dir = self.get_temp_dir(); + + let output: DownloadPrebuiltOutput = self + .plugin + .cache_func_with( + "download_prebuilt", + DownloadPrebuiltInput { + context: self.create_context(), + install_dir: self.to_virtual_path(install_dir), + }, + ) + .await?; + + // Download the prebuilt + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::Download); + }); + + let download_url = output.download_url; + let download_file = temp_dir.join(match output.download_name { + Some(name) => name, + None => extract_filename_from_url(&download_url)?, + }); + + if download_file.exists() { + debug!( + tool = self.id.as_str(), + "Tool already downloaded, continuing" + ); + } else { + debug!(tool = self.id.as_str(), "Tool not downloaded, downloading"); + + net::download_from_url_with_options( + &download_url, + &download_file, + DownloadOptions { + client: Some(client), + on_chunk: options.on_download_chunk.take(), + }, + ) + .await?; + } + + // Verify the checksum if applicable + if let Some(checksum_url) = output.checksum_url { + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::Verify); + }); + + let checksum_file = temp_dir.join(match output.checksum_name { + Some(name) => name, + None => extract_filename_from_url(&checksum_url)?, + }); + + if !checksum_file.exists() { + debug!( + tool = self.id.as_str(), + "Checksum does not exist, downloading" + ); + + net::download_from_url_with_options( + &checksum_url, + &checksum_file, + DownloadOptions { + client: Some(client), + on_chunk: None, + }, + ) + .await?; + } + + self.verify_checksum( + &checksum_file, + &download_file, + output.checksum_public_key.as_deref(), + ) + .await?; + } + + // Attempt to unpack the archive + debug!( + tool = self.id.as_str(), + download_file = ?download_file, + install_dir = ?install_dir, + "Attempting to unpack archive", + ); + + if self.plugin.has_func("unpack_archive").await { + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::Unpack); + }); + + self.plugin + .call_func_without_output( + "unpack_archive", + UnpackArchiveInput { + input_file: self.to_virtual_path(&download_file), + output_dir: self.to_virtual_path(install_dir), + context: self.create_context(), + }, + ) + .await?; + } + // Is an archive, unpack it + else if is_archive_file(&download_file) { + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::Unpack); + }); + + let mut archiver = Archiver::new(install_dir, &download_file); + + if let Some(prefix) = &output.archive_prefix { + archiver.set_prefix(prefix); + } + + let (ext, unpacked_path) = archiver.unpack_from_ext()?; + + // If the archive was `.gz` without tar or other formats, + // it's a single file, so assume a binary and update perms + if ext == "gz" && unpacked_path.is_file() { + fs::update_perms(unpacked_path, None)?; + } + } + // Not an archive, assume a binary and copy + else { + let install_path = install_dir.join(get_exe_file_name(&self.id)); + + fs::rename(&download_file, &install_path)?; + fs::update_perms(install_path, None)?; + } + + Ok(()) + } + + /// Install a tool into proto, either by downloading and unpacking + /// a pre-built archive, or by using a native installation method. + #[instrument(skip(self, options))] + pub async fn install(&mut self, options: InstallOptions) -> miette::Result { + if self.is_installed() { + debug!( + tool = self.id.as_str(), + "Tool already installed, continuing" + ); + + return Ok(false); + } + + if is_offline() { + return Err(ProtoError::InternetConnectionRequired.into()); + } + + let install_dir = self.get_product_dir(); + let mut installed = false; + + // Lock the install directory. If the inventory has been overridden, + // lock the internal proto tool directory instead. + let install_lock = fs::lock_directory(if self.metadata.inventory.override_dir.is_some() { + self.proto + .store + .inventory_dir + .join(self.id.as_str()) + .join(self.get_resolved_version().to_string()) + } else { + install_dir.clone() + })?; + + // If this function is defined, it acts like an escape hatch and + // takes precedence over all other install strategies + if self.plugin.has_func("native_install").await { + debug!(tool = self.id.as_str(), "Installing tool natively"); + + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::Native); + }); + + let output: NativeInstallOutput = self + .plugin + .call_func_with( + "native_install", + NativeInstallInput { + context: self.create_context(), + install_dir: self.to_virtual_path(&install_dir), + }, + ) + .await?; + + if !output.installed && !output.skip_install { + return Err(ProtoError::InstallFailed { + tool: self.get_name().to_owned(), + error: output.error.unwrap_or_default(), + } + .into()); + + // If native install fails, attempt other installers + } else { + installed = output.installed; + } + } + + if !installed { + // // Build the tool from source + // if build { + // self.build_from_source(&install_dir).await?; + + // // Install from a prebuilt archive + // } else { + // self.install_from_prebuilt(&install_dir).await?; + // } + + self.install_from_prebuilt(&install_dir, options).await?; + } + + install_lock.unlock()?; + + debug!( + tool = self.id.as_str(), + install_dir = ?install_dir, + "Successfully installed tool", + ); + + Ok(true) + } + + /// Uninstall the tool by deleting the current install directory. + #[instrument(skip_all)] + pub async fn uninstall(&self) -> miette::Result { + let install_dir = self.get_product_dir(); + + if !install_dir.exists() { + debug!( + tool = self.id.as_str(), + "Tool has not been installed, aborting" + ); + + return Ok(false); + } + + if self.plugin.has_func("native_uninstall").await { + debug!(tool = self.id.as_str(), "Uninstalling tool natively"); + + let output: NativeUninstallOutput = self + .plugin + .call_func_with( + "native_uninstall", + NativeUninstallInput { + context: self.create_context(), + }, + ) + .await?; + + if !output.uninstalled && !output.skip_uninstall { + return Err(ProtoError::UninstallFailed { + tool: self.get_name().to_owned(), + error: output.error.unwrap_or_default(), + } + .into()); + } + } + + debug!( + tool = self.id.as_str(), + install_dir = ?install_dir, + "Deleting install directory" + ); + + fs::remove_dir_all(install_dir)?; + + debug!(tool = self.id.as_str(), "Successfully uninstalled tool"); + + Ok(true) + } +} diff --git a/crates/core/src/flow/link.rs b/crates/core/src/flow/link.rs new file mode 100644 index 000000000..47f7335e6 --- /dev/null +++ b/crates/core/src/flow/link.rs @@ -0,0 +1,170 @@ +use crate::shim_registry::{Shim, ShimRegistry, ShimsMap}; +use crate::tool::Tool; +use miette::IntoDiagnostic; +use proto_pdk_api::*; +use proto_shim::*; +use starbase_utils::fs; +use std::collections::BTreeMap; +use tracing::{debug, instrument, warn}; + +impl Tool { + /// Create shim files for the current tool if they are missing or out of date. + /// If find only is enabled, will only check if they exist, and not create. + #[instrument(skip(self))] + pub async fn generate_shims(&mut self, force: bool) -> miette::Result<()> { + let shims = self.resolve_shim_locations().await?; + + if shims.is_empty() { + return Ok(()); + } + + let is_outdated = self.inventory.manifest.shim_version != SHIM_VERSION; + let force_create = force || is_outdated; + let find_only = !force_create; + + if force_create { + debug!( + tool = self.id.as_str(), + shims_dir = ?self.proto.store.shims_dir, + shim_version = SHIM_VERSION, + "Creating shims as they either do not exist, or are outdated" + ); + + self.inventory.manifest.shim_version = SHIM_VERSION; + self.inventory.manifest.save()?; + } + + let mut registry: ShimsMap = BTreeMap::default(); + registry.insert(self.id.to_string(), Shim::default()); + + let mut to_create = vec![]; + + for shim in shims { + let mut shim_entry = Shim::default(); + + // Handle before and after args + if let Some(before_args) = shim.config.shim_before_args { + shim_entry.before_args = match before_args { + StringOrVec::String(value) => shell_words::split(&value).into_diagnostic()?, + StringOrVec::Vec(value) => value, + }; + } + + if let Some(after_args) = shim.config.shim_after_args { + shim_entry.after_args = match after_args { + StringOrVec::String(value) => shell_words::split(&value).into_diagnostic()?, + StringOrVec::Vec(value) => value, + }; + } + + if let Some(env_vars) = shim.config.shim_env_vars { + shim_entry.env_vars.extend(env_vars); + } + + if !shim.primary { + shim_entry.parent = Some(self.id.to_string()); + + // Only use --alt when the secondary executable exists + if shim.config.exe_path.is_some() { + shim_entry.alt_bin = Some(true); + } + } + + // Create the shim file by copying the source bin + if force_create || find_only && !shim.path.exists() { + to_create.push(shim.path); + } + + // Update the registry + registry.insert(shim.name.clone(), shim_entry); + } + + // Only lock the directory and create shims if necessary + if !to_create.is_empty() { + let _lock = fs::lock_directory(&self.proto.store.shims_dir)?; + + for shim_path in to_create { + self.proto.store.create_shim(&shim_path)?; + + debug!( + tool = self.id.as_str(), + shim = ?shim_path, + shim_version = SHIM_VERSION, + "Creating shim" + ); + } + + ShimRegistry::update(&self.proto, registry)?; + } + + Ok(()) + } + + /// Symlink all primary and secondary binaries for the current tool. + #[instrument(skip(self))] + pub async fn symlink_bins(&mut self, force: bool) -> miette::Result<()> { + let bins = self.resolve_bin_locations().await?; + + if bins.is_empty() { + return Ok(()); + } + + if force { + debug!( + tool = self.id.as_str(), + bins_dir = ?self.proto.store.bin_dir, + "Creating symlinks to the original tool executables" + ); + } + + let tool_dir = self.get_product_dir(); + let mut to_create = vec![]; + + for bin in bins { + let input_path = tool_dir.join( + bin.config + .exe_link_path + .as_ref() + .or(bin.config.exe_path.as_ref()) + .unwrap(), + ); + let output_path = bin.path; + + if !input_path.exists() { + warn!( + tool = self.id.as_str(), + source = ?input_path, + target = ?output_path, + "Unable to symlink binary, source file does not exist" + ); + + continue; + } + + if !force && output_path.exists() { + continue; + } + + to_create.push((input_path, output_path)); + } + + // Only lock the directory and create bins if necessary + if !to_create.is_empty() { + let _lock = fs::lock_directory(&self.proto.store.bin_dir)?; + + for (input_path, output_path) in to_create { + debug!( + tool = self.id.as_str(), + source = ?input_path, + target = ?output_path, + "Creating binary symlink" + ); + + self.proto.store.unlink_bin(&output_path)?; + self.proto.store.link_bin(&output_path, &input_path)?; + } + } + + Ok(()) + } +} diff --git a/crates/core/src/flow/locate.rs b/crates/core/src/flow/locate.rs new file mode 100644 index 000000000..ddd00e71c --- /dev/null +++ b/crates/core/src/flow/locate.rs @@ -0,0 +1,323 @@ +use crate::error::ProtoError; +use crate::helpers::ENV_VAR; +use crate::tool::Tool; +use proto_pdk_api::{ExecutableConfig, LocateExecutablesInput, LocateExecutablesOutput}; +use proto_shim::{get_exe_file_name, get_shim_file_name}; +use serde::Serialize; +use starbase_utils::fs; +use std::env; +use std::path::PathBuf; +use tracing::{debug, instrument}; + +// Executable = File within the tool's install directory +// Binary/shim = File within proto's store directories + +#[derive(Debug, Default, Serialize)] +pub struct ExecutableLocation { + pub config: ExecutableConfig, + pub name: String, + pub path: PathBuf, + pub primary: bool, +} + +impl Tool { + pub(crate) async fn call_locate_executables(&self) -> miette::Result { + self.plugin + .cache_func_with( + "locate_executables", + LocateExecutablesInput { + context: self.create_context(), + }, + ) + .await + } + + /// Return location information for the primary executable within the tool directory. + pub async fn resolve_primary_exe_location(&self) -> miette::Result> { + let output = self.call_locate_executables().await?; + + if let Some(primary) = output.primary { + if let Some(exe_path) = &primary.exe_path { + return Ok(Some(ExecutableLocation { + path: self.get_product_dir().join(exe_path), + name: self.id.to_string(), + config: primary, + primary: true, + })); + } + } + + Ok(None) + } + + /// Return location information for all secondary executables within the tool directory. + pub async fn resolve_secondary_exe_locations(&self) -> miette::Result> { + let output = self.call_locate_executables().await?; + let mut locations = vec![]; + + for (name, secondary) in output.secondary { + if let Some(exe_path) = &secondary.exe_path { + locations.push(ExecutableLocation { + path: self.get_product_dir().join(exe_path), + name, + config: secondary, + primary: false, + }); + } + } + + Ok(locations) + } + + /// Return a list of all binaries that get created in `~/.proto/bin`. + /// The list will contain the executable config, and an absolute path + /// to the binaries final location. + pub async fn resolve_bin_locations(&self) -> miette::Result> { + let output = self.call_locate_executables().await?; + let mut locations = vec![]; + + let mut add = |name: &str, config: ExecutableConfig, primary: bool| { + if !config.no_bin + && config + .exe_link_path + .as_ref() + .or(config.exe_path.as_ref()) + .is_some() + { + locations.push(ExecutableLocation { + path: self.proto.store.bin_dir.join(get_exe_file_name(name)), + name: name.to_owned(), + config, + primary, + }); + } + }; + + if let Some(primary) = output.primary { + add(&self.id, primary, true); + } + + for (name, secondary) in output.secondary { + add(&name, secondary, false); + } + + Ok(locations) + } + + /// Return a list of all shims that get created in `~/.proto/shims`. + /// The list will contain the executable config, and an absolute path + /// to the shims final location. + pub async fn resolve_shim_locations(&self) -> miette::Result> { + let output = self.call_locate_executables().await?; + let mut locations = vec![]; + + let mut add = |name: &str, config: ExecutableConfig, primary: bool| { + if !config.no_shim { + locations.push(ExecutableLocation { + path: self.proto.store.shims_dir.join(get_shim_file_name(name)), + name: name.to_owned(), + config: config.clone(), + primary, + }); + } + }; + + if let Some(primary) = output.primary { + add(&self.id, primary, true); + } + + for (name, secondary) in output.secondary { + add(&name, secondary, false); + } + + Ok(locations) + } + + /// Locate the primary executable from the tool directory. + #[instrument(skip_all)] + pub async fn locate_exe_file(&mut self) -> miette::Result { + if self.exe_file.is_some() { + return Ok(self.exe_file.clone().unwrap()); + } + + debug!( + tool = self.id.as_str(), + "Locating primary executable for tool" + ); + + let exe_file = if let Some(location) = self.resolve_primary_exe_location().await? { + location.path + } else { + self.get_product_dir().join(self.id.as_str()) + }; + + if exe_file.exists() { + debug!(tool = self.id.as_str(), exe_path = ?exe_file, "Found an executable"); + + self.exe_file = Some(exe_file.clone()); + + return Ok(exe_file); + } + + Err(ProtoError::MissingToolExecutable { + tool: self.get_name().to_owned(), + path: exe_file, + } + .into()) + } + + /// Locate the directory that local executables are installed to. + #[instrument(skip_all)] + pub async fn locate_exes_dir(&mut self) -> miette::Result> { + if self.exes_dir.is_none() { + if !self.plugin.has_func("locate_executables").await { + return Ok(None); + } + + let output = self.call_locate_executables().await?; + + if let Some(exes_dir) = output.exes_dir { + self.exes_dir = Some(self.get_product_dir().join(exes_dir)); + } + } + + Ok(self.exes_dir.clone()) + } + + /// Return an absolute path to the globals directory that actually exists + /// and contains files (binaries). + #[instrument(skip_all)] + pub async fn locate_globals_dir(&mut self) -> miette::Result> { + if self.globals_dir.is_none() { + let globals_dirs = self.locate_globals_dirs().await?; + let lookup_count = globals_dirs.len() - 1; + + for (index, dir) in globals_dirs.into_iter().enumerate() { + if !dir.exists() { + continue; + } + + let has_files = fs::read_dir(&dir).is_ok_and(|list| { + !list + .into_iter() + .filter(|entry| entry.path().is_file()) + .collect::>() + .is_empty() + }); + + if has_files { + debug!(tool = self.id.as_str(), dir = ?dir, "Found a usable globals directory"); + + self.globals_dir = Some(dir); + break; + } + + if index == lookup_count { + debug!( + tool = self.id.as_str(), + dir = ?dir, + "No usable globals directory found, falling back to the last entry", + ); + + self.globals_dir = Some(dir); + break; + } + } + } + + Ok(self.globals_dir.clone()) + } + + /// Locate the directories that global packages are installed to. + /// Will expand environment variables, and filter out invalid paths. + #[instrument(skip_all)] + pub async fn locate_globals_dirs(&mut self) -> miette::Result> { + if !self.globals_dirs.is_empty() { + return Ok(self.globals_dirs.clone()); + } + + if !self.plugin.has_func("locate_executables").await { + return Ok(vec![]); + } + + debug!( + tool = self.id.as_str(), + "Locating globals directories for tool" + ); + + let install_dir = self.get_product_dir(); + let output = self.call_locate_executables().await?; + + // Set the prefix for simpler caching + self.globals_prefix = output.globals_prefix; + + // Find all possible global directories that packages can be installed to + let mut resolved_dirs = vec![]; + + 'outer: for dir_lookup in output.globals_lookup_dirs { + let mut dir = dir_lookup.clone(); + + // If a lookup contains an env var, find and replace it. + // If the var is not defined or is empty, skip this lookup. + for cap in ENV_VAR.captures_iter(&dir_lookup) { + let find_by = cap.get(0).unwrap().as_str(); + + let replace_with = match find_by { + "$CWD" | "$PWD" => self.proto.cwd.clone(), + "$HOME" | "$USERHOME" => self.proto.home.clone(), + "$PROTO_HOME" | "$PROTO_ROOT" => self.proto.root.clone(), + "$TOOL_DIR" => install_dir.clone(), + _ => match env::var_os(cap.get(1).unwrap().as_str()) { + Some(value) => PathBuf::from(value), + None => { + continue 'outer; + } + }, + }; + + if let Some(replacement) = replace_with.to_str() { + dir = dir.replace(find_by, replacement); + } else { + continue 'outer; + } + } + + let dir = if let Some(dir_suffix) = dir.strip_prefix('~') { + self.proto.home.join(dir_suffix) + } else { + PathBuf::from(dir) + }; + + // Don't use a set as we need to persist the order! + if !resolved_dirs.contains(&dir) { + resolved_dirs.push(dir); + } + } + + debug!( + tool = self.id.as_str(), + dirs = ?resolved_dirs, + "Located possible globals directories", + ); + + self.globals_dirs = resolved_dirs.clone(); + + Ok(resolved_dirs) + } + + /// Return a string that all globals are prefixed with. Will be used for filtering and listing. + #[instrument(skip_all)] + pub async fn locate_globals_prefix(&mut self) -> miette::Result> { + if self.globals_prefix.is_none() { + if !self.plugin.has_func("locate_executables").await { + return Ok(None); + } + + let output = self.call_locate_executables().await?; + + self.globals_prefix = output.globals_prefix; + } + + Ok(self.globals_prefix.clone()) + } +} diff --git a/crates/core/src/flow/mod.rs b/crates/core/src/flow/mod.rs index e5b1d65b7..7ed3510ad 100644 --- a/crates/core/src/flow/mod.rs +++ b/crates/core/src/flow/mod.rs @@ -1 +1,5 @@ pub mod install; +pub mod link; +pub mod locate; +pub mod resolve; +pub mod setup; diff --git a/crates/core/src/flow/resolve.rs b/crates/core/src/flow/resolve.rs new file mode 100644 index 000000000..aff7cd288 --- /dev/null +++ b/crates/core/src/flow/resolve.rs @@ -0,0 +1,258 @@ +use crate::error::ProtoError; +use crate::helpers::is_offline; +use crate::tool::Tool; +use crate::version_resolver::VersionResolver; +use proto_pdk_api::*; +use starbase_utils::fs; +use std::env; +use std::path::{Path, PathBuf}; +use tracing::{debug, instrument, trace}; + +impl Tool { + /// Load available versions to install and return a resolver instance. + /// To reduce network overhead, results will be cached for 24 hours. + #[instrument(skip(self))] + pub async fn load_version_resolver( + &self, + initial_version: &UnresolvedVersionSpec, + ) -> miette::Result { + debug!(tool = self.id.as_str(), "Loading available versions"); + + let mut versions = LoadVersionsOutput::default(); + let mut cached = false; + + if let Some(cached_versions) = self.inventory.load_remote_versions(!self.cache)? { + versions = cached_versions; + cached = true; + } + + // Nothing cached, so load from the plugin + if !cached { + if is_offline() { + return Err(ProtoError::InternetConnectionRequiredForVersion { + command: format!("{}_VERSION=1.2.3 {}", self.get_env_var_prefix(), self.id), + bin_dir: self.proto.store.bin_dir.clone(), + } + .into()); + } + + if env::var("PROTO_BYPASS_VERSION_CHECK").is_err() { + versions = self + .plugin + .cache_func_with( + "load_versions", + LoadVersionsInput { + initial: initial_version.to_owned(), + }, + ) + .await?; + + self.inventory.save_remote_versions(&versions)?; + } + } + + // Cache the results and create a resolver + let mut resolver = VersionResolver::from_output(versions); + + resolver.with_manifest(&self.inventory.manifest); + + let config = self.proto.load_config()?; + + if let Some(tool_config) = config.tools.get(&self.id) { + resolver.with_config(tool_config); + } + + Ok(resolver) + } + + /// Given an initial version, resolve it to a fully qualifed and semantic version + /// (or alias) according to the tool's ecosystem. + #[instrument(skip(self))] + pub async fn resolve_version( + &mut self, + initial_version: &UnresolvedVersionSpec, + short_circuit: bool, + ) -> miette::Result { + if self.version.is_some() { + return Ok(self.get_resolved_version()); + } + + debug!( + tool = self.id.as_str(), + initial_version = initial_version.to_string(), + "Resolving a semantic version or alias", + ); + + // If we have a fully qualified semantic version, + // exit early and assume the version is legitimate! + // Also canary is a special type that we can simply just use. + if short_circuit + && matches!( + initial_version, + UnresolvedVersionSpec::Calendar(_) | UnresolvedVersionSpec::Semantic(_) + ) + || matches!(initial_version, UnresolvedVersionSpec::Canary) + { + let version = initial_version.to_resolved_spec(); + + debug!( + tool = self.id.as_str(), + version = version.to_string(), + "Resolved to {} (without validation)", + version + ); + + self.set_version(version.clone()); + + return Ok(version); + } + + let resolver = self.load_version_resolver(initial_version).await?; + let version = self + .resolve_version_candidate(&resolver, initial_version, true) + .await?; + + debug!( + tool = self.id.as_str(), + version = version.to_string(), + "Resolved to {}", + version + ); + + self.set_version(version.clone()); + + Ok(version) + } + + #[instrument(skip(self, resolver))] + pub async fn resolve_version_candidate( + &self, + resolver: &VersionResolver<'_>, + initial_candidate: &UnresolvedVersionSpec, + with_manifest: bool, + ) -> miette::Result { + let resolve = |candidate: &UnresolvedVersionSpec| { + let result = if with_manifest { + resolver.resolve(candidate) + } else { + resolver.resolve_without_manifest(candidate) + }; + + result.ok_or_else(|| ProtoError::VersionResolveFailed { + tool: self.get_name().to_owned(), + version: candidate.to_string(), + }) + }; + + if self.plugin.has_func("resolve_version").await { + let output: ResolveVersionOutput = self + .plugin + .call_func_with( + "resolve_version", + ResolveVersionInput { + initial: initial_candidate.to_owned(), + }, + ) + .await?; + + if let Some(candidate) = output.candidate { + debug!( + tool = self.id.as_str(), + candidate = candidate.to_string(), + "Received a possible version or alias to use", + ); + + return Ok(resolve(&candidate)?); + } + + if let Some(candidate) = output.version { + debug!( + tool = self.id.as_str(), + version = candidate.to_string(), + "Received an explicit version or alias to use", + ); + + return Ok(candidate); + } + } + + Ok(resolve(initial_candidate)?) + } + + /// Attempt to detect an applicable version from the provided directory. + #[instrument(skip(self))] + pub async fn detect_version_from( + &self, + current_dir: &Path, + ) -> miette::Result> { + if !self.plugin.has_func("detect_version_files").await { + return Ok(None); + } + + let has_parser = self.plugin.has_func("parse_version_file").await; + let output: DetectVersionOutput = self.plugin.cache_func("detect_version_files").await?; + + if !output.ignore.is_empty() { + if let Some(dir) = current_dir.to_str() { + if output.ignore.iter().any(|ignore| dir.contains(ignore)) { + return Ok(None); + } + } + } + + trace!( + tool = self.id.as_str(), + dir = ?current_dir, + "Attempting to detect a version from directory" + ); + + for file in output.files { + let file_path = current_dir.join(&file); + + if !file_path.exists() { + continue; + } + + let content = fs::read_file(&file_path)?.trim().to_owned(); + + if content.is_empty() { + continue; + } + + let version = if has_parser { + let output: ParseVersionFileOutput = self + .plugin + .call_func_with( + "parse_version_file", + ParseVersionFileInput { + content, + file: file.clone(), + }, + ) + .await?; + + if output.version.is_none() { + continue; + } + + output.version.unwrap() + } else { + UnresolvedVersionSpec::parse(&content).map_err(|error| ProtoError::VersionSpec { + version: content, + error: Box::new(error), + })? + }; + + debug!( + tool = self.id.as_str(), + file = ?file_path, + version = version.to_string(), + "Detected a version" + ); + + return Ok(Some((version, file_path))); + } + + Ok(None) + } +} diff --git a/crates/core/src/flow/setup.rs b/crates/core/src/flow/setup.rs new file mode 100644 index 000000000..f78e92743 --- /dev/null +++ b/crates/core/src/flow/setup.rs @@ -0,0 +1,156 @@ +use crate::flow::install::InstallOptions; +use crate::proto_config::ProtoConfig; +use crate::tool::Tool; +use crate::tool_manifest::ToolManifestVersion; +use proto_pdk_api::*; +use starbase_utils::fs; +use tracing::{debug, instrument}; + +impl Tool { + /// Return true if the tool has been setup (installed and binaries are located). + #[instrument(skip(self))] + pub async fn is_setup( + &mut self, + initial_version: &UnresolvedVersionSpec, + ) -> miette::Result { + self.resolve_version(initial_version, true).await?; + + let install_dir = self.get_product_dir(); + + debug!( + tool = self.id.as_str(), + install_dir = ?install_dir, + "Checking if tool is installed", + ); + + if self.is_installed() { + debug!( + tool = self.id.as_str(), + install_dir = ?install_dir, + "Tool has already been installed, locating binaries and shims", + ); + + if self.exe_file.is_none() { + self.generate_shims(false).await?; + self.symlink_bins(false).await?; + self.locate_exe_file().await?; + } + + return Ok(true); + } + + debug!(tool = self.id.as_str(), "Tool has not been installed"); + + Ok(false) + } + + /// Setup the tool by resolving a semantic version, installing the tool, + /// locating binaries, creating shims, and more. + #[instrument(skip(self, options))] + pub async fn setup( + &mut self, + initial_version: &UnresolvedVersionSpec, + options: InstallOptions, + ) -> miette::Result { + self.resolve_version(initial_version, false).await?; + + if !self.install(options).await? { + return Ok(false); + } + + self.generate_shims(false).await?; + self.symlink_bins(false).await?; + self.cleanup().await?; + + let version = self.get_resolved_version(); + let default_version = self + .metadata + .default_version + .clone() + .unwrap_or_else(|| version.to_unresolved_spec()); + + // Add version to manifest + let manifest = &mut self.inventory.manifest; + manifest.installed_versions.insert(version.clone()); + manifest + .versions + .insert(version.clone(), ToolManifestVersion::default()); + manifest.save()?; + + // Pin the global version + ProtoConfig::update(self.proto.get_config_dir(true), |config| { + config + .versions + .get_or_insert(Default::default()) + .entry(self.id.clone()) + .or_insert(default_version); + })?; + + // Allow plugins to override manifest + self.sync_manifest().await?; + + Ok(true) + } + + /// Teardown the tool by uninstalling the current version, removing the version + /// from the manifest, and cleaning up temporary files. Return true if the teardown occurred. + #[instrument(skip_all)] + pub async fn teardown(&mut self) -> miette::Result { + self.cleanup().await?; + + if !self.uninstall().await? { + return Ok(false); + } + + let version = self.get_resolved_version(); + let mut removed_default_version = false; + + // Remove version from manifest + let manifest = &mut self.inventory.manifest; + manifest.installed_versions.remove(&version); + manifest.versions.remove(&version); + manifest.save()?; + + // Unpin global version if a match + ProtoConfig::update(self.proto.get_config_dir(true), |config| { + if let Some(versions) = &mut config.versions { + if versions.get(&self.id).is_some_and(|v| v == &version) { + debug!("Unpinning global version"); + + versions.remove(&self.id); + removed_default_version = true; + } + } + })?; + + // If no more default version, delete the symlink, + // otherwise the OS will throw errors for missing sources + if removed_default_version || self.inventory.manifest.installed_versions.is_empty() { + for bin in self.resolve_bin_locations().await? { + self.proto.store.unlink_bin(&bin.path)?; + } + } + + // If no more versions in general, delete all shims + if self.inventory.manifest.installed_versions.is_empty() { + for shim in self.resolve_shim_locations().await? { + self.proto.store.remove_shim(&shim.path)?; + } + } + + Ok(true) + } + + /// Delete temporary files and downloads for the current version. + #[instrument(skip_all)] + pub async fn cleanup(&mut self) -> miette::Result<()> { + debug!( + tool = self.id.as_str(), + "Cleaning up temporary files and downloads" + ); + + fs::remove_dir_all(self.get_temp_dir())?; + + Ok(()) + } +} diff --git a/crates/core/src/layout/store.rs b/crates/core/src/layout/store.rs index d7680cdf3..b92e04d2a 100644 --- a/crates/core/src/layout/store.rs +++ b/crates/core/src/layout/store.rs @@ -107,7 +107,7 @@ impl Store { #[cfg(windows)] fs::copy_file(src_path, bin_path)?; - #[cfg(not(windows))] + #[cfg(unix)] std::os::unix::fs::symlink(src_path, bin_path).into_diagnostic()?; Ok(()) @@ -120,7 +120,7 @@ impl Store { fs::remove_file(bin_path)?; // Unix uses symlinks - #[cfg(not(windows))] + #[cfg(unix)] fs::remove_link(bin_path)?; Ok(()) diff --git a/crates/core/src/tool.rs b/crates/core/src/tool.rs index 3912c53bb..a38304190 100644 --- a/crates/core/src/tool.rs +++ b/crates/core/src/tool.rs @@ -1,44 +1,20 @@ -use crate::checksum::verify_checksum; use crate::error::ProtoError; -use crate::flow::install::{InstallOptions, InstallPhase}; -use crate::helpers::{ - extract_filename_from_url, get_proto_version, is_archive_file, is_offline, ENV_VAR, -}; +use crate::helpers::get_proto_version; use crate::layout::{Inventory, Product}; use crate::proto::ProtoEnvironment; -use crate::proto_config::ProtoConfig; -use crate::shim_registry::{Shim, ShimRegistry, ShimsMap}; -use crate::tool_manifest::ToolManifestVersion; -use crate::version_resolver::VersionResolver; -use miette::IntoDiagnostic; use proto_pdk_api::*; -use proto_shim::*; use rustc_hash::{FxHashMap, FxHashSet}; -use serde::Serialize; -use starbase_archive::Archiver; use starbase_styles::color; -use starbase_utils::net::DownloadOptions; -use starbase_utils::{fs, net}; -use std::collections::BTreeMap; -use std::env; -use std::fmt::{self, Debug}; +use std::fmt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, instrument, trace, warn}; +use tracing::{debug, instrument}; use warpgate::{ host::{create_host_functions, HostData}, Id, PluginContainer, PluginLocator, PluginManifest, VirtualPath, Wasm, }; -#[derive(Debug, Default, Serialize)] -pub struct ExecutableLocation { - pub config: ExecutableConfig, - pub name: String, - pub path: PathBuf, - pub primary: bool, -} - pub struct Tool { pub id: Id, pub metadata: ToolMetadataOutput, @@ -51,11 +27,13 @@ pub struct Tool { pub inventory: Inventory, pub product: Product, - cache: bool, - exes_dir: Option, - exe_path: Option, - globals_dirs: Vec, - globals_prefix: Option, + // Cache + pub(crate) cache: bool, + pub(crate) exe_file: Option, + pub(crate) exes_dir: Option, + pub(crate) globals_dir: Option, + pub(crate) globals_dirs: Vec, + pub(crate) globals_prefix: Option, } impl Tool { @@ -71,8 +49,9 @@ impl Tool { let mut tool = Tool { cache: true, + exe_file: None, exes_dir: None, - exe_path: None, + globals_dir: None, globals_dirs: vec![], globals_prefix: None, id, @@ -91,7 +70,7 @@ impl Tool { } #[instrument(name = "new_tool", skip(proto, wasm))] - pub async fn load + Debug, P: AsRef>( + pub async fn load + fmt::Debug, P: AsRef>( id: I, proto: P, wasm: Wasm, @@ -149,17 +128,6 @@ impl Tool { Ok(manifest) } - async fn call_locate_executables(&self) -> miette::Result { - self.plugin - .cache_func_with( - "locate_executables", - LocateExecutablesInput { - context: self.create_context(), - }, - ) - .await - } - /// Disable internal caching when applicable. pub fn disable_caching(&mut self) { self.cache = false; @@ -284,7 +252,7 @@ impl Tool { debug!(tool = self.id.as_str(), "Syncing manifest with changes"); - let sync_changes: SyncManifestOutput = self + let output: SyncManifestOutput = self .plugin .call_func_with( "sync_manifest", @@ -294,14 +262,14 @@ impl Tool { ) .await?; - if sync_changes.skip_sync { + if output.skip_sync { return Ok(()); } let mut modified = false; let manifest = &mut self.inventory.manifest; - if let Some(versions) = sync_changes.versions { + if let Some(versions) = output.versions { modified = true; let mut entries = FxHashMap::default(); @@ -326,1289 +294,6 @@ impl Tool { } } -// VERSION RESOLUTION - -impl Tool { - /// Load available versions to install and return a resolver instance. - /// To reduce network overhead, results will be cached for 24 hours. - #[instrument(skip(self))] - pub async fn load_version_resolver( - &self, - initial_version: &UnresolvedVersionSpec, - ) -> miette::Result { - debug!(tool = self.id.as_str(), "Loading available versions"); - - let mut versions = LoadVersionsOutput::default(); - let mut cached = false; - - if let Some(cached_versions) = self.inventory.load_remote_versions(!self.cache)? { - versions = cached_versions; - cached = true; - } - - // Nothing cached, so load from the plugin - if !cached { - if is_offline() { - return Err(ProtoError::InternetConnectionRequiredForVersion { - command: format!("{}_VERSION=1.2.3 {}", self.get_env_var_prefix(), self.id), - bin_dir: self.proto.store.bin_dir.clone(), - } - .into()); - } - - if env::var("PROTO_BYPASS_VERSION_CHECK").is_err() { - versions = self - .plugin - .cache_func_with( - "load_versions", - LoadVersionsInput { - initial: initial_version.to_owned(), - }, - ) - .await?; - - self.inventory.save_remote_versions(&versions)?; - } - } - - // Cache the results and create a resolver - let mut resolver = VersionResolver::from_output(versions); - - resolver.with_manifest(&self.inventory.manifest); - - let config = self.proto.load_config()?; - - if let Some(tool_config) = config.tools.get(&self.id) { - resolver.with_config(tool_config); - } - - Ok(resolver) - } - - /// Given an initial version, resolve it to a fully qualifed and semantic version - /// (or alias) according to the tool's ecosystem. - #[instrument(skip(self))] - pub async fn resolve_version( - &mut self, - initial_version: &UnresolvedVersionSpec, - short_circuit: bool, - ) -> miette::Result<()> { - if self.version.is_some() { - return Ok(()); - } - - debug!( - tool = self.id.as_str(), - initial_version = initial_version.to_string(), - "Resolving a semantic version or alias", - ); - - // If we have a fully qualified semantic version, - // exit early and assume the version is legitimate! - // Also canary is a special type that we can simply just use. - if short_circuit - && matches!( - initial_version, - UnresolvedVersionSpec::Calendar(_) | UnresolvedVersionSpec::Semantic(_) - ) - || matches!(initial_version, UnresolvedVersionSpec::Canary) - { - let version = initial_version.to_resolved_spec(); - - debug!( - tool = self.id.as_str(), - version = version.to_string(), - "Resolved to {} (without validation)", - version - ); - - self.set_version(version); - - return Ok(()); - } - - let resolver = self.load_version_resolver(initial_version).await?; - let version = self - .resolve_version_candidate(&resolver, initial_version, true) - .await?; - - debug!( - tool = self.id.as_str(), - version = version.to_string(), - "Resolved to {}", - version - ); - - self.set_version(version); - - Ok(()) - } - - #[instrument(name = "candidate", skip(self, resolver))] - pub async fn resolve_version_candidate( - &self, - resolver: &VersionResolver<'_>, - initial_candidate: &UnresolvedVersionSpec, - with_manifest: bool, - ) -> miette::Result { - let resolve = |candidate: &UnresolvedVersionSpec| { - let result = if with_manifest { - resolver.resolve(candidate) - } else { - resolver.resolve_without_manifest(candidate) - }; - - result.ok_or_else(|| ProtoError::VersionResolveFailed { - tool: self.get_name().to_owned(), - version: candidate.to_string(), - }) - }; - - if self.plugin.has_func("resolve_version").await { - let result: ResolveVersionOutput = self - .plugin - .call_func_with( - "resolve_version", - ResolveVersionInput { - initial: initial_candidate.to_owned(), - }, - ) - .await?; - - if let Some(candidate) = result.candidate { - debug!( - tool = self.id.as_str(), - candidate = candidate.to_string(), - "Received a possible version or alias to use", - ); - - return Ok(resolve(&candidate)?); - } - - if let Some(candidate) = result.version { - debug!( - tool = self.id.as_str(), - version = candidate.to_string(), - "Received an explicit version or alias to use", - ); - - return Ok(candidate); - } - } - - Ok(resolve(initial_candidate)?) - } - - /// Attempt to detect an applicable version from the provided directory. - #[instrument(skip(self))] - pub async fn detect_version_from( - &self, - current_dir: &Path, - ) -> miette::Result> { - if !self.plugin.has_func("detect_version_files").await { - return Ok(None); - } - - let has_parser = self.plugin.has_func("parse_version_file").await; - let result: DetectVersionOutput = self.plugin.cache_func("detect_version_files").await?; - - if !result.ignore.is_empty() { - if let Some(dir) = current_dir.to_str() { - if result.ignore.iter().any(|ignore| dir.contains(ignore)) { - return Ok(None); - } - } - } - - trace!( - tool = self.id.as_str(), - dir = ?current_dir, - "Attempting to detect a version from directory" - ); - - for file in result.files { - let file_path = current_dir.join(&file); - - if !file_path.exists() { - continue; - } - - let content = fs::read_file(&file_path)?.trim().to_owned(); - - if content.is_empty() { - continue; - } - - let version = if has_parser { - let result: ParseVersionFileOutput = self - .plugin - .call_func_with( - "parse_version_file", - ParseVersionFileInput { - content, - file: file.clone(), - }, - ) - .await?; - - if result.version.is_none() { - continue; - } - - result.version.unwrap() - } else { - UnresolvedVersionSpec::parse(&content).map_err(|error| ProtoError::VersionSpec { - version: content, - error: Box::new(error), - })? - }; - - debug!( - tool = self.id.as_str(), - file = ?file_path, - version = version.to_string(), - "Detected a version" - ); - - return Ok(Some((version, file_path))); - } - - Ok(None) - } -} - -// INSTALLATION - -impl Tool { - /// Return true if the tool has been installed. This is less accurate than `is_setup`, - /// as it only checks for the existence of the inventory directory. - pub fn is_installed(&self) -> bool { - let dir = self.get_product_dir(); - - self.version - .as_ref() - // Canary can be overwritten so treat as not-installed - .is_some_and(|v| { - !v.is_latest() - && !v.is_canary() - && self.inventory.manifest.installed_versions.contains(v) - }) - && dir.exists() - && !fs::is_dir_locked(dir) - } - - /// Verify the downloaded file using the checksum strategy for the tool. - /// Common strategies are SHA256 and MD5. - #[instrument(skip(self))] - pub async fn verify_checksum( - &self, - checksum_file: &Path, - download_file: &Path, - checksum_public_key: Option<&str>, - ) -> miette::Result { - debug!( - tool = self.id.as_str(), - download_file = ?download_file, - checksum_file = ?checksum_file, - "Verifying checksum of downloaded file", - ); - - // Allow plugin to provide their own checksum verification method - let verified = if self.plugin.has_func("verify_checksum").await { - let result: VerifyChecksumOutput = self - .plugin - .call_func_with( - "verify_checksum", - VerifyChecksumInput { - checksum_file: self.to_virtual_path(checksum_file), - download_file: self.to_virtual_path(download_file), - context: self.create_context(), - }, - ) - .await?; - - result.verified - - // Otherwise attempt to verify it ourselves - } else { - verify_checksum(download_file, checksum_file, checksum_public_key)? - }; - - if verified { - debug!( - tool = self.id.as_str(), - "Successfully verified, checksum matches" - ); - - return Ok(true); - } - - Err(ProtoError::InvalidChecksum { - checksum: checksum_file.to_path_buf(), - download: download_file.to_path_buf(), - } - .into()) - } - - #[instrument(skip(self))] - pub async fn build_from_source(&self, _install_dir: &Path) -> miette::Result<()> { - debug!( - tool = self.id.as_str(), - "Installing tool by building from source" - ); - - if !self.plugin.has_func("build_instructions").await { - return Err(ProtoError::UnsupportedBuildFromSource { - tool: self.get_name().to_owned(), - } - .into()); - } - - // let temp_dir = self.get_temp_dir(); - - // let options: BuildInstructionsOutput = self.plugin.cache_func_with( - // "build_instructions", - // BuildInstructionsInput { - // context: self.create_context(), - // }, - // )?; - - // match &options.source { - // // Should this do anything? - // SourceLocation::None => { - // return Ok(()); - // } - - // // Download from archive - // SourceLocation::Archive { url: archive_url } => { - // let download_file = temp_dir.join(extract_filename_from_url(archive_url)?); - - // debug!( - // tool = self.id.as_str(), - // archive_url, - // download_file = ?download_file, - // install_dir = ?install_dir, - // "Attempting to download and unpack sources", - // ); - - // net::download_from_url_with_client( - // archive_url, - // &download_file, - // self.proto.get_plugin_loader()?.get_client()?, - // ) - // .await?; - - // Archiver::new(install_dir, &download_file).unpack_from_ext()?; - // } - - // // Clone from Git repository - // SourceLocation::Git { - // url: repo_url, - // reference: ref_name, - // submodules, - // } => { - // debug!( - // tool = self.id.as_str(), - // repo_url, - // ref_name, - // install_dir = ?install_dir, - // "Attempting to clone a Git repository", - // ); - - // let run_git = |args: &[&str]| -> miette::Result<()> { - // let status = Command::new("git") - // .args(args) - // .current_dir(install_dir) - // .spawn() - // .into_diagnostic()? - // .wait() - // .into_diagnostic()?; - - // if !status.success() { - // return Err(ProtoError::BuildFailed { - // tool: self.get_name().to_owned(), - // url: repo_url.clone(), - // status: format!("exit code {}", status), - // } - // .into()); - // } - - // Ok(()) - // }; - - // // TODO, pull if already cloned - - // fs::create_dir_all(install_dir)?; - - // run_git(&[ - // "clone", - // if *submodules { - // "--recurse-submodules" - // } else { - // "" - // }, - // repo_url, - // ".", - // ])?; - - // run_git(&["checkout", ref_name])?; - // } - // }; - - Ok(()) - } - - /// Download the tool (as an archive) from its distribution registry - /// into the `~/.proto/tools/` folder, and optionally verify checksums. - #[instrument(skip(self, options))] - pub async fn install_from_prebuilt( - &self, - install_dir: &Path, - mut options: InstallOptions, - ) -> miette::Result<()> { - debug!( - tool = self.id.as_str(), - "Installing tool from a pre-built archive" - ); - - let client = self.proto.get_plugin_loader()?.get_client()?; - let params: DownloadPrebuiltOutput = self - .plugin - .cache_func_with( - "download_prebuilt", - DownloadPrebuiltInput { - context: self.create_context(), - install_dir: self.to_virtual_path(install_dir), - }, - ) - .await?; - - let temp_dir = self.get_temp_dir(); - - // Download the prebuilt - options.on_phase_change.as_ref().inspect(|func| { - func(InstallPhase::Download); - }); - - let download_url = params.download_url; - let download_file = temp_dir.join(match params.download_name { - Some(name) => name, - None => extract_filename_from_url(&download_url)?, - }); - - if download_file.exists() { - debug!( - tool = self.id.as_str(), - "Tool already downloaded, continuing" - ); - } else { - debug!(tool = self.id.as_str(), "Tool not downloaded, downloading"); - - net::download_from_url_with_options( - &download_url, - &download_file, - DownloadOptions { - client: Some(client), - on_chunk: options.on_download_chunk.take(), - }, - ) - .await?; - } - - // Verify the checksum if applicable - if let Some(checksum_url) = params.checksum_url { - options.on_phase_change.as_ref().inspect(|func| { - func(InstallPhase::Verify); - }); - - let checksum_file = temp_dir.join(match params.checksum_name { - Some(name) => name, - None => extract_filename_from_url(&checksum_url)?, - }); - - if !checksum_file.exists() { - debug!( - tool = self.id.as_str(), - "Checksum does not exist, downloading" - ); - - net::download_from_url_with_options( - &checksum_url, - &checksum_file, - DownloadOptions { - client: Some(client), - on_chunk: None, - }, - ) - .await?; - } - - self.verify_checksum( - &checksum_file, - &download_file, - params.checksum_public_key.as_deref(), - ) - .await?; - } - - // Attempt to unpack the archive - debug!( - tool = self.id.as_str(), - download_file = ?download_file, - install_dir = ?install_dir, - "Attempting to unpack archive", - ); - - if self.plugin.has_func("unpack_archive").await { - options.on_phase_change.as_ref().inspect(|func| { - func(InstallPhase::Unpack); - }); - - self.plugin - .call_func_without_output( - "unpack_archive", - UnpackArchiveInput { - input_file: self.to_virtual_path(&download_file), - output_dir: self.to_virtual_path(install_dir), - context: self.create_context(), - }, - ) - .await?; - } - // Is an archive, unpack it - else if is_archive_file(&download_file) { - options.on_phase_change.as_ref().inspect(|func| { - func(InstallPhase::Unpack); - }); - - let mut archiver = Archiver::new(install_dir, &download_file); - - if let Some(prefix) = ¶ms.archive_prefix { - archiver.set_prefix(prefix); - } - - let (ext, unpacked_path) = archiver.unpack_from_ext()?; - - // If the archive was `.gz` without tar or other formats, - // it's a single file, so assume a binary and update perms - if ext == "gz" && unpacked_path.is_file() { - fs::update_perms(unpacked_path, None)?; - } - } - // Not an archive, assume a binary and copy - else { - let install_path = install_dir.join(get_exe_file_name(&self.id)); - - fs::rename(&download_file, &install_path)?; - fs::update_perms(install_path, None)?; - } - - Ok(()) - } - - /// Install a tool into proto, either by downloading and unpacking - /// a pre-built archive, or by using a native installation method. - #[instrument(skip(self, options))] - pub async fn install(&mut self, options: InstallOptions) -> miette::Result { - if self.is_installed() { - debug!( - tool = self.id.as_str(), - "Tool already installed, continuing" - ); - - return Ok(false); - } - - if is_offline() { - return Err(ProtoError::InternetConnectionRequired.into()); - } - - let install_dir = self.get_product_dir(); - let mut installed = false; - - // Lock the install directory. If the inventory has been overridden, - // lock the internal proto tool directory instead. - let install_lock = fs::lock_directory(if self.metadata.inventory.override_dir.is_some() { - self.proto - .store - .inventory_dir - .join(self.id.as_str()) - .join(self.get_resolved_version().to_string()) - } else { - install_dir.clone() - })?; - - // If this function is defined, it acts like an escape hatch and - // takes precedence over all other install strategies - if self.plugin.has_func("native_install").await { - debug!(tool = self.id.as_str(), "Installing tool natively"); - - options.on_phase_change.as_ref().inspect(|func| { - func(InstallPhase::Native); - }); - - let result: NativeInstallOutput = self - .plugin - .call_func_with( - "native_install", - NativeInstallInput { - context: self.create_context(), - install_dir: self.to_virtual_path(&install_dir), - }, - ) - .await?; - - if !result.installed && !result.skip_install { - return Err(ProtoError::InstallFailed { - tool: self.get_name().to_owned(), - error: result.error.unwrap_or_default(), - } - .into()); - - // If native install fails, attempt other installers - } else { - installed = result.installed; - } - } - - if !installed { - // // Build the tool from source - // if build { - // self.build_from_source(&install_dir).await?; - - // // Install from a prebuilt archive - // } else { - // self.install_from_prebuilt(&install_dir).await?; - // } - - self.install_from_prebuilt(&install_dir, options).await?; - } - - install_lock.unlock()?; - - debug!( - tool = self.id.as_str(), - install_dir = ?install_dir, - "Successfully installed tool", - ); - - Ok(true) - } - - /// Uninstall the tool by deleting the current install directory. - #[instrument(skip_all)] - pub async fn uninstall(&self) -> miette::Result { - let install_dir = self.get_product_dir(); - - if !install_dir.exists() { - debug!( - tool = self.id.as_str(), - "Tool has not been installed, aborting" - ); - - return Ok(false); - } - - if self.plugin.has_func("native_uninstall").await { - debug!(tool = self.id.as_str(), "Uninstalling tool natively"); - - let result: NativeUninstallOutput = self - .plugin - .call_func_with( - "native_uninstall", - NativeUninstallInput { - context: self.create_context(), - }, - ) - .await?; - - if !result.uninstalled && !result.skip_uninstall { - return Err(ProtoError::UninstallFailed { - tool: self.get_name().to_owned(), - error: result.error.unwrap_or_default(), - } - .into()); - } - } - - debug!( - tool = self.id.as_str(), - install_dir = ?install_dir, - "Deleting install directory" - ); - - fs::remove_dir_all(install_dir)?; - - debug!(tool = self.id.as_str(), "Successfully uninstalled tool"); - - Ok(true) - } -} - -// BINARIES, SHIMS - -impl Tool { - /// Create all executables for the current tool. - /// - Locate the primary binary to execute. - /// - Generate shims to `~/.proto/shims`. - /// - Symlink bins to `~/.proto/bin`. - #[instrument(skip(self))] - pub async fn create_executables( - &mut self, - force_shims: bool, - force_bins: bool, - ) -> miette::Result<()> { - self.locate_executable().await?; - self.generate_shims(force_shims).await?; - self.symlink_bins(force_bins).await?; - - Ok(()) - } - - /// Return an absolute path to the executable file for the tool. - pub fn get_exe_path(&self) -> miette::Result<&Path> { - self.exe_path.as_deref().ok_or_else(|| { - ProtoError::UnknownTool { - id: self.id.clone(), - } - .into() - }) - } - - /// Return an absolute path to the pre-installed executables directory.s - pub fn get_exes_dir(&self) -> Option<&PathBuf> { - self.exes_dir.as_ref() - } - - /// Return an absolute path to the globals directory that actually exists. - pub fn get_globals_dir(&self) -> Option<&PathBuf> { - let lookup_count = self.globals_dirs.len() - 1; - - for (index, dir) in self.globals_dirs.iter().enumerate() { - if dir.exists() || index == lookup_count { - debug!(tool = self.id.as_str(), dir = ?dir, "Found a usable globals directory"); - - return Some(dir); - } - } - - None - } - - /// Return a list of all possible globals directories. - pub fn get_globals_dirs(&self) -> &[PathBuf] { - &self.globals_dirs - } - - /// Return a string that all globals are prefixed with. Will be used for filtering and listing. - pub fn get_globals_prefix(&self) -> Option<&str> { - self.globals_prefix.as_deref() - } - - /// Return a list of all binaries that get created in `~/.proto/bin`. - /// The list will contain the executable config, and an absolute path - /// to the binaries final location. - pub async fn get_bin_locations(&self) -> miette::Result> { - let options = self.call_locate_executables().await?; - let mut locations = vec![]; - - let mut add = |name: &str, config: ExecutableConfig, primary: bool| { - if !config.no_bin - && config - .exe_link_path - .as_ref() - .or(config.exe_path.as_ref()) - .is_some() - { - locations.push(ExecutableLocation { - path: self.proto.store.bin_dir.join(get_exe_file_name(name)), - name: name.to_owned(), - config, - primary, - }); - } - }; - - if let Some(primary) = options.primary { - add(&self.id, primary, true); - } - - for (name, secondary) in options.secondary { - add(&name, secondary, false); - } - - Ok(locations) - } - - /// Return location information for the primary executable within the tool directory. - pub async fn get_exe_location(&self) -> miette::Result> { - let options = self.call_locate_executables().await?; - - if let Some(primary) = options.primary { - if let Some(exe_path) = &primary.exe_path { - return Ok(Some(ExecutableLocation { - path: self.get_product_dir().join(exe_path), - name: self.id.to_string(), - config: primary, - primary: true, - })); - } - } - - Ok(None) - } - - /// Return a list of all shims that get created in `~/.proto/shims`. - /// The list will contain the executable config, and an absolute path - /// to the shims final location. - pub async fn get_shim_locations(&self) -> miette::Result> { - let options = self.call_locate_executables().await?; - let mut locations = vec![]; - - let mut add = |name: &str, config: ExecutableConfig, primary: bool| { - if !config.no_shim { - locations.push(ExecutableLocation { - path: self.proto.store.shims_dir.join(get_shim_file_name(name)), - name: name.to_owned(), - config: config.clone(), - primary, - }); - } - }; - - if let Some(primary) = options.primary { - add(&self.id, primary, true); - } - - for (name, secondary) in options.secondary { - add(&name, secondary, false); - } - - Ok(locations) - } - - /// Locate the primary executable from the tool directory. - #[instrument(skip_all)] - pub async fn locate_executable(&mut self) -> miette::Result<()> { - debug!(tool = self.id.as_str(), "Locating executable for tool"); - - let exe_path = if let Some(location) = self.get_exe_location().await? { - location.path - } else { - self.get_product_dir().join(self.id.as_str()) - }; - - if exe_path.exists() { - debug!(tool = self.id.as_str(), exe_path = ?exe_path, "Found an executable"); - - self.exe_path = Some(exe_path); - - return Ok(()); - } - - Err(ProtoError::MissingToolExecutable { - tool: self.get_name().to_owned(), - path: exe_path, - } - .into()) - } - - /// Locate the directory that local executables are installed to. - #[instrument(skip_all)] - pub async fn locate_exes_dir(&mut self) -> miette::Result<()> { - if !self.plugin.has_func("locate_executables").await || self.exes_dir.is_some() { - return Ok(()); - } - - let options = self.call_locate_executables().await?; - - if let Some(exes_dir) = options.exes_dir { - self.exes_dir = Some(self.get_product_dir().join(exes_dir)); - } - - Ok(()) - } - - /// Locate the directories that global packages are installed to. - #[instrument(skip_all)] - pub async fn locate_globals_dirs(&mut self) -> miette::Result<()> { - if !self.plugin.has_func("locate_executables").await || !self.globals_dirs.is_empty() { - return Ok(()); - } - - debug!( - tool = self.id.as_str(), - "Locating globals bin directories for tool" - ); - - let install_dir = self.get_product_dir(); - let options = self.call_locate_executables().await?; - - self.globals_prefix = options.globals_prefix; - - // Find all possible global directories that packages can be installed to - let mut resolved_dirs = vec![]; - - 'outer: for dir_lookup in options.globals_lookup_dirs { - let mut dir = dir_lookup.clone(); - - // If a lookup contains an env var, find and replace it. - // If the var is not defined or is empty, skip this lookup. - for cap in ENV_VAR.captures_iter(&dir_lookup) { - let find_by = cap.get(0).unwrap().as_str(); - - let replace_with = match find_by { - "$CWD" | "$PWD" => self.proto.cwd.clone(), - "$HOME" => self.proto.home.clone(), - "$PROTO_HOME" | "$PROTO_ROOT" => self.proto.root.clone(), - "$TOOL_DIR" => install_dir.clone(), - _ => match env::var_os(cap.get(1).unwrap().as_str()) { - Some(value) => PathBuf::from(value), - None => { - continue 'outer; - } - }, - }; - - if let Some(replacement) = replace_with.to_str() { - dir = dir.replace(find_by, replacement); - } else { - continue 'outer; - } - } - - let dir = if let Some(dir_suffix) = dir.strip_prefix('~') { - self.proto.home.join(dir_suffix) - } else { - PathBuf::from(dir) - }; - - // Don't use a set as we need to persist the order! - if !resolved_dirs.contains(&dir) { - resolved_dirs.push(dir); - } - } - - debug!( - tool = self.id.as_str(), - dirs = ?resolved_dirs, - "Located possible globals directories", - ); - - self.globals_dirs = resolved_dirs; - - Ok(()) - } - - /// Create shim files for the current tool if they are missing or out of date. - /// If find only is enabled, will only check if they exist, and not create. - #[instrument(skip(self))] - pub async fn generate_shims(&mut self, force: bool) -> miette::Result<()> { - let shims = self.get_shim_locations().await?; - - if shims.is_empty() { - return Ok(()); - } - - let is_outdated = self.inventory.manifest.shim_version != SHIM_VERSION; - let force_create = force || is_outdated; - let find_only = !force_create; - - if force_create { - debug!( - tool = self.id.as_str(), - shims_dir = ?self.proto.store.shims_dir, - shim_version = SHIM_VERSION, - "Creating shims as they either do not exist, or are outdated" - ); - - self.inventory.manifest.shim_version = SHIM_VERSION; - self.inventory.manifest.save()?; - } - - let mut registry: ShimsMap = BTreeMap::default(); - registry.insert(self.id.to_string(), Shim::default()); - - let mut to_create = vec![]; - - for shim in shims { - let mut shim_entry = Shim::default(); - - // Handle before and after args - if let Some(before_args) = shim.config.shim_before_args { - shim_entry.before_args = match before_args { - StringOrVec::String(value) => shell_words::split(&value).into_diagnostic()?, - StringOrVec::Vec(value) => value, - }; - } - - if let Some(after_args) = shim.config.shim_after_args { - shim_entry.after_args = match after_args { - StringOrVec::String(value) => shell_words::split(&value).into_diagnostic()?, - StringOrVec::Vec(value) => value, - }; - } - - if let Some(env_vars) = shim.config.shim_env_vars { - shim_entry.env_vars.extend(env_vars); - } - - if !shim.primary { - shim_entry.parent = Some(self.id.to_string()); - - // Only use --alt when the secondary executable exists - if shim.config.exe_path.is_some() { - shim_entry.alt_bin = Some(true); - } - } - - // Create the shim file by copying the source bin - if force_create || find_only && !shim.path.exists() { - to_create.push(shim.path); - } - - // Update the registry - registry.insert(shim.name.clone(), shim_entry); - } - - // Only lock the directory and create shims if necessary - if !to_create.is_empty() { - let _lock = fs::lock_directory(&self.proto.store.shims_dir)?; - - for shim_path in to_create { - self.proto.store.create_shim(&shim_path)?; - - debug!( - tool = self.id.as_str(), - shim = ?shim_path, - shim_version = SHIM_VERSION, - "Creating shim" - ); - } - - ShimRegistry::update(&self.proto, registry)?; - } - - Ok(()) - } - - /// Symlink all primary and secondary binaries for the current tool. - #[instrument(skip(self))] - pub async fn symlink_bins(&mut self, force: bool) -> miette::Result<()> { - let bins = self.get_bin_locations().await?; - - if bins.is_empty() { - return Ok(()); - } - - if force { - debug!( - tool = self.id.as_str(), - bins_dir = ?self.proto.store.bin_dir, - "Creating symlinks to the original tool executables" - ); - } - - let tool_dir = self.get_product_dir(); - let mut to_create = vec![]; - - for bin in bins { - let input_path = tool_dir.join( - bin.config - .exe_link_path - .as_ref() - .or(bin.config.exe_path.as_ref()) - .unwrap(), - ); - let output_path = bin.path; - - if !input_path.exists() { - warn!( - tool = self.id.as_str(), - source = ?input_path, - target = ?output_path, - "Unable to symlink binary, source file does not exist" - ); - - continue; - } - - if !force && output_path.exists() { - continue; - } - - to_create.push((input_path, output_path)); - } - - // Only lock the directory and create bins if necessary - if !to_create.is_empty() { - let _lock = fs::lock_directory(&self.proto.store.bin_dir)?; - - for (input_path, output_path) in to_create { - debug!( - tool = self.id.as_str(), - source = ?input_path, - target = ?output_path, - "Creating binary symlink" - ); - - self.proto.store.unlink_bin(&output_path)?; - self.proto.store.link_bin(&output_path, &input_path)?; - } - } - - Ok(()) - } -} - -// OPERATIONS - -impl Tool { - /// Return true if the tool has been setup (installed and binaries are located). - #[instrument(skip(self))] - pub async fn is_setup( - &mut self, - initial_version: &UnresolvedVersionSpec, - ) -> miette::Result { - self.resolve_version(initial_version, true).await?; - - let install_dir = self.get_product_dir(); - - debug!( - tool = self.id.as_str(), - install_dir = ?install_dir, - "Checking if tool is installed", - ); - - if self.is_installed() { - debug!( - tool = self.id.as_str(), - install_dir = ?install_dir, - "Tool has already been installed, locating binaries and shims", - ); - - if self.exe_path.is_none() { - self.create_executables(false, false).await?; - } - - return Ok(true); - } else { - debug!(tool = self.id.as_str(), "Tool has not been installed"); - } - - Ok(false) - } - - /// Setup the tool by resolving a semantic version, installing the tool, - /// locating binaries, creating shims, and more. - #[instrument(skip(self, options))] - pub async fn setup( - &mut self, - initial_version: &UnresolvedVersionSpec, - options: InstallOptions, - ) -> miette::Result { - self.resolve_version(initial_version, false).await?; - - if !self.install(options).await? { - return Ok(false); - } - - self.create_executables(false, false).await?; - self.cleanup().await?; - - let version = self.get_resolved_version(); - let default_version = self - .metadata - .default_version - .clone() - .unwrap_or_else(|| version.to_unresolved_spec()); - - // Add version to manifest - let manifest = &mut self.inventory.manifest; - manifest.installed_versions.insert(version.clone()); - manifest - .versions - .insert(version.clone(), ToolManifestVersion::default()); - manifest.save()?; - - // Pin the global version - ProtoConfig::update(self.proto.get_config_dir(true), |config| { - config - .versions - .get_or_insert(Default::default()) - .entry(self.id.clone()) - .or_insert(default_version); - })?; - - // Allow plugins to override manifest - self.sync_manifest().await?; - - Ok(true) - } - - /// Teardown the tool by uninstalling the current version, removing the version - /// from the manifest, and cleaning up temporary files. Return true if the teardown occurred. - #[instrument(skip_all)] - pub async fn teardown(&mut self) -> miette::Result { - self.cleanup().await?; - - if !self.uninstall().await? { - return Ok(false); - } - - let version = self.get_resolved_version(); - let mut removed_default_version = false; - - // Remove version from manifest - let manifest = &mut self.inventory.manifest; - manifest.installed_versions.remove(&version); - manifest.versions.remove(&version); - manifest.save()?; - - // Unpin global version if a match - ProtoConfig::update(self.proto.get_config_dir(true), |config| { - if let Some(versions) = &mut config.versions { - if versions.get(&self.id).is_some_and(|v| v == &version) { - debug!("Unpinning global version"); - - versions.remove(&self.id); - removed_default_version = true; - } - } - })?; - - // If no more default version, delete the symlink, - // otherwise the OS will throw errors for missing sources - if removed_default_version || self.inventory.manifest.installed_versions.is_empty() { - for bin in self.get_bin_locations().await? { - self.proto.store.unlink_bin(&bin.path)?; - } - } - - // If no more versions in general, delete all shims - if self.inventory.manifest.installed_versions.is_empty() { - for shim in self.get_shim_locations().await? { - self.proto.store.remove_shim(&shim.path)?; - } - } - - Ok(true) - } - - /// Delete temporary files and downloads for the current version. - #[instrument(skip_all)] - pub async fn cleanup(&mut self) -> miette::Result<()> { - debug!( - tool = self.id.as_str(), - "Cleaning up temporary files and downloads" - ); - - fs::remove_dir_all(self.get_temp_dir())?; - - Ok(()) - } -} - impl fmt::Debug for Tool { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Tool") diff --git a/crates/pdk-test-utils/src/macros.rs b/crates/pdk-test-utils/src/macros.rs index d4519eb0b..cf1ca11c6 100644 --- a/crates/pdk-test-utils/src/macros.rs +++ b/crates/pdk-test-utils/src/macros.rs @@ -31,14 +31,14 @@ macro_rules! generate_download_install_tests { assert!(base_dir.exists()); // Check bin path exists (would panic) - plugin.tool.get_exe_path().unwrap(); + plugin.tool.locate_exe_file().await.unwrap(); // Check things exist - for bin in plugin.tool.get_bin_locations().await.unwrap() { + for bin in plugin.tool.resolve_bin_locations().await.unwrap() { assert!(bin.path.exists()); } - for shim in plugin.tool.get_shim_locations().await.unwrap() { + for shim in plugin.tool.resolve_shim_locations().await.unwrap() { assert!(shim.path.exists()); } } diff --git a/plugins/Cargo.lock b/plugins/Cargo.lock index 88a4ff435..af7fa606f 100644 --- a/plugins/Cargo.lock +++ b/plugins/Cargo.lock @@ -865,9 +865,9 @@ dependencies = [ [[package]] name = "extism-pdk" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c26a361aeddab8ebbb6157eeed9a8341358d6843c5f5a53677466caf5f0b5eaf" +checksum = "0334e38735348bd085e518bfc284395fcb5c198ba65ce36f9712bd25ff9e7be0" dependencies = [ "anyhow", "base64 0.22.1", @@ -880,9 +880,9 @@ dependencies = [ [[package]] name = "extism-pdk-derive" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a024b0f20295098d1d19ad443fad077c1d8c1d81d09a2c20f0618ebd201517e" +checksum = "cc4b8ea80a1b89cf8b053bdc5df2385125bcb9110d19be289c2030c61c7c6ad9" dependencies = [ "proc-macro2", "quote", @@ -1982,7 +1982,7 @@ dependencies = [ [[package]] name = "proto_core" -version = "0.39.3" +version = "0.40.2" dependencies = [ "indexmap 2.4.0", "miette", @@ -2012,7 +2012,7 @@ dependencies = [ [[package]] name = "proto_pdk" -version = "0.22.0" +version = "0.23.0" dependencies = [ "extism-pdk", "proto_pdk_api", @@ -2023,7 +2023,7 @@ dependencies = [ [[package]] name = "proto_pdk_api" -version = "0.22.0" +version = "0.23.0" dependencies = [ "rustc-hash 2.0.0", "schematic", @@ -2038,7 +2038,7 @@ dependencies = [ [[package]] name = "proto_pdk_test_utils" -version = "0.26.0" +version = "0.27.0" dependencies = [ "proto_core", "proto_pdk_api", @@ -2050,7 +2050,7 @@ dependencies = [ [[package]] name = "proto_shim" -version = "0.4.3" +version = "0.5.0" dependencies = [ "command-group", "dirs 5.0.1", @@ -2261,9 +2261,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.12.5" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "base64 0.22.1", "bytes", @@ -2301,7 +2301,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "windows-registry", ] [[package]] @@ -2451,9 +2451,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.14" +version = "2.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79da19444d9da7a9a82b80ecf059eceba6d3129d84a8610fd25ff2364f255466" +checksum = "aeb7ac86243095b70a7920639507b71d51a63390d1ba26c4f60a552fbb914a37" dependencies = [ "sdd", ] @@ -2469,9 +2469,9 @@ dependencies = [ [[package]] name = "schematic" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14bf79931c9c99d9bcdc29a448836dbcea9ad5991285cc83dd4c9f46cf771ac2" +checksum = "a27ae5ae2fb243bce05160f4983d7e258978843e5b9dbb21375c4311cec68300" dependencies = [ "garde", "indexmap 2.4.0", @@ -2489,9 +2489,9 @@ dependencies = [ [[package]] name = "schematic_macros" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f037e36d93185ba8e25049754095e4aabb985ed5a8157da369d40be60d9fa" +checksum = "b67892882d3a0f4e4186f4805a00bb23f978847438ed2efb1b615eb847c8c9cc" dependencies = [ "convert_case", "darling", @@ -2502,9 +2502,9 @@ dependencies = [ [[package]] name = "schematic_types" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce97b2ad673e2183ec94cce86b039e692ecdf1b3b8a1195c90ad31f8adbbd0d" +checksum = "653e1eacf66aa291a836204f0bbe8c2ae7adbc943b3e3ef8f4a2abfac77d12dd" dependencies = [ "indexmap 2.4.0", "semver", @@ -2851,23 +2851,26 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.1", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -2891,7 +2894,7 @@ dependencies = [ [[package]] name = "system_env" -version = "0.5.0" +version = "0.6.0" dependencies = [ "schematic", "serde", @@ -2972,9 +2975,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", @@ -3227,9 +3230,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" +checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" dependencies = [ "base64 0.22.1", "flate2", @@ -3277,7 +3280,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "version_spec" -version = "0.6.1" +version = "0.7.0" dependencies = [ "human-sort", "regex", @@ -3317,7 +3320,7 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.16.1" +version = "0.17.0" dependencies = [ "extism", "miette", @@ -3342,7 +3345,7 @@ dependencies = [ [[package]] name = "warpgate_api" -version = "0.9.0" +version = "0.10.0" dependencies = [ "anyhow", "rustc-hash 2.0.0", @@ -3355,7 +3358,7 @@ dependencies = [ [[package]] name = "warpgate_pdk" -version = "0.7.0" +version = "0.8.0" dependencies = [ "extism-pdk", "serde", @@ -3918,6 +3921,36 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -3942,7 +3975,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -3977,17 +4010,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -4004,9 +4038,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -4022,9 +4056,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -4040,9 +4074,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -4058,9 +4098,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -4076,9 +4116,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -4094,9 +4134,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -4112,9 +4152,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -4134,16 +4174,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winx" version = "0.36.3"