diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index c1d21fd672..e10603291d 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -28,6 +28,8 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: autofix diff --git a/.github/workflows/hyperfine.yml b/.github/workflows/hyperfine.yml index c59a0d2403..55f0142f71 100644 --- a/.github/workflows/hyperfine.yml +++ b/.github/workflows/hyperfine.yml @@ -27,6 +27,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 - run: curl https://mise.run | MISE_INSTALL_PATH="$HOME/bin/mise-release" sh - run: echo "$HOME/bin" >> "$GITHUB_PATH" diff --git a/.github/workflows/ppa-publish.yml b/.github/workflows/ppa-publish.yml index 75268626c1..52bff84a3c 100644 --- a/.github/workflows/ppa-publish.yml +++ b/.github/workflows/ppa-publish.yml @@ -35,6 +35,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + submodules: true - name: Set up environment variables run: | diff --git a/.github/workflows/registry.yml b/.github/workflows/registry.yml index 60e90aefdb..140be1e0e5 100644 --- a/.github/workflows/registry.yml +++ b/.github/workflows/registry.yml @@ -30,6 +30,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build diff --git a/.github/workflows/release-fig.yml b/.github/workflows/release-fig.yml index 21d576135f..69d3cce461 100644 --- a/.github/workflows/release-fig.yml +++ b/.github/workflows/release-fig.yml @@ -14,6 +14,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.MISE_GH_TOKEN }} + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 05e42a507a..87c5d55d02 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -32,6 +32,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.MISE_GH_TOKEN }} + submodules: true - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6 with: gpg_private_key: ${{ secrets.MISE_GPG_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b44ab406c6..837107f477 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,6 +45,8 @@ jobs: target: armv7-unknown-linux-musleabi steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - name: Install cross uses: taiki-e/install-action@0aa4f22591557b744fe31e55dbfcdfea74a073f7 # v2 with: @@ -96,6 +98,8 @@ jobs: p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12 }} p12-password: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS }} - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - name: cache crates id: cache-crates uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 @@ -138,6 +142,8 @@ jobs: target: x86_64-pc-windows-msvc steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - run: rustup target add ${{matrix.target}} - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: diff --git a/.github/workflows/test-vfox.yml b/.github/workflows/test-vfox.yml index 8a6d99f016..587976de9b 100644 --- a/.github/workflows/test-vfox.yml +++ b/.github/workflows/test-vfox.yml @@ -24,6 +24,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 - run: | cargo build --all-features diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6dd7178b92..f55676a4bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,8 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build @@ -54,6 +56,8 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build @@ -82,6 +86,8 @@ jobs: MISE_CACHE_DIR: ~/.cache/mise steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build @@ -118,6 +124,7 @@ jobs: with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.head_ref }} + submodules: true - uses: taiki-e/install-action@0aa4f22591557b744fe31e55dbfcdfea74a073f7 # v2 with: tool: cargo-deny,cargo-msrv,cargo-machete @@ -156,6 +163,7 @@ jobs: with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.head_ref }} + submodules: true - run: rustup default nightly - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: @@ -186,6 +194,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + submodules: true - name: Install build and test dependencies run: | sudo apt-get update @@ -235,6 +244,8 @@ jobs: MISE_CACHE_DIR: ~/.cache/mise steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: unit @@ -254,6 +265,8 @@ jobs: MISE_CACHE_DIR: ~/.cache/mise steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: mise-windows-latest diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..4f3fbc88f7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,24 @@ +[submodule "crates/vfox/embedded-plugins/vfox-aapt2"] + path = crates/vfox/embedded-plugins/vfox-aapt2 + url = https://github.com/mise-plugins/vfox-aapt2.git +[submodule "crates/vfox/embedded-plugins/vfox-ag"] + path = crates/vfox/embedded-plugins/vfox-ag + url = https://github.com/mise-plugins/vfox-ag.git +[submodule "crates/vfox/embedded-plugins/vfox-android-sdk"] + path = crates/vfox/embedded-plugins/vfox-android-sdk + url = https://github.com/mise-plugins/vfox-android-sdk.git +[submodule "crates/vfox/embedded-plugins/vfox-ant"] + path = crates/vfox/embedded-plugins/vfox-ant + url = https://github.com/mise-plugins/vfox-ant.git +[submodule "crates/vfox/embedded-plugins/vfox-bfs"] + path = crates/vfox/embedded-plugins/vfox-bfs + url = https://github.com/mise-plugins/vfox-bfs.git +[submodule "crates/vfox/embedded-plugins/vfox-bpkg"] + path = crates/vfox/embedded-plugins/vfox-bpkg + url = https://github.com/mise-plugins/vfox-bpkg.git +[submodule "crates/vfox/embedded-plugins/vfox-chicken"] + path = crates/vfox/embedded-plugins/vfox-chicken + url = https://github.com/mise-plugins/vfox-chicken.git +[submodule "crates/vfox/embedded-plugins/vfox-vlang"] + path = crates/vfox/embedded-plugins/vfox-vlang + url = https://github.com/mise-plugins/vfox-vlang.git diff --git a/.markdownlintignore b/.markdownlintignore index dfe34bff89..8bc02f5a91 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -1,6 +1,7 @@ /registry/ /target/ CHANGELOG.md +crates/vfox/embedded-plugins/ docs/node_modules/ docs/cli/watch.md node_modules/ diff --git a/crates/vfox/.prettierignore b/crates/vfox/.prettierignore index 29fde18168..44839fa761 100644 --- a/crates/vfox/.prettierignore +++ b/crates/vfox/.prettierignore @@ -1 +1,2 @@ plugins/nodejs +embedded-plugins diff --git a/crates/vfox/Cargo.toml b/crates/vfox/Cargo.toml index ce16c1cb2b..54ba06b39a 100644 --- a/crates/vfox/Cargo.toml +++ b/crates/vfox/Cargo.toml @@ -7,7 +7,17 @@ description = "Interface to vfox plugins" documentation = "https://docs.rs/vfox" homepage = "https://github.com/jdx/mise" repository = "https://github.com/jdx/mise" -include = ["src", "lua", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE"] +include = [ + "src", + "lua", + "embedded-plugins", + "build.rs", + "Cargo.toml", + "Cargo.lock", + "README.md", + "LICENSE", +] +build = "build.rs" [lib] name = "vfox" diff --git a/crates/vfox/build.rs b/crates/vfox/build.rs new file mode 100644 index 0000000000..6199bb23b4 --- /dev/null +++ b/crates/vfox/build.rs @@ -0,0 +1,221 @@ +use std::collections::BTreeMap; +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + codegen_embedded_plugins(); +} + +/// Convert a path to a string with forward slashes (required for include_str! on Windows) +fn path_to_forward_slashes(path: &Path) -> String { + path.to_string_lossy().replace('\\', "/") +} + +fn codegen_embedded_plugins() { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("embedded_plugins.rs"); + + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let embedded_dir = Path::new(&manifest_dir).join("embedded-plugins"); + + // Tell Cargo to re-run if any embedded plugin files change + println!("cargo:rerun-if-changed=embedded-plugins"); + + if !embedded_dir.exists() { + // Generate empty implementation if no embedded plugins + let code = r#" +#[derive(Debug)] +pub struct EmbeddedPlugin { + pub metadata: &'static str, + pub hooks: &'static [(&'static str, &'static str)], + pub lib: &'static [(&'static str, &'static str)], +} + +pub fn get_embedded_plugin(_name: &str) -> Option<&'static EmbeddedPlugin> { + None +} + +pub fn list_embedded_plugins() -> &'static [&'static str] { + &[] +} +"#; + fs::write(&dest_path, code).unwrap(); + return; + } + + let mut plugins: BTreeMap = BTreeMap::new(); + + // Scan for plugin directories + for entry in fs::read_dir(&embedded_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let dir_name = path.file_name().unwrap().to_string_lossy().to_string(); + if !dir_name.starts_with("vfox-") { + continue; + } + + // Tell Cargo to re-run if this plugin directory or any Lua files change + println!("cargo:rerun-if-changed={}", path.display()); + + // Also track subdirectories and individual Lua files + let hooks_dir = path.join("hooks"); + if hooks_dir.exists() { + println!("cargo:rerun-if-changed={}", hooks_dir.display()); + for entry in fs::read_dir(&hooks_dir).unwrap().flatten() { + if entry.path().extension().is_some_and(|ext| ext == "lua") { + println!("cargo:rerun-if-changed={}", entry.path().display()); + } + } + } + let lib_dir = path.join("lib"); + if lib_dir.exists() { + println!("cargo:rerun-if-changed={}", lib_dir.display()); + for entry in fs::read_dir(&lib_dir).unwrap().flatten() { + if entry.path().extension().is_some_and(|ext| ext == "lua") { + println!("cargo:rerun-if-changed={}", entry.path().display()); + } + } + } + let metadata_file = path.join("metadata.lua"); + if metadata_file.exists() { + println!("cargo:rerun-if-changed={}", metadata_file.display()); + } + + let plugin = collect_plugin_files(&path); + plugins.insert(dir_name, plugin); + } + + // Generate Rust code + let mut code = String::new(); + + // Struct definition + code.push_str( + r#" +#[derive(Debug)] +pub struct EmbeddedPlugin { + pub metadata: &'static str, + pub hooks: &'static [(&'static str, &'static str)], + pub lib: &'static [(&'static str, &'static str)], +} + +"#, + ); + + // Generate static instances for each plugin + for (name, files) in &plugins { + let var_name = name.replace('-', "_").to_uppercase(); + code.push_str(&format!( + "static {var_name}: EmbeddedPlugin = EmbeddedPlugin {{\n" + )); + + // Metadata - use absolute path with forward slashes for cross-platform include_str! + let metadata_path = embedded_dir.join(name).join("metadata.lua"); + code.push_str(&format!( + " metadata: include_str!(\"{}\"),\n", + path_to_forward_slashes(&metadata_path) + )); + + // Hooks + code.push_str(" hooks: &[\n"); + for hook in &files.hooks { + let hook_path = embedded_dir + .join(name) + .join("hooks") + .join(format!("{}.lua", hook)); + code.push_str(&format!( + " (\"{}\", include_str!(\"{}\")),\n", + hook, + path_to_forward_slashes(&hook_path) + )); + } + code.push_str(" ],\n"); + + // Lib files + code.push_str(" lib: &[\n"); + for lib in &files.lib { + let lib_path = embedded_dir + .join(name) + .join("lib") + .join(format!("{}.lua", lib)); + code.push_str(&format!( + " (\"{}\", include_str!(\"{}\")),\n", + lib, + path_to_forward_slashes(&lib_path) + )); + } + code.push_str(" ],\n"); + + code.push_str("};\n\n"); + } + + // Generate lookup function + code.push_str("pub fn get_embedded_plugin(name: &str) -> Option<&'static EmbeddedPlugin> {\n"); + code.push_str(" match name {\n"); + for name in plugins.keys() { + let var_name = name.replace('-', "_").to_uppercase(); + let short_name = name.strip_prefix("vfox-").unwrap_or(name); + code.push_str(&format!( + " \"{}\" | \"{}\" => Some(&{}),\n", + name, short_name, var_name + )); + } + code.push_str(" _ => None,\n"); + code.push_str(" }\n"); + code.push_str("}\n\n"); + + // Generate list function + code.push_str("pub fn list_embedded_plugins() -> &'static [&'static str] {\n"); + code.push_str(" &[\n"); + for name in plugins.keys() { + code.push_str(&format!(" \"{}\",\n", name)); + } + code.push_str(" ]\n"); + code.push_str("}\n"); + + fs::write(&dest_path, code).unwrap(); +} + +struct PluginFiles { + hooks: Vec, + lib: Vec, +} + +fn collect_plugin_files(plugin_dir: &Path) -> PluginFiles { + let mut hooks = Vec::new(); + let mut lib = Vec::new(); + + // Collect hooks + let hooks_dir = plugin_dir.join("hooks"); + if hooks_dir.exists() { + for entry in fs::read_dir(&hooks_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "lua") { + let name = path.file_stem().unwrap().to_string_lossy().to_string(); + hooks.push(name); + } + } + } + hooks.sort(); + + // Collect lib files + let lib_dir = plugin_dir.join("lib"); + if lib_dir.exists() { + for entry in fs::read_dir(&lib_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "lua") { + let name = path.file_stem().unwrap().to_string_lossy().to_string(); + lib.push(name); + } + } + } + lib.sort(); + + PluginFiles { hooks, lib } +} diff --git a/crates/vfox/embedded-plugins/vfox-aapt2 b/crates/vfox/embedded-plugins/vfox-aapt2 new file mode 160000 index 0000000000..144a5eafe7 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-aapt2 @@ -0,0 +1 @@ +Subproject commit 144a5eafe7a76e954e91e8a9ca7aae99b895deda diff --git a/crates/vfox/embedded-plugins/vfox-ag b/crates/vfox/embedded-plugins/vfox-ag new file mode 160000 index 0000000000..1ca4f81221 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-ag @@ -0,0 +1 @@ +Subproject commit 1ca4f81221b51a0d68de1bb958dede5762d6c08f diff --git a/crates/vfox/embedded-plugins/vfox-android-sdk b/crates/vfox/embedded-plugins/vfox-android-sdk new file mode 160000 index 0000000000..5f8b78d944 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-android-sdk @@ -0,0 +1 @@ +Subproject commit 5f8b78d944f63f0c78775916aad0808a524148ae diff --git a/crates/vfox/embedded-plugins/vfox-ant b/crates/vfox/embedded-plugins/vfox-ant new file mode 160000 index 0000000000..0254ebce47 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-ant @@ -0,0 +1 @@ +Subproject commit 0254ebce476d03b703604bd876d69f5026447c1a diff --git a/crates/vfox/embedded-plugins/vfox-bfs b/crates/vfox/embedded-plugins/vfox-bfs new file mode 160000 index 0000000000..165106df1d --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-bfs @@ -0,0 +1 @@ +Subproject commit 165106df1da197070e2d56edf0661f1cb2e2699d diff --git a/crates/vfox/embedded-plugins/vfox-bpkg b/crates/vfox/embedded-plugins/vfox-bpkg new file mode 160000 index 0000000000..365ac71fbd --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-bpkg @@ -0,0 +1 @@ +Subproject commit 365ac71fbdfdd9372dcd42796a9bdb1b67171370 diff --git a/crates/vfox/embedded-plugins/vfox-chicken b/crates/vfox/embedded-plugins/vfox-chicken new file mode 160000 index 0000000000..85ce8f79ed --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-chicken @@ -0,0 +1 @@ +Subproject commit 85ce8f79ed99057a3415627c94af28e49781feec diff --git a/crates/vfox/embedded-plugins/vfox-vlang b/crates/vfox/embedded-plugins/vfox-vlang new file mode 160000 index 0000000000..61dee71718 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-vlang @@ -0,0 +1 @@ +Subproject commit 61dee71718b40c42314220cf03a34977c341d18c diff --git a/crates/vfox/src/embedded_plugins.rs b/crates/vfox/src/embedded_plugins.rs new file mode 100644 index 0000000000..5a7b180f41 --- /dev/null +++ b/crates/vfox/src/embedded_plugins.rs @@ -0,0 +1,4 @@ +// This module provides access to embedded vfox plugin Lua code. +// The actual code is generated at build time by build.rs + +include!(concat!(env!("OUT_DIR"), "/embedded_plugins.rs")); diff --git a/crates/vfox/src/lib.rs b/crates/vfox/src/lib.rs index ea2fcf4886..19f659c117 100644 --- a/crates/vfox/src/lib.rs +++ b/crates/vfox/src/lib.rs @@ -13,6 +13,7 @@ pub use vfox::Vfox; mod config; mod context; +pub mod embedded_plugins; mod error; mod hooks; mod http; diff --git a/crates/vfox/src/lua_mod/hooks.rs b/crates/vfox/src/lua_mod/hooks.rs index aef1f5ae37..573067c98f 100644 --- a/crates/vfox/src/lua_mod/hooks.rs +++ b/crates/vfox/src/lua_mod/hooks.rs @@ -1,3 +1,4 @@ +use crate::embedded_plugins::EmbeddedPlugin; use crate::error::Result; use mlua::Lua; use std::collections::BTreeSet; @@ -5,11 +6,11 @@ use std::path::Path; pub struct HookFunc { _name: &'static str, - filename: &'static str, + pub filename: &'static str, } #[rustfmt::skip] -const HOOK_FUNCS: [HookFunc; 12] = [ +pub const HOOK_FUNCS: [HookFunc; 12] = [ HookFunc { _name: "Available", filename: "available" }, HookFunc { _name: "PreInstall", filename: "pre_install" }, HookFunc { _name: "EnvKeys", filename: "env_keys" }, @@ -22,7 +23,7 @@ const HOOK_FUNCS: [HookFunc; 12] = [ HookFunc { _name: "BackendListVersions", filename: "backend_list_versions" }, HookFunc { _name: "BackendInstall", filename: "backend_install" }, HookFunc { _name: "BackendExecEnv", filename: "backend_exec_env" }, - + // mise HookFunc { _name: "MiseEnv", filename: "mise_env" }, HookFunc { _name: "MisePath", filename: "mise_path" }, @@ -39,3 +40,32 @@ pub fn mod_hooks(lua: &Lua, root: &Path) -> Result> { } Ok(hooks) } + +pub fn hooks_embedded(lua: &Lua, embedded: &EmbeddedPlugin) -> Result> { + let mut hooks = BTreeSet::new(); + + // Get package.loaded table to preload hooks + let package: mlua::Table = lua.globals().get("package")?; + let loaded: mlua::Table = package.get("loaded")?; + + for (hook_name, hook_code) in embedded.hooks { + // Execute the hook code to define the function + lua.load(*hook_code).exec()?; + + // Also preload into package.loaded so require("hooks/") works + // The hook code typically defines a function on the PLUGIN table + // We need to register a module that can be required + let module_name = format!("hooks/{}", hook_name); + // Create a simple module that returns true (the hook code has already been executed) + loaded.set(module_name, true)?; + + // Find the matching hook filename from HOOK_FUNCS + for hook in &HOOK_FUNCS { + if hook.filename == *hook_name { + hooks.insert(hook.filename); + break; + } + } + } + Ok(hooks) +} diff --git a/crates/vfox/src/lua_mod/mod.rs b/crates/vfox/src/lua_mod/mod.rs index 9e40178a18..e0f75ac75d 100644 --- a/crates/vfox/src/lua_mod/mod.rs +++ b/crates/vfox/src/lua_mod/mod.rs @@ -12,6 +12,7 @@ pub use archiver::mod_archiver as archiver; pub use cmd::mod_cmd as cmd; pub use env::mod_env as env; pub use file::mod_file as file; +pub use hooks::hooks_embedded; pub use hooks::mod_hooks as hooks; pub use html::mod_html as html; pub use http::mod_http as http; diff --git a/crates/vfox/src/plugin.rs b/crates/vfox/src/plugin.rs index 80e5378323..81d46630e1 100644 --- a/crates/vfox/src/plugin.rs +++ b/crates/vfox/src/plugin.rs @@ -2,21 +2,29 @@ use std::cmp::Ordering; use std::fmt::Display; use std::path::{Path, PathBuf}; -use mlua::{AsChunk, FromLuaMulti, IntoLua, Lua, Table}; +use mlua::{AsChunk, FromLuaMulti, IntoLua, Lua, Table, Value}; use once_cell::sync::OnceCell; use crate::config::Config; use crate::context::Context; +use crate::embedded_plugins::{self, EmbeddedPlugin}; use crate::error::Result; use crate::metadata::Metadata; use crate::runtime::Runtime; use crate::sdk_info::SdkInfo; use crate::{VfoxError, config, error, lua_mod}; +#[derive(Debug)] +pub enum PluginSource { + Filesystem(PathBuf), + Embedded(&'static EmbeddedPlugin), +} + #[derive(Debug)] pub struct Plugin { pub name: String, pub dir: PathBuf, + source: PluginSource, lua: Lua, metadata: OnceCell, } @@ -31,16 +39,56 @@ impl Plugin { Ok(Self { name: dir.file_name().unwrap().to_string_lossy().to_string(), dir: dir.to_path_buf(), + source: PluginSource::Filesystem(dir.to_path_buf()), + lua, + metadata: OnceCell::new(), + }) + } + + pub fn from_embedded(name: &str, embedded: &'static EmbeddedPlugin) -> Result { + let lua = Lua::new(); + // Use a dummy path for embedded plugins + let dummy_dir = PathBuf::from(format!("embedded:{}", name)); + lua.set_named_registry_value("plugin_dir", dummy_dir.clone())?; + lua.set_named_registry_value("embedded_plugin", true)?; + Ok(Self { + name: name.to_string(), + dir: dummy_dir, + source: PluginSource::Embedded(embedded), lua, metadata: OnceCell::new(), }) } pub fn from_name(name: &str) -> Result { + // Check filesystem first - allows user to override embedded plugins let dir = Config::get().plugin_dir.join(name); + if dir.exists() { + return Self::from_dir(&dir); + } + // Fall back to embedded plugin if available + if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) { + return Self::from_embedded(name, embedded); + } Self::from_dir(&dir) } + pub fn from_name_or_dir(name: &str, dir: &Path) -> Result { + // Check filesystem first - allows user to override embedded plugins + if dir.exists() { + return Self::from_dir(dir); + } + // Fall back to embedded plugin if available + if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) { + return Self::from_embedded(name, embedded); + } + Self::from_dir(dir) + } + + pub fn is_embedded(&self) -> bool { + matches!(self.source, PluginSource::Embedded(_)) + } + pub fn list() -> Result> { let config = Config::get(); if !config.plugin_dir.exists() { @@ -107,15 +155,21 @@ impl Plugin { fn load(&self) -> Result<&Metadata> { self.metadata.get_or_try_init(|| { debug!("[vfox] Getting metadata for {self}"); - set_paths( - &self.lua, - &[ - self.dir.join("?.lua"), //xx - self.dir.join("hooks/?.lua"), - self.dir.join("lib/?.lua"), - ], - )?; + // For filesystem plugins, set Lua package paths + if let PluginSource::Filesystem(dir) = &self.source { + set_paths( + &self.lua, + &[ + dir.join("?.lua"), + dir.join("hooks/?.lua"), + dir.join("lib/?.lua"), + ], + )?; + } + + // Load standard Lua modules (http, json, etc.) FIRST + // These must be available before loading embedded lib files lua_mod::archiver(&self.lua)?; lua_mod::cmd(&self.lua)?; lua_mod::file(&self.lua)?; @@ -125,6 +179,12 @@ impl Plugin { lua_mod::strings(&self.lua)?; lua_mod::env(&self.lua)?; + // For embedded plugins, load lib modules AFTER standard modules + // (lib files may require http, json, etc.) + if let PluginSource::Embedded(embedded) = &self.source { + self.load_embedded_libs(embedded)?; + } + let metadata = self.load_metadata()?; self.set_global("PLUGIN", metadata.clone())?; self.set_global("RUNTIME", Runtime::get(self.dir.clone()))?; @@ -133,12 +193,34 @@ impl Plugin { let mut metadata: Metadata = metadata.try_into()?; - metadata.hooks = lua_mod::hooks(&self.lua, &self.dir)?; + metadata.hooks = match &self.source { + PluginSource::Filesystem(dir) => lua_mod::hooks(&self.lua, dir)?, + PluginSource::Embedded(embedded) => lua_mod::hooks_embedded(&self.lua, embedded)?, + }; Ok(metadata) }) } + fn load_embedded_libs(&self, embedded: &EmbeddedPlugin) -> Result<()> { + let package: Table = self.lua.globals().get("package")?; + let preload: Table = package.get("preload")?; + + // Register lib modules in package.preload so require() works regardless of load order + // This allows lib files to require each other without alphabetical ordering issues + for (name, code) in embedded.lib { + let lua = self.lua.clone(); + let code = *code; + let loader = lua.create_function(move |lua, _: ()| { + let module: Value = lua.load(code).eval()?; + Ok(module) + })?; + preload.set(*name, loader)?; + } + + Ok(()) + } + fn set_global(&self, name: &str, value: V) -> Result<()> where V: IntoLua, @@ -148,16 +230,26 @@ impl Plugin { } fn load_metadata(&self) -> Result { - let metadata = self - .lua - .load( - r#" - require "metadata" - return PLUGIN - "#, - ) - .eval()?; - Ok(metadata) + match &self.source { + PluginSource::Filesystem(_) => { + let metadata = self + .lua + .load( + r#" + require "metadata" + return PLUGIN + "#, + ) + .eval()?; + Ok(metadata) + } + PluginSource::Embedded(embedded) => { + // Load metadata from embedded string + self.lua.load(embedded.metadata).exec()?; + let metadata = self.lua.globals().get("PLUGIN")?; + Ok(metadata) + } + } } } diff --git a/crates/vfox/src/vfox.rs b/crates/vfox/src/vfox.rs index 283cbd0e2e..79e0623b49 100644 --- a/crates/vfox/src/vfox.rs +++ b/crates/vfox/src/vfox.rs @@ -99,16 +99,24 @@ impl Vfox { } pub fn get_sdk(&self, name: &str) -> Result { - Plugin::from_dir(&self.plugin_dir.join(name)) + Plugin::from_name_or_dir(name, &self.plugin_dir.join(name)) } pub fn install_plugin(&self, sdk: &str) -> Result { + // Check filesystem first - allows user to override embedded plugins let plugin_dir = self.plugin_dir.join(sdk); - if !plugin_dir.exists() { - let url = registry::sdk_url(sdk).ok_or_else(|| format!("Unknown SDK: {sdk}"))?; - return self.install_plugin_from_url(url); + if plugin_dir.exists() { + return Plugin::from_dir(&plugin_dir); } - Plugin::from_dir(&plugin_dir) + + // Fall back to embedded plugin if available + if let Some(embedded) = crate::embedded_plugins::get_embedded_plugin(sdk) { + return Plugin::from_embedded(sdk, embedded); + } + + // Otherwise install from registry + let url = registry::sdk_url(sdk).ok_or_else(|| format!("Unknown SDK: {sdk}"))?; + self.install_plugin_from_url(url) } pub fn install_plugin_from_url(&self, url: &Url) -> Result { diff --git a/e2e/backend/test_vfox_embedded_override_slow b/e2e/backend/test_vfox_embedded_override_slow new file mode 100644 index 0000000000..7eb73b0adf --- /dev/null +++ b/e2e/backend/test_vfox_embedded_override_slow @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# Test that embedded vfox plugins can be overridden by installing a filesystem plugin + +# Verify embedded plugin works without installation - use the short "bfs" name +# which goes through the versions host cache (doesn't require network to the plugin) +assert_contains "mise ls-remote bfs" "3.4" + +# Embedded plugins don't show git info in plugins list +assert_not_contains "mise plugins --urls" "vfox-bfs" + +# Install the same vfox plugin as a filesystem override +mise plugin install vfox-bfs https://github.com/mise-plugins/vfox-bfs + +# Verify the filesystem plugin is now shown with git URL (overrides embedded) +assert_contains "mise plugins --urls" "vfox-bfs" +assert_contains "mise plugins --urls" "github.com/mise-plugins/vfox-bfs" + +# Verify it still works (now using filesystem version) +assert_contains "mise ls-remote bfs" "3.4" + +# Uninstall the override +mise plugin uninstall vfox-bfs + +# Verify it falls back to embedded (no longer in plugins list with URL) +assert_not_contains "mise plugins --urls" "vfox-bfs" + +# Verify embedded still works after uninstall +assert_contains "mise ls-remote bfs" "3.4" diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index 7535e07962..e3b820cc86 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -15,6 +15,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, MutexGuard, mpsc}; use url::Url; use vfox::Vfox; +use vfox::embedded_plugins; use xx::regex; #[derive(Debug)] @@ -108,6 +109,10 @@ impl VfoxPlugin { )?; Ok(()) } + + pub fn is_embedded(&self) -> bool { + embedded_plugins::get_embedded_plugin(&self.name).is_some() + } } #[async_trait] @@ -130,21 +135,24 @@ impl Plugin for VfoxPlugin { } fn current_abbrev_ref(&self) -> eyre::Result> { - if !self.is_installed() { + // No git ref for embedded plugins or if plugin_path doesn't exist + if !self.plugin_path.exists() { return Ok(None); } self.repo().current_abbrev_ref().map(Some) } fn current_sha_short(&self) -> eyre::Result> { - if !self.is_installed() { + // No git sha for embedded plugins or if plugin_path doesn't exist + if !self.plugin_path.exists() { return Ok(None); } self.repo().current_sha_short().map(Some) } fn is_installed(&self) -> bool { - self.plugin_path.exists() + // Embedded plugins are always "installed" + self.is_embedded() || self.plugin_path.exists() } fn is_installed_err(&self) -> eyre::Result<()> { @@ -162,6 +170,11 @@ impl Plugin for VfoxPlugin { _force: bool, dry_run: bool, ) -> Result<()> { + // Skip installation for embedded plugins + if self.is_embedded() { + return Ok(()); + } + if !self.plugin_path.exists() { let url = self.get_repo_url(config)?; trace!("Cloning vfox plugin: {url}"); @@ -175,6 +188,16 @@ impl Plugin for VfoxPlugin { } async fn update(&self, pr: &dyn SingleReport, gitref: Option) -> Result<()> { + // If only embedded (no filesystem plugin), warn that it can't be updated + if self.is_embedded() && !self.plugin_path.exists() { + warn!( + "plugin:{} is embedded in mise, not updating", + style(&self.name).blue().for_stderr() + ); + pr.finish_with_message("embedded plugin".into()); + return Ok(()); + } + let plugin_path = self.plugin_path.to_path_buf(); if plugin_path.is_symlink() { warn!( @@ -206,6 +229,15 @@ impl Plugin for VfoxPlugin { if !self.is_installed() { return Ok(()); } + // If only embedded (no filesystem plugin), warn that it can't be uninstalled + if self.is_embedded() && !self.plugin_path.exists() { + warn!( + "plugin:{} is embedded in mise, cannot uninstall", + style(&self.name).blue().for_stderr() + ); + pr.finish_with_message("embedded plugin".into()); + return Ok(()); + } pr.set_message("uninstall".into()); let rmdir = |dir: &Path| { diff --git a/xtasks/release-plz b/xtasks/release-plz index 331dfda20f..a2ca0ba5e6 100755 --- a/xtasks/release-plz +++ b/xtasks/release-plz @@ -160,14 +160,71 @@ changelog="$(echo "$changelog" | tail -n +3)" mise up mise lock + +# Update embedded vfox plugin submodules based on mise registry +# Tools where vfox is the first backend should have embedded plugins +EMBEDDED_PLUGINS_DIR="crates/vfox/embedded-plugins" + +# Get current embedded plugins (submodule directory names) +CURRENT_EMBEDDED="" +if [[ -d $EMBEDDED_PLUGINS_DIR ]]; then + CURRENT_EMBEDDED="$(find "$EMBEDDED_PLUGINS_DIR" -maxdepth 1 -type d -name 'vfox-*' -exec basename {} \; | sort)" +fi + +# Get tools where vfox is the FIRST (default) backend using mise registry command +# Output format: "toolname backend1 backend2 ..." +# We only want tools where the first backend starts with "vfox:" +VFOX_REGISTRY="$(mise registry | awk '$2 ~ /^vfox:/ {print}')" + +# Extract just the plugin directory names (e.g., vfox-aapt2) for comparison +DESIRED_EMBEDDED="$(echo "$VFOX_REGISTRY" | awk '{print $2}' | sed 's|vfox:||' | sed 's|.*/||' | sort | uniq)" + +# Find plugins to add (in desired but not in current) +PLUGINS_TO_ADD="$(comm -23 <(echo "$DESIRED_EMBEDDED") <(echo "$CURRENT_EMBEDDED"))" + +# Find plugins to remove (in current but not in desired) +PLUGINS_TO_REMOVE="$(comm -13 <(echo "$DESIRED_EMBEDDED") <(echo "$CURRENT_EMBEDDED"))" + +# Add new submodules +if [[ -n $PLUGINS_TO_ADD ]]; then + echo "Adding embedded vfox plugins: $PLUGINS_TO_ADD" + while IFS= read -r plugin; do + [[ -z $plugin ]] && continue + # Extract the org/repo from mise registry output for this plugin + # Use pattern that matches plugin name at end of URL (followed by space or EOL) + repo_path="$(echo "$VFOX_REGISTRY" | grep -E "/$plugin( |\$)" | awk '{print $2}' | sed 's|vfox:||' | head -1)" + if [[ -n $repo_path ]]; then + echo "Adding submodule for $plugin from https://github.com/$repo_path" + git submodule add "https://github.com/$repo_path.git" "$EMBEDDED_PLUGINS_DIR/$plugin" || true + fi + done <<<"$PLUGINS_TO_ADD" +fi + +# Remove old submodules +if [[ -n $PLUGINS_TO_REMOVE ]]; then + echo "Removing embedded vfox plugins: $PLUGINS_TO_REMOVE" + while IFS= read -r plugin; do + [[ -z $plugin ]] && continue + echo "Removing submodule for $plugin" + git submodule deinit -f "$EMBEDDED_PLUGINS_DIR/$plugin" || true + git rm -f "$EMBEDDED_PLUGINS_DIR/$plugin" || true + rm -rf ".git/modules/$EMBEDDED_PLUGINS_DIR/$plugin" || true + done <<<"$PLUGINS_TO_REMOVE" +fi + +# Update existing submodules to latest +git submodule update --remote --merge "$EMBEDDED_PLUGINS_DIR" || true + git status # cargo update git add \ + .gitmodules \ Cargo.lock \ Cargo.toml \ CHANGELOG.md \ README.md \ crates/aqua-registry/aqua-registry \ + crates/vfox/embedded-plugins \ default.nix \ snapcraft.yaml \ docs/.vitepress/stars.data.ts \