1use std::path::PathBuf;
2
3use super::extras::Language;
4use super::output::{SetupConfig, StringOrVec};
5use super::tools::{LangContext, require_tool};
6
7pub fn default_setup_workdir(lang: Language) -> Option<PathBuf> {
13 match lang {
14 Language::Swift => Some(PathBuf::from("packages/swift")),
15 Language::KotlinAndroid => Some(PathBuf::from("packages/kotlin-android")),
16 Language::Dart => Some(PathBuf::from("packages/dart")),
17 Language::Zig => Some(PathBuf::from("packages/zig")),
18 _ => None,
19 }
20}
21
22pub fn setup_config_for_language(lang: Language) -> SetupConfig {
28 SetupConfig {
29 precondition: None,
30 before: None,
31 install: None,
32 timeout_seconds: 600,
33 workdir: default_setup_workdir(lang),
34 }
35}
36
37pub(crate) fn default_setup_config(lang: Language, output_dir: &str, ctx: &LangContext) -> SetupConfig {
43 match lang {
44 Language::Rust => {
45 let mut commands: Vec<String> = vec!["rustup update stable".to_string()];
46 commands.extend(
47 ctx.tools
48 .rust_tools()
49 .iter()
50 .map(|t| format!("cargo install {t} --locked")),
51 );
52 commands.push("rustup component add rustfmt clippy".to_string());
53 SetupConfig {
54 precondition: Some(require_tool("cargo")),
55 before: None,
56 install: Some(StringOrVec::Multiple(commands)),
57 timeout_seconds: 600,
58 workdir: default_setup_workdir(lang),
59 }
60 }
61 Language::Python => {
62 let pm = ctx.tools.python_pm();
63 let install_cmd = match pm {
64 "pip" => format!("cd {output_dir} && pip install -e ."),
65 "poetry" => format!("cd {output_dir} && poetry install"),
66 _ => format!("cd {output_dir} && uv sync --no-install-project --no-install-workspace"),
67 };
68 SetupConfig {
69 precondition: Some(require_tool(pm)),
70 before: None,
71 install: Some(StringOrVec::Single(install_cmd)),
72 timeout_seconds: 600,
73 workdir: default_setup_workdir(lang),
74 }
75 }
76 Language::Node | Language::Wasm => {
77 let pm = ctx.tools.node_pm();
78 let install_cmd = match pm {
79 "npm" => format!("cd {output_dir} && npm install"),
80 "yarn" => format!("cd {output_dir} && yarn install"),
81 _ => format!("cd {output_dir} && pnpm install"),
82 };
83 SetupConfig {
84 precondition: Some(require_tool(pm)),
85 before: None,
86 install: Some(StringOrVec::Single(install_cmd)),
87 timeout_seconds: 600,
88 workdir: default_setup_workdir(lang),
89 }
90 }
91 Language::Go => SetupConfig {
92 precondition: Some(require_tool("go")),
93 before: None,
94 install: Some(StringOrVec::Single(format!(
95 "cd {output_dir} && GOWORK=off go mod download"
96 ))),
97 timeout_seconds: 600,
98 workdir: default_setup_workdir(lang),
99 },
100 Language::Ruby => SetupConfig {
101 precondition: Some(require_tool("bundle")),
102 before: None,
103 install: Some(StringOrVec::Single(format!("cd {output_dir} && bundle install"))),
104 timeout_seconds: 600,
105 workdir: default_setup_workdir(lang),
106 },
107 Language::Php => SetupConfig {
108 precondition: Some(require_tool("composer")),
109 before: None,
110 install: Some(StringOrVec::Single(format!("cd {output_dir} && composer install"))),
111 timeout_seconds: 600,
112 workdir: default_setup_workdir(lang),
113 },
114 Language::Java => SetupConfig {
115 precondition: Some(require_tool("mvn")),
116 before: None,
117 install: Some(StringOrVec::Single(format!(
118 "mvn -f {output_dir}/pom.xml dependency:resolve -q"
119 ))),
120 timeout_seconds: 600,
121 workdir: default_setup_workdir(lang),
122 },
123 Language::Csharp => SetupConfig {
124 precondition: Some(format!(
128 "command -v dotnet >/dev/null 2>&1 && [ -n \"$(find {output_dir} -maxdepth 3 \\( -name '*.sln' -o -name '*.csproj' \\) 2>/dev/null | head -1)\" ]"
129 )),
130 before: None,
131 install: Some(StringOrVec::Single(format!(
135 "dotnet restore $(find {output_dir} -maxdepth 3 \\( -name '*.sln' -o -name '*.csproj' \\) 2>/dev/null | head -1)"
136 ))),
137 timeout_seconds: 600,
138 workdir: default_setup_workdir(lang),
139 },
140 Language::Elixir => SetupConfig {
141 precondition: Some(require_tool("mix")),
142 before: None,
143 install: Some(StringOrVec::Single(format!("cd {output_dir} && mix deps.get"))),
144 timeout_seconds: 600,
145 workdir: default_setup_workdir(lang),
146 },
147 Language::R => SetupConfig {
148 precondition: Some(require_tool("Rscript")),
149 before: None,
150 install: Some(StringOrVec::Single(format!(
151 "cd {output_dir} && Rscript -e \"remotes::install_deps()\""
152 ))),
153 timeout_seconds: 600,
154 workdir: default_setup_workdir(lang),
155 },
156 Language::Ffi => SetupConfig {
157 precondition: None,
160 before: None,
161 install: None,
162 timeout_seconds: 600,
163 workdir: default_setup_workdir(lang),
164 },
165 Language::C => SetupConfig {
166 precondition: None,
167 before: None,
168 install: None,
169 timeout_seconds: 600,
170 workdir: default_setup_workdir(lang),
171 },
172 Language::Kotlin | Language::KotlinAndroid => SetupConfig {
173 precondition: Some(require_tool("gradle")),
174 before: None,
175 install: Some(StringOrVec::Single("gradle build --refresh-dependencies".to_string())),
176 timeout_seconds: 600,
177 workdir: default_setup_workdir(lang),
178 },
179 Language::Swift => SetupConfig {
180 precondition: Some(require_tool("swift")),
181 before: None,
182 install: Some(StringOrVec::Single("swift package resolve".to_string())),
183 timeout_seconds: 600,
184 workdir: default_setup_workdir(lang),
185 },
186 Language::Dart => SetupConfig {
187 precondition: Some(require_tool("dart")),
188 before: None,
189 install: Some(StringOrVec::Single("dart pub get".to_string())),
190 timeout_seconds: 600,
191 workdir: default_setup_workdir(lang),
192 },
193 Language::Zig => SetupConfig {
194 precondition: Some(require_tool("zig")),
195 before: None,
196 install: Some(StringOrVec::Single("zig build --fetch".to_string())),
197 timeout_seconds: 600,
198 workdir: default_setup_workdir(lang),
199 },
200 Language::Gleam => SetupConfig {
201 precondition: Some(require_tool("gleam")),
202 before: None,
203 install: Some(StringOrVec::Single(format!("cd {output_dir} && gleam deps download"))),
204 timeout_seconds: 600,
205 workdir: default_setup_workdir(lang),
206 },
207 Language::Jni => SetupConfig {
208 precondition: None,
209 before: None,
210 install: None,
211 timeout_seconds: 600,
212 workdir: default_setup_workdir(lang),
213 },
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::super::tools::ToolsConfig;
220 use super::*;
221
222 fn all_languages() -> Vec<Language> {
223 vec![
224 Language::Python,
225 Language::Node,
226 Language::Wasm,
227 Language::Ruby,
228 Language::Php,
229 Language::Go,
230 Language::Java,
231 Language::Csharp,
232 Language::Elixir,
233 Language::R,
234 Language::Ffi,
235 Language::Rust,
236 Language::Kotlin,
237 Language::Swift,
238 Language::Dart,
239 Language::Gleam,
240 Language::Zig,
241 ]
242 }
243
244 fn cfg(lang: Language, dir: &str) -> SetupConfig {
245 let tools = ToolsConfig::default();
246 let ctx = LangContext::default(&tools);
247 default_setup_config(lang, dir, &ctx)
248 }
249
250 #[test]
251 fn ffi_has_no_install_command() {
252 let c = cfg(Language::Ffi, "packages/ffi");
253 assert!(c.install.is_none());
254 }
255
256 #[test]
257 fn non_ffi_languages_have_install_command() {
258 for lang in all_languages() {
259 if matches!(lang, Language::Ffi) {
260 continue;
261 }
262 let c = cfg(lang, "packages/test");
263 assert!(c.install.is_some(), "{lang} should have a default install command");
264 }
265 }
266
267 #[test]
268 fn non_ffi_languages_have_default_precondition() {
269 for lang in all_languages() {
270 if matches!(lang, Language::Ffi) {
271 continue;
272 }
273 let c = cfg(lang, "packages/test");
274 let pre = c
275 .precondition
276 .unwrap_or_else(|| panic!("{lang} should have a precondition"));
277 assert!(pre.starts_with("command -v "));
278 }
279 }
280
281 #[test]
282 fn rust_install_lists_full_tool_set() {
283 let c = cfg(Language::Rust, "packages/rust");
284 let install = c.install.unwrap();
285 let cmds = install.commands();
286 let joined = cmds.join(" || ");
287 assert!(joined.contains("rustup update stable"));
288 for tool in super::super::tools::DEFAULT_RUST_DEV_TOOLS {
289 assert!(
290 joined.contains(&format!("cargo install {tool} --locked")),
291 "Rust setup should install {tool}, got: {joined}"
292 );
293 }
294 assert!(joined.contains("rustup component add rustfmt clippy"));
295 }
296
297 #[test]
298 fn rust_install_respects_user_tool_list() {
299 let tools = ToolsConfig {
300 rust_dev_tools: Some(vec!["cargo-edit".to_string(), "cargo-foo".to_string()]),
301 ..Default::default()
302 };
303 let ctx = LangContext::default(&tools);
304 let c = default_setup_config(Language::Rust, "packages/rust", &ctx);
305 let cmds = c.install.unwrap().commands().join(" || ");
306 assert!(cmds.contains("cargo install cargo-edit --locked"));
307 assert!(cmds.contains("cargo install cargo-foo --locked"));
308 assert!(!cmds.contains("cargo install cargo-deny"));
310 }
311
312 fn python_tools(pm: &str) -> ToolsConfig {
313 ToolsConfig {
314 python_package_manager: Some(pm.to_string()),
315 ..Default::default()
316 }
317 }
318
319 fn node_tools(pm: &str) -> ToolsConfig {
320 ToolsConfig {
321 node_package_manager: Some(pm.to_string()),
322 ..Default::default()
323 }
324 }
325
326 #[test]
327 fn python_setup_dispatches_on_package_manager() {
328 for (pm, expected_install, expected_pre) in [
329 (
330 "uv",
331 "uv sync --no-install-project --no-install-workspace",
332 "command -v uv >/dev/null 2>&1",
333 ),
334 ("pip", "pip install -e", "command -v pip >/dev/null 2>&1"),
335 ("poetry", "poetry install", "command -v poetry >/dev/null 2>&1"),
336 ] {
337 let tools = python_tools(pm);
338 let ctx = LangContext::default(&tools);
339 let c = default_setup_config(Language::Python, "packages/python", &ctx);
340 assert!(c.install.unwrap().commands().join(" ").contains(expected_install));
341 assert_eq!(c.precondition.as_deref(), Some(expected_pre));
342 }
343 }
344
345 #[test]
346 fn node_setup_dispatches_on_package_manager() {
347 for (pm, expected_install) in [
348 ("pnpm", "pnpm install"),
349 ("npm", "npm install"),
350 ("yarn", "yarn install"),
351 ] {
352 let tools = node_tools(pm);
353 let ctx = LangContext::default(&tools);
354 let c = default_setup_config(Language::Node, "packages/node", &ctx);
355 assert!(c.install.unwrap().commands().join(" ").contains(expected_install));
356 }
357 }
358
359 #[test]
360 fn python_uses_uv_sync_by_default() {
361 let c = cfg(Language::Python, "packages/python");
362 let install = c.install.unwrap().commands().join(" ");
363 assert!(install.contains("uv sync"));
364 assert!(install.contains("packages/python"));
365 }
366
367 #[test]
368 fn node_uses_pnpm_install_by_default() {
369 let c = cfg(Language::Node, "packages/node");
370 let install = c.install.unwrap().commands().join(" ");
371 assert!(install.contains("pnpm install"));
372 }
373
374 #[test]
375 fn wasm_matches_node() {
376 let node = cfg(Language::Node, "packages/foo");
378 let wasm = cfg(Language::Wasm, "packages/foo");
379 assert_eq!(
380 node.install.unwrap().commands().join(" "),
381 wasm.install.unwrap().commands().join(" "),
382 "WASM and Node should share install command"
383 );
384 }
385
386 #[test]
387 fn go_uses_go_mod_download() {
388 let c = cfg(Language::Go, "packages/go");
389 let install = c.install.unwrap().commands().join(" ");
390 assert!(install.contains("go mod download"));
391 }
392
393 #[test]
394 fn ruby_uses_bundle_install() {
395 let c = cfg(Language::Ruby, "packages/ruby");
396 let install = c.install.unwrap().commands().join(" ");
397 assert!(install.contains("bundle install"));
398 }
399
400 #[test]
401 fn java_uses_maven_dependency_resolve() {
402 let c = cfg(Language::Java, "packages/java");
403 let install = c.install.unwrap().commands().join(" ");
404 assert!(install.contains("mvn"));
405 assert!(install.contains("dependency:resolve"));
406 }
407
408 #[test]
409 fn csharp_uses_dotnet_restore() {
410 let c = cfg(Language::Csharp, "packages/csharp");
411 let install = c.install.unwrap().commands().join(" ");
412 assert!(install.contains("dotnet restore"));
413 }
414
415 #[test]
416 fn elixir_uses_mix_deps_get() {
417 let c = cfg(Language::Elixir, "packages/elixir");
418 let install = c.install.unwrap().commands().join(" ");
419 assert!(install.contains("mix deps.get"));
420 }
421
422 #[test]
423 fn r_uses_remotes_install_deps() {
424 let c = cfg(Language::R, "packages/r");
425 let install = c.install.unwrap().commands().join(" ");
426 assert!(install.contains("remotes::install_deps()"));
427 }
428
429 #[test]
430 fn gleam_uses_gleam_deps_download() {
431 let c = cfg(Language::Gleam, "packages/gleam");
432 let install = c.install.unwrap().commands().join(" ");
433 assert!(
434 install.contains("gleam deps download"),
435 "Gleam setup should use gleam deps download, got: {install}"
436 );
437 assert_eq!(c.precondition.as_deref(), Some("command -v gleam >/dev/null 2>&1"));
438 }
439
440 #[test]
441 fn output_dir_substituted_in_commands() {
442 let c = cfg(Language::Go, "my/custom/path");
443 let install = c.install.unwrap().commands().join(" ");
444 assert!(install.contains("my/custom/path"));
445 }
446}