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::Kotlin => SetupConfig {
123 precondition: Some(require_tool("gradle")),
124 before: None,
125 install: Some(StringOrVec::Single("gradle build --refresh-dependencies".to_string())),
126 timeout_seconds: 600,
127 },
128 Language::Swift => SetupConfig {
129 precondition: Some(require_tool("swift")),
130 before: None,
131 install: Some(StringOrVec::Single("swift package resolve".to_string())),
132 timeout_seconds: 600,
133 },
134 Language::Dart => SetupConfig {
135 precondition: Some(require_tool("dart")),
136 before: None,
137 install: Some(StringOrVec::Single("dart pub get".to_string())),
138 timeout_seconds: 600,
139 },
140 Language::Gleam => SetupConfig {
141 precondition: Some(require_tool("gleam")),
142 before: None,
143 install: Some(StringOrVec::Single("gleam deps download".to_string())),
144 timeout_seconds: 600,
145 },
146 Language::Zig => SetupConfig {
147 precondition: Some(require_tool("zig")),
148 before: None,
149 install: Some(StringOrVec::Single("zig build --fetch".to_string())),
150 timeout_seconds: 600,
151 },
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::super::tools::ToolsConfig;
158 use super::*;
159
160 fn all_languages() -> Vec<Language> {
161 vec![
162 Language::Python,
163 Language::Node,
164 Language::Wasm,
165 Language::Ruby,
166 Language::Php,
167 Language::Go,
168 Language::Java,
169 Language::Csharp,
170 Language::Elixir,
171 Language::R,
172 Language::Ffi,
173 Language::Rust,
174 Language::Kotlin,
175 Language::Swift,
176 Language::Dart,
177 Language::Gleam,
178 Language::Zig,
179 ]
180 }
181
182 fn cfg(lang: Language, dir: &str) -> SetupConfig {
183 let tools = ToolsConfig::default();
184 let ctx = LangContext::default(&tools);
185 default_setup_config(lang, dir, &ctx)
186 }
187
188 #[test]
189 fn ffi_has_no_install_command() {
190 let c = cfg(Language::Ffi, "packages/ffi");
191 assert!(c.install.is_none());
192 }
193
194 #[test]
195 fn non_ffi_languages_have_install_command() {
196 for lang in all_languages() {
197 if matches!(lang, Language::Ffi) {
198 continue;
199 }
200 let c = cfg(lang, "packages/test");
201 assert!(c.install.is_some(), "{lang} should have a default install command");
202 }
203 }
204
205 #[test]
206 fn non_ffi_languages_have_default_precondition() {
207 for lang in all_languages() {
208 if matches!(lang, Language::Ffi) {
209 continue;
210 }
211 let c = cfg(lang, "packages/test");
212 let pre = c
213 .precondition
214 .unwrap_or_else(|| panic!("{lang} should have a precondition"));
215 assert!(pre.starts_with("command -v "));
216 }
217 }
218
219 #[test]
220 fn rust_install_lists_full_tool_set() {
221 let c = cfg(Language::Rust, "packages/rust");
222 let install = c.install.unwrap();
223 let cmds = install.commands();
224 let joined = cmds.join(" || ");
225 assert!(joined.contains("rustup update stable"));
226 for tool in super::super::tools::DEFAULT_RUST_DEV_TOOLS {
227 assert!(
228 joined.contains(&format!("cargo install {tool} --locked")),
229 "Rust setup should install {tool}, got: {joined}"
230 );
231 }
232 assert!(joined.contains("rustup component add rustfmt clippy"));
233 }
234
235 #[test]
236 fn rust_install_respects_user_tool_list() {
237 let tools = ToolsConfig {
238 rust_dev_tools: Some(vec!["cargo-edit".to_string(), "cargo-foo".to_string()]),
239 ..Default::default()
240 };
241 let ctx = LangContext::default(&tools);
242 let c = default_setup_config(Language::Rust, "packages/rust", &ctx);
243 let cmds = c.install.unwrap().commands().join(" || ");
244 assert!(cmds.contains("cargo install cargo-edit --locked"));
245 assert!(cmds.contains("cargo install cargo-foo --locked"));
246 assert!(!cmds.contains("cargo install cargo-deny"));
248 }
249
250 fn python_tools(pm: &str) -> ToolsConfig {
251 ToolsConfig {
252 python_package_manager: Some(pm.to_string()),
253 ..Default::default()
254 }
255 }
256
257 fn node_tools(pm: &str) -> ToolsConfig {
258 ToolsConfig {
259 node_package_manager: Some(pm.to_string()),
260 ..Default::default()
261 }
262 }
263
264 #[test]
265 fn python_setup_dispatches_on_package_manager() {
266 for (pm, expected_install, expected_pre) in [
267 ("uv", "uv sync", "command -v uv >/dev/null 2>&1"),
268 ("pip", "pip install -e", "command -v pip >/dev/null 2>&1"),
269 ("poetry", "poetry install", "command -v poetry >/dev/null 2>&1"),
270 ] {
271 let tools = python_tools(pm);
272 let ctx = LangContext::default(&tools);
273 let c = default_setup_config(Language::Python, "packages/python", &ctx);
274 assert!(c.install.unwrap().commands().join(" ").contains(expected_install));
275 assert_eq!(c.precondition.as_deref(), Some(expected_pre));
276 }
277 }
278
279 #[test]
280 fn node_setup_dispatches_on_package_manager() {
281 for (pm, expected_install) in [
282 ("pnpm", "pnpm install"),
283 ("npm", "npm install"),
284 ("yarn", "yarn install"),
285 ] {
286 let tools = node_tools(pm);
287 let ctx = LangContext::default(&tools);
288 let c = default_setup_config(Language::Node, "packages/node", &ctx);
289 assert!(c.install.unwrap().commands().join(" ").contains(expected_install));
290 }
291 }
292
293 #[test]
294 fn python_uses_uv_sync_by_default() {
295 let c = cfg(Language::Python, "packages/python");
296 let install = c.install.unwrap().commands().join(" ");
297 assert!(install.contains("uv sync"));
298 assert!(install.contains("packages/python"));
299 }
300
301 #[test]
302 fn node_uses_pnpm_install_by_default() {
303 let c = cfg(Language::Node, "packages/node");
304 let install = c.install.unwrap().commands().join(" ");
305 assert!(install.contains("pnpm install"));
306 }
307
308 #[test]
309 fn wasm_matches_node() {
310 let node = cfg(Language::Node, "packages/foo");
312 let wasm = cfg(Language::Wasm, "packages/foo");
313 assert_eq!(
314 node.install.unwrap().commands().join(" "),
315 wasm.install.unwrap().commands().join(" "),
316 "WASM and Node should share install command"
317 );
318 }
319
320 #[test]
321 fn go_uses_go_mod_download() {
322 let c = cfg(Language::Go, "packages/go");
323 let install = c.install.unwrap().commands().join(" ");
324 assert!(install.contains("go mod download"));
325 }
326
327 #[test]
328 fn ruby_uses_bundle_install() {
329 let c = cfg(Language::Ruby, "packages/ruby");
330 let install = c.install.unwrap().commands().join(" ");
331 assert!(install.contains("bundle install"));
332 }
333
334 #[test]
335 fn java_uses_maven_dependency_resolve() {
336 let c = cfg(Language::Java, "packages/java");
337 let install = c.install.unwrap().commands().join(" ");
338 assert!(install.contains("mvn"));
339 assert!(install.contains("dependency:resolve"));
340 }
341
342 #[test]
343 fn csharp_uses_dotnet_restore() {
344 let c = cfg(Language::Csharp, "packages/csharp");
345 let install = c.install.unwrap().commands().join(" ");
346 assert!(install.contains("dotnet restore"));
347 }
348
349 #[test]
350 fn elixir_uses_mix_deps_get() {
351 let c = cfg(Language::Elixir, "packages/elixir");
352 let install = c.install.unwrap().commands().join(" ");
353 assert!(install.contains("mix deps.get"));
354 }
355
356 #[test]
357 fn r_uses_remotes_install_deps() {
358 let c = cfg(Language::R, "packages/r");
359 let install = c.install.unwrap().commands().join(" ");
360 assert!(install.contains("remotes::install_deps()"));
361 }
362
363 #[test]
364 fn output_dir_substituted_in_commands() {
365 let c = cfg(Language::Go, "my/custom/path");
366 let install = c.install.unwrap().commands().join(" ");
367 assert!(install.contains("my/custom/path"));
368 }
369}