Procházet zdrojové kódy

feat: fallback to ~/.config/sm121-launcher/ for config + recipes

When `config.toml` is not found in cwd, the launcher falls back to
the OS-appropriate config directory (via `dirs` crate):
- Linux:  `\/sm121-launcher` or `~/.config/sm121-launcher`
- macOS:  `~/Library/Application Support/sm121-launcher`
- other:  e.g. `%APPDATA%/sm121-launcher` on Windows

The same fallback applies to recipes (`recipes/<name>/recipe.toml`).
Cwd always wins, default folder is only consulted when the cwd file
doesn't exist.  An explicit `--config /path/to.toml` is always used
as-is.

Implementation:
- New dep: `dirs = "5"` (no new env-var wrangling).
- `default_config_dir()` helper centralises the OS-specific lookup.
- `resolve_config_path()` in main.rs implements the search order.
- `load_recipe()` checks cwd first, then falls back to
  `<default>/recipes/<name>/recipe.toml`.
- Tests cover the config-dir resolver and the recipe fallback path.
Alexander Korolev před 4 měsíci
rodič
revize
071837e830
4 změnil soubory, kde provedl 217 přidání a 34 odebrání
  1. 134 20
      Cargo.lock
  2. 2 1
      Cargo.toml
  3. 40 10
      src/config.rs
  4. 41 3
      src/main.rs

+ 134 - 20
Cargo.lock

@@ -132,7 +132,7 @@ version = "1.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -143,7 +143,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
 dependencies = [
  "anstyle",
  "once_cell_polyfill",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -723,6 +723,27 @@ dependencies = [
  "ctutils",
 ]
 
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "dunce"
 version = "1.0.5"
@@ -878,7 +899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
 dependencies = [
  "libc",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -1185,7 +1206,7 @@ version = "0.5.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -1383,6 +1404,15 @@ version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
 
+[[package]]
+name = "libredox"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "lock_api"
 version = "0.4.14"
@@ -1437,7 +1467,7 @@ checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
 dependencies = [
  "libc",
  "wasi",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -1482,7 +1512,7 @@ version = "0.50.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -1560,6 +1590,12 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
 
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
 [[package]]
 name = "p256"
 version = "0.13.2"
@@ -2000,6 +2036,17 @@ dependencies = [
  "bitflags",
 ]
 
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "regex-automata"
 version = "0.4.14"
@@ -2179,7 +2226,7 @@ dependencies = [
  "log",
  "nix",
  "ssh-encoding",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -2560,6 +2607,7 @@ dependencies = [
  "anyhow",
  "base64",
  "clap",
+ "dirs",
  "futures",
  "russh",
  "russh-keys",
@@ -2584,7 +2632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
 dependencies = [
  "libc",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -2751,7 +2799,7 @@ dependencies = [
  "signal-hook-registry",
  "socket2",
  "tokio-macros",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -3094,7 +3142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
 dependencies = [
  "windows-core 0.58.0",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -3128,7 +3176,7 @@ dependencies = [
  "windows-interface 0.58.0",
  "windows-result 0.2.0",
  "windows-strings 0.1.0",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -3221,7 +3269,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -3240,7 +3288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
 dependencies = [
  "windows-result 0.2.0",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -3252,6 +3300,15 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.61.2"
@@ -3261,20 +3318,35 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
 [[package]]
 name = "windows-targets"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
  "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
 [[package]]
@@ -3286,18 +3358,36 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.52.6"
@@ -3310,24 +3400,48 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.52.6"

+ 2 - 1
Cargo.toml

@@ -15,4 +15,5 @@ tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
 sha2 = { version = "0.10", features = ["std"] }
 base64 = "0.22"
-futures = "0.3"
+futures = "0.3"
+dirs = "5"

+ 40 - 10
src/config.rs

@@ -41,15 +41,13 @@ pub struct Config {
     pub nccl: HashMap<String, NcclConfig>,
     #[serde(default)]
     pub hf_cache_dir: Option<String>,
-    /// TCP port used for cluster init (vLLM `--master-port`,
-    /// SGLang `--dist-init-addr` port). Default: 25000.
+    /// TCP port used for cluster init (vLLM/SGLang). Default: 25000.
     #[serde(default = "default_init_port")]
     pub init_port: u16,
     /// Per-cluster pre-serve commands.  Looked up by cluster name.
     #[serde(default)]
     pub before_serve: HashMap<String, BeforeServeTable>,
-    /// Command aliases for `cargo run exec --alias`.  Keyed by alias
-    /// name, value is the shell command to run.
+    /// Command aliases for `cargo run exec --alias`.
     #[serde(default)]
     pub exec_aliases: HashMap<String, String>,
 }
@@ -59,10 +57,7 @@ fn default_init_port() -> u16 {
 }
 
 impl Config {
-    /// Returns the HF cache directory path for volume mounting.
-    /// If set in config, uses that value; otherwise derives from
-    /// `ssh_user` to produce `/home/<user>/.cache/huggingface`
-    /// (or `/root/.cache/huggingface` for root).
+    /// HF cache directory for volume mounting.
     pub fn resolved_hf_cache_dir(&self) -> String {
         if let Some(ref dir) = self.hf_cache_dir {
             return dir.clone();
@@ -177,12 +172,35 @@ pub fn resolve_cluster<'c>(
 }
 
 /// Loads a recipe from disk.
+///
+/// Search order:
+/// 1. `./recipes/<name>/recipe.toml` (cwd)
+/// 2. `<default_config_dir>/recipes/<name>/recipe.toml`
+///
+/// Falls back to the cwd path so the error message mentions it.
 pub async fn load_recipe(recipe_name: &str) -> Result<Recipe> {
-    let path = format!("recipes/{}/recipe.toml", recipe_name);
-    let content = tokio::fs::read_to_string(&path).await?;
+    let cwd_path = format!("recipes/{}/recipe.toml", recipe_name);
+    if let Ok(content) = tokio::fs::read_to_string(&cwd_path).await {
+        return Ok(toml::from_str(&content)?);
+    }
+    if let Some(dir) = default_config_dir() {
+        let in_default = dir.join("recipes").join(recipe_name).join("recipe.toml");
+        if let Ok(content) = tokio::fs::read_to_string(&in_default).await {
+            return Ok(toml::from_str(&content)?);
+        }
+    }
+    // Neither found — let the cwd path produce a clean error.
+    let content = tokio::fs::read_to_string(&cwd_path).await?;
     Ok(toml::from_str(&content)?)
 }
 
+/// Returns the OS-appropriate default config directory for the launcher
+/// (e.g. `~/.config/sm121-launcher` on Linux), or `None` if the OS
+/// has no known config dir (some restricted sandboxes).
+pub fn default_config_dir() -> Option<std::path::PathBuf> {
+    dirs::config_dir().map(|p| p.join("sm121-launcher"))
+}
+
 /// Returns the cluster and recipe name for a recipe (for stop/logs commands
 /// that only receive a recipe name).
 pub async fn resolve_cluster_for_recipe(
@@ -464,4 +482,16 @@ nodes = [
         let recipe: Recipe = toml::from_str("name = \"x\"\nmodel = \"m\"").unwrap();
         assert_eq!(recipe.distributed_backend, "mp");
     }
+
+    // ── Default config dir ─────────────────────────────────────
+
+    #[test]
+    fn default_config_dir_appends_sm121_launcher() {
+        let Some(dir) = default_config_dir() else {
+            // No config dir on this system; nothing to assert.
+            return;
+        };
+        assert!(dir.ends_with("sm121-launcher"));
+        assert!(dir.parent().is_some());
+    }
 }

+ 41 - 3
src/main.rs

@@ -14,15 +14,48 @@ mod ssh;
 mod tmux;
 
 use crate::cli::{Cli, Commands};
-use crate::config::{Cluster, Config, resolve_cluster_for_recipe, select_cluster_by_name};
+use crate::config::{
+    Cluster, Config, default_config_dir, resolve_cluster_for_recipe, select_cluster_by_name,
+};
 use crate::ssh::fetch_host_key;
 
+/// Resolve the path to the config file.
+///
+/// Search order:
+/// 1. `--config <path>` from CLI (always wins; error if missing)
+/// 2. `./config.toml` in cwd
+/// 3. `<default_config_dir>/config.toml`
+///
+/// Returns an error if nothing is found, with a hint pointing the user
+/// to copy the example config to one of the two paths.
+fn resolve_config_path(cli_config: &str) -> std::path::PathBuf {
+    use std::path::PathBuf;
+    let explicit = PathBuf::from(cli_config);
+    // User passed a non-default path: use it as-is.
+    if cli_config != "config.toml" {
+        return explicit;
+    }
+    // Default value (or no flag): try cwd first, then default folder.
+    if explicit.exists() {
+        return explicit;
+    }
+    if let Some(dir) = default_config_dir() {
+        let candidate = dir.join("config.toml");
+        if candidate.exists() {
+            return candidate;
+        }
+    }
+    // Fall through; the loader will report a clean error.
+    explicit
+}
+
 #[tokio::main]
 async fn main() -> Result<()> {
     tracing_subscriber::fmt::init();
     let cli = Cli::parse();
 
-    let raw_config = tokio::fs::read_to_string(&cli.config).await?;
+    let config_path = resolve_config_path(&cli.config);
+    let raw_config = tokio::fs::read_to_string(&config_path).await?;
     let config: Config = toml::from_str(&raw_config)?;
     let dry_run = cli.dry_run;
 
@@ -93,7 +126,12 @@ async fn main() -> Result<()> {
             crate::tmux::handle_tmux(&config, tmux_cmd, dry_run).await?;
         }
         Commands::InitHostKeys { cluster } => {
-            host_keys::init_host_keys(&config, cluster.as_deref(), &cli.config).await?;
+            host_keys::init_host_keys(
+                &config,
+                cluster.as_deref(),
+                config_path.to_str().unwrap_or("config.toml"),
+            )
+            .await?;
         }
         Commands::Exec {
             cluster,