diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-23 01:23:40 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-23 01:23:40 +0900 |
| commit | c7a5859e1770dbc7cbc7cb1785c34b880162dfac (patch) | |
| tree | 58081330c415776c9020771f12227889abcffa12 /crates/mozart/src/commands | |
| parent | 7372dc668476cde4bcb789e586cb9a65af1afca0 (diff) | |
| download | php-mozart-c7a5859e1770dbc7cbc7cb1785c34b880162dfac.tar.gz php-mozart-c7a5859e1770dbc7cbc7cb1785c34b880162dfac.tar.zst php-mozart-c7a5859e1770dbc7cbc7cb1785c34b880162dfac.zip | |
fix(global): add ~/.composer fallback, XDG auto-detection, and no-arg handling
Rewrite composer_home() with Composer-compatible resolution: detect XDG
environment (any XDG_* var or /etc/xdg), prefer existing directories,
and fall back to ~/.composer for legacy systems. Consolidate duplicate
composer_home_dir() from global.rs into shared config_helpers. Accept
no subcommand gracefully with a helpful error instead of a parse error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src/commands')
| -rw-r--r-- | crates/mozart/src/commands/config.rs | 6 | ||||
| -rw-r--r-- | crates/mozart/src/commands/config_helpers.rs | 60 | ||||
| -rw-r--r-- | crates/mozart/src/commands/dump_autoload.rs | 17 | ||||
| -rw-r--r-- | crates/mozart/src/commands/global.rs | 99 | ||||
| -rw-r--r-- | crates/mozart/src/commands/repository.rs | 2 |
5 files changed, 84 insertions, 100 deletions
diff --git a/crates/mozart/src/commands/config.rs b/crates/mozart/src/commands/config.rs index 5105fd6..92f9b59 100644 --- a/crates/mozart/src/commands/config.rs +++ b/crates/mozart/src/commands/config.rs @@ -147,7 +147,7 @@ impl ComposerConfig { .unwrap_or("vendor") .to_string(); - let home = composer_home(); + let home = composer_home().to_string_lossy().into_owned(); let cache_dir = self .values @@ -466,7 +466,7 @@ fn resolve_config_file_path(args: &ConfigArgs, cli: &super::Cli) -> anyhow::Resu anyhow::bail!("Cannot combine --global and --file"); } if args.global { - return Ok(PathBuf::from(composer_home()).join("config.json")); + return Ok(composer_home().join("config.json")); } if let Some(ref file) = args.file { return Ok(PathBuf::from(file)); @@ -900,7 +900,7 @@ fn execute_read( let mut config = ComposerConfig::defaults(); if args.global { - let global_config_path = PathBuf::from(composer_home()).join("config.json"); + let global_config_path = composer_home().join("config.json"); let overrides = load_config_section(&global_config_path)?; config.merge(&overrides); } else { diff --git a/crates/mozart/src/commands/config_helpers.rs b/crates/mozart/src/commands/config_helpers.rs index 31792f4..fe5deb8 100644 --- a/crates/mozart/src/commands/config_helpers.rs +++ b/crates/mozart/src/commands/config_helpers.rs @@ -2,32 +2,64 @@ use anyhow::anyhow; use std::path::{Path, PathBuf}; /// Return the Composer home directory, respecting `COMPOSER_HOME` and -/// falling back to the platform default (`~/.config/composer` on Unix, -/// `%APPDATA%/Composer` on Windows). -pub(crate) fn composer_home() -> String { - if let Ok(home) = std::env::var("COMPOSER_HOME") { - return home; +/// falling back to the platform default using Composer-compatible logic. +/// +/// On Unix: +/// - If XDG is in use (any `XDG_*` env var exists, or `/etc/xdg` exists), +/// prefer `$XDG_CONFIG_HOME/composer` (or `$HOME/.config/composer`). +/// - Always include `$HOME/.composer` as a fallback candidate. +/// - Return the first candidate directory that exists on disk; +/// if none exist, return the first candidate. +pub(crate) fn composer_home() -> PathBuf { + if let Ok(val) = std::env::var("COMPOSER_HOME") + && !val.is_empty() + { + return PathBuf::from(val); } #[cfg(target_os = "windows")] { - std::env::var("APPDATA") - .map(|p| format!("{p}/Composer")) - .unwrap_or_else(|_| "C:/ProgramData/ComposerSetup/bin".to_string()) + if let Ok(appdata) = std::env::var("APPDATA") + && !appdata.is_empty() + { + return PathBuf::from(appdata).join("Composer"); + } + return PathBuf::from("C:/ProgramData/ComposerSetup/bin"); } #[cfg(not(target_os = "windows"))] { - if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { - format!("{xdg}/composer") - } else { - std::env::var("HOME") - .map(|h| format!("{h}/.config/composer")) - .unwrap_or_else(|_| "/tmp/composer".to_string()) + let home_dir = std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/tmp")); + + let mut candidates: Vec<PathBuf> = Vec::new(); + + if use_xdg() { + let xdg_config = std::env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| home_dir.join(".config")); + candidates.push(xdg_config.join("composer")); } + + candidates.push(home_dir.join(".composer")); + + // Return first candidate that exists; otherwise return the first + candidates + .iter() + .find(|p| p.is_dir()) + .cloned() + .unwrap_or_else(|| candidates.into_iter().next().unwrap()) } } +/// Check whether XDG base directories are in use: +/// any env var starting with `XDG_` exists, OR `/etc/xdg` directory exists. +fn use_xdg() -> bool { + std::env::vars().any(|(k, _)| k.starts_with("XDG_")) + || std::path::Path::new("/etc/xdg").is_dir() +} + /// Build the working directory path, preferring `--working-dir` over `cwd`. pub(crate) fn working_dir(cli: &super::Cli) -> anyhow::Result<PathBuf> { match &cli.working_dir { diff --git a/crates/mozart/src/commands/dump_autoload.rs b/crates/mozart/src/commands/dump_autoload.rs index 0828288..ca559f8 100644 --- a/crates/mozart/src/commands/dump_autoload.rs +++ b/crates/mozart/src/commands/dump_autoload.rs @@ -71,14 +71,15 @@ pub async fn execute( let mut composer_config = super::config::ComposerConfig::defaults(); if composer_json_path.exists() && let Ok(content) = std::fs::read_to_string(&composer_json_path) - && let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) - && let Some(cfg_obj) = value.get("config").and_then(|v| v.as_object()) { - let overrides: std::collections::BTreeMap<String, serde_json::Value> = cfg_obj - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - composer_config.merge(&overrides); - } + && let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) + && let Some(cfg_obj) = value.get("config").and_then(|v| v.as_object()) + { + let overrides: std::collections::BTreeMap<String, serde_json::Value> = cfg_obj + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + composer_config.merge(&overrides); + } let optimize = args.optimize || composer_config diff --git a/crates/mozart/src/commands/global.rs b/crates/mozart/src/commands/global.rs index dfa4fc2..12b3a4e 100644 --- a/crates/mozart/src/commands/global.rs +++ b/crates/mozart/src/commands/global.rs @@ -1,10 +1,9 @@ use clap::Args; -use std::path::PathBuf; #[derive(Args)] pub struct GlobalArgs { /// The command name to run - pub command_name: String, + pub command_name: Option<String>, /// Arguments to pass to the command #[arg(trailing_var_arg = true, allow_hyphen_values = true)] @@ -21,7 +20,16 @@ pub async fn execute( use clap::Parser as _; use std::fs; - let home = composer_home_dir()?; + let command_name = match &args.command_name { + Some(name) => name.clone(), + None => { + anyhow::bail!( + "The global command requires a subcommand, e.g. `mozart global require package/name`" + ); + } + }; + + let home = super::config_helpers::composer_home(); fs::create_dir_all(&home)?; @@ -36,7 +44,7 @@ pub async fn execute( argv.extend(append_global_options(cli)); argv.push("--working-dir".to_string()); argv.push(home.to_string_lossy().into_owned()); - argv.push(args.command_name.clone()); + argv.push(command_name); argv.extend(args.args.iter().cloned()); let new_cli = super::Cli::try_parse_from(&argv)?; @@ -45,26 +53,6 @@ pub async fn execute( // ─── Helpers ───────────────────────────────────────────────────────────────── -fn composer_home_dir() -> anyhow::Result<PathBuf> { - if let Ok(val) = std::env::var("COMPOSER_HOME") - && !val.is_empty() - { - return Ok(PathBuf::from(val)); - } - - if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") - && !xdg.is_empty() - { - return Ok(PathBuf::from(xdg).join("composer")); - } - - let home = std::env::var("HOME") - .map(PathBuf::from) - .map_err(|_| anyhow::anyhow!("Cannot determine home directory: $HOME is not set"))?; - - Ok(home.join(".config").join("composer")) -} - fn append_global_options(cli: &super::Cli) -> Vec<String> { let mut opts: Vec<String> = Vec::new(); @@ -119,54 +107,6 @@ mod tests { Cli::try_parse_from(["mozart", "about"]).unwrap() } - // ── composer_home_dir tests ─────────────────────────────────────────────── - - /// Guards env-var mutations so the three composer_home_dir tests - /// cannot race each other when `cargo test` runs them in parallel. - static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); - - #[test] - fn test_composer_home_dir_from_env() { - let _lock = ENV_MUTEX.lock().unwrap(); - // SAFETY: test-only; protected by ENV_MUTEX - unsafe { - std::env::set_var("COMPOSER_HOME", "/tmp/test-composer-home"); - } - let result = composer_home_dir().unwrap(); - unsafe { - std::env::remove_var("COMPOSER_HOME"); - } - assert_eq!(result, PathBuf::from("/tmp/test-composer-home")); - } - - #[test] - fn test_composer_home_dir_xdg() { - let _lock = ENV_MUTEX.lock().unwrap(); - // SAFETY: test-only; protected by ENV_MUTEX - unsafe { - std::env::remove_var("COMPOSER_HOME"); - std::env::set_var("XDG_CONFIG_HOME", "/tmp/test-xdg-config"); - } - let result = composer_home_dir().unwrap(); - unsafe { - std::env::remove_var("XDG_CONFIG_HOME"); - } - assert_eq!(result, PathBuf::from("/tmp/test-xdg-config/composer")); - } - - #[test] - fn test_composer_home_dir_default() { - let _lock = ENV_MUTEX.lock().unwrap(); - // SAFETY: test-only; protected by ENV_MUTEX - unsafe { - std::env::remove_var("COMPOSER_HOME"); - std::env::remove_var("XDG_CONFIG_HOME"); - } - let result = composer_home_dir().unwrap(); - let home = std::env::var("HOME").map(PathBuf::from).unwrap(); - assert_eq!(result, home.join(".config").join("composer")); - } - // ── append_global_options tests ─────────────────────────────────────────── #[test] @@ -222,7 +162,7 @@ mod tests { // Verify GlobalArgs parses correctly through the CLI let cli = Cli::try_parse_from(["mozart", "global", "require", "vendor/package"]).unwrap(); if let Some(Commands::Global(args)) = cli.command { - assert_eq!(args.command_name, "require"); + assert_eq!(args.command_name, Some("require".to_string())); assert_eq!(args.args, vec!["vendor/package"]); } else { panic!("Expected Global command"); @@ -235,10 +175,21 @@ mod tests { let cli = Cli::try_parse_from(["mozart", "global", "require", "vendor/pkg", "--no-update"]) .unwrap(); if let Some(Commands::Global(args)) = cli.command { - assert_eq!(args.command_name, "require"); + assert_eq!(args.command_name, Some("require".to_string())); assert!(args.args.contains(&"--no-update".to_string())); } else { panic!("Expected Global command"); } } + + #[test] + fn test_global_args_no_subcommand() { + // Verify that no subcommand parses to None + let cli = Cli::try_parse_from(["mozart", "global"]).unwrap(); + if let Some(Commands::Global(args)) = cli.command { + assert_eq!(args.command_name, None); + } else { + panic!("Expected Global command"); + } + } } diff --git a/crates/mozart/src/commands/repository.rs b/crates/mozart/src/commands/repository.rs index e9c52f4..f8e151d 100644 --- a/crates/mozart/src/commands/repository.rs +++ b/crates/mozart/src/commands/repository.rs @@ -47,7 +47,7 @@ fn resolve_file_path(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result< anyhow::bail!("Cannot combine --global and --file"); } if args.global { - return Ok(PathBuf::from(composer_home()).join("config.json")); + return Ok(composer_home().join("config.json")); } if let Some(ref file) = args.file { return Ok(PathBuf::from(file)); |
