1use std::fs;
9use std::path::Path;
10use std::process::Command;
11
12use crate::util::prompt;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum Mode {
16 Mounted,
17 Docker,
18 Native,
19}
20
21pub fn run(
22 name: Option<&str>,
23 docker: bool,
24 native: bool,
25 with_hf: bool,
26) -> Result<(), String> {
27 let name = name.ok_or("usage: fdl init <project-name>")?;
28 validate_name(name)?;
29
30 if Path::new(name).exists() {
31 return Err(format!("'{}' already exists", name));
32 }
33
34 if docker && native {
35 return Err("--docker and --native are mutually exclusive".into());
36 }
37 let flag_driven = docker || native || with_hf;
38 let mode = if docker {
39 Mode::Docker
40 } else if native {
41 Mode::Native
42 } else {
43 pick_mode_interactively()
44 };
45 let include_hf = if flag_driven {
50 with_hf
51 } else {
52 prompt::ask_yn(
53 "Include flodl-hf (HuggingFace: BERT/RoBERTa/DistilBERT, Hub loader, tokenizer)?",
54 false,
55 )
56 };
57
58 let crate_name = name.replace('-', "_");
59 let flodl_dep = resolve_flodl_dep();
60
61 fs::create_dir_all(format!("{}/src", name))
62 .map_err(|e| format!("cannot create directory: {}", e))?;
63
64 match mode {
65 Mode::Mounted => scaffold_mounted(name, &crate_name, &flodl_dep)?,
66 Mode::Docker => scaffold_docker(name, &crate_name, &flodl_dep)?,
67 Mode::Native => scaffold_native(name, &crate_name, &flodl_dep)?,
68 }
69
70 write_file(
72 &format!("{}/src/main.rs", name),
73 &main_rs_template(),
74 )?;
75 write_file(
76 &format!("{}/.gitignore", name),
77 &gitignore_template(mode),
78 )?;
79 write_file(
80 &format!("{}/fdl.yml.example", name),
81 &fdl_yml_example_template(name, mode),
82 )?;
83 if mode != Mode::Native {
87 write_file(
88 &format!("{}/.env.example", name),
89 &env_example_template(mode),
90 )?;
91 }
92 write_fdl_bootstrap(name)?;
93
94 if include_hf {
95 let project_dir = Path::new(name);
96 if let Err(e) = crate::add::add_flodl_hf_at(project_dir) {
97 eprintln!("warning: flodl-hf scaffold failed: {e}");
100 eprintln!("You can retry after `cd {}` with `fdl add flodl-hf`.", name);
101 }
102 }
103
104 print_next_steps(name, mode, include_hf);
105 crate::util::install_prompt::offer_global_install();
106 Ok(())
107}
108
109fn pick_mode_interactively() -> Mode {
113 println!();
114 if !prompt::ask_yn("Use Docker for builds?", true) {
115 return Mode::Native;
116 }
117
118 if cfg!(target_os = "macos") {
123 println!();
124 println!(" Heads up: on macOS, Docker runs Linux containers under emulation");
125 println!(" (Rosetta / QEMU). Builds and training will be substantially slower");
126 println!(" than running cargo natively on the host. Native mode keeps");
127 println!(" everything on the Mac and uses macOS libtorch directly.");
128 println!();
129 if !prompt::ask_yn("Continue with Docker?", true) {
130 return Mode::Native;
131 }
132 }
133
134 let choice = prompt::ask_choice(
136 "How should libtorch be provided to the container?",
137 &[
138 "Mount it from the host (recommended: lighter image, swap CUDA variants)",
139 "Bake it into the image at build time (zero host setup)",
140 ],
141 1,
142 );
143 match choice {
144 2 => Mode::Docker,
145 _ => Mode::Mounted,
146 }
147}
148
149fn print_next_steps(name: &str, mode: Mode, include_hf: bool) {
150 println!();
151 println!("Project '{}' created. Next steps:", name);
152 println!();
153 println!(" cd {}", name);
154 match mode {
155 Mode::Mounted => {
156 println!(" ./fdl setup # detect hardware + download libtorch");
157 println!(" ./fdl build # build the project");
158 }
159 Mode::Docker => {
160 println!(" ./fdl build # first build (downloads libtorch, ~5 min)");
161 }
162 Mode::Native => {
163 println!(" ./fdl libtorch download --cpu # or --cuda 12.8");
164 println!(" ./fdl build # cargo build on the host");
165 }
166 }
167 println!(" ./fdl test # run tests");
168 println!(" ./fdl run # train the model");
169 if mode != Mode::Native {
170 println!(" ./fdl shell # interactive shell");
171 }
172 if include_hf {
173 println!();
174 println!(" cd flodl-hf && fdl classify # try the HuggingFace playground");
175 }
176 println!();
177 println!("`./fdl --help` lists every command defined in fdl.yml.");
178 println!("Edit src/main.rs to build your model.");
179 println!();
180 println!("Guides:");
181 println!(" Tutorials: https://flodl.dev/guide/tensors");
182 println!(" Graph Tree: https://flodl.dev/guide/graph-tree");
183 println!(" PyTorch migration: https://flodl.dev/guide/migration");
184 println!(" Troubleshooting: https://flodl.dev/guide/troubleshooting");
185}
186
187fn write_fdl_bootstrap(name: &str) -> Result<(), String> {
188 let fdl_script = include_str!("../assets/fdl");
189 write_file(&format!("{}/fdl", name), fdl_script)?;
190 #[cfg(unix)]
191 {
192 use std::os::unix::fs::PermissionsExt;
193 let _ = fs::set_permissions(
194 format!("{}/fdl", name),
195 fs::Permissions::from_mode(0o755),
196 );
197 }
198 Ok(())
199}
200
201fn validate_name(name: &str) -> Result<(), String> {
202 if name.is_empty() {
203 return Err("project name cannot be empty".into());
204 }
205 if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
206 return Err("project name must contain only letters, digits, hyphens, underscores".into());
207 }
208 Ok(())
209}
210
211fn resolve_flodl_dep() -> String {
212 if let Some(version) = crates_io_version() {
214 format!("flodl = \"{}\"", version)
215 } else {
216 "flodl = { git = \"https://github.com/flodl-labs/flodl.git\" }".into()
217 }
218}
219
220fn crates_io_version() -> Option<String> {
221 let output = Command::new("curl")
222 .args(["-sL", "https://crates.io/api/v1/crates/flodl"])
223 .output()
224 .ok()?;
225 let body = String::from_utf8_lossy(&output.stdout);
226 let marker = "\"max_stable_version\":\"";
228 let start = body.find(marker)? + marker.len();
229 let end = start + body[start..].find('"')?;
230 let version = &body[start..end];
231 if version.is_empty() { None } else { Some(version.to_string()) }
232}
233
234fn scaffold_docker(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(
244 &format!("{}/Dockerfile.cpu", name),
245 DOCKERFILE_CPU,
246 )?;
247 write_file(
248 &format!("{}/Dockerfile.cuda", name),
249 DOCKERFILE_CUDA,
250 )?;
251 write_file(
252 &format!("{}/docker-compose.yml", name),
253 &docker_compose_template(crate_name, true),
254 )?;
255 Ok(())
256}
257
258fn scaffold_mounted(name: &str, crate_name: &str, flodl_dep: &str) -> Result<(), String> {
263 write_file(
264 &format!("{}/Cargo.toml", name),
265 &cargo_toml_template(crate_name, flodl_dep),
266 )?;
267 write_file(
268 &format!("{}/Dockerfile", name),
269 DOCKERFILE_MOUNTED,
270 )?;
271 write_file(
272 &format!("{}/Dockerfile.cuda", name),
273 DOCKERFILE_CUDA_MOUNTED,
274 )?;
275 write_file(
276 &format!("{}/docker-compose.yml", name),
277 &docker_compose_template(crate_name, false),
278 )?;
279 Ok(())
280}
281
282fn scaffold_native(name: &str, crate_name: &str, flodl_dep: &str) -> Result<(), String> {
287 write_file(
288 &format!("{}/Cargo.toml", name),
289 &cargo_toml_template(crate_name, flodl_dep),
290 )?;
291 Ok(())
294}
295
296fn cargo_toml_template(crate_name: &str, flodl_dep: &str) -> String {
301 format!(
302 r#"[package]
303name = "{crate_name}"
304version = "0.1.0"
305edition = "2024"
306
307[dependencies]
308{flodl_dep}
309
310# Optimize floDl in dev builds -- your code stays fast to compile.
311# After the first build, only your graph code recompiles (~2s).
312[profile.dev.package.flodl]
313opt-level = 3
314
315[profile.dev.package.flodl-sys]
316opt-level = 3
317
318# Release: cross-crate optimization for maximum throughput.
319[profile.release]
320lto = "thin"
321codegen-units = 1
322"#
323 )
324}
325
326fn main_rs_template() -> String {
327 r#"//! floDl training template.
328//!
329//! This is a starting point for your model. Edit the architecture,
330//! data loading, and training loop to fit your task.
331//!
332//! New to Rust? Read: https://flodl.dev/guide/rust-primer
333//! Stuck? Read: https://flodl.dev/guide/troubleshooting
334
335use flodl::*;
336use flodl::monitor::Monitor;
337
338fn main() -> Result<()> {
339 // --- Model ---
340 let model = FlowBuilder::from(Linear::new(4, 32)?)
341 .through(GELU)
342 .through(LayerNorm::new(32)?)
343 .also(Linear::new(32, 32)?) // residual connection
344 .through(Linear::new(32, 1)?)
345 .build()?;
346
347 // --- Optimizer ---
348 let params = model.parameters();
349 let mut optimizer = Adam::new(¶ms, 0.001);
350 let scheduler = CosineScheduler::new(0.001, 1e-6, 100);
351 model.train();
352
353 // --- Data ---
354 // Replace this with your data loading.
355 let opts = TensorOptions::default();
356 let batches: Vec<(Tensor, Tensor)> = (0..32)
357 .map(|_| {
358 let x = Tensor::randn(&[16, 4], opts).unwrap();
359 let y = Tensor::randn(&[16, 1], opts).unwrap();
360 (x, y)
361 })
362 .collect();
363
364 // --- Training loop ---
365 let num_epochs = 100usize;
366 let mut monitor = Monitor::new(num_epochs);
367 // monitor.serve(3000)?; // uncomment for live dashboard
368 // monitor.watch(&model); // uncomment to show graph SVG
369 // monitor.save_html("report.html"); // uncomment to save HTML report
370
371 for epoch in 0..num_epochs {
372 let t = std::time::Instant::now();
373 let mut epoch_loss = 0.0;
374
375 for (input_t, target_t) in &batches {
376 let input = Variable::new(input_t.clone(), true);
377 let target = Variable::new(target_t.clone(), false);
378
379 optimizer.zero_grad();
380 let pred = model.forward(&input)?;
381 let loss = mse_loss(&pred, &target)?;
382 loss.backward()?;
383 clip_grad_norm(¶ms, 1.0)?;
384 optimizer.step()?;
385
386 epoch_loss += loss.item()?;
387 }
388
389 let avg_loss = epoch_loss / batches.len() as f64;
390 let lr = scheduler.lr(epoch);
391 optimizer.set_lr(lr);
392 monitor.log(epoch, t.elapsed(), &[("loss", avg_loss), ("lr", lr)]);
393 }
394
395 monitor.finish();
396 Ok(())
397}
398"#
399 .into()
400}
401
402fn gitignore_template(mode: Mode) -> String {
403 let mut s = String::from(
404 "/target
405*.fdl
406*.log
407*.csv
408*.html
409
410# Local fdl config (fdl.yml.example is committed; fdl copies it on first run)
411fdl.yml
412fdl.yaml
413
414# Local docker-compose env (per-machine: UID/GID, libtorch variant override,
415# cargo job throttle)
416.env
417",
418 );
419 match mode {
420 Mode::Docker => {
421 s.push_str(
423 ".cargo-cache/
424.cargo-git/
425.cargo-cache-cuda/
426.cargo-git-cuda/
427",
428 );
429 }
430 Mode::Mounted => {
431 s.push_str(
433 ".cargo-cache/
434.cargo-git/
435.cargo-cache-cuda/
436.cargo-git-cuda/
437libtorch/
438",
439 );
440 }
441 Mode::Native => {
442 s.push_str("libtorch/\n");
445 }
446 }
447 s
448}
449
450fn docker_compose_template(crate_name: &str, baked: bool) -> String {
451 if baked {
452 format!(
453 r#"services:
454 dev:
455 build:
456 context: .
457 dockerfile: Dockerfile.cpu
458 image: {crate_name}-dev
459 user: "${{UID:-1000}}:${{GID:-1000}}"
460 volumes:
461 - .:/workspace
462 - ./.cargo-cache:/usr/local/cargo/registry
463 - ./.cargo-git:/usr/local/cargo/git
464 working_dir: /workspace
465 stdin_open: true
466 tty: true
467 environment:
468 # Throttle cargo's link parallelism. Unset on Linux native (empty →
469 # cargo's default); Mac hosts set it in `.env` to keep `ld` within what
470 # a virtiofs-backed workspace can serve. See docs/mac-apple-silicon.md.
471 - CARGO_BUILD_JOBS
472 # flodl runtime knobs, forwarded from the host (or `.env`):
473 # verbosity is what `fdl -v/-vv/...` sets per invocation, and the
474 # timeout scale stretches distributed network deadlines on slow links.
475 - FLODL_VERBOSITY
476 - FLODL_NET_TIMEOUT_SCALE
477
478 cuda:
479 build:
480 context: .
481 dockerfile: Dockerfile.cuda
482 image: {crate_name}-cuda
483 user: "${{UID:-1000}}:${{GID:-1000}}"
484 volumes:
485 - .:/workspace
486 - ./.cargo-cache-cuda:/usr/local/cargo/registry
487 - ./.cargo-git-cuda:/usr/local/cargo/git
488 working_dir: /workspace
489 stdin_open: true
490 tty: true
491 environment:
492 # flodl runtime knobs, forwarded from the host (or `.env`):
493 # verbosity is what `fdl -v/-vv/...` sets per invocation, and the
494 # timeout scale stretches distributed network deadlines on slow links.
495 - FLODL_VERBOSITY
496 - FLODL_NET_TIMEOUT_SCALE
497 deploy:
498 resources:
499 reservations:
500 devices:
501 - driver: nvidia
502 count: all
503 capabilities: [gpu]
504"#
505 )
506 } else {
507 format!(
508 r#"services:
509 dev:
510 build:
511 context: .
512 dockerfile: Dockerfile
513 image: {crate_name}-dev
514 user: "${{UID:-1000}}:${{GID:-1000}}"
515 volumes:
516 - .:/workspace
517 - ./.cargo-cache:/usr/local/cargo/registry
518 - ./.cargo-git:/usr/local/cargo/git
519 - ${{LIBTORCH_CPU_PATH:-./libtorch/precompiled/cpu}}:/usr/local/libtorch:ro
520 working_dir: /workspace
521 stdin_open: true
522 tty: true
523 environment:
524 # Throttle cargo's link parallelism. Required on macOS Docker / OrbStack,
525 # where the virtiofs-mounted libtorch directory cannot serve many
526 # concurrent `ld` lookups — the linker reports `cannot find -ltorch`
527 # spuriously. Unset on Linux native (empty → cargo's default).
528 - CARGO_BUILD_JOBS
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
535 cuda:
536 build:
537 context: .
538 dockerfile: Dockerfile.cuda
539 args:
540 CUDA_VERSION: ${{CUDA_VERSION:-12.8.0}}
541 image: {crate_name}-cuda:${{CUDA_TAG:-12.8}}
542 user: "${{UID:-1000}}:${{GID:-1000}}"
543 volumes:
544 - .:/workspace
545 - ./.cargo-cache-cuda:/usr/local/cargo/registry
546 - ./.cargo-git-cuda:/usr/local/cargo/git
547 - ${{LIBTORCH_HOST_PATH:-./libtorch/precompiled/cu128}}:/usr/local/libtorch:ro
548 working_dir: /workspace
549 stdin_open: true
550 tty: true
551 environment:
552 # flodl runtime knobs, forwarded from the host (or `.env`):
553 # verbosity is what `fdl -v/-vv/...` sets per invocation, and the
554 # timeout scale stretches distributed network deadlines on slow links.
555 - FLODL_VERBOSITY
556 - FLODL_NET_TIMEOUT_SCALE
557 deploy:
558 resources:
559 reservations:
560 devices:
561 - driver: nvidia
562 count: all
563 capabilities: [gpu]
564"#
565 )
566 }
567}
568
569const DOCKERFILE_CPU: &str = r#"# CPU-only dev image for floDl projects.
575FROM ubuntu:24.04
576
577ENV DEBIAN_FRONTEND=noninteractive
578
579RUN apt-get update && apt-get install -y --no-install-recommends \
580 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
581 && rm -rf /var/lib/apt/lists/*
582
583# Rust
584ENV CARGO_HOME="/usr/local/cargo"
585ENV RUSTUP_HOME="/usr/local/rustup"
586RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
587 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
588ENV PATH="${CARGO_HOME}/bin:${PATH}"
589
590# libtorch (CPU-only, ~200MB)
591ARG LIBTORCH_VERSION=2.10.0
592RUN wget -q https://download.pytorch.org/libtorch/cpu/libtorch-shared-with-deps-${LIBTORCH_VERSION}%2Bcpu.zip \
593 && unzip -q libtorch-shared-with-deps-${LIBTORCH_VERSION}+cpu.zip -d /usr/local \
594 && rm libtorch-shared-with-deps-${LIBTORCH_VERSION}+cpu.zip
595
596ENV LIBTORCH_PATH="/usr/local/libtorch"
597ENV LD_LIBRARY_PATH="${LIBTORCH_PATH}/lib"
598ENV LIBRARY_PATH="${LIBTORCH_PATH}/lib"
599
600WORKDIR /workspace
601"#;
602
603const DOCKERFILE_CUDA: &str = r#"# CUDA dev image for floDl projects.
604# Requires: docker run --gpus all ...
605FROM nvidia/cuda:12.8.0-devel-ubuntu24.04
606
607ENV DEBIAN_FRONTEND=noninteractive
608
609RUN apt-get update && apt-get install -y --no-install-recommends \
610 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
611 && rm -rf /var/lib/apt/lists/*
612
613# Rust
614ENV CARGO_HOME="/usr/local/cargo"
615ENV RUSTUP_HOME="/usr/local/rustup"
616RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
617 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
618ENV PATH="${CARGO_HOME}/bin:${PATH}"
619
620# libtorch (CUDA 12.8)
621ARG LIBTORCH_VERSION=2.10.0
622RUN wget -q "https://download.pytorch.org/libtorch/cu128/libtorch-shared-with-deps-${LIBTORCH_VERSION}%2Bcu128.zip" \
623 && unzip -q "libtorch-shared-with-deps-${LIBTORCH_VERSION}+cu128.zip" -d /usr/local \
624 && rm "libtorch-shared-with-deps-${LIBTORCH_VERSION}+cu128.zip"
625
626ENV LIBTORCH_PATH="/usr/local/libtorch"
627ENV LD_LIBRARY_PATH="${LIBTORCH_PATH}/lib:/usr/local/cuda/lib64"
628ENV LIBRARY_PATH="${LIBTORCH_PATH}/lib:/usr/local/cuda/lib64"
629ENV CUDA_HOME="/usr/local/cuda"
630
631WORKDIR /workspace
632"#;
633
634const DOCKERFILE_MOUNTED: &str = r#"# CPU dev image for floDl projects (libtorch mounted at runtime).
636FROM ubuntu:24.04
637
638ENV DEBIAN_FRONTEND=noninteractive
639
640RUN apt-get update && apt-get install -y --no-install-recommends \
641 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
642 && rm -rf /var/lib/apt/lists/*
643
644# Rust
645ENV CARGO_HOME="/usr/local/cargo"
646ENV RUSTUP_HOME="/usr/local/rustup"
647RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
648 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
649ENV PATH="${CARGO_HOME}/bin:${PATH}"
650
651ENV LIBTORCH_PATH="/usr/local/libtorch"
652ENV LD_LIBRARY_PATH="${LIBTORCH_PATH}/lib"
653ENV LIBRARY_PATH="${LIBTORCH_PATH}/lib"
654
655WORKDIR /workspace
656"#;
657
658const DOCKERFILE_CUDA_MOUNTED: &str = r#"# CUDA dev image for floDl projects (libtorch mounted at runtime).
659# Requires: docker run --gpus all ...
660ARG CUDA_VERSION=12.8.0
661FROM nvidia/cuda:${CUDA_VERSION}-devel-ubuntu24.04
662
663ENV DEBIAN_FRONTEND=noninteractive
664
665RUN apt-get update && apt-get install -y --no-install-recommends \
666 wget curl unzip ca-certificates git gcc g++ pkg-config graphviz \
667 && rm -rf /var/lib/apt/lists/*
668
669# Rust
670ENV CARGO_HOME="/usr/local/cargo"
671ENV RUSTUP_HOME="/usr/local/rustup"
672RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
673 && chmod -R a+rwx "$CARGO_HOME" "$RUSTUP_HOME"
674ENV PATH="${CARGO_HOME}/bin:${PATH}"
675
676ENV LIBTORCH_PATH="/usr/local/libtorch"
677ENV LD_LIBRARY_PATH="${LIBTORCH_PATH}/lib:/usr/local/cuda/lib64"
678ENV LIBRARY_PATH="${LIBTORCH_PATH}/lib:/usr/local/cuda/lib64"
679ENV CUDA_HOME="/usr/local/cuda"
680
681WORKDIR /workspace
682"#;
683
684fn env_example_template(mode: Mode) -> String {
700 let mut s = String::from(
701 "# Local Docker environment overrides for docker-compose.yml.
702# Copy this to `.env` (gitignored) and uncomment what you need:
703# cp .env.example .env
704# docker-compose auto-reads `.env` from this directory. This `.env.example`
705# is only a template; compose never reads it directly.
706
707# Host user/group mapping, so files created in the container are owned by you
708# rather than root. Defaults to 1000:1000 when unset; macOS is usually 501:20
709# (`id -u` / `id -g`).
710#UID=1000
711#GID=1000
712",
713 );
714 if mode == Mode::Mounted {
715 s.push_str(
716 "
717# libtorch mount points (host paths). Defaults live in docker-compose.yml.
718# Override to point at a different variant, e.g. an extracted linux-aarch64
719# build on Apple Silicon.
720#LIBTORCH_CPU_PATH=./libtorch/precompiled/cpu
721#LIBTORCH_HOST_PATH=./libtorch/precompiled/cu128
722
723# CUDA base image version and image tag for the `cuda` service.
724# Only affects direct `docker compose` calls: `fdl` derives both from the active
725# libtorch variant's `.arch` metadata and overrides whatever is set here.
726#CUDA_VERSION=12.8.0
727#CUDA_TAG=12.8
728",
729 );
730 }
731 s.push_str(
732 "
733# Throttle cargo build/link parallelism. Leave unset on Linux (uses all cores).
734# On Apple Silicon via Docker/OrbStack, set to 2 to avoid spurious
735# \"cannot find -ltorch\" linker errors caused by virtiofs mount latency.
736#CARGO_BUILD_JOBS=2
737
738# flodl log verbosity. `fdl -v/-vv/...` sets this per invocation; setting it
739# here makes a level stick without the flag.
740#FLODL_VERBOSITY=1
741
742# Scale every distributed network timeout (socket setup, coordinator deadlines).
743# Raise it on slow or congested links where the 30s LAN defaults are too tight.
744#FLODL_NET_TIMEOUT_SCALE=2
745",
746 );
747 s
748}
749
750fn fdl_yml_example_template(project_name: &str, mode: Mode) -> String {
751 let use_docker = matches!(mode, Mode::Mounted | Mode::Docker);
752 let (cpu_svc, cuda_svc) = if use_docker {
753 ("\n docker: dev", "\n docker: cuda")
754 } else {
755 ("", "")
756 };
757 let cuda_note = if use_docker {
758 "(requires NVIDIA Container Toolkit)"
759 } else {
760 "(requires a matching CUDA toolkit on the host)"
761 };
762 let preamble = if use_docker {
763 "# Run any of these with `./fdl <cmd>` (or `fdl <cmd>` once installed\n\
764 # globally via `./fdl install`). Libtorch env vars are derived from\n\
765 # `libtorch/.active` automatically; missing libtorch surfaces as a\n\
766 # clean linker error, with `./fdl setup` one call away."
767 } else {
768 "# Native mode: commands run on the host. Make sure libtorch is\n\
769 # installed (`./fdl libtorch download --cpu` or `--cuda 12.8`)\n\
770 # and that `$LIBTORCH` / `$LD_LIBRARY_PATH` are exported so\n\
771 # cargo can link. `./fdl libtorch info` prints the commands you\n\
772 # need after a download."
773 };
774
775 let shell_block = if use_docker {
776 format!(
777 r#" shell:
778 description: Interactive shell (CPU container)
779 run: bash{cpu_svc}
780
781"#
782 )
783 } else {
784 String::new()
786 };
787
788 let cuda_shell_block = if use_docker {
789 format!(
790 r#" cuda-shell:
791 description: Interactive shell (CUDA container)
792 run: bash{cuda_svc}
793"#
794 )
795 } else {
796 String::new()
797 };
798
799 format!(
800 r#"description: {project_name}
801
802{preamble}
803
804commands:
805 # --- CPU ---
806 build:
807 description: Build (debug)
808 run: cargo build{cpu_svc}
809 test:
810 description: Run CPU tests
811 run: cargo test -- --nocapture{cpu_svc}
812 run:
813 description: cargo run
814 run: cargo run{cpu_svc}
815 check:
816 description: Type-check without building
817 run: cargo check{cpu_svc}
818 clippy:
819 description: Lint
820 run: cargo clippy -- -W clippy::all{cpu_svc}
821{shell_block} # --- CUDA {cuda_note} ---
822 cuda-build:
823 description: Build with CUDA feature
824 run: cargo build --features cuda{cuda_svc}
825 cuda-test:
826 description: Run CUDA tests
827 run: cargo test --features cuda -- --nocapture{cuda_svc}
828 cuda-run:
829 description: cargo run --features cuda
830 run: cargo run --features cuda{cuda_svc}
831{cuda_shell_block}"#
832 )
833}
834
835fn write_file(path: &str, content: &str) -> Result<(), String> {
840 fs::write(path, content).map_err(|e| format!("cannot write {}: {}", path, e))
841}
842
843#[cfg(test)]
844mod tests {
845 use super::validate_name;
846
847 #[test]
848 fn validate_name_accepts_alnum_hyphen_underscore() {
849 for ok in ["my_project", "my-project", "Proj123", "a", "x_1-2"] {
850 assert!(validate_name(ok).is_ok(), "{ok:?} should be valid");
851 }
852 }
853
854 #[test]
855 fn validate_name_rejects_empty() {
856 let err = validate_name("").unwrap_err();
857 assert!(err.contains("empty"), "unexpected: {err}");
858 }
859
860 #[test]
861 fn validate_name_rejects_disallowed_chars() {
862 for bad in ["my project", "my.project", "a/b", "../evil", "name!"] {
865 let err = validate_name(bad).unwrap_err();
866 assert!(err.contains("only letters"), "{bad:?} -> unexpected: {err}");
867 }
868 }
869}