A Cargo subcommand to temporarily patch workspace dependencies for local plugin development.
cargo-mujina is designed specifically to streamline local plugin development across Rust workspaces. It safely and temporarily patches your Cargo.toml to point to local plugin paths or Git branches, executes your specified cargo command, and keeps your original workspace configuration safe.
“Mujina” is a Japanese yokai often confused with raccoon dogs or badgers, believed to be a creature capable of shapeshifting. Additionally, the proverb “Onaji ana no mujina (same hole, same mujina)” means that even if appearances or origins differ, the essence remains the same.
This tool works in a similar way: it merely temporarily alters the “appearance” of references such as path or git, without changing the essential behavior of the crate itself (how it is treated as a dependency). The name is derived from this concept.
When building plugin architectures in Rust, developing and testing plugins locally alongside a host application requires tedious and error-prone Cargo.toml tweaks (such as adding path = "...", editing [workspace.dependencies], or setting up [patch] sections).
cargo-mujina automates this entire local plugin development workflow. It dynamically patches dependencies, auto-discovers plugin crates, and patches transitive Git dependencies back to your local host crate—all without polluting your git working tree with temporary paths.
The primary purpose is an inventory-based plugin system. (Dynamically importing crates that match [package.metadata.plugin].prefix from a workspace in a different repository into [workspace.dependencies].)
However, since the mechanism itself is a general-purpose process that “temporarily swaps the dependency resolution source between two workspaces that have workspace member crates with the same name,” it can also be used in other scenarios such as:
- A development loop where you build and test downstream projects while making local modifications to upstream libraries (temporarily swap using
-W ‘{ path = “../core” }’; since it’s rebuilt from Cargo.toml.org each time, it’s easy to revert to the original state withcargo mujina restore) - Bisecting a regression caused by a specific commit in a dependency crate (repeat
cargo mujina testwhile changing-W ‘{ git = “...”, tag = “...” }’) - Temporary bulk replacement with a patched fork for vulnerability fixes, etc.
- Local pseudo-merge in cases where repositories are split for ownership reasons but development is unified (the plugin system is a type of this)
cargo install --path .Use cargo mujina exactly like you would use standard Cargo commands (e.g., build, test, check, run), but pass the target repository you want to patch in via the -W or --with flag.
# Patch dependencies using a local workspace path and run `cargo build`
cargo mujina build -W '{ path = "../other-workspace" }'
# Pass additional arguments to cargo by placing them at the end
cargo mujina test -W '{ path = "../other-workspace" }' -- --nocapture
# Patch using a specific Git repository and branch
cargo mujina check -W '{ git = "https://github.com/example/repo.git", branch = "main" }'cargo-mujina acts as a transparent wrapper for most standard cargo commands. The following commands are fully supported and will be forwarded to cargo after the temporary patches are applied:
benchbuild(alias:b)check(alias:c)clippydoc(alias:d)fetchmetadatamirirun(alias:r)rustcrustdoctest(alias:t)treevendor
edit: Applies the patches toCargo.tomlbut exits without invoking any cargo command. Useful if you just want to update theCargo.tomlfor your IDE.restore: Restores the originalCargo.tomlfrom theCargo.toml.orgbackup.
When you run cargo mujina <cmd> -W <spec>, the following steps occur:
- Safety First (Backup): Checks for
Cargo.toml.org. If it doesn't exist, it backs up your currentCargo.tomltoCargo.toml.org. - Workspace Validation: Ensures both your current directory and the target
-Wdirectory are Cargo workspaces. - Dependency Replacement: Finds matching crates between the workspaces and replaces their entries in
[workspace.dependencies]with the specified local path or git source. - Git Patch Injection: If the target workspace depends on any of your current workspace's crates via Git, it automatically appends a
[patch."<git-url>"]section pointing back to your local crates. - Command Execution: Runs the specified cargo command (e.g.,
cargo build) with any trailing arguments you provided.
If you are developing a plugin system, you can configure cargo-mujina to automatically add new plugins from the target workspace even if they aren't already listed in your dependencies.
Add the following to your root Cargo.toml:
[package.metadata.plugin]
prefix = "my-plugin-"When you run cargo mujina, any crate in the target workspace whose name starts with my-plugin- will be:
- Added to your
[workspace.dependencies]. - Appended to your root-level
[dependencies]as{ workspace = true }.
cargo-mujina intentionally leaves the modified Cargo.toml in place so your language server (like rust-analyzer) can utilize the patched local dependencies.
When you are done with local testing, restore your original configuration:
cargo mujina restoreIf you don't want to install a Rust toolchain or use the CLI directly, cargo-mujina also ships
as a GitHub Actions composite action. It downloads a prebuilt cargo-mujina binary, clones your
core repository plus one or more replacement repositories, runs a cargo command, and lets you
download the result — all from the Actions tab, with no local setup.
Add the following workflow file to the repository you want to build/test from
(sample/mujina-dispatch.yml). No other files are needed in that repository — the
action itself lives in this cargo-mujina repository and is referenced remotely, so there's
nothing to copy in and no actions/checkout step required.
name: cargo-mujina patch & run
on:
workflow_dispatch:
inputs:
os:
description: 'OS to build/run on'
required: false
type: choice
default: 'ubuntu-latest'
options:
- ubuntu-latest
- windows-latest
- macos-latest
core-repo:
description: 'Core (host) repository URL'
required: true
default: ''
core-ref:
description: 'Branch/tag of core-repo (leave empty for the default branch)'
required: false
default: ''
patches:
description: >-
Replacement repos as "<git-url>@<branch>", separated by ";" (or a newline).
Example: https://github.com/your-org/plugin-a.git@feature-x;https://github.com/your-org/plugin-b.git@main
required: true
command:
description: 'cargo subcommand to run (build / test / check / run / ...). Do not use "edit".'
required: false
default: 'build'
args:
description: 'Extra args forwarded after `--` to the cargo subcommand'
required: false
default: ''
artifact-path:
description: >-
Path (relative to the repo root) of the build output to upload as a downloadable
artifact, e.g. "workspace/target/debug/mybinary" or a glob like
"workspace/target/release/*". Leave empty to skip uploading (e.g. for `test`/`check`,
where there's nothing meaningful to grab afterward).
required: false
default: ''
jobs:
patch-and-run:
runs-on: ${{ inputs.os }}
steps:
- name: Patch and run via cargo-mujina
uses: Taqman-probe/mujina/.github/actions/cargo-mujina-build@main
with:
core-repo: ${{ inputs.core-repo }}
core-ref: ${{ inputs.core-ref }}
patches: ${{ inputs.patches }}
command: ${{ inputs.command }}
args: ${{ inputs.args }}
- name: Upload build artifact
if: ${{ inputs.artifact-path != '' }}
uses: actions/upload-artifact@v4
with:
name: build-output
path: ${{ inputs.artifact-path }}
if-no-files-found: error
Commit and push this one file. That's the entire setup.
- Go to the Actions tab of the repository → select "cargo-mujina patch & run" in the left sidebar → click "Run workflow".
- Fill in the inputs:
- os: the target OS (
ubuntu-latest/windows-latest/macos-latest). - core-repo / core-ref: the Git URL and branch of the host project.
- patches: one or more
<git-url>@<branch>entries for the crates you want to swap in, separated by;or a newline. - command: the cargo subcommand to run (
build,test,check,run, ...). Avoidedit. - artifact-path: set this if you want to download the result afterward, e.g.
workspace/target/debug/<binary-name>(...\<binary-name>.exeon Windows).
- os: the target OS (
- Click "Run workflow" and wait for it to finish.
- If
artifact-pathwas set, open the completed run's summary page — there's an Artifacts section near the bottom with a downloadable zip.
The downloaded artifact is always a .zip, even for a Linux/macOS binary, and the execute bit is
often lost along the way. After unzipping:
unzip build-output.zip
chmod +x ./<binary-name>
./<binary-name>Even if you add a crate to register a plugin using inventory::submit!, it won’t work on its own. If that crate isn’t used anywhere, the linker will determine that it is “unused” and remove the entire crate—including its static initializers—which may prevent the registration with inventory from being executed at all.
Therefore, the following two steps are required:
- Insert
use <plugin_crate> as _;intobuild.rs.cargo_metadatareads the [dependencies] of the root crate, generates ausestatement for each crate name that matches the prefix (converting hyphens to underscores), and writes them to$OUT_DIR/generated_plugins.rs. - Include that file using
include!inmain.rs.
include!(concat!(env!("OUT_DIR"), "/generated_plugins.rs"));
You only need to insert one line, but if you forget to do so, the plugin will not work for the reasons mentioned above.
For specific implementation examples, please refer to sample/build.rs and sample.main.rs.
Note: While the prefix check in
build.rsincludes a fallback (“{package name}-plugin-”) for cases where[package.metadata.plugin].prefixis not set, the automatic addition of new crates bycargo-mujinaitself (apply_patches) only works when the prefix is explicitly set.
Licensed under the Apache License, Version 2.0.