mirror of
https://github.com/nushell/nushell
synced 2024-12-27 05:23:11 +00:00
Overhaul the plugin cache file with a new msgpack+brotli format (#12579)
# Description - Plugin signatures are now saved to `plugin.msgpackz`, which is brotli-compressed MessagePack. - The file is updated incrementally, rather than writing all plugin commands in the engine every time. - The file always contains the result of the `Signature` call to the plugin, even if commands were removed. - Invalid data for a particular plugin just causes an error to be reported, but the rest of the plugins can still be parsed # User-Facing Changes - The plugin file has a different filename, and it's not a nushell script. - The default `plugin.nu` file will be automatically migrated the first time, but not other plugin config files. - We don't currently provide any utilities that could help edit this file, beyond `plugin add` and `plugin rm` - `from msgpackz`, `to msgpackz` could also help - New commands: `plugin add`, `plugin rm` # Tests + Formatting Tests added for the format and for the invalid handling. - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` # After Submitting - [ ] Check for documentation changes - [ ] Definitely needs release notes
This commit is contained in:
parent
6cba7c6b40
commit
2595f31541
45 changed files with 1462 additions and 211 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -520,7 +520,18 @@ checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
"brotli-decompressor",
|
"brotli-decompressor 2.5.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brotli"
|
||||||
|
version = "5.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67"
|
||||||
|
dependencies = [
|
||||||
|
"alloc-no-stdlib",
|
||||||
|
"alloc-stdlib",
|
||||||
|
"brotli-decompressor 4.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -533,6 +544,16 @@ dependencies = [
|
||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brotli-decompressor"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76"
|
||||||
|
dependencies = [
|
||||||
|
"alloc-no-stdlib",
|
||||||
|
"alloc-stdlib",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bstr"
|
name = "bstr"
|
||||||
version = "1.9.1"
|
version = "1.9.1"
|
||||||
|
@ -2861,6 +2882,7 @@ dependencies = [
|
||||||
"nu-cmd-dataframe",
|
"nu-cmd-dataframe",
|
||||||
"nu-cmd-extra",
|
"nu-cmd-extra",
|
||||||
"nu-cmd-lang",
|
"nu-cmd-lang",
|
||||||
|
"nu-cmd-plugin",
|
||||||
"nu-command",
|
"nu-command",
|
||||||
"nu-engine",
|
"nu-engine",
|
||||||
"nu-explore",
|
"nu-explore",
|
||||||
|
@ -2914,6 +2936,7 @@ dependencies = [
|
||||||
"nu-engine",
|
"nu-engine",
|
||||||
"nu-parser",
|
"nu-parser",
|
||||||
"nu-path",
|
"nu-path",
|
||||||
|
"nu-plugin",
|
||||||
"nu-protocol",
|
"nu-protocol",
|
||||||
"nu-test-support",
|
"nu-test-support",
|
||||||
"nu-utils",
|
"nu-utils",
|
||||||
|
@ -3000,6 +3023,17 @@ dependencies = [
|
||||||
"shadow-rs",
|
"shadow-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-cmd-plugin"
|
||||||
|
version = "0.92.3"
|
||||||
|
dependencies = [
|
||||||
|
"itertools 0.12.1",
|
||||||
|
"nu-engine",
|
||||||
|
"nu-path",
|
||||||
|
"nu-plugin",
|
||||||
|
"nu-protocol",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-color-config"
|
name = "nu-color-config"
|
||||||
version = "0.92.3"
|
version = "0.92.3"
|
||||||
|
@ -3262,6 +3296,7 @@ dependencies = [
|
||||||
name = "nu-protocol"
|
name = "nu-protocol"
|
||||||
version = "0.92.3"
|
version = "0.92.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"brotli 5.0.0",
|
||||||
"byte-unit",
|
"byte-unit",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-humanize",
|
"chrono-humanize",
|
||||||
|
@ -3274,6 +3309,8 @@ dependencies = [
|
||||||
"nu-test-support",
|
"nu-test-support",
|
||||||
"nu-utils",
|
"nu-utils",
|
||||||
"num-format",
|
"num-format",
|
||||||
|
"pretty_assertions",
|
||||||
|
"rmp-serde",
|
||||||
"rstest",
|
"rstest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -4282,7 +4319,7 @@ dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"brotli",
|
"brotli 3.5.0",
|
||||||
"ethnum",
|
"ethnum",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures",
|
"futures",
|
||||||
|
|
|
@ -31,6 +31,7 @@ members = [
|
||||||
"crates/nu-cmd-base",
|
"crates/nu-cmd-base",
|
||||||
"crates/nu-cmd-extra",
|
"crates/nu-cmd-extra",
|
||||||
"crates/nu-cmd-lang",
|
"crates/nu-cmd-lang",
|
||||||
|
"crates/nu-cmd-plugin",
|
||||||
"crates/nu-cmd-dataframe",
|
"crates/nu-cmd-dataframe",
|
||||||
"crates/nu-command",
|
"crates/nu-command",
|
||||||
"crates/nu-color-config",
|
"crates/nu-color-config",
|
||||||
|
@ -62,6 +63,7 @@ alphanumeric-sort = "1.5"
|
||||||
ansi-str = "0.8"
|
ansi-str = "0.8"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
bracoxide = "0.1.2"
|
bracoxide = "0.1.2"
|
||||||
|
brotli = "5.0"
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
bytesize = "1.3"
|
bytesize = "1.3"
|
||||||
calamine = "0.24.0"
|
calamine = "0.24.0"
|
||||||
|
@ -126,6 +128,7 @@ ratatui = "0.26"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
reedline = "0.31.0"
|
reedline = "0.31.0"
|
||||||
regex = "1.9.5"
|
regex = "1.9.5"
|
||||||
|
rmp-serde = "1.2"
|
||||||
ropey = "1.6.1"
|
ropey = "1.6.1"
|
||||||
roxmltree = "0.19"
|
roxmltree = "0.19"
|
||||||
rstest = { version = "0.18", default-features = false }
|
rstest = { version = "0.18", default-features = false }
|
||||||
|
@ -168,6 +171,7 @@ winreg = "0.52"
|
||||||
nu-cli = { path = "./crates/nu-cli", version = "0.92.3" }
|
nu-cli = { path = "./crates/nu-cli", version = "0.92.3" }
|
||||||
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.92.3" }
|
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.92.3" }
|
||||||
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.92.3" }
|
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.92.3" }
|
||||||
|
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.92.3", optional = true }
|
||||||
nu-cmd-dataframe = { path = "./crates/nu-cmd-dataframe", version = "0.92.3", features = [
|
nu-cmd-dataframe = { path = "./crates/nu-cmd-dataframe", version = "0.92.3", features = [
|
||||||
"dataframe",
|
"dataframe",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
@ -223,6 +227,7 @@ tempfile = { workspace = true }
|
||||||
[features]
|
[features]
|
||||||
plugin = [
|
plugin = [
|
||||||
"nu-plugin",
|
"nu-plugin",
|
||||||
|
"nu-cmd-plugin",
|
||||||
"nu-cli/plugin",
|
"nu-cli/plugin",
|
||||||
"nu-parser/plugin",
|
"nu-parser/plugin",
|
||||||
"nu-command/plugin",
|
"nu-command/plugin",
|
||||||
|
|
|
@ -21,6 +21,7 @@ nu-cmd-base = { path = "../nu-cmd-base", version = "0.92.3" }
|
||||||
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
||||||
nu-path = { path = "../nu-path", version = "0.92.3" }
|
nu-path = { path = "../nu-path", version = "0.92.3" }
|
||||||
nu-parser = { path = "../nu-parser", version = "0.92.3" }
|
nu-parser = { path = "../nu-parser", version = "0.92.3" }
|
||||||
|
nu-plugin = { path = "../nu-plugin", version = "0.92.3", optional = true }
|
||||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
|
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.92.3" }
|
nu-utils = { path = "../nu-utils", version = "0.92.3" }
|
||||||
nu-color-config = { path = "../nu-color-config", version = "0.92.3" }
|
nu-color-config = { path = "../nu-color-config", version = "0.92.3" }
|
||||||
|
@ -44,5 +45,5 @@ uuid = { workspace = true, features = ["v4"] }
|
||||||
which = { workspace = true }
|
which = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
plugin = []
|
plugin = ["nu-plugin"]
|
||||||
system-clipboard = ["reedline/system_clipboard"]
|
system-clipboard = ["reedline/system_clipboard"]
|
||||||
|
|
|
@ -6,13 +6,15 @@ use nu_protocol::{
|
||||||
report_error, HistoryFileFormat, PipelineData,
|
report_error, HistoryFileFormat, PipelineData,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
use nu_protocol::{ParseError, Spanned};
|
use nu_protocol::{ParseError, PluginCacheFile, Spanned};
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
use nu_utils::utils::perf;
|
use nu_utils::utils::perf;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
const PLUGIN_FILE: &str = "plugin.nu";
|
const PLUGIN_FILE: &str = "plugin.msgpackz";
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
const OLD_PLUGIN_FILE: &str = "plugin.nu";
|
||||||
|
|
||||||
const HISTORY_FILE_TXT: &str = "history.txt";
|
const HISTORY_FILE_TXT: &str = "history.txt";
|
||||||
const HISTORY_FILE_SQLITE: &str = "history.sqlite3";
|
const HISTORY_FILE_SQLITE: &str = "history.sqlite3";
|
||||||
|
@ -20,14 +22,38 @@ const HISTORY_FILE_SQLITE: &str = "history.sqlite3";
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub fn read_plugin_file(
|
pub fn read_plugin_file(
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
stack: &mut Stack,
|
|
||||||
plugin_file: Option<Spanned<String>>,
|
plugin_file: Option<Spanned<String>>,
|
||||||
storage_path: &str,
|
storage_path: &str,
|
||||||
) {
|
) {
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use nu_protocol::{report_error_new, ShellError};
|
||||||
|
|
||||||
|
let span = plugin_file.as_ref().map(|s| s.span);
|
||||||
|
|
||||||
|
// Check and warn + abort if this is a .nu plugin file
|
||||||
|
if plugin_file
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| Path::new(&p.item).extension())
|
||||||
|
.is_some_and(|ext| ext == "nu")
|
||||||
|
{
|
||||||
|
report_error_new(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: "Wrong plugin file format".into(),
|
||||||
|
msg: ".nu plugin files are no longer supported".into(),
|
||||||
|
span,
|
||||||
|
help: Some("please recreate this file in the new .msgpackz format".into()),
|
||||||
|
inner: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut start_time = std::time::Instant::now();
|
let mut start_time = std::time::Instant::now();
|
||||||
// Reading signatures from signature file
|
// Reading signatures from plugin cache file
|
||||||
// The plugin.nu file stores the parsed signature collected from each registered plugin
|
// The plugin.msgpackz file stores the parsed signature collected from each registered plugin
|
||||||
add_plugin_file(engine_state, plugin_file, storage_path);
|
add_plugin_file(engine_state, plugin_file.clone(), storage_path);
|
||||||
perf(
|
perf(
|
||||||
"add plugin file to engine_state",
|
"add plugin file to engine_state",
|
||||||
start_time,
|
start_time,
|
||||||
|
@ -38,14 +64,82 @@ pub fn read_plugin_file(
|
||||||
);
|
);
|
||||||
|
|
||||||
start_time = std::time::Instant::now();
|
start_time = std::time::Instant::now();
|
||||||
let plugin_path = engine_state.plugin_signatures.clone();
|
let plugin_path = engine_state.plugin_path.clone();
|
||||||
if let Some(plugin_path) = plugin_path {
|
if let Some(plugin_path) = plugin_path {
|
||||||
let plugin_filename = plugin_path.to_string_lossy();
|
// Open the plugin file
|
||||||
let plug_path = plugin_filename.to_string();
|
let mut file = match std::fs::File::open(&plugin_path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(err) => {
|
||||||
|
if err.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
log::warn!("Plugin file not found: {}", plugin_path.display());
|
||||||
|
|
||||||
|
// Try migration of an old plugin file if this wasn't a custom plugin file
|
||||||
|
if plugin_file.is_none() && migrate_old_plugin_file(engine_state, storage_path)
|
||||||
|
{
|
||||||
|
let Ok(file) = std::fs::File::open(&plugin_path) else {
|
||||||
|
log::warn!("Failed to load newly migrated plugin file");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
file
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
report_error_new(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: format!(
|
||||||
|
"Error while opening plugin cache file: {}",
|
||||||
|
plugin_path.display()
|
||||||
|
),
|
||||||
|
msg: "plugin path defined here".into(),
|
||||||
|
span,
|
||||||
|
help: None,
|
||||||
|
inner: vec![err.into()],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Abort if the file is empty.
|
||||||
|
if file.metadata().is_ok_and(|m| m.len() == 0) {
|
||||||
|
log::warn!(
|
||||||
|
"Not reading plugin file because it's empty: {}",
|
||||||
|
plugin_path.display()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the contents of the plugin file
|
||||||
|
let contents = match PluginCacheFile::read_from(&mut file, span) {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("Failed to read plugin cache file: {err:?}");
|
||||||
|
report_error_new(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: format!(
|
||||||
|
"Error while reading plugin cache file: {}",
|
||||||
|
plugin_path.display()
|
||||||
|
),
|
||||||
|
msg: "plugin path defined here".into(),
|
||||||
|
span,
|
||||||
|
help: Some(
|
||||||
|
"you might try deleting the file and registering all of your \
|
||||||
|
plugins again"
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
inner: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Ok(contents) = std::fs::read(&plugin_path) {
|
|
||||||
perf(
|
perf(
|
||||||
&format!("read plugin file {}", &plug_path),
|
&format!("read plugin file {}", plugin_path.display()),
|
||||||
start_time,
|
start_time,
|
||||||
file!(),
|
file!(),
|
||||||
line!(),
|
line!(),
|
||||||
|
@ -53,16 +147,18 @@ pub fn read_plugin_file(
|
||||||
engine_state.get_config().use_ansi_coloring,
|
engine_state.get_config().use_ansi_coloring,
|
||||||
);
|
);
|
||||||
start_time = std::time::Instant::now();
|
start_time = std::time::Instant::now();
|
||||||
eval_source(
|
|
||||||
engine_state,
|
let mut working_set = StateWorkingSet::new(engine_state);
|
||||||
stack,
|
|
||||||
&contents,
|
nu_plugin::load_plugin_file(&mut working_set, &contents, span);
|
||||||
&plugin_filename,
|
|
||||||
PipelineData::empty(),
|
if let Err(err) = engine_state.merge_delta(working_set.render()) {
|
||||||
false,
|
report_error_new(engine_state, &err);
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
perf(
|
perf(
|
||||||
&format!("eval_source plugin file {}", &plug_path),
|
&format!("load plugin file {}", plugin_path.display()),
|
||||||
start_time,
|
start_time,
|
||||||
file!(),
|
file!(),
|
||||||
line!(),
|
line!(),
|
||||||
|
@ -70,7 +166,6 @@ pub fn read_plugin_file(
|
||||||
engine_state.get_config().use_ansi_coloring,
|
engine_state.get_config().use_ansi_coloring,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
|
@ -79,15 +174,30 @@ pub fn add_plugin_file(
|
||||||
plugin_file: Option<Spanned<String>>,
|
plugin_file: Option<Spanned<String>>,
|
||||||
storage_path: &str,
|
storage_path: &str,
|
||||||
) {
|
) {
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
let working_set = StateWorkingSet::new(engine_state);
|
let working_set = StateWorkingSet::new(engine_state);
|
||||||
let cwd = working_set.get_cwd();
|
let cwd = working_set.get_cwd();
|
||||||
|
|
||||||
if let Some(plugin_file) = plugin_file {
|
if let Some(plugin_file) = plugin_file {
|
||||||
if let Ok(path) = canonicalize_with(&plugin_file.item, cwd) {
|
let path = Path::new(&plugin_file.item);
|
||||||
engine_state.plugin_signatures = Some(path)
|
let path_dir = path.parent().unwrap_or(path);
|
||||||
|
// Just try to canonicalize the directory of the plugin file first.
|
||||||
|
if let Ok(path_dir) = canonicalize_with(path_dir, &cwd) {
|
||||||
|
// Try to canonicalize the actual filename, but it's ok if that fails. The file doesn't
|
||||||
|
// have to exist.
|
||||||
|
let path = path_dir.join(path.file_name().unwrap_or(path.as_os_str()));
|
||||||
|
let path = canonicalize_with(&path, &cwd).unwrap_or(path);
|
||||||
|
engine_state.plugin_path = Some(path)
|
||||||
} else {
|
} else {
|
||||||
let e = ParseError::FileNotFound(plugin_file.item, plugin_file.span);
|
// It's an error if the directory for the plugin file doesn't exist.
|
||||||
report_error(&working_set, &e);
|
report_error(
|
||||||
|
&working_set,
|
||||||
|
&ParseError::FileNotFound(
|
||||||
|
path_dir.to_string_lossy().into_owned(),
|
||||||
|
plugin_file.span,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if let Some(mut plugin_path) = nu_path::config_dir() {
|
} else if let Some(mut plugin_path) = nu_path::config_dir() {
|
||||||
// Path to store plugins signatures
|
// Path to store plugins signatures
|
||||||
|
@ -95,7 +205,7 @@ pub fn add_plugin_file(
|
||||||
let mut plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
|
let mut plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
|
||||||
plugin_path.push(PLUGIN_FILE);
|
plugin_path.push(PLUGIN_FILE);
|
||||||
let plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
|
let plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
|
||||||
engine_state.plugin_signatures = Some(plugin_path);
|
engine_state.plugin_path = Some(plugin_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,3 +261,129 @@ pub(crate) fn get_history_path(storage_path: &str, mode: HistoryFileFormat) -> O
|
||||||
history_path
|
history_path
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -> bool {
|
||||||
|
use nu_protocol::{
|
||||||
|
report_error_new, PluginCacheItem, PluginCacheItemData, PluginExample, PluginIdentity,
|
||||||
|
PluginSignature, ShellError,
|
||||||
|
};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
|
let cwd = engine_state.current_work_dir();
|
||||||
|
|
||||||
|
let Some(config_dir) = nu_path::config_dir().and_then(|mut dir| {
|
||||||
|
dir.push(storage_path);
|
||||||
|
nu_path::canonicalize_with(dir, &cwd).ok()
|
||||||
|
}) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(old_plugin_file_path) = nu_path::canonicalize_with(OLD_PLUGIN_FILE, &config_dir) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let old_contents = match std::fs::read(&old_plugin_file_path) {
|
||||||
|
Ok(old_contents) => old_contents,
|
||||||
|
Err(err) => {
|
||||||
|
report_error_new(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: "Can't read old plugin file to migrate".into(),
|
||||||
|
msg: "".into(),
|
||||||
|
span: None,
|
||||||
|
help: Some(err.to_string()),
|
||||||
|
inner: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make a copy of the engine state, because we'll read the newly generated file
|
||||||
|
let mut engine_state = engine_state.clone();
|
||||||
|
let mut stack = Stack::new();
|
||||||
|
|
||||||
|
if !eval_source(
|
||||||
|
&mut engine_state,
|
||||||
|
&mut stack,
|
||||||
|
&old_contents,
|
||||||
|
&old_plugin_file_path.to_string_lossy(),
|
||||||
|
PipelineData::Empty,
|
||||||
|
false,
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the plugin commands are loaded, we just have to generate the file
|
||||||
|
let mut contents = PluginCacheFile::new();
|
||||||
|
|
||||||
|
let mut groups = BTreeMap::<PluginIdentity, Vec<PluginSignature>>::new();
|
||||||
|
|
||||||
|
for decl in engine_state.plugin_decls() {
|
||||||
|
if let Some(identity) = decl.plugin_identity() {
|
||||||
|
groups
|
||||||
|
.entry(identity.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(PluginSignature {
|
||||||
|
sig: decl.signature(),
|
||||||
|
examples: decl
|
||||||
|
.examples()
|
||||||
|
.into_iter()
|
||||||
|
.map(PluginExample::from)
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (identity, commands) in groups {
|
||||||
|
contents.upsert_plugin(PluginCacheItem {
|
||||||
|
name: identity.name().to_owned(),
|
||||||
|
filename: identity.filename().to_owned(),
|
||||||
|
shell: identity.shell().map(|p| p.to_owned()),
|
||||||
|
data: PluginCacheItemData::Valid { commands },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the new file
|
||||||
|
let new_plugin_file_path = config_dir.join(PLUGIN_FILE);
|
||||||
|
if let Err(err) = std::fs::File::create(&new_plugin_file_path)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
.and_then(|file| contents.write_to(file, None))
|
||||||
|
{
|
||||||
|
report_error_new(
|
||||||
|
&engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: "Failed to save migrated plugin file".into(),
|
||||||
|
msg: "".into(),
|
||||||
|
span: None,
|
||||||
|
help: Some("ensure `$nu.plugin-path` is writable".into()),
|
||||||
|
inner: vec![err],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if engine_state.is_interactive {
|
||||||
|
eprintln!(
|
||||||
|
"Your old plugin.nu file has been migrated to the new format: {}",
|
||||||
|
new_plugin_file_path.display()
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"The plugin.nu file has not been removed. If `plugin list` looks okay, \
|
||||||
|
you may do so manually."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
perf(
|
||||||
|
"migrate old plugin file",
|
||||||
|
start_time,
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
column!(),
|
||||||
|
engine_state.get_config().use_ansi_coloring,
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
|
@ -32,4 +32,6 @@ pub use validation::NuValidator;
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub use config_files::add_plugin_file;
|
pub use config_files::add_plugin_file;
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
|
pub use config_files::migrate_old_plugin_file;
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
pub use config_files::read_plugin_file;
|
pub use config_files::read_plugin_file;
|
||||||
|
|
|
@ -71,13 +71,3 @@ pub use try_::Try;
|
||||||
pub use use_::Use;
|
pub use use_::Use;
|
||||||
pub use version::Version;
|
pub use version::Version;
|
||||||
pub use while_::While;
|
pub use while_::While;
|
||||||
|
|
||||||
mod plugin;
|
|
||||||
mod plugin_list;
|
|
||||||
mod plugin_stop;
|
|
||||||
mod register;
|
|
||||||
|
|
||||||
pub use plugin::PluginCommand;
|
|
||||||
pub use plugin_list::PluginList;
|
|
||||||
pub use plugin_stop::PluginStop;
|
|
||||||
pub use register::Register;
|
|
||||||
|
|
|
@ -63,9 +63,6 @@ pub fn create_default_context() -> EngineState {
|
||||||
While,
|
While,
|
||||||
};
|
};
|
||||||
|
|
||||||
//#[cfg(feature = "plugin")]
|
|
||||||
bind_command!(PluginCommand, PluginList, PluginStop, Register,);
|
|
||||||
|
|
||||||
working_set.render()
|
working_set.render()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
20
crates/nu-cmd-plugin/Cargo.toml
Normal file
20
crates/nu-cmd-plugin/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
authors = ["The Nushell Project Developers"]
|
||||||
|
description = "Commands for managing Nushell plugins."
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
name = "nu-cmd-plugin"
|
||||||
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
|
||||||
|
version = "0.92.3"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
||||||
|
nu-path = { path = "../nu-path", version = "0.92.3" }
|
||||||
|
nu-protocol = { path = "../nu-protocol", version = "0.92.3", features = ["plugin"] }
|
||||||
|
nu-plugin = { path = "../nu-plugin", version = "0.92.3" }
|
||||||
|
|
||||||
|
itertools = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
21
crates/nu-cmd-plugin/LICENSE
Normal file
21
crates/nu-cmd-plugin/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 - 2023 The Nushell Project Developers
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
3
crates/nu-cmd-plugin/README.md
Normal file
3
crates/nu-cmd-plugin/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# nu-cmd-plugin
|
||||||
|
|
||||||
|
This crate implements Nushell commands related to plugin management.
|
5
crates/nu-cmd-plugin/src/commands/mod.rs
Normal file
5
crates/nu-cmd-plugin/src/commands/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod plugin;
|
||||||
|
mod register;
|
||||||
|
|
||||||
|
pub use plugin::*;
|
||||||
|
pub use register::Register;
|
148
crates/nu-cmd-plugin/src/commands/plugin/add.rs
Normal file
148
crates/nu-cmd-plugin/src/commands/plugin/add.rs
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use nu_engine::{command_prelude::*, current_dir};
|
||||||
|
use nu_plugin::{GetPlugin, PersistentPlugin};
|
||||||
|
use nu_protocol::{PluginCacheItem, PluginGcConfig, PluginIdentity, RegisteredPlugin};
|
||||||
|
|
||||||
|
use crate::util::modify_plugin_file;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PluginAdd;
|
||||||
|
|
||||||
|
impl Command for PluginAdd {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"plugin add"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build(self.name())
|
||||||
|
.input_output_type(Type::Nothing, Type::Nothing)
|
||||||
|
// This matches the option to `nu`
|
||||||
|
.named(
|
||||||
|
"plugin-config",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"Use a plugin cache file other than the one set in `$nu.plugin-path`",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.named(
|
||||||
|
"shell",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"Use an additional shell program (cmd, sh, python, etc.) to run the plugin",
|
||||||
|
Some('s'),
|
||||||
|
)
|
||||||
|
.required(
|
||||||
|
"filename",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"Path to the executable for the plugin",
|
||||||
|
)
|
||||||
|
.category(Category::Plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"Add a plugin to the plugin cache file."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_usage(&self) -> &str {
|
||||||
|
r#"
|
||||||
|
This does not load the plugin commands into the scope - see `register` for that.
|
||||||
|
|
||||||
|
Instead, it runs the plugin to get its command signatures, and then edits the
|
||||||
|
plugin cache file (by default, `$nu.plugin-path`). The changes will be
|
||||||
|
apparent the next time `nu` is next launched with that plugin cache file.
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["plugin", "add", "register", "load", "signature"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
example: "plugin add nu_plugin_inc",
|
||||||
|
description: "Run the `nu_plugin_inc` plugin from the current directory or $env.NU_PLUGIN_DIRS and install its signatures.",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "plugin add --plugin-config polars.msgpackz nu_plugin_polars",
|
||||||
|
description: "Run the `nu_plugin_polars` plugin from the current directory or $env.NU_PLUGIN_DIRS, and install its signatures to the \"polars.msgpackz\" plugin cache file.",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let filename: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||||
|
let shell: Option<Spanned<String>> = call.get_flag(engine_state, stack, "shell")?;
|
||||||
|
|
||||||
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
|
|
||||||
|
// Check the current directory, or fall back to NU_PLUGIN_DIRS
|
||||||
|
let filename_expanded = match nu_path::canonicalize_with(&filename.item, &cwd) {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(err) => {
|
||||||
|
// Try to find it in NU_PLUGIN_DIRS first, before giving up
|
||||||
|
let mut found = None;
|
||||||
|
if let Some(nu_plugin_dirs) = stack.get_env_var(engine_state, "NU_PLUGIN_DIRS") {
|
||||||
|
for dir in nu_plugin_dirs.into_list().unwrap_or(vec![]) {
|
||||||
|
if let Ok(path) = nu_path::canonicalize_with(dir.as_str()?, &cwd)
|
||||||
|
.and_then(|dir| nu_path::canonicalize_with(&filename.item, dir))
|
||||||
|
{
|
||||||
|
found = Some(path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found.ok_or(err.into_spanned(filename.span))?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let shell_expanded = shell
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| {
|
||||||
|
nu_path::canonicalize_with(&s.item, &cwd).map_err(|err| err.into_spanned(s.span))
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
// Parse the plugin filename so it can be used to spawn the plugin
|
||||||
|
let identity = PluginIdentity::new(filename_expanded, shell_expanded).map_err(|_| {
|
||||||
|
ShellError::GenericError {
|
||||||
|
error: "Plugin filename is invalid".into(),
|
||||||
|
msg: "plugin executable files must start with `nu_plugin_`".into(),
|
||||||
|
span: Some(filename.span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
|
||||||
|
|
||||||
|
// Start the plugin manually, to get the freshest signatures and to not affect engine
|
||||||
|
// state. Provide a GC config that will stop it ASAP
|
||||||
|
let plugin = Arc::new(PersistentPlugin::new(
|
||||||
|
identity,
|
||||||
|
PluginGcConfig {
|
||||||
|
enabled: true,
|
||||||
|
stop_after: 0,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?;
|
||||||
|
let commands = interface.get_signature()?;
|
||||||
|
|
||||||
|
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
|
||||||
|
// Update the file with the received signatures
|
||||||
|
let item = PluginCacheItem::new(plugin.identity(), commands);
|
||||||
|
contents.upsert_plugin(item);
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Value::nothing(call.head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ impl Command for PluginList {
|
||||||
("commands".into(), Type::List(Type::String.into())),
|
("commands".into(), Type::List(Type::String.into())),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.category(Category::Core)
|
.category(Category::Plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usage(&self) -> &str {
|
fn usage(&self) -> &str {
|
|
@ -1,5 +1,15 @@
|
||||||
use nu_engine::{command_prelude::*, get_full_help};
|
use nu_engine::{command_prelude::*, get_full_help};
|
||||||
|
|
||||||
|
mod add;
|
||||||
|
mod list;
|
||||||
|
mod rm;
|
||||||
|
mod stop;
|
||||||
|
|
||||||
|
pub use add::PluginAdd;
|
||||||
|
pub use list::PluginList;
|
||||||
|
pub use rm::PluginRm;
|
||||||
|
pub use stop::PluginStop;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PluginCommand;
|
pub struct PluginCommand;
|
||||||
|
|
||||||
|
@ -11,7 +21,7 @@ impl Command for PluginCommand {
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("plugin")
|
Signature::build("plugin")
|
||||||
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||||
.category(Category::Core)
|
.category(Category::Plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usage(&self) -> &str {
|
fn usage(&self) -> &str {
|
||||||
|
@ -54,6 +64,16 @@ impl Command for PluginCommand {
|
||||||
description: "Stop the plugin named `inc`.",
|
description: "Stop the plugin named `inc`.",
|
||||||
result: None,
|
result: None,
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
example: "plugin add nu_plugin_inc",
|
||||||
|
description: "Run the `nu_plugin_inc` plugin from the current directory and install its signatures.",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "plugin rm inc",
|
||||||
|
description: "Remove the installed signatures for the `inc` plugin.",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
100
crates/nu-cmd-plugin/src/commands/plugin/rm.rs
Normal file
100
crates/nu-cmd-plugin/src/commands/plugin/rm.rs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
|
use crate::util::modify_plugin_file;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PluginRm;
|
||||||
|
|
||||||
|
impl Command for PluginRm {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"plugin rm"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build(self.name())
|
||||||
|
.input_output_type(Type::Nothing, Type::Nothing)
|
||||||
|
// This matches the option to `nu`
|
||||||
|
.named(
|
||||||
|
"plugin-config",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"Use a plugin cache file other than the one set in `$nu.plugin-path`",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"force",
|
||||||
|
"Don't cause an error if the plugin name wasn't found in the file",
|
||||||
|
Some('f'),
|
||||||
|
)
|
||||||
|
.required(
|
||||||
|
"name",
|
||||||
|
SyntaxShape::String,
|
||||||
|
"The name of the plugin to remove (not the filename)",
|
||||||
|
)
|
||||||
|
.category(Category::Plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"Remove a plugin from the plugin cache file."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_usage(&self) -> &str {
|
||||||
|
r#"
|
||||||
|
This does not remove the plugin commands from the current scope or from `plugin
|
||||||
|
list` in the current shell. It instead removes the plugin from the plugin
|
||||||
|
cache file (by default, `$nu.plugin-path`). The changes will be apparent the
|
||||||
|
next time `nu` is launched with that plugin cache file.
|
||||||
|
|
||||||
|
This can be useful for removing an invalid plugin signature, if it can't be
|
||||||
|
fixed with `plugin add`.
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["plugin", "rm", "remove", "delete", "signature"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
example: "plugin rm inc",
|
||||||
|
description: "Remove the installed signatures for the `inc` plugin.",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "plugin rm --plugin-config polars.msgpackz polars",
|
||||||
|
description: "Remove the installed signatures for the `polars` plugin from the \"polars.msgpackz\" plugin cache file.",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let name: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||||
|
let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
|
||||||
|
let force = call.has_flag(engine_state, stack, "force")?;
|
||||||
|
|
||||||
|
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
|
||||||
|
if !force && !contents.plugins.iter().any(|p| p.name == name.item) {
|
||||||
|
Err(ShellError::GenericError {
|
||||||
|
error: format!("Failed to remove the `{}` plugin", name.item),
|
||||||
|
msg: "couldn't find a plugin with this name in the cache file".into(),
|
||||||
|
span: Some(name.span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
contents.remove_plugin(&name.item);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Value::nothing(call.head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ impl Command for PluginStop {
|
||||||
SyntaxShape::String,
|
SyntaxShape::String,
|
||||||
"The name of the plugin to stop.",
|
"The name of the plugin to stop.",
|
||||||
)
|
)
|
||||||
.category(Category::Core)
|
.category(Category::Plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usage(&self) -> &str {
|
fn usage(&self) -> &str {
|
|
@ -31,7 +31,7 @@ impl Command for Register {
|
||||||
"path of shell used to run plugin (cmd, sh, python, etc)",
|
"path of shell used to run plugin (cmd, sh, python, etc)",
|
||||||
Some('s'),
|
Some('s'),
|
||||||
)
|
)
|
||||||
.category(Category::Core)
|
.category(Category::Plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extra_usage(&self) -> &str {
|
fn extra_usage(&self) -> &str {
|
||||||
|
@ -39,6 +39,10 @@ impl Command for Register {
|
||||||
https://www.nushell.sh/book/thinking_in_nu.html"#
|
https://www.nushell.sh/book/thinking_in_nu.html"#
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["plugin", "add", "register"]
|
||||||
|
}
|
||||||
|
|
||||||
fn is_parser_keyword(&self) -> bool {
|
fn is_parser_keyword(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
31
crates/nu-cmd-plugin/src/default_context.rs
Normal file
31
crates/nu-cmd-plugin/src/default_context.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use crate::*;
|
||||||
|
use nu_protocol::engine::{EngineState, StateWorkingSet};
|
||||||
|
|
||||||
|
pub fn add_plugin_command_context(mut engine_state: EngineState) -> EngineState {
|
||||||
|
let delta = {
|
||||||
|
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||||
|
|
||||||
|
macro_rules! bind_command {
|
||||||
|
( $( $command:expr ),* $(,)? ) => {
|
||||||
|
$( working_set.add_decl(Box::new($command)); )*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_command!(
|
||||||
|
PluginCommand,
|
||||||
|
PluginAdd,
|
||||||
|
PluginList,
|
||||||
|
PluginRm,
|
||||||
|
PluginStop,
|
||||||
|
Register,
|
||||||
|
);
|
||||||
|
|
||||||
|
working_set.render()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = engine_state.merge_delta(delta) {
|
||||||
|
eprintln!("Error creating default context: {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
engine_state
|
||||||
|
}
|
8
crates/nu-cmd-plugin/src/lib.rs
Normal file
8
crates/nu-cmd-plugin/src/lib.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
//! Nushell commands for managing plugins.
|
||||||
|
|
||||||
|
mod commands;
|
||||||
|
mod default_context;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
pub use commands::*;
|
||||||
|
pub use default_context::*;
|
50
crates/nu-cmd-plugin/src/util.rs
Normal file
50
crates/nu-cmd-plugin/src/util.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use std::fs::{self, File};
|
||||||
|
|
||||||
|
use nu_engine::{command_prelude::*, current_dir};
|
||||||
|
use nu_protocol::PluginCacheFile;
|
||||||
|
|
||||||
|
pub(crate) fn modify_plugin_file(
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
span: Span,
|
||||||
|
custom_path: Option<Spanned<String>>,
|
||||||
|
operate: impl FnOnce(&mut PluginCacheFile) -> Result<(), ShellError>,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
let cwd = current_dir(engine_state, stack)?;
|
||||||
|
|
||||||
|
let plugin_cache_file_path = if let Some(ref custom_path) = custom_path {
|
||||||
|
nu_path::expand_path_with(&custom_path.item, cwd, true)
|
||||||
|
} else {
|
||||||
|
engine_state
|
||||||
|
.plugin_path
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| ShellError::GenericError {
|
||||||
|
error: "Plugin cache file not set".into(),
|
||||||
|
msg: "pass --plugin-config explicitly here".into(),
|
||||||
|
span: Some(span),
|
||||||
|
help: Some("you may be running `nu` with --no-config-file".into()),
|
||||||
|
inner: vec![],
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to read the plugin file if it exists
|
||||||
|
let mut contents = if fs::metadata(&plugin_cache_file_path).is_ok_and(|m| m.len() > 0) {
|
||||||
|
PluginCacheFile::read_from(
|
||||||
|
File::open(&plugin_cache_file_path).map_err(|err| err.into_spanned(span))?,
|
||||||
|
Some(span),
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
PluginCacheFile::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Do the operation
|
||||||
|
operate(&mut contents)?;
|
||||||
|
|
||||||
|
// Save the modified file on success
|
||||||
|
contents.write_to(
|
||||||
|
File::create(&plugin_cache_file_path).map_err(|err| err.into_spanned(span))?,
|
||||||
|
Some(span),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -3550,7 +3550,8 @@ pub fn parse_where(working_set: &mut StateWorkingSet, lite_command: &LiteCommand
|
||||||
pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline {
|
pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline {
|
||||||
use nu_plugin::{get_signature, PersistentPlugin, PluginDeclaration};
|
use nu_plugin::{get_signature, PersistentPlugin, PluginDeclaration};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::Stack, IntoSpanned, PluginIdentity, PluginSignature, RegisteredPlugin,
|
engine::Stack, IntoSpanned, PluginCacheItem, PluginIdentity, PluginSignature,
|
||||||
|
RegisteredPlugin,
|
||||||
};
|
};
|
||||||
|
|
||||||
let spans = &lite_command.parts;
|
let spans = &lite_command.parts;
|
||||||
|
@ -3743,10 +3744,10 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
if signatures.is_ok() {
|
if let Ok(ref signatures) = signatures {
|
||||||
// mark plugins file as dirty only when the user is registering plugins
|
// Add the loaded plugin to the delta
|
||||||
// and not when we evaluate plugin.nu on shell startup
|
working_set
|
||||||
working_set.mark_plugins_file_dirty();
|
.update_plugin_cache(PluginCacheItem::new(&identity, signatures.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
signatures
|
signatures
|
||||||
|
|
|
@ -78,10 +78,10 @@ pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer};
|
||||||
// Used by other nu crates.
|
// Used by other nu crates.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use plugin::{
|
pub use plugin::{
|
||||||
create_plugin_signature, get_signature, serve_plugin_io, EngineInterfaceManager, GetPlugin,
|
create_plugin_signature, get_signature, load_plugin_cache_item, load_plugin_file,
|
||||||
Interface, InterfaceManager, PersistentPlugin, PluginDeclaration,
|
serve_plugin_io, EngineInterfaceManager, GetPlugin, Interface, InterfaceManager,
|
||||||
PluginExecutionCommandContext, PluginExecutionContext, PluginInterface, PluginInterfaceManager,
|
PersistentPlugin, PluginDeclaration, PluginExecutionCommandContext, PluginExecutionContext,
|
||||||
PluginSource, ServePluginError,
|
PluginInterface, PluginInterfaceManager, PluginSource, ServePluginError,
|
||||||
};
|
};
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use protocol::{PluginCustomValue, PluginInput, PluginOutput};
|
pub use protocol::{PluginCustomValue, PluginInput, PluginOutput};
|
||||||
|
|
|
@ -750,15 +750,7 @@ impl PluginInterface {
|
||||||
help: Some(format!(
|
help: Some(format!(
|
||||||
"the plugin may have experienced an error. Try registering the plugin again \
|
"the plugin may have experienced an error. Try registering the plugin again \
|
||||||
with `{}`",
|
with `{}`",
|
||||||
if let Some(shell) = self.state.source.shell() {
|
self.state.source.identity.register_command(),
|
||||||
format!(
|
|
||||||
"register --shell '{}' '{}'",
|
|
||||||
shell.display(),
|
|
||||||
self.state.source.filename().display(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("register '{}'", self.state.source.filename().display())
|
|
||||||
}
|
|
||||||
)),
|
)),
|
||||||
inner: vec![],
|
inner: vec![],
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -23,8 +23,9 @@ use std::{
|
||||||
|
|
||||||
use nu_engine::documentation::get_flags_section;
|
use nu_engine::documentation::get_flags_section;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginSignature,
|
ast::Operator, engine::StateWorkingSet, report_error_new, CustomValue, IntoSpanned,
|
||||||
ShellError, Spanned, Value,
|
LabeledError, PipelineData, PluginCacheFile, PluginCacheItem, PluginCacheItemData,
|
||||||
|
PluginIdentity, PluginSignature, ShellError, Span, Spanned, Value,
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -919,3 +920,79 @@ pub fn get_plugin_encoding(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load the definitions from the plugin file into the engine state
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn load_plugin_file(
|
||||||
|
working_set: &mut StateWorkingSet,
|
||||||
|
plugin_cache_file: &PluginCacheFile,
|
||||||
|
span: Option<Span>,
|
||||||
|
) {
|
||||||
|
for plugin in &plugin_cache_file.plugins {
|
||||||
|
// Any errors encountered should just be logged.
|
||||||
|
if let Err(err) = load_plugin_cache_item(working_set, plugin, span) {
|
||||||
|
report_error_new(working_set.permanent_state, &err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a definition from the plugin file into the engine state
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn load_plugin_cache_item(
|
||||||
|
working_set: &mut StateWorkingSet,
|
||||||
|
plugin: &PluginCacheItem,
|
||||||
|
span: Option<Span>,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
let identity =
|
||||||
|
PluginIdentity::new(plugin.filename.clone(), plugin.shell.clone()).map_err(|_| {
|
||||||
|
ShellError::GenericError {
|
||||||
|
error: "Invalid plugin filename in plugin cache file".into(),
|
||||||
|
msg: "loaded from here".into(),
|
||||||
|
span,
|
||||||
|
help: Some(format!(
|
||||||
|
"the filename for `{}` is not a valid nushell plugin: {}",
|
||||||
|
plugin.name,
|
||||||
|
plugin.filename.display()
|
||||||
|
)),
|
||||||
|
inner: vec![],
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match &plugin.data {
|
||||||
|
PluginCacheItemData::Valid { commands } => {
|
||||||
|
// Find garbage collection config for the plugin
|
||||||
|
let gc_config = working_set
|
||||||
|
.get_config()
|
||||||
|
.plugin_gc
|
||||||
|
.get(identity.name())
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// Add it to / get it from the working set
|
||||||
|
let plugin = working_set.find_or_create_plugin(&identity, || {
|
||||||
|
Arc::new(PersistentPlugin::new(identity.clone(), gc_config.clone()))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Downcast the plugin to `PersistentPlugin` - we generally expect this to succeed.
|
||||||
|
// The trait object only exists so that nu-protocol can contain plugins without knowing
|
||||||
|
// anything about their implementation, but we only use `PersistentPlugin` in practice.
|
||||||
|
let plugin: Arc<PersistentPlugin> =
|
||||||
|
plugin
|
||||||
|
.as_any()
|
||||||
|
.downcast()
|
||||||
|
.map_err(|_| ShellError::NushellFailed {
|
||||||
|
msg: "encountered unexpected RegisteredPlugin type".into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Create the declarations from the commands
|
||||||
|
for signature in commands {
|
||||||
|
let decl = PluginDeclaration::new(plugin.clone(), signature.clone());
|
||||||
|
working_set.add_decl(Box::new(decl));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
PluginCacheItemData::Invalid => Err(ShellError::PluginCacheDataInvalid {
|
||||||
|
plugin_name: identity.name().to_owned(),
|
||||||
|
register_command: identity.register_command(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ nu-utils = { path = "../nu-utils", version = "0.92.3" }
|
||||||
nu-path = { path = "../nu-path", version = "0.92.3" }
|
nu-path = { path = "../nu-path", version = "0.92.3" }
|
||||||
nu-system = { path = "../nu-system", version = "0.92.3" }
|
nu-system = { path = "../nu-system", version = "0.92.3" }
|
||||||
|
|
||||||
|
brotli = { workspace = true, optional = true }
|
||||||
byte-unit = { version = "5.1", features = [ "serde" ] }
|
byte-unit = { version = "5.1", features = [ "serde" ] }
|
||||||
chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false }
|
chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false }
|
||||||
chrono-humanize = { workspace = true }
|
chrono-humanize = { workspace = true }
|
||||||
|
@ -25,19 +26,25 @@ indexmap = { workspace = true }
|
||||||
lru = { workspace = true }
|
lru = { workspace = true }
|
||||||
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
||||||
num-format = { workspace = true }
|
num-format = { workspace = true }
|
||||||
|
rmp-serde = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, default-features = false }
|
serde = { workspace = true, default-features = false }
|
||||||
serde_json = { workspace = true, optional = true }
|
serde_json = { workspace = true, optional = true }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
typetag = "0.2"
|
typetag = "0.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
plugin = ["serde_json"]
|
plugin = [
|
||||||
|
"brotli",
|
||||||
|
"rmp-serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
strum = "0.26"
|
strum = "0.26"
|
||||||
strum_macros = "0.26"
|
strum_macros = "0.26"
|
||||||
nu-test-support = { path = "../nu-test-support", version = "0.92.3" }
|
nu-test-support = { path = "../nu-test-support", version = "0.92.3" }
|
||||||
|
pretty_assertions = "1.0"
|
||||||
rstest = { workspace = true }
|
rstest = { workspace = true }
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|
|
@ -24,7 +24,7 @@ use std::{
|
||||||
type PoisonDebuggerError<'a> = PoisonError<MutexGuard<'a, Box<dyn Debugger>>>;
|
type PoisonDebuggerError<'a> = PoisonError<MutexGuard<'a, Box<dyn Debugger>>>;
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
use crate::RegisteredPlugin;
|
use crate::{PluginCacheFile, PluginCacheItem, RegisteredPlugin};
|
||||||
|
|
||||||
pub static PWD_ENV: &str = "PWD";
|
pub static PWD_ENV: &str = "PWD";
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ pub struct EngineState {
|
||||||
pub repl_state: Arc<Mutex<ReplState>>,
|
pub repl_state: Arc<Mutex<ReplState>>,
|
||||||
pub table_decl_id: Option<usize>,
|
pub table_decl_id: Option<usize>,
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub plugin_signatures: Option<PathBuf>,
|
pub plugin_path: Option<PathBuf>,
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
plugins: Vec<Arc<dyn RegisteredPlugin>>,
|
plugins: Vec<Arc<dyn RegisteredPlugin>>,
|
||||||
config_path: HashMap<String, PathBuf>,
|
config_path: HashMap<String, PathBuf>,
|
||||||
|
@ -155,7 +155,7 @@ impl EngineState {
|
||||||
})),
|
})),
|
||||||
table_decl_id: None,
|
table_decl_id: None,
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
plugin_signatures: None,
|
plugin_path: None,
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
plugins: vec![],
|
plugins: vec![],
|
||||||
config_path: HashMap::new(),
|
config_path: HashMap::new(),
|
||||||
|
@ -255,7 +255,7 @@ impl EngineState {
|
||||||
if let Some(existing) = self
|
if let Some(existing) = self
|
||||||
.plugins
|
.plugins
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|p| p.identity() == plugin.identity())
|
.find(|p| p.identity().name() == plugin.identity().name())
|
||||||
{
|
{
|
||||||
// Stop the existing plugin, so that the new plugin definitely takes over
|
// Stop the existing plugin, so that the new plugin definitely takes over
|
||||||
existing.stop()?;
|
existing.stop()?;
|
||||||
|
@ -267,10 +267,10 @@ impl EngineState {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
if delta.plugins_changed {
|
if !delta.plugin_cache_items.is_empty() {
|
||||||
// Update the plugin file with the new signatures.
|
// Update the plugin file with the new signatures.
|
||||||
if self.plugin_signatures.is_some() {
|
if self.plugin_path.is_some() {
|
||||||
self.update_plugin_file()?;
|
self.update_plugin_file(std::mem::take(&mut delta.plugin_cache_items))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,94 +480,59 @@ impl EngineState {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub fn update_plugin_file(&self) -> Result<(), ShellError> {
|
pub fn update_plugin_file(
|
||||||
use std::io::Write;
|
&self,
|
||||||
|
updated_items: Vec<PluginCacheItem>,
|
||||||
use crate::{PluginExample, PluginSignature};
|
) -> Result<(), ShellError> {
|
||||||
|
|
||||||
// Updating the signatures plugin file with the added signatures
|
// Updating the signatures plugin file with the added signatures
|
||||||
self.plugin_signatures
|
use std::fs::File;
|
||||||
|
|
||||||
|
let plugin_path = self
|
||||||
|
.plugin_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| ShellError::PluginFailedToLoad {
|
.ok_or_else(|| ShellError::GenericError {
|
||||||
msg: "Plugin file not found".into(),
|
error: "Plugin file path not set".into(),
|
||||||
})
|
msg: "".into(),
|
||||||
.and_then(|plugin_path| {
|
span: None,
|
||||||
// Always create the file, which will erase previous signatures
|
help: Some("you may be running nu with --no-config-file".into()),
|
||||||
std::fs::File::create(plugin_path.as_path()).map_err(|err| {
|
inner: vec![],
|
||||||
ShellError::PluginFailedToLoad {
|
})?;
|
||||||
msg: err.to_string(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.and_then(|mut plugin_file| {
|
|
||||||
// Plugin definitions with parsed signature
|
|
||||||
self.plugin_decls().try_for_each(|decl| {
|
|
||||||
// A successful plugin registration already includes the plugin filename
|
|
||||||
// No need to check the None option
|
|
||||||
let identity = decl.plugin_identity().expect("plugin should have identity");
|
|
||||||
let mut file_name = identity
|
|
||||||
.filename()
|
|
||||||
.to_str()
|
|
||||||
.expect("path was checked during registration as a str")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// Fix files or folders with quotes
|
// Read the current contents of the plugin file if it exists
|
||||||
if file_name.contains('\'')
|
let mut contents = match File::open(plugin_path.as_path()) {
|
||||||
|| file_name.contains('"')
|
Ok(mut plugin_file) => PluginCacheFile::read_from(&mut plugin_file, None),
|
||||||
|| file_name.contains(' ')
|
Err(err) => {
|
||||||
{
|
if err.kind() == std::io::ErrorKind::NotFound {
|
||||||
file_name = format!("`{file_name}`");
|
Ok(PluginCacheFile::default())
|
||||||
}
|
} else {
|
||||||
|
Err(ShellError::GenericError {
|
||||||
let sig = decl.signature();
|
error: "Failed to open plugin file".into(),
|
||||||
let examples = decl
|
msg: "".into(),
|
||||||
.examples()
|
|
||||||
.into_iter()
|
|
||||||
.map(PluginExample::from)
|
|
||||||
.collect();
|
|
||||||
let sig_with_examples = PluginSignature::new(sig, examples);
|
|
||||||
serde_json::to_string_pretty(&sig_with_examples)
|
|
||||||
.map(|signature| {
|
|
||||||
// Extracting the possible path to the shell used to load the plugin
|
|
||||||
let shell_str = identity
|
|
||||||
.shell()
|
|
||||||
.map(|path| {
|
|
||||||
format!(
|
|
||||||
"-s {}",
|
|
||||||
path.to_str().expect(
|
|
||||||
"shell path was checked during registration as a str"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Each signature is stored in the plugin file with the shell and signature
|
|
||||||
// This information will be used when loading the plugin
|
|
||||||
// information when nushell starts
|
|
||||||
format!("register {file_name} {shell_str} {signature}\n\n")
|
|
||||||
})
|
|
||||||
.map_err(|err| ShellError::PluginFailedToLoad {
|
|
||||||
msg: err.to_string(),
|
|
||||||
})
|
|
||||||
.and_then(|line| {
|
|
||||||
plugin_file.write_all(line.as_bytes()).map_err(|err| {
|
|
||||||
ShellError::PluginFailedToLoad {
|
|
||||||
msg: err.to_string(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.and_then(|_| {
|
|
||||||
plugin_file.flush().map_err(|err| ShellError::GenericError {
|
|
||||||
error: "Error flushing plugin file".into(),
|
|
||||||
msg: format! {"{err}"},
|
|
||||||
span: None,
|
span: None,
|
||||||
help: None,
|
help: None,
|
||||||
inner: vec![],
|
inner: vec![err.into()],
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// Update the given signatures
|
||||||
|
for item in updated_items {
|
||||||
|
contents.upsert_plugin(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write it to the same path
|
||||||
|
let plugin_file =
|
||||||
|
File::create(plugin_path.as_path()).map_err(|err| ShellError::GenericError {
|
||||||
|
error: "Failed to write plugin file".into(),
|
||||||
|
msg: "".into(),
|
||||||
|
span: None,
|
||||||
|
help: None,
|
||||||
|
inner: vec![err.into()],
|
||||||
|
})?;
|
||||||
|
|
||||||
|
contents.write_to(plugin_file, None)
|
||||||
|
}
|
||||||
|
|
||||||
/// Update plugins with new garbage collection config
|
/// Update plugins with new garbage collection config
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
use crate::RegisteredPlugin;
|
use crate::{PluginCacheItem, RegisteredPlugin};
|
||||||
|
|
||||||
/// A delta (or change set) between the current global state and a possible future global state. Deltas
|
/// A delta (or change set) between the current global state and a possible future global state. Deltas
|
||||||
/// can be applied to the global state to update it to contain both previous state and the state held
|
/// can be applied to the global state to update it to contain both previous state and the state held
|
||||||
|
@ -24,9 +24,9 @@ pub struct StateDelta {
|
||||||
pub(super) usage: Usage,
|
pub(super) usage: Usage,
|
||||||
pub scope: Vec<ScopeFrame>,
|
pub scope: Vec<ScopeFrame>,
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub(super) plugins_changed: bool, // marks whether plugin file should be updated
|
|
||||||
#[cfg(feature = "plugin")]
|
|
||||||
pub(super) plugins: Vec<Arc<dyn RegisteredPlugin>>,
|
pub(super) plugins: Vec<Arc<dyn RegisteredPlugin>>,
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
pub(super) plugin_cache_items: Vec<PluginCacheItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StateDelta {
|
impl StateDelta {
|
||||||
|
@ -48,9 +48,9 @@ impl StateDelta {
|
||||||
scope: vec![scope_frame],
|
scope: vec![scope_frame],
|
||||||
usage: Usage::new(),
|
usage: Usage::new(),
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
plugins_changed: false,
|
|
||||||
#[cfg(feature = "plugin")]
|
|
||||||
plugins: vec![],
|
plugins: vec![],
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
plugin_cache_items: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
use crate::{PluginIdentity, RegisteredPlugin};
|
use crate::{PluginCacheItem, PluginIdentity, RegisteredPlugin};
|
||||||
|
|
||||||
/// A temporary extension to the global state. This handles bridging between the global state and the
|
/// A temporary extension to the global state. This handles bridging between the global state and the
|
||||||
/// additional declarations and scope changes that are not yet part of the global scope.
|
/// additional declarations and scope changes that are not yet part of the global scope.
|
||||||
|
@ -159,11 +159,6 @@ impl<'a> StateWorkingSet<'a> {
|
||||||
.insert(name, decl_id)
|
.insert(name, decl_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
|
||||||
pub fn mark_plugins_file_dirty(&mut self) {
|
|
||||||
self.delta.plugins_changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub fn find_or_create_plugin(
|
pub fn find_or_create_plugin(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -186,6 +181,11 @@ impl<'a> StateWorkingSet<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
pub fn update_plugin_cache(&mut self, item: PluginCacheItem) {
|
||||||
|
self.delta.plugin_cache_items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn merge_predecl(&mut self, name: &[u8]) -> Option<DeclId> {
|
pub fn merge_predecl(&mut self, name: &[u8]) -> Option<DeclId> {
|
||||||
self.move_predecls_to_overlay();
|
self.move_predecls_to_overlay();
|
||||||
|
|
||||||
|
|
|
@ -750,6 +750,19 @@ pub enum ShellError {
|
||||||
span: Span,
|
span: Span,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// The cached plugin data (in `$nu.plugin-path`) for a plugin is invalid.
|
||||||
|
///
|
||||||
|
/// ## Resolution
|
||||||
|
///
|
||||||
|
/// `register` the plugin again to update the data, or remove it.
|
||||||
|
#[error("The cached plugin data for `{plugin_name}` is invalid")]
|
||||||
|
#[diagnostic(code(nu::shell::plugin_cache_data_invalid))]
|
||||||
|
PluginCacheDataInvalid {
|
||||||
|
plugin_name: String,
|
||||||
|
#[help("try registering the plugin again with `{}`")]
|
||||||
|
register_command: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// A plugin failed to load.
|
/// A plugin failed to load.
|
||||||
///
|
///
|
||||||
/// ## Resolution
|
/// ## Resolution
|
||||||
|
|
|
@ -116,7 +116,7 @@ pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Valu
|
||||||
{
|
{
|
||||||
record.push(
|
record.push(
|
||||||
"plugin-path",
|
"plugin-path",
|
||||||
if let Some(path) = &engine_state.plugin_signatures {
|
if let Some(path) = &engine_state.plugin_path {
|
||||||
let canon_plugin_path = canonicalize_path(engine_state, path);
|
let canon_plugin_path = canonicalize_path(engine_state, path);
|
||||||
Value::string(canon_plugin_path.to_string_lossy(), span)
|
Value::string(canon_plugin_path.to_string_lossy(), span)
|
||||||
} else {
|
} else {
|
||||||
|
@ -124,7 +124,7 @@ pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Valu
|
||||||
config_path.clone().map_or_else(
|
config_path.clone().map_or_else(
|
||||||
|e| e,
|
|e| e,
|
||||||
|mut path| {
|
|mut path| {
|
||||||
path.push("plugin.nu");
|
path.push("plugin.msgpackz");
|
||||||
let canonical_plugin_path = canonicalize_path(engine_state, &path);
|
let canonical_plugin_path = canonicalize_path(engine_state, &path);
|
||||||
Value::string(canonical_plugin_path.to_string_lossy(), span)
|
Value::string(canonical_plugin_path.to_string_lossy(), span)
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,7 +13,7 @@ pub struct Example<'a> {
|
||||||
// and `description` fields, because these information is fetched from plugin, a third party
|
// and `description` fields, because these information is fetched from plugin, a third party
|
||||||
// binary, nushell have no way to construct it directly.
|
// binary, nushell have no way to construct it directly.
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct PluginExample {
|
pub struct PluginExample {
|
||||||
pub example: String,
|
pub example: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
|
175
crates/nu-protocol/src/plugin/cache_file/mod.rs
Normal file
175
crates/nu-protocol/src/plugin/cache_file/mod.rs
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
use std::{
|
||||||
|
io::{Read, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{PluginIdentity, PluginSignature, ShellError, Span};
|
||||||
|
|
||||||
|
// This has a big impact on performance
|
||||||
|
const BUFFER_SIZE: usize = 65536;
|
||||||
|
|
||||||
|
// Chose settings at the low end, because we're just trying to get the maximum speed
|
||||||
|
const COMPRESSION_QUALITY: u32 = 1;
|
||||||
|
const WIN_SIZE: u32 = 20; // recommended 20-22
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct PluginCacheFile {
|
||||||
|
/// The Nushell version that last updated the file.
|
||||||
|
pub nushell_version: String,
|
||||||
|
|
||||||
|
/// The installed plugins.
|
||||||
|
pub plugins: Vec<PluginCacheItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PluginCacheFile {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginCacheFile {
|
||||||
|
/// Create a new, empty plugin cache file.
|
||||||
|
pub fn new() -> PluginCacheFile {
|
||||||
|
PluginCacheFile {
|
||||||
|
nushell_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||||
|
plugins: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the plugin cache file from a reader, e.g. [`File`](std::fs::File).
|
||||||
|
pub fn read_from(
|
||||||
|
reader: impl Read,
|
||||||
|
error_span: Option<Span>,
|
||||||
|
) -> Result<PluginCacheFile, ShellError> {
|
||||||
|
// Format is brotli compressed messagepack
|
||||||
|
let brotli_reader = brotli::Decompressor::new(reader, BUFFER_SIZE);
|
||||||
|
|
||||||
|
rmp_serde::from_read(brotli_reader).map_err(|err| ShellError::GenericError {
|
||||||
|
error: format!("Failed to load plugin file: {err}"),
|
||||||
|
msg: "plugin file load attempted here".into(),
|
||||||
|
span: error_span,
|
||||||
|
help: Some(
|
||||||
|
"it may be corrupt. Try deleting it and registering your plugins again".into(),
|
||||||
|
),
|
||||||
|
inner: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the plugin cache file to a writer, e.g. [`File`](std::fs::File).
|
||||||
|
///
|
||||||
|
/// The `nushell_version` will be updated to the current version before writing.
|
||||||
|
pub fn write_to(
|
||||||
|
&mut self,
|
||||||
|
writer: impl Write,
|
||||||
|
error_span: Option<Span>,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
// Update the Nushell version before writing
|
||||||
|
self.nushell_version = env!("CARGO_PKG_VERSION").to_owned();
|
||||||
|
|
||||||
|
// Format is brotli compressed messagepack
|
||||||
|
let mut brotli_writer =
|
||||||
|
brotli::CompressorWriter::new(writer, BUFFER_SIZE, COMPRESSION_QUALITY, WIN_SIZE);
|
||||||
|
|
||||||
|
rmp_serde::encode::write_named(&mut brotli_writer, self)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
.and_then(|_| brotli_writer.flush().map_err(|err| err.to_string()))
|
||||||
|
.map_err(|err| ShellError::GenericError {
|
||||||
|
error: "Failed to save plugin file".into(),
|
||||||
|
msg: "plugin file save attempted here".into(),
|
||||||
|
span: error_span,
|
||||||
|
help: Some(err.to_string()),
|
||||||
|
inner: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert or update a plugin in the plugin cache file.
|
||||||
|
pub fn upsert_plugin(&mut self, item: PluginCacheItem) {
|
||||||
|
if let Some(existing_item) = self.plugins.iter_mut().find(|p| p.name == item.name) {
|
||||||
|
*existing_item = item;
|
||||||
|
} else {
|
||||||
|
self.plugins.push(item);
|
||||||
|
|
||||||
|
// Sort the plugins for consistency
|
||||||
|
self.plugins
|
||||||
|
.sort_by(|item1, item2| item1.name.cmp(&item2.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a plugin from the plugin cache file by name.
|
||||||
|
pub fn remove_plugin(&mut self, name: &str) {
|
||||||
|
self.plugins.retain_mut(|item| item.name != name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single plugin definition from a [`PluginCacheFile`].
|
||||||
|
///
|
||||||
|
/// Contains the information necessary for the [`PluginIdentity`], as well as possibly valid data
|
||||||
|
/// about the plugin including the cached command signatures.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct PluginCacheItem {
|
||||||
|
/// The name of the plugin, as would show in `plugin list`. This does not include the file
|
||||||
|
/// extension or the `nu_plugin_` prefix.
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// The path to the file.
|
||||||
|
pub filename: PathBuf,
|
||||||
|
|
||||||
|
/// The shell program used to run the plugin, if applicable.
|
||||||
|
pub shell: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Additional data that might be invalid so that we don't fail to load the whole plugin file
|
||||||
|
/// if there's a deserialization error.
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub data: PluginCacheItemData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginCacheItem {
|
||||||
|
/// Create a [`PluginCacheItem`] from an identity and signatures.
|
||||||
|
pub fn new(identity: &PluginIdentity, mut commands: Vec<PluginSignature>) -> PluginCacheItem {
|
||||||
|
// Sort the commands for consistency
|
||||||
|
commands.sort_by(|cmd1, cmd2| cmd1.sig.name.cmp(&cmd2.sig.name));
|
||||||
|
|
||||||
|
PluginCacheItem {
|
||||||
|
name: identity.name().to_owned(),
|
||||||
|
filename: identity.filename().to_owned(),
|
||||||
|
shell: identity.shell().map(|p| p.to_owned()),
|
||||||
|
data: PluginCacheItemData::Valid { commands },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Possibly valid data about a plugin in a [`PluginCacheFile`]. If deserialization fails, it will
|
||||||
|
/// be `Invalid`.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum PluginCacheItemData {
|
||||||
|
Valid {
|
||||||
|
/// Signatures and examples for each command provided by the plugin.
|
||||||
|
commands: Vec<PluginSignature>,
|
||||||
|
},
|
||||||
|
#[serde(
|
||||||
|
serialize_with = "serialize_invalid",
|
||||||
|
deserialize_with = "deserialize_invalid"
|
||||||
|
)]
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_invalid<S>(serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
().serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_invalid<'de, D>(deserializer: D) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
serde::de::IgnoredAny::deserialize(deserializer)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
120
crates/nu-protocol/src/plugin/cache_file/tests.rs
Normal file
120
crates/nu-protocol/src/plugin/cache_file/tests.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
use super::{PluginCacheFile, PluginCacheItem, PluginCacheItemData};
|
||||||
|
use crate::{
|
||||||
|
Category, PluginExample, PluginSignature, ShellError, Signature, SyntaxShape, Type, Value,
|
||||||
|
};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
fn foo_plugin() -> PluginCacheItem {
|
||||||
|
PluginCacheItem {
|
||||||
|
name: "foo".into(),
|
||||||
|
filename: "/path/to/nu_plugin_foo".into(),
|
||||||
|
shell: None,
|
||||||
|
data: PluginCacheItemData::Valid {
|
||||||
|
commands: vec![PluginSignature {
|
||||||
|
sig: Signature::new("foo")
|
||||||
|
.input_output_type(Type::Int, Type::List(Box::new(Type::Int)))
|
||||||
|
.category(Category::Experimental),
|
||||||
|
examples: vec![PluginExample {
|
||||||
|
example: "16 | foo".into(),
|
||||||
|
description: "powers of two up to 16".into(),
|
||||||
|
result: Some(Value::test_list(vec![
|
||||||
|
Value::test_int(2),
|
||||||
|
Value::test_int(4),
|
||||||
|
Value::test_int(8),
|
||||||
|
Value::test_int(16),
|
||||||
|
])),
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bar_plugin() -> PluginCacheItem {
|
||||||
|
PluginCacheItem {
|
||||||
|
name: "bar".into(),
|
||||||
|
filename: "/path/to/nu_plugin_bar".into(),
|
||||||
|
shell: None,
|
||||||
|
data: PluginCacheItemData::Valid {
|
||||||
|
commands: vec![PluginSignature {
|
||||||
|
sig: Signature::new("bar")
|
||||||
|
.usage("overwrites files with random data")
|
||||||
|
.switch("force", "ignore errors", Some('f'))
|
||||||
|
.required(
|
||||||
|
"path",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"file to overwrite with random data",
|
||||||
|
)
|
||||||
|
.category(Category::Experimental),
|
||||||
|
examples: vec![],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip() -> Result<(), ShellError> {
|
||||||
|
let mut plugin_cache_file = PluginCacheFile {
|
||||||
|
nushell_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||||
|
plugins: vec![foo_plugin(), bar_plugin()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut output = vec![];
|
||||||
|
|
||||||
|
plugin_cache_file.write_to(&mut output, None)?;
|
||||||
|
|
||||||
|
let read_file = PluginCacheFile::read_from(Cursor::new(&output[..]), None)?;
|
||||||
|
|
||||||
|
assert_eq!(plugin_cache_file, read_file);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_invalid() -> Result<(), ShellError> {
|
||||||
|
let mut plugin_cache_file = PluginCacheFile {
|
||||||
|
nushell_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||||
|
plugins: vec![PluginCacheItem {
|
||||||
|
name: "invalid".into(),
|
||||||
|
filename: "/path/to/nu_plugin_invalid".into(),
|
||||||
|
shell: None,
|
||||||
|
data: PluginCacheItemData::Invalid,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut output = vec![];
|
||||||
|
|
||||||
|
plugin_cache_file.write_to(&mut output, None)?;
|
||||||
|
|
||||||
|
let read_file = PluginCacheFile::read_from(Cursor::new(&output[..]), None)?;
|
||||||
|
|
||||||
|
assert_eq!(plugin_cache_file, read_file);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn upsert_new() {
|
||||||
|
let mut file = PluginCacheFile::new();
|
||||||
|
|
||||||
|
file.plugins.push(foo_plugin());
|
||||||
|
|
||||||
|
file.upsert_plugin(bar_plugin());
|
||||||
|
|
||||||
|
assert_eq!(2, file.plugins.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn upsert_replace() {
|
||||||
|
let mut file = PluginCacheFile::new();
|
||||||
|
|
||||||
|
file.plugins.push(foo_plugin());
|
||||||
|
|
||||||
|
let mut mutated_foo = foo_plugin();
|
||||||
|
mutated_foo.shell = Some("/bin/sh".into());
|
||||||
|
|
||||||
|
file.upsert_plugin(mutated_foo);
|
||||||
|
|
||||||
|
assert_eq!(1, file.plugins.len());
|
||||||
|
assert_eq!(Some("/bin/sh".into()), file.plugins[0].shell);
|
||||||
|
}
|
|
@ -88,6 +88,19 @@ impl PluginIdentity {
|
||||||
PluginIdentity::new(format!(r"/fake/path/nu_plugin_{name}"), None)
|
PluginIdentity::new(format!(r"/fake/path/nu_plugin_{name}"), None)
|
||||||
.expect("fake plugin identity path is invalid")
|
.expect("fake plugin identity path is invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A command that could be used to register the plugin, for suggesting in errors.
|
||||||
|
pub fn register_command(&self) -> String {
|
||||||
|
if let Some(shell) = self.shell() {
|
||||||
|
format!(
|
||||||
|
"register --shell '{}' '{}'",
|
||||||
|
shell.display(),
|
||||||
|
self.filename().display(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("register '{}'", self.filename().display())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
mod cache_file;
|
||||||
mod identity;
|
mod identity;
|
||||||
mod registered;
|
mod registered;
|
||||||
mod signature;
|
mod signature;
|
||||||
|
|
||||||
|
pub use cache_file::*;
|
||||||
pub use identity::*;
|
pub use identity::*;
|
||||||
pub use registered::*;
|
pub use registered::*;
|
||||||
pub use signature::*;
|
pub use signature::*;
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{PluginExample, Signature};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// A simple wrapper for Signature that includes examples.
|
/// A simple wrapper for Signature that includes examples.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct PluginSignature {
|
pub struct PluginSignature {
|
||||||
pub sig: Signature,
|
pub sig: Signature,
|
||||||
pub examples: Vec<PluginExample>,
|
pub examples: Vec<PluginExample>,
|
||||||
|
|
|
@ -56,6 +56,7 @@ pub enum Category {
|
||||||
Network,
|
Network,
|
||||||
Path,
|
Path,
|
||||||
Platform,
|
Platform,
|
||||||
|
Plugin,
|
||||||
Random,
|
Random,
|
||||||
Shells,
|
Shells,
|
||||||
Strings,
|
Strings,
|
||||||
|
@ -90,6 +91,7 @@ impl std::fmt::Display for Category {
|
||||||
Category::Network => "network",
|
Category::Network => "network",
|
||||||
Category::Path => "path",
|
Category::Path => "path",
|
||||||
Category::Platform => "platform",
|
Category::Platform => "platform",
|
||||||
|
Category::Plugin => "plugin",
|
||||||
Category::Random => "random",
|
Category::Random => "random",
|
||||||
Category::Shells => "shells",
|
Category::Shells => "shells",
|
||||||
Category::Strings => "strings",
|
Category::Strings => "strings",
|
||||||
|
|
|
@ -202,11 +202,11 @@ macro_rules! nu_with_std {
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! nu_with_plugins {
|
macro_rules! nu_with_plugins {
|
||||||
(cwd: $cwd:expr, plugins: [$(($plugin_name:expr)),+$(,)?], $command:expr) => {{
|
(cwd: $cwd:expr, plugins: [$(($plugin_name:expr)),*$(,)?], $command:expr) => {{
|
||||||
nu_with_plugins!(
|
nu_with_plugins!(
|
||||||
cwd: $cwd,
|
cwd: $cwd,
|
||||||
envs: Vec::<(&str, &str)>::new(),
|
envs: Vec::<(&str, &str)>::new(),
|
||||||
plugins: [$(($plugin_name)),+],
|
plugins: [$(($plugin_name)),*],
|
||||||
$command
|
$command
|
||||||
)
|
)
|
||||||
}};
|
}};
|
||||||
|
@ -222,10 +222,10 @@ macro_rules! nu_with_plugins {
|
||||||
(
|
(
|
||||||
cwd: $cwd:expr,
|
cwd: $cwd:expr,
|
||||||
envs: $envs:expr,
|
envs: $envs:expr,
|
||||||
plugins: [$(($plugin_name:expr)),+$(,)?],
|
plugins: [$(($plugin_name:expr)),*$(,)?],
|
||||||
$command:expr
|
$command:expr
|
||||||
) => {{
|
) => {{
|
||||||
$crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$($plugin_name),+], $command)
|
$crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$($plugin_name),*], $command)
|
||||||
}};
|
}};
|
||||||
(cwd: $cwd:expr, envs: $envs:expr, plugin: ($plugin_name:expr), $command:expr) => {{
|
(cwd: $cwd:expr, envs: $envs:expr, plugin: ($plugin_name:expr), $command:expr) => {{
|
||||||
$crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$plugin_name], $command)
|
$crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$plugin_name], $command)
|
||||||
|
@ -329,13 +329,15 @@ where
|
||||||
});
|
});
|
||||||
|
|
||||||
let temp = tempdir().expect("couldn't create a temporary directory");
|
let temp = tempdir().expect("couldn't create a temporary directory");
|
||||||
let [temp_config_file, temp_env_config_file, temp_plugin_file] =
|
let [temp_config_file, temp_env_config_file] = ["config.nu", "env.nu"].map(|name| {
|
||||||
["config.nu", "env.nu", "plugin.nu"].map(|name| {
|
|
||||||
let temp_file = temp.path().join(name);
|
let temp_file = temp.path().join(name);
|
||||||
std::fs::File::create(&temp_file).expect("couldn't create temporary config file");
|
std::fs::File::create(&temp_file).expect("couldn't create temporary config file");
|
||||||
temp_file
|
temp_file
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// We don't have to write the plugin cache file, it's ok for it to not exist
|
||||||
|
let temp_plugin_file = temp.path().join("plugin.msgpackz");
|
||||||
|
|
||||||
crate::commands::ensure_plugins_built();
|
crate::commands::ensure_plugins_built();
|
||||||
|
|
||||||
let registrations: String = plugins
|
let registrations: String = plugins
|
||||||
|
|
|
@ -340,7 +340,7 @@ impl Command for Nu {
|
||||||
signature = signature.named(
|
signature = signature.named(
|
||||||
"plugin-config",
|
"plugin-config",
|
||||||
SyntaxShape::String,
|
SyntaxShape::String,
|
||||||
"start with an alternate plugin signature file",
|
"start with an alternate plugin cache file",
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,7 +214,7 @@ pub(crate) fn setup_config(
|
||||||
);
|
);
|
||||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
read_plugin_file(engine_state, stack, plugin_file, NUSHELL_FOLDER);
|
read_plugin_file(engine_state, plugin_file, NUSHELL_FOLDER);
|
||||||
|
|
||||||
read_config_file(engine_state, stack, env_file, true);
|
read_config_file(engine_state, stack, env_file, true);
|
||||||
read_config_file(engine_state, stack, config_file, false);
|
read_config_file(engine_state, stack, config_file, false);
|
||||||
|
|
|
@ -43,6 +43,8 @@ use std::{
|
||||||
|
|
||||||
fn get_engine_state() -> EngineState {
|
fn get_engine_state() -> EngineState {
|
||||||
let engine_state = nu_cmd_lang::create_default_context();
|
let engine_state = nu_cmd_lang::create_default_context();
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
let engine_state = nu_cmd_plugin::add_plugin_command_context(engine_state);
|
||||||
let engine_state = nu_command::add_shell_command_context(engine_state);
|
let engine_state = nu_command::add_shell_command_context(engine_state);
|
||||||
let engine_state = nu_cmd_extra::add_extra_command_context(engine_state);
|
let engine_state = nu_cmd_extra::add_extra_command_context(engine_state);
|
||||||
#[cfg(feature = "dataframe")]
|
#[cfg(feature = "dataframe")]
|
||||||
|
|
14
src/run.rs
14
src/run.rs
|
@ -30,12 +30,7 @@ pub(crate) fn run_commands(
|
||||||
// if the --no-config-file(-n) flag is passed, do not load plugin, env, or config files
|
// if the --no-config-file(-n) flag is passed, do not load plugin, env, or config files
|
||||||
if parsed_nu_cli_args.no_config_file.is_none() {
|
if parsed_nu_cli_args.no_config_file.is_none() {
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
read_plugin_file(
|
read_plugin_file(engine_state, parsed_nu_cli_args.plugin_file, NUSHELL_FOLDER);
|
||||||
engine_state,
|
|
||||||
&mut stack,
|
|
||||||
parsed_nu_cli_args.plugin_file,
|
|
||||||
NUSHELL_FOLDER,
|
|
||||||
);
|
|
||||||
|
|
||||||
perf(
|
perf(
|
||||||
"read plugins",
|
"read plugins",
|
||||||
|
@ -155,12 +150,7 @@ pub(crate) fn run_file(
|
||||||
if parsed_nu_cli_args.no_config_file.is_none() {
|
if parsed_nu_cli_args.no_config_file.is_none() {
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
read_plugin_file(
|
read_plugin_file(engine_state, parsed_nu_cli_args.plugin_file, NUSHELL_FOLDER);
|
||||||
engine_state,
|
|
||||||
&mut stack,
|
|
||||||
parsed_nu_cli_args.plugin_file,
|
|
||||||
NUSHELL_FOLDER,
|
|
||||||
);
|
|
||||||
perf(
|
perf(
|
||||||
"read plugins",
|
"read plugins",
|
||||||
start_time,
|
start_time,
|
||||||
|
|
|
@ -117,7 +117,7 @@ fn test_config_path_helper(playground: &mut Playground, config_dir_nushell: Path
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
{
|
{
|
||||||
let plugin_path = config_dir_nushell.join("plugin.nu");
|
let plugin_path = config_dir_nushell.join("plugin.msgpackz");
|
||||||
let canon_plugin_path =
|
let canon_plugin_path =
|
||||||
adjust_canonicalization(std::fs::canonicalize(&plugin_path).unwrap_or(plugin_path));
|
adjust_canonicalization(std::fs::canonicalize(&plugin_path).unwrap_or(plugin_path));
|
||||||
let actual = run(playground, "$nu.plugin-path");
|
let actual = run(playground, "$nu.plugin-path");
|
||||||
|
@ -161,7 +161,7 @@ fn test_default_symlink_config_path_broken_symlink_config_files() {
|
||||||
"history.txt",
|
"history.txt",
|
||||||
"history.sqlite3",
|
"history.sqlite3",
|
||||||
"login.nu",
|
"login.nu",
|
||||||
"plugin.nu",
|
"plugin.msgpackz",
|
||||||
] {
|
] {
|
||||||
let fake_file = fake_dir.join(config_file);
|
let fake_file = fake_dir.join(config_file);
|
||||||
File::create(playground.cwd().join(&fake_file)).unwrap();
|
File::create(playground.cwd().join(&fake_file)).unwrap();
|
||||||
|
@ -194,7 +194,7 @@ fn test_default_config_path_symlinked_config_files() {
|
||||||
"history.txt",
|
"history.txt",
|
||||||
"history.sqlite3",
|
"history.sqlite3",
|
||||||
"login.nu",
|
"login.nu",
|
||||||
"plugin.nu",
|
"plugin.msgpackz",
|
||||||
] {
|
] {
|
||||||
let empty_file = playground.cwd().join(format!("empty-{config_file}"));
|
let empty_file = playground.cwd().join(format!("empty-{config_file}"));
|
||||||
File::create(&empty_file).unwrap();
|
File::create(&empty_file).unwrap();
|
||||||
|
|
211
tests/plugins/cache_file.rs
Normal file
211
tests/plugins/cache_file.rs
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
path::PathBuf,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
use nu_protocol::{PluginCacheFile, PluginCacheItem, PluginCacheItemData};
|
||||||
|
use nu_test_support::{fs::Stub, nu, nu_with_plugins, playground::Playground};
|
||||||
|
|
||||||
|
fn example_plugin_path() -> PathBuf {
|
||||||
|
nu_test_support::commands::ensure_plugins_built();
|
||||||
|
|
||||||
|
let bins_path = nu_test_support::fs::binaries();
|
||||||
|
nu_path::canonicalize_with(
|
||||||
|
if cfg!(windows) {
|
||||||
|
"nu_plugin_example.exe"
|
||||||
|
} else {
|
||||||
|
"nu_plugin_example"
|
||||||
|
},
|
||||||
|
bins_path,
|
||||||
|
)
|
||||||
|
.expect("nu_plugin_example not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_add_then_restart_nu() {
|
||||||
|
let result = nu_with_plugins!(
|
||||||
|
cwd: ".",
|
||||||
|
plugins: [],
|
||||||
|
&format!("
|
||||||
|
plugin add '{}'
|
||||||
|
(
|
||||||
|
^$nu.current-exe
|
||||||
|
--config $nu.config-path
|
||||||
|
--env-config $nu.env-path
|
||||||
|
--plugin-config $nu.plugin-path
|
||||||
|
--commands 'plugin list | get name | to json --raw'
|
||||||
|
)
|
||||||
|
", example_plugin_path().display())
|
||||||
|
);
|
||||||
|
assert!(result.status.success());
|
||||||
|
assert_eq!(r#"["example"]"#, result.out);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_add_to_custom_path() {
|
||||||
|
let example_plugin_path = example_plugin_path();
|
||||||
|
Playground::setup("plugin add to custom path", |dirs, _playground| {
|
||||||
|
let result = nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
&format!("
|
||||||
|
plugin add --plugin-config test-plugin-file.msgpackz '{}'
|
||||||
|
", example_plugin_path.display())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.status.success());
|
||||||
|
|
||||||
|
let contents = PluginCacheFile::read_from(
|
||||||
|
File::open(dirs.test().join("test-plugin-file.msgpackz"))
|
||||||
|
.expect("failed to open plugin file"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("failed to read plugin file");
|
||||||
|
|
||||||
|
assert_eq!(1, contents.plugins.len());
|
||||||
|
assert_eq!("example", contents.plugins[0].name);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_rm_then_restart_nu() {
|
||||||
|
let result = nu_with_plugins!(
|
||||||
|
cwd: ".",
|
||||||
|
plugin: ("nu_plugin_example"),
|
||||||
|
r#"
|
||||||
|
plugin rm example
|
||||||
|
^$nu.current-exe --config $nu.config-path --env-config $nu.env-path --plugin-config $nu.plugin-path --commands 'plugin list | get name | to json --raw'
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
assert!(result.status.success());
|
||||||
|
assert_eq!(r#"[]"#, result.out);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_rm_not_found() {
|
||||||
|
let result = nu_with_plugins!(
|
||||||
|
cwd: ".",
|
||||||
|
plugins: [],
|
||||||
|
r#"
|
||||||
|
plugin rm example
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
assert!(!result.status.success());
|
||||||
|
assert!(result.err.contains("example"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_rm_from_custom_path() {
|
||||||
|
let example_plugin_path = example_plugin_path();
|
||||||
|
Playground::setup("plugin rm from custom path", |dirs, _playground| {
|
||||||
|
let file = File::create(dirs.test().join("test-plugin-file.msgpackz"))
|
||||||
|
.expect("failed to create file");
|
||||||
|
let mut contents = PluginCacheFile::new();
|
||||||
|
|
||||||
|
contents.upsert_plugin(PluginCacheItem {
|
||||||
|
name: "example".into(),
|
||||||
|
filename: example_plugin_path,
|
||||||
|
shell: None,
|
||||||
|
data: PluginCacheItemData::Valid { commands: vec![] },
|
||||||
|
});
|
||||||
|
|
||||||
|
contents.upsert_plugin(PluginCacheItem {
|
||||||
|
name: "foo".into(),
|
||||||
|
// this doesn't exist, but it should be ok
|
||||||
|
filename: dirs.test().join("nu_plugin_foo"),
|
||||||
|
shell: None,
|
||||||
|
data: PluginCacheItemData::Valid { commands: vec![] },
|
||||||
|
});
|
||||||
|
|
||||||
|
contents
|
||||||
|
.write_to(file, None)
|
||||||
|
.expect("failed to write plugin file");
|
||||||
|
|
||||||
|
let result = nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"plugin rm --plugin-config test-plugin-file.msgpackz example",
|
||||||
|
);
|
||||||
|
assert!(result.status.success());
|
||||||
|
assert!(result.err.trim().is_empty());
|
||||||
|
|
||||||
|
// Check the contents after running
|
||||||
|
let contents = PluginCacheFile::read_from(
|
||||||
|
File::open(dirs.test().join("test-plugin-file.msgpackz")).expect("failed to open file"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("failed to read file");
|
||||||
|
|
||||||
|
assert!(!contents.plugins.iter().any(|p| p.name == "example"));
|
||||||
|
|
||||||
|
// Shouldn't remove anything else
|
||||||
|
assert!(contents.plugins.iter().any(|p| p.name == "foo"));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Running nu with a test plugin file that fails to parse on one plugin should just cause a warning
|
||||||
|
/// but the others should be loaded
|
||||||
|
#[test]
|
||||||
|
fn warning_on_invalid_plugin_item() {
|
||||||
|
let example_plugin_path = example_plugin_path();
|
||||||
|
Playground::setup("warning on invalid plugin item", |dirs, playground| {
|
||||||
|
playground.with_files(vec![
|
||||||
|
Stub::FileWithContent("config.nu", ""),
|
||||||
|
Stub::FileWithContent("env.nu", ""),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let file = File::create(dirs.test().join("test-plugin-file.msgpackz"))
|
||||||
|
.expect("failed to create file");
|
||||||
|
let mut contents = PluginCacheFile::new();
|
||||||
|
|
||||||
|
contents.upsert_plugin(PluginCacheItem {
|
||||||
|
name: "example".into(),
|
||||||
|
filename: example_plugin_path,
|
||||||
|
shell: None,
|
||||||
|
data: PluginCacheItemData::Valid { commands: vec![] },
|
||||||
|
});
|
||||||
|
|
||||||
|
contents.upsert_plugin(PluginCacheItem {
|
||||||
|
name: "badtest".into(),
|
||||||
|
// this doesn't exist, but it should be ok
|
||||||
|
filename: dirs.test().join("nu_plugin_badtest"),
|
||||||
|
shell: None,
|
||||||
|
data: PluginCacheItemData::Invalid,
|
||||||
|
});
|
||||||
|
|
||||||
|
contents
|
||||||
|
.write_to(file, None)
|
||||||
|
.expect("failed to write plugin file");
|
||||||
|
|
||||||
|
let result = Command::new(nu_test_support::fs::executable_path())
|
||||||
|
.current_dir(dirs.test())
|
||||||
|
.args([
|
||||||
|
"--no-std-lib",
|
||||||
|
"--config",
|
||||||
|
"config.nu",
|
||||||
|
"--env-config",
|
||||||
|
"env.nu",
|
||||||
|
"--plugin-config",
|
||||||
|
"test-plugin-file.msgpackz",
|
||||||
|
"--commands",
|
||||||
|
"plugin list | get name | to json --raw",
|
||||||
|
])
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.expect("failed to run nu");
|
||||||
|
|
||||||
|
let out = String::from_utf8_lossy(&result.stdout).trim().to_owned();
|
||||||
|
let err = String::from_utf8_lossy(&result.stderr).trim().to_owned();
|
||||||
|
|
||||||
|
println!("=== stdout\n{out}\n=== stderr\n{err}");
|
||||||
|
|
||||||
|
// The code should still execute successfully
|
||||||
|
assert!(result.status.success());
|
||||||
|
// The "example" plugin should be unaffected
|
||||||
|
assert_eq!(r#"["example"]"#, out);
|
||||||
|
// The warning should be in there
|
||||||
|
assert!(err.contains("cached plugin data"));
|
||||||
|
assert!(err.contains("badtest"));
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod cache_file;
|
||||||
mod config;
|
mod config;
|
||||||
mod core_inc;
|
mod core_inc;
|
||||||
mod custom_values;
|
mod custom_values;
|
||||||
|
|
Loading…
Reference in a new issue