1use std::{
2 fs,
3 io::{self, BufRead, Read, Write},
4 ops::ControlFlow,
5 path::{Path, PathBuf},
6};
7
8use anyhow::{Context, bail};
9use is_executable::IsExecutable as _;
10use parking_lot::Mutex;
11use zng_env::About;
12
13use crate::{res::built_in::ZR_APP_ID, res_tool_util::*, util::unix_path};
14
15pub fn visit_tools(local: &Path, mut tool: impl FnMut(Tool) -> anyhow::Result<ControlFlow<()>>) -> anyhow::Result<()> {
17 macro_rules! tool {
18 ($($args:tt)+) => {
19 let flow = tool($($args)+)?;
20 if flow.is_break() {
21 return Ok(())
22 }
23 };
24 }
25
26 let mut local_bin_crate = None;
27 if local.exists() {
28 for entry in fs::read_dir(local).with_context(|| format!("cannot read_dir {}", local.display()))? {
29 let path = entry.with_context(|| format!("cannot read_dir entry {}", local.display()))?.path();
30 if path.is_dir() {
31 let name = path.file_name().unwrap().to_string_lossy();
32 if let Some(name) = name.strip_prefix("cargo-zng-res-") {
33 if path.join("Cargo.toml").exists() {
34 tool!(Tool {
35 name: name.to_owned(),
36 kind: ToolKind::LocalCrate,
37 path,
38 });
39 }
40 } else if name == "cargo-zng-res" && path.join("Cargo.toml").exists() {
41 local_bin_crate = Some(path);
42 }
43 }
44 }
45 }
46
47 if let Some(path) = local_bin_crate {
48 let bin_dir = path.join("src/bin");
49 for entry in fs::read_dir(&bin_dir).with_context(|| format!("cannot read_dir {}", bin_dir.display()))? {
50 let path = entry
51 .with_context(|| format!("cannot read_dir entry {}", bin_dir.display()))?
52 .path();
53 if path.is_file() {
54 let name = path.file_name().unwrap().to_string_lossy();
55 if let Some(name) = name.strip_suffix(".rs") {
56 tool!(Tool {
57 name: name.to_owned(),
58 kind: ToolKind::LocalBin,
59 path,
60 });
61 }
62 }
63 }
64 }
65
66 let current_exe = std::env::current_exe()?;
67
68 for &name in crate::res::built_in::BUILT_INS {
69 tool!(Tool {
70 name: name.to_owned(),
71 kind: ToolKind::BuiltIn,
72 path: current_exe.clone(),
73 });
74 }
75
76 let install_dir = current_exe
77 .parent()
78 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no cargo install dir"))?;
79
80 for entry in fs::read_dir(install_dir).with_context(|| format!("cannot read_dir {}", install_dir.display()))? {
81 let path = entry
82 .with_context(|| format!("cannot read_dir entry {}", install_dir.display()))?
83 .path();
84 if path.is_file() {
85 let name = path.file_name().unwrap().to_string_lossy();
86 if let Some(name) = name.strip_prefix("cargo-zng-res-")
87 && path.is_executable()
88 {
89 tool!(Tool {
90 name: name.split('.').next().unwrap().to_owned(),
91 kind: ToolKind::Installed,
92 path,
93 });
94 }
95 }
96 }
97
98 Ok(())
99}
100
101pub fn visit_about_vars(about: &About, mut visit: impl FnMut(&str, &str)) {
102 visit(ZR_APP_ID, &about.app_id);
103 visit(ZR_APP, &about.app);
104 visit(ZR_CRATE_NAME, &about.crate_name());
105 visit(ZR_HOMEPAGE, &about.homepage);
106 visit(ZR_LICENSE, &about.license);
107 visit(ZR_ORG, &about.org);
108 visit(ZR_PKG_AUTHORS, &about.pkg_authors.clone().join(","));
109 visit(ZR_PKG_NAME, &about.pkg_name);
110 visit(ZR_QUALIFIER, &about.qualifier());
111 visit(ZR_VERSION, &about.version.to_string());
112 visit(ZR_DESCRIPTION, &about.description);
113 for (key, value) in &about.meta {
114 if !value.is_empty() && !key.is_empty() {
115 visit(&format!("ZR_META_{}", key.to_uppercase().replace('-', "_")), value);
116 }
117 }
118}
119
120pub struct Tool {
121 pub name: String,
122 pub kind: ToolKind,
123
124 pub path: PathBuf,
125}
126impl Tool {
127 pub fn help(&self) -> anyhow::Result<String> {
128 let out = self.cmd().env(ZR_HELP, "").output()?;
129 if !out.status.success() {
130 let error = String::from_utf8_lossy(&out.stderr);
131 bail!("{error}\nhelp run failed, exit code {}", out.status.code().unwrap_or(0));
132 }
133 Ok(String::from_utf8_lossy(&out.stdout).into_owned())
134 }
135
136 fn run(
137 &self,
138 cache: &Path,
139 source_dir: &Path,
140 target_dir: &Path,
141 request: &Path,
142 about: &About,
143 final_args: Option<String>,
144 ) -> anyhow::Result<ToolOutput> {
145 use sha2::Digest;
146 let mut hasher = digest_io::IoWrapper(sha2::Sha256::new());
147
148 hasher.0.update(source_dir.as_os_str().as_encoded_bytes());
149 hasher.0.update(target_dir.as_os_str().as_encoded_bytes());
150 hasher.0.update(request.as_os_str().as_encoded_bytes());
151
152 let mut hash_request = || -> anyhow::Result<()> {
153 let mut file = fs::File::open(request)?;
154 io::copy(&mut file, &mut hasher)?;
155 Ok(())
156 };
157 if let Err(e) = hash_request() {
158 fatal!("cannot read request `{}`, {e}", request.display());
159 }
160
161 let cache_dir = format!("{:x}", base16ct::HexDisplay(&hasher.0.finalize()));
162
163 let mut cmd = self.cmd();
164 if let Some(args) = final_args {
165 cmd.env(ZR_FINAL, args);
166 }
167
168 let mut target = request.with_extension("");
170 if let Ok(p) = target.strip_prefix(source_dir) {
172 target = target_dir.join(p);
173 }
174
175 cmd.env(ZR_WORKSPACE_DIR, &*unix_path(&std::env::current_dir().unwrap()))
176 .env(ZR_SOURCE_DIR, &*unix_path(source_dir))
177 .env(ZR_TARGET_DIR, &*unix_path(target_dir))
178 .env(ZR_REQUEST_DD, &*unix_path(request.parent().unwrap()))
179 .env(ZR_REQUEST, &*unix_path(request))
180 .env(ZR_TARGET_DD, &*unix_path(target.parent().unwrap()))
181 .env(ZR_TARGET, &*unix_path(&target))
182 .env(ZR_CACHE_DIR, &*unix_path(&cache.join(cache_dir)));
183 visit_about_vars(about, |key, value| {
184 cmd.env(key, value);
185 });
186 self.run_cmd(&mut cmd)
187 }
188
189 fn cmd(&self) -> std::process::Command {
190 use std::process::Command;
191
192 match self.kind {
193 ToolKind::LocalCrate => {
194 let mut cmd = Command::new("cargo");
195 cmd.arg("run")
196 .arg("--quiet")
197 .arg("--manifest-path")
198 .arg(self.path.join("Cargo.toml"))
199 .arg("--");
200 cmd
201 }
202 ToolKind::LocalBin => {
203 let mut cmd = Command::new("cargo");
204 cmd.arg("run")
205 .arg("--quiet")
206 .arg("--manifest-path")
207 .arg(self.path.parent().unwrap().parent().unwrap().parent().unwrap().join("Cargo.toml"))
208 .arg("--bin")
209 .arg(&self.name)
210 .arg("--");
211 cmd
212 }
213 ToolKind::BuiltIn => {
214 let mut cmd = Command::new(&self.path);
215 cmd.env(crate::res::built_in::ENV_TOOL, &self.name);
216 cmd
217 }
218 ToolKind::Installed => Command::new(&self.path),
219 }
220 }
221
222 fn run_cmd(&self, cmd: &mut std::process::Command) -> anyhow::Result<ToolOutput> {
223 let mut cmd = cmd
224 .stdin(std::process::Stdio::null())
225 .stdout(std::process::Stdio::piped())
226 .stderr(std::process::Stdio::piped())
227 .spawn()?;
228
229 let cmd_err = cmd.stderr.take().unwrap();
231 let error_pipe = std::thread::Builder::new()
232 .name("stderr-reader".into())
233 .spawn(move || {
234 for line in io::BufReader::new(cmd_err).lines() {
235 match line {
236 Ok(l) => eprintln!(" {l}"),
237 Err(e) => {
238 error!("{e}");
239 return;
240 }
241 }
242 }
243 })
244 .expect("failed to spawn thread");
245
246 let mut requests = vec![];
248 const REQUEST: &[u8] = b"zng-res::";
249 let mut cmd_out = cmd.stdout.take().unwrap();
250 let mut out = io::stdout();
251 let mut buf = [0u8; 1024];
252
253 let mut at_line_start = true;
254 let mut maybe_request_start = None;
255
256 print!("\x1B[2m"); loop {
258 let len = cmd_out.read(&mut buf)?;
259 if len == 0 {
260 break;
261 }
262
263 for s in buf[..len].split_inclusive(|&c| c == b'\n') {
264 if at_line_start {
265 if s.starts_with(REQUEST) || REQUEST.starts_with(s) {
266 maybe_request_start = Some(requests.len());
267 }
268 if maybe_request_start.is_none() {
269 out.write_all(b" ")?;
270 }
271 }
272 if maybe_request_start.is_none() {
273 out.write_all(s)?;
274 out.flush()?;
275 } else {
276 requests.write_all(s).unwrap();
277 }
278
279 at_line_start = s.last() == Some(&b'\n');
280 if at_line_start
281 && let Some(i) = maybe_request_start.take()
282 && !requests[i..].starts_with(REQUEST)
283 {
284 out.write_all(&requests[i..])?;
285 out.flush()?;
286 requests.truncate(i);
287 }
288 }
289 }
290 print!("\x1B[0m"); let _ = std::io::stdout().flush();
292
293 let status = cmd.wait()?;
294 let _ = error_pipe.join();
295 if status.success() {
296 Ok(ToolOutput::from(String::from_utf8_lossy(&requests).as_ref()))
297 } else {
298 bail!("command failed, exit code {}", status.code().unwrap_or(0))
299 }
300 }
301}
302
303pub struct Tools {
304 tools: Vec<Tool>,
305 cache: PathBuf,
306 on_final: Mutex<Vec<(usize, PathBuf, String)>>,
307 about: About,
308}
309impl Tools {
310 pub fn capture(local: &Path, cache: PathBuf, about: About, verbose: bool) -> anyhow::Result<Self> {
311 let mut tools = vec![];
312 visit_tools(local, |t| {
313 if verbose {
314 println!("found tool `{}` in `{}`", t.name, t.path.display())
315 }
316 tools.push(t);
317 Ok(ControlFlow::Continue(()))
318 })?;
319 Ok(Self {
320 tools,
321 cache,
322 on_final: Mutex::new(vec![]),
323 about,
324 })
325 }
326
327 pub fn run(&self, tool_name: &str, source: &Path, target: &Path, request: &Path) -> anyhow::Result<bool> {
329 println!("{}", display_path(request));
330 for (i, tool) in self.tools.iter().enumerate() {
331 if tool.name == tool_name {
332 let mut fin = self.on_final.lock();
333 if fin.iter().any(|(ti, r, _)| *ti == i && r == request) {
334 return Ok(false);
336 }
337
338 let output = tool.run(&self.cache, source, target, request, &self.about, None)?;
339 for warn in output.warnings {
340 warn!("{warn}")
341 }
342 let done = output.on_final.is_empty();
343 if done {
344 if request.starts_with(target) {
345 if let Err(e) = fs::remove_file(request) {
347 bail!("cannot cleanup request {}, {e}", display_path(target))
348 }
349 }
350 } else {
351 for args in output.on_final {
352 fin.push((i, request.to_owned(), args));
353 }
354 }
355 if !output.delegate {
356 return Ok(done);
357 }
358 }
359 }
360 bail!("no tool `{tool_name}` to handle request")
361 }
362
363 pub fn run_final(self, source: &Path, target: &Path) -> anyhow::Result<()> {
364 let on_final = self.on_final.into_inner();
365 if !on_final.is_empty() {
366 println!("--final--");
367 for (i, request, args) in on_final {
368 println!("{}", display_path(&request));
369 let output = self.tools[i].run(&self.cache, source, target, &request, &self.about, Some(args))?;
370 for warn in output.warnings {
371 warn!("{warn}")
372 }
373 if request.starts_with(target) {
374 if let Err(e) = fs::remove_file(request) {
376 bail!("cannot cleanup request {}, {e}", display_path(target))
377 }
378 }
379 }
380 }
381 Ok(())
382 }
383}
384
385struct ToolOutput {
386 pub delegate: bool,
388 pub warnings: Vec<String>,
390 pub on_final: Vec<String>,
392}
393impl From<&str> for ToolOutput {
394 fn from(value: &str) -> Self {
395 let mut out = Self {
396 delegate: false,
397 warnings: vec![],
398 on_final: vec![],
399 };
400 for line in value.lines() {
401 if line == "zng-res::delegate" {
402 out.delegate = true;
403 } else if let Some(w) = line.strip_prefix("zng-res::warning=") {
404 out.warnings.push(w.to_owned());
405 } else if let Some(a) = line.strip_prefix("zng-res::on-final=") {
406 out.on_final.push(a.to_owned());
407 }
408 }
409 out
410 }
411}
412
413#[derive(Clone, Copy)]
414pub enum ToolKind {
415 LocalCrate,
416 LocalBin,
417 BuiltIn,
418 Installed,
419}