dsfb_semiconductor/
output_paths.rs1use crate::error::Result;
2use chrono::Local;
3use std::fs;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8pub fn crate_root() -> PathBuf {
9 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
10}
11
12pub fn repo_root() -> PathBuf {
13 crate_root()
14 .parent()
15 .and_then(Path::parent)
16 .map(Path::to_path_buf)
17 .expect("crate directory to live under <repo>/crates/dsfb-semiconductor")
18}
19
20pub fn default_data_root() -> PathBuf {
21 crate_root().join("data").join("raw")
22}
23
24pub fn default_output_root() -> PathBuf {
25 repo_root().join("output-dsfb-semiconductor")
26}
27
28pub fn create_timestamped_run_dir(output_root: &Path, dataset: &str) -> std::io::Result<PathBuf> {
29 let timestamp = Local::now().format("%Y%m%d_%H%M%S_%3f").to_string();
30 let base = format!("{timestamp}_dsfb-semiconductor_{dataset}");
31 for attempt in 0..1000 {
32 let candidate = if attempt == 0 {
33 output_root.join(&base)
34 } else {
35 output_root.join(format!("{base}_{attempt:03}"))
36 };
37 match std::fs::create_dir(&candidate) {
38 Ok(()) => return Ok(candidate),
39 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
40 Err(err) => return Err(err),
41 }
42 }
43
44 Err(std::io::Error::new(
45 std::io::ErrorKind::AlreadyExists,
46 format!(
47 "failed to allocate unique run directory under {}",
48 output_root.display()
49 ),
50 ))
51}
52
53pub(crate) fn compile_pdf(tex_path: &Path, output_dir: &Path) -> (Option<PathBuf>, Option<String>) {
57 let filename = tex_path
58 .file_name()
59 .map(|n| n.to_string_lossy().to_string())
60 .unwrap_or_else(|| "engineering_report.tex".into());
61 let pdf_path = output_dir.join(filename.replace(".tex", ".pdf"));
62 let mut combined_output = String::new();
63 let mut any_success = false;
64
65 for _ in 0..3 {
66 match Command::new("pdflatex")
67 .arg("-interaction=nonstopmode")
68 .arg("-halt-on-error")
69 .arg("-output-directory")
70 .arg(".")
71 .arg(&filename)
72 .current_dir(output_dir)
73 .output()
74 {
75 Ok(output) => {
76 let pass_output = format!(
77 "{}{}",
78 String::from_utf8_lossy(&output.stderr),
79 String::from_utf8_lossy(&output.stdout)
80 );
81 let needs_rerun = pass_output.contains("Rerun to get outlines right")
82 || pass_output.contains("Label(s) may have changed")
83 || pass_output.contains("Rerun to get cross-references right");
84 combined_output.push_str(&String::from_utf8_lossy(&output.stderr));
85 combined_output.push_str(&String::from_utf8_lossy(&output.stdout));
86 if output.status.success() {
87 any_success = true;
88 if !needs_rerun {
89 break;
90 }
91 }
92 }
93 Err(err) => {
94 if pdf_path.exists() {
95 return (Some(pdf_path), Some(err.to_string()));
96 }
97 return (None, Some(err.to_string()));
98 }
99 }
100 }
101
102 if any_success && pdf_path.exists() {
103 return (Some(pdf_path), None);
104 }
105 if pdf_path.exists() {
106 return (
107 Some(pdf_path),
108 (!combined_output.trim().is_empty()).then_some(combined_output),
109 );
110 }
111 (
112 None,
113 (!combined_output.trim().is_empty()).then_some(combined_output),
114 )
115}
116
117pub(crate) fn zip_directory(run_dir: &Path, zip_path: &Path) -> Result<()> {
119 use zip::write::SimpleFileOptions;
120 let file = fs::File::create(zip_path)?;
121 let mut zip = zip::ZipWriter::new(file);
122 let options =
123 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
124 let mut stack = vec![run_dir.to_path_buf()];
125 while let Some(dir) = stack.pop() {
126 for entry in fs::read_dir(&dir)? {
127 let entry = entry?;
128 let path = entry.path();
129 let relative = path
130 .strip_prefix(run_dir)
131 .unwrap_or(&path)
132 .to_string_lossy()
133 .replace('\\', "/");
134 if path.is_dir() {
135 stack.push(path);
136 } else {
137 zip.start_file(relative, options)?;
138 zip.write_all(&fs::read(path)?)?;
139 }
140 }
141 }
142 zip.finish()?;
143 Ok(())
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn output_root_targets_repo_level_directory() {
152 let output_root = default_output_root();
153 assert_eq!(
154 output_root.file_name().unwrap(),
155 "output-dsfb-semiconductor"
156 );
157 }
158
159 #[test]
160 fn timestamped_dir_contains_dataset_suffix() {
161 let temp = tempfile::tempdir().unwrap();
162 let run_dir = create_timestamped_run_dir(temp.path(), "secom").unwrap();
163 let name = run_dir.file_name().unwrap().to_string_lossy();
164 assert!(name.ends_with("_secom"));
165 assert!(name.contains("dsfb-semiconductor"));
166 }
167
168 #[test]
169 fn timestamped_dir_never_reuses_existing_directory_name() {
170 let temp = tempfile::tempdir().unwrap();
171 let first = create_timestamped_run_dir(temp.path(), "secom").unwrap();
172 let second = create_timestamped_run_dir(temp.path(), "secom").unwrap();
173 assert_ne!(first, second);
174 }
175}