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"),
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 ("uv", "uv sync", "command -v uv >/dev/null 2>&1"),
330 ("pip", "pip install -e", "command -v pip >/dev/null 2>&1"),
331 ("poetry", "poetry install", "command -v poetry >/dev/null 2>&1"),
332 ] {
333 let tools = python_tools(pm);
334 let ctx = LangContext::default(&tools);
335 let c = default_setup_config(Language::Python, "packages/python", &ctx);
336 assert!(c.install.unwrap().commands().join(" ").contains(expected_install));
337 assert_eq!(c.precondition.as_deref(), Some(expected_pre));
338 }
339 }
340
341 #[test]
342 fn node_setup_dispatches_on_package_manager() {
343 for (pm, expected_install) in [
344 ("pnpm", "pnpm install"),
345 ("npm", "npm install"),
346 ("yarn", "yarn install"),
347 ] {
348 let tools = node_tools(pm);
349 let ctx = LangContext::default(&tools);
350 let c = default_setup_config(Language::Node, "packages/node", &ctx);
351 assert!(c.install.unwrap().commands().join(" ").contains(expected_install));
352 }
353 }
354
355 #[test]
356 fn python_uses_uv_sync_by_default() {
357 let c = cfg(Language::Python, "packages/python");
358 let install = c.install.unwrap().commands().join(" ");
359 assert!(install.contains("uv sync"));
360 assert!(install.contains("packages/python"));
361 }
362
363 #[test]
364 fn node_uses_pnpm_install_by_default() {
365 let c = cfg(Language::Node, "packages/node");
366 let install = c.install.unwrap().commands().join(" ");
367 assert!(install.contains("pnpm install"));
368 }
369
370 #[test]
371 fn wasm_matches_node() {
372 let node = cfg(Language::Node, "packages/foo");
374 let wasm = cfg(Language::Wasm, "packages/foo");
375 assert_eq!(
376 node.install.unwrap().commands().join(" "),
377 wasm.install.unwrap().commands().join(" "),
378 "WASM and Node should share install command"
379 );
380 }
381
382 #[test]
383 fn go_uses_go_mod_download() {
384 let c = cfg(Language::Go, "packages/go");
385 let install = c.install.unwrap().commands().join(" ");
386 assert!(install.contains("go mod download"));
387 }
388
389 #[test]
390 fn ruby_uses_bundle_install() {
391 let c = cfg(Language::Ruby, "packages/ruby");
392 let install = c.install.unwrap().commands().join(" ");
393 assert!(install.contains("bundle install"));
394 }
395
396 #[test]
397 fn java_uses_maven_dependency_resolve() {
398 let c = cfg(Language::Java, "packages/java");
399 let install = c.install.unwrap().commands().join(" ");
400 assert!(install.contains("mvn"));
401 assert!(install.contains("dependency:resolve"));
402 }
403
404 #[test]
405 fn csharp_uses_dotnet_restore() {
406 let c = cfg(Language::Csharp, "packages/csharp");
407 let install = c.install.unwrap().commands().join(" ");
408 assert!(install.contains("dotnet restore"));
409 }
410
411 #[test]
412 fn elixir_uses_mix_deps_get() {
413 let c = cfg(Language::Elixir, "packages/elixir");
414 let install = c.install.unwrap().commands().join(" ");
415 assert!(install.contains("mix deps.get"));
416 }
417
418 #[test]
419 fn r_uses_remotes_install_deps() {
420 let c = cfg(Language::R, "packages/r");
421 let install = c.install.unwrap().commands().join(" ");
422 assert!(install.contains("remotes::install_deps()"));
423 }
424
425 #[test]
426 fn gleam_uses_gleam_deps_download() {
427 let c = cfg(Language::Gleam, "packages/gleam");
428 let install = c.install.unwrap().commands().join(" ");
429 assert!(
430 install.contains("gleam deps download"),
431 "Gleam setup should use gleam deps download, got: {install}"
432 );
433 assert_eq!(c.precondition.as_deref(), Some("command -v gleam >/dev/null 2>&1"));
434 }
435
436 #[test]
437 fn output_dir_substituted_in_commands() {
438 let c = cfg(Language::Go, "my/custom/path");
439 let install = c.install.unwrap().commands().join(" ");
440 assert!(install.contains("my/custom/path"));
441 }
442}