Skip to main content

zig_core/
pack.rs

1use std::io::Write;
2use std::path::{Path, PathBuf};
3
4use crate::error::ZigError;
5use crate::workflow::parser;
6
7/// Pack a workflow directory into a `.zug` zip archive.
8///
9/// The directory must contain exactly one workflow TOML file (`.toml` or `.zug`
10/// that contains a `[workflow]` section). All files in the directory are included
11/// in the archive. The resulting zip file can be used directly with `zig run`
12/// and `zig validate`.
13pub fn pack(dir_path: &str, output: Option<&str>) -> Result<PathBuf, ZigError> {
14    let dir = Path::new(dir_path);
15    if !dir.is_dir() {
16        return Err(ZigError::Io(format!(
17            "'{}' is not a directory",
18            dir.display()
19        )));
20    }
21
22    // Find the workflow TOML file
23    let toml_file = find_workflow_toml(dir)?;
24
25    // Validate it parses correctly
26    let content = std::fs::read_to_string(&toml_file)
27        .map_err(|e| ZigError::Io(format!("failed to read {}: {e}", toml_file.display())))?;
28    let wf = parser::parse(&content)?;
29
30    // Determine output path
31    let output_path = if let Some(out) = output {
32        PathBuf::from(out)
33    } else {
34        let name = wf.workflow.name.replace(' ', "-").to_lowercase();
35        PathBuf::from(format!("{name}.zug"))
36    };
37
38    // Collect all files in the directory
39    let files = collect_files(dir)?;
40
41    // Create the zip archive
42    let file = std::fs::File::create(&output_path)
43        .map_err(|e| ZigError::Io(format!("failed to create {}: {e}", output_path.display())))?;
44    let mut zip = zip::ZipWriter::new(file);
45    let options = zip::write::SimpleFileOptions::default()
46        .compression_method(zip::CompressionMethod::Deflated);
47
48    for file_path in &files {
49        let relative = file_path
50            .strip_prefix(dir)
51            .map_err(|e| ZigError::Io(format!("path error: {e}")))?;
52        let name = relative.to_string_lossy().replace('\\', "/");
53
54        zip.start_file(&name, options)
55            .map_err(|e| ZigError::Io(format!("failed to add {name} to archive: {e}")))?;
56
57        let contents = std::fs::read(file_path)
58            .map_err(|e| ZigError::Io(format!("failed to read {}: {e}", file_path.display())))?;
59        zip.write_all(&contents)
60            .map_err(|e| ZigError::Io(format!("failed to write {name} to archive: {e}")))?;
61    }
62
63    zip.finish()
64        .map_err(|e| ZigError::Io(format!("failed to finalize archive: {e}")))?;
65
66    eprintln!(
67        "packed {} files into '{}' (workflow: '{}')",
68        files.len(),
69        output_path.display(),
70        wf.workflow.name
71    );
72
73    Ok(output_path)
74}
75
76/// Find the single workflow TOML file in a directory.
77fn find_workflow_toml(dir: &Path) -> Result<PathBuf, ZigError> {
78    let mut candidates = Vec::new();
79
80    for entry in std::fs::read_dir(dir)
81        .map_err(|e| ZigError::Io(format!("failed to read directory {}: {e}", dir.display())))?
82    {
83        let entry =
84            entry.map_err(|e| ZigError::Io(format!("failed to read directory entry: {e}")))?;
85        let path = entry.path();
86        if path.is_file() {
87            if let Some(ext) = path.extension() {
88                if ext == "toml" || ext == "zug" {
89                    if let Ok(content) = std::fs::read_to_string(&path) {
90                        if content.contains("[workflow]") {
91                            candidates.push(path);
92                        }
93                    }
94                }
95            }
96        }
97    }
98
99    match candidates.len() {
100        0 => Err(ZigError::Io(format!(
101            "no workflow TOML file found in '{}'",
102            dir.display()
103        ))),
104        1 => Ok(candidates.into_iter().next().unwrap()),
105        n => Err(ZigError::Io(format!(
106            "found {n} workflow files in '{}' (expected exactly one): {}",
107            dir.display(),
108            candidates
109                .iter()
110                .map(|p| p
111                    .file_name()
112                    .unwrap_or_default()
113                    .to_string_lossy()
114                    .to_string())
115                .collect::<Vec<_>>()
116                .join(", ")
117        ))),
118    }
119}
120
121/// Recursively collect all files in a directory.
122fn collect_files(dir: &Path) -> Result<Vec<PathBuf>, ZigError> {
123    let mut files = Vec::new();
124
125    fn walk(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), ZigError> {
126        for entry in std::fs::read_dir(dir)
127            .map_err(|e| ZigError::Io(format!("failed to read directory: {e}")))?
128        {
129            let entry =
130                entry.map_err(|e| ZigError::Io(format!("failed to read directory entry: {e}")))?;
131            let path = entry.path();
132            if path.is_dir() {
133                walk(&path, files)?;
134            } else {
135                files.push(path);
136            }
137        }
138        Ok(())
139    }
140
141    walk(dir, &mut files)?;
142    files.sort();
143    Ok(files)
144}
145
146#[cfg(test)]
147#[path = "pack_tests.rs"]
148mod tests;