fix: allow building without bundled plugin assets (#5433)

* fix: allow building without bundled plugin assets

* add pr
This commit is contained in:
Aram Drevekenin
2026-08-03 13:54:55 +02:00
committed by GitHub
parent c3c7e65965
commit 34e7491548
7 changed files with 177 additions and 46 deletions
+1
View File
@@ -33,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* chore: remove outdated manpage and some build fixes (https://github.com/zellij-org/zellij/pull/5426)
* feat: implement support for the kitty graphics protocol to display images in the terminal (https://github.com/zellij-org/zellij/pull/5428)
* fix: only advertise Sixel support if the attached terminal also supports it (https://github.com/zellij-org/zellij/pull/5432)
* fix: properly unbundle plugin wasm executables when stripping them (to assist distro packagers) (https://github.com/zellij-org/zellij/pull/5433)
## [0.44.3] - 2026-05-13
* fix(windows): bump windows-sys to 0.59 to align manifest with code, fixing source builds via `cargo install`/`cargo binstall` (https://github.com/zellij-org/zellij/pull/5139)
+51
View File
@@ -47,6 +47,57 @@ You can see a list of all commands (with supported arguments) with `cargo xtask
To run `test`, you will need the package `pkg-config` and a version of `openssl`.
### The cargo target directory
Debug builds embed the plugins from `<repository>/target/wasm32-wasip1/debug`, which is
resolved at compile time. `.cargo/config.toml` therefore pins `build.target-dir` to
`target`, so that a `target-dir` configured globally in `~/.cargo/config.toml` does not
move the plugins out of reach. Removing that pin breaks debug builds for everyone who
configures a shared target directory.
If you want to share compilation artifacts between projects anyway, symlink the folder
instead of pointing cargo elsewhere:
```sh
ln -s /path/to/shared/target /path/to/zellij/target
```
Setting the `CARGO_TARGET_DIR` environment variable takes precedence over
`.cargo/config.toml` and will break debug builds for the same reason. Release builds are
unaffected, since they embed the plugins from `zellij-utils/assets/plugins`.
## Packaging Zellij for a distribution
Builtin plugins (`status-bar`, `tab-bar`, ...) are `.wasm` files that are embedded into
the Zellij binary at compile time. Pre-built copies live in `zellij-utils/assets/plugins`
so that `cargo install zellij` works without further tooling. Distributions that forbid
pre-built binaries in source packages can strip that folder and build the plugins from
source instead:
```sh
# 1. build the plugins from source (repeat for every plugin, or use `cargo x build --release`)
cargo build --release --target wasm32-wasip1 \
-p status-bar -p tab-bar -p compact-bar -p strider -p session-manager \
-p configuration -p plugin-manager -p about -p share -p multiple-select \
-p layout-manager -p link -p mobile
# 2. install the resulting artifacts from <target-dir>/wasm32-wasip1/release/*.wasm
# into $PREFIX/share/zellij/plugins/
# 3. build zellij without bundled plugins
PREFIX=/usr cargo build --release --bin zellij \
--no-default-features --features disable_automatic_asset_installation
```
With the `disable_automatic_asset_installation` feature no plugin is embedded into the
binary and `zellij setup --dump-plugins` is unavailable. At runtime builtin plugins are
looked up in the configured plugin directory and then in `$PREFIX/share/zellij/plugins`.
`zellij setup --check` prints both locations.
`protoc` is required to regenerate the protobuf definitions in `zellij-utils/assets/prost*`
via `cargo x proto`. The `web_server_capability` and `vendored_curl` features are optional
and may be dropped with `--no-default-features`.
## Running the end-to-end tests
Zellij includes some end-to-end tests which test the whole application as a black-box from the outside.
These tests work by running a docker container which contains the Zellij binary, connecting to it via ssh, sending some commands and comparing the output received against predefined snapshots.
+2
View File
@@ -126,6 +126,8 @@ pub fn run(sh: &Shell, mut flags: flags::Run) -> anyhow::Result<()> {
"disable_automatic_asset_installation web_server_capability"
};
build::ensure_plugin_assets(sh).with_context(|| err_context(&flags))?;
crate::cargo()
.and_then(|cargo| {
cmd!(sh, "{cargo} run")
+65 -14
View File
@@ -116,6 +116,22 @@ pub const FEATURES: &[&str] = &[
"disable_automatic_asset_installation",
];
pub const BUILTIN_PLUGIN_NAMES: &[&str] = &[
"compact-bar",
"status-bar",
"tab-bar",
"strider",
"session-manager",
"configuration",
"plugin-manager",
"about",
"share",
"multiple-select",
"layout-manager",
"link",
"mobile",
];
#[cfg(not(target_family = "wasm"))]
pub use not_wasm::*;
@@ -133,6 +149,10 @@ mod not_wasm {
// `plugins_from_target` feature IS NOT set
// - `zellij-utils/../target/wasm32-wasip1/debug`: When building in debug mode AND the
// `plugins_from_target` feature IS set
//
// When the `disable_automatic_asset_installation` feature is set, no plugins are embedded at
// all and `ASSET_MAP` is empty. Builtin plugins must then be provided in the plugin directory.
#[cfg(not(feature = "disable_automatic_asset_installation"))]
macro_rules! add_plugin {
($assets:expr, $plugin:literal) => {
$assets.insert(
@@ -158,23 +178,54 @@ mod not_wasm {
lazy_static! {
// Zellij asset map
pub static ref ASSET_MAP: HashMap<PathBuf, Vec<u8>> = {
let mut assets = std::collections::HashMap::new();
add_plugin!(assets, "compact-bar.wasm");
add_plugin!(assets, "status-bar.wasm");
add_plugin!(assets, "tab-bar.wasm");
add_plugin!(assets, "strider.wasm");
add_plugin!(assets, "session-manager.wasm");
add_plugin!(assets, "configuration.wasm");
add_plugin!(assets, "plugin-manager.wasm");
add_plugin!(assets, "about.wasm");
add_plugin!(assets, "share.wasm");
add_plugin!(assets, "multiple-select.wasm");
add_plugin!(assets, "layout-manager.wasm");
add_plugin!(assets, "link.wasm");
add_plugin!(assets, "mobile.wasm");
#[allow(unused_mut)]
let mut assets: HashMap<PathBuf, Vec<u8>> = std::collections::HashMap::new();
#[cfg(not(feature = "disable_automatic_asset_installation"))]
{
add_plugin!(assets, "compact-bar.wasm");
add_plugin!(assets, "status-bar.wasm");
add_plugin!(assets, "tab-bar.wasm");
add_plugin!(assets, "strider.wasm");
add_plugin!(assets, "session-manager.wasm");
add_plugin!(assets, "configuration.wasm");
add_plugin!(assets, "plugin-manager.wasm");
add_plugin!(assets, "about.wasm");
add_plugin!(assets, "share.wasm");
add_plugin!(assets, "multiple-select.wasm");
add_plugin!(assets, "layout-manager.wasm");
add_plugin!(assets, "link.wasm");
add_plugin!(assets, "mobile.wasm");
}
assets
};
}
#[cfg(all(test, not(feature = "disable_automatic_asset_installation")))]
mod asset_map_test {
use super::ASSET_MAP;
use crate::consts::BUILTIN_PLUGIN_NAMES;
use std::path::PathBuf;
#[test]
fn asset_map_matches_builtin_plugin_names() {
let mut embedded: Vec<String> = ASSET_MAP
.keys()
.filter_map(|path| {
path.file_stem()
.map(|stem| stem.to_string_lossy().to_string())
})
.collect();
let mut expected: Vec<String> =
BUILTIN_PLUGIN_NAMES.iter().map(|s| s.to_string()).collect();
embedded.sort();
expected.sort();
assert_eq!(embedded, expected);
for name in BUILTIN_PLUGIN_NAMES {
assert!(ASSET_MAP
.contains_key(&PathBuf::from("plugins").join(format!("{}.wasm", name))));
}
}
}
}
#[cfg(unix)]
+4 -3
View File
@@ -703,9 +703,10 @@ If you're a developer:
plugin directory.
Possible fix for your problem:
Run `zellij setup --dump-plugins`, and optionally point it to your
'DATA DIR', visible in e.g. the output of `zellij setup --check`. Without
further arguments, it will use the default 'DATA DIR'.
Place the builtin plugin '.wasm' files in the plugin directory shown above,
or in the 'plugins' folder of the system data directory. Both are visible in
the output of `zellij setup --check`. This build carries no bundled plugins,
so `zellij setup --dump-plugins` cannot provide them.
"
)]
BuiltinPluginMissing {
+33 -29
View File
@@ -10,6 +10,7 @@ use url::Url;
use super::layout::{PluginUserConfiguration, RunPlugin, RunPluginLocation};
#[cfg(not(target_family = "wasm"))]
use crate::consts::ASSET_MAP;
use crate::consts::BUILTIN_PLUGIN_NAMES;
pub use crate::data::PluginTag;
use crate::errors::prelude::*;
@@ -57,20 +58,7 @@ impl PluginConfig {
}),
RunPluginLocation::Zellij(tag) => {
let tag = tag.to_string();
if tag == "status-bar"
|| tag == "tab-bar"
|| tag == "compact-bar"
|| tag == "strider"
|| tag == "session-manager"
|| tag == "configuration"
|| tag == "plugin-manager"
|| tag == "about"
|| tag == "share"
|| tag == "multiple-select"
|| tag == "layout-manager"
|| tag == "link"
|| tag == "mobile"
{
if BUILTIN_PLUGIN_NAMES.contains(&tag.as_str()) {
Some(PluginConfig {
path: PathBuf::from(&tag),
_allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
@@ -99,13 +87,16 @@ impl PluginConfig {
/// binary-internal asset map. Otherwise:
///
/// Attempts to first resolve the plugin path as an absolute path, then adds a ".wasm"
/// extension to the path and resolves that, finally we use the plugin directory joined with
/// the path with an appended ".wasm" extension. So if our path is "tab-bar" and the given
/// plugin dir is "/home/bob/.zellij/plugins" the lookup chain will be this:
/// extension to the path and resolves that, then the plugin directory joined with the path
/// with an appended ".wasm" extension, and finally the system data directory joined with
/// "plugins" and the same file name. So if our path is "tab-bar" and the given plugin dir is
/// "/home/bob/.local/share/zellij/plugins" the lookup chain will be this:
///
/// ```bash
/// /tab-bar
/// /tab-bar.wasm
/// tab-bar
/// tab-bar.wasm
/// /home/bob/.local/share/zellij/plugins/tab-bar.wasm
/// /usr/share/zellij/plugins/tab-bar.wasm
/// ```
///
pub fn resolve_wasm_bytes(&self, plugin_dir: &Path) -> Result<Vec<u8>> {
@@ -113,14 +104,21 @@ impl PluginConfig {
|err: std::io::Error, path: &PathBuf| format!("{}: '{}'", err, path.display());
// Locations we check for valid plugins
let paths_arr = [
&self.path,
&self.path.with_extension("wasm"),
&plugin_dir.join(&self.path).with_extension("wasm"),
#[allow(unused_mut)]
let mut paths: Vec<PathBuf> = vec![
self.path.clone(),
self.path.with_extension("wasm"),
plugin_dir.join(&self.path).with_extension("wasm"),
];
#[cfg(not(target_family = "wasm"))]
paths.push(
crate::home::system_data_dir()
.join("plugins")
.join(&self.path)
.with_extension("wasm"),
);
// Throw out dupes, because it's confusing to read that zellij checked the same plugin
// location multiple times. Do NOT sort the vector here, because it will break the lookup!
let mut paths = paths_arr.to_vec();
paths.dedup();
// This looks weird and usually we would handle errors like this differently, but in this
@@ -134,11 +132,11 @@ impl PluginConfig {
// from memory, don't bother with the disk.
#[cfg(not(target_family = "wasm"))]
if !cfg!(feature = "disable_automatic_asset_installation") && self.is_builtin() {
let asset_path = PathBuf::from("plugins").join(path);
let asset_path = PathBuf::from("plugins").join(&path);
if let Some(bytes) = ASSET_MAP.get(&asset_path) {
log::debug!("Loaded plugin '{}' from internal assets", path.display());
if plugin_dir.join(path).with_extension("wasm").exists() {
if plugin_dir.join(&path).with_extension("wasm").exists() {
log::info!(
"Plugin '{}' exists in the 'PLUGIN DIR' at '{}' but is being ignored",
path.display(),
@@ -168,9 +166,7 @@ impl PluginConfig {
// Layout requested a builtin plugin that wasn't found
let plugin_path = self.path.with_extension("wasm");
if cfg!(feature = "disable_automatic_asset_installation")
&& ASSET_MAP.contains_key(&PathBuf::from("plugins").join(&plugin_path))
{
if cfg!(feature = "disable_automatic_asset_installation") && self.is_builtin_name() {
return Err(ZellijError::BuiltinPluginMissing {
plugin_path,
plugin_dir: plugin_dir.to_owned(),
@@ -192,6 +188,14 @@ impl PluginConfig {
pub fn is_builtin(&self) -> bool {
matches!(self.location, RunPluginLocation::Zellij(_))
}
pub fn is_builtin_name(&self) -> bool {
self.path
.file_stem()
.and_then(|stem| stem.to_str())
.map(|name| BUILTIN_PLUGIN_NAMES.contains(&name))
.unwrap_or(false)
}
}
#[derive(Error, Debug, PartialEq)]
+21
View File
@@ -402,6 +402,15 @@ impl Setup {
}
if let Some(maybe_path) = &self.dump_plugins {
if cfg!(feature = "disable_automatic_asset_installation") {
return Err(anyhow!(
"This zellij was built without bundled plugins (feature \
'disable_automatic_asset_installation'). Builtin plugins are provided by the \
distributor of this build and must be placed in the plugin directory, \
visible in the output of `zellij setup --check`."
))
.context("failed to dump plugins");
}
let data_dir = &opts.data_dir.clone().unwrap_or_else(get_default_data_dir);
let dir = match maybe_path {
Some(path) => path,
@@ -494,6 +503,18 @@ impl Setup {
" Create a custom layout if you require this behavior."
)
.unwrap();
} else {
writeln!(
&mut message,
" This zellij was built without bundled plugins."
)
.unwrap();
writeln!(
&mut message,
" Builtin plugins are loaded from the 'PLUGIN DIR' above, or from '{}'.",
system_data_dir.join("plugins").display()
)
.unwrap();
}
if let Some(layout_dir) = layout_dir {
writeln!(&mut message, "[LAYOUT DIR]: \"{}\"", layout_dir.display()).unwrap();