1use std::fs;
9use std::path::Path;
10
11use crate::util::prompt;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14enum Mode {
15 Mounted,
16 Docker,
17 Native,
18}
19
20pub fn run(name: Option<&str>, docker: bool, native: bool, with_hf: bool) -> Result<(), String> {
21 let name = name.ok_or("usage: fdl init <project-name>")?;
22 validate_name(name)?;
23
24 if Path::new(name).exists() {
25 return Err(format!("'{}' already exists", name));
26 }
27
28 if docker && native {
29 return Err("--docker and --native are mutually exclusive".into());
30 }
31 let flag_driven = docker || native || with_hf;
32 let mode = if docker {
33 Mode::Docker
34 } else if native {
35 Mode::Native
36 } else {
37 pick_mode_interactively()
38 };
39 let include_hf = if flag_driven {
44 with_hf
45 } else {
46 prompt::ask_yn(
47 "Include flodl-hf (HuggingFace: BERT/RoBERTa/DistilBERT, Hub loader, tokenizer)?",
48 false,
49 )
50 };
51
52 let crate_name = name.replace('-', "_");
53 let flodl_dep = resolve_flodl_dep();
54
55 fs::create_dir_all(format!("{}/src", name))
56 .map_err(|e| format!("cannot create directory: {}", e))?;
57
58 match mode {
59 Mode::Mounted => scaffold_mounted(name, &crate_name, &flodl_dep)?,
60 Mode::Docker => scaffold_docker(name, &crate_name, &flodl_dep)?,
61 Mode::Native => scaffold_native(name, &crate_name, &flodl_dep)?,
62 }
63
64 write_file(&format!("{}/src/main.rs", name), &main_rs_template())?;
66 write_file(&format!("{}/.gitignore", name), &gitignore_template(mode))?;
67 write_file(
68 &format!("{}/fdl.yml.example", name),
69 &fdl_yml_example_template(name, mode),
70 )?;
71 if mode != Mode::Native {
75 write_file(
76 &format!("{}/.env.example", name),
77 &env_example_template(mode),
78 )?;
79 }
80 write_fdl_bootstrap(name)?;
81
82 if include_hf {
83 let project_dir = Path::new(name);
84 if let Err(e) = crate::add::add_flodl_hf_at(project_dir) {
85 eprintln!("warning: flodl-hf scaffold failed: {e}");
88 eprintln!("You can retry after `cd {}` with `fdl add flodl-hf`.", name);
89 }
90 }
91
92 print_next_steps(name, mode, include_hf);
93 crate::util::install_prompt::offer_global_install();
94 Ok(())
95}
96
97fn pick_mode_interactively() -> Mode {
101 println!();
102 if !prompt::ask_yn("Use Docker for builds?", true) {
103 return Mode::Native;
104 }
105
106 if cfg!(target_os = "macos") {
111 println!();
112 println!(" Heads up: on macOS, Docker runs Linux containers under emulation");
113 println!(" (Rosetta / QEMU). Builds and training will be substantially slower");
114 println!(" than running cargo natively on the host. Native mode keeps");
115 println!(" everything on the Mac and uses macOS libtorch directly.");
116 println!();
117 if !prompt::ask_yn("Continue with Docker?", true) {
118 return Mode::Native;
119 }
120 }
121
122 let choice = prompt::ask_choice(
124 "How should libtorch be provided to the container?",
125 &[
126 "Mount it from the host (recommended: lighter image, swap CUDA variants)",
127 "Bake it into the image at build time (zero host setup)",
128 ],
129 1,
130 );
131 match choice {
132 2 => Mode::Docker,
133 _ => Mode::Mounted,
134 }
135}
136
137fn print_next_steps(name: &str, mode: Mode, include_hf: bool) {
138 println!();
139 println!("Project '{}' created. Next steps:", name);
140 println!();
141 println!(" cd {}", name);
142 match mode {
143 Mode::Mounted => {
144 println!(" ./fdl setup # detect hardware + download libtorch");
145 println!(" ./fdl build # build the project");
146 }
147 Mode::Docker => {
148 println!(" ./fdl build # first build (downloads libtorch, ~5 min)");
149 }
150 Mode::Native => {
151 println!(" ./fdl libtorch download --cpu # or --cuda 12.8");
152 println!(" ./fdl build # cargo build on the host");
153 }
154 }
155 println!(" ./fdl test # run tests");
156 println!(" ./fdl run # train the model");
157 if mode != Mode::Native {
158 println!(" ./fdl shell # interactive shell");
159 }
160 if include_hf {
161 println!();
162 println!(" cd flodl-hf && fdl classify # try the HuggingFace playground");
163 }
164 println!();
165 println!("`./fdl --help` lists every command defined in fdl.yml.");
166 println!("Edit src/main.rs to build your model.");
167 println!();
168 println!("Guides:");
169 println!(" Tutorials: https://flodl.dev/guide/tensors");
170 println!(" Graph Tree: https://flodl.dev/guide/graph-tree");
171 println!(" PyTorch migration: https://flodl.dev/guide/pytorch/migration");
172 println!(" Troubleshooting: https://flodl.dev/guide/troubleshooting");
173}
174
175fn write_fdl_bootstrap(name: &str) -> Result<(), String> {
176 let fdl_script = include_str!("../assets/fdl");
177 write_file(&format!("{}/fdl", name), fdl_script)?;
178 #[cfg(unix)]
179 {
180 use std::os::unix::fs::PermissionsExt;
181 let _ = fs::set_permissions(format!("{}/fdl", name), fs::Permissions::from_mode(0o755));
182 }
183 Ok(())
184}
185
186fn validate_name(name: &str) -> Result<(), String> {
187 if name.is_empty() {
188 return Err("project name cannot be empty".into());
189 }
190 if !name
191 .chars()
192 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
193 {
194 return Err("project name must contain only letters, digits, hyphens, underscores".into());
195 }
196 Ok(())
197}
198
199fn resolve_flodl_dep() -> String {
210 let version = crate::update_check::probe_crates_io("flodl")
211 .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
212 format!("flodl = \"{version}\"")
213}
214
215fn scaffold_docker(name: &str, crate_name: &str, flodl_dep: &str) -> Result<(), String> {
220 write_file(
221 &format!("{}/Cargo.toml", name),
222 &cargo_toml_template(crate_name, flodl_dep),
223 )?;
224 write_file(&format!("{}/Dockerfile.cpu", name), DOCKERFILE_CPU)?;
225 write_file(&format!("{}/Dockerfile.cuda", name), DOCKERFILE_CUDA)?;
226 write_file(&format!("{}/Dockerfile.rocm", name), DOCKERFILE_ROCM)?;
227 write_file(
228 &format!("{}/docker-compose.yml", name),
229 &docker_compose_template(crate_name, true),
230 )?;
231 Ok(())
232}
233
234fn scaffold_mounted(name: &str, crate_name: &str, flodl_dep: &str) -> Result<(), String> {
239 write_file(
240 &format!("{}/Cargo.toml", name),
241 &cargo_toml_template(crate_name, flodl_dep),
242 )?;
243 write_file(&format!("{}/Dockerfile", name), DOCKERFILE_MOUNTED)?;
244 write_file(
245 &format!("{}/Dockerfile.cuda", name),
246 DOCKERFILE_CUDA_MOUNTED,
247 )?;
248 write_file(
249 &format!("{}/Dockerfile.rocm", name),
250 DOCKERFILE_ROCM_MOUNTED,
251 )?;
252 write_file(
253 &format!("{}/docker-compose.yml", name),
254 &docker_compose_template(crate_name, false),
255 )?;
256 Ok(())
257}
258
259fn scaffold_native(name: &str, crate_name: &str, flodl_dep: &str) -> Result<(), String> {
264 write_file(
265 &format!("{}/Cargo.toml", name),
266 &cargo_toml_template(crate_name, flodl_dep),
267 )?;
268 Ok(())
271}
272
273fn cargo_toml_template(crate_name: &str, flodl_dep: &str) -> String {
278 format!(
279 r#"[package]
280name = "{crate_name}"
281version = "0.1.0"
282edition = "2024"
283
284[dependencies]
285{flodl_dep}
286
287# GPU support is opt-in. `fdl gpu-*` picks the right one for you through
288# $FDL_GPU_FEATURE, derived from the libtorch variant you have active.
289# (Without this section `cargo build --features cuda` fails outright with
290# "does not contain this feature" -- cargo resolves it against THIS
291# package, not the dependency.)
292[features]
293cuda = ["flodl/cuda"]
294
295# Optimize floDl in dev builds -- your code stays fast to compile.
296# After the first build, only your graph code recompiles (~2s).
297[profile.dev.package.flodl]
298opt-level = 3
299
300[profile.dev.package.flodl-sys]
301opt-level = 3
302
303# Release: cross-crate optimization for maximum throughput.
304[profile.release]
305lto = "thin"
306codegen-units = 1
307"#
308 )
309}
310
311fn main_rs_template() -> String {
312 r#"//! floDl training template.
313//!
314//! This is a starting point for your model. Edit the architecture,
315//! data loading, and training loop to fit your task.
316//!
317//! New to Rust? Read: https://flodl.dev/guide/pytorch/rust-primer
318//! Stuck? Read: https://flodl.dev/guide/troubleshooting
319
320use flodl::*;
321use flodl::monitor::Monitor;
322
323fn main() -> Result<()> {
324 // --- Model ---
325 let model = FlowBuilder::from(Linear::new(4, 32)?)
326 .through(GELU)
327 .through(LayerNorm::new(32)?)
328 .also(Linear::new(32, 32)?) // residual connection
329 .through(Linear::new(32, 1)?)
330 .build()?;
331
332 // --- Optimizer ---
333 let params = model.parameters();
334 let mut optimizer = Adam::new(¶ms, 0.001);
335 let scheduler = CosineScheduler::new(0.001, 1e-6, 100);
336 model.train();
337
338 // --- Data ---
339 // Replace this with your data loading.
340 let opts = TensorOptions::default();
341 let batches: Vec<(Tensor, Tensor)> = (0..32)
342 .map(|_| {
343 let x = Tensor::randn(&[16, 4], opts).unwrap();
344 let y = Tensor::randn(&[16, 1], opts).unwrap();
345 (x, y)
346 })
347 .collect();
348
349 // --- Training loop ---
350 let num_epochs = 100usize;
351 let mut monitor = Monitor::new(num_epochs);
352 // monitor.serve(3000)?; // uncomment for live dashboard
353 // monitor.watch(&model); // uncomment to show graph SVG
354 // monitor.save_html("report.html"); // uncomment to save HTML report
355
356 for epoch in 0..num_epochs {
357 let t = std::time::Instant::now();
358 let mut epoch_loss = 0.0;
359
360 for (input_t, target_t) in &batches {
361 let input = Variable::new(input_t.clone(), true);
362 let target = Variable::new(target_t.clone(), false);
363
364 optimizer.zero_grad();
365 let pred = model.forward(&input)?;
366 let loss = mse_loss(&pred, &target)?;
367 loss.backward()?;
368 clip_grad_norm(¶ms, 1.0)?;
369 optimizer.step()?;
370
371 epoch_loss += loss.item()?;
372 }
373
374 let avg_loss = epoch_loss / batches.len() as f64;
375 let lr = scheduler.lr(epoch);
376 optimizer.set_lr(lr);
377 monitor.log(epoch, t.elapsed(), &[("loss", avg_loss), ("lr", lr)]);
378 }
379
380 monitor.finish();
381 Ok(())
382}
383"#
384 .into()
385}
386
387fn gitignore_template(mode: Mode) -> String {
388 let mut s = String::from(
389 "/target
390*.fdl
391*.log
392*.csv
393*.html
394
395# Local fdl config (fdl.yml.example is committed; fdl copies it on first run)
396fdl.yml
397fdl.yaml
398
399# Local docker-compose env (per-machine: UID/GID, libtorch variant override,
400# cargo job throttle)
401.env
402",
403 );
404 match mode {
405 Mode::Docker => {
406 s.push_str(
408 ".cargo-cache/
409.cargo-git/
410.cargo-cache-cuda/
411.cargo-git-cuda/
412",
413 );
414 }
415 Mode::Mounted => {
416 s.push_str(
418 ".cargo-cache/
419.cargo-git/
420.cargo-cache-cuda/
421.cargo-git-cuda/
422libtorch/
423",
424 );
425 }
426 Mode::Native => {
427 s.push_str("libtorch/\n");
430 }
431 }
432 s
433}
434
435fn docker_compose_template(crate_name: &str, baked: bool) -> String {
436 if baked {
437 format!(
438 r#"services:
439 dev:
440 build:
441 context: .
442 dockerfile: Dockerfile.cpu
443 image: {crate_name}-dev
444 user: "${{UID:-1000}}:${{GID:-1000}}"
445 volumes:
446 - .:/workspace
447 - ./.cargo-cache:/usr/local/cargo/registry
448 - ./.cargo-git:/usr/local/cargo/git
449 working_dir: /workspace
450 stdin_open: true
451 tty: true
452 environment:
453 # Throttle cargo's link parallelism. Unset on Linux native (empty →
454 # cargo's default); Mac hosts set it in `.env` to keep `ld` within what
455 # a virtiofs-backed workspace can serve. See docs/mac-apple-silicon.md.
456 - CARGO_BUILD_JOBS
457 # flodl runtime knobs, forwarded from the host (or `.env`):
458 # verbosity is what `fdl -v/-vv/...` sets per invocation, and the
459 # timeout scale stretches distributed network deadlines on slow links.
460 - FLODL_VERBOSITY
461 - FLODL_NET_TIMEOUT_SCALE
462 # The cargo feature the active libtorch variant needs, computed by
463 # fdl from the variant path, so a run: line can say
464 # `--features "$FDL_GPU_FEATURE"` instead of hardcoding a vendor.
465 # Compose only passes variables listed here into the container —
466 # without this line that spelling breaks inside every service.
467 - FDL_GPU_FEATURE
468
469 cuda:
470 build:
471 context: .
472 dockerfile: Dockerfile.cuda
473 image: {crate_name}-cuda
474 user: "${{UID:-1000}}:${{GID:-1000}}"
475 volumes:
476 - .:/workspace
477 - ./.cargo-cache-cuda:/usr/local/cargo/registry
478 - ./.cargo-git-cuda:/usr/local/cargo/git
479 working_dir: /workspace
480 stdin_open: true
481 tty: true
482 environment:
483 # flodl runtime knobs, forwarded from the host (or `.env`):
484 # verbosity is what `fdl -v/-vv/...` sets per invocation, and the
485 # timeout scale stretches distributed network deadlines on slow links.
486 - FLODL_VERBOSITY
487 - FLODL_NET_TIMEOUT_SCALE
488 # The cargo feature the active libtorch variant needs, computed by
489 # fdl from the variant path, so a run: line can say
490 # `--features "$FDL_GPU_FEATURE"` instead of hardcoding a vendor.
491 # Compose only passes variables listed here into the container —
492 # without this line that spelling breaks inside every service.
493 - FDL_GPU_FEATURE
494 deploy:
495 resources:
496 reservations:
497 devices:
498 - driver: nvidia
499 count: all
500 capabilities: [gpu]
501
502 rocm:
503 build:
504 context: .
505 dockerfile: Dockerfile.rocm
506 args:
507 ROCM_VERSION: ${{ROCM_VERSION:-7.0}}
508 image: {crate_name}-rocm
509 user: "${{UID:-1000}}:${{GID:-1000}}"
510 devices:
511 - /dev/kfd
512 - /dev/dri
513 group_add:
514 - video
515 - render
516 # HSA needs these to map queues; without them the runtime fails at
517 # device init rather than at first op.
518 security_opt:
519 - seccomp:unconfined
520 ipc: host
521 volumes:
522 - .:/workspace
523 - ./.cargo-cache-rocm:/usr/local/cargo/registry
524 - ./.cargo-git-rocm:/usr/local/cargo/git
525 working_dir: /workspace
526 stdin_open: true
527 tty: true
528 environment:
529 # flodl runtime knobs, forwarded from the host (or `.env`):
530 # verbosity is what `fdl -v/-vv/...` sets per invocation, and the
531 # timeout scale stretches distributed network deadlines on slow links.
532 - FLODL_VERBOSITY
533 - FLODL_NET_TIMEOUT_SCALE
534 # The cargo feature the active libtorch variant needs, computed by
535 # fdl from the variant path, so a run: line can say
536 # `--features "$FDL_GPU_FEATURE"` instead of hardcoding a vendor.
537 # Compose only passes variables listed here into the container —
538 # without this line that spelling breaks inside every service.
539 - FDL_GPU_FEATURE
540"#
541 )
542 } else {
543 format!(
544 r#"services:
545 dev:
546 build:
547 context: .
548 dockerfile: Dockerfile
549 image: {crate_name}-dev
550 user: "${{UID:-1000}}:${{GID:-1000}}"
551 volumes:
552 - .:/workspace
553 - ./.cargo-cache:/usr/local/cargo/registry
554 - ./.cargo-git:/usr/local/cargo/git
555 - ${{LIBTORCH_CPU_PATH:-./libtorch/precompiled/cpu}}:/usr/local/libtorch:ro
556 working_dir: /workspace
557 stdin_open: true
558 tty: true
559 environment:
560 # Throttle cargo's link parallelism. Required on macOS Docker / OrbStack,
561 # where the virtiofs-mounted libtorch directory cannot serve many
562 # concurrent `ld` lookups — the linker reports `cannot find -ltorch`
563 # spuriously. Unset on Linux native (empty → cargo's default).
564 - CARGO_BUILD_JOBS
565 # flodl runtime knobs, forwarded from the host (or `.env`):
566 # verbosity is what `fdl -v/-vv/...` sets per invocation, and the
567 # timeout scale stretches distributed network deadlines on slow links.
568 - FLODL_VERBOSITY
569 - FLODL_NET_TIMEOUT_SCALE
570 # The cargo feature the active libtorch variant needs, computed by
571 # fdl from the variant path, so a run: line can say
572 # `--features "$FDL_GPU_FEATURE"` instead of hardcoding a vendor.
573 # Compose only passes variables listed here into the container —
574 # without this line that spelling breaks inside every service.
575 - FDL_GPU_FEATURE
576
577 cuda:
578 build:
579 context: .
580 dockerfile: Dockerfile.cuda
581 args:
582 CUDA_VERSION: ${{CUDA_VERSION:-12.8.0}}
583 image: {crate_name}-cuda:${{CUDA_TAG:-12.8}}
584 user: "${{UID:-1000}}:${{GID:-1000}}"
585 volumes:
586 - .:/workspace
587 - ./.cargo-cache-cuda:/usr/local/cargo/registry
588 - ./.cargo-git-cuda:/usr/local/cargo/git
589 - ${{LIBTORCH_HOST_PATH:-./libtorch/precompiled/cu128}}:/usr/local/libtorch:ro
590 working_dir: /workspace
591 stdin_open: true
592 tty: true
593 environment:
594 # flodl runtime knobs, forwarded from the host (or `.env`):
595 # verbosity is what `fdl -v/-vv/...` sets per invocation, and the
596 # timeout scale stretches distributed network deadlines on slow links.
597 - FLODL_VERBOSITY
598 - FLODL_NET_TIMEOUT_SCALE
599 # The cargo feature the active libtorch variant needs, computed by
600 # fdl from the variant path, so a run: line can say
601 # `--features "$FDL_GPU_FEATURE"` instead of hardcoding a vendor.
602 # Compose only passes variables listed here into the container —
603 # without this line that spelling breaks inside every service.
604 - FDL_GPU_FEATURE
605 deploy:
606 resources:
607 reservations:
608 devices:
609 - driver: nvidia
610 count: all
611 capabilities: [gpu]
612
613 rocm:
614 build:
615 context: .
616 dockerfile: Dockerfile.rocm
617 args:
618 ROCM_VERSION: ${{ROCM_VERSION:-7.0}}
619 image: {crate_name}-rocm
620 user: "${{UID:-1000}}:${{GID:-1000}}"
621 devices:
622 - /dev/kfd
623 - /dev/dri
624 group_add:
625 - video
626 - render
627 # HSA needs these to map queues; without them the runtime fails at
628 # device init rather than at first op.
629 security_opt:
630 - seccomp:unconfined
631 ipc: host
632 volumes:
633 - .:/workspace
634 - ./.cargo-cache-rocm:/usr/local/cargo/registry
635 - ./.cargo-git-rocm:/usr/local/cargo/git
636 - ${{LIBTORCH_HOST_PATH:-./libtorch/precompiled/rocm70}}:/usr/local/libtorch:ro
637 working_dir: /workspace
638 stdin_open: true
639 tty: true
640 environment:
641 - FLODL_VERBOSITY
642 - FLODL_NET_TIMEOUT_SCALE
643 # The cargo feature the active libtorch variant needs, computed by
644 # fdl from the variant path, so a run: line can say
645 # `--features "$FDL_GPU_FEATURE"` instead of hardcoding a vendor.
646 # Compose only passes variables listed here into the container —
647 # without this line that spelling breaks inside every service.
648 - FDL_GPU_FEATURE
649"#
650 )
651 }
652}
653
654const DOCKERFILE_CPU: &str = r#"# CPU-only dev image for floDl projects.
660FROM ubuntu:24.04
661
662ENV DEBIAN_FRONTEND=noninteractive
663
664RUN apt-get update && apt-get install -y --no-install-recommends \
665 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
666 && rm -rf /var/lib/apt/lists/*
667
668# Rust
669ENV CARGO_HOME="/usr/local/cargo"
670ENV RUSTUP_HOME="/usr/local/rustup"
671RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
672 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
673ENV PATH="${CARGO_HOME}/bin:${PATH}"
674
675# libtorch (CPU-only, ~200MB)
676ARG LIBTORCH_VERSION=2.10.0
677RUN wget -q https://download.pytorch.org/libtorch/cpu/libtorch-shared-with-deps-${LIBTORCH_VERSION}%2Bcpu.zip \
678 && unzip -q libtorch-shared-with-deps-${LIBTORCH_VERSION}+cpu.zip -d /usr/local \
679 && rm libtorch-shared-with-deps-${LIBTORCH_VERSION}+cpu.zip
680
681ENV LIBTORCH_PATH="/usr/local/libtorch"
682ENV LD_LIBRARY_PATH="${LIBTORCH_PATH}/lib"
683ENV LIBRARY_PATH="${LIBTORCH_PATH}/lib"
684
685WORKDIR /workspace
686"#;
687
688const DOCKERFILE_CUDA: &str = r#"# CUDA dev image for floDl projects.
689# Requires: docker run --gpus all ...
690FROM nvidia/cuda:12.8.0-devel-ubuntu24.04
691
692ENV DEBIAN_FRONTEND=noninteractive
693
694RUN apt-get update && apt-get install -y --no-install-recommends \
695 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
696 && rm -rf /var/lib/apt/lists/*
697
698# Rust
699ENV CARGO_HOME="/usr/local/cargo"
700ENV RUSTUP_HOME="/usr/local/rustup"
701RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
702 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
703ENV PATH="${CARGO_HOME}/bin:${PATH}"
704
705# libtorch (CUDA 12.8)
706ARG LIBTORCH_VERSION=2.10.0
707RUN wget -q "https://download.pytorch.org/libtorch/cu128/libtorch-shared-with-deps-${LIBTORCH_VERSION}%2Bcu128.zip" \
708 && unzip -q "libtorch-shared-with-deps-${LIBTORCH_VERSION}+cu128.zip" -d /usr/local \
709 && rm "libtorch-shared-with-deps-${LIBTORCH_VERSION}+cu128.zip"
710
711ENV LIBTORCH_PATH="/usr/local/libtorch"
712ENV LD_LIBRARY_PATH="${LIBTORCH_PATH}/lib:/usr/local/cuda/lib64"
713ENV LIBRARY_PATH="${LIBTORCH_PATH}/lib:/usr/local/cuda/lib64"
714ENV CUDA_HOME="/usr/local/cuda"
715
716WORKDIR /workspace
717"#;
718
719const DOCKERFILE_MOUNTED: &str = r#"# CPU dev image for floDl projects (libtorch mounted at runtime).
721FROM ubuntu:24.04
722
723ENV DEBIAN_FRONTEND=noninteractive
724
725RUN apt-get update && apt-get install -y --no-install-recommends \
726 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
727 && rm -rf /var/lib/apt/lists/*
728
729# Rust
730ENV CARGO_HOME="/usr/local/cargo"
731ENV RUSTUP_HOME="/usr/local/rustup"
732RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
733 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
734ENV PATH="${CARGO_HOME}/bin:${PATH}"
735
736ENV LIBTORCH_PATH="/usr/local/libtorch"
737ENV LD_LIBRARY_PATH="${LIBTORCH_PATH}/lib"
738ENV LIBRARY_PATH="${LIBTORCH_PATH}/lib"
739
740WORKDIR /workspace
741"#;
742
743const DOCKERFILE_CUDA_MOUNTED: &str = r#"# CUDA dev image for floDl projects (libtorch mounted at runtime).
744# Requires: docker run --gpus all ...
745ARG CUDA_VERSION=12.8.0
746FROM nvidia/cuda:${CUDA_VERSION}-devel-ubuntu24.04
747
748ENV DEBIAN_FRONTEND=noninteractive
749
750RUN apt-get update && apt-get install -y --no-install-recommends \
751 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
752 && rm -rf /var/lib/apt/lists/*
753
754# Rust
755ENV CARGO_HOME="/usr/local/cargo"
756ENV RUSTUP_HOME="/usr/local/rustup"
757RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
758 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
759ENV PATH="${CARGO_HOME}/bin:${PATH}"
760
761ENV LIBTORCH_PATH="/usr/local/libtorch"
762ENV LD_LIBRARY_PATH="${LIBTORCH_PATH}/lib:/usr/local/cuda/lib64"
763ENV LIBRARY_PATH="${LIBTORCH_PATH}/lib:/usr/local/cuda/lib64"
764ENV CUDA_HOME="/usr/local/cuda"
765
766WORKDIR /workspace
767"#;
768
769const DOCKERFILE_ROCM: &str = r#"# ROCm dev image for floDl projects.
779# Requires: docker run --device /dev/kfd --device /dev/dri ...
780ARG ROCM_VERSION=7.0
781FROM rocm/dev-ubuntu-24.04:${ROCM_VERSION}-complete
782
783ENV DEBIAN_FRONTEND=noninteractive
784
785RUN apt-get update && apt-get install -y --no-install-recommends \
786 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
787 && rm -rf /var/lib/apt/lists/*
788
789# Rust
790ENV CARGO_HOME="/usr/local/cargo"
791ENV RUSTUP_HOME="/usr/local/rustup"
792RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
793 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
794ENV PATH="${CARGO_HOME}/bin:${PATH}"
795
796# libtorch (ROCm 7.0)
797ARG LIBTORCH_VERSION=2.10.0
798RUN wget -q "https://download.pytorch.org/libtorch/rocm7.0/libtorch-shared-with-deps-${LIBTORCH_VERSION}%2Brocm7.0.zip" \
799 && unzip -q "libtorch-shared-with-deps-${LIBTORCH_VERSION}+rocm7.0.zip" -d /usr/local \
800 && rm "libtorch-shared-with-deps-${LIBTORCH_VERSION}+rocm7.0.zip"
801
802ENV LIBTORCH_PATH="/usr/local/libtorch"
803ENV ROCM_PATH="/opt/rocm"
804# System ROCm FIRST -- see the note above this template.
805ENV LD_LIBRARY_PATH="${ROCM_PATH}/lib:${LIBTORCH_PATH}/lib"
806ENV LIBRARY_PATH="${ROCM_PATH}/lib:${LIBTORCH_PATH}/lib"
807
808WORKDIR /workspace
809"#;
810
811const DOCKERFILE_ROCM_MOUNTED: &str = r#"# ROCm dev image for floDl projects (libtorch mounted at runtime).
812# Requires: docker run --device /dev/kfd --device /dev/dri ...
813ARG ROCM_VERSION=7.0
814FROM rocm/dev-ubuntu-24.04:${ROCM_VERSION}-complete
815
816ENV DEBIAN_FRONTEND=noninteractive
817
818RUN apt-get update && apt-get install -y --no-install-recommends \
819 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
820 && rm -rf /var/lib/apt/lists/*
821
822# Rust
823ENV CARGO_HOME="/usr/local/cargo"
824ENV RUSTUP_HOME="/usr/local/rustup"
825RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
826 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
827ENV PATH="${CARGO_HOME}/bin:${PATH}"
828
829ENV LIBTORCH_PATH="/usr/local/libtorch"
830ENV ROCM_PATH="/opt/rocm"
831# System ROCm FIRST -- see the note above this template.
832ENV LD_LIBRARY_PATH="${ROCM_PATH}/lib:${LIBTORCH_PATH}/lib"
833ENV LIBRARY_PATH="${ROCM_PATH}/lib:${LIBTORCH_PATH}/lib"
834
835WORKDIR /workspace
836"#;
837
838fn env_example_template(mode: Mode) -> String {
854 let mut s = String::from(
855 "# Local Docker environment overrides for docker-compose.yml.
856# Copy this to `.env` (gitignored) and uncomment what you need:
857# cp .env.example .env
858# docker-compose auto-reads `.env` from this directory. This `.env.example`
859# is only a template; compose never reads it directly.
860
861# Host user/group mapping, so files created in the container are owned by you
862# rather than root. Defaults to 1000:1000 when unset; macOS is usually 501:20
863# (`id -u` / `id -g`).
864#UID=1000
865#GID=1000
866",
867 );
868 if mode == Mode::Mounted {
869 s.push_str(
870 "
871# libtorch mount points (host paths). Defaults live in docker-compose.yml.
872# Override to point at a different variant, e.g. an extracted linux-aarch64
873# build on Apple Silicon.
874#LIBTORCH_CPU_PATH=./libtorch/precompiled/cpu
875#LIBTORCH_HOST_PATH=./libtorch/precompiled/cu128
876
877# CUDA base image version and image tag for the `cuda` service.
878# Only affects direct `docker compose` calls: `fdl` derives both from the active
879# libtorch variant's `.arch` metadata and overrides whatever is set here.
880#CUDA_VERSION=12.8.0
881#CUDA_TAG=12.8
882
883# ROCm base image version for the `rocm` service (same rule). The service
884# mounts LIBTORCH_HOST_PATH like the cuda one, so the variant override above
885# covers both vendors.
886#ROCM_VERSION=7.0
887",
888 );
889 }
890 s.push_str(
891 "
892# Throttle cargo build/link parallelism. Leave unset on Linux (uses all cores).
893# On Apple Silicon via Docker/OrbStack, set to 2 to avoid spurious
894# \"cannot find -ltorch\" linker errors caused by virtiofs mount latency.
895#CARGO_BUILD_JOBS=2
896
897# flodl log verbosity. `fdl -v/-vv/...` sets this per invocation; setting it
898# here makes a level stick without the flag.
899#FLODL_VERBOSITY=1
900
901# Scale every distributed network timeout (socket setup, coordinator deadlines).
902# Raise it on slow or congested links where the 30s LAN defaults are too tight.
903#FLODL_NET_TIMEOUT_SCALE=2
904",
905 );
906 s
907}
908
909fn fdl_yml_example_template(project_name: &str, mode: Mode) -> String {
910 let use_docker = matches!(mode, Mode::Mounted | Mode::Docker);
911 let (cpu_svc, gpu_svc) = if use_docker {
915 ("\n docker: dev", "\n docker: gpu")
916 } else {
917 ("", "")
918 };
919 let gpu_note = if use_docker {
920 "(NVIDIA: Container Toolkit; AMD: /dev/kfd + render group)"
921 } else {
922 "(requires the vendor toolkit on the host)"
923 };
924 let preamble = if use_docker {
925 "# Run any of these with `./fdl <cmd>` (or `fdl <cmd>` once installed\n\
926 # globally via `./fdl install`). Libtorch env vars are derived from\n\
927 # `libtorch/.active` automatically; missing libtorch surfaces as a\n\
928 # clean linker error, with `./fdl setup` one call away."
929 } else {
930 "# Native mode: commands run on the host. Install libtorch first\n\
931 # (`./fdl libtorch download --cpu` or `--cuda 12.8`); `./fdl`\n\
932 # commands then export `LIBTORCH_PATH` / `LD_LIBRARY_PATH` from\n\
933 # the active variant automatically. Bypassing fdl (bare cargo)\n\
934 # needs them by hand — `./fdl libtorch info` prints the exports."
935 };
936
937 let shell_block = if use_docker {
938 format!(
939 r#" shell:
940 description: Interactive shell (CPU container)
941 run: bash{cpu_svc}
942
943"#
944 )
945 } else {
946 String::new()
948 };
949
950 let gpu_shell_block = if use_docker {
951 format!(
952 r#" gpu-shell:
953 description: Interactive shell (GPU container)
954 run: bash{gpu_svc}
955"#
956 )
957 } else {
958 String::new()
959 };
960
961 format!(
962 r#"description: {project_name}
963
964{preamble}
965
966commands:
967 # --- CPU ---
968 build:
969 description: Build (debug)
970 run: cargo build{cpu_svc}
971 test:
972 description: Run CPU tests
973 run: cargo test -- --nocapture{cpu_svc}
974 run:
975 description: cargo run
976 run: cargo run{cpu_svc}
977 check:
978 description: Type-check without building
979 run: cargo check{cpu_svc}
980 clippy:
981 description: Lint
982 run: cargo clippy -- -D clippy::all{cpu_svc}
983{shell_block} # --- GPU {gpu_note} ---
984 # $FDL_GPU_FEATURE is exported by fdl from the active libtorch variant,
985 # so these stay correct if you switch variants.
986 gpu-build:
987 description: Build with GPU support
988 run: cargo build --features "$FDL_GPU_FEATURE"{gpu_svc}
989 gpu-test:
990 description: Run GPU tests
991 run: cargo test --features "$FDL_GPU_FEATURE" -- --nocapture{gpu_svc}
992 gpu-run:
993 description: cargo run with GPU support
994 run: cargo run --features "$FDL_GPU_FEATURE"{gpu_svc}
995{gpu_shell_block}"#
996 )
997}
998
999fn write_file(path: &str, content: &str) -> Result<(), String> {
1004 fs::write(path, content).map_err(|e| format!("cannot write {}: {}", path, e))
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use super::validate_name;
1010
1011 #[test]
1012 fn validate_name_accepts_alnum_hyphen_underscore() {
1013 for ok in ["my_project", "my-project", "Proj123", "a", "x_1-2"] {
1014 assert!(validate_name(ok).is_ok(), "{ok:?} should be valid");
1015 }
1016 }
1017
1018 #[test]
1019 fn validate_name_rejects_empty() {
1020 let err = validate_name("").unwrap_err();
1021 assert!(err.contains("empty"), "unexpected: {err}");
1022 }
1023
1024 #[test]
1025 fn validate_name_rejects_disallowed_chars() {
1026 for bad in ["my project", "my.project", "a/b", "../evil", "name!"] {
1029 let err = validate_name(bad).unwrap_err();
1030 assert!(err.contains("only letters"), "{bad:?} -> unexpected: {err}");
1031 }
1032 }
1033}