1use crate::context::Context;
6use crate::libtorch::{build, detect, download};
7use crate::util::{docker, prompt, requirements, system};
8
9const CPU_VARIANT: &str = "precompiled/cpu";
11
12#[derive(Default)]
13pub struct SetupOpts {
14 pub non_interactive: bool,
16 pub force: bool,
18}
19
20#[derive(Debug, PartialEq, Eq, Clone, Copy)]
26enum MacDockerPlan {
27 HostBuild,
29 ForceLinuxX86,
32 HostBuildThenManualArm64,
37}
38
39fn macos_docker_plan(os: &str, arch: &str, docker_project: bool) -> MacDockerPlan {
43 if os != "macos" || !docker_project {
44 return MacDockerPlan::HostBuild;
45 }
46 match arch {
47 "aarch64" => MacDockerPlan::HostBuildThenManualArm64,
48 _ => MacDockerPlan::ForceLinuxX86,
49 }
50}
51
52pub fn run(opts: SetupOpts) -> Result<(), String> {
53 println!();
54 println!(" floDl Setup");
55 println!(" ===========");
56 println!();
57 println!(" floDl is a Rust deep learning framework built on libtorch");
58 println!(" (PyTorch's C++ backend). This wizard will help you set up");
59 println!(" your development environment.");
60 println!();
61
62 println!(" Step 1: Detecting your system");
65 println!(" -----------------------------");
66 println!();
67
68 let cpu = system::cpu_model().unwrap_or_else(|| "Unknown".into());
69 let threads = system::cpu_threads();
70 let ram_gb = system::ram_total_gb();
71 println!(" CPU: {} ({} threads, {}GB RAM)", cpu, threads, ram_gb);
72
73 let has_docker = docker::has_docker();
74 let has_cargo = system::has_cargo();
75
76 if has_docker {
77 if let Some(v) = system::docker_version() {
78 println!(" Docker: {}", v);
79 } else {
80 println!(" Docker: available");
81 }
82 } else {
83 println!(" Docker: not found");
84 }
85
86 if has_cargo {
87 println!(" Rust: available");
88 } else {
89 println!(" Rust: not found");
90 }
91
92 let survey = flodl_hw::survey();
93 let gpus = &survey.devices;
94 if !gpus.is_empty() {
95 println!();
96 println!(" GPUs:");
97 for g in gpus {
98 println!(
99 " [{}] {} -- {}, {}GB VRAM",
100 g.index,
101 g.name,
102 g.arch_label(),
103 g.total_memory_mb / 1024
104 );
105 }
106 } else {
107 println!();
108 println!(" GPU: not detected (CPU-only mode)");
109 for note in survey.notes.iter().filter(|n| n.kind.explains_absence()) {
115 println!(" {}", note.message);
116 }
117 }
118
119 if !has_docker && !has_cargo {
120 println!();
121 println!(" You need at least one of these to continue:");
122 println!();
123 println!(" Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh");
124 println!(" Docker: https://docs.docker.com/engine/install/");
125 println!();
126 println!(" Install one or both and run 'fdl setup' again.");
127 return Err("no Rust or Docker found".into());
128 }
129
130 let tools = requirements::missing_host_tools();
135 if !tools.is_empty() && has_cargo {
136 let owned: Vec<String> = tools.iter().map(|t| (*t).to_string()).collect();
137 println!();
138 if has_docker {
139 println!(
140 " Note: building natively would also need: {}",
141 tools.join(", ")
142 );
143 println!(" {}", requirements::install_hint(&owned));
144 println!(" (not needed if you build in the dev container)");
145 } else {
146 println!(" Native builds need these first: {}", tools.join(", "));
147 println!(" {}", requirements::install_hint(&owned));
148 }
149 }
150
151 println!();
154 println!(" Step 2: libtorch");
155 println!(" ----------------");
156 println!();
157 println!(" floDl needs libtorch, PyTorch's C++ library.");
158 println!(" This downloads pre-built binaries (~2GB for CUDA, ~200MB for CPU).");
159 println!();
160
161 let ctx = Context::resolve();
162 let root = &ctx.root;
163
164 if !ctx.is_project {
165 println!(" Not inside a floDl project.");
166 println!(
167 " libtorch will be installed to: {}",
168 ctx.libtorch_dir().display()
169 );
170 println!();
171 }
172
173 let existing = detect::read_active(root);
174 let mut skip_download = false;
175
176 if !opts.force
177 && let Some(ref info) = existing
178 {
179 match detect::variant_vendor(&info.path) {
185 Some(vendor) => {
186 println!(" Found existing {vendor} libtorch: {}", info.path);
187 if opts.non_interactive {
188 println!(" Keeping existing installation.");
189 skip_download = true;
190 } else if !prompt::ask_yn(" Download fresh?", false) {
191 skip_download = true;
192 }
193 println!();
194 }
195 None => println!(" Found existing CPU libtorch."),
196 }
197 }
198
199 if !skip_download {
200 let mounted_docker_project = ctx.is_project && ctx.root.join("Dockerfile").exists();
202 let plan = macos_docker_plan(
203 std::env::consts::OS,
204 std::env::consts::ARCH,
205 mounted_docker_project,
206 );
207 let force_linux = plan == MacDockerPlan::ForceLinuxX86;
208 let apple_silicon_docker = plan == MacDockerPlan::HostBuildThenManualArm64;
209 if force_linux {
210 println!(" macOS + Docker-mounted project: fetching Linux libtorch");
211 println!(" for the container (host arch would not load inside Linux).");
212 }
213 println!(" Downloading CPU libtorch...");
214 let cpu_opts = download::DownloadOpts {
215 variant: download::Variant::Cpu,
216 activate: false, force_linux,
218 ..Default::default()
219 };
220 download::run_with_context(cpu_opts, &ctx)?;
221
222 if apple_silicon_docker {
223 println!();
224 println!(" That is the macOS build, for the host. The dev container is");
225 println!(" linux/arm64 and needs Linux aarch64 libtorch, which PyTorch");
226 println!(" does not publish; it has to be extracted from the PyPI wheel.");
227 println!(" Steps 1 and 2 of the Apple Silicon guide do this:");
228 println!(" https://flodl.dev/guide/mac-apple-silicon");
229 println!(" Until then `fdl build` / `fdl test` will not link.");
230 }
231
232 let majors: Vec<u32> = gpus.iter().filter_map(|g| g.sm_major()).collect();
236
237 let amd: Vec<_> = gpus
241 .iter()
242 .filter(|g| g.vendor == system::GpuVendor::Amd)
243 .collect();
244 if !amd.is_empty() {
245 let covered = download::rocm_covered(gpus);
246 if !majors.is_empty() {
247 println!();
248 println!(" AMD GPU(s) detected alongside NVIDIA. One libtorch build");
249 println!(" serves one vendor, so the NVIDIA cards are set up here.");
250 println!(" For the AMD cards: fdl libtorch download --rocm 7.0");
251 } else if covered.is_empty() {
252 let names: Vec<String> = amd
253 .iter()
254 .map(|g| format!("{} ({})", g.short_name(), g.arch_label()))
255 .collect();
256 println!();
257 println!(
258 " AMD GPU(s) detected ({}) outside the ROCm 7.0",
259 names.join(", ")
260 );
261 println!(" build's targets, so only CPU libtorch is installed.");
262 println!(" Covered targets: {}", download::rocm_archs());
263 } else {
264 println!();
265 println!(" Downloading ROCm libtorch (rocm7.0 for your AMD GPU)...");
266 let rocm_opts = download::DownloadOpts {
267 variant: download::Variant::Rocm70,
268 ..Default::default()
269 };
270 download::run_with_context(rocm_opts, &ctx)?;
271 }
272 }
273
274 if !majors.is_empty() {
276 let lo_major = majors.iter().copied().min().unwrap_or(0);
277 let hi_major = majors.iter().copied().max().unwrap_or(0);
278
279 if lo_major < 7 && hi_major >= 10 {
280 println!();
282 println!(" Your GPUs span sm_{}.x to sm_{}.x.", lo_major, hi_major);
283 println!(" No pre-built libtorch covers both architectures.");
284 println!();
285
286 let has_source_build = detect::list_variants(root)
288 .iter()
289 .any(|v| v.starts_with("builds/"));
290
291 if has_source_build {
292 println!(" Found existing source build in libtorch/builds/.");
293 } else if opts.non_interactive {
294 println!(" Downloading cu126 (broadest coverage).");
295 let cuda_opts = download::DownloadOpts {
296 variant: download::Variant::Cuda126,
297 ..Default::default()
298 };
299 download::run_with_context(cuda_opts, &ctx)?;
300 } else {
301 let choice = prompt::ask_choice(
302 " Choice",
303 &[
304 "Build libtorch from source (2-6 hours, covers all GPUs)",
305 "Download cu128 (Volta+ only, your older GPU won't work)",
306 "Download cu126 (pre-Volta only, your newer GPU won't work)",
307 "Skip for now",
308 ],
309 4,
310 );
311
312 match choice {
313 1 => {
314 println!();
315 println!(" Starting libtorch source build...");
316 println!(" This will take 2-6 hours. You can safely Ctrl-C and");
317 println!(" resume later with: fdl libtorch build");
318 println!();
319 build::run(build::BuildOpts::default())?;
320 }
321 2 => {
322 println!(" Downloading cu128...");
323 let cuda_opts = download::DownloadOpts {
324 variant: download::Variant::Cuda128,
325 ..Default::default()
326 };
327 download::run_with_context(cuda_opts, &ctx)?;
328 }
329 3 => {
330 println!(" Downloading cu126...");
331 let cuda_opts = download::DownloadOpts {
332 variant: download::Variant::Cuda126,
333 ..Default::default()
334 };
335 download::run_with_context(cuda_opts, &ctx)?;
336 }
337 _ => {
338 println!(" Skipping CUDA libtorch. You can download later with:");
339 println!(" fdl libtorch download --cuda 12.8");
340 println!(" # or build from source:");
341 println!(" fdl libtorch build");
342 }
343 }
344 }
345 } else if lo_major < 7 {
346 println!();
347 println!(" Downloading CUDA libtorch (cu126 for your pre-Volta GPU)...");
348 let cuda_opts = download::DownloadOpts {
349 variant: download::Variant::Cuda126,
350 ..Default::default()
351 };
352 download::run_with_context(cuda_opts, &ctx)?;
353 } else {
354 println!();
355 println!(" Downloading CUDA libtorch (cu128 for your Volta+ GPU)...");
356 let cuda_opts = download::DownloadOpts {
357 variant: download::Variant::Cuda128,
358 ..Default::default()
359 };
360 download::run_with_context(cuda_opts, &ctx)?;
361 }
362 }
363
364 if detect::read_active(root).is_none() && detect::is_valid_variant(root, CPU_VARIANT) {
373 detect::set_active(root, CPU_VARIANT)?;
374 }
375 }
376
377 let active = detect::read_active(root);
383 let active_vendor = active
384 .as_ref()
385 .and_then(|info| detect::variant_vendor(&info.path));
386 let active_label = |v: Option<system::GpuVendor>| match v {
387 Some(vendor) => vendor.to_string(),
388 None => "CPU".to_string(),
389 };
390
391 if !ctx.is_project {
394 println!();
396 println!(" Setup complete!");
397 println!(" ===============");
398 println!();
399 if let Some(info) = &active {
400 println!(
401 " libtorch: {} ({})",
402 info.path,
403 active_label(active_vendor)
404 );
405 println!(" Location: {}", ctx.libtorch_dir().display());
406 }
407 println!();
408 println!(" Next steps:");
409 println!(" fdl init my-project # scaffold a new project");
410 println!(" fdl diagnose # verify GPU compatibility");
411 println!();
412 return Ok(());
413 }
414
415 println!();
416 println!(" Step 3: Build environment");
417 println!(" -------------------------");
418 println!();
419 println!(" floDl compiles Rust code that links against libtorch.");
420 println!(" You can build with Docker (isolated, reproducible) or");
421 println!(" natively (faster iteration, requires Rust + C++ toolchain).");
422 println!();
423
424 let build_mode = if has_docker && has_cargo {
425 if opts.non_interactive {
426 "docker"
427 } else {
428 let choice = prompt::ask_choice(
429 " Choice",
430 &[
431 "Docker (recommended) -- isolated, reproducible builds",
432 "Native -- faster iteration, requires C++ compiler on host",
433 "Both -- set up Docker and show native instructions",
434 ],
435 1,
436 );
437 match choice {
438 1 => "docker",
439 2 => "native",
440 3 => "both",
441 _ => "docker",
442 }
443 }
444 } else if has_docker {
445 if opts.non_interactive {
446 "docker"
447 } else {
448 println!(" Docker is available. Rust is not installed on this machine.");
449 println!(" Docker is the easiest way to get started (no Rust install needed).");
450 println!();
451 if prompt::ask_yn(" Set up Docker build environment?", true) {
452 "docker"
453 } else {
454 println!();
458 println!(" No worries. To build flodl natively you need Rust on the host:");
459 println!();
460 println!(" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh");
461 println!();
462 println!(" More: https://www.rust-lang.org/tools/install");
463 println!(" Then re-run `fdl setup` and the native path will be picked up.");
464 println!();
465 if prompt::ask_yn(" Or use Docker after all?", false) {
466 "docker"
467 } else {
468 "none"
469 }
470 }
471 }
472 } else {
473 println!(" Rust is available. Docker is not installed.");
474 println!(" You can build natively (requires C++ compiler on the host).");
475 println!();
476 "native"
477 };
478
479 if build_mode == "docker" || build_mode == "both" {
481 println!();
482 println!(" Building Docker images...");
483
484 let _ = std::fs::create_dir_all(".cargo-cache");
486 let _ = std::fs::create_dir_all(".cargo-git");
487
488 let status = docker::compose_run(".", &["build", "dev"])?;
489 if !status.success() {
490 println!(" Warning: CPU Docker image build failed.");
491 }
492
493 if let Some(vendor) = active_vendor.filter(|_| !gpus.is_empty()) {
499 let service = crate::run::resolve_docker_service(crate::run::LOGICAL_GPU_SERVICE, root);
500 let _ = std::fs::create_dir_all(format!(".cargo-cache-{service}"));
501 let _ = std::fs::create_dir_all(format!(".cargo-git-{service}"));
502
503 let status = docker::compose_run(".", &["build", &service])?;
504 if !status.success() {
505 println!(" Warning: {vendor} Docker image build failed.");
506 }
507 }
508
509 println!(" Docker images ready.");
510 }
511
512 println!();
515 println!(" Setup complete!");
516 println!(" ===============");
517 println!();
518
519 if let Some(info) = &active {
521 println!(
522 " libtorch: {} ({})",
523 info.path,
524 active_label(active_vendor)
525 );
526 }
527
528 let gpu_ready = !gpus.is_empty() && active_vendor.is_some();
529
530 if build_mode == "docker" || build_mode == "both" {
532 println!();
533 println!(" Build with Docker:");
534 if gpu_ready {
535 println!(" fdl gpu-test # run GPU tests");
536 println!(" fdl gpu-build # compile for the GPU");
537 println!(" fdl gpu-shell # interactive shell");
538 } else {
539 println!(" fdl test # run tests");
540 println!(" fdl build # compile");
541 println!(" fdl shell # interactive shell");
542 }
543 }
544
545 if (build_mode == "native" || build_mode == "both")
547 && let Some(info) = &active
548 {
549 let lt_path = format!("libtorch/{}", info.path);
550 println!();
551 println!(" Build natively:");
552 println!(" export LIBTORCH_PATH=\"{}\"", lt_path);
553 for line in detect::ld_library_path_lines(active_vendor, "$LIBTORCH_PATH/lib") {
554 println!(" {line}");
555 }
556 match active_vendor.filter(|_| gpu_ready) {
557 Some(vendor) => println!(" cargo test --features {}", vendor.cargo_feature()),
558 None => println!(" cargo test"),
559 }
560 }
561
562 if build_mode == "none" {
569 println!();
570 println!(" No build environment configured.");
571 println!(" Install Rust (link above) for native builds, or re-run `fdl setup`");
572 println!(" and pick Docker. libtorch is already in place either way.");
573 }
574
575 println!();
576 println!(" Other commands:");
577 println!(" fdl diagnose # verify GPU compatibility");
578 println!(" fdl init my-project # scaffold a new project");
579 println!();
580
581 if !opts.non_interactive {
582 crate::util::install_prompt::offer_global_install();
583 }
584
585 Ok(())
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
597 fn apple_silicon_docker_never_forces_an_x86_download() {
598 assert_eq!(
601 macos_docker_plan("macos", "aarch64", true),
602 MacDockerPlan::HostBuildThenManualArm64
603 );
604 }
605
606 #[test]
607 fn intel_mac_docker_forces_the_linux_build() {
608 assert_eq!(
609 macos_docker_plan("macos", "x86_64", true),
610 MacDockerPlan::ForceLinuxX86
611 );
612 }
613
614 #[test]
615 fn a_mac_without_a_docker_project_builds_for_the_host() {
616 for arch in ["aarch64", "x86_64"] {
617 assert_eq!(
618 macos_docker_plan("macos", arch, false),
619 MacDockerPlan::HostBuild,
620 "{arch} native"
621 );
622 }
623 }
624
625 #[test]
626 fn non_macos_hosts_are_unaffected() {
627 for (os, arch) in [
628 ("linux", "x86_64"),
629 ("linux", "aarch64"),
630 ("windows", "x86_64"),
631 ] {
632 assert_eq!(
633 macos_docker_plan(os, arch, true),
634 MacDockerPlan::HostBuild,
635 "{os}/{arch}"
636 );
637 }
638 }
639}