use crate::context::Context;
use crate::libtorch::{build, detect, download};
use crate::util::{docker, prompt, requirements, system};
const CPU_VARIANT: &str = "precompiled/cpu";
#[derive(Default)]
pub struct SetupOpts {
pub non_interactive: bool,
pub force: bool,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum MacDockerPlan {
HostBuild,
ForceLinuxX86,
HostBuildThenManualArm64,
}
fn macos_docker_plan(os: &str, arch: &str, docker_project: bool) -> MacDockerPlan {
if os != "macos" || !docker_project {
return MacDockerPlan::HostBuild;
}
match arch {
"aarch64" => MacDockerPlan::HostBuildThenManualArm64,
_ => MacDockerPlan::ForceLinuxX86,
}
}
pub fn run(opts: SetupOpts) -> Result<(), String> {
println!();
println!(" floDl Setup");
println!(" ===========");
println!();
println!(" floDl is a Rust deep learning framework built on libtorch");
println!(" (PyTorch's C++ backend). This wizard will help you set up");
println!(" your development environment.");
println!();
println!(" Step 1: Detecting your system");
println!(" -----------------------------");
println!();
let cpu = system::cpu_model().unwrap_or_else(|| "Unknown".into());
let threads = system::cpu_threads();
let ram_gb = system::ram_total_gb();
println!(" CPU: {} ({} threads, {}GB RAM)", cpu, threads, ram_gb);
let has_docker = docker::has_docker();
let has_cargo = system::has_cargo();
if has_docker {
if let Some(v) = system::docker_version() {
println!(" Docker: {}", v);
} else {
println!(" Docker: available");
}
} else {
println!(" Docker: not found");
}
if has_cargo {
println!(" Rust: available");
} else {
println!(" Rust: not found");
}
let survey = flodl_hw::survey();
let gpus = &survey.devices;
if !gpus.is_empty() {
println!();
println!(" GPUs:");
for g in gpus {
println!(
" [{}] {} -- {}, {}GB VRAM",
g.index,
g.name,
g.arch_label(),
g.total_memory_mb / 1024
);
}
} else {
println!();
println!(" GPU: not detected (CPU-only mode)");
for note in survey.notes.iter().filter(|n| n.kind.explains_absence()) {
println!(" {}", note.message);
}
}
if !has_docker && !has_cargo {
println!();
println!(" You need at least one of these to continue:");
println!();
println!(" Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh");
println!(" Docker: https://docs.docker.com/engine/install/");
println!();
println!(" Install one or both and run 'fdl setup' again.");
return Err("no Rust or Docker found".into());
}
let tools = requirements::missing_host_tools();
if !tools.is_empty() && has_cargo {
let owned: Vec<String> = tools.iter().map(|t| (*t).to_string()).collect();
println!();
if has_docker {
println!(
" Note: building natively would also need: {}",
tools.join(", ")
);
println!(" {}", requirements::install_hint(&owned));
println!(" (not needed if you build in the dev container)");
} else {
println!(" Native builds need these first: {}", tools.join(", "));
println!(" {}", requirements::install_hint(&owned));
}
}
println!();
println!(" Step 2: libtorch");
println!(" ----------------");
println!();
println!(" floDl needs libtorch, PyTorch's C++ library.");
println!(" This downloads pre-built binaries (~2GB for CUDA, ~200MB for CPU).");
println!();
let ctx = Context::resolve();
let root = &ctx.root;
if !ctx.is_project {
println!(" Not inside a floDl project.");
println!(
" libtorch will be installed to: {}",
ctx.libtorch_dir().display()
);
println!();
}
let existing = detect::read_active(root);
let mut skip_download = false;
if !opts.force
&& let Some(ref info) = existing
{
match detect::variant_vendor(&info.path) {
Some(vendor) => {
println!(" Found existing {vendor} libtorch: {}", info.path);
if opts.non_interactive {
println!(" Keeping existing installation.");
skip_download = true;
} else if !prompt::ask_yn(" Download fresh?", false) {
skip_download = true;
}
println!();
}
None => println!(" Found existing CPU libtorch."),
}
}
if !skip_download {
let mounted_docker_project = ctx.is_project && ctx.root.join("Dockerfile").exists();
let plan = macos_docker_plan(
std::env::consts::OS,
std::env::consts::ARCH,
mounted_docker_project,
);
let force_linux = plan == MacDockerPlan::ForceLinuxX86;
let apple_silicon_docker = plan == MacDockerPlan::HostBuildThenManualArm64;
if force_linux {
println!(" macOS + Docker-mounted project: fetching Linux libtorch");
println!(" for the container (host arch would not load inside Linux).");
}
println!(" Downloading CPU libtorch...");
let cpu_opts = download::DownloadOpts {
variant: download::Variant::Cpu,
activate: false, force_linux,
..Default::default()
};
download::run_with_context(cpu_opts, &ctx)?;
if apple_silicon_docker {
println!();
println!(" That is the macOS build, for the host. The dev container is");
println!(" linux/arm64 and needs Linux aarch64 libtorch, which PyTorch");
println!(" does not publish; it has to be extracted from the PyPI wheel.");
println!(" Steps 1 and 2 of the Apple Silicon guide do this:");
println!(" https://flodl.dev/guide/mac-apple-silicon");
println!(" Until then `fdl build` / `fdl test` will not link.");
}
let majors: Vec<u32> = gpus.iter().filter_map(|g| g.sm_major()).collect();
let amd: Vec<_> = gpus
.iter()
.filter(|g| g.vendor == system::GpuVendor::Amd)
.collect();
if !amd.is_empty() {
let covered = download::rocm_covered(gpus);
if !majors.is_empty() {
println!();
println!(" AMD GPU(s) detected alongside NVIDIA. One libtorch build");
println!(" serves one vendor, so the NVIDIA cards are set up here.");
println!(" For the AMD cards: fdl libtorch download --rocm 7.0");
} else if covered.is_empty() {
let names: Vec<String> = amd
.iter()
.map(|g| format!("{} ({})", g.short_name(), g.arch_label()))
.collect();
println!();
println!(
" AMD GPU(s) detected ({}) outside the ROCm 7.0",
names.join(", ")
);
println!(" build's targets, so only CPU libtorch is installed.");
println!(" Covered targets: {}", download::rocm_archs());
} else {
println!();
println!(" Downloading ROCm libtorch (rocm7.0 for your AMD GPU)...");
let rocm_opts = download::DownloadOpts {
variant: download::Variant::Rocm70,
..Default::default()
};
download::run_with_context(rocm_opts, &ctx)?;
}
}
if !majors.is_empty() {
let lo_major = majors.iter().copied().min().unwrap_or(0);
let hi_major = majors.iter().copied().max().unwrap_or(0);
if lo_major < 7 && hi_major >= 10 {
println!();
println!(" Your GPUs span sm_{}.x to sm_{}.x.", lo_major, hi_major);
println!(" No pre-built libtorch covers both architectures.");
println!();
let has_source_build = detect::list_variants(root)
.iter()
.any(|v| v.starts_with("builds/"));
if has_source_build {
println!(" Found existing source build in libtorch/builds/.");
} else if opts.non_interactive {
println!(" Downloading cu126 (broadest coverage).");
let cuda_opts = download::DownloadOpts {
variant: download::Variant::Cuda126,
..Default::default()
};
download::run_with_context(cuda_opts, &ctx)?;
} else {
let choice = prompt::ask_choice(
" Choice",
&[
"Build libtorch from source (2-6 hours, covers all GPUs)",
"Download cu128 (Volta+ only, your older GPU won't work)",
"Download cu126 (pre-Volta only, your newer GPU won't work)",
"Skip for now",
],
4,
);
match choice {
1 => {
println!();
println!(" Starting libtorch source build...");
println!(" This will take 2-6 hours. You can safely Ctrl-C and");
println!(" resume later with: fdl libtorch build");
println!();
build::run(build::BuildOpts::default())?;
}
2 => {
println!(" Downloading cu128...");
let cuda_opts = download::DownloadOpts {
variant: download::Variant::Cuda128,
..Default::default()
};
download::run_with_context(cuda_opts, &ctx)?;
}
3 => {
println!(" Downloading cu126...");
let cuda_opts = download::DownloadOpts {
variant: download::Variant::Cuda126,
..Default::default()
};
download::run_with_context(cuda_opts, &ctx)?;
}
_ => {
println!(" Skipping CUDA libtorch. You can download later with:");
println!(" fdl libtorch download --cuda 12.8");
println!(" # or build from source:");
println!(" fdl libtorch build");
}
}
}
} else if lo_major < 7 {
println!();
println!(" Downloading CUDA libtorch (cu126 for your pre-Volta GPU)...");
let cuda_opts = download::DownloadOpts {
variant: download::Variant::Cuda126,
..Default::default()
};
download::run_with_context(cuda_opts, &ctx)?;
} else {
println!();
println!(" Downloading CUDA libtorch (cu128 for your Volta+ GPU)...");
let cuda_opts = download::DownloadOpts {
variant: download::Variant::Cuda128,
..Default::default()
};
download::run_with_context(cuda_opts, &ctx)?;
}
}
if detect::read_active(root).is_none() && detect::is_valid_variant(root, CPU_VARIANT) {
detect::set_active(root, CPU_VARIANT)?;
}
}
let active = detect::read_active(root);
let active_vendor = active
.as_ref()
.and_then(|info| detect::variant_vendor(&info.path));
let active_label = |v: Option<system::GpuVendor>| match v {
Some(vendor) => vendor.to_string(),
None => "CPU".to_string(),
};
if !ctx.is_project {
println!();
println!(" Setup complete!");
println!(" ===============");
println!();
if let Some(info) = &active {
println!(
" libtorch: {} ({})",
info.path,
active_label(active_vendor)
);
println!(" Location: {}", ctx.libtorch_dir().display());
}
println!();
println!(" Next steps:");
println!(" fdl init my-project # scaffold a new project");
println!(" fdl diagnose # verify GPU compatibility");
println!();
return Ok(());
}
println!();
println!(" Step 3: Build environment");
println!(" -------------------------");
println!();
println!(" floDl compiles Rust code that links against libtorch.");
println!(" You can build with Docker (isolated, reproducible) or");
println!(" natively (faster iteration, requires Rust + C++ toolchain).");
println!();
let build_mode = if has_docker && has_cargo {
if opts.non_interactive {
"docker"
} else {
let choice = prompt::ask_choice(
" Choice",
&[
"Docker (recommended) -- isolated, reproducible builds",
"Native -- faster iteration, requires C++ compiler on host",
"Both -- set up Docker and show native instructions",
],
1,
);
match choice {
1 => "docker",
2 => "native",
3 => "both",
_ => "docker",
}
}
} else if has_docker {
if opts.non_interactive {
"docker"
} else {
println!(" Docker is available. Rust is not installed on this machine.");
println!(" Docker is the easiest way to get started (no Rust install needed).");
println!();
if prompt::ask_yn(" Set up Docker build environment?", true) {
"docker"
} else {
println!();
println!(" No worries. To build flodl natively you need Rust on the host:");
println!();
println!(" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh");
println!();
println!(" More: https://www.rust-lang.org/tools/install");
println!(" Then re-run `fdl setup` and the native path will be picked up.");
println!();
if prompt::ask_yn(" Or use Docker after all?", false) {
"docker"
} else {
"none"
}
}
}
} else {
println!(" Rust is available. Docker is not installed.");
println!(" You can build natively (requires C++ compiler on the host).");
println!();
"native"
};
if build_mode == "docker" || build_mode == "both" {
println!();
println!(" Building Docker images...");
let _ = std::fs::create_dir_all(".cargo-cache");
let _ = std::fs::create_dir_all(".cargo-git");
let status = docker::compose_run(".", &["build", "dev"])?;
if !status.success() {
println!(" Warning: CPU Docker image build failed.");
}
if let Some(vendor) = active_vendor.filter(|_| !gpus.is_empty()) {
let service = crate::run::resolve_docker_service(crate::run::LOGICAL_GPU_SERVICE, root);
let _ = std::fs::create_dir_all(format!(".cargo-cache-{service}"));
let _ = std::fs::create_dir_all(format!(".cargo-git-{service}"));
let status = docker::compose_run(".", &["build", &service])?;
if !status.success() {
println!(" Warning: {vendor} Docker image build failed.");
}
}
println!(" Docker images ready.");
}
println!();
println!(" Setup complete!");
println!(" ===============");
println!();
if let Some(info) = &active {
println!(
" libtorch: {} ({})",
info.path,
active_label(active_vendor)
);
}
let gpu_ready = !gpus.is_empty() && active_vendor.is_some();
if build_mode == "docker" || build_mode == "both" {
println!();
println!(" Build with Docker:");
if gpu_ready {
println!(" fdl gpu-test # run GPU tests");
println!(" fdl gpu-build # compile for the GPU");
println!(" fdl gpu-shell # interactive shell");
} else {
println!(" fdl test # run tests");
println!(" fdl build # compile");
println!(" fdl shell # interactive shell");
}
}
if (build_mode == "native" || build_mode == "both")
&& let Some(info) = &active
{
let lt_path = format!("libtorch/{}", info.path);
println!();
println!(" Build natively:");
println!(" export LIBTORCH_PATH=\"{}\"", lt_path);
for line in detect::ld_library_path_lines(active_vendor, "$LIBTORCH_PATH/lib") {
println!(" {line}");
}
match active_vendor.filter(|_| gpu_ready) {
Some(vendor) => println!(" cargo test --features {}", vendor.cargo_feature()),
None => println!(" cargo test"),
}
}
if build_mode == "none" {
println!();
println!(" No build environment configured.");
println!(" Install Rust (link above) for native builds, or re-run `fdl setup`");
println!(" and pick Docker. libtorch is already in place either way.");
}
println!();
println!(" Other commands:");
println!(" fdl diagnose # verify GPU compatibility");
println!(" fdl init my-project # scaffold a new project");
println!();
if !opts.non_interactive {
crate::util::install_prompt::offer_global_install();
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apple_silicon_docker_never_forces_an_x86_download() {
assert_eq!(
macos_docker_plan("macos", "aarch64", true),
MacDockerPlan::HostBuildThenManualArm64
);
}
#[test]
fn intel_mac_docker_forces_the_linux_build() {
assert_eq!(
macos_docker_plan("macos", "x86_64", true),
MacDockerPlan::ForceLinuxX86
);
}
#[test]
fn a_mac_without_a_docker_project_builds_for_the_host() {
for arch in ["aarch64", "x86_64"] {
assert_eq!(
macos_docker_plan("macos", arch, false),
MacDockerPlan::HostBuild,
"{arch} native"
);
}
}
#[test]
fn non_macos_hosts_are_unaffected() {
for (os, arch) in [
("linux", "x86_64"),
("linux", "aarch64"),
("windows", "x86_64"),
] {
assert_eq!(
macos_docker_plan(os, arch, true),
MacDockerPlan::HostBuild,
"{os}/{arch}"
);
}
}
}