aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/mozart-core/src/lib.rs1
-rw-r--r--crates/mozart-core/src/script_events.rs88
-rw-r--r--crates/mozart/src/commands/run_script.rs200
3 files changed, 176 insertions, 113 deletions
diff --git a/crates/mozart-core/src/lib.rs b/crates/mozart-core/src/lib.rs
index 7b78280..d898b62 100644
--- a/crates/mozart-core/src/lib.rs
+++ b/crates/mozart-core/src/lib.rs
@@ -13,6 +13,7 @@ pub mod package_info;
pub mod package_sorter;
pub mod platform;
pub mod repository_utils;
+pub mod script_events;
pub mod suggest;
pub mod validation;
pub mod version_bumper;
diff --git a/crates/mozart-core/src/script_events.rs b/crates/mozart-core/src/script_events.rs
new file mode 100644
index 0000000..bb7fbdc
--- /dev/null
+++ b/crates/mozart-core/src/script_events.rs
@@ -0,0 +1,88 @@
+//! Script event name constants.
+//!
+//! Mirrors `Composer\Script\ScriptEvents` (user-runnable script events) and the
+//! internal event constants from `Composer\Installer\PackageEvents`,
+//! `Composer\Installer\InstallerEvents`, and `Composer\Plugin\PluginEvents`
+//! that are referenced by the dispatcher when validating run-script input.
+
+// User-runnable script events — `Composer\Script\ScriptEvents`.
+pub const PRE_INSTALL_CMD: &str = "pre-install-cmd";
+pub const POST_INSTALL_CMD: &str = "post-install-cmd";
+pub const PRE_UPDATE_CMD: &str = "pre-update-cmd";
+pub const POST_UPDATE_CMD: &str = "post-update-cmd";
+pub const PRE_STATUS_CMD: &str = "pre-status-cmd";
+pub const POST_STATUS_CMD: &str = "post-status-cmd";
+pub const POST_ROOT_PACKAGE_INSTALL: &str = "post-root-package-install";
+pub const POST_CREATE_PROJECT_CMD: &str = "post-create-project-cmd";
+pub const PRE_ARCHIVE_CMD: &str = "pre-archive-cmd";
+pub const POST_ARCHIVE_CMD: &str = "post-archive-cmd";
+pub const PRE_AUTOLOAD_DUMP: &str = "pre-autoload-dump";
+pub const POST_AUTOLOAD_DUMP: &str = "post-autoload-dump";
+
+// Internal events — `Composer\Installer\PackageEvents`.
+pub const PRE_PACKAGE_INSTALL: &str = "pre-package-install";
+pub const POST_PACKAGE_INSTALL: &str = "post-package-install";
+pub const PRE_PACKAGE_UPDATE: &str = "pre-package-update";
+pub const POST_PACKAGE_UPDATE: &str = "post-package-update";
+pub const PRE_PACKAGE_UNINSTALL: &str = "pre-package-uninstall";
+pub const POST_PACKAGE_UNINSTALL: &str = "post-package-uninstall";
+
+// Internal events — `Composer\Installer\InstallerEvents`.
+pub const PRE_OPERATIONS_EXEC: &str = "pre-operations-exec";
+
+// Internal events — `Composer\Plugin\PluginEvents`.
+pub const INIT: &str = "init";
+pub const COMMAND: &str = "command";
+pub const PRE_FILE_DOWNLOAD: &str = "pre-file-download";
+pub const POST_FILE_DOWNLOAD: &str = "post-file-download";
+pub const PRE_COMMAND_RUN: &str = "pre-command-run";
+pub const PRE_POOL_CREATE: &str = "pre-pool-create";
+
+/// Script events the user is allowed to invoke via `run-script`.
+///
+/// Mirrors `RunScriptCommand::$scriptEvents` in Composer.
+pub const USER_RUNNABLE: &[&str] = &[
+ PRE_INSTALL_CMD,
+ POST_INSTALL_CMD,
+ PRE_UPDATE_CMD,
+ POST_UPDATE_CMD,
+ PRE_STATUS_CMD,
+ POST_STATUS_CMD,
+ POST_ROOT_PACKAGE_INSTALL,
+ POST_CREATE_PROJECT_CMD,
+ PRE_ARCHIVE_CMD,
+ POST_ARCHIVE_CMD,
+ PRE_AUTOLOAD_DUMP,
+ POST_AUTOLOAD_DUMP,
+];
+
+/// All recognised event names — user-runnable plus internal events emitted by
+/// the dispatcher during install/update/etc. Used by `run-script` to surface a
+/// "cannot be run with this command" error for known internal events.
+pub const ALL: &[&str] = &[
+ PRE_INSTALL_CMD,
+ POST_INSTALL_CMD,
+ PRE_UPDATE_CMD,
+ POST_UPDATE_CMD,
+ PRE_STATUS_CMD,
+ POST_STATUS_CMD,
+ POST_ROOT_PACKAGE_INSTALL,
+ POST_CREATE_PROJECT_CMD,
+ PRE_ARCHIVE_CMD,
+ POST_ARCHIVE_CMD,
+ PRE_AUTOLOAD_DUMP,
+ POST_AUTOLOAD_DUMP,
+ PRE_PACKAGE_INSTALL,
+ POST_PACKAGE_INSTALL,
+ PRE_PACKAGE_UPDATE,
+ POST_PACKAGE_UPDATE,
+ PRE_PACKAGE_UNINSTALL,
+ POST_PACKAGE_UNINSTALL,
+ PRE_OPERATIONS_EXEC,
+ INIT,
+ COMMAND,
+ PRE_FILE_DOWNLOAD,
+ POST_FILE_DOWNLOAD,
+ PRE_COMMAND_RUN,
+ PRE_POOL_CREATE,
+];
diff --git a/crates/mozart/src/commands/run_script.rs b/crates/mozart/src/commands/run_script.rs
index 77ed1c7..abfb93a 100644
--- a/crates/mozart/src/commands/run_script.rs
+++ b/crates/mozart/src/commands/run_script.rs
@@ -1,6 +1,7 @@
use clap::Args;
use mozart_core::composer::Composer;
-use mozart_core::console_writeln;
+use mozart_core::script_events;
+use mozart_core::{console_writeln, console_writeln_error};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
@@ -14,9 +15,9 @@ pub struct RunScriptArgs {
#[arg(trailing_var_arg = true)]
pub args: Vec<String>,
- /// Set the script timeout in seconds
+ /// Sets script timeout in seconds, or 0 for never.
#[arg(long)]
- pub timeout: Option<u64>,
+ pub timeout: Option<String>,
/// Sets the dev mode
#[arg(long)]
@@ -31,47 +32,6 @@ pub struct RunScriptArgs {
pub list: bool,
}
-const ALLOWED_SCRIPT_EVENTS: &[&str] = &[
- "pre-install-cmd",
- "post-install-cmd",
- "pre-update-cmd",
- "post-update-cmd",
- "pre-status-cmd",
- "post-status-cmd",
- "post-root-package-install",
- "post-create-project-cmd",
- "pre-archive-cmd",
- "post-archive-cmd",
- "pre-autoload-dump",
- "post-autoload-dump",
-];
-
-const ALL_SCRIPT_EVENTS: &[&str] = &[
- "pre-install-cmd",
- "post-install-cmd",
- "pre-update-cmd",
- "post-update-cmd",
- "pre-status-cmd",
- "post-status-cmd",
- "post-root-package-install",
- "post-create-project-cmd",
- "pre-archive-cmd",
- "post-archive-cmd",
- "pre-autoload-dump",
- "post-autoload-dump",
- "pre-dependencies-solving",
- "post-dependencies-solving",
- "pre-package-install",
- "post-package-install",
- "pre-package-update",
- "post-package-update",
- "pre-package-uninstall",
- "post-package-uninstall",
- "pre-operations-exec",
- "pre-pool-create",
- "pre-file-download",
-];
-
pub async fn execute(
args: &RunScriptArgs,
cli: &super::Cli,
@@ -79,39 +39,45 @@ pub async fn execute(
) -> anyhow::Result<()> {
let working_dir = cli.working_dir()?;
- // RunScriptCommand uses requireComposer in Composer; composer.json must exist.
- let composer = Composer::require(&working_dir)?;
-
- let (scripts, descriptions) = load_scripts(&working_dir)?;
-
if args.list {
+ Composer::require(&working_dir)?;
+ let (scripts, descriptions) = load_scripts(&working_dir)?;
return list_scripts(&scripts, &descriptions, console);
}
- if cli.no_scripts {
- return Ok(());
- }
-
- let script_name = match &args.script {
+ let script = match &args.script {
Some(name) => name.clone(),
- None => {
- anyhow::bail!("Missing required argument: script");
- }
+ None => anyhow::bail!("Missing required argument \"script\""),
};
- if !ALLOWED_SCRIPT_EVENTS.contains(&script_name.as_str())
- && ALL_SCRIPT_EVENTS.contains(&script_name.as_str())
+ if !script_events::USER_RUNNABLE.contains(&script.as_str())
+ && script_events::ALL.contains(&script.as_str())
{
- anyhow::bail!("Script \"{}\" cannot be run with this command", script_name);
+ anyhow::bail!("Script \"{}\" cannot be run with this command", script);
}
- if !scripts.contains_key(&script_name) {
- anyhow::bail!("Script \"{}\" is not defined in this package", script_name);
+ let composer = Composer::require(&working_dir)?;
+ let dev_mode = args.dev || !args.no_dev;
+
+ let (scripts, _descriptions) = load_scripts(&working_dir)?;
+ if !scripts.contains_key(&script) {
+ anyhow::bail!("Script \"{}\" is not defined in this package", script);
}
- let timeout = match args.timeout {
- Some(0) => None,
- Some(secs) => Some(Duration::from_secs(secs)),
+ let timeout = match &args.timeout {
+ Some(s) => {
+ if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
+ anyhow::bail!(
+ "Timeout value must be numeric and positive if defined, or 0 for forever"
+ );
+ }
+ let secs: u64 = s.parse()?;
+ if secs == 0 {
+ None
+ } else {
+ Some(Duration::from_secs(secs))
+ }
+ }
None => {
let t = composer.config().process_timeout;
if t != 0 {
@@ -122,8 +88,6 @@ pub async fn execute(
}
};
- let dev_mode = !args.no_dev;
-
// SAFETY: single-threaded at this point; no concurrent env access
unsafe {
std::env::set_var("COMPOSER_DEV_MODE", if dev_mode { "1" } else { "0" });
@@ -133,7 +97,7 @@ pub async fn execute(
let mut event_stack: Vec<String> = Vec::new();
let exit_code = run_script(
- &script_name,
+ &script,
&args.args,
&scripts,
&working_dir,
@@ -167,7 +131,7 @@ fn load_scripts(
let mut scripts: BTreeMap<String, Vec<String>> = BTreeMap::new();
if let Some(scripts_obj) = parsed.get("scripts").and_then(|v| v.as_object()) {
for (name, value) in scripts_obj {
- let entries = match value {
+ let listeners = match value {
serde_json::Value::String(s) => vec![s.clone()],
serde_json::Value::Array(arr) => arr
.iter()
@@ -176,7 +140,7 @@ fn load_scripts(
.collect(),
_ => vec![],
};
- scripts.insert(name.clone(), entries);
+ scripts.insert(name.clone(), listeners);
}
}
@@ -200,18 +164,28 @@ fn list_scripts(
descriptions: &BTreeMap<String, String>,
console: &mozart_core::console::Console,
) -> anyhow::Result<()> {
- console_writeln!(console, "scripts:");
+ if scripts.is_empty() {
+ return Ok(());
+ }
+
+ console_writeln_error!(
+ console,
+ &mozart_core::console_format!("<info>scripts:</info>"),
+ );
+
+ let name_width = scripts.keys().map(|n| n.len() + 2).max().unwrap_or(0);
for name in scripts.keys() {
let desc = descriptions.get(name).map(|s| s.as_str()).unwrap_or("");
- console_writeln!(console, &format!(" {} {}", name, desc));
+ let padded = format!(" {:<w$}", name, w = name_width - 2);
+ console_writeln!(console, &format!("{} {}", padded, desc));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run_script(
- script_name: &str,
- additional_args: &[String],
+ script: &str,
+ args: &[String],
scripts: &BTreeMap<String, Vec<String>>,
working_dir: &Path,
bin_dir: &Path,
@@ -221,24 +195,24 @@ fn run_script(
verbose: u8,
console: &mozart_core::console::Console,
) -> anyhow::Result<i32> {
- if event_stack.contains(&script_name.to_string()) {
+ if event_stack.contains(&script.to_string()) {
anyhow::bail!(
"Circular script reference detected: {} -> {}",
event_stack.join(" -> "),
- script_name
+ script
);
}
- event_stack.push(script_name.to_string());
+ event_stack.push(script.to_string());
- let entries = scripts.get(script_name).cloned().unwrap_or_default();
+ let listeners = scripts.get(script).cloned().unwrap_or_default();
let mut max_exit_code = 0;
- for entry in &entries {
+ for listener in &listeners {
let code = run_script_entry(
- entry,
- additional_args,
+ listener,
+ args,
scripts,
working_dir,
bin_dir,
@@ -255,7 +229,7 @@ fn run_script(
event_stack.pop();
anyhow::bail!(
"Script \"{}\" returned a non-zero exit code: {}",
- script_name,
+ script,
code
);
}
@@ -268,7 +242,7 @@ fn run_script(
#[allow(clippy::too_many_arguments)]
fn run_script_entry(
entry: &str,
- additional_args: &[String],
+ args: &[String],
scripts: &BTreeMap<String, Vec<String>>,
working_dir: &Path,
bin_dir: &Path,
@@ -279,11 +253,7 @@ fn run_script_entry(
console: &mozart_core::console::Console,
) -> anyhow::Result<i32> {
let suppress_additional_args = entry.contains("@no_additional_args");
- let effective_args: &[String] = if suppress_additional_args {
- &[]
- } else {
- additional_args
- };
+ let effective_args: &[String] = if suppress_additional_args { &[] } else { args };
let entry = entry.replace("@no_additional_args", "").trim().to_string();
@@ -555,10 +525,10 @@ mod tests {
.unwrap();
let (scripts, _) = load_scripts(dir.path()).unwrap();
- let entries = scripts.get("test").unwrap();
- assert_eq!(entries.len(), 2);
- assert_eq!(entries[0], "echo a");
- assert_eq!(entries[1], "echo b");
+ let listeners = scripts.get("test").unwrap();
+ assert_eq!(listeners.len(), 2);
+ assert_eq!(listeners[0], "echo a");
+ assert_eq!(listeners[1], "echo b");
}
#[test]
@@ -571,9 +541,9 @@ mod tests {
.unwrap();
let (scripts, _) = load_scripts(dir.path()).unwrap();
- let entries = scripts.get("test").unwrap();
- assert_eq!(entries.len(), 1);
- assert_eq!(entries[0], "echo a");
+ let listeners = scripts.get("test").unwrap();
+ assert_eq!(listeners.len(), 1);
+ assert_eq!(listeners[0], "echo a");
}
#[test]
@@ -612,10 +582,10 @@ mod tests {
.unwrap();
let (scripts, _) = load_scripts(dir.path()).unwrap();
- let test_entries = scripts.get("test").unwrap();
- assert_eq!(test_entries.len(), 1);
- let post_entries = scripts.get("post-install-cmd").unwrap();
- assert_eq!(post_entries.len(), 2);
+ let test_listeners = scripts.get("test").unwrap();
+ assert_eq!(test_listeners.len(), 1);
+ let post_listeners = scripts.get("post-install-cmd").unwrap();
+ assert_eq!(post_listeners.len(), 2);
}
#[test]
@@ -627,7 +597,14 @@ mod tests {
let mut descriptions = BTreeMap::new();
descriptions.insert("test".to_string(), "Run tests".to_string());
- // Just verify the function doesn't error
+ let result = list_scripts(&scripts, &descriptions, &test_console());
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_list_scripts_empty_silent() {
+ let scripts: BTreeMap<String, Vec<String>> = BTreeMap::new();
+ let descriptions: BTreeMap<String, String> = BTreeMap::new();
let result = list_scripts(&scripts, &descriptions, &test_console());
assert!(result.is_ok());
}
@@ -820,7 +797,6 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let bin_dir = dir.path().join("vendor/bin");
- // Use @no_additional_args -- extra args should not be forwarded
let mut scripts = BTreeMap::new();
scripts.insert(
"test".to_string(),
@@ -922,7 +898,6 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let bin_dir = dir.path().join("vendor/bin");
- // Run with dev mode true
let code = run_shell_command(
"test \"$COMPOSER_DEV_MODE\" = \"1\"",
dir.path(),
@@ -933,7 +908,6 @@ mod tests {
.unwrap();
assert_eq!(code, 0);
- // Run with dev mode false
let code = run_shell_command(
"test \"$COMPOSER_DEV_MODE\" = \"0\"",
dir.path(),
@@ -964,15 +938,15 @@ mod tests {
#[test]
fn test_internal_event_rejected() {
- // Internal events are in ALL_SCRIPT_EVENTS but not in ALLOWED_SCRIPT_EVENTS
- assert!(ALL_SCRIPT_EVENTS.contains(&"pre-package-install"));
- assert!(ALL_SCRIPT_EVENTS.contains(&"post-package-install"));
- assert!(ALL_SCRIPT_EVENTS.contains(&"pre-dependencies-solving"));
- assert!(!ALLOWED_SCRIPT_EVENTS.contains(&"pre-package-install"));
- assert!(!ALLOWED_SCRIPT_EVENTS.contains(&"post-package-install"));
- assert!(!ALLOWED_SCRIPT_EVENTS.contains(&"pre-dependencies-solving"));
- // User-runnable events are in both
- assert!(ALLOWED_SCRIPT_EVENTS.contains(&"pre-install-cmd"));
- assert!(ALL_SCRIPT_EVENTS.contains(&"pre-install-cmd"));
+ // Internal events are in script_events::ALL but not in USER_RUNNABLE.
+ assert!(script_events::ALL.contains(&"pre-package-install"));
+ assert!(script_events::ALL.contains(&"post-package-install"));
+ assert!(script_events::ALL.contains(&"pre-operations-exec"));
+ assert!(!script_events::USER_RUNNABLE.contains(&"pre-package-install"));
+ assert!(!script_events::USER_RUNNABLE.contains(&"post-package-install"));
+ assert!(!script_events::USER_RUNNABLE.contains(&"pre-operations-exec"));
+ // User-runnable events are in both.
+ assert!(script_events::USER_RUNNABLE.contains(&"pre-install-cmd"));
+ assert!(script_events::ALL.contains(&"pre-install-cmd"));
}
}