feat(vfox): embed vfox plugin Lua code in binary#7369
Conversation
Embed vfox plugin Lua code directly into the mise binary at build time for tools where vfox is the first/default backend. This eliminates git clone operations for 8 tools. Embedded plugins: - vfox-aapt2 - vfox-ag - vfox-android-sdk - vfox-ant - vfox-bfs - vfox-bpkg - vfox-chicken - vfox-vlang Implementation: - Add git submodules for plugin repos - Build-time code generation via build.rs using include_str!() - Plugin struct supports both filesystem and embedded sources - Embedded plugins skip git clone and are always "installed" - Updates to embedded plugins ship with mise releases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR embeds vfox plugin Lua code directly into the mise binary at build time, eliminating the need for git clone operations for 8 tools that use vfox as their primary backend. The plugins are managed as git submodules and processed by a build script that generates Rust code to include the Lua files.
Key Changes:
- Build-time code generation that scans embedded plugin directories and creates Rust code with
include_str!()macros - Plugin source abstraction via
PluginSourceenum to distinguish between filesystem and embedded plugins - Automatic detection and usage of embedded plugins with seamless fallback to git clone for non-embedded plugins
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
crates/vfox/build.rs |
New build script that scans embedded-plugins/ directory and generates Rust code with embedded Lua content |
crates/vfox/src/embedded_plugins.rs |
Module that includes the generated embedded plugin code |
crates/vfox/src/plugin.rs |
Added PluginSource enum and logic to load plugins from embedded sources or filesystem |
crates/vfox/src/vfox.rs |
Updated to check for embedded plugins before attempting installation |
crates/vfox/src/lua_mod/hooks.rs |
Added hooks_embedded() function to load hooks from embedded plugin data |
src/plugins/vfox_plugin.rs |
Added is_embedded() check and skip installation/updates for embedded plugins |
crates/vfox/Cargo.toml |
Added build script reference and included embedded-plugins in package |
.gitmodules |
Registered 8 vfox plugin repositories as git submodules |
crates/vfox/embedded-plugins/* |
Added 8 plugin submodules (aapt2, ag, android-sdk, ant, bfs, bpkg, chicken, vlang) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub fn from_name(name: &str) -> Result<Self> { | ||
| // Check for embedded plugin first | ||
| if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) { | ||
| return Self::from_embedded(name, embedded); | ||
| } | ||
| let dir = Config::get().plugin_dir.join(name); | ||
| Self::from_dir(&dir) | ||
| } | ||
|
|
||
| pub fn from_name_or_dir(name: &str, dir: &Path) -> Result<Self> { | ||
| // Check for embedded plugin first | ||
| if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) { | ||
| return Self::from_embedded(name, embedded); | ||
| } | ||
| Self::from_dir(dir) | ||
| } |
There was a problem hiding this comment.
The embedded plugin check logic is duplicated in from_name() and from_name_or_dir(). Consider extracting this check into a private helper function to reduce duplication and ensure consistency if the logic changes in the future.
| // Execute the hook code to define the function | ||
| lua.load(*hook_code).exec()?; | ||
|
|
||
| // Also preload into package.loaded so require("hooks/<name>") 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) |
There was a problem hiding this comment.
The comment explains that the hook code has already been executed, but it's unclear why registering a module with value true is the correct approach. Consider expanding the comment to explain what downstream code expects from package.loaded["hooks/<name>"] and why true satisfies that requirement.
| // Execute the hook code to define the function | |
| lua.load(*hook_code).exec()?; | |
| // Also preload into package.loaded so require("hooks/<name>") 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) | |
| // Execute the hook code so it can register its hooks (typically by mutating | |
| // some global/PLUGIN table that the runtime will later call into). | |
| lua.load(*hook_code).exec()?; | |
| // Also mark the hook as loaded in package.loaded so that | |
| // require("hooks/<name>") | |
| // succeeds for embedded hooks. At this point the hook code has already | |
| // run for its side effects, and downstream code does not use the value | |
| // returned by require; it only needs require(...) to succeed and | |
| // Lua's module loader to treat "hooks/<name>" as already loaded. | |
| // | |
| // In Lua, any non-nil value in package.loaded["hooks/<name>"] indicates | |
| // that the module is loaded, so storing boolean true is sufficient and | |
| // makes the intent clear: there is no module table to export, only | |
| // previously executed hook side effects. | |
| let module_name = format!("hooks/{}", hook_name); |
| 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") { |
There was a problem hiding this comment.
The pattern is_some_and(|ext| ext == "lua") is repeated for both hooks and lib file collection. Consider extracting a helper function is_lua_file(path: &Path) -> bool to improve maintainability and readability.
Update GitHub Actions workflows to checkout git submodules, which are needed for the embedded vfox plugins feature. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
On Windows, path.display() outputs backslashes which are escape characters in Rust string literals, causing include_str! to fail. Convert all paths to forward slashes for cross-platform compatibility. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Bug: Uninstall method missing embedded plugin check
The uninstall method is missing a check for embedded plugins, unlike update() (lines 189-197) and ensure_installed() (lines 171-174) which both have early returns with warnings for embedded plugins. For embedded plugins, is_installed() returns true, so uninstall proceeds and silently does nothing (since plugin_path doesn't exist). The user sees success but the embedded plugin continues to work. This creates confusing behavior where a user believes they've uninstalled a plugin that will still function.
src/plugins/vfox_plugin.rs#L225-L248
mise/src/plugins/vfox_plugin.rs
Lines 225 to 248 in ac83d2b
Embedded plugins cannot be uninstalled since they're bundled with mise. Now shows a warning like update() and ensure_installed() do, instead of silently succeeding while the plugin continues to work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Filesystem plugins now take priority over embedded plugins - Updated from_name_or_dir() to check filesystem first - uninstall() only warns when no filesystem plugin exists - Added e2e test for embedded plugin override behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| ); | ||
| pr.finish_with_message("embedded plugin".into()); | ||
| return Ok(()); | ||
| } |
There was a problem hiding this comment.
Bug: Update blocks filesystem overrides of embedded plugins
The update method checks is_embedded() which returns true if an embedded plugin with that name exists, but it doesn't check if there's a filesystem override at plugin_path. This prevents users from updating their filesystem overrides of embedded plugins. The uninstall method correctly handles this with if self.is_embedded() && !self.plugin_path.exists(), but update is missing the && !self.plugin_path.exists() check. A user who installs an override (as shown in the e2e test) won't be able to update it.
Additional Locations (1)
There was a problem hiding this comment.
Bug: `install_plugin` ignores filesystem overrides for embedded plugins
The install_plugin method checks for embedded plugins FIRST (line 107-108) and returns immediately if one exists, before checking if a filesystem override exists at plugin_dir. This is the opposite of get_sdk which correctly uses from_name_or_dir (filesystem first, then embedded). Since install_plugin is called by Vfox::install (line 152), installing a tool version for an embedded plugin that has a filesystem override will use the embedded version instead of the user's override. The check order needs to be swapped to match from_name_or_dir.
crates/vfox/src/vfox.rs#L104-L117
Lines 104 to 117 in 5b749c0
The embedded plugin submodules have their own formatting conventions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- install_plugin: check filesystem first, then embedded, then registry - update: only warn when no filesystem plugin exists - current_abbrev_ref/current_sha_short: check plugin_path.exists() This ensures users can override embedded plugins by installing their own version, and those overrides can be updated/uninstalled properly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Modify release-plz to automatically add/remove vfox plugin submodules based on `mise registry` output. Tools where vfox is the first (default) backend will have their plugins embedded at build time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Lib modules can legitimately return non-table values (functions, strings, nil, etc.). Changed load_embedded_libs to use Value type instead of Table to handle all valid Lua module return types. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The from_name method was checking embedded plugins before filesystem, while from_name_or_dir and install_plugin check filesystem first. This inconsistency prevented user overrides when code paths used from_name. Now all methods consistently check filesystem first, allowing users to override embedded plugins by installing their own version. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add submodules: true to workflows that build Rust code: - hyperfine.yml (benchmark) - ppa-publish.yml - registry.yml - release-fig.yml - release-plz.yml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Fix e2e test to use vfox-compatible plugin override instead of asdf plugin (mise-tiny). Now tests with the actual vfox-bfs plugin. 2. Fix lib-to-lib require dependencies by using package.preload instead of package.loaded. This allows lib files to require each other regardless of alphabetical ordering. 3. Fix build script to track hooks/ and lib/ subdirectories and individual .lua files for proper rebuild detection. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The submodule READMEs have their own formatting that we shouldn't modify. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The \b word boundary incorrectly matches plugins with similar prefixes (e.g., vfox-android would match vfox-android-sdk). Use explicit pattern that matches plugin name followed by space or end of line. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.12.10 x -- echo |
21.1 ± 0.8 | 19.6 | 26.3 | 1.00 |
mise x -- echo |
21.3 ± 0.6 | 19.5 | 24.1 | 1.01 ± 0.05 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.12.10 env |
20.5 ± 1.0 | 18.8 | 31.7 | 1.00 |
mise env |
21.2 ± 0.6 | 19.2 | 24.4 | 1.03 ± 0.06 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.12.10 hook-env |
20.6 ± 0.4 | 19.4 | 22.4 | 1.00 |
mise hook-env |
21.0 ± 0.8 | 19.5 | 29.2 | 1.02 ± 0.04 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.12.10 ls |
18.3 ± 1.0 | 16.8 | 24.3 | 1.00 |
mise ls |
19.6 ± 1.2 | 17.8 | 26.0 | 1.07 ± 0.09 |
xtasks/test/perf
| Command | mise-2025.12.10 | mise | Variance |
|---|---|---|---|
| install (cached) | 113ms | 114ms | +0% |
| ls (cached) | 69ms | 69ms | +0% |
| bin-paths (cached) | 75ms | 76ms | -1% |
| task-ls (cached) | 286ms | 292ms | -2% |
### 🚀 Features - **(alias)** rename alias to tool-alias, add shell-alias command by @jdx in [#7357](#7357) - **(upgrade)** display summary of upgraded tools by @jdx in [#7372](#7372) - **(vfox)** embed vfox plugin Lua code in binary by @jdx in [#7369](#7369) ### 🐛 Bug Fixes - **(aqua)** add start_operations for progress reporting by @jdx in [#7354](#7354) - **(github)** improve asset detection for distro-specific and Swift artifacts by @jdx in [#7347](#7347) - **(github)** clean up static_helpers.rs and fix archive bin= option by @jdx in [#7366](#7366) - **(http)** add start_operations for progress reporting by @jdx in [#7355](#7355) - **(lockfile)** place lockfile alongside config file by @jdx in [#7360](#7360) - **(progress)** add start_operations to core plugins by @jdx in [#7351](#7351) - **(ruby-install)** Use ruby_install_bin to update by @calebhearth in [#7350](#7350) - **(rust)** add release_url for rust versions by @jdx in [#7373](#7373) - **(schema)** add `tool_alias`, mark `alias` as deprecated by @SKalt in [#7358](#7358) - **(toolset)** filter tools by OS in list_current_versions by @jdx in [#7356](#7356) - **(ubi)** only show deprecation warning during installation by @jdx in [#7380](#7380) - **(ui)** remove noisy "record size" message during install by @jdx in [#7381](#7381) - update mise-versions URL to use /tools/ prefix by @jdx in [#7378](#7378) ### 🚜 Refactor - **(backend)** unified AssetMatcher with checksum fetching by @jdx in [#7370](#7370) - **(backend)** deprecate ubi backend in favor of github by @jdx in [#7374](#7374) - **(toolset)** decompose mod.rs into smaller modules by @jdx in [#7371](#7371) ### 🧪 Testing - **(e2e)** fix and rename ubi and vfox_embedded_override tests by @jdx in [052ea40](052ea40) ### 📦 Registry - add vfox-gcloud backend for gcloud by @jdx in [#7349](#7349) - convert amplify to use github backend by @jdx in [#7365](#7365) - add github backend for djinni tool by @jdx in [#7363](#7363) - switch glab to native gitlab backend by @jdx in [#7364](#7364) - add s5cmd by @jdx in [#7376](#7376) ### Chore - **(registry)** disable flaky tests for gitu and ktlint by @jdx in [64151cb](64151cb) - resolve clippy warnings and add stricter CI check by @jdx in [#7367](#7367) - suppress dead_code warnings in asset_matcher module by @jdx in [#7377](#7377) ### New Contributors - @calebhearth made their first contribution in [#7350](#7350)
* upstream/main: fix(ui): remove noisy "record size" message during install (jdx#7381) test(e2e): fix and rename ubi and vfox_embedded_override tests fix: update mise-versions URL to use /tools/ prefix (jdx#7378) fix(ubi): only show deprecation warning during installation (jdx#7380) registry: add s5cmd (jdx#7376) chore: suppress dead_code warnings in asset_matcher module (jdx#7377) refactor(backend): deprecate ubi backend in favor of github (jdx#7374) fix(rust): add release_url for rust versions (jdx#7373) feat(vfox): embed vfox plugin Lua code in binary (jdx#7369) refactor(backend): unified AssetMatcher with checksum fetching (jdx#7370) feat(upgrade): display summary of upgraded tools (jdx#7372) fix(github): clean up static_helpers.rs and fix archive bin= option (jdx#7366) refactor(toolset): decompose mod.rs into smaller modules (jdx#7371) chore: resolve clippy warnings and add stricter CI check (jdx#7367) registry: switch glab to native gitlab backend (jdx#7364) fix(ruby-install): Use ruby_install_bin to update (jdx#7350) registry: add github backend for djinni tool (jdx#7363) registry: convert amplify to use github backend (jdx#7365) chore(registry): disable flaky tests for gitu and ktlint
Summary
Embedded Plugins
Implementation Details
crates/vfox/build.rsscansembedded-plugins/and generates Rust code withinclude_str!()macrosPluginSourceenum distinguishes between filesystem and embedded pluginsTest plan
mise ls-remote vfox:agworks (and other embedded plugins)🤖 Generated with Claude Code
Note
Embeds selected vfox plugins into the binary via build-time codegen, falls back to embedded plugins when not installed, auto-manages plugin submodules, and updates CI to checkout submodules.
crates/vfox/build.rs) to embed plugin Lua (metadata,hooks,lib) fromcrates/vfox/embedded-pluginsusinginclude_str!.embedded_pluginsmodule andPluginSource(filesystem vs embedded); loader updated to set Lua paths only for filesystem and preload embedded libs/hooks.Plugin::from_name_or_dir/from_embedded;Vfox::install_pluginfalls back to embedded.src/plugins/vfox_plugin.rs: embedded plugins treated as installed; skip clone/update/uninstall; no git ref/sha; supports filesystem override.test_vfox_embedded_override_slowverifies embedded works, can be overridden by filesystem, and falls back after uninstall.actions/checkoutwithsubmodules: true.xtasks/release-plz: auto-sync embedded plugin submodules from registry (add/remove/update); include.gitmodulesandcrates/vfox/embedded-pluginsin commits..gitmodulesentries for embedded plugins; ignorecrates/vfox/embedded-pluginsin lint/prettier.Written by Cursor Bugbot for commit e3ef5f2. This will update automatically on new commits. Configure here.