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