Files
openidea/plugins/build.sh
T
1c5a92f952 feat(plugins): plugin system with extension points, consent, Wasm runtime and plugin API 1.0 (#971)
A plugin system built on WebAssembly components (wasmtime, WASI p2), with a
typed WIT API (`openidea:plugin@1.0.0`) and a Rust SDK, shipped as a preview.

- Extension points declared in `plugin.toml` (commands, languages, themes,
  icon themes, menus, key bindings, status items, settings, views, custom
  editors, file icons and templates, run detectors, editor providers),
  validated before anything takes effect and projected onto the workbench's
  own registries: plugin themes and file icon themes are chosen in
  Settings › Appearance and Quick Switch, plugin shortcuts are edited in
  Settings › Keymap, plugin problems are grouped in Settings › Inspections.
- Capabilities with risk levels and consent at install, pinned `process:`
  programs, secrets in the OS keychain.
- Runtime: one thread per active plugin, fuel/time/memory budgets, soft and
  hard deadlines on the editing path, cancellation, crash backoff, idle
  reclaim, a precompiled-component cache; remote projects run code plugins on
  `openidea-server`.
- Web views and custom editors with a sandboxed page, a document bridge
  joined to the text editor's undo history, and split or preview layouts.
- Distribution: signed `.oiplugin` packages, a static marketplace index with
  revocation, versioned installs with rollback, install from a `.zip` or a
  folder. The release workflow signs and publishes the official plugins and
  their index, which released builds use as the default marketplace.
- Official plugins: String Tools, Key Promoter, Git Toolbox, AI Completion,
  Checkstyle, MyBatis Navigator, CSV Editor, Solidity, Material Icons, Harbor
  Night, Excalidraw, Graphviz, Regex Tester, License Header.

Closes #732

Co-authored-by: Deng Liu <5108773+dengliu@users.noreply.github.com>
Co-authored-by: florentvdry <16673235+florentvdry@users.noreply.github.com>
2026-11-04 02:54:31 +08:00

140 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Builds the repository's plugins: Rust code into WebAssembly components
# (wasm32-wasip2 emits a component directly; no cargo-component / wasm-tools),
# and web pages (`web-src/`, a Vite project) into `web/`.
#
# plugins/build.sh # every plugin with code or a web page
# plugins/build.sh official/csv-editor # one plugin (a path under plugins/, or any path)
# plugins/build.sh --check [...] # change nothing tracked; fail if a checked-in
# # component is stale or a plugin doesn't build (CI)
#
# Output: <plugin>/dist/plugin.wasm (referenced by `wasm = ...` in plugin.toml)
# and <plugin>/dist/plugin.wasm.sources, a fingerprint of the sources it was
# built from: the plugin (minus target/, dist/, web/ and web-src/), the SDK
# and wit/. Components are small and checked in; builds are not
# byte-reproducible across machines, so --check compares fingerprints, not
# bytes. A web page (web-src/, a pnpm project) is built into <plugin>/web/,
# which is not checked in (it can be tens of MB): build it before installing
# the plugin from the repository. web.sources (not checked in either) skips
# rebuilding an unchanged page; --check builds it too and checks the result.
# CARGO_TARGET_DIR, when set, is shared by all plugins.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(cd "$here/.." && pwd)"
rustup target list --installed | grep -qx wasm32-wasip2 || rustup target add wasm32-wasip2
check=0
if [ "${1:-}" = "--check" ]; then
check=1
shift
fi
sha256() { if command -v sha256sum >/dev/null; then sha256sum; else shasum -a 256; fi; }
# Lists "<path> <sha256>" for the files git sees (tracked, or new and not
# ignored) under the given paths of directory $1, minus build output and Finder
# metadata; so a
# checkout fingerprints the same as the working tree that committed it.
list() {
local dir="$1"
shift
(cd "$dir" && git ls-files --cached --others --exclude-standard -- "$@") |
{ grep -Ev '(^|/)(dist|target|node_modules)/|(^|/)\.DS_Store$|^web(-src)?/|^web\.sources$' || true; } | LC_ALL=C sort -u | while read -r f; do
if [ -f "$dir/$f" ]; then printf '%s %s\n' "$f" "$(sha256 <"$dir/$f" | cut -d' ' -f1)"; fi
done
}
# Prints the fingerprint of everything plugin $1's component is built from.
fingerprint() {
{ list "$1" .; list "$repo" sdk/rust/openidea-plugin/Cargo.toml sdk/rust/openidea-plugin/src sdk/rust/openidea-plugin/wit wit; } |
sha256 | cut -d' ' -f1
}
# Prints the fingerprint of plugin $1's web page sources.
web_fingerprint() {
(cd "$1/web-src" && git ls-files --cached --others --exclude-standard -- .) |
{ grep -Ev '(^|/)node_modules/|(^|/)\.DS_Store$' || true; } | LC_ALL=C sort -u | while read -r f; do
if [ -f "$1/web-src/$f" ]; then printf '%s %s\n' "$f" "$(sha256 <"$1/web-src/$f" | cut -d' ' -f1)"; fi
done | sha256 | cut -d' ' -f1
}
# Builds plugin $1's web page into web/ unless it was built from the current
# web-src/, then checks what the manifest's `web = "…"` entries need: each
# page is there and loads its files by relative URL (it is served from the package).
build_web() {
local dir="$1" rel="${1#"$repo"/}" want entry
want="$(web_fingerprint "$dir")"
if [ "$want" != "$(cat "$dir/web.sources" 2>/dev/null || true)" ] || [ ! -d "$dir/web" ]; then
echo "building the web page of $rel"
if ! { pnpm --dir "$dir/web-src" install --frozen-lockfile --ignore-workspace --silent &&
pnpm --dir "$dir/web-src" run build; }; then
echo "FAIL $rel: the web page does not build"
failed=1
return
fi
echo "$want" >"$dir/web.sources"
fi
for entry in $(sed -n 's/^web *= *"\(.*\)".*/\1/p' "$dir/plugin.toml" | sort -u); do
if [ ! -f "$dir/$entry" ]; then
echo "FAIL $rel: $entry was not built"
failed=1
elif grep -Eq '(src|href)="/' "$dir/$entry"; then
echo "FAIL $rel: $entry loads files by absolute URL; build it with relative paths (Vite: base: \"./\")"
failed=1
fi
done
echo "ok $rel (web, $(du -sh "$dir/web" | cut -f1))"
}
failed=0
build() {
local dir="$1"
if [ -f "$dir/web-src/package.json" ]; then build_web "$dir"; fi
[ -f "$dir/Cargo.toml" ] || return 0
local name target out
name="$(sed -n 's/^name *= *"\(.*\)"/\1/p' "$dir/Cargo.toml" | head -1)"
target="${CARGO_TARGET_DIR:-$dir/target}"
out="$target/wasm32-wasip2/release/${name//-/_}.wasm"
local rel="${dir#"$repo"/}"
if [ "$check" = 1 ]; then
if ! CARGO_TARGET_DIR="$target" cargo build -q --locked --manifest-path "$dir/Cargo.toml" --release --target wasm32-wasip2; then
echo "FAIL $rel: does not build"
failed=1
return
fi
local want have
want="$(fingerprint "$dir")"
have="$(cat "$dir/dist/plugin.wasm.sources" 2>/dev/null || true)"
if [ "$want" != "$have" ]; then
echo "STALE $rel: dist/plugin.wasm was not built from the current sources; run plugins/build.sh $rel"
failed=1
else
echo "ok $rel"
fi
return
fi
echo "building $name ($rel)"
CARGO_TARGET_DIR="$target" cargo build --manifest-path "$dir/Cargo.toml" --release --target wasm32-wasip2
mkdir -p "$dir/dist"
cp "$out" "$dir/dist/plugin.wasm"
fingerprint "$dir" >"$dir/dist/plugin.wasm.sources"
echo " -> $rel/dist/plugin.wasm ($(wc -c <"$dir/dist/plugin.wasm" | tr -d " ") bytes)"
}
if [ $# -gt 0 ]; then
for p in "$@"; do
if [ -d "$here/$p" ]; then build "$here/$p"
elif [ -d "$repo/$p" ]; then build "$(cd "$repo/$p" && pwd)"
elif [ -d "$p" ]; then build "$(cd "$p" && pwd)"
else echo "no plugin at $p" >&2; exit 2
fi
done
else
# Every component (plugin.toml + Cargo.toml) and web page (plugin.toml + web-src/).
for manifest in "$here"/official/*/plugin.toml "$repo"/crates/ide-plugin/tests/fixtures/*/plugin.toml; do
dir="$(dirname "$manifest")"
if [ -f "$dir/Cargo.toml" ] || [ -f "$dir/web-src/package.json" ]; then build "$dir"; fi
done
fi
exit "$failed"