diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47b40a540..3bb55358f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: os: [ubuntu-latest, windows-latest] fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: moonrepo/setup-rust@v1 with: components: rustfmt @@ -30,7 +30,7 @@ jobs: os: [ubuntu-latest, windows-latest] fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: moonrepo/setup-rust@v1 with: components: clippy @@ -44,7 +44,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: moonrepo/setup-proto@v1 - uses: moonrepo/setup-rust@v1 with: @@ -71,7 +71,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: moonrepo/setup-rust@v1 with: bins: cargo-wasi diff --git a/Cargo.lock b/Cargo.lock index 87a0d75fb..58d1996cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -481,9 +481,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ "clap_builder", "clap_derive", @@ -491,9 +491,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ "anstream", "anstyle", @@ -503,11 +503,11 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.4.2" +version = "4.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8baeccdb91cd69189985f87f3c7e453a3a451ab5746cf3be6acc92120bd16d24" +checksum = "e3ae8ba90b9d8b007efe66e55e48fb936272f5ca00349b5b0e89877520d35ea7" dependencies = [ - "clap 4.4.5", + "clap 4.4.6", ] [[package]] @@ -2340,7 +2340,7 @@ name = "proto_cli" version = "0.18.5" dependencies = [ "chrono", - "clap 4.4.5", + "clap 4.4.6", "clap_complete", "convert_case", "dialoguer", @@ -2360,6 +2360,7 @@ dependencies = [ "starbase_sandbox", "starbase_styles", "starbase_utils", + "system_env", "tokio", "tracing", "winreg 0.51.0", @@ -2415,6 +2416,7 @@ dependencies = [ "semver", "serde", "serde_json", + "system_env", "thiserror", "warpgate_api", ] @@ -3248,6 +3250,15 @@ dependencies = [ "winx", ] +[[package]] +name = "system_env" +version = "0.0.1" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "tar" version = "0.4.40" diff --git a/Cargo.toml b/Cargo.toml index 99e4f5232..35688467b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,8 @@ default-members = ["crates/cli"] [workspace.dependencies] cached = "0.46.0" -clap = "4.4.5" -clap_complete = "4.4.2" +clap = "4.4.6" +clap_complete = "4.4.3" convert_case = "0.6.0" extism = "0.5.2" extism-pdk = "0.3.4" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index bcd07e35e..47279fe30 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -30,6 +30,7 @@ path = "src/main.rs" proto_core = { version = "0.18.6", path = "../core" } proto_pdk_api = { version = "0.7.2", path = "../pdk-api" } proto_wasm_plugin = { version = "0.6.7", path = "../wasm-plugin" } +system_env = { version = "0.0.1", path = "../system-env" } chrono = "0.4.31" clap = { workspace = true, features = ["derive", "env"] } clap_complete = { workspace = true } diff --git a/crates/cli/src/commands/install.rs b/crates/cli/src/commands/install.rs index 0a939a2fc..19a546a57 100644 --- a/crates/cli/src/commands/install.rs +++ b/crates/cli/src/commands/install.rs @@ -126,7 +126,7 @@ pub async fn internal_install(args: InstallArgs) -> SystemResult { tool.get_resolved_version() )); - let installed = tool.setup(&version).await?; + let installed = tool.setup(&version, false).await?; pb.finish_and_clear(); diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index 87aca8429..2d9e661f6 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -1,15 +1,13 @@ use crate::commands::install::{internal_install, InstallArgs}; use clap::Args; use miette::IntoDiagnostic; -use proto_core::{ - detect_version, is_command_on_path, load_tool, Id, ProtoError, UnresolvedVersionSpec, - UserConfig, -}; +use proto_core::{detect_version, load_tool, Id, ProtoError, UnresolvedVersionSpec, UserConfig}; use proto_pdk_api::RunHook; use starbase::system; use starbase_styles::color; use std::env; use std::process::exit; +use system_env::is_command_on_path; use tokio::process::Command; use tracing::debug; @@ -103,7 +101,7 @@ pub async fn run(args: ArgsRef) -> SystemResult { // Run the command let mut command = match bin_path.extension().map(|e| e.to_str().unwrap()) { Some("ps1") => { - let mut cmd = Command::new(if is_command_on_path("pwsh".into()) { + let mut cmd = Command::new(if is_command_on_path("pwsh") { "pwsh" } else { "powershell" diff --git a/crates/cli/tests/plugins_test.rs b/crates/cli/tests/plugins_test.rs index 46ed63859..0ca50721f 100644 --- a/crates/cli/tests/plugins_test.rs +++ b/crates/cli/tests/plugins_test.rs @@ -21,7 +21,7 @@ where env::set_var("PROTO_HOME", fixture.path().to_string_lossy().to_string()); - tool.setup(&UnresolvedVersionSpec::parse("1.0.0").unwrap()) + tool.setup(&UnresolvedVersionSpec::parse("1.0.0").unwrap(), false) .await .unwrap(); diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 812d32e3e..90de3bedc 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -17,6 +17,10 @@ pub enum ProtoError { #[error("Failed to install {tool}. {error}")] InstallFailed { tool: String, error: String }, + #[diagnostic(code(proto::tool::build_failed))] + #[error("Failed to build tool from {}: {status}", .url.style(Style::Url))] + BuildFailed { url: String, status: String }, + #[diagnostic(code(proto::misc::offline))] #[error("Internet connection required, unable to download and install tools.")] InternetConnectionRequired, @@ -60,6 +64,10 @@ pub enum ProtoError { )] UnknownTool { id: Id }, + #[diagnostic(code(proto::build::unsupported))] + #[error("Build from source is not supported for {}.", .tool.style(Style::Id))] + UnsupportedBuildFromSource { tool: Id }, + #[diagnostic(code(proto::unsupported::shell))] #[error("Unable to detect shell.")] UnsupportedShell, diff --git a/crates/core/src/helpers.rs b/crates/core/src/helpers.rs index c9a7ab6cd..8df7b4e0b 100644 --- a/crates/core/src/helpers.rs +++ b/crates/core/src/helpers.rs @@ -1,5 +1,6 @@ use crate::error::ProtoError; use cached::proc_macro::cached; +use miette::IntoDiagnostic; use once_cell::sync::Lazy; use regex::Regex; use serde::de::DeserializeOwned; @@ -92,45 +93,6 @@ pub fn is_cache_enabled() -> bool { }) } -#[cached] -#[cfg(windows)] -pub fn is_command_on_path(name: String) -> bool { - let Ok(system_path) = env::var("PATH") else { - return false; - }; - let Ok(path_ext) = env::var("PATHEXT") else { - return false; - }; - let exts = path_ext.split(';').collect::>(); - - for path_dir in env::split_paths(&system_path) { - for ext in &exts { - if path_dir.join(format!("{name}{ext}")).exists() { - return true; - } - } - } - - false -} - -#[cached] -#[cfg(not(windows))] -pub fn is_command_on_path(name: String) -> bool { - let Ok(system_path) = env::var("PATH") else { - return false; - }; - - for path_dir in env::split_paths(&system_path) { - #[allow(clippy::needless_borrow)] - if path_dir.join(&name).exists() { - return true; - } - } - - false -} - pub fn is_archive_file>(path: P) -> bool { is_supported_archive_extension(path.as_ref()) } @@ -155,6 +117,13 @@ pub fn hash_file_contents>(path: P) -> miette::Result { Ok(hash) } +pub fn extract_filename_from_url>(url: U) -> miette::Result { + let url = url::Url::parse(url.as_ref()).into_diagnostic()?; + let segments = url.path_segments().unwrap(); + + Ok(segments.last().unwrap().to_owned()) +} + pub fn read_json_file_with_lock(path: impl AsRef) -> miette::Result { let path = path.as_ref(); let mut content = fs::read_file_with_lock(path)?; diff --git a/crates/core/src/tool.rs b/crates/core/src/tool.rs index 0c4b6d348..6f5f7c065 100644 --- a/crates/core/src/tool.rs +++ b/crates/core/src/tool.rs @@ -1,8 +1,8 @@ use crate::error::ProtoError; use crate::events::*; use crate::helpers::{ - hash_file_contents, is_archive_file, is_cache_enabled, is_offline, read_json_file_with_lock, - write_json_file_with_lock, ENV_VAR, + extract_filename_from_url, hash_file_contents, is_archive_file, is_cache_enabled, is_offline, + read_json_file_with_lock, write_json_file_with_lock, ENV_VAR, }; use crate::proto::ProtoEnvironment; use crate::shimmer::{ @@ -24,6 +24,7 @@ use std::env; use std::fmt::Debug; use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; +use std::process::Command; use std::time::{Duration, SystemTime}; use tracing::{debug, trace}; use version_spec::*; @@ -680,6 +681,114 @@ impl Tool { .into()) } + 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") { + return Err(ProtoError::UnsupportedBuildFromSource { + tool: self.id.clone(), + } + .into()); + } + + let temp_dir = self + .get_temp_dir() + .join(self.get_resolved_version().to_string()); + + 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", + ); + + download_from_url_to_file( + archive_url, + &download_file, + self.proto.get_http_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 { + 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/temp` folder, and optionally verify checksums. pub async fn install_from_prebuilt(&self, install_dir: &Path) -> miette::Result { @@ -704,12 +813,7 @@ impl Tool { let download_url = options.download_url; let download_file = match options.download_name { Some(name) => temp_dir.join(name), - None => { - let url = url::Url::parse(&download_url).into_diagnostic()?; - let segments = url.path_segments().unwrap(); - - temp_dir.join(segments.last().unwrap()) - } + None => temp_dir.join(extract_filename_from_url(&download_url)?), }; if download_file.exists() { @@ -785,7 +889,7 @@ impl Tool { /// Install a tool into proto, either by downloading and unpacking /// a pre-built archive, or by using a native installation method. - pub async fn install(&mut self) -> miette::Result { + pub async fn install(&mut self, _build: bool) -> miette::Result { if self.is_installed() { debug!( tool = self.id.as_str(), @@ -834,8 +938,16 @@ impl Tool { } } - // Install from a prebuilt archive 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).await?; } @@ -1236,10 +1348,14 @@ impl Tool { /// Setup the tool by resolving a semantic version, installing the tool, /// locating binaries, creating shims, and more. - pub async fn setup(&mut self, initial_version: &UnresolvedVersionSpec) -> miette::Result { + pub async fn setup( + &mut self, + initial_version: &UnresolvedVersionSpec, + build_from_source: bool, + ) -> miette::Result { self.resolve_version(initial_version).await?; - if self.install().await? { + if self.install(build_from_source).await? { self.cleanup().await?; self.locate_bins().await?; self.setup_shims(true).await?; diff --git a/crates/core/src/tool_loader.rs b/crates/core/src/tool_loader.rs index 585578a68..551e18cd0 100644 --- a/crates/core/src/tool_loader.rs +++ b/crates/core/src/tool_loader.rs @@ -8,11 +8,7 @@ use miette::IntoDiagnostic; use proto_pdk_api::{HostArch, HostEnvironment, HostOS, UserConfigSettings}; use proto_wasm_plugin::Wasm; use starbase_utils::{json, toml}; -use std::str::FromStr; -use std::{ - env::{self, consts}, - path::Path, -}; +use std::{env, path::Path}; use tracing::{debug, trace}; use warpgate::{create_http_client_with_options, to_virtual_path, Id, PluginLocator}; @@ -42,8 +38,8 @@ pub fn inject_default_manifest_config( .insert("proto_user_config".to_string(), value); let value = json::to_string(&HostEnvironment { - arch: HostArch::from_str(consts::ARCH).into_diagnostic()?, - os: HostOS::from_str(consts::OS).into_diagnostic()?, + arch: HostArch::from_env(), + os: HostOS::from_env(), home_dir: to_virtual_path(manifest, &proto.home), proto_dir: to_virtual_path(manifest, &proto.root), }) diff --git a/crates/pdk-api/Cargo.toml b/crates/pdk-api/Cargo.toml index 5d55ff56a..f73eab206 100644 --- a/crates/pdk-api/Cargo.toml +++ b/crates/pdk-api/Cargo.toml @@ -8,6 +8,7 @@ homepage = "https://moonrepo.dev/proto" repository = "https://github.com/moonrepo/proto" [dependencies] +system_env = { version = "0.0.1", path = "../system-env" } warpgate_api = { version = "0.1.3", path = "../warpgate-api" } anyhow = "1.0.75" semver = { workspace = true, features = ["serde"] } diff --git a/crates/pdk-api/src/api.rs b/crates/pdk-api/src/api.rs index 1a05fe91b..db76fcb08 100644 --- a/crates/pdk-api/src/api.rs +++ b/crates/pdk-api/src/api.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::ffi::OsStr; use std::path::PathBuf; +use system_env::SystemDependency; use warpgate_api::VirtualPath; pub use semver::{Version, VersionReq}; @@ -184,6 +185,62 @@ json_struct!( } ); +json_struct!( + /// Input passed to the `build_instructions` function. + pub struct BuildInstructionsInput { + /// Current tool context. + pub context: ToolContext, + } +); + +json_enum!( + #[derive(Default)] + #[serde(tag = "type", rename_all = "lowercase")] + pub enum SourceLocation { + #[default] + None, + Archive { + url: String, + }, + Git { + url: String, + reference: String, + submodules: bool, + }, + } +); + +json_enum!( + #[serde(tag = "type", rename_all = "lowercase")] + pub enum BuildInstruction { + Command { + bin: String, + args: Vec, + env: HashMap, + }, + } +); + +json_struct!( + /// Output returned by the `build_instructions` function. + pub struct BuildInstructionsOutput { + /// Link to the documentation/help. + pub help_url: Option, + + /// Location in which to acquire the source files. Can be an archive URL, + /// or Git repository. + pub source: SourceLocation, + + /// List of instructions to execute to build the tool, after system + /// dependencies have been installed. + pub instructions: Vec, + + /// List of system dependencies that are required for building from source. + /// If a dependency does not exist, it will be installed. + pub system_dependencies: Vec, + } +); + json_struct!( /// Input passed to the `download_prebuilt` function. pub struct DownloadPrebuiltInput { diff --git a/crates/pdk-api/src/host.rs b/crates/pdk-api/src/host.rs index 52e7113bb..67affc139 100644 --- a/crates/pdk-api/src/host.rs +++ b/crates/pdk-api/src/host.rs @@ -1,113 +1,8 @@ -use crate::error::PluginError; use crate::json_struct; use serde::{Deserialize, Serialize}; -use std::fmt; -use std::str::FromStr; use warpgate_api::VirtualPath; -/// Architecture of the host environment. -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum HostArch { - X86, - #[default] - X64, - Arm, - Arm64, - Mips, - Mips64, - Powerpc, - Powerpc64, - S390x, -} - -impl HostArch { - pub fn to_rust_arch(&self) -> String { - match self { - Self::X64 => "x86_64".into(), - Self::Arm64 => "aarch64".into(), - _ => self.to_string(), - } - } -} - -impl fmt::Display for HostArch { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", format!("{:?}", self).to_lowercase()) - } -} - -impl FromStr for HostArch { - type Err = PluginError; - - fn from_str(s: &str) -> Result { - match s { - "x86" => Ok(Self::X86), - "x86_64" => Ok(Self::X64), - "arm" => Ok(Self::Arm), - "aarch64" => Ok(Self::Arm64), - "mips" => Ok(Self::Mips), - "mips64" => Ok(Self::Mips64), - "powerpc" => Ok(Self::Powerpc), - "powerpc64" => Ok(Self::Powerpc64), - "s390x" => Ok(Self::S390x), - arch => Err(PluginError::Message(format!( - "Unsupported architecture {arch}." - ))), - } - } -} - -/// Operating system of the host environment. -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum HostOS { - #[default] - Linux, - MacOS, - FreeBSD, - NetBSD, - OpenBSD, - Windows, -} - -impl HostOS { - pub fn is_bsd(&self) -> bool { - matches!(self, Self::FreeBSD | Self::NetBSD | Self::OpenBSD) - } - - pub fn is_linux(&self) -> bool { - !matches!(self, Self::MacOS | Self::Windows) - } - - pub fn to_rust_os(&self) -> String { - self.to_string() - } -} - -impl fmt::Display for HostOS { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", format!("{:?}", self).to_lowercase()) - } -} - -impl FromStr for HostOS { - type Err = PluginError; - - fn from_str(s: &str) -> Result { - match s { - "linux" => Ok(Self::Linux), - "macos" => Ok(Self::MacOS), - "freebsd" => Ok(Self::FreeBSD), - "netbsd" => Ok(Self::NetBSD), - "openbsd" => Ok(Self::OpenBSD), - "windows" => Ok(Self::Windows), - os => Err(PluginError::Message(format!( - "Unsupported operating system {os}." - ))), - } - } -} +pub use system_env::{SystemArch as HostArch, SystemOS as HostOS}; json_struct!( /// Information about the host environment (the current runtime). diff --git a/crates/pdk-api/src/lib.rs b/crates/pdk-api/src/lib.rs index 063cf123d..ff6ca2586 100644 --- a/crates/pdk-api/src/lib.rs +++ b/crates/pdk-api/src/lib.rs @@ -9,4 +9,5 @@ pub use error::*; pub use hooks::*; pub use host::*; pub use host_funcs::*; +pub use system_env::{DependencyConfig, DependencyName, SystemDependency, SystemPackageManager}; pub use warpgate_api::*; diff --git a/crates/pdk-test-utils/src/macros.rs b/crates/pdk-test-utils/src/macros.rs index d934376c1..113f34b36 100644 --- a/crates/pdk-test-utils/src/macros.rs +++ b/crates/pdk-test-utils/src/macros.rs @@ -15,7 +15,10 @@ macro_rules! generate_download_install_tests { plugin .tool - .setup(&proto_pdk_test_utils::UnresolvedVersionSpec::parse($version).unwrap()) + .setup( + &proto_pdk_test_utils::UnresolvedVersionSpec::parse($version).unwrap(), + false, + ) .await .unwrap(); @@ -82,7 +85,7 @@ macro_rules! generate_download_install_tests { std::fs::create_dir_all(&tool.get_tool_dir()).unwrap(); - assert!(!tool.install().await.unwrap()); + assert!(!tool.install(false).await.unwrap()); } }; } diff --git a/crates/pdk/src/helpers.rs b/crates/pdk/src/helpers.rs index 8b3063e6a..19bfbbd48 100644 --- a/crates/pdk/src/helpers.rs +++ b/crates/pdk/src/helpers.rs @@ -118,12 +118,12 @@ where /// Return the name of the binary for the provided name and OS. /// On Windows, will append ".exe", and keep as-is on other OS's. -pub fn format_bin_name(name: &str, os: HostOS) -> String { +pub fn format_bin_name>(name: T, os: HostOS) -> String { if os == HostOS::Windows { - return format!("{}.exe", name); + return format!("{}.exe", name.as_ref()); } - name.to_owned() + name.as_ref().to_owned() } /// Validate the current host OS and architecture against the @@ -168,7 +168,7 @@ pub fn command_exists(env: &HostEnvironment, command: &str) -> bool { /// Detect whether the current OS is utilizing musl instead of gnu. pub fn is_musl(env: &HostEnvironment) -> bool { - if !env.os.is_linux() { + if !env.os.is_unix() || env.os.is_mac() { return false; } diff --git a/crates/system-env/Cargo.toml b/crates/system-env/Cargo.toml new file mode 100644 index 000000000..8aa1a5c76 --- /dev/null +++ b/crates/system-env/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "system_env" +version = "0.0.1" +edition = "2021" +license = "MIT" +description = "Information about the system environment: operating system, architecture, package manager, etc." +homepage = "https://moonrepo.dev/proto" +repository = "https://github.com/moonrepo/proto" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/system-env/README.md b/crates/system-env/README.md new file mode 100644 index 000000000..d70e75154 --- /dev/null +++ b/crates/system-env/README.md @@ -0,0 +1,5 @@ +# system_env + +![Crates.io](https://img.shields.io/crates/v/system_env) ![Crates.io](https://img.shields.io/crates/d/system_env) + +Information about the system environment: operating system, architecture, package manager, etc. diff --git a/crates/system-env/src/deps.rs b/crates/system-env/src/deps.rs new file mode 100644 index 000000000..3f65be472 --- /dev/null +++ b/crates/system-env/src/deps.rs @@ -0,0 +1,117 @@ +use crate::env::*; +use crate::error::Error; +use crate::pm::*; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(untagged)] +pub enum DependencyName { + Single(String), + SingleMap(HashMap), + Multiple(Vec), +} + +impl Default for DependencyName { + fn default() -> DependencyName { + DependencyName::Single(String::new()) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(default)] +pub struct DependencyConfig { + pub arch: Option, + pub dep: DependencyName, + pub manager: Option, + // pub optional: bool, + pub os: Option, + pub sudo: bool, + pub version: Option, +} + +impl DependencyConfig { + pub fn get_package_names( + &self, + os: &SystemOS, + pm: &SystemPackageManager, + ) -> Result, Error> { + match &self.dep { + DependencyName::Single(name) => Ok(vec![name.to_owned()]), + DependencyName::SingleMap(map) => map + .get(&pm.to_string()) + .or_else(|| map.get(&os.to_string())) + .or_else(|| map.get("*")) + .map(|name| vec![name.to_owned()]) + .ok_or(Error::MissingName), + DependencyName::Multiple(list) => Ok(list.clone()), + } + } +} + +// This shape is what users configure. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(untagged)] +pub enum SystemDependency { + Name(String), + Names(Vec), + Config(DependencyConfig), + Map(HashMap), +} + +impl SystemDependency { + pub fn name(name: &str) -> SystemDependency { + SystemDependency::Name(name.to_owned()) + } + + pub fn names(names: I) -> SystemDependency + where + I: IntoIterator, + V: AsRef, + { + SystemDependency::Names(names.into_iter().map(|n| n.as_ref().to_owned()).collect()) + } + + pub fn for_arch(name: &str, arch: SystemArch) -> SystemDependency { + SystemDependency::Config(DependencyConfig { + arch: Some(arch), + dep: DependencyName::Single(name.into()), + ..DependencyConfig::default() + }) + } + + pub fn for_os(name: &str, os: SystemOS) -> SystemDependency { + SystemDependency::Config(DependencyConfig { + dep: DependencyName::Single(name.into()), + os: Some(os), + ..DependencyConfig::default() + }) + } + + pub fn for_os_arch(name: &str, os: SystemOS, arch: SystemArch) -> SystemDependency { + SystemDependency::Config(DependencyConfig { + arch: Some(arch), + dep: DependencyName::Single(name.into()), + os: Some(os), + ..DependencyConfig::default() + }) + } + + pub fn to_config(self) -> DependencyConfig { + match self { + Self::Name(name) => DependencyConfig { + dep: DependencyName::Single(name), + ..DependencyConfig::default() + }, + Self::Names(names) => DependencyConfig { + dep: DependencyName::Multiple(names), + ..DependencyConfig::default() + }, + Self::Map(map) => DependencyConfig { + dep: DependencyName::SingleMap(map), + ..DependencyConfig::default() + }, + Self::Config(config) => config, + } + } +} diff --git a/crates/system-env/src/env.rs b/crates/system-env/src/env.rs new file mode 100644 index 000000000..f51595404 --- /dev/null +++ b/crates/system-env/src/env.rs @@ -0,0 +1,153 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::env::{self, consts}; +use std::fmt; + +/// Architecture of the host environment. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum SystemArch { + X86, + #[serde(alias = "x86_64")] + X64, + Arm, + #[serde(alias = "aarch64")] + Arm64, + #[serde(alias = "loongarch64")] + LongArm64, + M68k, + Mips, + Mips64, + Powerpc, + Powerpc64, + Riscv64, + S390x, + Sparc64, +} + +impl SystemArch { + pub fn from_env() -> SystemArch { + serde_json::from_value(Value::String(consts::ARCH.to_owned())) + .expect("Unknown architecture!") + } + + pub fn to_rust_arch(&self) -> String { + match self { + Self::X64 => "x86_64".into(), + Self::Arm64 => "aarch64".into(), + Self::LongArm64 => "loongarch64".into(), + _ => self.to_string(), + } + } +} + +impl Default for SystemArch { + fn default() -> Self { + SystemArch::from_env() + } +} + +impl fmt::Display for SystemArch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", format!("{:?}", self).to_lowercase()) + } +} + +/// Operating system of the host environment. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum SystemOS { + Android, + Dragonfly, + FreeBSD, + IOS, + Linux, + #[serde(alias = "mac")] + MacOS, + NetBSD, + OpenBSD, + Solaris, + Windows, +} + +impl SystemOS { + pub fn from_env() -> SystemOS { + serde_json::from_value(Value::String(consts::OS.to_owned())) + .expect("Unknown operating system!") + } + + pub fn is_bsd(&self) -> bool { + matches!( + self, + Self::Dragonfly | Self::FreeBSD | Self::NetBSD | Self::OpenBSD + ) + } + + pub fn is_linux(&self) -> bool { + matches!(self, Self::Linux) + } + + pub fn is_mac(&self) -> bool { + matches!(self, Self::MacOS) + } + + pub fn is_unix(&self) -> bool { + self.is_bsd() || matches!(self, Self::Linux | Self::MacOS) + } + + pub fn is_windows(&self) -> bool { + matches!(self, Self::Windows) + } + + pub fn to_rust_os(&self) -> String { + self.to_string() + } +} + +impl Default for SystemOS { + fn default() -> Self { + SystemOS::from_env() + } +} + +impl fmt::Display for SystemOS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", format!("{:?}", self).to_lowercase()) + } +} + +#[cfg(windows)] +pub fn is_command_on_path(name: &str) -> bool { + let Ok(system_path) = env::var("PATH") else { + return false; + }; + let Ok(path_ext) = env::var("PATHEXT") else { + return false; + }; + let exts = path_ext.split(';').collect::>(); + + for path_dir in env::split_paths(&system_path) { + for ext in &exts { + if path_dir.join(format!("{name}{ext}")).exists() { + return true; + } + } + } + + false +} + +#[cfg(not(windows))] +pub fn is_command_on_path(name: &str) -> bool { + let Ok(system_path) = env::var("PATH") else { + return false; + }; + + for path_dir in env::split_paths(&system_path) { + if path_dir.join(name).exists() { + return true; + } + } + + false +} diff --git a/crates/system-env/src/error.rs b/crates/system-env/src/error.rs new file mode 100644 index 000000000..bbf6cbf3f --- /dev/null +++ b/crates/system-env/src/error.rs @@ -0,0 +1,11 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("System dependency is missing a package name for the target OS and architecture.")] + MissingName, + + #[error("No system package manager was detected.")] + MissingPackageManager, + + #[error("Unknown or unsupported system package manager `{0}`.")] + UnknownPackageManager(String), +} diff --git a/crates/system-env/src/lib.rs b/crates/system-env/src/lib.rs new file mode 100644 index 000000000..b5e4b66a9 --- /dev/null +++ b/crates/system-env/src/lib.rs @@ -0,0 +1,13 @@ +mod deps; +mod env; +mod error; +mod pm; +mod pm_vendor; +mod system; + +pub use deps::*; +pub use env::*; +pub use error::*; +pub use pm::*; +pub use pm_vendor::*; +pub use system::*; diff --git a/crates/system-env/src/pm.rs b/crates/system-env/src/pm.rs new file mode 100644 index 000000000..2ffa91cd2 --- /dev/null +++ b/crates/system-env/src/pm.rs @@ -0,0 +1,107 @@ +use crate::error::Error; +use crate::is_command_on_path; +use crate::pm_vendor::*; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum SystemPackageManager { + // BSD + Pkg, + Pkgin, + + // Linux + Apk, + Apt, + Dnf, + Pacman, + Yum, + + // MacOS + #[serde(alias = "homebrew")] + Brew, + + // Windows + #[serde(alias = "chocolatey")] + Choco, + Scoop, +} + +impl SystemPackageManager { + pub fn detect() -> Result { + #[cfg(target_os = "linux")] + { + let release = std::fs::read_to_string("/etc/os-release").unwrap_or_default(); + + if let Some(id) = release.lines().find(|l| l.starts_with("ID=")) { + return match id[3..].trim_matches('"') { + "debian" | "ubuntu" | "pop-os" | "deepin" | "elementary OS" | "kali" + | "linuxmint" => Ok(SystemPackageManager::Apt), + "arch" | "manjaro" => Ok(SystemPackageManager::Pacman), + "centos" | "redhat" | "rhel" => Ok(SystemPackageManager::Yum), + "fedora" => Ok(SystemPackageManager::Dnf), + "alpine" => Ok(SystemPackageManager::Apk), + name => Err(Error::UnknownPackageManager(name.to_owned())), + }; + } + } + + #[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + if is_command_on_path("pkg") { + return Ok(SystemPackageManager::Pkg); + } + + if is_command_on_path("pkgin") { + return Ok(SystemPackageManager::Pkgin); + } + } + + #[cfg(target_os = "macos")] + { + if is_command_on_path("brew") { + return Ok(SystemPackageManager::Brew); + } + } + + #[cfg(target_os = "windows")] + { + if is_command_on_path("choco") { + return Ok(SystemPackageManager::Choco); + } + + if is_command_on_path("scoop") { + return Ok(SystemPackageManager::Scoop); + } + } + + Err(Error::MissingPackageManager) + } + + pub fn get_config(&self) -> PackageVendorConfig { + match self { + Self::Apk => apk(), + Self::Apt => apt(), + Self::Dnf => dnf(), + Self::Pacman => pacman(), + Self::Pkg => pkg(), + Self::Pkgin => pkgin(), + Self::Yum => yum(), + Self::Brew => brew(), + Self::Choco => choco(), + Self::Scoop => scoop(), + } + } +} + +impl fmt::Display for SystemPackageManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", format!("{:?}", self).to_lowercase()) + } +} diff --git a/crates/system-env/src/pm_vendor.rs b/crates/system-env/src/pm_vendor.rs new file mode 100644 index 000000000..9b69cc666 --- /dev/null +++ b/crates/system-env/src/pm_vendor.rs @@ -0,0 +1,179 @@ +use std::collections::HashMap; + +macro_rules! string_vec { + ($($item:expr),+ $(,)?) => {{ + vec![ + $( String::from($item), )* + ] + }}; +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Command { + InstallPackage, + UpdateIndex, +} + +#[derive(Clone, Debug)] +pub enum PromptArgument { + None, + // -i + Interactive(String), + // -y + Skip(String), +} + +#[derive(Clone, Debug)] +pub enum VersionArgument { + None, + // pkg=1.2.3 + Inline(String), + // pkg --version 1.2.3 + Separate(String), +} + +#[derive(Clone, Debug)] +pub struct PackageVendorConfig { + pub commands: HashMap>, + pub prompt_arg: PromptArgument, + pub prompt_for: Vec, + pub version_arg: VersionArgument, +} + +pub fn apk() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([ + (Command::InstallPackage, string_vec!["apk", "add", "$"]), + (Command::UpdateIndex, string_vec!["apk", "update"]), + ]), + prompt_arg: PromptArgument::Interactive("-i".into()), + prompt_for: vec![Command::InstallPackage, Command::UpdateIndex], + version_arg: VersionArgument::Inline("=".into()), + } +} + +pub fn apt() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([ + ( + Command::InstallPackage, + string_vec!["apt", "install", "--install-recommends", "$"], + ), + (Command::UpdateIndex, string_vec!["apt", "update"]), + ]), + prompt_arg: PromptArgument::Skip("-y".into()), + prompt_for: vec![Command::InstallPackage, Command::UpdateIndex], + version_arg: VersionArgument::Inline("=".into()), + } +} + +pub fn brew() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([ + (Command::InstallPackage, string_vec!["brew", "install", "$"]), + (Command::UpdateIndex, string_vec!["brew", "update"]), + ]), + prompt_arg: PromptArgument::Interactive("-i".into()), + prompt_for: vec![Command::InstallPackage], + version_arg: VersionArgument::Inline("@".into()), + } +} + +pub fn choco() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([( + Command::InstallPackage, + string_vec!["choco", "install", "$"], + )]), + prompt_arg: PromptArgument::Skip("-y".into()), + prompt_for: vec![Command::InstallPackage], + version_arg: VersionArgument::Separate("--version".into()), + } +} + +pub fn dnf() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([ + (Command::InstallPackage, string_vec!["dnf", "install", "$"]), + (Command::UpdateIndex, string_vec!["dnf", "check-update"]), + ]), + prompt_arg: PromptArgument::Skip("-y".into()), + prompt_for: vec![Command::InstallPackage, Command::UpdateIndex], + version_arg: VersionArgument::Inline("-".into()), + } +} + +pub fn pacman() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([ + (Command::InstallPackage, string_vec!["pacman", "-S", "$"]), + (Command::UpdateIndex, string_vec!["pacman", "-Syy"]), + ]), + prompt_arg: PromptArgument::Skip("--noconfirm".into()), + prompt_for: vec![Command::InstallPackage], + version_arg: VersionArgument::Inline(">=".into()), + } +} + +pub fn pkg() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([ + (Command::InstallPackage, string_vec!["pkg", "install", "$"]), + (Command::UpdateIndex, string_vec!["pkg", "update"]), + ]), + prompt_arg: PromptArgument::Skip("-y".into()), + prompt_for: vec![Command::InstallPackage], + version_arg: VersionArgument::None, + } +} + +pub fn pkg_alt() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([(Command::InstallPackage, string_vec!["pkg_add", "$"])]), + prompt_arg: PromptArgument::Skip("-I".into()), + prompt_for: vec![Command::InstallPackage], + version_arg: VersionArgument::None, + } +} + +pub fn pkgin() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([ + ( + Command::InstallPackage, + string_vec!["pkgin", "install", "$"], + ), + (Command::UpdateIndex, string_vec!["pkgin", "update"]), + ]), + prompt_arg: PromptArgument::Skip("-y".into()), + prompt_for: vec![Command::InstallPackage, Command::UpdateIndex], + version_arg: VersionArgument::Inline("-".into()), + } +} + +pub fn scoop() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([ + ( + Command::InstallPackage, + string_vec!["scoop", "install", "$"], + ), + (Command::UpdateIndex, string_vec!["scoop", "update"]), + ]), + prompt_arg: PromptArgument::None, + prompt_for: vec![], + version_arg: VersionArgument::Inline("@".into()), + } +} + +pub fn yum() -> PackageVendorConfig { + PackageVendorConfig { + commands: HashMap::from_iter([ + (Command::InstallPackage, string_vec!["yum", "install", "$"]), + (Command::UpdateIndex, string_vec!["yum", "check-update"]), + ]), + prompt_arg: PromptArgument::Skip("-y".into()), + prompt_for: vec![Command::InstallPackage], + version_arg: VersionArgument::Inline("-".into()), + } +} diff --git a/crates/system-env/src/system.rs b/crates/system-env/src/system.rs new file mode 100644 index 000000000..49f3429cf --- /dev/null +++ b/crates/system-env/src/system.rs @@ -0,0 +1,129 @@ +use crate::deps::{DependencyConfig, SystemDependency}; +use crate::env::*; +use crate::error::Error; +use crate::pm::*; +use crate::pm_vendor::*; + +pub struct System { + pub arch: SystemArch, + pub manager: SystemPackageManager, + pub os: SystemOS, +} + +impl System { + pub fn new() -> Result { + Ok(System::with_manager(SystemPackageManager::detect()?)) + } + + pub fn with_manager(manager: SystemPackageManager) -> Self { + System { + arch: SystemArch::from_env(), + manager, + os: SystemOS::from_env(), + } + } + + pub fn get_install_package_command( + &self, + dep_config: &DependencyConfig, + interactive: bool, + ) -> Result, Error> { + let os = dep_config.os.unwrap_or(self.os); + let pm = dep_config.manager.unwrap_or(self.manager); + let pm_config = pm.get_config(); + let mut args = vec![]; + + for arg in pm_config + .commands + .get(&Command::InstallPackage) + .cloned() + .unwrap() + { + if arg == "$" { + for dep in dep_config.get_package_names(&os, &pm)? { + if let Some(ver) = &dep_config.version { + match &pm_config.version_arg { + VersionArgument::None => { + args.push(dep); + } + VersionArgument::Inline(op) => { + args.push(format!("{dep}{op}{ver}")); + } + VersionArgument::Separate(opt) => { + args.push(dep); + args.push(opt.to_owned()); + args.push(ver.to_owned()); + } + }; + } else { + args.push(dep); + } + } + } else { + args.push(arg); + } + } + + self.append_interactive(Command::InstallPackage, &pm_config, &mut args, interactive); + + Ok(args) + } + + pub fn get_update_index_command(&self, interactive: bool) -> Option> { + let pm_config = self.manager.get_config(); + + if let Some(args) = pm_config.commands.get(&Command::UpdateIndex) { + let mut args = args.to_owned(); + + self.append_interactive(Command::UpdateIndex, &pm_config, &mut args, interactive); + + return Some(args); + } + + None + } + + pub fn resolve_dependencies(&self, deps: Vec) -> Vec { + let mut configs = vec![]; + + for dep in deps { + let config = dep.to_config(); + + if config.os.as_ref().is_some_and(|o| o != &self.os) { + continue; + } + + if config.arch.as_ref().is_some_and(|a| a != &self.arch) { + continue; + } + + configs.push(config); + } + + configs + } + + fn append_interactive( + &self, + command: Command, + config: &PackageVendorConfig, + args: &mut Vec, + interactive: bool, + ) { + if config.prompt_for.contains(&command) { + match &config.prompt_arg { + PromptArgument::None => {} + PromptArgument::Interactive(i) => { + if interactive { + args.push(i.to_owned()); + } + } + PromptArgument::Skip(y) => { + if !interactive { + args.push(y.to_owned()); + } + } + }; + } + } +} diff --git a/crates/system-env/tests/pm_test.rs b/crates/system-env/tests/pm_test.rs new file mode 100644 index 000000000..a1f25989c --- /dev/null +++ b/crates/system-env/tests/pm_test.rs @@ -0,0 +1,561 @@ +use system_env::*; + +fn one_dep() -> DependencyConfig { + SystemDependency::name("foo").to_config() +} + +fn many_dep() -> DependencyConfig { + SystemDependency::names(["foo", "bar", "baz"]).to_config() +} + +mod pm { + use super::*; + + mod apk { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Apk); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["apk", "add", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["apk", "add", "foo", "-i"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec!["apk", "add", "foo", "bar", "baz"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec!["apk", "add", "foo", "bar", "baz", "-i"] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Apk); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["apk", "add", "foo=1.2.3"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Apk); + + assert_eq!( + pm.get_update_index_command(false).unwrap(), + vec!["apk", "update"] + ); + assert_eq!( + pm.get_update_index_command(true).unwrap(), + vec!["apk", "update", "-i"] + ); + } + } + + mod apt { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Apt); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["apt", "install", "--install-recommends", "foo", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["apt", "install", "--install-recommends", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec![ + "apt", + "install", + "--install-recommends", + "foo", + "bar", + "baz", + "-y" + ] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec![ + "apt", + "install", + "--install-recommends", + "foo", + "bar", + "baz" + ] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Apt); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["apt", "install", "--install-recommends", "foo=1.2.3", "-y"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Apt); + + assert_eq!( + pm.get_update_index_command(false).unwrap(), + vec!["apt", "update", "-y"] + ); + assert_eq!( + pm.get_update_index_command(true).unwrap(), + vec!["apt", "update"] + ); + } + } + + mod brew { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Brew); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["brew", "install", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["brew", "install", "foo", "-i"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec!["brew", "install", "foo", "bar", "baz"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec!["brew", "install", "foo", "bar", "baz", "-i"] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Brew); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["brew", "install", "foo@1.2.3"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Brew); + + assert_eq!( + pm.get_update_index_command(false).unwrap(), + vec!["brew", "update"] + ); + assert_eq!( + pm.get_update_index_command(true).unwrap(), + vec!["brew", "update"] + ); + } + } + + mod choco { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Choco); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["choco", "install", "foo", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["choco", "install", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec!["choco", "install", "foo", "bar", "baz", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec!["choco", "install", "foo", "bar", "baz"] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Choco); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["choco", "install", "foo", "--version", "1.2.3", "-y"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Choco); + + assert_eq!(pm.get_update_index_command(false), None); + } + } + + mod dnf { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Dnf); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["dnf", "install", "foo", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["dnf", "install", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec!["dnf", "install", "foo", "bar", "baz", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec!["dnf", "install", "foo", "bar", "baz"] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Dnf); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["dnf", "install", "foo-1.2.3", "-y"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Dnf); + + assert_eq!( + pm.get_update_index_command(false).unwrap(), + vec!["dnf", "check-update", "-y"] + ); + assert_eq!( + pm.get_update_index_command(true).unwrap(), + vec!["dnf", "check-update"] + ); + } + } + + mod pacman { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Pacman); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["pacman", "-S", "foo", "--noconfirm"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["pacman", "-S", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec!["pacman", "-S", "foo", "bar", "baz", "--noconfirm"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec!["pacman", "-S", "foo", "bar", "baz"] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Pacman); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["pacman", "-S", "foo>=1.2.3", "--noconfirm"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Pacman); + + assert_eq!( + pm.get_update_index_command(false).unwrap(), + vec!["pacman", "-Syy"] + ); + assert_eq!( + pm.get_update_index_command(true).unwrap(), + vec!["pacman", "-Syy"] + ); + } + } + + mod pkg { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Pkg); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["pkg", "install", "foo", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["pkg", "install", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec!["pkg", "install", "foo", "bar", "baz", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec!["pkg", "install", "foo", "bar", "baz"] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Pkg); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["pkg", "install", "foo", "-y"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Pkg); + + assert_eq!( + pm.get_update_index_command(false).unwrap(), + vec!["pkg", "update"] + ); + assert_eq!( + pm.get_update_index_command(true).unwrap(), + vec!["pkg", "update"] + ); + } + } + + mod pkgin { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Pkgin); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["pkgin", "install", "foo", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["pkgin", "install", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec!["pkgin", "install", "foo", "bar", "baz", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec!["pkgin", "install", "foo", "bar", "baz"] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Pkgin); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["pkgin", "install", "foo-1.2.3", "-y"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Pkgin); + + assert_eq!( + pm.get_update_index_command(false).unwrap(), + vec!["pkgin", "update", "-y"] + ); + assert_eq!( + pm.get_update_index_command(true).unwrap(), + vec!["pkgin", "update"] + ); + } + } + + mod scoop { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Scoop); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["scoop", "install", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["scoop", "install", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec!["scoop", "install", "foo", "bar", "baz"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec!["scoop", "install", "foo", "bar", "baz"] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Scoop); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["scoop", "install", "foo@1.2.3"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Scoop); + + assert_eq!( + pm.get_update_index_command(false).unwrap(), + vec!["scoop", "update"] + ); + assert_eq!( + pm.get_update_index_command(true).unwrap(), + vec!["scoop", "update"] + ); + } + } + + mod yum { + use super::*; + + #[test] + fn install_package() { + let pm = System::with_manager(SystemPackageManager::Yum); + let one_cfg = one_dep(); + let many_cfg = many_dep(); + + assert_eq!( + pm.get_install_package_command(&one_cfg, false).unwrap(), + vec!["yum", "install", "foo", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&one_cfg, true).unwrap(), + vec!["yum", "install", "foo"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, false).unwrap(), + vec!["yum", "install", "foo", "bar", "baz", "-y"] + ); + assert_eq!( + pm.get_install_package_command(&many_cfg, true).unwrap(), + vec!["yum", "install", "foo", "bar", "baz"] + ); + } + + #[test] + fn install_package_with_version() { + let pm = System::with_manager(SystemPackageManager::Yum); + let mut cfg = one_dep(); + cfg.version = Some("1.2.3".into()); + + assert_eq!( + pm.get_install_package_command(&cfg, false).unwrap(), + vec!["yum", "install", "foo-1.2.3", "-y"] + ); + } + + #[test] + fn update_index() { + let pm = System::with_manager(SystemPackageManager::Yum); + + assert_eq!( + pm.get_update_index_command(false).unwrap(), + vec!["yum", "check-update"] + ); + assert_eq!( + pm.get_update_index_command(true).unwrap(), + vec!["yum", "check-update"] + ); + } + } +} diff --git a/plugins/Cargo.lock b/plugins/Cargo.lock index d8c4a4424..69e2c80f6 100644 --- a/plugins/Cargo.lock +++ b/plugins/Cargo.lock @@ -47,6 +47,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "ambient-authority" version = "0.0.2" @@ -258,13 +264,14 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cached" -version = "0.45.1" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eb5776f28a149524d1d8623035760b4454ec881e8cf3838fa8d7e1b11254b3" +checksum = "8cead8ece0da6b744b2ad8ef9c58a4cdc7ef2921e60a6ddfb9eaaa86839b5fc5" dependencies = [ + "ahash", "cached_proc_macro", "cached_proc_macro_types", - "hashbrown 0.13.2", + "hashbrown 0.14.0", "instant", "once_cell", "thiserror", @@ -1270,6 +1277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ "ahash", + "allocator-api2", ] [[package]] @@ -2044,7 +2052,7 @@ dependencies = [ [[package]] name = "proto_core" -version = "0.18.4" +version = "0.18.6" dependencies = [ "cached", "extism", @@ -2073,7 +2081,7 @@ dependencies = [ [[package]] name = "proto_pdk" -version = "0.7.3" +version = "0.7.4" dependencies = [ "anyhow", "extism-pdk", @@ -2091,6 +2099,7 @@ dependencies = [ "semver", "serde", "serde_json", + "system_env", "thiserror", "warpgate_api", ] @@ -2108,7 +2117,7 @@ dependencies = [ [[package]] name = "proto_wasm_plugin" -version = "0.6.6" +version = "0.6.7" dependencies = [ "extism", "proto_pdk_api", @@ -2527,9 +2536,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" dependencies = [ "serde", ] @@ -2610,9 +2619,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2849,6 +2858,15 @@ dependencies = [ "winx 0.36.2", ] +[[package]] +name = "system_env" +version = "0.0.1" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "tar" version = "0.4.40" @@ -2902,18 +2920,18 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", @@ -3288,7 +3306,7 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.5.7" +version = "0.5.8" dependencies = [ "extism", "miette",