1use std::fs;
4use std::path::{Path, PathBuf};
5
6use crate::context::Context;
7use crate::util::http;
8use crate::util::archive;
9use crate::util::system;
10use super::detect;
11
12const LIBTORCH_VERSION: &str = "2.10.0";
17
18struct VariantSpec {
20 label: &'static str,
22 dir_name: &'static str,
24 arch_cuda: &'static str,
26 arch_archs: &'static str,
28 arch_variant: &'static str,
30}
31
32const CPU_SPEC: VariantSpec = VariantSpec {
33 label: "CPU",
34 dir_name: "cpu",
35 arch_cuda: "none",
36 arch_archs: "cpu",
37 arch_variant: "cpu",
38};
39
40const CU126_SPEC: VariantSpec = VariantSpec {
41 label: "CUDA 12.6",
42 dir_name: "cu126",
43 arch_cuda: "12.6",
44 arch_archs: "5.0 5.2 6.0 6.1 7.0 7.5 8.0 8.6 8.9 9.0",
45 arch_variant: "cu126",
46};
47
48const CU128_SPEC: VariantSpec = VariantSpec {
49 label: "CUDA 12.8",
50 dir_name: "cu128",
51 arch_cuda: "12.8",
52 arch_archs: "7.0 7.5 8.0 8.6 8.9 9.0 12.0",
53 arch_variant: "cu128",
54};
55
56pub enum Variant {
61 Cpu,
62 Cuda126,
63 Cuda128,
64 Auto,
65}
66
67pub struct DownloadOpts {
68 pub variant: Variant,
69 pub custom_path: Option<PathBuf>,
70 pub activate: bool,
71 pub dry_run: bool,
72 pub force_linux: bool,
78}
79
80impl Default for DownloadOpts {
81 fn default() -> Self {
82 Self {
83 variant: Variant::Auto,
84 custom_path: None,
85 activate: true,
86 dry_run: false,
87 force_linux: false,
88 }
89 }
90}
91
92fn download_url(spec: &VariantSpec, force_linux: bool) -> Result<String, String> {
97 let (os, arch) = if force_linux {
101 ("linux", "x86_64")
102 } else {
103 (std::env::consts::OS, std::env::consts::ARCH)
104 };
105
106 match (os, arch) {
107 ("linux", "x86_64") => {}
108 ("macos", "aarch64") => {
109 if spec.arch_cuda != "none" {
110 return Err("macOS only supports CPU libtorch".into());
111 }
112 }
113 ("macos", _) => {
114 return Err(format!(
115 "macOS libtorch requires Apple Silicon (arm64), got {}.\n\
116 macOS x86_64 was dropped after PyTorch 2.2.",
117 arch
118 ));
119 }
120 ("windows", "x86_64") => {}
121 _ => {
122 return Err(format!(
123 "Unsupported platform: {} {}.\n\
124 libtorch is available for Linux x86_64, macOS arm64, and Windows x86_64.",
125 os, arch
126 ));
127 }
128 }
129
130 if os == "macos" {
132 return Ok(format!(
133 "https://download.pytorch.org/libtorch/cpu/libtorch-macos-arm64-{}.zip",
134 LIBTORCH_VERSION
135 ));
136 }
137
138 let filename = match spec.arch_variant {
140 "cpu" => format!(
141 "libtorch-shared-with-deps-{}%2Bcpu.zip",
142 LIBTORCH_VERSION
143 ),
144 variant => format!(
145 "libtorch-shared-with-deps-{}%2B{}.zip",
146 LIBTORCH_VERSION, variant
147 ),
148 };
149
150 let bucket = spec.arch_variant; Ok(format!(
152 "https://download.pytorch.org/libtorch/{}/{}",
153 bucket, filename
154 ))
155}
156
157fn auto_detect_variant() -> &'static VariantSpec {
162 let gpus = system::detect_gpus();
163 if gpus.is_empty() {
164 println!(" No NVIDIA GPU detected. Using CPU variant.");
165 return &CPU_SPEC;
166 }
167
168 let lo_major = gpus.iter().map(|g| g.sm_major).min().unwrap_or(0);
170 let hi_major = gpus.iter().map(|g| g.sm_major).max().unwrap_or(0);
171
172 if lo_major >= 7 {
174 println!(" Detected Volta+ GPU(s). Using cu128.");
175 &CU128_SPEC
176 } else if hi_major >= 10 {
177 println!(
180 " Mixed GPU architectures (sm_{}.x to sm_{}.x).",
181 lo_major, hi_major
182 );
183 println!(" Using cu126 (broadest pre-Volta coverage).");
184 println!(" For all GPUs, consider: fdl libtorch build");
185 &CU126_SPEC
186 } else {
187 println!(" Detected pre-Volta GPU(s). Using cu126.");
188 &CU126_SPEC
189 }
190}
191
192fn resolve_variant(variant: &Variant) -> &'static VariantSpec {
193 match variant {
194 Variant::Cpu => &CPU_SPEC,
195 Variant::Cuda126 => &CU126_SPEC,
196 Variant::Cuda128 => &CU128_SPEC,
197 Variant::Auto => auto_detect_variant(),
198 }
199}
200
201pub fn run(opts: DownloadOpts) -> Result<(), String> {
206 let ctx = Context::resolve();
207 run_with_context(opts, &ctx)
208}
209
210pub fn run_with_context(opts: DownloadOpts, ctx: &Context) -> Result<(), String> {
212 let spec = resolve_variant(&opts.variant);
213 let url = download_url(spec, opts.force_linux)?;
214
215 let install_path = if let Some(ref p) = opts.custom_path {
217 p.clone()
218 } else {
219 ctx.root.join(format!("libtorch/precompiled/{}", spec.dir_name))
220 };
221
222 let variant_id = format!("precompiled/{}", spec.dir_name);
223
224 println!();
225 println!(" libtorch {} ({})", LIBTORCH_VERSION, spec.label);
226 println!(" URL: {}", url);
227 println!(" Path: {}", install_path.display());
228
229 if opts.dry_run {
230 println!();
231 println!(" [dry-run] Would download and extract to above path.");
232 return Ok(());
233 }
234
235 if install_path.exists() {
237 let build_ver_path = install_path.join("build-version");
238 let existing_ver = fs::read_to_string(&build_ver_path)
239 .ok()
240 .map(|s| s.trim().to_string());
241
242 let ver_matches = existing_ver.as_deref().is_some_and(|v| {
244 v == LIBTORCH_VERSION || v.starts_with(&format!("{}+", LIBTORCH_VERSION))
245 });
246
247 if ver_matches {
248 println!();
249 println!(" Already installed (version {}).", LIBTORCH_VERSION);
250 return Ok(());
251 }
252
253 println!();
254 println!(
255 " Removing existing installation (version: {})...",
256 existing_ver.as_deref().unwrap_or("unknown")
257 );
258 fs::remove_dir_all(&install_path)
259 .map_err(|e| format!("cannot remove {}: {}", install_path.display(), e))?;
260 }
261
262 let tmp_dir = std::env::temp_dir();
264 let tmp_zip = tmp_dir.join(format!("libtorch-{}-{}.zip", spec.dir_name, LIBTORCH_VERSION));
265
266 println!();
267 println!(" Downloading...");
268 http::download_file(&url, &tmp_zip)?;
269
270 let tmp_extract = tmp_dir.join(format!("libtorch-extract-{}", std::process::id()));
272 println!(" Extracting...");
273 archive::extract_zip(&tmp_zip, &tmp_extract)?;
274
275 let extracted_lt = tmp_extract.join("libtorch");
277 let source = if extracted_lt.is_dir() {
278 &extracted_lt
279 } else {
280 &tmp_extract
281 };
282
283 fs::create_dir_all(&install_path)
284 .map_err(|e| format!("cannot create {}: {}", install_path.display(), e))?;
285
286 move_contents(source, &install_path)?;
288
289 let _ = fs::remove_file(&tmp_zip);
291 let _ = fs::remove_dir_all(&tmp_extract);
292
293 let lib_dir = install_path.join("lib");
295 let has_lib = lib_dir.join("libtorch.so").exists()
296 || lib_dir.join("libtorch.dylib").exists()
297 || lib_dir.join("torch.lib").exists();
298
299 if !has_lib {
300 return Err(format!(
301 "libtorch library not found at {}.\n\
302 The archive structure may have changed.\n\
303 Check: ls {}",
304 lib_dir.display(),
305 lib_dir.display()
306 ));
307 }
308
309 let arch_content = format!(
311 "cuda={}\ntorch={}\narchs={}\nsource=precompiled\nvariant={}\n",
312 spec.arch_cuda, LIBTORCH_VERSION, spec.arch_archs, spec.arch_variant
313 );
314 fs::write(install_path.join(".arch"), arch_content)
315 .map_err(|e| format!("cannot write .arch: {}", e))?;
316
317 if opts.activate {
318 detect::set_active(&ctx.root, &variant_id)?;
319 }
320
321 println!();
322 println!(" ================================================");
323 println!(" libtorch {} ({}) installed", LIBTORCH_VERSION, spec.label);
324 println!(" {}", install_path.display());
325 println!(" ================================================");
326
327 if ctx.is_project {
328 println!();
329 println!(" .arch: {}/.arch", install_path.display());
330 if opts.activate {
331 println!(" .active: libtorch/.active -> {}", variant_id);
332 }
333 println!();
334 if spec.arch_cuda != "none" {
335 println!(" Run 'fdl cuda-test' to verify.");
336 } else {
337 println!(" Run 'fdl test' to verify.");
338 }
339 } else {
340 println!();
341 println!(" Installed to: {}", install_path.display());
342 println!();
343 println!(" To use with tch-rs or flodl, add to your shell profile:");
344 println!();
345 println!(" export LIBTORCH=\"{}\"", install_path.display());
346 println!(
347 " export LD_LIBRARY_PATH=\"{}/lib${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\"",
348 install_path.display()
349 );
350 println!();
351 println!(" Or start a new floDl project:");
352 println!(" fdl init my-project");
353 }
354
355 Ok(())
356}
357
358fn move_contents(src: &Path, dest: &Path) -> Result<(), String> {
364 let entries = fs::read_dir(src)
365 .map_err(|e| format!("cannot read {}: {}", src.display(), e))?;
366
367 for entry in entries {
368 let entry = entry.map_err(|e| format!("read_dir error: {}", e))?;
369 let from = entry.path();
370 let name = entry.file_name();
371 let to = dest.join(&name);
372
373 if fs::rename(&from, &to).is_err() {
375 if from.is_dir() {
376 copy_dir_recursive(&from, &to)?;
377 } else {
378 fs::copy(&from, &to)
379 .map_err(|e| format!("copy {} -> {}: {}", from.display(), to.display(), e))?;
380 }
381 }
382 }
383 Ok(())
384}
385
386fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), String> {
387 fs::create_dir_all(dest)
388 .map_err(|e| format!("cannot create {}: {}", dest.display(), e))?;
389
390 for entry in fs::read_dir(src).map_err(|e| format!("read {}: {}", src.display(), e))? {
391 let entry = entry.map_err(|e| format!("read_dir error: {}", e))?;
392 let from = entry.path();
393 let to = dest.join(entry.file_name());
394
395 if from.is_dir() {
396 copy_dir_recursive(&from, &to)?;
397 } else {
398 fs::copy(&from, &to)
399 .map_err(|e| format!("copy {} -> {}: {}", from.display(), to.display(), e))?;
400 }
401 }
402 Ok(())
403}
404
405#[allow(dead_code)]
407pub fn libtorch_version() -> &'static str {
408 LIBTORCH_VERSION
409}