Skip to content

Commit a4e8975

Browse files
ptrthomasclaude
andcommitted
make ext folder composable: load extensions from both global and local
Previously, ext/ used the same override logic as dist/ and jre/ - if local .karate/ext/ existed, it replaced global ~/.karate/ext/. This made it impossible to have project-specific extensions while keeping global ones. Changes: - platform.rs: add all_ext_dirs() method returning both global and local ext paths - delegate.rs: update build_classpath() to iterate over all ext directories - doctor.rs: update extensions list to show jars from all ext directories - spec.md: clarify that ext/ is composable while dist/ and jre/ remain override Behavior: - dist/: local overrides global (unchanged) - jre/: local overrides global (unchanged) - ext/: both global AND local jars are loaded (new composable behavior) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 67426cc commit a4e8975

4 files changed

Lines changed: 46 additions & 22 deletions

File tree

docs/spec.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,12 @@ Karate CLI uses a two-level resolution for resources (dist, jre, ext):
403403
- `.karate/` folder in current working directory
404404

405405
**Resolution per resource:**
406-
- If `.karate/{resource}/` exists in cwd → use local
407-
- Otherwise → use global `{home}/{resource}/`
406+
- `dist/`: If `.karate/dist/` exists in cwd → use local, otherwise → use global
407+
- `jre/`: If `.karate/jre/` exists in cwd → use local, otherwise → use global
408+
- `ext/`: Extensions from BOTH global `~/.karate/ext/` AND local `.karate/ext/` are loaded (composable, not override)
408409

409410
**Example:** A project with `.karate/ext/` but no `.karate/jre/`:
410-
- Extensions: loaded from `.karate/ext/` (local)
411+
- Extensions: loaded from BOTH `~/.karate/ext/` (global) AND `.karate/ext/` (local)
411412
- JRE: loaded from `~/.karate/jre/` (global fallback)
412413
- Dist: loaded from `~/.karate/dist/` (global fallback)
413414

src/commands/doctor.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,12 @@ fn build_report() -> Result<DoctorReport> {
110110
// Karate JAR info
111111
let karate_jar = find_karate_jar(&paths);
112112

113-
// Extensions
114-
let extensions = list_jars(&paths.ext);
113+
// Extensions (from both global and local ext directories)
114+
let extensions: Vec<String> = paths
115+
.all_ext_dirs()
116+
.iter()
117+
.flat_map(|dir| list_jars(dir))
118+
.collect();
115119

116120
// Config info
117121
let local_config_path = KaratePaths::local_config();
@@ -292,16 +296,20 @@ fn print_report(report: &DoctorReport) {
292296

293297
// Config
294298
println!("{}", style("Configuration").bold().underlined());
295-
let global_status = if report.config.global_exists {
296-
style("✓").green()
299+
if report.config.global_exists {
300+
println!(" Global: {} {}", style("✓").green(), report.config.global_path);
297301
} else {
298-
style("-").dim()
299-
};
300-
let local_status = if report.config.local_exists {
301-
style("✓").green()
302+
println!(
303+
" Global: {}",
304+
style(format!("(none) create with: karate config --global")).dim()
305+
);
306+
}
307+
if report.config.local_exists {
308+
println!(" Local: {} {}", style("✓").green(), report.config.local_path);
302309
} else {
303-
style("-").dim()
304-
};
305-
println!(" Global: {} {}", global_status, report.config.global_path);
306-
println!(" Local: {} {}", local_status, report.config.local_path);
310+
println!(
311+
" Local: {}",
312+
style(format!("(none) create with: karate config --local")).dim()
313+
);
314+
}
307315
}

src/delegate.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,15 @@ fn find_karate_jar(dist_dir: &Path) -> Result<PathBuf> {
115115
fn build_classpath(paths: &KaratePaths, jar_path: &Path) -> Result<String> {
116116
let mut classpath_parts = vec![jar_path.to_string_lossy().to_string()];
117117

118-
// Add user extensions from ext/
119-
if paths.ext.exists() {
120-
for entry in std::fs::read_dir(&paths.ext)? {
121-
let entry = entry?;
122-
let path = entry.path();
123-
if path.extension().map(|e| e == "jar").unwrap_or(false) {
124-
classpath_parts.push(path.to_string_lossy().to_string());
118+
// Add extensions from both global and local ext directories
119+
for ext_dir in paths.all_ext_dirs() {
120+
if ext_dir.exists() {
121+
for entry in std::fs::read_dir(&ext_dir)? {
122+
let entry = entry?;
123+
let path = entry.path();
124+
if path.extension().map(|e| e == "jar").unwrap_or(false) {
125+
classpath_parts.push(path.to_string_lossy().to_string());
126+
}
125127
}
126128
}
127129
}

src/platform.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,19 @@ impl KaratePaths {
230230
pub fn has_local_overrides(&self) -> bool {
231231
self.local.is_some()
232232
}
233+
234+
/// Get all ext directories to check (global + optional local).
235+
/// Extensions are composable: both global and local ext jars are loaded.
236+
pub fn all_ext_dirs(&self) -> Vec<PathBuf> {
237+
let mut dirs = vec![self.home.join("ext")];
238+
if let Some(ref local) = self.local {
239+
let local_ext = local.join("ext");
240+
if local_ext.exists() {
241+
dirs.push(local_ext);
242+
}
243+
}
244+
dirs
245+
}
233246
}
234247

235248
impl Default for KaratePaths {

0 commit comments

Comments
 (0)