Skip to main content

dsfb_semiconductor/
output_paths.rs

1use 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
53/// Compile a `.tex` file in-place, running pdflatex up to three times to
54/// resolve cross-references.  Returns the pdf path (if produced) and any
55/// captured error text.
56pub(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
117/// Recursively ZIP all files in `run_dir` into `zip_path`.
118pub(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}