cargo_zng/
res.rs

1use std::{
2    fs, io,
3    ops::ControlFlow,
4    path::{Path, PathBuf},
5    time::Instant,
6};
7
8use anyhow::{Context as _, bail};
9
10use built_in::{ZR_WORKSPACE_DIR, display_path};
11use clap::*;
12use color_print::cstr;
13use zng_env::About;
14
15use crate::util;
16
17use self::tool::Tools;
18
19mod about;
20pub mod built_in;
21mod tool;
22
23#[derive(Args, Debug)]
24pub struct ResArgs {
25    /// Resources source dir
26    #[arg(default_value = "res")]
27    source: PathBuf,
28    /// Resources target dir
29    ///
30    /// This directory is wiped before each build.
31    #[arg(default_value = "target/res")]
32    target: PathBuf,
33
34    /// Copy all static files to the target dir
35    #[arg(long, action)]
36    pack: bool,
37
38    /// Search for `zng-res-{tool}` in this directory first
39    #[arg(long, default_value = "tools", value_name = "DIR")]
40    tool_dir: PathBuf,
41    /// Prints help for all tools available
42    #[arg(long, action)]
43    tools: bool,
44    /// Prints the full help for a tool
45    #[arg(long)]
46    tool: Option<String>,
47
48    /// Tools cache dir
49    #[arg(long, default_value = "target/res.cache")]
50    tool_cache: PathBuf,
51
52    /// Number of build passes allowed before final
53    #[arg(long, default_value = "32")]
54    recursion_limit: u32,
55
56    /// TOML file that that defines metadata uses by tools (ZR_APP, ZR_ORG, ..)
57    ///
58    /// This is only needed if the workspace has multiple bin crates
59    /// and none or many set '[package.metadata.zng.about]'.
60    ///
61    /// See `zng::env::About` for more details.
62    #[arg(long, value_name = "TOML_FILE")]
63    metadata: Option<PathBuf>,
64
65    /// Writes the metadata extracted the workspace or --metadata
66    #[arg(long, action)]
67    metadata_dump: bool,
68
69    /// Use verbose output.
70    #[arg(short, long, action)]
71    verbose: bool,
72}
73
74fn canonicalize(path: &Path) -> PathBuf {
75    dunce::canonicalize(path).unwrap_or_else(|e| fatal!("cannot resolve path, {e}"))
76}
77
78pub(crate) fn run(mut args: ResArgs) {
79    if args.tool_dir.exists() {
80        args.tool_dir = canonicalize(&args.tool_dir);
81    }
82    if args.tools {
83        return tools_help(&args.tool_dir);
84    }
85    if let Some(t) = args.tool {
86        return tool_help(&args.tool_dir, &t);
87    }
88
89    if args.metadata_dump {
90        let about = about::find_about(args.metadata.as_deref(), args.verbose);
91        crate::res::tool::visit_about_vars(&about, |key, value| {
92            println!("{key}={value}");
93        });
94        return;
95    }
96
97    if !args.source.exists() {
98        fatal!("source dir does not exist");
99    }
100    if let Err(e) = fs::create_dir_all(&args.tool_cache) {
101        fatal!("cannot create cache dir, {e}");
102    }
103    if let Err(e) = fs::remove_dir_all(&args.target) {
104        if e.kind() != io::ErrorKind::NotFound {
105            fatal!("cannot remove target dir, {e}");
106        }
107    }
108    if let Err(e) = fs::create_dir_all(&args.target) {
109        fatal!("cannot create target dir, {e}");
110    }
111
112    args.source = canonicalize(&args.source);
113    args.target = canonicalize(&args.target);
114    args.tool_cache = canonicalize(&args.tool_cache);
115
116    if args.source == args.target {
117        fatal!("cannot build res to same dir");
118    }
119
120    let about = about::find_about(args.metadata.as_deref(), args.verbose);
121
122    // tool request paths are relative to the workspace root
123    if let Some(p) = util::workspace_dir() {
124        if let Err(e) = std::env::set_current_dir(p) {
125            fatal!("cannot change dir, {e}");
126        }
127    } else {
128        warn!("source is not in a cargo workspace, tools will run using source as root");
129        if let Err(e) = std::env::set_current_dir(&args.source) {
130            fatal!("cannot change dir, {e}");
131        }
132    }
133
134    unsafe {
135        // SAFETY: cargo-zng res is single-threaded
136        //
137        // to use `display_path` in the tool runner (current process)
138        std::env::set_var(ZR_WORKSPACE_DIR, std::env::current_dir().unwrap());
139    }
140
141    let start = Instant::now();
142    if let Err(e) = build(&args, about) {
143        let e = e.to_string();
144        for line in e.lines() {
145            eprintln!("   {line}");
146        }
147        fatal!("res build failed");
148    }
149
150    println!(cstr!("<bold><green>Finished</green></bold> res build in {:?}"), start.elapsed());
151    println!("         {}", args.target.display());
152}
153
154fn build(args: &ResArgs, about: About) -> anyhow::Result<()> {
155    let tools = Tools::capture(&args.tool_dir, args.tool_cache.clone(), about, args.verbose)?;
156    source_to_target_pass(args, &tools, &args.source, &args.target)?;
157
158    let mut passes = 0;
159    while target_to_target_pass(args, &tools, &args.target)? {
160        passes += 1;
161        if passes >= args.recursion_limit {
162            bail!("reached --recursion-limit of {}", args.recursion_limit)
163        }
164    }
165
166    tools.run_final(&args.source, &args.target)
167}
168
169fn source_to_target_pass(args: &ResArgs, tools: &Tools, source: &Path, target: &Path) -> anyhow::Result<()> {
170    for entry in walkdir::WalkDir::new(source).min_depth(1).max_depth(1).sort_by_file_name() {
171        let entry = entry.with_context(|| format!("cannot read dir entry {}", source.display()))?;
172        if entry.file_type().is_dir() {
173            let source = entry.path();
174            // mirror dir in target
175            println!("{}", display_path(source));
176            let target = target.join(source.file_name().unwrap());
177            fs::create_dir(&target).with_context(|| format!("cannot create_dir {}", target.display()))?;
178            println!(cstr!("  <dim>{}</>"), display_path(&target));
179
180            source_to_target_pass(args, tools, source, &target)?;
181        } else if entry.file_type().is_file() {
182            let source = entry.path();
183
184            // run tool
185            if let Some(ext) = source.extension() {
186                let ext = ext.to_string_lossy();
187                if let Some(tool) = ext.strip_prefix("zr-") {
188                    // run prints request
189                    tools.run(tool, &args.source, &args.target, source)?;
190                    continue;
191                }
192            }
193
194            // or pack
195            if args.pack {
196                println!("{}", display_path(source));
197                let target = target.join(source.file_name().unwrap());
198                fs::copy(source, &target).with_context(|| format!("cannot copy {} to {}", source.display(), target.display()))?;
199                println!(cstr!("  <dim>{}</>"), display_path(&target));
200            }
201        } else if entry.file_type().is_symlink() {
202            built_in::symlink_warn(entry.path());
203        }
204    }
205    Ok(())
206}
207
208fn target_to_target_pass(args: &ResArgs, tools: &Tools, dir: &Path) -> anyhow::Result<bool> {
209    let mut any = false;
210    for entry in walkdir::WalkDir::new(dir).min_depth(1).sort_by_file_name() {
211        let entry = entry.with_context(|| format!("cannot read dir entry {}", dir.display()))?;
212        if entry.file_type().is_file() {
213            let path = entry.path();
214            // run tool
215            if let Some(ext) = path.extension() {
216                let ext = ext.to_string_lossy();
217                if let Some(tool) = ext.strip_prefix("zr-") {
218                    any = true;
219                    // run prints request
220                    let tool_r = tools.run(tool, &args.source, &args.target, path);
221                    fs::remove_file(path)?;
222                    tool_r?;
223                }
224            }
225        }
226    }
227    Ok(any)
228}
229
230fn tools_help(tools: &Path) {
231    let r = tool::visit_tools(tools, |tool| {
232        if crate::util::ansi_enabled() {
233            println!(cstr!("<bold>.zr-{}</bold> @ {}"), tool.name, display_tool_path(&tool.path));
234        } else {
235            println!(".zr-{} @ {}", tool.name, display_tool_path(&tool.path));
236        }
237        match tool.help() {
238            Ok(h) => {
239                if let Some(line) = h.trim().lines().next() {
240                    println!("  {line}");
241                    println!();
242                }
243            }
244            Err(e) => error!("{e}"),
245        }
246        Ok(ControlFlow::Continue(()))
247    });
248    if let Err(e) = r {
249        fatal!("{e}")
250    }
251    println!("call 'cargo zng res --help tool' to read full help from a tool");
252}
253
254fn tool_help(tools: &Path, name: &str) {
255    let name = name.strip_prefix(".zr-").unwrap_or(name);
256    let mut found = false;
257    let r = tool::visit_tools(tools, |tool| {
258        if tool.name == name {
259            if crate::util::ansi_enabled() {
260                println!(cstr!("<bold>.zr-{}</bold> @ {}"), tool.name, display_tool_path(&tool.path));
261            } else {
262                println!(".zr-{}</bold> @ {}", tool.name, display_tool_path(&tool.path));
263            }
264            match tool.help() {
265                Ok(h) => {
266                    for line in h.trim().lines() {
267                        println!("  {line}");
268                    }
269                    if !h.is_empty() {
270                        println!();
271                    }
272                }
273                Err(e) => error!("{e}"),
274            }
275            found = true;
276            Ok(ControlFlow::Break(()))
277        } else {
278            Ok(ControlFlow::Continue(()))
279        }
280    });
281    if let Err(e) = r {
282        fatal!("{e}")
283    }
284    if !found {
285        fatal!("did not find tool `{name}`")
286    }
287}
288
289fn display_tool_path(p: &Path) -> String {
290    let base = util::workspace_dir().unwrap_or_else(|| std::env::current_dir().unwrap());
291    let r = if let Ok(local) = p.strip_prefix(base) {
292        local.display().to_string()
293    } else {
294        p.file_name().unwrap().to_string_lossy().into_owned()
295    };
296
297    #[cfg(windows)]
298    return r.replace('\\', "/");
299
300    #[cfg(not(windows))]
301    r
302}