1#[doc(hidden)]
6pub mod config;
7mod depinfo;
8#[doc(hidden)]
9pub mod diagnostic;
10mod input;
11mod links;
12mod monostate;
13mod output;
14mod preproc;
15mod verify;
16
17#[doc(inline)]
19pub use self::{input::TargetType, output::TemplateContext};
20
21use self::{
22 config::{Config, ConfigOutput},
23 diagnostic::DiagnosticPrinter,
24 input::{CrateCode, InputFile}
25};
26use camino::Utf8Path;
27use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Target};
28use either::Either;
29use itertools::Itertools as _;
30use miette::{miette, Context as _, IntoDiagnostic as _, MietteHandlerOpts, Severity};
31use std::{
32 borrow::Cow,
33 env, fmt,
34 fs::{self, File},
35 io, iter,
36 sync::{Arc, Mutex}
37};
38
39#[doc(hidden)]
40pub fn install_miette_hook(wrap_lines: bool) {
41 miette::set_hook(Box::new(move |_| {
44 let mut styles = miette::ThemeStyles::ansi();
45 styles.highlights.truncate(1);
46 Box::new(
47 MietteHandlerOpts::new()
48 .unicode(false)
49 .graphical_theme(miette::GraphicalTheme {
50 characters: miette::ThemeCharacters {
51 error: "\nERROR:".into(),
52 warning: "\nWARNING:".into(),
53 advice: "\nINFO:".into(),
54 ..miette::ThemeCharacters::unicode()
55 },
56 styles
57 })
58 .tab_width(4)
59 .wrap_lines(wrap_lines)
60 .build()
61 )
62 }))
63 .expect("Failed to initialise error report hook");
64}
65
66#[doc(hidden)]
67pub struct App<W> {
68 cfg: Config,
69
70 pub stderr: W,
72
73 metadata: Metadata
75}
76
77#[doc(hidden)]
78pub struct Instance<'a, W> {
79 cfg: &'a Config,
81 metadata: &'a Metadata,
83
84 pkg: Option<&'a str>,
86 out: Cow<'a, Utf8Path>,
88 template_path: Option<Cow<'a, Utf8Path>>,
90
91 stderr: W,
93 code: CrateCode,
95
96 input_file: InputFile,
97 template_filename: Cow<'static, str>,
98 template: Cow<'static, str>,
99 builtin_template: bool
100}
101
102impl<W: io::Write> App<W> {
103 pub fn with_config(cfg: Config, stderr: W) -> miette::Result<Self> {
104 let mut cmd = MetadataCommand::new();
106 cmd.features(CargoOpt::AllFeatures);
107 if let Some(path) = &cfg.manifest_path {
108 cmd.manifest_path(path);
109 }
110 let metadata = cmd
111 .exec()
112 .map_err(|err| diagnostic::ExecError::new(err, &cmd.cargo_command()))?;
113
114 Ok(Self {
115 cfg,
116 stderr,
117 metadata
118 })
119 }
120
121 pub fn check_cfg(&mut self) {
122 let code = CrateCode::new_unknown();
123 let mut printer = DiagnosticPrinter::new(&code, &mut self.stderr);
124
125 if self.cfg.package.is_some() && self.cfg.packages.is_some() {
126 printer.print(diagnostic::PackagesIgnored);
127 }
128
129 if !self.cfg.expand_macros {
130 if self.cfg.features.is_some() {
131 printer.print(diagnostic::NoOpWithoutExpandMacros::flag("--features"));
132 }
133 if !self.cfg.default_features {
134 printer.print(diagnostic::NoOpWithoutExpandMacros::flag(
135 "--no-default-features"
136 ));
137 }
138 if self.cfg.all_features {
139 printer
140 .print(diagnostic::NoOpWithoutExpandMacros::flag("--all-features"));
141 }
142 }
143 }
144
145 #[track_caller]
151 pub fn instance(&mut self) -> Instance<'_, &mut W> {
152 if self.cfg.workspace && self.cfg.package.is_none() {
155 panic!("There might be multiple instances");
156 }
157 let out = match &self.cfg.out {
160 ConfigOutput::SingleFile(out) => out,
161 ConfigOutput::MultiFile(_) => panic!("There might be multiple instances")
162 };
163
164 Instance {
165 cfg: &self.cfg,
166 metadata: &self.metadata,
167 pkg: self.cfg.package.as_deref(),
168 out: out.as_path().into(),
169 template_path: self.cfg.template.as_deref().map(Into::into),
170 stderr: &mut self.stderr,
171 code: CrateCode::new_unknown(),
172 input_file: InputFile::dummy(),
173 template_filename: "<builtin-template>".into(),
174 template: include_str!("README.j2").into(),
175 builtin_template: true
176 }
177 }
178
179 pub fn instances(
180 &mut self
181 ) -> impl Iterator<Item = Instance<'_, impl io::Write + '_>> + '_ {
182 let pkg_iter = if self.cfg.workspace && self.cfg.package.is_none() {
183 let iter = self
184 .metadata
185 .workspace_packages()
186 .into_iter()
187 .map(|pkg| pkg.name.as_str());
188 Either::Left(
189 match &self.cfg.packages {
190 Some(pkgs) => Either::Left(iter.filter(|pkg| pkgs.contains(*pkg))),
191 None => Either::Right(iter)
192 }
193 .map(Some)
194 )
195 } else {
196 Either::Right(iter::once(self.cfg.package.as_deref()))
197 };
198
199 let out_iter = match &self.cfg.out {
200 ConfigOutput::SingleFile(out) => {
201 Either::Left(iter::once((out, self.cfg.template.as_ref())))
202 },
203 ConfigOutput::MultiFile(files) if files.is_empty() => {
204 let code = CrateCode::new_unknown();
205 let mut printer = DiagnosticPrinter::new(&code, &mut self.stderr);
206 printer.print(diagnostic::NoOutputs);
207 return Either::Left(iter::empty());
208 },
209 ConfigOutput::MultiFile(files) => Either::Right(files.iter().map(|file| {
210 (
211 &file.out,
212 file.template.as_ref().or(self.cfg.template.as_ref())
213 )
214 }))
215 };
216
217 let cfg = &self.cfg;
218 let metadata = &self.metadata;
219 let stderr = SharedWrite::new(&mut self.stderr);
220
221 Either::Right(pkg_iter.cartesian_product(out_iter).map(
222 move |(pkg, (out, template_path))| Instance {
223 cfg,
224 metadata,
225 pkg,
226 out: out.as_path().into(),
227 template_path: template_path.map(|path| path.as_path().into()),
228 stderr: stderr.clone(),
229 code: CrateCode::new_unknown(),
230 input_file: InputFile::dummy(),
231 template_filename: "<builtin-template>".into(),
232 template: include_str!("README.j2").into(),
233 builtin_template: true
234 }
235 ))
236 }
237}
238
239struct SharedWrite<'a, W>(Arc<Mutex<&'a mut W>>);
240
241impl<'a, W> SharedWrite<'a, W> {
242 pub fn new(inner: &'a mut W) -> Self {
243 Self(Arc::new(Mutex::new(inner)))
244 }
245}
246
247impl<W> Clone for SharedWrite<'_, W> {
248 fn clone(&self) -> Self {
249 Self(Arc::clone(&self.0))
250 }
251}
252
253impl<W: io::Write> io::Write for SharedWrite<'_, W> {
254 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
255 self.0.lock().unwrap().write(buf)
256 }
257
258 fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
259 self.0.lock().unwrap().write_vectored(bufs)
260 }
261
262 fn flush(&mut self) -> io::Result<()> {
263 self.0.lock().unwrap().flush()
264 }
265
266 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
267 self.0.lock().unwrap().write_all(buf)
268 }
269
270 fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> {
271 self.0.lock().unwrap().write_fmt(args)
272 }
273}
274
275impl<W: io::Write> Instance<'_, W> {
276 pub fn read_input(&mut self) -> miette::Result<()> {
277 let features = match &self.cfg.features {
279 None => None,
280 Some(Either::Left(features)) => Some(features.clone()),
281 Some(Either::Right(features)) => Some(features.join(","))
282 };
283
284 let pkg = match self.pkg {
286 Some(package) => {
287 let pkg = self
288 .metadata
289 .packages
290 .iter()
291 .find(|pkg| pkg.name == package)
292 .ok_or_else(|| {
293 diagnostic::PackageNotFoundError::new(
294 package,
295 self.metadata.workspace_packages()
296 )
297 })?;
298 if !self.metadata.workspace_members.contains(&pkg.id) {
299 let code = CrateCode::new_unknown();
300 let mut printer = DiagnosticPrinter::new(&code, &mut self.stderr);
301
302 printer.print(diagnostic::PackageNotInWorkspace::new(
303 package,
304 self.metadata.workspace_packages()
305 ));
306 }
307 pkg
309 },
310 None => self.metadata.root_package().ok_or_else(|| {
311 diagnostic::NoPackageError::new(self.metadata.workspace_packages())
312 })?
313 };
314
315 if self.cfg.workspace {
317 let parent = match pkg.manifest_path.parent() {
318 Some(parent) => parent,
319 None => {
320 return Err(miette!(
321 "Unable to get parent directory of {}",
322 pkg.manifest_path
323 ))
324 },
325 };
326 if self.out.is_relative() {
327 self.out = parent.join(&self.out).into();
328 }
329 if let Some(template_path) = self.template_path.as_deref() {
330 if template_path.is_relative() {
331 self.template_path = Some(parent.join(template_path).into());
332 }
333 }
334 }
335
336 let is_lib = |target: &&Target| target.is_lib() || target.is_proc_macro();
340 let is_default_bin =
341 |target: &&Target| target.is_bin() && target.name == pkg.name.as_str();
342 let target_and_type = if self.cfg.preferred_target == config::Target::Bin {
343 pkg.targets
344 .iter()
345 .find(is_default_bin)
346 .map(|target| (target, TargetType::Bin))
347 .or_else(|| {
348 pkg.targets
349 .iter()
350 .find(is_lib)
351 .map(|target| (target, TargetType::Lib))
352 })
353 } else {
354 pkg.targets
355 .iter()
356 .find(is_lib)
357 .map(|target| (target, TargetType::Lib))
358 .or_else(|| {
359 pkg.targets
360 .iter()
361 .find(is_default_bin)
362 .map(|target| (target, TargetType::Bin))
363 })
364 };
365 let (target, target_type) = target_and_type
366 .or_else(|| {
367 pkg.targets
368 .iter()
369 .find(|target| target.is_bin())
370 .map(|target| (target, TargetType::Bin))
371 })
372 .ok_or(diagnostic::NoTargetError)?;
373
374 let default_template: &Utf8Path = "README.j2".as_ref();
376 let (template_filename, template, builtin_template) = match &self.template_path {
377 Some(template_path) => {
378 let template = fs::read_to_string(template_path.as_std_path())
379 .into_diagnostic()
380 .with_context(|| {
381 format!("Failed to read template from `{template_path}'",)
382 })?;
383 let template_path_stripped =
384 template_path.strip_prefix(env::current_dir().unwrap());
385 (
386 template_path_stripped
387 .unwrap_or(template_path)
388 .to_string()
389 .into(),
390 template.into(),
391 false
392 )
393 },
394 None if default_template.exists() => {
395 let template = fs::read_to_string(default_template)
396 .into_diagnostic()
397 .with_context(|| {
398 format!("Failed to read template from `{default_template}'",)
399 })?;
400 (default_template.as_str().into(), template.into(), false)
401 },
402 None => (
403 "<builtin-template>".into(),
404 include_str!("README.j2").into(),
405 true
406 )
407 };
408 self.template_filename = template_filename;
409 self.template = template;
410 self.builtin_template = builtin_template;
411
412 let file = &target.src_path;
414 self.code = if self.cfg.expand_macros {
415 CrateCode::read_expansion(
416 self.cfg.manifest_path.as_ref(),
417 self.pkg,
418 target,
419 features.as_deref(),
420 !self.cfg.default_features,
421 self.cfg.all_features
422 )?
423 } else {
424 CrateCode::read_from_disk(file)
425 .into_diagnostic()
426 .with_context(|| format!("Failed to read source from `{file}'"))?
427 };
428
429 self.input_file = input::read_code(
431 self.metadata,
432 pkg,
433 &self.code,
434 target_type,
435 &mut self.stderr
436 )?;
437
438 Ok(())
439 }
440
441 fn out_reader(&self) -> io::Result<impl io::Read> {
445 Ok(if self.out.as_str() == "-" {
446 Either::Left(io::stdin())
447 } else {
448 Either::Right(File::open(self.out.as_std_path())?)
449 })
450 }
451
452 fn out_writer(&self) -> io::Result<impl io::Write> {
454 Ok(if self.out.as_str() == "-" {
455 Either::Left(io::stdout())
456 } else {
457 Either::Right(File::create(self.out.as_std_path())?)
458 })
459 }
460
461 pub fn check_up2date(self) -> bool {
464 let reader = self.out_reader().into_diagnostic().with_context(|| {
465 let out_str = if self.out.as_str() == "-" {
466 "<stdin>"
467 } else {
468 self.out.as_str()
469 };
470 format!("Failed to open {out_str}")
471 });
472 let reader = match reader {
473 Ok(reader) => reader,
474 Err(err) => {
475 let mut printer = DiagnosticPrinter::new(&self.code, self.stderr);
476 printer.print(err);
477 return false;
478 }
479 };
480
481 let res = verify::check_up2date(
482 self.input_file,
483 &self.template_filename,
484 &self.template,
485 self.builtin_template,
486 reader
487 );
488 if let Err(err) = res {
489 let is_error = err.severity().is_none_or(|s| s == Severity::Error);
490 let mut printer = DiagnosticPrinter::new(&self.code, self.stderr);
491 printer.print(err);
492 return !is_error;
493 }
494
495 true
496 }
497
498 pub fn emit(self) -> miette::Result<()> {
499 let writer = self.out_writer().into_diagnostic().with_context(|| {
500 let out_str = if self.out.as_str() == "-" {
501 "<stdout>"
502 } else {
503 self.out.as_str()
504 };
505 format!("Failed to open {out_str}")
506 })?;
507 self.emit_to(writer)
508 }
509
510 pub fn emit_to(self, writer: impl io::Write) -> miette::Result<()> {
511 output::emit(
512 &self.input_file,
513 &self.template_filename,
514 &self.template,
515 self.builtin_template,
516 writer
517 )
518 }
519}