From 98aa4bcc3c486d4f3a5b608f708eee9dd204206a Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 21 Jan 2025 15:53:36 -0800 Subject: [PATCH] new: Add build from source flow. (#701) --- .prototools | 8 +- CHANGELOG.md | 12 + Cargo.lock | 180 +++-- Cargo.toml | 26 +- crates/cli/Cargo.toml | 8 +- crates/cli/src/commands/install.rs | 82 +- crates/cli/src/workflows/install_workflow.rs | 49 +- crates/cli/tests/run_test.rs | 4 +- crates/codegen/Cargo.toml | 6 +- crates/core/Cargo.toml | 14 +- crates/core/src/error.rs | 6 +- crates/core/src/flow/build.rs | 739 +++++++++++++++++++ crates/core/src/flow/build_error.rs | 44 ++ crates/core/src/flow/install.rs | 218 +++--- crates/core/src/flow/mod.rs | 2 + crates/core/src/layout/store.rs | 2 + crates/core/src/proto.rs | 3 + crates/installer/Cargo.toml | 4 +- crates/pdk-api/Cargo.toml | 8 +- crates/pdk-api/src/api/build_source.rs | 51 +- crates/pdk-api/src/api/mod.rs | 19 +- crates/pdk-api/src/error.rs | 3 + crates/pdk-api/src/lib.rs | 4 +- crates/pdk-test-utils/Cargo.toml | 8 +- crates/pdk-test-utils/src/macros.rs | 1 + crates/pdk/Cargo.toml | 6 +- crates/system-env/Cargo.toml | 2 +- crates/system-env/src/deps.rs | 83 ++- crates/system-env/src/env.rs | 9 +- crates/system-env/src/error.rs | 3 + crates/system-env/src/pm.rs | 7 +- crates/system-env/src/pm_vendor.rs | 2 +- crates/system-env/src/system.rs | 161 +++- crates/system-env/tests/pm_test.rs | 242 ++++-- crates/version-spec/Cargo.toml | 2 +- crates/version-spec/src/lib.rs | 2 +- crates/warpgate-api/Cargo.toml | 4 +- crates/warpgate-pdk/Cargo.toml | 4 +- crates/warpgate/Cargo.toml | 6 +- plugins/Cargo.lock | 384 ++++++++-- 40 files changed, 1915 insertions(+), 503 deletions(-) create mode 100644 crates/core/src/flow/build.rs create mode 100644 crates/core/src/flow/build_error.rs diff --git a/.prototools b/.prototools index 59679d82b..e8e9e3001 100644 --- a/.prototools +++ b/.prototools @@ -1,6 +1,10 @@ [plugins] -# moon-test = "file://./crates/cli/tests/fixtures/moon-schema.yaml" -moon-test = "https://raw.githubusercontent.com/moonrepo/moon/master/proto-plugin.toml" +# deno-test = "file://../moonrepo/plugins/target/wasm32-wasip1/debug/deno_tool.wasm" +# go-test = "file://../moonrepo/plugins/target/wasm32-wasip1/debug/go_tool.wasm" +# node-test = "file://../moonrepo/plugins/target/wasm32-wasip1/debug/node_tool.wasm" +# moon-test = "file://../moonrepo/plugins/target/wasm32-wasip1/debug/moon_tool.wasm" +# python-test = "file://../moonrepo/plugins/target/wasm32-wasip1/debug/python_tool.wasm" +# ruby-test = "file://../moonrepo/plugins/target/wasm32-wasip1/debug/ruby_tool.wasm" wasm-test = "file://./plugins/target/wasm32-wasip1/debug/proto_wasm_test.wasm" # [tools.wasm-test] diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be545aa6..88786a486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ - [Rust](https://github.com/moonrepo/plugins/blob/master/tools/rust/CHANGELOG.md) - [Schema (TOML, JSON, YAML)](https://github.com/moonrepo/plugins/blob/master/tools/internal-schema/CHANGELOG.md) +## Unreleased + +#### 🚀 Updates + +- Added a new interactive "build from source" flow for many built-in tools. + - Added `--build` and `--no-build` to `proto install`. + - Supported for `deno`, `go`, `node`, `python`, and `ruby`. +- WASM API + - Added a `build_instructions` plugin function for building from source, with associated structs and enums. + - Added a `ToolMetadataOutput.default_install_strategy` field, which defaults to prebuilds. + - Added a `ToolMetadataOutput.unstable` field, which can mark the tool as unstable. + ## 0.44.7 #### 🚀 Updates diff --git a/Cargo.lock b/Cargo.lock index 12d5ea667..de4d4ab37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,9 +472,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.25" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95dca1b68188a08ca6af9d96a6576150f598824bdb528c1190460c2940a0b48" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -482,9 +482,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.25" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab52925392148efd3f7562f2136a81ffb778076bcc85727c6e020d6dd57cf15" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -606,9 +606,9 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" dependencies = [ "unicode-segmentation", ] @@ -946,6 +946,15 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -953,7 +962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -965,10 +974,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.59.0", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -976,7 +997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -1359,9 +1380,9 @@ dependencies = [ [[package]] name = "garde" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dbf10452e3dbf51033a5035a05762b2653c43bf84d46e96f15bc93beedd426d" +checksum = "6a989bd2fd12136080f7825ff410d9239ce84a2a639487fc9d924ee42e2fb84f" dependencies = [ "compact_str", "garde_derive", @@ -1372,9 +1393,9 @@ dependencies = [ [[package]] name = "garde_derive" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfdbc9c39fad7991686e229c55cf71565eafe73dcb2cf38ddf1d4aa3ca7e176" +checksum = "1f7f0545bbbba0a37d4d445890fa5759814e0716f02417b39f6fab292193df68" dependencies = [ "proc-macro2", "quote", @@ -1878,9 +1899,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.0", @@ -1917,9 +1938,9 @@ checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" [[package]] name = "iocraft" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44a6c534027c6c1355aa1e223ed9786e2f0362d5ddbca8cc95512665ec70d516" +checksum = "5a34280c018dcd0a94d2c7a8b2999578d62ffee4decf52b01aeabd77a980932c" dependencies = [ "any_key", "bitflags", @@ -2077,6 +2098,16 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check 0.9.5", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2611,7 +2642,7 @@ dependencies = [ "clap", "clap_complete", "clap_complete_nushell", - "dirs 5.0.1", + "dirs 6.0.0", "extism", "indexmap", "iocraft", @@ -2647,7 +2678,7 @@ dependencies = [ [[package]] name = "proto_codegen" -version = "0.2.1" +version = "0.3.0" dependencies = [ "proto_core", "proto_pdk_api", @@ -2657,12 +2688,13 @@ dependencies = [ [[package]] name = "proto_core" -version = "0.44.6" +version = "0.45.0" dependencies = [ "clap", "convert_case", "dotenvy", "indexmap", + "iocraft", "miette 7.4.0", "minisign-verify", "once_cell", @@ -2678,9 +2710,11 @@ dependencies = [ "sha2", "shell-words", "starbase_archive", + "starbase_console", "starbase_sandbox", "starbase_styles", "starbase_utils", + "system_env", "thiserror 2.0.11", "tokio", "tracing", @@ -2692,7 +2726,7 @@ dependencies = [ [[package]] name = "proto_installer" -version = "0.8.0" +version = "0.9.0" dependencies = [ "miette 7.4.0", "reqwest", @@ -2706,7 +2740,7 @@ dependencies = [ [[package]] name = "proto_pdk" -version = "0.25.5" +version = "0.26.0" dependencies = [ "extism-pdk", "proto_pdk_api", @@ -2717,7 +2751,7 @@ dependencies = [ [[package]] name = "proto_pdk_api" -version = "0.24.6" +version = "0.25.0" dependencies = [ "proto_pdk_api", "rustc-hash", @@ -2733,7 +2767,7 @@ dependencies = [ [[package]] name = "proto_pdk_test_utils" -version = "0.31.0" +version = "0.32.0" dependencies = [ "proto_core", "proto_pdk_api", @@ -2748,7 +2782,7 @@ name = "proto_shim" version = "0.5.0" dependencies = [ "command-group", - "dirs 5.0.1", + "dirs 6.0.0", ] [[package]] @@ -2898,6 +2932,17 @@ dependencies = [ "thiserror 1.0.64", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror 2.0.11", +] + [[package]] name = "reflink-copy" version = "0.1.20" @@ -3254,9 +3299,9 @@ dependencies = [ [[package]] name = "schematic" -version = "0.17.8" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc902ccf410e32f7d1fdcad8080729def0cd5e696fb70ca7468bfcd02ff4fe80" +checksum = "c5780c4ee657b40b2422f965d4f7525be233dab38c3b624bf446888d62e24b19" dependencies = [ "garde", "indexmap", @@ -3276,9 +3321,9 @@ dependencies = [ [[package]] name = "schematic_macros" -version = "0.17.6" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f638e083cb574cc60a6f15a9eecc45bb4ace4bbbeda0e904298788f04e9453" +checksum = "1e5ea932a91232414dc5c0f34f0e3ad60c8244968f9e8b06e8b3a4b87ae99051" dependencies = [ "convert_case", "darling", @@ -3289,9 +3334,9 @@ dependencies = [ [[package]] name = "schematic_types" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f320deb050277c5bcc79213b01d749f8a847a63713e76d5748fdad654f47ed52" +checksum = "15254cba352d302f26c0e427bf2634e513dff936f8a86c6df4f94843818fa4e3" dependencies = [ "indexmap", "semver", @@ -3338,9 +3383,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" dependencies = [ "serde", ] @@ -3367,9 +3412,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "indexmap", "itoa", @@ -3410,16 +3455,18 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "serde_yml" +version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" dependencies = [ "indexmap", "itoa", + "libyml", + "memchr", "ryu", "serde", - "unsafe-libyaml", + "version_check 0.9.5", ] [[package]] @@ -3643,9 +3690,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "starbase" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c724a819d301240d0a381812c9d1b6d49a25f8d91067b4c50bc5e80a98f73cd" +checksum = "985934bcffec6009ca2eb721b93fd7b87aee8605e9d134205d8d7e6ba141a158" dependencies = [ "async-trait", "chrono", @@ -3659,9 +3706,9 @@ dependencies = [ [[package]] name = "starbase_archive" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f47e43d0768400f8b83d372b6f136b55c12e431c5d5fb38b06f70a0b17f5cd" +checksum = "550327b4bb767734859d2bca7ed08ba362ac289fbf3fbf52ae54c41ce68f25a2" dependencies = [ "binstall-tar", "bzip2", @@ -3679,9 +3726,9 @@ dependencies = [ [[package]] name = "starbase_console" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "499b8afb848cd44603f363b7bcb10fc1bb80747f2aae3628b5fbda5395bf47c2" +checksum = "dfb2fd7146c0e55b53ab44aa4a178d4f27f2fd5ce33718054207cf8aaeacc020" dependencies = [ "crossterm", "iocraft", @@ -3694,9 +3741,9 @@ dependencies = [ [[package]] name = "starbase_sandbox" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6b0744e5208869e328432b88a09a51d8e047dfc4d559229ce4683ca3bf7f9cc" +checksum = "bf641da9f534f7c67e318a75cb30bd37ef9de303fb77483c431c2e27a912ee56" dependencies = [ "assert_cmd", "assert_fs", @@ -3709,9 +3756,9 @@ dependencies = [ [[package]] name = "starbase_shell" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c1e1840cdc0bfea3a95d3b7611001ce7f20750328c9357283cfb05eb6dcb6db" +checksum = "000669be46d025d8716bda6ee3b2cb1ef051c0a5c0f46707d47828cf4500d496" dependencies = [ "miette 7.4.0", "regex", @@ -3722,11 +3769,11 @@ dependencies = [ [[package]] name = "starbase_styles" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa762771c938c1d2435b6ee09791cf47492ba9f80ac2496ad93f722a33c15b7e" +checksum = "65b486cfc419b43fee8b42a87f8d654c1c1d439df34cdf1bfa152cc6bdf8456d" dependencies = [ - "dirs 5.0.1", + "dirs 6.0.0", "miette 7.4.0", "owo-colors", "supports-color", @@ -3734,12 +3781,12 @@ dependencies = [ [[package]] name = "starbase_utils" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf0fbbff304c62e8ac3b31d96ac986faf11d47ee2e11d6033412451cfefa45e" +checksum = "8ccad09b8d83d873b1873aa4aa1ceeb9b26e96b367000805cb6b2085319eb784" dependencies = [ "async-trait", - "dirs 5.0.1", + "dirs 6.0.0", "fs4", "json-strip-comments", "miette 7.4.0", @@ -3747,10 +3794,9 @@ dependencies = [ "reqwest", "serde", "serde_json", - "serde_yaml", + "serde_yml", "starbase_styles", "thiserror 2.0.11", - "tokio", "toml", "tracing", "url", @@ -3879,7 +3925,7 @@ dependencies = [ [[package]] name = "system_env" -version = "0.6.1" +version = "0.7.0" dependencies = [ "schematic", "serde", @@ -4309,12 +4355,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -4370,9 +4410,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ "getrandom", ] @@ -4397,7 +4437,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "version_spec" -version = "0.7.1" +version = "0.7.2" dependencies = [ "compact_str", "human-sort", @@ -4438,7 +4478,7 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.20.3" +version = "0.21.0" dependencies = [ "async-trait", "compact_str", @@ -4470,7 +4510,7 @@ dependencies = [ [[package]] name = "warpgate_api" -version = "0.10.1" +version = "0.11.0" dependencies = [ "anyhow", "rustc-hash", @@ -4483,7 +4523,7 @@ dependencies = [ [[package]] name = "warpgate_pdk" -version = "0.8.1" +version = "0.9.0" dependencies = [ "extism-pdk", "serde", diff --git a/Cargo.toml b/Cargo.toml index fd2087bd4..1f0534fcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,18 +6,18 @@ default-members = ["crates/cli"] [workspace.dependencies] anyhow = "1.0.95" async-trait = "0.1.85" -clap = "4.5.25" +clap = "4.5.27" clap_complete = "4.5.42" compact_str = { version = "0.8.1", default-features = false, features = [ "serde", ] } -dirs = "5.0.1" +dirs = "6.0.0" extism = ">=1.6.0" # Lower for consumers extism-pdk = "1.3.0" http-cache-reqwest = "0.15.0" human-sort = "0.2.2" indexmap = "2.7.0" -iocraft = "0.6.1" +iocraft = "0.6.2" # iocraft = { git = "https://github.com/ccbrown/iocraft.git", branch = "main" } miette = "7.4.0" once_cell = "1.20.2" @@ -34,14 +34,14 @@ reqwest-middleware = { version = "0.4.0", default-features = false, features = [ reqwest-netrc = "0.1.2" rustc-hash = "2.1.0" scc = "2.3.0" -schematic = { version = "0.17.8", default-features = false } -semver = { version = "1.0.24", features = ["serde"] } +schematic = { version = "0.17.10", default-features = false } +semver = { version = "1.0.25", features = ["serde"] } serde = { version = "1.0.217", features = ["derive"] } -serde_json = "1.0.135" +serde_json = "1.0.137" sha2 = "0.10.8" shell-words = "1.1.0" -starbase = { version = "0.9.8" } -starbase_archive = { version = "0.9.1", features = [ +starbase = { version = "0.9.9" } +starbase_archive = { version = "0.9.3", features = [ "gz", "miette", "tar-bz2", @@ -51,13 +51,13 @@ starbase_archive = { version = "0.9.1", features = [ "zip", "zip-deflate", ] } -starbase_console = { version = "0.4.4", features = [ +starbase_console = { version = "0.4.6", features = [ "ui", ] } # , path = "../starbase/crates/console" } -starbase_sandbox = { version = "0.8.1" } -starbase_shell = { version = "0.6.12", features = ["miette"] } +starbase_sandbox = { version = "0.8.2" } +starbase_shell = { version = "0.6.13", features = ["miette"] } starbase_styles = { version = "0.4.11" } -starbase_utils = { version = "0.9.3", default-features = false, features = [ +starbase_utils = { version = "0.10.0", default-features = false, features = [ "json", "miette", "net", @@ -66,7 +66,7 @@ starbase_utils = { version = "0.9.3", default-features = false, features = [ thiserror = "2.0.11" tokio = { version = "1.43.0", features = ["full", "tracing"] } tracing = "0.1.41" -uuid = { version = "1.11.1", features = ["v4"] } +uuid = { version = "1.12.1", features = ["v4"] } [profile.dist] inherits = "release" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a1e4c4e6c..2353618a9 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -32,11 +32,11 @@ name = "proto-shim" path = "src/main_shim.rs" [dependencies] -proto_core = { version = "0.44.6", path = "../core", features = ["clap"] } -proto_installer = { version = "0.8.0", path = "../installer" } -proto_pdk_api = { version = "0.24.6", path = "../pdk-api" } +proto_core = { version = "0.45.0", path = "../core", features = ["clap"] } +proto_installer = { version = "0.9.0", path = "../installer" } +proto_pdk_api = { version = "0.25.0", path = "../pdk-api" } proto_shim = { version = "0.5.0", path = "../shim" } -system_env = { version = "0.6.1", path = "../system-env" } +system_env = { version = "0.7.0", path = "../system-env" } anyhow = { workspace = true } async-trait = { workspace = true } chrono = "0.4.39" diff --git a/crates/cli/src/commands/install.rs b/crates/cli/src/commands/install.rs index 7eb561139..3e81f8be4 100644 --- a/crates/cli/src/commands/install.rs +++ b/crates/cli/src/commands/install.rs @@ -7,6 +7,7 @@ use clap::Args; use iocraft::prelude::element; use miette::IntoDiagnostic; use proto_core::{ConfigMode, Id, PinLocation, Tool, UnresolvedVersionSpec}; +use proto_pdk_api::InstallStrategy; use starbase::AppResult; use starbase_console::ui::*; use starbase_console::utils::formats::format_duration; @@ -30,6 +31,20 @@ pub struct InstallArgs { )] pub spec: Option, + #[arg( + long, + help = "Build from source instead of downloading a pre-built", + group = "build-type" + )] + pub build: bool, + + #[arg( + long, + help = "Download a pre-built instead of building from source", + group = "build-type" + )] + pub no_build: bool, + #[arg( long, help = "When installing one tool, use a canary (nightly, etc) version", @@ -56,6 +71,16 @@ pub struct InstallArgs { } impl InstallArgs { + fn get_strategy(&self) -> Option { + if self.build { + Some(InstallStrategy::BuildFromSource) + } else if self.no_build { + Some(InstallStrategy::DownloadPrebuilt) + } else { + None + } + } + fn get_pin_location(&self) -> Option { self.pin.as_ref().map(|pin| pin.unwrap_or_default()) } @@ -101,35 +126,56 @@ pub async fn install_one(session: ProtoSession, args: InstallArgs, id: Id) -> Ap } // Create our workflow and setup the progress reporter - let mut workflow = InstallWorkflow::new(tool); - let reporter = workflow.progress_reporter.clone(); - let console = session.console.clone(); + let mut workflow = InstallWorkflow::new(tool, session.console.clone()); + let mut handle = None; - let handle = spawn(async move { - console - .render_loop(element! { - InstallProgress(reporter) - }) - .await - }); + // Only show the progress bars when not building + if workflow.is_build(args.get_strategy()) { + session.console.render(element! { + Notice(variant: Variant::Caution) { + StyledText( + content: "Building from source is currently unstable. Please report general issues to https://github.com/moonrepo/proto", + ) + StyledText( + content: "and tool specific issues to https://github.com/moonrepo/plugins.", + ) + } + })?; + } else { + let reporter = workflow.progress_reporter.clone(); + let console = session.console.clone(); + + handle = Some(spawn(async move { + console + .render_loop(element! { + InstallProgress(reporter) + }) + .await + })); - // Wait a bit for the component to be rendered - sleep(Duration::from_millis(50)).await; + // Wait a bit for the component to be rendered + sleep(Duration::from_millis(50)).await; + } let result = workflow .install( args.get_unresolved_spec(), InstallWorkflowParams { pin_to: args.get_pin_location(), + strategy: args.get_strategy(), force: args.force, multiple: false, passthrough_args: args.passthrough, + skip_prompts: session.should_skip_prompts(), }, ) .await; workflow.progress_reporter.exit(); - handle.await.into_diagnostic()??; + + if let Some(handle) = handle { + handle.await.into_diagnostic()??; + } let outcome = result?; let tool = workflow.tool; @@ -237,6 +283,8 @@ async fn install_all(session: ProtoSession, args: InstallArgs) -> AppResult { let started = Instant::now(); let force = args.force; let pin_to = args.get_pin_location(); + let skip_prompts = session.should_skip_prompts(); + let strategy = args.get_strategy(); for tool in tools { enforce_requirements(&tool, &versions)?; @@ -248,7 +296,7 @@ async fn install_all(session: ProtoSession, args: InstallArgs) -> AppResult { let tool_id = tool.id.clone(); let initial_version = version.clone(); let topo_graph = topo_graph.clone(); - let mut workflow = InstallWorkflow::new(tool); + let mut workflow = InstallWorkflow::new(tool, session.console.clone()); // Clone the progress reporters so that we can render // multiple progress bars in parallel @@ -297,9 +345,11 @@ async fn install_all(session: ProtoSession, args: InstallArgs) -> AppResult { initial_version, InstallWorkflowParams { force, - pin_to, multiple: true, - ..Default::default() + passthrough_args: vec![], + pin_to, + skip_prompts, + strategy, }, ) .await diff --git a/crates/cli/src/workflows/install_workflow.rs b/crates/cli/src/workflows/install_workflow.rs index 29113233e..993f40240 100644 --- a/crates/cli/src/workflows/install_workflow.rs +++ b/crates/cli/src/workflows/install_workflow.rs @@ -1,10 +1,11 @@ use crate::commands::pin::internal_pin; +use crate::session::ProtoConsole; use crate::shell::{self, Export}; use crate::telemetry::*; use crate::utils::tool_record::ToolRecord; use proto_core::flow::install::{InstallOptions, InstallPhase}; use proto_core::{PinLocation, UnresolvedVersionSpec, PROTO_PLUGIN_KEY}; -use proto_pdk_api::{InstallHook, SyncShellProfileInput, SyncShellProfileOutput}; +use proto_pdk_api::{InstallHook, InstallStrategy, SyncShellProfileInput, SyncShellProfileOutput}; use starbase_console::ui::{ProgressDisplay, ProgressReporter}; use starbase_console::utils::formats::format_duration; use starbase_shell::ShellType; @@ -16,30 +17,40 @@ pub enum InstallOutcome { AlreadyInstalled, Installed, FailedToInstall, + NotInstalled, } -#[derive(Default)] pub struct InstallWorkflowParams { pub force: bool, - #[allow(dead_code)] pub multiple: bool, pub passthrough_args: Vec, pub pin_to: Option, + pub skip_prompts: bool, + pub strategy: Option, } pub struct InstallWorkflow { + pub console: ProtoConsole, pub progress_reporter: ProgressReporter, pub tool: ToolRecord, } impl InstallWorkflow { - pub fn new(tool: ToolRecord) -> Self { + pub fn new(tool: ToolRecord, console: ProtoConsole) -> Self { Self { + console, progress_reporter: ProgressReporter::default(), tool, } } + pub fn is_build(&self, strategy: Option) -> bool { + matches!( + strategy.unwrap_or(self.tool.metadata.default_install_strategy), + InstallStrategy::BuildFromSource + ) + } + pub async fn install( &mut self, initial_version: UnresolvedVersionSpec, @@ -47,6 +58,14 @@ impl InstallWorkflow { ) -> miette::Result { let started = Instant::now(); + if params.multiple && self.is_build(params.strategy) { + self.progress_reporter.set_message( + "Build from source is currently not supported in the multi-install workflow", + ); + + return Ok(InstallOutcome::NotInstalled); + } + self.progress_reporter.set_message(format!( "Installing {} with specification {}", self.tool.get_name(), @@ -104,11 +123,6 @@ impl InstallWorkflow { async fn pre_install(&self, params: &InstallWorkflowParams) -> miette::Result<()> { let tool = &self.tool; - env::set_var( - format!("{}_VERSION", tool.get_env_var_prefix()), - tool.get_resolved_version().to_string(), - ); - env::set_var("PROTO_INSTALL", tool.id.to_string()); if tool.plugin.has_func("pre_install").await { @@ -135,6 +149,7 @@ impl InstallWorkflow { self.tool.resolve_version(initial_version, false).await?; let resolved_version = self.tool.get_resolved_version(); + let default_strategy = self.tool.metadata.default_install_strategy; self.progress_reporter.set_message( if initial_version == &resolved_version.to_unresolved_spec() { @@ -179,7 +194,11 @@ impl InstallWorkflow { InstallPhase::Native => "Installing natively".to_owned(), InstallPhase::Verify { file, .. } => format!("Verifying checksum against {file}"), InstallPhase::Unpack { file } => format!("Unpacking archive {file}"), - InstallPhase::Download { file, .. } => format!("Downloading pre-built archive {file} | {{bytes}} / {{total_bytes}} | {{bytes_per_sec}}") + InstallPhase::Download { file, .. } => format!("Downloading pre-built archive {file} | {{bytes}} / {{total_bytes}} | {{bytes_per_sec}}"), + InstallPhase::InstallDeps => "Installing system dependencies".into(), + InstallPhase::CheckRequirements => "Checking requirements".into(), + InstallPhase::ExecuteInstructions => "Executing build instructions".into(), + InstallPhase::CloneRepository { url } => format!("Cloning repository {url}") }); }); @@ -187,10 +206,18 @@ impl InstallWorkflow { .setup( initial_version, InstallOptions { + // When installing multiple tools, we can't render the nice + // UI for the build flow, so rely on the progress bars + console: if params.multiple { + None + } else { + Some(self.console.clone()) + }, on_download_chunk: Some(on_download_chunk), on_phase_change: Some(on_phase_change), force: params.force, - ..InstallOptions::default() + skip_prompts: params.skip_prompts, + strategy: params.strategy.unwrap_or(default_strategy), }, ) .await diff --git a/crates/cli/tests/run_test.rs b/crates/cli/tests/run_test.rs index d6f49e977..7bc1c9063 100644 --- a/crates/cli/tests/run_test.rs +++ b/crates/cli/tests/run_test.rs @@ -190,7 +190,7 @@ mod run { }) .success(); - assert.stdout(predicate::str::contains("Node.js 19.0.0 installed")); + assert.stdout(predicate::str::contains("installed")); } #[test] @@ -209,7 +209,7 @@ mod run { }) .success(); - assert.stdout(predicate::str::contains("Node.js 19.0.0 installed")); + assert.stdout(predicate::str::contains("installed")); env::remove_var("PROTO_AUTO_INSTALL"); } diff --git a/crates/codegen/Cargo.toml b/crates/codegen/Cargo.toml index 9d9c744d7..3795c1d05 100644 --- a/crates/codegen/Cargo.toml +++ b/crates/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proto_codegen" -version = "0.2.1" +version = "0.3.0" edition = "2021" license = "MIT" publish = false @@ -9,8 +9,8 @@ publish = false dist = false [dependencies] -proto_core = { version = "0.44.6", path = "../core" } -proto_pdk_api = { version = "0.24.6", path = "../pdk-api", features = [ +proto_core = { version = "0.45.0", path = "../core" } +proto_pdk_api = { version = "0.25.0", path = "../pdk-api", features = [ "schematic", ] } schematic = { workspace = true, features = [ diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 47dab1649..e16d6b045 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proto_core" -version = "0.44.6" +version = "0.45.0" edition = "2021" license = "MIT" description = "Core proto APIs." @@ -8,20 +8,22 @@ homepage = "https://moonrepo.dev/proto" repository = "https://github.com/moonrepo/proto" [dependencies] -proto_pdk_api = { version = "0.24.6", path = "../pdk-api", features = [ +proto_pdk_api = { version = "0.25.0", path = "../pdk-api", features = [ "schematic", ] } proto_shim = { version = "0.5.0", path = "../shim" } -version_spec = { version = "0.7.1", path = "../version-spec", features = [ +system_env = { version = "0.7.0", path = "../system-env" } +version_spec = { version = "0.7.2", path = "../version-spec", features = [ "schematic", ] } -warpgate = { version = "0.20.3", path = "../warpgate", features = [ +warpgate = { version = "0.21.0", path = "../warpgate", features = [ "schematic", ] } clap = { workspace = true, optional = true } -convert_case = "0.6.0" +convert_case = "0.7.1" dotenvy = "0.15.7" indexmap = { workspace = true } +iocraft = { workspace = true } miette = { workspace = true } minisign-verify = "0.2.3" once_cell = { workspace = true } @@ -42,9 +44,11 @@ serde_json = { workspace = true } sha2 = { workspace = true } shell-words = { workspace = true } starbase_archive = { workspace = true } +starbase_console = { workspace = true, features = ["ui"] } starbase_styles = { workspace = true } starbase_utils = { workspace = true, features = ["fs-lock", "yaml"] } thiserror = { workspace = true } +tokio = { workspace = true } tracing = { workspace = true } url = { version = "2.5.4", features = ["serde"] } uuid = { workspace = true } diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index f1ea3d24a..778200a49 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -127,8 +127,12 @@ pub enum ProtoError { )] UnknownTool { id: Id }, + #[diagnostic(code(proto::prebuilt::unsupported))] + #[error("Downloading a pre-built is not supported for {tool}. Try building from source by passing {}.", "--build".style(Style::Shell))] + UnsupportedDownloadPrebuilt { tool: String }, + #[diagnostic(code(proto::build::unsupported))] - #[error("Build from source is not supported for {tool}.")] + #[error("Building from source is not supported for {tool}. Try downloading a pre-built by passing {}.", "--no-build".style(Style::Shell))] UnsupportedBuildFromSource { tool: String }, #[diagnostic( diff --git a/crates/core/src/flow/build.rs b/crates/core/src/flow/build.rs new file mode 100644 index 000000000..fe987e4df --- /dev/null +++ b/crates/core/src/flow/build.rs @@ -0,0 +1,739 @@ +use super::build_error::*; +use super::install::{InstallPhase, OnPhaseFn}; +use crate::helpers::extract_filename_from_url; +use crate::proto::{ProtoConsole, ProtoEnvironment}; +use iocraft::prelude::{element, FlexDirection, View}; +use miette::IntoDiagnostic; +use proto_pdk_api::{ + BuildInstruction, BuildInstructionsOutput, BuildRequirement, GitSource, SourceLocation, +}; +use rustc_hash::FxHashMap; +use schematic::color::apply_style_tags; +use semver::Version; +use starbase_archive::Archiver; +use starbase_console::ui::{ + Confirm, Container, Entry, ListCheck, ListItem, Section, Style, StyledText, +}; +use starbase_styles::color; +use starbase_utils::{fs, net}; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use system_env::{find_command_on_path, is_command_on_path, System}; +use tokio::process::Command; +use tracing::{debug, error, trace}; +use version_spec::{get_semver_regex, VersionSpec}; +use warpgate::HttpClient; + +pub struct InstallBuildOptions<'a> { + pub console: Option<&'a ProtoConsole>, + pub http_client: &'a HttpClient, + pub install_dir: &'a Path, + pub on_phase_change: Option, + pub skip_prompts: bool, + pub system: System, + pub temp_dir: &'a Path, + pub version: VersionSpec, +} + +struct StepManager<'a> { + errors: u8, + options: &'a InstallBuildOptions<'a>, +} + +impl StepManager<'_> { + pub fn new<'a>(options: &'a InstallBuildOptions<'a>) -> StepManager<'a> { + StepManager { errors: 0, options } + } + + pub fn has_errors(&self) -> bool { + self.errors > 0 + } + + pub fn render_header(&self, title: impl AsRef) -> miette::Result<()> { + let title = title.as_ref(); + + if let Some(console) = &self.options.console { + console.out.write_newline()?; + console.render(element! { + Container { + Section(title) + } + })?; + } else { + debug!("{title}"); + } + + Ok(()) + } + + pub fn render_check(&mut self, message: impl AsRef, passed: bool) -> miette::Result<()> { + let message = message.as_ref(); + + if let Some(console) = &self.options.console { + console.render(element! { + ListCheck(checked: passed) { + StyledText(content: message) + } + })?; + } else { + let message = apply_style_tags(message); + + if passed { + debug!("{message}"); + } else { + error!("{message}"); + } + } + + if !passed { + self.errors += 1; + } + + Ok(()) + } + + pub fn render_checkpoint(&self, message: impl AsRef) -> miette::Result<()> { + let message = message.as_ref(); + + if let Some(console) = &self.options.console { + console.render(element! { + ListItem(bullet: "❯".to_owned()) { + StyledText(content: message) + } + })?; + } else { + debug!("{}", apply_style_tags(message)); + } + + Ok(()) + } + + pub async fn prompt_continue(&self, label: &str) -> miette::Result<()> { + if self.options.skip_prompts { + return Ok(()); + } + + if let Some(console) = &self.options.console { + let mut confirmed = false; + + console + .render_interactive(element! { + Confirm(label, on_confirm: &mut confirmed) + }) + .await?; + + if !confirmed { + return Err(ProtoBuildError::Cancelled.into()); + } + } + + Ok(()) + } +} + +async fn exec_command(command: &mut Command) -> miette::Result { + let inner = command.as_std(); + let command_line = format!( + "{} {}", + inner.get_program().to_string_lossy(), + shell_words::join( + inner + .get_args() + .map(|arg| arg.to_string_lossy()) + .collect::>() + ) + ); + + trace!( + cwd = ?inner.get_current_dir(), + env = ?inner.get_envs() + .filter_map(|(key, val)| val.map(|v| (key, v.to_string_lossy()))) + .collect::>(), + "Running command {}", color::shell(&command_line) + ); + + let child = command + .spawn() + .map_err(|error| ProtoBuildError::CommandFailed { + command: command_line.clone(), + error: Box::new(error), + })?; + + let output = + child + .wait_with_output() + .await + .map_err(|error| ProtoBuildError::CommandFailed { + command: command_line.clone(), + error: Box::new(error), + })?; + + let stderr = String::from_utf8(output.stderr).into_diagnostic()?; + let stdout = String::from_utf8(output.stdout).into_diagnostic()?; + let code = output.status.code().unwrap_or(-1); + + trace!( + code, + stderr, + stdout, + "Ran command {}", + color::shell(&command_line) + ); + + if !output.status.success() { + return Err(ProtoBuildError::CommandNonZeroExit { + command: command_line.clone(), + code, + } + .into()); + } + + Ok(stdout) +} + +async fn exec_command_piped(command: &mut Command) -> miette::Result { + exec_command(command.stderr(Stdio::piped()).stdout(Stdio::piped())).await +} + +async fn checkout_git_repo( + git: &GitSource, + cwd: &Path, + step: &StepManager<'_>, +) -> miette::Result<()> { + if cwd.join(".git").exists() { + exec_command( + Command::new("git") + .args(["pull", "--ff", "--prune"]) + .current_dir(cwd), + ) + .await?; + + return Ok(()); + } + + fs::create_dir_all(cwd)?; + + exec_command( + Command::new("git") + .args(if git.submodules { + vec!["clone", "--recurse-submodules"] + } else { + vec!["clone"] + }) + .arg(&git.url) + .arg(".") + .current_dir(cwd), + ) + .await?; + + if let Some(reference) = &git.reference { + step.render_checkpoint(format!("Checking out reference {}", reference))?; + + exec_command( + Command::new("git") + .arg("checkout") + .arg(reference) + .current_dir(cwd), + ) + .await?; + } + + Ok(()) +} + +// STEP 1 + +pub async fn install_system_dependencies( + build: &BuildInstructionsOutput, + options: &InstallBuildOptions<'_>, +) -> miette::Result<()> { + let step = StepManager::new(options); + let system = &options.system; + + if let Some(console) = &options.console { + console.render(element! { + Container { + Section(title: "Build information") + View(padding_left: 2, flex_direction: FlexDirection::Column) { + Entry(name: "Operating system", content: system.os.to_string()) + Entry(name: "Architecture", content: system.arch.to_string()) + #(system.manager.map(|pm| { + element! { + Entry(name: "Package manager", content: pm.to_string()) + } + })) + Entry(name: "Version", value: element! { + StyledText(content: options.version.to_string(), style: Style::Hash) + }.into_any()) + #(build.help_url.as_ref().map(|url| { + element! { + Entry(name: "Documentation", value: element! { + StyledText(content: url, style: Style::Url) + }.into_any()) + } + })) + } + } + })?; + } else { + debug!( + os = ?system.os, + arch = ?system.arch, + pm = ?system.manager, + "Gathering system information", + ); + } + + let Some(pm) = system.manager else { + return Ok(()); + }; + + step.render_header("Installing system dependencies")?; + + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::InstallDeps); + }); + + if let Some(mut index_args) = system + .get_update_index_command(!options.skip_prompts) + .into_diagnostic()? + { + step.render_checkpoint("Updating package manager index")?; + + exec_command(Command::new(index_args.remove(0)).args(index_args)).await?; + } + + let dep_configs = system.resolve_dependencies(&build.system_dependencies); + + if !dep_configs.is_empty() { + if let Some(mut install_args) = system + .get_install_packages_command(&dep_configs, !options.skip_prompts) + .into_diagnostic()? + { + step.render_checkpoint(format!( + "Required {} packages: {}", + pm, + dep_configs + .iter() + .filter_map(|cfg| cfg.get_package_names(&pm).ok()) + .flatten() + .map(|name| format!("{name}")) + .collect::>() + .join(", ") + ))?; + + step.prompt_continue("Install packages?").await?; + + exec_command(Command::new(install_args.remove(0)).args(install_args)).await?; + } + } + + Ok(()) +} + +// STEP 2 + +async fn get_command_version(cmd: &str, version_arg: &str) -> miette::Result { + let output = exec_command_piped(Command::new(cmd).arg(version_arg)).await?; + + // Remove leading ^ and trailing $ + let base_pattern = get_semver_regex().as_str(); + let pattern = regex::Regex::new(&base_pattern[1..(base_pattern.len() - 1)]).unwrap(); + + let value = pattern + .find(&output) + .map(|res| res.as_str()) + .unwrap_or(&output); + + Ok( + Version::parse(value).map_err(|error| ProtoBuildError::VersionParseFailed { + value: value.to_owned(), + error: Box::new(error), + })?, + ) +} + +pub async fn check_requirements( + build: &BuildInstructionsOutput, + options: &InstallBuildOptions<'_>, +) -> miette::Result<()> { + if build.requirements.is_empty() { + return Ok(()); + } + + let mut step = StepManager::new(options); + + step.render_header("Checking requirements")?; + + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::CheckRequirements); + }); + + for req in &build.requirements { + match req { + BuildRequirement::CommandExistsOnPath(cmd) => { + debug!(cmd, "Checking if a command exists on PATH"); + + if let Some(cmd_path) = find_command_on_path(cmd) { + step.render_check( + format!( + "Command {cmd} exists on PATH: {}", + cmd_path.display() + ), + true, + )?; + } else { + step.render_check( + format!("Command {cmd} does NOT exist on PATH, please install it and try again"), + false, + )?; + } + } + BuildRequirement::CommandVersion(cmd, version_req, version_arg) => { + debug!( + cmd, + "Checking if a command meets the required version of {version_req}" + ); + + if is_command_on_path(cmd) { + let version = + get_command_version(cmd, version_arg.as_deref().unwrap_or("--version")) + .await?; + + if version_req.matches(&version) { + step.render_check( + format!("Command {cmd} meets the minimum required version of {version_req}"), + true, + )?; + } else { + step.render_check( + format!("Command {cmd} does NOT meet the minimum required version of {version_req}, found {version}"), + false, + )?; + } + } else { + step.render_check( + format!("Command {cmd} does NOT exist on PATH, please install it and try again"), + false, + )?; + } + } + BuildRequirement::ManualIntercept(url) => { + step.render_check( + format!("Please read the following documentation before proceeding: {url}"), + true, + )?; + + step.prompt_continue("Continue install?").await?; + } + BuildRequirement::GitConfigSetting(config_key, expected_value) => { + debug!( + config_key, + expected_value, "Checking if a Git config setting has the expected value" + ); + + let actual_value = + exec_command_piped(Command::new("git").args(["config", "--get", config_key])) + .await?; + + if &actual_value == expected_value { + step.render_check( + format!("Git config {config_key} matches the required value of {expected_value}"), + true, + )?; + } else { + step.render_check( + format!("Git config {config_key} does NOT match the required value or {expected_value}, found {actual_value}"), + false, + )?; + } + } + BuildRequirement::GitVersion(version_req) => { + debug!("Checking if Git meets the required version of {version_req}"); + + let version = get_command_version("git", "--version").await?; + + if version_req.matches(&version) { + step.render_check( + format!("Git meets the minimum required version of {version_req}"), + true, + )?; + } else { + step.render_check( + format!("Git does NOT meet the minimum required version of {version_req}, found {version}"), + false, + )?; + } + } + BuildRequirement::XcodeCommandLineTools => { + if options.system.os.is_mac() { + debug!("Checking if Xcode command line tools are installed"); + + let result = + exec_command_piped(Command::new("xcode-select").arg("--version")).await; + + if result.is_err() || result.is_ok_and(|out| out.is_empty()) { + step.render_check( + "Xcode command line tools are NOT installed, install them with xcode-select --install", + false, + )?; + } else { + step.render_check("Xcode command line tools are installed", true)?; + } + } + } + BuildRequirement::WindowsDeveloperMode => { + if options.system.os.is_windows() { + debug!("Checking if Windows developer mode is enabled"); + + // Is this possible from the command line? + } + } + }; + } + + if step.has_errors() { + return Err(ProtoBuildError::RequirementsNotMet.into()); + } + + Ok(()) +} + +// STEP 3 + +pub async fn download_sources( + build: &BuildInstructionsOutput, + options: &InstallBuildOptions<'_>, +) -> miette::Result<()> { + // Ensure the install directory is empty, otherwise Git will fail and + // we also want to avoid colliding/stale artifacts. This should also + // run if there's no source, as it's required for instructions! + fs::remove_dir_all(options.install_dir)?; + fs::create_dir_all(options.install_dir)?; + + let Some(source) = &build.source else { + return Ok(()); + }; + + let step = StepManager::new(options); + + step.render_header("Acquiring source files")?; + + match source { + SourceLocation::Archive(archive) => { + let filename = extract_filename_from_url(&archive.url)?; + let download_file = options.temp_dir.join(&filename); + + // Download + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::Download { + url: archive.url.clone(), + file: filename.clone(), + }); + }); + + step.render_checkpoint(format!( + "Downloading archive from {}", + archive.url + ))?; + + net::download_from_url_with_client( + &archive.url, + &download_file, + options.http_client.to_inner(), + ) + .await?; + + // Unpack + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::Unpack { + file: filename.clone(), + }); + }); + + step.render_checkpoint(format!( + "Unpacking archive to {}", + options.install_dir.display() + ))?; + + let mut archiver = Archiver::new(options.install_dir, &download_file); + + if let Some(prefix) = &archive.prefix { + archiver.set_prefix(prefix); + } + + archiver.unpack_from_ext()?; + } + SourceLocation::Git(git) => { + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::CloneRepository { + url: git.url.clone(), + }); + }); + + step.render_checkpoint(format!("Cloning repository {}", git.url))?; + + checkout_git_repo(git, options.install_dir, &step).await?; + } + }; + + Ok(()) +} + +// STEP 4 + +pub async fn execute_instructions( + build: &BuildInstructionsOutput, + options: &InstallBuildOptions<'_>, + proto: &ProtoEnvironment, +) -> miette::Result<()> { + if build.instructions.is_empty() { + return Ok(()); + } + + let step = StepManager::new(options); + + step.render_header("Executing build instructions")?; + + options.on_phase_change.as_ref().inspect(|func| { + func(InstallPhase::ExecuteInstructions); + }); + + let make_absolute = |path: &Path| { + if path.is_absolute() { + path.to_path_buf() + } else { + options.install_dir.join(path) + } + }; + + let total = build.instructions.len(); + let mut builder_exes = FxHashMap::default(); + + for (index, instruction) in build.instructions.iter().enumerate() { + debug!("Executing build instruction {} of {total}", index + 1); + + let prefix = format!("[{}/{total}]", index + 1); + + match instruction { + BuildInstruction::InstallBuilder(builder) => { + step.render_checkpoint(format!( + "{prefix} Installing {} builder ({})", + builder.id, builder.git.url + ))?; + + let builder_dir = proto.store.builders_dir.join(&builder.id); + let builder_exe = builder_dir.join(&builder.exe); + + checkout_git_repo(&builder.git, &builder_dir, &step).await?; + + if !builder_exe.exists() { + return Err(ProtoBuildError::MissingBuilderExe { + exe: builder_exe, + id: builder.id.clone(), + } + .into()); + } + + fs::update_perms(&builder_exe, None)?; + builder_exes.insert(builder.id.clone(), builder_exe); + } + BuildInstruction::MakeExecutable(file) => { + let file = make_absolute(file); + + step.render_checkpoint(format!( + "{prefix} Making file {} executable", + file.display() + ))?; + + fs::update_perms(file, None)?; + } + BuildInstruction::MoveFile(from, to) => { + let from = make_absolute(from); + let to = make_absolute(to); + + step.render_checkpoint(format!( + "{prefix} Moving {} to {}", + from.display(), + to.display(), + ))?; + + fs::rename(from, to)?; + } + BuildInstruction::RemoveDir(dir) => { + let dir = make_absolute(dir); + + step.render_checkpoint(format!( + "{prefix} Removing directory {}", + dir.display() + ))?; + + fs::remove_dir_all(dir)?; + } + BuildInstruction::RemoveFile(file) => { + let file = make_absolute(file); + + step.render_checkpoint(format!( + "{prefix} Removing file {}", + file.display() + ))?; + + fs::remove_file(file)?; + } + BuildInstruction::RequestScript(url) => { + let filename = extract_filename_from_url(url)?; + let download_file = options.temp_dir.join(&filename); + + step.render_checkpoint(format!("{prefix} Requesting script {url}"))?; + + net::download_from_url_with_client( + url, + &download_file, + options.http_client.to_inner(), + ) + .await?; + + fs::rename(download_file, options.install_dir.join(filename))?; + } + BuildInstruction::RunCommand(cmd) => { + let exe = if cmd.builder { + builder_exes.get(&cmd.bin).cloned().ok_or_else(|| { + ProtoBuildError::MissingBuilder { + id: cmd.bin.clone(), + } + })? + } else { + PathBuf::from(&cmd.bin) + }; + + step.render_checkpoint(format!( + "{prefix} Running command {} {}", + exe.file_name().unwrap().to_str().unwrap(), + shell_words::join(&cmd.args) + ))?; + + exec_command( + Command::new(exe) + .args(&cmd.args) + .envs(&cmd.env) + .current_dir( + cmd.cwd + .as_deref() + .map(make_absolute) + .unwrap_or_else(|| options.install_dir.to_path_buf()), + ), + ) + .await?; + } + BuildInstruction::SetEnvVar(key, value) => { + step.render_checkpoint(format!( + "{prefix} Setting environment variable {key} to {value}", + ))?; + + std::env::set_var(key, value); + } + }; + } + + Ok(()) +} diff --git a/crates/core/src/flow/build_error.rs b/crates/core/src/flow/build_error.rs new file mode 100644 index 000000000..7a2efad43 --- /dev/null +++ b/crates/core/src/flow/build_error.rs @@ -0,0 +1,44 @@ +use miette::Diagnostic; +use starbase_styles::{Style, Stylize}; +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Error, Debug, Diagnostic)] +pub enum ProtoBuildError { + #[diagnostic(code(proto::install::build::command_failed))] + #[error("Failed to execute command {}.", .command.style(Style::Shell))] + CommandFailed { + command: String, + #[source] + error: Box, + }, + + #[diagnostic(code(proto::install::build::command_failed))] + #[error("Command {} returned a {code} exit code.", .command.style(Style::Shell))] + CommandNonZeroExit { command: String, code: i32 }, + + #[diagnostic(code(proto::install::build::missing_builder))] + #[error("Builder {} has not been installed.", .id.style(Style::Id))] + MissingBuilder { id: String }, + + #[diagnostic(code(proto::install::build::missing_builder_exe))] + #[error("Executable {} from builder {} does not exist.", .exe.style(Style::Path), .id.style(Style::Id))] + MissingBuilderExe { exe: PathBuf, id: String }, + + #[diagnostic(code(proto::install::build::parse_version_failed))] + #[error("Failed to parse version from {}.", .value.style(Style::Symbol))] + VersionParseFailed { + value: String, + #[source] + error: Box, + }, + + #[diagnostic(code(proto::install::build::unmet_requirements))] + #[error("Build requirements have not been met, unable to proceed.\nPlease satisfy the requirements before attempting the build again.")] + RequirementsNotMet, + + #[diagnostic(code(proto::install::build::cancelled))] + #[error("Build has been cancelled.")] + Cancelled, +} diff --git a/crates/core/src/flow/install.rs b/crates/core/src/flow/install.rs index 67dfc36a4..1f30a6850 100644 --- a/crates/core/src/flow/install.rs +++ b/crates/core/src/flow/install.rs @@ -1,6 +1,8 @@ +use super::build::*; use crate::checksum::verify_checksum; use crate::error::ProtoError; use crate::helpers::{extract_filename_from_url, is_archive_file, is_offline}; +use crate::proto::ProtoConsole; use crate::tool::Tool; use proto_pdk_api::*; use proto_shim::*; @@ -8,33 +10,34 @@ use starbase_archive::Archiver; use starbase_utils::net::DownloadOptions; use starbase_utils::{fs, net}; use std::path::Path; +use system_env::System; use tracing::{debug, instrument}; -#[derive(Debug, Default)] -pub enum InstallStrategy { - BuildFromSource, - #[default] - DownloadPrebuilt, -} - +// Prebuilt: Download -> verify -> unpack +// Build: InstallDeps -> CheckRequirements -> ExecuteInstructions -> ... #[derive(Clone, Debug)] pub enum InstallPhase { Native, - // Download -> verify -> unpack Download { url: String, file: String }, Verify { url: String, file: String }, Unpack { file: String }, + InstallDeps, + CheckRequirements, + ExecuteInstructions, + CloneRepository { url: String }, } pub use starbase_utils::net::OnChunkFn; -pub type OnPhaseFn = Box; +pub type OnPhaseFn = Box; #[derive(Default)] pub struct InstallOptions { + pub console: Option, + pub force: bool, pub on_download_chunk: Option, pub on_phase_change: Option, + pub skip_prompts: bool, pub strategy: InstallStrategy, - pub force: bool, } impl Tool { @@ -102,8 +105,15 @@ impl Tool { .into()) } - #[instrument(skip(self))] - pub async fn build_from_source(&self, _install_dir: &Path) -> miette::Result<()> { + /// Build the tool from source using a set of requirements and instructions + /// into the `~/.proto/tools/` folder. + #[instrument(skip(self, options))] + pub async fn build_from_source( + &self, + install_dir: &Path, + temp_dir: &Path, + mut options: InstallOptions, + ) -> miette::Result<()> { debug!( tool = self.id.as_str(), "Installing tool by building from source" @@ -116,96 +126,42 @@ impl Tool { .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])?; - // } - // }; + let output: BuildInstructionsOutput = self + .plugin + .cache_func_with( + "build_instructions", + BuildInstructionsInput { + context: self.create_context(), + }, + ) + .await?; + + let build_options = InstallBuildOptions { + console: options.console.as_ref(), + install_dir, + http_client: self.proto.get_plugin_loader()?.get_client()?, + on_phase_change: options.on_phase_change.take(), + skip_prompts: options.skip_prompts, + system: System::default(), + temp_dir, + version: self.get_resolved_version(), + }; + + // The build process may require using itself to build itself, + // so allow proto to use any available version instead of failing + std::env::set_var(format!("{}_VERSION", self.get_env_var_prefix()), "*"); + + // Step 1 + install_system_dependencies(&output, &build_options).await?; + + // Step 2 + check_requirements(&output, &build_options).await?; + + // Step 3 + download_sources(&output, &build_options).await?; + + // Step 4 + execute_instructions(&output, &build_options, &self.proto).await?; Ok(()) } @@ -216,6 +172,7 @@ impl Tool { pub async fn install_from_prebuilt( &self, install_dir: &Path, + temp_dir: &Path, mut options: InstallOptions, ) -> miette::Result<()> { debug!( @@ -223,8 +180,14 @@ impl Tool { "Installing tool by downloading a pre-built archive" ); + if !self.plugin.has_func("download_prebuilt").await { + return Err(ProtoError::UnsupportedDownloadPrebuilt { + tool: self.get_name().to_owned(), + } + .into()); + } + let client = self.proto.get_plugin_loader()?.get_client()?; - let temp_dir = self.get_temp_dir(); let output: DownloadPrebuiltOutput = self .plugin @@ -375,21 +338,14 @@ impl Tool { return Err(ProtoError::InternetConnectionRequired.into()); } + let temp_dir = self.get_temp_dir(); 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() - })?; + // Lock the temporary directory instead of the install directory, + // because the latter needs to be clean for "build from source", + // and the `.lock` file breaks that contract + let mut install_lock = fs::lock_directory(&temp_dir)?; // If this function is defined, it acts like an escape hatch and // takes precedence over all other install strategies @@ -425,16 +381,32 @@ impl Tool { } if !installed { - // // Build the tool from source - // if build { - // self.build_from_source(&install_dir).await?; + // Build the tool from source + let result = if matches!(options.strategy, InstallStrategy::BuildFromSource) { + self.build_from_source(&install_dir, &temp_dir, options) + .await + } + // Install from a prebuilt archive + else { + self.install_from_prebuilt(&install_dir, &temp_dir, options) + .await + }; + + // Clean up if the install failed + if let Err(error) = result { + debug!( + tool = self.id.as_str(), + install_dir = ?install_dir, + "Failed to install tool, cleaning up", + ); - // // Install from a prebuilt archive - // } else { - // self.install_from_prebuilt(&install_dir).await?; - // } + install_lock.unlock()?; - self.install_from_prebuilt(&install_dir, options).await?; + fs::remove_dir_all(&install_dir)?; + fs::remove_dir_all(&temp_dir)?; + + return Err(error); + } } debug!( diff --git a/crates/core/src/flow/mod.rs b/crates/core/src/flow/mod.rs index 7ed3510ad..c420485f7 100644 --- a/crates/core/src/flow/mod.rs +++ b/crates/core/src/flow/mod.rs @@ -1,3 +1,5 @@ +mod build; +mod build_error; pub mod install; pub mod link; pub mod locate; diff --git a/crates/core/src/layout/store.rs b/crates/core/src/layout/store.rs index 84621e144..ecc454079 100644 --- a/crates/core/src/layout/store.rs +++ b/crates/core/src/layout/store.rs @@ -16,6 +16,7 @@ use warpgate::Id; pub struct Store { pub dir: PathBuf, pub bin_dir: PathBuf, + pub builders_dir: PathBuf, pub cache_dir: PathBuf, pub inventory_dir: PathBuf, pub plugins_dir: PathBuf, @@ -32,6 +33,7 @@ impl Store { Self { dir: dir.to_path_buf(), bin_dir: dir.join("bin"), + builders_dir: dir.join("builders"), cache_dir: dir.join("cache"), inventory_dir: dir.join("tools"), plugins_dir: dir.join("plugins"), diff --git a/crates/core/src/proto.rs b/crates/core/src/proto.rs index 0ff671a4c..e64a2a7b3 100644 --- a/crates/core/src/proto.rs +++ b/crates/core/src/proto.rs @@ -5,6 +5,7 @@ use crate::proto_config::{ ConfigMode, PinLocation, ProtoConfig, ProtoConfigFile, ProtoConfigManager, PROTO_CONFIG_NAME, }; use once_cell::sync::OnceCell; +use starbase_console::{Console, EmptyReporter}; use starbase_utils::dirs::home_dir; use starbase_utils::env::path_var; use std::collections::BTreeMap; @@ -15,6 +16,8 @@ use std::sync::Arc; use tracing::debug; use warpgate::PluginLoader; +pub type ProtoConsole = Console; + #[derive(Clone, Default)] pub struct ProtoEnvironment { pub config_mode: ConfigMode, diff --git a/crates/installer/Cargo.toml b/crates/installer/Cargo.toml index f1ad328a2..4312d8851 100644 --- a/crates/installer/Cargo.toml +++ b/crates/installer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proto_installer" -version = "0.8.0" +version = "0.9.0" edition = "2021" license = "MIT" description = "Download and install proto." @@ -8,7 +8,7 @@ homepage = "https://moonrepo.dev/proto" repository = "https://github.com/moonrepo/proto" [dependencies] -system_env = { version = "0.6.1", path = "../system-env" } +system_env = { version = "0.7.0", path = "../system-env" } miette = { workspace = true } reqwest = { workspace = true, features = ["stream"] } starbase_archive = { workspace = true } diff --git a/crates/pdk-api/Cargo.toml b/crates/pdk-api/Cargo.toml index 5359490cc..abbfb418b 100644 --- a/crates/pdk-api/Cargo.toml +++ b/crates/pdk-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proto_pdk_api" -version = "0.24.6" +version = "0.25.0" edition = "2021" license = "MIT" description = "Core APIs for creating proto WASM plugins." @@ -8,9 +8,9 @@ homepage = "https://moonrepo.dev/proto" repository = "https://github.com/moonrepo/proto" [dependencies] -system_env = { version = "0.6.1", path = "../system-env" } -version_spec = { version = "0.7.1", path = "../version-spec" } -warpgate_api = { version = "0.10.1", path = "../warpgate-api" } +system_env = { version = "0.7.0", path = "../system-env" } +version_spec = { version = "0.7.2", path = "../version-spec" } +warpgate_api = { version = "0.11.0", path = "../warpgate-api" } rustc-hash = { workspace = true } schematic = { workspace = true, features = [ "schema", diff --git a/crates/pdk-api/src/api/build_source.rs b/crates/pdk-api/src/api/build_source.rs index eb6a439f2..b275b8488 100644 --- a/crates/pdk-api/src/api/build_source.rs +++ b/crates/pdk-api/src/api/build_source.rs @@ -1,3 +1,4 @@ +use super::is_false; use crate::ToolContext; use rustc_hash::FxHashMap; use semver::VersionReq; @@ -20,7 +21,7 @@ api_struct!( pub url: String, /// A path prefix within the archive to remove. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub prefix: Option, } ); @@ -32,10 +33,11 @@ api_struct!( pub url: String, /// The branch/commit/tag to checkout. - pub reference: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reference: Option, /// Include submodules during checkout. - #[serde(default)] + #[serde(default, skip_serializing_if = "is_false")] pub submodules: bool, } ); @@ -54,30 +56,49 @@ api_enum!( } ); +api_struct!( + /// A builder and its parameters for installing the builder. + pub struct BuilderInstruction { + /// Unique identifier for this builder. + pub id: String, + + /// Main executable, relative from the source root. + pub exe: PathBuf, + + /// The Git source location for the builder. + pub git: GitSource, + } +); + api_struct!( /// A command and its parameters to be executed as a child process. pub struct CommandInstruction { /// The binary on `PATH`. pub bin: String, + /// If the binary should reference a builder executable. + pub builder: bool, + /// List of arguments. + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub args: Vec, /// Map of environment variables. - #[serde(default)] + #[serde(default, skip_serializing_if = "FxHashMap::is_empty")] pub env: FxHashMap, /// The working directory. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub cwd: Option, } ); impl CommandInstruction { - /// Create a new command. + /// Create a new command with the binary and arguments. pub fn new, V: AsRef>(bin: &str, args: I) -> Self { Self { bin: bin.to_owned(), + builder: false, args: args .into_iter() .map(|arg| arg.as_ref().to_owned()) @@ -86,12 +107,22 @@ impl CommandInstruction { cwd: None, } } + + /// Create a new command that executes a binary from a builder with the arguments. + pub fn with_builder, V: AsRef>(id: &str, args: I) -> Self { + let mut cmd = Self::new(id, args); + cmd.builder = true; + cmd + } } api_enum!( /// An instruction to execute. #[serde(tag = "type", content = "instruction", rename_all = "kebab-case")] pub enum BuildInstruction { + /// Install a builder locally that can be referenced in subsequent instructions. + InstallBuilder(Box), + /// Update a file and make it executable. MakeExecutable(PathBuf), @@ -110,6 +141,9 @@ api_enum!( /// Execute a command as a child process. #[cfg_attr(feature = "schematic", schema(nested))] RunCommand(Box), + + /// Set an environment variable. + SetEnvVar(String, String), } ); @@ -118,11 +152,10 @@ api_enum!( #[serde(tag = "type", content = "requirement", rename_all = "kebab-case")] pub enum BuildRequirement { CommandExistsOnPath(String), + CommandVersion(String, VersionReq, Option), ManualIntercept(String), // url GitConfigSetting(String, String), GitVersion(VersionReq), - PythonVersion(VersionReq), - RubyVersion(VersionReq), // macOS XcodeCommandLineTools, // Windows @@ -135,6 +168,7 @@ api_struct!( #[serde(default)] pub struct BuildInstructionsOutput { /// Link to the documentation/help. + #[serde(skip_serializing_if = "Option::is_none")] pub help_url: Option, /// List of instructions to execute to build the tool, after system @@ -148,6 +182,7 @@ api_struct!( pub requirements: Vec, /// Location in which to acquire the source files. + #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// List of system dependencies that are required for building from source. diff --git a/crates/pdk-api/src/api/mod.rs b/crates/pdk-api/src/api/mod.rs index 6a0194366..97138cf26 100644 --- a/crates/pdk-api/src/api/mod.rs +++ b/crates/pdk-api/src/api/mod.rs @@ -9,7 +9,7 @@ use warpgate_api::*; pub use build_source::*; pub use semver::{Version, VersionReq}; -fn is_false(value: &bool) -> bool { +pub(crate) fn is_false(value: &bool) -> bool { !(*value) } @@ -66,6 +66,16 @@ api_struct!( } ); +api_enum!( + /// Supported strategies for installing a tool. + #[derive(Copy, Default)] + pub enum InstallStrategy { + BuildFromSource, + #[default] + DownloadPrebuilt, + } +); + api_struct!( /// Output returned by the `register_tool` function. pub struct ToolMetadataOutput { @@ -73,6 +83,10 @@ api_struct!( #[serde(default, skip_serializing_if = "Option::is_none")] pub config_schema: Option, + /// Default strategy to use when installing a tool. + #[serde(default)] + pub default_install_strategy: InstallStrategy, + /// Default alias or version to use as a fallback. #[serde(default, skip_serializing_if = "Option::is_none")] pub default_version: Option, @@ -111,6 +125,7 @@ api_struct!( pub type_of: PluginType, /// Whether this plugin is unstable or not. + #[serde(default)] pub unstable: Switch, } ); @@ -377,7 +392,7 @@ api_struct!( /// Relative directory path from the tool install directory in which /// pre-installed executables can be located. This directory path - /// will be used during `proto active`, but not for bins/shims. + /// will be used during `proto activate`, but not for bins/shims. #[serde(skip_serializing_if = "Option::is_none")] pub exes_dir: Option, diff --git a/crates/pdk-api/src/error.rs b/crates/pdk-api/src/error.rs index 4e7f5146c..e24dc5bf1 100644 --- a/crates/pdk-api/src/error.rs +++ b/crates/pdk-api/src/error.rs @@ -21,4 +21,7 @@ pub enum PluginError { arch: String, os: String, }, + + #[error("Build from source is currently not supported on Windows.")] + UnsupportedWindowsBuild, } diff --git a/crates/pdk-api/src/lib.rs b/crates/pdk-api/src/lib.rs index 12504a4fc..02ad0315c 100644 --- a/crates/pdk-api/src/lib.rs +++ b/crates/pdk-api/src/lib.rs @@ -7,6 +7,8 @@ pub use api::*; pub use error::*; pub use hooks::*; pub use shapes::*; -pub use system_env::{DependencyConfig, DependencyName, SystemDependency, SystemPackageManager}; +pub use system_env::{ + DependencyConfig, DependencyName, SystemDependency, SystemPackageManager as HostPackageManager, +}; pub use version_spec::*; pub use warpgate_api::*; diff --git a/crates/pdk-test-utils/Cargo.toml b/crates/pdk-test-utils/Cargo.toml index 090040afe..0ee4c243c 100644 --- a/crates/pdk-test-utils/Cargo.toml +++ b/crates/pdk-test-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proto_pdk_test_utils" -version = "0.31.0" +version = "0.32.0" edition = "2021" license = "MIT" description = "Utilities for testing proto WASM plugins." @@ -8,9 +8,9 @@ homepage = "https://moonrepo.dev/proto" repository = "https://github.com/moonrepo/proto" [dependencies] -proto_core = { version = "0.44.6", path = "../core" } -proto_pdk_api = { version = "0.24.6", path = "../pdk-api" } -warpgate = { version = "0.20.3", path = "../warpgate" } +proto_core = { version = "0.45.0", path = "../core" } +proto_pdk_api = { version = "0.25.0", path = "../pdk-api" } +warpgate = { version = "0.21.0", path = "../warpgate" } # extism = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/pdk-test-utils/src/macros.rs b/crates/pdk-test-utils/src/macros.rs index 62ee9a3e1..72e2b24ea 100644 --- a/crates/pdk-test-utils/src/macros.rs +++ b/crates/pdk-test-utils/src/macros.rs @@ -59,6 +59,7 @@ macro_rules! generate_download_install_tests { tool.install_from_prebuilt( &tool.get_product_dir(), + &temp_dir, proto_pdk_test_utils::flow::install::InstallOptions::default(), ) .await diff --git a/crates/pdk/Cargo.toml b/crates/pdk/Cargo.toml index 0ec3b8df7..cb64a277f 100644 --- a/crates/pdk/Cargo.toml +++ b/crates/pdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proto_pdk" -version = "0.25.5" +version = "0.26.0" edition = "2021" license = "MIT" description = "A plugin development kit for creating proto WASM plugins." @@ -8,8 +8,8 @@ homepage = "https://moonrepo.dev/proto" repository = "https://github.com/moonrepo/proto" [dependencies] -proto_pdk_api = { version = "0.24.6", path = "../pdk-api" } -warpgate_pdk = { version = "0.8.1", path = "../warpgate-pdk" } +proto_pdk_api = { version = "0.25.0", path = "../pdk-api" } +warpgate_pdk = { version = "0.9.0", path = "../warpgate-pdk" } extism-pdk = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } diff --git a/crates/system-env/Cargo.toml b/crates/system-env/Cargo.toml index bcbe3113c..bf8bc6b72 100644 --- a/crates/system-env/Cargo.toml +++ b/crates/system-env/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "system_env" -version = "0.6.1" +version = "0.7.0" edition = "2021" license = "MIT" description = "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 index 9ed726300..aae6d07d0 100644 --- a/crates/system-env/src/deps.rs +++ b/crates/system-env/src/deps.rs @@ -13,11 +13,15 @@ pub enum DependencyName { Single(String), /// A single package by name, but with different names (values) - /// depending on operating system or package manager (keys). - SingleMap(HashMap), + /// depending on package manager (keys). + SingleMap(HashMap), /// Multiple packages by name. Multiple(Vec), + + /// Multiple packages by name, but with different names (values) + /// depending on package manager (keys). + MultipleMap(HashMap>), } impl Default for DependencyName { @@ -52,20 +56,20 @@ pub struct DependencyConfig { impl DependencyConfig { /// Get a list of package names for the provided OS and package manager. - pub fn get_package_names( - &self, - os: &SystemOS, - pm: &SystemPackageManager, - ) -> Result, Error> { + pub fn get_package_names(&self, 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("*")) + .get(pm) + .or_else(|| map.get(&SystemPackageManager::All)) .map(|name| vec![name.to_owned()]) .ok_or(Error::MissingName), DependencyName::Multiple(list) => Ok(list.clone()), + DependencyName::MultipleMap(map) => map + .get(pm) + .or_else(|| map.get(&SystemPackageManager::All)) + .cloned() + .ok_or(Error::MissingName), } } } @@ -78,22 +82,26 @@ pub enum SystemDependency { /// A single package by name. Name(String), + /// A single package by name, but with different names (values) + /// depending on package manager (keys). + NameMap(HashMap), + /// Multiple packages by name. Names(Vec), + /// Multiple packages by name, but with different names (values) + /// depending on package manager (keys). + NamesMap(HashMap>), + /// Either a single or multiple package, defined as an /// explicit configuration object. Config(Box), - - /// A single package by name, but with different names (values) - /// depending on operating system or package manager (keys). - Map(HashMap), } impl SystemDependency { /// Create a single dependency by name. - pub fn name(name: &str) -> SystemDependency { - SystemDependency::Name(name.to_owned()) + pub fn name(name: impl AsRef) -> SystemDependency { + SystemDependency::Name(name.as_ref().to_owned()) } /// Create multiple dependencies by name. @@ -106,49 +114,68 @@ impl SystemDependency { } /// Create a single dependency by name for the target architecture. - pub fn for_arch(name: &str, arch: SystemArch) -> SystemDependency { + pub fn for_arch(arch: SystemArch, name: impl AsRef) -> SystemDependency { SystemDependency::Config(Box::new(DependencyConfig { arch: Some(arch), - dep: DependencyName::Single(name.into()), + dep: DependencyName::Single(name.as_ref().into()), ..DependencyConfig::default() })) } /// Create a single dependency by name for the target operating system. - pub fn for_os(name: &str, os: SystemOS) -> SystemDependency { + pub fn for_os(os: SystemOS, name: impl AsRef) -> SystemDependency { SystemDependency::Config(Box::new(DependencyConfig { - dep: DependencyName::Single(name.into()), + dep: DependencyName::Single(name.as_ref().into()), os: Some(os), ..DependencyConfig::default() })) } /// Create a single dependency by name for the target operating system and architecture. - pub fn for_os_arch(name: &str, os: SystemOS, arch: SystemArch) -> SystemDependency { + pub fn for_os_arch(os: SystemOS, arch: SystemArch, name: impl AsRef) -> SystemDependency { SystemDependency::Config(Box::new(DependencyConfig { arch: Some(arch), - dep: DependencyName::Single(name.into()), + dep: DependencyName::Single(name.as_ref().into()), os: Some(os), ..DependencyConfig::default() })) } + /// Create multiple dependencies by name for the target package manager. + pub fn for_pm(pm: SystemPackageManager, names: I) -> SystemDependency + where + I: IntoIterator, + V: AsRef, + { + SystemDependency::Config(Box::new(DependencyConfig { + dep: DependencyName::Multiple( + names.into_iter().map(|n| n.as_ref().to_owned()).collect(), + ), + manager: Some(pm), + ..DependencyConfig::default() + })) + } + /// Convert and expand to a dependency configuration. - pub fn to_config(self) -> DependencyConfig { + pub fn to_config(&self) -> DependencyConfig { match self { Self::Name(name) => DependencyConfig { - dep: DependencyName::Single(name), + dep: DependencyName::Single(name.to_owned()), + ..DependencyConfig::default() + }, + Self::NameMap(map) => DependencyConfig { + dep: DependencyName::SingleMap(map.to_owned()), ..DependencyConfig::default() }, Self::Names(names) => DependencyConfig { - dep: DependencyName::Multiple(names), + dep: DependencyName::Multiple(names.to_owned()), ..DependencyConfig::default() }, - Self::Map(map) => DependencyConfig { - dep: DependencyName::SingleMap(map), + Self::NamesMap(map) => DependencyConfig { + dep: DependencyName::MultipleMap(map.to_owned()), ..DependencyConfig::default() }, - Self::Config(config) => *config, + Self::Config(config) => (**config).to_owned(), } } } diff --git a/crates/system-env/src/env.rs b/crates/system-env/src/env.rs index d8183147d..4adbbd782 100644 --- a/crates/system-env/src/env.rs +++ b/crates/system-env/src/env.rs @@ -111,10 +111,13 @@ impl SystemOS { /// Return the provided file name formatted with the extension (without dot) /// when on Windows. On Unix, returns the name as-is. pub fn get_file_name(&self, name: impl AsRef, windows_ext: impl AsRef) -> String { - if self.is_windows() { - format!("{}.{}", name.as_ref(), windows_ext.as_ref()) + let name = name.as_ref(); + let ext = windows_ext.as_ref(); + + if self.is_windows() && !name.ends_with(ext) { + format!("{}.{ext}", name) } else { - name.as_ref().to_owned() + name.to_owned() } } diff --git a/crates/system-env/src/error.rs b/crates/system-env/src/error.rs index bbf6cbf3f..10431c61e 100644 --- a/crates/system-env/src/error.rs +++ b/crates/system-env/src/error.rs @@ -6,6 +6,9 @@ pub enum Error { #[error("No system package manager was detected.")] MissingPackageManager, + #[error("A system package manager is required for this operation.")] + RequiredPackageManager, + #[error("Unknown or unsupported system package manager `{0}`.")] UnknownPackageManager(String), } diff --git a/crates/system-env/src/pm.rs b/crates/system-env/src/pm.rs index 9ea596c3b..570e410c0 100644 --- a/crates/system-env/src/pm.rs +++ b/crates/system-env/src/pm.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; /// Package manager of the system environment. -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[cfg_attr(feature = "schematic", derive(schematic::Schematic))] #[serde(rename_all = "kebab-case")] pub enum SystemPackageManager { @@ -27,6 +27,10 @@ pub enum SystemPackageManager { #[serde(alias = "chocolatey")] Choco, Scoop, + + // Used for name indexing + #[serde(alias = "*")] + All, } impl SystemPackageManager { @@ -110,6 +114,7 @@ impl SystemPackageManager { Self::Brew => brew(), Self::Choco => choco(), Self::Scoop => scoop(), + Self::All => unreachable!(), } } } diff --git a/crates/system-env/src/pm_vendor.rs b/crates/system-env/src/pm_vendor.rs index 69b5d5c33..f7ef7ef07 100644 --- a/crates/system-env/src/pm_vendor.rs +++ b/crates/system-env/src/pm_vendor.rs @@ -93,7 +93,7 @@ pub(crate) fn brew() -> PackageManagerConfig { ), (CommandType::UpdateIndex, string_vec!["brew", "update"]), ]), - prompt_arg: PromptArgument::Interactive("-i".into()), + prompt_arg: PromptArgument::None, // Interactive("-i".into()), prompt_for: vec![CommandType::InstallPackage], version_arg: VersionArgument::Inline("@".into()), } diff --git a/crates/system-env/src/system.rs b/crates/system-env/src/system.rs index 8c1e526ed..adefa4a4a 100644 --- a/crates/system-env/src/system.rs +++ b/crates/system-env/src/system.rs @@ -6,28 +6,39 @@ use crate::pm_vendor::*; /// Represents the current system, including architecture, operating system, /// and package manager information. +#[derive(Debug)] pub struct System { /// Platform architecture. pub arch: SystemArch, /// Package manager. - pub manager: SystemPackageManager, + pub manager: Option, /// Operating system. pub os: SystemOS, } +impl Default for System { + fn default() -> Self { + Self { + arch: SystemArch::from_env(), + manager: SystemPackageManager::detect().ok(), + os: SystemOS::from_env(), + } + } +} + impl System { /// Create a new instance and detect system information. pub fn new() -> Result { - Ok(System::with_manager(SystemPackageManager::detect()?)) + Ok(Self::with_manager(SystemPackageManager::detect()?)) } /// Create a new instance with the provided package manager. pub fn with_manager(manager: SystemPackageManager) -> Self { - System { + Self { arch: SystemArch::from_env(), - manager, + manager: Some(manager), os: SystemOS::from_env(), } } @@ -35,44 +46,72 @@ impl System { /// Return the command and arguments to "install a package" for the /// current package manager. Will replace `$` in an argument with the /// dependency name, derived from [`DependencyName`]. + /// + /// The [`DependencyConfig`] must be filtered for the current operating + /// system and package manager beforehad. 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); + ) -> Result>, Error> { + let Some(pm) = dep_config.manager.or(self.manager) else { + return Err(Error::RequiredPackageManager); + }; + let pm_config = pm.get_config(); let mut args = vec![]; - for arg in pm_config - .commands - .get(&CommandType::InstallPackage) - .cloned() - .unwrap() - { + let Some(base_args) = pm_config.commands.get(&CommandType::InstallPackage) else { + return Ok(None); + }; + + for arg in base_args { 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); - } + args.extend(self.extract_package_args(dep_config, &pm_config, &pm)?); + } else { + args.push(arg.to_owned()); + } + } + + self.append_interactive( + CommandType::InstallPackage, + &pm_config, + &mut args, + interactive, + ); + + Ok(Some(args)) + } + + /// Return the command and arguments to "install many packages" for the + /// current package manager. Will replace `$` in an argument with the + /// dependency name, derived from [`DependencyName`]. + /// + /// The [`DependencyConfig`]s must be filtered for the current operating + /// system and package manager beforehad. + pub fn get_install_packages_command( + &self, + dep_configs: &[DependencyConfig], + interactive: bool, + ) -> Result>, Error> { + let Some(pm) = self.manager else { + return Err(Error::RequiredPackageManager); + }; + + let pm_config = pm.get_config(); + let mut args = vec![]; + + let Some(base_args) = pm_config.commands.get(&CommandType::InstallPackage) else { + return Ok(None); + }; + + for arg in base_args { + if arg == "$" { + for dep_config in dep_configs { + args.extend(self.extract_package_args(dep_config, &pm_config, &pm)?); } } else { - args.push(arg); + args.push(arg.to_owned()); } } @@ -83,28 +122,35 @@ impl System { interactive, ); - Ok(args) + Ok(Some(args)) } /// Return the command and arguments to "update the registry index" /// for the current package manager. - pub fn get_update_index_command(&self, interactive: bool) -> Option> { - let pm_config = self.manager.get_config(); + pub fn get_update_index_command( + &self, + interactive: bool, + ) -> Result>, Error> { + let Some(pm) = self.manager else { + return Err(Error::RequiredPackageManager); + }; + + let pm_config = pm.get_config(); if let Some(args) = pm_config.commands.get(&CommandType::UpdateIndex) { let mut args = args.to_owned(); self.append_interactive(CommandType::UpdateIndex, &pm_config, &mut args, interactive); - return Some(args); + return Ok(Some(args)); } - None + Ok(None) } /// Resolve and reduce the dependencies to a list that's applicable /// to the current system. - pub fn resolve_dependencies(&self, deps: Vec) -> Vec { + pub fn resolve_dependencies(&self, deps: &[SystemDependency]) -> Vec { let mut configs = vec![]; for dep in deps { @@ -118,6 +164,14 @@ impl System { continue; } + if let Some(pm) = &self.manager { + if config.manager.as_ref().is_some_and(|m| m != pm) { + continue; + } + } else if config.manager.is_some() { + continue; + } + configs.push(config); } @@ -147,4 +201,35 @@ impl System { }; } } + + fn extract_package_args( + &self, + dep_config: &DependencyConfig, + pm_config: &PackageManagerConfig, + pm: &SystemPackageManager, + ) -> Result, Error> { + let mut args = vec![]; + + for dep in dep_config.get_package_names(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); + } + } + + Ok(args) + } } diff --git a/crates/system-env/tests/pm_test.rs b/crates/system-env/tests/pm_test.rs index a1f25989c..c516dc6fa 100644 --- a/crates/system-env/tests/pm_test.rs +++ b/crates/system-env/tests/pm_test.rs @@ -21,19 +21,27 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["apk", "add", "foo"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), vec!["apk", "add", "foo", "-i"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .unwrap(), vec!["apk", "add", "foo", "bar", "baz"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, true).unwrap(), + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), vec!["apk", "add", "foo", "bar", "baz", "-i"] ); } @@ -45,7 +53,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["apk", "add", "foo=1.2.3"] ); } @@ -55,11 +65,11 @@ mod pm { let pm = System::with_manager(SystemPackageManager::Apk); assert_eq!( - pm.get_update_index_command(false).unwrap(), + pm.get_update_index_command(false).unwrap().unwrap(), vec!["apk", "update"] ); assert_eq!( - pm.get_update_index_command(true).unwrap(), + pm.get_update_index_command(true).unwrap().unwrap(), vec!["apk", "update", "-i"] ); } @@ -75,15 +85,21 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["apt", "install", "--install-recommends", "foo", "-y"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), vec!["apt", "install", "--install-recommends", "foo"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .unwrap(), vec![ "apt", "install", @@ -95,7 +111,9 @@ mod pm { ] ); assert_eq!( - pm.get_install_package_command(&many_cfg, true).unwrap(), + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), vec![ "apt", "install", @@ -114,7 +132,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["apt", "install", "--install-recommends", "foo=1.2.3", "-y"] ); } @@ -124,11 +144,11 @@ mod pm { let pm = System::with_manager(SystemPackageManager::Apt); assert_eq!( - pm.get_update_index_command(false).unwrap(), + pm.get_update_index_command(false).unwrap().unwrap(), vec!["apt", "update", "-y"] ); assert_eq!( - pm.get_update_index_command(true).unwrap(), + pm.get_update_index_command(true).unwrap().unwrap(), vec!["apt", "update"] ); } @@ -144,20 +164,28 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["brew", "install", "foo"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), - vec!["brew", "install", "foo", "-i"] + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), + vec!["brew", "install", "foo"], // , "-i"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .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"] + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), + vec!["brew", "install", "foo", "bar", "baz"], // , "-i"] ); } @@ -168,7 +196,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["brew", "install", "foo@1.2.3"] ); } @@ -178,11 +208,11 @@ mod pm { let pm = System::with_manager(SystemPackageManager::Brew); assert_eq!( - pm.get_update_index_command(false).unwrap(), + pm.get_update_index_command(false).unwrap().unwrap(), vec!["brew", "update"] ); assert_eq!( - pm.get_update_index_command(true).unwrap(), + pm.get_update_index_command(true).unwrap().unwrap(), vec!["brew", "update"] ); } @@ -198,19 +228,27 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["choco", "install", "foo", "-y"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), vec!["choco", "install", "foo"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .unwrap(), vec!["choco", "install", "foo", "bar", "baz", "-y"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, true).unwrap(), + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), vec!["choco", "install", "foo", "bar", "baz"] ); } @@ -222,7 +260,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["choco", "install", "foo", "--version", "1.2.3", "-y"] ); } @@ -231,7 +271,7 @@ mod pm { fn update_index() { let pm = System::with_manager(SystemPackageManager::Choco); - assert_eq!(pm.get_update_index_command(false), None); + assert_eq!(pm.get_update_index_command(false).unwrap(), None); } } @@ -245,19 +285,27 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["dnf", "install", "foo", "-y"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), vec!["dnf", "install", "foo"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .unwrap(), vec!["dnf", "install", "foo", "bar", "baz", "-y"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, true).unwrap(), + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), vec!["dnf", "install", "foo", "bar", "baz"] ); } @@ -269,7 +317,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["dnf", "install", "foo-1.2.3", "-y"] ); } @@ -279,11 +329,11 @@ mod pm { let pm = System::with_manager(SystemPackageManager::Dnf); assert_eq!( - pm.get_update_index_command(false).unwrap(), + pm.get_update_index_command(false).unwrap().unwrap(), vec!["dnf", "check-update", "-y"] ); assert_eq!( - pm.get_update_index_command(true).unwrap(), + pm.get_update_index_command(true).unwrap().unwrap(), vec!["dnf", "check-update"] ); } @@ -299,19 +349,27 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["pacman", "-S", "foo", "--noconfirm"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), vec!["pacman", "-S", "foo"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .unwrap(), vec!["pacman", "-S", "foo", "bar", "baz", "--noconfirm"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, true).unwrap(), + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), vec!["pacman", "-S", "foo", "bar", "baz"] ); } @@ -323,7 +381,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["pacman", "-S", "foo>=1.2.3", "--noconfirm"] ); } @@ -333,11 +393,11 @@ mod pm { let pm = System::with_manager(SystemPackageManager::Pacman); assert_eq!( - pm.get_update_index_command(false).unwrap(), + pm.get_update_index_command(false).unwrap().unwrap(), vec!["pacman", "-Syy"] ); assert_eq!( - pm.get_update_index_command(true).unwrap(), + pm.get_update_index_command(true).unwrap().unwrap(), vec!["pacman", "-Syy"] ); } @@ -353,19 +413,27 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["pkg", "install", "foo", "-y"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), vec!["pkg", "install", "foo"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .unwrap(), vec!["pkg", "install", "foo", "bar", "baz", "-y"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, true).unwrap(), + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), vec!["pkg", "install", "foo", "bar", "baz"] ); } @@ -377,7 +445,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["pkg", "install", "foo", "-y"] ); } @@ -387,11 +457,11 @@ mod pm { let pm = System::with_manager(SystemPackageManager::Pkg); assert_eq!( - pm.get_update_index_command(false).unwrap(), + pm.get_update_index_command(false).unwrap().unwrap(), vec!["pkg", "update"] ); assert_eq!( - pm.get_update_index_command(true).unwrap(), + pm.get_update_index_command(true).unwrap().unwrap(), vec!["pkg", "update"] ); } @@ -407,19 +477,27 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["pkgin", "install", "foo", "-y"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), vec!["pkgin", "install", "foo"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .unwrap(), vec!["pkgin", "install", "foo", "bar", "baz", "-y"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, true).unwrap(), + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), vec!["pkgin", "install", "foo", "bar", "baz"] ); } @@ -431,7 +509,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["pkgin", "install", "foo-1.2.3", "-y"] ); } @@ -441,11 +521,11 @@ mod pm { let pm = System::with_manager(SystemPackageManager::Pkgin); assert_eq!( - pm.get_update_index_command(false).unwrap(), + pm.get_update_index_command(false).unwrap().unwrap(), vec!["pkgin", "update", "-y"] ); assert_eq!( - pm.get_update_index_command(true).unwrap(), + pm.get_update_index_command(true).unwrap().unwrap(), vec!["pkgin", "update"] ); } @@ -461,19 +541,27 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["scoop", "install", "foo"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), vec!["scoop", "install", "foo"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .unwrap(), vec!["scoop", "install", "foo", "bar", "baz"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, true).unwrap(), + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), vec!["scoop", "install", "foo", "bar", "baz"] ); } @@ -485,7 +573,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["scoop", "install", "foo@1.2.3"] ); } @@ -495,11 +585,11 @@ mod pm { let pm = System::with_manager(SystemPackageManager::Scoop); assert_eq!( - pm.get_update_index_command(false).unwrap(), + pm.get_update_index_command(false).unwrap().unwrap(), vec!["scoop", "update"] ); assert_eq!( - pm.get_update_index_command(true).unwrap(), + pm.get_update_index_command(true).unwrap().unwrap(), vec!["scoop", "update"] ); } @@ -515,19 +605,27 @@ mod pm { let many_cfg = many_dep(); assert_eq!( - pm.get_install_package_command(&one_cfg, false).unwrap(), + pm.get_install_package_command(&one_cfg, false) + .unwrap() + .unwrap(), vec!["yum", "install", "foo", "-y"] ); assert_eq!( - pm.get_install_package_command(&one_cfg, true).unwrap(), + pm.get_install_package_command(&one_cfg, true) + .unwrap() + .unwrap(), vec!["yum", "install", "foo"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, false).unwrap(), + pm.get_install_package_command(&many_cfg, false) + .unwrap() + .unwrap(), vec!["yum", "install", "foo", "bar", "baz", "-y"] ); assert_eq!( - pm.get_install_package_command(&many_cfg, true).unwrap(), + pm.get_install_package_command(&many_cfg, true) + .unwrap() + .unwrap(), vec!["yum", "install", "foo", "bar", "baz"] ); } @@ -539,7 +637,9 @@ mod pm { cfg.version = Some("1.2.3".into()); assert_eq!( - pm.get_install_package_command(&cfg, false).unwrap(), + pm.get_install_package_command(&cfg, false) + .unwrap() + .unwrap(), vec!["yum", "install", "foo-1.2.3", "-y"] ); } @@ -549,11 +649,11 @@ mod pm { let pm = System::with_manager(SystemPackageManager::Yum); assert_eq!( - pm.get_update_index_command(false).unwrap(), + pm.get_update_index_command(false).unwrap().unwrap(), vec!["yum", "check-update"] ); assert_eq!( - pm.get_update_index_command(true).unwrap(), + pm.get_update_index_command(true).unwrap().unwrap(), vec!["yum", "check-update"] ); } diff --git a/crates/version-spec/Cargo.toml b/crates/version-spec/Cargo.toml index a7a53da5d..e0280a978 100644 --- a/crates/version-spec/Cargo.toml +++ b/crates/version-spec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "version_spec" -version = "0.7.1" +version = "0.7.2" edition = "2021" license = "MIT" description = "A specification for working with partial, full, or aliased versions. Supports semver and calver." diff --git a/crates/version-spec/src/lib.rs b/crates/version-spec/src/lib.rs index ccab28d61..6c8c0fe58 100644 --- a/crates/version-spec/src/lib.rs +++ b/crates/version-spec/src/lib.rs @@ -90,7 +90,7 @@ static SEMVER_REGEX: OnceLock = OnceLock::new(); pub fn get_semver_regex() -> &'static Regex { // https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions SEMVER_REGEX.get_or_init(|| { - Regex::new(r"^(?[0-9]+).(?[0-9]+).(?[0-9]+)(?
-[-0-9a-zA-Z.]+)?(?\+[-0-9a-zA-Z.]+)?$",)
+        Regex::new(r"^(?[0-9]+).(?[0-9]+).(?[0-9]+)(?
-[-0-9a-zA-Z.]+)?(?\+[-0-9a-zA-Z.]+)?$")
         .unwrap()
     })
 }
diff --git a/crates/warpgate-api/Cargo.toml b/crates/warpgate-api/Cargo.toml
index b120e4a33..abdb3ab09 100644
--- a/crates/warpgate-api/Cargo.toml
+++ b/crates/warpgate-api/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "warpgate_api"
-version = "0.10.1"
+version = "0.11.0"
 edition = "2021"
 license = "MIT"
 description = "APIs for working with Warpgate plugins."
@@ -8,7 +8,7 @@ homepage = "https://moonrepo.dev/proto"
 repository = "https://github.com/moonrepo/proto"
 
 [dependencies]
-system_env = { version = "0.6.1", path = "../system-env" }
+system_env = { version = "0.7.0", path = "../system-env" }
 anyhow = { workspace = true }
 rustc-hash = { workspace = true }
 schematic = { workspace = true, optional = true, features = ["schema", "json"] }
diff --git a/crates/warpgate-pdk/Cargo.toml b/crates/warpgate-pdk/Cargo.toml
index a61862690..c7e882c0c 100644
--- a/crates/warpgate-pdk/Cargo.toml
+++ b/crates/warpgate-pdk/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "warpgate_pdk"
-version = "0.8.1"
+version = "0.9.0"
 edition = "2021"
 license = "MIT"
 description = "Reusable WASM macros and functions for plugin developer kits."
@@ -8,6 +8,6 @@ homepage = "https://moonrepo.dev/proto"
 repository = "https://github.com/moonrepo/proto"
 
 [dependencies]
-warpgate_api = { version = "0.10.1", path = "../warpgate-api" }
+warpgate_api = { version = "0.11.0", path = "../warpgate-api" }
 extism-pdk = { workspace = true }
 serde = { workspace = true }
diff --git a/crates/warpgate/Cargo.toml b/crates/warpgate/Cargo.toml
index 122df622e..7f57cad4e 100644
--- a/crates/warpgate/Cargo.toml
+++ b/crates/warpgate/Cargo.toml
@@ -1,14 +1,14 @@
 [package]
 name = "warpgate"
-version = "0.20.3"
+version = "0.21.0"
 edition = "2021"
 license = "MIT"
 description = "Download, resolve, and manage Extism WASM plugins."
 repository = "https://github.com/moonrepo/proto"
 
 [dependencies]
-system_env = { version = "0.6.1", path = "../system-env" }
-warpgate_api = { version = "0.10.1", path = "../warpgate-api" }
+system_env = { version = "0.7.0", path = "../system-env" }
+warpgate_api = { version = "0.11.0", path = "../warpgate-api" }
 async-trait = { workspace = true }
 compact_str = { workspace = true }
 extism = { workspace = true, features = ["http"] }
diff --git a/plugins/Cargo.lock b/plugins/Cargo.lock
index 9d5a7b4e1..1743fe565 100644
--- a/plugins/Cargo.lock
+++ b/plugins/Cargo.lock
@@ -34,7 +34,7 @@ checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
  "cfg-if",
  "once_cell",
- "version_check",
+ "version_check 0.9.5",
  "zerocopy",
 ]
 
@@ -68,6 +68,16 @@ version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
 
+[[package]]
+name = "any_key"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d21bb2cdab8087ed9d69411dd99c608dbede1df847c255b4d609f0399a3cb452"
+dependencies = [
+ "debugit",
+ "mopa",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.95"
@@ -83,6 +93,12 @@ dependencies = [
  "derive_arbitrary",
 ]
 
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
 [[package]]
 name = "assert_cmd"
 version = "2.0.16"
@@ -462,9 +478,9 @@ dependencies = [
 
 [[package]]
 name = "convert_case"
-version = "0.6.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
 dependencies = [
  "unicode-segmentation",
 ]
@@ -659,6 +675,32 @@ version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
 
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "futures-core",
+ "mio",
+ "parking_lot",
+ "rustix",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "crypto-common"
 version = "0.1.6"
@@ -713,6 +755,15 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "debugit"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63c2f7e3034df2b09f750327e23c1adfe33301e6b7388f05bb4fcc0fa46825e3"
+dependencies = [
+ "version_check 0.1.5",
+]
+
 [[package]]
 name = "deranged"
 version = "0.3.11"
@@ -783,6 +834,15 @@ dependencies = [
  "dirs-sys 0.4.1",
 ]
 
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys 0.5.0",
+]
+
 [[package]]
 name = "dirs-sys"
 version = "0.3.7"
@@ -790,7 +850,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
 dependencies = [
  "libc",
- "redox_users",
+ "redox_users 0.4.6",
  "winapi",
 ]
 
@@ -802,10 +862,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
 dependencies = [
  "libc",
  "option-ext",
- "redox_users",
+ "redox_users 0.4.6",
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users 0.5.0",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "dirs-sys-next"
 version = "0.1.2"
@@ -813,7 +885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
 dependencies = [
  "libc",
- "redox_users",
+ "redox_users 0.4.6",
  "winapi",
 ]
 
@@ -1018,9 +1090,9 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.0.34"
+version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
+checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
 dependencies = [
  "crc32fast",
  "miniz_oxide",
@@ -1028,9 +1100,9 @@ dependencies = [
 
 [[package]]
 name = "float-cmp"
-version = "0.9.0"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
 dependencies = [
  "num-traits",
 ]
@@ -1190,9 +1262,9 @@ dependencies = [
 
 [[package]]
 name = "garde"
-version = "0.21.0"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1dbf10452e3dbf51033a5035a05762b2653c43bf84d46e96f15bc93beedd426d"
+checksum = "6a989bd2fd12136080f7825ff410d9239ce84a2a639487fc9d924ee42e2fb84f"
 dependencies = [
  "compact_str",
  "garde_derive",
@@ -1203,9 +1275,9 @@ dependencies = [
 
 [[package]]
 name = "garde_derive"
-version = "0.21.0"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccfdbc9c39fad7991686e229c55cf71565eafe73dcb2cf38ddf1d4aa3ca7e176"
+checksum = "1f7f0545bbbba0a37d4d445890fa5759814e0716f02417b39f6fab292193df68"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1213,6 +1285,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "generational-box"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "557cf2cbacd0504c6bf8c29f52f8071e0de1d9783346713dc6121d7fa1e5d0e0"
+dependencies = [
+ "parking_lot",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.14.7"
@@ -1220,7 +1301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
 dependencies = [
  "typenum",
- "version_check",
+ "version_check 0.9.5",
 ]
 
 [[package]]
@@ -1281,6 +1362,12 @@ dependencies = [
  "walkdir",
 ]
 
+[[package]]
+name = "grid"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be136d9dacc2a13cc70bb6c8f902b414fb2641f8db1314637c6b7933411a8f82"
+
 [[package]]
 name = "h2"
 version = "0.4.6"
@@ -1703,9 +1790,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.7.0"
+version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
+checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
 dependencies = [
  "equivalent",
  "hashbrown 0.15.0",
@@ -1740,6 +1827,35 @@ version = "2.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c"
 
+[[package]]
+name = "iocraft"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a34280c018dcd0a94d2c7a8b2999578d62ffee4decf52b01aeabd77a980932c"
+dependencies = [
+ "any_key",
+ "bitflags",
+ "crossterm",
+ "futures",
+ "generational-box",
+ "iocraft-macros",
+ "taffy",
+ "textwrap",
+ "unicode-width",
+]
+
+[[package]]
+name = "iocraft-macros"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41115c1e8afddf13f7d87c8f5b7e0bdec76567cea630944c7ad4227702d018ec"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "uuid",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.10.1"
@@ -1867,6 +1983,16 @@ dependencies = [
  "redox_syscall",
 ]
 
+[[package]]
+name = "libyml"
+version = "0.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980"
+dependencies = [
+ "anyhow",
+ "version_check 0.9.5",
+]
+
 [[package]]
 name = "linked-hash-map"
 version = "0.5.6"
@@ -2070,10 +2196,17 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
 dependencies = [
  "hermit-abi",
  "libc",
+ "log",
  "wasi",
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "mopa"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a785740271256c230f57462d3b83e52f998433a7062fc18f96d5999474a9f915"
+
 [[package]]
 name = "nix"
 version = "0.27.1"
@@ -2268,9 +2401,9 @@ dependencies = [
 
 [[package]]
 name = "predicates"
-version = "3.1.2"
+version = "3.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97"
+checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
 dependencies = [
  "anstyle",
  "difflib",
@@ -2360,11 +2493,12 @@ dependencies = [
 
 [[package]]
 name = "proto_core"
-version = "0.44.4"
+version = "0.44.6"
 dependencies = [
  "convert_case",
  "dotenvy",
  "indexmap",
+ "iocraft",
  "miette 7.4.0",
  "minisign-verify",
  "once_cell",
@@ -2380,9 +2514,12 @@ dependencies = [
  "sha2",
  "shell-words",
  "starbase_archive",
+ "starbase_console",
  "starbase_styles",
- "starbase_utils 0.9.1",
- "thiserror 2.0.10",
+ "starbase_utils 0.10.0",
+ "system_env",
+ "thiserror 2.0.11",
+ "tokio",
  "tracing",
  "url",
  "uuid",
@@ -2403,7 +2540,7 @@ dependencies = [
 
 [[package]]
 name = "proto_pdk_api"
-version = "0.24.5"
+version = "0.24.6"
 dependencies = [
  "rustc-hash 2.1.0",
  "schematic",
@@ -2411,7 +2548,7 @@ dependencies = [
  "serde",
  "serde_json",
  "system_env",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
  "version_spec",
  "warpgate_api",
 ]
@@ -2424,7 +2561,7 @@ dependencies = [
  "proto_pdk_api",
  "serde",
  "serde_json",
- "starbase_sandbox 0.8.0",
+ "starbase_sandbox 0.8.2",
  "warpgate",
 ]
 
@@ -2433,7 +2570,7 @@ name = "proto_shim"
 version = "0.5.0"
 dependencies = [
  "command-group",
- "dirs 5.0.1",
+ "dirs 6.0.0",
 ]
 
 [[package]]
@@ -2584,6 +2721,17 @@ dependencies = [
  "thiserror 1.0.64",
 ]
 
+[[package]]
+name = "redox_users"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror 2.0.11",
+]
+
 [[package]]
 name = "reflink-copy"
 version = "0.1.21"
@@ -2927,9 +3075,9 @@ dependencies = [
 
 [[package]]
 name = "schematic"
-version = "0.17.8"
+version = "0.17.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc902ccf410e32f7d1fdcad8080729def0cd5e696fb70ca7468bfcd02ff4fe80"
+checksum = "c5780c4ee657b40b2422f965d4f7525be233dab38c3b624bf446888d62e24b19"
 dependencies = [
  "garde",
  "indexmap",
@@ -2940,16 +3088,16 @@ dependencies = [
  "serde_json",
  "serde_path_to_error",
  "starbase_styles",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
  "toml",
  "tracing",
 ]
 
 [[package]]
 name = "schematic_macros"
-version = "0.17.6"
+version = "0.17.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55f638e083cb574cc60a6f15a9eecc45bb4ace4bbbeda0e904298788f04e9453"
+checksum = "1e5ea932a91232414dc5c0f34f0e3ad60c8244968f9e8b06e8b3a4b87ae99051"
 dependencies = [
  "convert_case",
  "darling",
@@ -2960,9 +3108,9 @@ dependencies = [
 
 [[package]]
 name = "schematic_types"
-version = "0.9.6"
+version = "0.9.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f320deb050277c5bcc79213b01d749f8a847a63713e76d5748fdad654f47ed52"
+checksum = "15254cba352d302f26c0e427bf2634e513dff936f8a86c6df4f94843818fa4e3"
 dependencies = [
  "indexmap",
  "semver",
@@ -3009,9 +3157,9 @@ dependencies = [
 
 [[package]]
 name = "semver"
-version = "1.0.24"
+version = "1.0.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
+checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
 dependencies = [
  "serde",
 ]
@@ -3038,9 +3186,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.135"
+version = "1.0.137"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
+checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
 dependencies = [
  "indexmap",
  "itoa",
@@ -3081,16 +3229,18 @@ dependencies = [
 ]
 
 [[package]]
-name = "serde_yaml"
-version = "0.9.34+deprecated"
+name = "serde_yml"
+version = "0.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd"
 dependencies = [
  "indexmap",
  "itoa",
+ "libyml",
+ "memchr",
  "ryu",
  "serde",
- "unsafe-libyaml",
+ "version_check 0.9.5",
 ]
 
 [[package]]
@@ -3167,6 +3317,27 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
+[[package]]
+name = "signal-hook"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
 [[package]]
 name = "signal-hook-registry"
 version = "1.4.2"
@@ -3203,6 +3374,15 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7"
 
+[[package]]
+name = "slotmap"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
+dependencies = [
+ "version_check 0.9.5",
+]
+
 [[package]]
 name = "smallvec"
 version = "1.13.2"
@@ -3212,6 +3392,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "smawk"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+
 [[package]]
 name = "socket2"
 version = "0.5.7"
@@ -3259,9 +3445,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
 [[package]]
 name = "starbase_archive"
-version = "0.9.0"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d80c1d1b4368f34f8d7ee9db29e44141a7a6ae349ce2ddf30b86914f499dc8d3"
+checksum = "550327b4bb767734859d2bca7ed08ba362ac289fbf3fbf52ae54c41ce68f25a2"
 dependencies = [
  "binstall-tar",
  "bzip2",
@@ -3269,14 +3455,29 @@ dependencies = [
  "miette 7.4.0",
  "rustc-hash 2.1.0",
  "starbase_styles",
- "starbase_utils 0.9.1",
- "thiserror 2.0.10",
+ "starbase_utils 0.10.0",
+ "thiserror 2.0.11",
  "tracing",
  "xz2",
  "zip",
  "zstd",
 ]
 
+[[package]]
+name = "starbase_console"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfb2fd7146c0e55b53ab44aa4a178d4f27f2fd5ce33718054207cf8aaeacc020"
+dependencies = [
+ "crossterm",
+ "iocraft",
+ "miette 7.4.0",
+ "parking_lot",
+ "starbase_styles",
+ "tokio",
+ "tracing",
+]
+
 [[package]]
 name = "starbase_sandbox"
 version = "0.7.4"
@@ -3294,9 +3495,9 @@ dependencies = [
 
 [[package]]
 name = "starbase_sandbox"
-version = "0.8.0"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "739eca5deee5d5d859c72e6758788d59dfc494bb7f41918a0ba90da749319276"
+checksum = "bf641da9f534f7c67e318a75cb30bd37ef9de303fb77483c431c2e27a912ee56"
 dependencies = [
  "assert_cmd",
  "assert_fs",
@@ -3304,16 +3505,16 @@ dependencies = [
  "insta",
  "predicates",
  "pretty_assertions",
- "starbase_utils 0.9.1",
+ "starbase_utils 0.10.0",
 ]
 
 [[package]]
 name = "starbase_styles"
-version = "0.4.10"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a4df972f8b4010b3ca083555953f6b320de4962aa2b63823c273de9830a3e9f"
+checksum = "65b486cfc419b43fee8b42a87f8d654c1c1d439df34cdf1bfa152cc6bdf8456d"
 dependencies = [
- "dirs 5.0.1",
+ "dirs 6.0.0",
  "owo-colors",
  "supports-color",
 ]
@@ -3332,12 +3533,12 @@ dependencies = [
 
 [[package]]
 name = "starbase_utils"
-version = "0.9.1"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6e34eb4c6d9fc4f9d6159738a8844363d0dc23393718a53b4ea84e570ef9f58"
+checksum = "8ccad09b8d83d873b1873aa4aa1ceeb9b26e96b367000805cb6b2085319eb784"
 dependencies = [
  "async-trait",
- "dirs 5.0.1",
+ "dirs 6.0.0",
  "fs4",
  "json-strip-comments",
  "miette 7.4.0",
@@ -3345,10 +3546,9 @@ dependencies = [
  "reqwest",
  "serde",
  "serde_json",
- "serde_yaml",
+ "serde_yml",
  "starbase_styles",
- "thiserror 2.0.10",
- "tokio",
+ "thiserror 2.0.11",
  "toml",
  "tracing",
  "url",
@@ -3458,7 +3658,20 @@ dependencies = [
  "serde",
  "serde_json",
  "shell-words",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
+]
+
+[[package]]
+name = "taffy"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cb893bff0f80ae17d3a57e030622a967b8dbc90e38284d9b4b1442e23873c94"
+dependencies = [
+ "arrayvec",
+ "grid",
+ "num-traits",
+ "serde",
+ "slotmap",
 ]
 
 [[package]]
@@ -3495,6 +3708,17 @@ version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
 
+[[package]]
+name = "textwrap"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.64"
@@ -3506,11 +3730,11 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "2.0.10"
+version = "2.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
 dependencies = [
- "thiserror-impl 2.0.10",
+ "thiserror-impl 2.0.11",
 ]
 
 [[package]]
@@ -3526,9 +3750,9 @@ dependencies = [
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.10"
+version = "2.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3807,6 +4031,12 @@ version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
 
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
 [[package]]
 name = "unicode-segmentation"
 version = "1.12.0"
@@ -3825,12 +4055,6 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
 
-[[package]]
-name = "unsafe-libyaml"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
-
 [[package]]
 name = "untrusted"
 version = "0.9.0"
@@ -3880,9 +4104,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
 
 [[package]]
 name = "uuid"
-version = "1.11.0"
+version = "1.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
+checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
 dependencies = [
  "getrandom",
 ]
@@ -3893,6 +4117,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
+[[package]]
+name = "version_check"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
+
 [[package]]
 name = "version_check"
 version = "0.9.5"
@@ -3909,7 +4139,7 @@ dependencies = [
  "schematic",
  "semver",
  "serde",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
 ]
 
 [[package]]
@@ -3942,7 +4172,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate"
-version = "0.20.2"
+version = "0.20.3"
 dependencies = [
  "async-trait",
  "compact_str",
@@ -3962,9 +4192,9 @@ dependencies = [
  "sha2",
  "starbase_archive",
  "starbase_styles",
- "starbase_utils 0.9.1",
+ "starbase_utils 0.10.0",
  "system_env",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
  "tokio",
  "tracing",
  "ureq",
@@ -3981,7 +4211,7 @@ dependencies = [
  "serde",
  "serde_json",
  "system_env",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
 ]
 
 [[package]]
@@ -5072,7 +5302,7 @@ dependencies = [
  "flate2",
  "indexmap",
  "memchr",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
  "zopfli",
 ]