Skip to main content

flodl_cli/
setup.rs

1//! `fdl setup` -- interactive guided setup wizard.
2//!
3//! Detects hardware, downloads libtorch, optionally builds Docker images.
4
5use crate::context::Context;
6use crate::libtorch::{build, detect, download};
7use crate::util::{docker, prompt, system};
8
9#[derive(Default)]
10pub struct SetupOpts {
11    /// Skip all prompts, use auto-detected defaults.
12    pub non_interactive: bool,
13    /// Re-download/rebuild even if libtorch exists.
14    pub force: bool,
15}
16
17pub fn run(opts: SetupOpts) -> Result<(), String> {
18    println!();
19    println!("  floDl Setup");
20    println!("  ===========");
21    println!();
22    println!("  floDl is a Rust deep learning framework built on libtorch");
23    println!("  (PyTorch's C++ backend). This wizard will help you set up");
24    println!("  your development environment.");
25    println!();
26
27    // ---- Step 1: Detect system ----
28
29    println!("  Step 1: Detecting your system");
30    println!("  -----------------------------");
31    println!();
32
33    let cpu = system::cpu_model().unwrap_or_else(|| "Unknown".into());
34    let threads = system::cpu_threads();
35    let ram_gb = system::ram_total_gb();
36    println!("  CPU:    {} ({} threads, {}GB RAM)", cpu, threads, ram_gb);
37
38    let has_docker = docker::has_docker();
39    let has_cargo = system::has_cargo();
40
41    if has_docker {
42        if let Some(v) = system::docker_version() {
43            println!("  Docker: {}", v);
44        } else {
45            println!("  Docker: available");
46        }
47    } else {
48        println!("  Docker: not found");
49    }
50
51    if has_cargo {
52        println!("  Rust:   available");
53    } else {
54        println!("  Rust:   not found");
55    }
56
57    let gpus = system::detect_gpus();
58    if !gpus.is_empty() {
59        println!();
60        println!("  GPUs:");
61        for g in &gpus {
62            println!(
63                "    [{}] {} -- sm_{}.{}, {}GB VRAM",
64                g.index,
65                g.name,
66                g.sm_major,
67                g.sm_minor,
68                g.total_memory_mb / 1024
69            );
70        }
71    } else {
72        println!();
73        println!("  GPU:    not detected (CPU-only mode)");
74    }
75
76    if !has_docker && !has_cargo {
77        println!();
78        println!("  You need at least one of these to continue:");
79        println!();
80        println!("    Rust:   curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh");
81        println!("    Docker: https://docs.docker.com/engine/install/");
82        println!();
83        println!("  Install one or both and run 'fdl setup' again.");
84        return Err("no Rust or Docker found".into());
85    }
86
87    // ---- Step 2: libtorch ----
88
89    println!();
90    println!("  Step 2: libtorch");
91    println!("  ----------------");
92    println!();
93    println!("  floDl needs libtorch, PyTorch's C++ library.");
94    println!("  This downloads pre-built binaries (~2GB for CUDA, ~200MB for CPU).");
95    println!();
96
97    let ctx = Context::resolve();
98    let root = &ctx.root;
99
100    if !ctx.is_project {
101        println!("  Not inside a floDl project.");
102        println!("  libtorch will be installed to: {}", ctx.libtorch_dir().display());
103        println!();
104    }
105
106    let existing = detect::read_active(root);
107    let mut skip_download = false;
108
109    if !opts.force {
110        if let Some(ref info) = existing {
111            let is_cuda = info.cuda_version.as_deref() != Some("none");
112            if is_cuda {
113                println!("  Found existing CUDA libtorch: {}", info.path);
114                if opts.non_interactive {
115                    println!("  Keeping existing installation.");
116                    skip_download = true;
117                } else if !prompt::ask_yn("  Download fresh?", false) {
118                    skip_download = true;
119                }
120                println!();
121            } else {
122                println!("  Found existing CPU libtorch.");
123            }
124        }
125    }
126
127    if !skip_download {
128        // Always download CPU variant (useful as fallback).
129        //
130        // Special case: on macOS in a Docker-Mounted project the libtorch
131        // ends up bind-mounted into a Linux container, so the host's
132        // macOS arm64 (Mach-O) build is useless. Force Linux x86_64 in
133        // that combination so the mount works as expected.
134        let mounted_docker_project =
135            ctx.is_project && ctx.root.join("Dockerfile").exists();
136        let force_linux = cfg!(target_os = "macos") && mounted_docker_project;
137        if force_linux {
138            println!("  macOS + Docker-Mounted project: fetching Linux libtorch");
139            println!("  for the container (host arch would not load inside Linux).");
140        }
141        println!("  Downloading CPU libtorch...");
142        let cpu_opts = download::DownloadOpts {
143            variant: download::Variant::Cpu,
144            activate: false, // don't activate CPU if we'll also get CUDA
145            force_linux,
146            ..Default::default()
147        };
148        download::run_with_context(cpu_opts, &ctx)?;
149
150        // CUDA libtorch
151        if !gpus.is_empty() {
152            let lo_major = gpus.iter().map(|g| g.sm_major).min().unwrap_or(0);
153            let hi_major = gpus.iter().map(|g| g.sm_major).max().unwrap_or(0);
154
155            if lo_major < 7 && hi_major >= 10 {
156                // Mixed architectures -- no single prebuilt covers both
157                println!();
158                println!("  Your GPUs span sm_{}.x to sm_{}.x.", lo_major, hi_major);
159                println!("  No pre-built libtorch covers both architectures.");
160                println!();
161
162                // Check for existing source build
163                let has_source_build = detect::list_variants(root)
164                    .iter()
165                    .any(|v| v.starts_with("builds/"));
166
167                if has_source_build {
168                    println!("  Found existing source build in libtorch/builds/.");
169                } else if opts.non_interactive {
170                    println!("  Downloading cu126 (broadest coverage).");
171                    let cuda_opts = download::DownloadOpts {
172                        variant: download::Variant::Cuda126,
173                        ..Default::default()
174                    };
175                    download::run_with_context(cuda_opts, &ctx)?;
176                } else {
177                    let choice = prompt::ask_choice(
178                        "  Choice",
179                        &[
180                            "Build libtorch from source (2-6 hours, covers all GPUs)",
181                            "Download cu128 (Volta+ only, your older GPU won't work)",
182                            "Download cu126 (pre-Volta only, your newer GPU won't work)",
183                            "Skip for now",
184                        ],
185                        4,
186                    );
187
188                    match choice {
189                        1 => {
190                            println!();
191                            println!("  Starting libtorch source build...");
192                            println!("  This will take 2-6 hours. You can safely Ctrl-C and");
193                            println!("  resume later with: fdl libtorch build");
194                            println!();
195                            build::run(build::BuildOpts::default())?;
196                        }
197                        2 => {
198                            println!("  Downloading cu128...");
199                            let cuda_opts = download::DownloadOpts {
200                                variant: download::Variant::Cuda128,
201                                ..Default::default()
202                            };
203                            download::run_with_context(cuda_opts, &ctx)?;
204                        }
205                        3 => {
206                            println!("  Downloading cu126...");
207                            let cuda_opts = download::DownloadOpts {
208                                variant: download::Variant::Cuda126,
209                                ..Default::default()
210                            };
211                            download::run_with_context(cuda_opts, &ctx)?;
212                        }
213                        _ => {
214                            println!("  Skipping CUDA libtorch. You can download later with:");
215                            println!("    fdl libtorch download --cuda 12.8");
216                            println!("    # or build from source:");
217                            println!("    fdl libtorch build");
218                        }
219                    }
220                }
221            } else if lo_major < 7 {
222                println!();
223                println!("  Downloading CUDA libtorch (cu126 for your pre-Volta GPU)...");
224                let cuda_opts = download::DownloadOpts {
225                    variant: download::Variant::Cuda126,
226                    ..Default::default()
227                };
228                download::run_with_context(cuda_opts, &ctx)?;
229            } else {
230                println!();
231                println!("  Downloading CUDA libtorch (cu128 for your Volta+ GPU)...");
232                let cuda_opts = download::DownloadOpts {
233                    variant: download::Variant::Cuda128,
234                    ..Default::default()
235                };
236                download::run_with_context(cuda_opts, &ctx)?;
237            }
238        }
239    }
240
241    // ---- Step 3: Build environment (project-only) ----
242
243    if !ctx.is_project {
244        // Skip Docker image building when running standalone
245        println!();
246        println!("  Setup complete!");
247        println!("  ===============");
248        println!();
249        if let Some(info) = detect::read_active(root) {
250            let cuda_str = if info.cuda_version.as_deref() != Some("none") { "CUDA" } else { "CPU" };
251            println!("  libtorch:  {} ({})", info.path, cuda_str);
252            println!("  Location:  {}", ctx.libtorch_dir().display());
253        }
254        println!();
255        println!("  Next steps:");
256        println!("    fdl init my-project  # scaffold a new project");
257        println!("    fdl diagnose         # verify GPU compatibility");
258        println!();
259        return Ok(());
260    }
261
262    println!();
263    println!("  Step 3: Build environment");
264    println!("  -------------------------");
265    println!();
266    println!("  floDl compiles Rust code that links against libtorch.");
267    println!("  You can build with Docker (isolated, reproducible) or");
268    println!("  natively (faster iteration, requires Rust + C++ toolchain).");
269    println!();
270
271    let build_mode = if has_docker && has_cargo {
272        if opts.non_interactive {
273            "docker"
274        } else {
275            let choice = prompt::ask_choice(
276                "  Choice",
277                &[
278                    "Docker (recommended) -- isolated, reproducible builds",
279                    "Native -- faster iteration, requires C++ compiler on host",
280                    "Both -- set up Docker and show native instructions",
281                ],
282                1,
283            );
284            match choice {
285                1 => "docker",
286                2 => "native",
287                3 => "both",
288                _ => "docker",
289            }
290        }
291    } else if has_docker {
292        if opts.non_interactive {
293            "docker"
294        } else {
295            println!("  Docker is available. Rust is not installed on this machine.");
296            println!("  Docker is the easiest way to get started (no Rust install needed).");
297            println!();
298            if prompt::ask_yn("  Set up Docker build environment?", true) {
299                "docker"
300            } else {
301                // User declined Docker but has no Rust either. Show the
302                // Rust install pointers and offer one chance to flip back
303                // to Docker before settling on a "none" build mode.
304                println!();
305                println!("  No worries. To build flodl natively you need Rust on the host:");
306                println!();
307                println!("    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh");
308                println!();
309                println!("  More: https://www.rust-lang.org/tools/install");
310                println!("  Then re-run `fdl setup` and the native path will be picked up.");
311                println!();
312                if prompt::ask_yn("  Or use Docker after all?", false) {
313                    "docker"
314                } else {
315                    "none"
316                }
317            }
318        }
319    } else {
320        println!("  Rust is available. Docker is not installed.");
321        println!("  You can build natively (requires C++ compiler on the host).");
322        println!();
323        "native"
324    };
325
326    // Build Docker images
327    if build_mode == "docker" || build_mode == "both" {
328        println!();
329        println!("  Building Docker images...");
330
331        // Create cargo cache dirs
332        let _ = std::fs::create_dir_all(".cargo-cache");
333        let _ = std::fs::create_dir_all(".cargo-git");
334
335        let status = docker::compose_run(".", &["build", "dev"])?;
336        if !status.success() {
337            println!("  Warning: CPU Docker image build failed.");
338        }
339
340        // CUDA image if we have GPUs and CUDA libtorch
341        let has_cuda_lt = detect::read_active(root)
342            .is_some_and(|i| i.cuda_version.as_deref() != Some("none"));
343
344        if !gpus.is_empty() && has_cuda_lt {
345            let _ = std::fs::create_dir_all(".cargo-cache-cuda");
346            let _ = std::fs::create_dir_all(".cargo-git-cuda");
347
348            let status = docker::compose_run(".", &["build", "cuda"])?;
349            if !status.success() {
350                println!("  Warning: CUDA Docker image build failed.");
351            }
352        }
353
354        println!("  Docker images ready.");
355    }
356
357    // ---- Summary ----
358
359    println!();
360    println!("  Setup complete!");
361    println!("  ===============");
362    println!();
363
364    // Show active libtorch
365    if let Some(info) = detect::read_active(root) {
366        let cuda_str = if info.cuda_version.as_deref() != Some("none") {
367            "CUDA"
368        } else {
369            "CPU"
370        };
371        println!("  libtorch:  {} ({})", info.path, cuda_str);
372    }
373
374    // Docker instructions
375    if build_mode == "docker" || build_mode == "both" {
376        println!();
377        println!("  Build with Docker:");
378        let has_cuda_lt = detect::read_active(root)
379            .is_some_and(|i| i.cuda_version.as_deref() != Some("none"));
380        if !gpus.is_empty() && has_cuda_lt {
381            println!("    fdl cuda-test        # run GPU tests");
382            println!("    fdl cuda-build       # compile with CUDA");
383            println!("    fdl cuda-shell       # interactive shell");
384        } else {
385            println!("    fdl test             # run tests");
386            println!("    fdl build            # compile");
387            println!("    fdl shell            # interactive shell");
388        }
389    }
390
391    // Native instructions
392    if build_mode == "native" || build_mode == "both" {
393        if let Some(info) = detect::read_active(root) {
394            let lt_path = format!("libtorch/{}", info.path);
395            println!();
396            println!("  Build natively:");
397            println!("    export LIBTORCH_PATH=\"{}\"", lt_path);
398            println!(
399                "    export LD_LIBRARY_PATH=\"$LIBTORCH_PATH/lib${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\""
400            );
401            let has_cuda_lt = info.cuda_version.as_deref() != Some("none");
402            if !gpus.is_empty() && has_cuda_lt {
403                println!("    cargo test --features cuda");
404            } else {
405                println!("    cargo test");
406            }
407        }
408    }
409
410    // No-build-environment fallback: only reachable from the
411    // docker-only-no-cargo branch where the user declined Docker
412    // twice. The Rust install pointers were already printed during
413    // Step 3; the summary just re-anchors the next move so the
414    // user doesn't drop into the trailing "Other commands" block
415    // without context.
416    if build_mode == "none" {
417        println!();
418        println!("  No build environment configured.");
419        println!("  Install Rust (link above) for native builds, or re-run `fdl setup`");
420        println!("  and pick Docker. libtorch is already in place either way.");
421    }
422
423    println!();
424    println!("  Other commands:");
425    println!("    fdl diagnose         # verify GPU compatibility");
426    println!("    fdl init my-project  # scaffold a new project");
427    println!();
428
429    if !opts.non_interactive {
430        crate::util::install_prompt::offer_global_install();
431    }
432
433    Ok(())
434}