fix(plugins): build new plugins outside the repository

- `openidea-plugin new` makes a Rust plugin depend on the SDK at the tool's
  release tag (git) unless the plugin is inside the source tree the tool was
  built from or `--sdk` is given; the path written before only resolved on
  the machine that built the tool.
- The command template compiles again: `run_command` returns
  `openidea_plugin::Error`. A test now runs `cargo test` on the generated
  plugin against the SDK.
- plugins/README.md: installing the tool with `cargo install --git`, the SDK
  dependency, where plugins can be installed (welcome screen and trusted
  projects), and where to look when a plugin misbehaves.
Цей коміт міститься в:
l0ng-ai committed 2026-11-04 20:55:31 +08:00
1 джерело 2cd16a723d
коміт 4b5ef7069c
3 files changed
+98 -14

No files matched your search

+68 -8
Переглянути файл
@@ -33,7 +33,9 @@ usage: openidea-plugin <command> [options]
commands:
new <dir> [--template T] [--id ID] [--name NAME] [--sdk PATH]
create a plugin; T = command (default), theme, icon-theme, language
create a plugin; T = command (default), theme, icon-theme, language. A Rust
plugin depends on the SDK of this tool's release (git); --sdk PATH uses a
local sdk/rust/openidea-plugin instead
build [dir] [--debug] compile a Rust plugin to dist/plugin.wasm and check it
check [dir|package] validate the package (folder or .oiplugin) as the IDE would install it
link [dir] load the folder in the IDE without installing it
@@ -177,7 +179,7 @@ fn cmd_new(args: &[String]) -> Result<(), String> {
std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
let dir = canonical(&dir)?;
let sdk = match template {
templates::Template::Command => Some(sdk_path(a.value("sdk"), &dir)?),
templates::Template::Command => Some(sdk_dependency(a.value("sdk"), &dir)?),
_ => None,
};
let vars = templates::Vars { id: &id, name: &name, sdk: sdk.as_deref() };
@@ -232,12 +234,16 @@ fn title_case(s: &str) -> String {
.join(" ")
}
/// The SDK dependency path written into a new plugin's Cargo.toml: `--sdk`,
/// or the SDK of the source tree this tool was built from.
fn sdk_path(explicit: Option<&str>, plugin_dir: &Path) -> Result<String, String> {
/// The SDK dependency written into a new plugin's Cargo.toml: by path with `--sdk`, or
/// when the plugin is inside the source tree this tool was built from; otherwise the
/// repository at this tool's release tag.
fn sdk_dependency(explicit: Option<&str>, plugin_dir: &Path) -> Result<String, String> {
let sdk = match explicit {
Some(p) => PathBuf::from(p),
None => PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../sdk/rust/openidea-plugin")),
None => match dunce::canonicalize(concat!(env!("CARGO_MANIFEST_DIR"), "/../..")) {
Ok(tree) if plugin_dir.starts_with(&tree) => tree.join("sdk/rust/openidea-plugin"),
_ => return Ok(format!("{{ git = \"{REPOSITORY}\", tag = \"v{}\" }}", env!("CARGO_PKG_VERSION"))),
},
};
let sdk = dunce::canonicalize(&sdk).map_err(|_| {
format!("the plugin SDK was not found at {}; pass --sdk <path to sdk/rust/openidea-plugin>", sdk.display())
@@ -246,9 +252,11 @@ fn sdk_path(explicit: Option<&str>, plugin_dir: &Path) -> Result<String, String>
return Err(format!("{} is not the openidea-plugin SDK (no Cargo.toml)", sdk.display()));
}
let path = relative(plugin_dir, &sdk).unwrap_or(sdk);
Ok(path.to_string_lossy().replace('\\', "/"))
Ok(format!("{{ path = \"{}\" }}", path.to_string_lossy().replace('\\', "/")))
}
const REPOSITORY: &str = "https://github.com/l0ng-ai/openidea";
/// `to` relative to `from` when they share a root (keeps packages in a repo movable).
fn relative(from: &Path, to: &Path) -> Option<PathBuf> {
let (f, t): (Vec<_>, Vec<_>) = (from.components().collect(), to.components().collect());
@@ -781,6 +789,27 @@ mod tests {
assert_eq!(relative(Path::new("/x/y"), Path::new("/z")), None);
}
#[test]
fn new_plugins_depend_on_the_sdk_by_path_only_in_its_source_tree() {
let tree = dunce::canonicalize(concat!(env!("CARGO_MANIFEST_DIR"), "/../..")).unwrap();
assert_eq!(
sdk_dependency(None, &tree.join("plugins/official/new")).unwrap(),
"{ path = \"../../../sdk/rust/openidea-plugin\" }"
);
let outside = dunce::canonicalize(std::env::temp_dir()).unwrap().join("my-plugin");
assert_eq!(
sdk_dependency(None, &outside).unwrap(),
format!("{{ git = \"https://github.com/l0ng-ai/openidea\", tag = \"v{}\" }}", env!("CARGO_PKG_VERSION"))
);
let sdk = tree.join("sdk/rust/openidea-plugin");
assert!(sdk_dependency(Some(sdk.to_str().unwrap()), &outside).unwrap().starts_with("{ path = "));
assert!(
sdk_dependency(Some(tree.join("wit").to_str().unwrap()), &outside)
.unwrap_err()
.contains("not the openidea-plugin SDK")
);
}
/// A theme package `id` signed by `publisher` with `key`, packed into `out`.
fn signed_theme(tmp: &Path, id: &str, publisher: &str, key: &str, out: &Path) {
signed_theme_version(tmp, id, "0.1.0", "^1.0", publisher, key, out);
@@ -901,13 +930,44 @@ mod tests {
assert!(err.contains("unknown publisher"), "{err}");
}
/// `cargo test` of a new Rust plugin (its own tests against the mock host) with this SDK.
#[test]
fn the_command_template_builds_against_the_sdk() {
let dir = std::env::temp_dir().join(format!("openidea-plugin-template-build-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let dir = dunce::canonicalize(&dir).unwrap();
let plugin = dir.join("plugin");
let sdk = concat!(env!("CARGO_MANIFEST_DIR"), "/../../sdk/rust/openidea-plugin");
let sdk = sdk_dependency(Some(sdk), &plugin).unwrap();
let vars = templates::Vars { id: "acme.sample", name: "Sample", sdk: Some(&sdk) };
for (rel, content) in templates::render(templates::Template::Command, &vars) {
let path = plugin.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, content).unwrap();
}
let out = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
.args(["test", "--offline", "--manifest-path"])
.arg(plugin.join("Cargo.toml"))
.env("CARGO_TARGET_DIR", dir.join("target"))
.output()
.unwrap();
assert!(
out.status.success(),
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn every_template_is_a_valid_package() {
for t in templates::Template::ALL {
let dir =
std::env::temp_dir().join(format!("openidea-plugin-template-{}-{}", t.label(), std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let vars = templates::Vars { id: "acme.sample", name: "Sample", sdk: Some("../sdk") };
let vars = templates::Vars { id: "acme.sample", name: "Sample", sdk: Some("{ path = \"../sdk\" }") };
for (rel, content) in templates::render(t, &vars) {
let path = dir.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+5 -4
Переглянути файл
@@ -32,6 +32,7 @@ impl Template {
pub struct Vars<'a> {
pub id: &'a str,
pub name: &'a str,
/// The SDK dependency, as a TOML value (`{ path = "…" }`).
pub sdk: Option<&'a str>,
}
@@ -118,7 +119,7 @@ publish = false
crate-type = ["cdylib", "rlib"]
[dependencies]
openidea-plugin = { path = "__SDK__" }
openidea-plugin = __SDK__
[profile.release]
opt-level = "s"
@@ -130,7 +131,7 @@ panic = "abort"
const COMMAND_LIB: &str = r#"//! __NAME__: an OpenIDEA plugin.
use openidea_plugin::{Invocation, NotificationLevel, Plugin, host, register_plugin};
use openidea_plugin::{Error, Invocation, NotificationLevel, Plugin, host, register_plugin};
#[derive(Default)]
pub struct Main {
@@ -138,7 +139,7 @@ pub struct Main {
}
impl Plugin for Main {
fn run_command(&mut self, id: &str, ctx: &Invocation, _arg: Option<&str>) -> Result<Option<String>, String> {
fn run_command(&mut self, id: &str, ctx: &Invocation, _arg: Option<&str>) -> Result<Option<String>, Error> {
match id {
"say-hello" => {
// Instance state lasts until the plugin is idle for a while;
@@ -153,7 +154,7 @@ impl Plugin for Main {
let text = editor.document()?.text;
Ok(Some(format!("{} lines in {}", text.lines().count(), editor.path())))
}
_ => Err(format!("unknown command {id}")),
_ => Err(format!("unknown command {id}").into()),
}
}
}
+25 -2
Переглянути файл
@@ -44,7 +44,8 @@ The plugin API is defined in [`wit/`](../wit/) (`openidea:[email protected]`, world
The `openidea-plugin` tool scaffolds, builds, checks and links plugins:
```sh
cargo install --path crates/ide-host --bin openidea-plugin # or: cargo run -p ide-host --bin openidea-plugin -- …
cargo install --locked --git https://github.com/l0ng-ai/openidea ide-host --bin openidea-plugin
# in a checkout of this repository: cargo install --path crates/ide-host --bin openidea-plugin
openidea-plugin new my-plugin # Rust command plugin (SDK)
openidea-plugin new my-theme --template theme # also: icon-theme, language
@@ -54,6 +55,17 @@ openidea-plugin build # → dist/plugin.wasm, t
openidea-plugin link # load this folder in the IDE
```
Each release also attaches the tool for Linux x64
(`openidea-plugin-linux-x86_64.gz`). A plugin is a project of its own: keep it
in its own repository, and list it in the community marketplace when it is
ready (§8.1).
A Rust plugin made by `new` depends on the SDK of the tool's release, from
this repository's tag (`openidea-plugin = { git = "…", tag = "vX.Y.Z" }`);
change the tag to build against a later API. Inside a checkout of this
repository, or with `--sdk <path to sdk/rust/openidea-plugin>`, it depends on
that SDK by path instead.
`build` runs `cargo build --release --target wasm32-wasip2` (installing the
target through rustup if needed), copies the component to the manifest's
`wasm` path and runs `check`. `check` validates the package exactly as
@@ -212,7 +224,7 @@ edition = "2024"
crate-type = ["cdylib", "rlib"]
[dependencies]
openidea-plugin = { path = "…/sdk/rust/openidea-plugin" }
openidea-plugin = { git = "https://github.com/l0ng-ai/openidea", tag = "vX.Y.Z" } # or { path = "…/sdk/rust/openidea-plugin" }
[profile.release]
opt-level = "s"
@@ -773,8 +785,19 @@ granted, and starts the approved program.
- **Project plugins:** put (or symlink) the package in
`<project>/.openidea/plugins/<name>/`. They load only in trusted projects and
only after you approve their capabilities.
- **Where:** installing, removing, enabling, granting and reloading plugins
work on the welcome screen and in trusted projects. A project opened in
restricted mode can browse the marketplace and show a package's install
page, but not change which plugin code runs.
- Commands appear in Search Everywhere › Actions under their category, and can
be bound in Settings › Keymap like any other action.
- **Troubleshooting:** `openidea-plugin check` says why a package would be
refused. Settings › Plugins shows each plugin's state, why it failed if it
did, and its call stats (§3, _Sandbox and limits_). `host::log` lines go to
the IDE log (Help › _Show Log in Finder_ / _Explorer_ / _Files_) tagged with
the plugin id; debug and trace lines are written when the IDE is started
with `RUST_LOG=info,ide_plugin=trace`. `eprintln!` output is logged as
well, and a panic's message is shown with the crash.
## 8. Distribution