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 #[arg(default_value = "res")]
27 source: PathBuf,
28 #[arg(default_value = "target/res")]
32 target: PathBuf,
33
34 #[arg(long, action)]
36 pack: bool,
37
38 #[arg(long, default_value = "tools", value_name = "DIR")]
40 tool_dir: PathBuf,
41 #[arg(long, action)]
43 tools: bool,
44 #[arg(long)]
46 tool: Option<String>,
47
48 #[arg(long, default_value = "target/res.cache")]
50 tool_cache: PathBuf,
51
52 #[arg(long, default_value = "32")]
54 recursion_limit: u32,
55
56 #[arg(long, value_name = "TOML_FILE")]
63 metadata: Option<PathBuf>,
64
65 #[arg(long, action)]
67 metadata_dump: bool,
68
69 #[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 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 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 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 if let Some(ext) = source.extension() {
186 let ext = ext.to_string_lossy();
187 if let Some(tool) = ext.strip_prefix("zr-") {
188 tools.run(tool, &args.source, &args.target, source)?;
190 continue;
191 }
192 }
193
194 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 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 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}