playdate_build/assets/
plan.rs

1use std::collections::BTreeMap;
2use std::hash::Hash;
3use std::borrow::Cow;
4use std::str::FromStr;
5use std::path::{Path, PathBuf, MAIN_SEPARATOR};
6
7use wax::{Glob, Pattern};
8
9use crate::config::Env;
10use crate::metadata::format::{AssetsOptions, AssetsRules, RuleValue};
11
12use super::resolver::*;
13
14
15/// Create build plan for assets.
16pub fn build_plan<'l, 'r, S>(env: &Env,
17                             assets: &AssetsRules<S>,
18                             options: &AssetsOptions,
19                             crate_root: Option<Cow<'_, Path>>)
20                             -> Result<BuildPlan<'l, 'r>, super::Error>
21	where S: Eq + Hash + ToString + AsRef<str>
22{
23	// copy_unresolved    => get all files with glob
24	// include_unresolved => same
25	// exclude_unresolved =>
26	// 							- filter include_unresolved (actually already resolved)
27	// 							- filter list of files for items in copy_unresolved
28	// 								=> mark as have exclusions, so linking "file-file" instead of "dir-dir"
29
30	let mut map_unresolved = Vec::new();
31	let mut include_unresolved = Vec::new();
32	let mut exclude_exprs = Vec::new();
33
34	const PATH_SEPARATOR: [char; 2] = [MAIN_SEPARATOR, '/'];
35
36	let enver = EnvResolver::with_cache();
37	let crate_root = crate_root.unwrap_or_else(|| env.cargo_manifest_dir().into());
38	let link_behavior = options.link_behavior();
39
40	let to_relative = |s: &S| -> String {
41		let s = s.to_string();
42		let p = Path::new(&s);
43		if p.is_absolute() || p.has_root() {
44			let trailing_sep = p.components().count() > 1 && s.ends_with(PATH_SEPARATOR);
45			let mut s = p.components().skip(1).collect::<PathBuf>().display().to_string();
46			// preserve trailing separator
47			if trailing_sep && !s.ends_with(PATH_SEPARATOR) {
48				s.push(MAIN_SEPARATOR);
49			}
50			sanitize_path_pattern(&s).into_owned()
51		} else {
52			s.to_owned()
53		}
54	};
55
56
57	let assets_requirements = assets.env_required();
58	let compile_target_agnostic = !assets_requirements.contains(&"TARGET");
59	log::debug!("assets required env vars: {assets_requirements:?}");
60	log::debug!("compile-target-agnostic: {compile_target_agnostic}");
61
62
63	match assets {
64		AssetsRules::List(vec) => {
65			include_unresolved.extend(
66			                          vec.iter()
67			                             .map(to_relative)
68			                             .map(Expr::from)
69			                             .map(|e| enver.expr(e, env)),
70			)
71		},
72		AssetsRules::Map(map) => {
73			for (k, v) in map {
74				let k = to_relative(k);
75				match v {
76					RuleValue::Boolean(v) => {
77						match v {
78							true => include_unresolved.push(enver.expr(Expr::from(k), env)),
79							false => exclude_exprs.push(enver.expr(Expr::from(k), env)),
80						}
81					},
82					RuleValue::String(from) => {
83						map_unresolved.push((enver.expr(Expr::from(k), env), enver.expr(Expr::from(from), env)))
84					},
85				}
86			}
87		},
88	}
89
90
91	// prepare globs:
92	// TODO: possible opt - split exclude_exprs into absolute and relative
93	let exclude_globs: Vec<_> =
94		exclude_exprs.iter()
95		             .filter_map(|expr| {
96			             Glob::from_str(expr.as_str()).map_err(|err| error!("invalid filter expression: {err}"))
97			                                          .ok()
98		             })
99		             .collect();
100
101
102	// resolve map-pairs:
103	let mut mappings = Vec::new();
104	for (k, v) in map_unresolved.into_iter() {
105		let key = PathBuf::from(k.as_str());
106		let value = Cow::Borrowed(v.as_str());
107		let into_dir = k.as_str().ends_with(PATH_SEPARATOR);
108		let source_exists = abs_if_existing(Path::new(value.as_ref()), &crate_root)?.is_some();
109
110		let mapping = match (source_exists, into_dir) {
111			(true, true) => Mapping::Into(Match::new(value.as_ref(), key), (k, v)),
112			(true, false) => Mapping::AsIs(Match::new(value.as_ref(), key), (k, v)),
113			(false, _) => {
114				let mut resolved = resolve_includes(value, &crate_root, &exclude_exprs, link_behavior)?;
115
116				debug!("Possible ManyInto, resolved: {}", resolved.len());
117
118				// filter resolved includes:
119				let _excluded: Vec<_> = resolved.extract_if(.., |inc| {
120					                                let path = key.join(inc.target());
121					                                glob_matches_any(&path, &exclude_globs)
122				                                })
123				                                .collect();
124
125
126				Mapping::ManyInto { sources: resolved,
127				                    target: (&k).into(),
128				                    exprs: (k, v),
129				                    #[cfg(feature = "assets-report")]
130				                    excluded: _excluded }
131			},
132		};
133
134		mappings.push(mapping);
135	}
136
137
138	// re-mapping if needed:
139	for mapping in mappings.iter_mut() {
140		let possible = match &mapping {
141			Mapping::AsIs(inc, ..) => inc.source().is_dir() && possibly_matching_any(&inc.target(), &exclude_exprs),
142			Mapping::Into(inc, ..) => inc.source().is_dir() && possibly_matching_any(&inc.target(), &exclude_exprs),
143			Mapping::ManyInto { .. } => false,
144		};
145
146		if possible {
147			let (source_root, target, exprs) = match &mapping {
148				// 0. we're have path of existing dir `source`.
149				// 1. get all files from root `source` => `path` of files related to `source`
150				// 2. `target` path of file will depends on this `mapping`:
151				Mapping::AsIs(inc, expr) => {
152					// 2. `target` path of file:
153					// replace `source` with `target` in the abs path
154					(inc.source(), inc.target(), expr)
155				},
156				Mapping::Into(inc, expr) => {
157					// 2. `target` path of file:
158					// `target`/{source.name}/{rel path of file}
159					let source = inc.source();
160					let target = inc.target();
161					let target_base = target.join(source.file_name().expect("source filename"));
162					(source, Cow::from(target_base), expr)
163				},
164				Mapping::ManyInto { .. } => unreachable!(),
165			};
166
167			// find all/any files in the source:
168			let mut resolved = resolve_includes("**/*", &source_root, &exclude_exprs, link_behavior)?;
169
170			// filter resolved includes:
171			let is_not_empty = |inc: &Match| !inc.target().as_os_str().is_empty();
172			let excluded: Vec<_> = resolved.extract_if(.., |inc| {
173				                               let target = target.join(inc.target());
174				                               !is_not_empty(inc) ||
175				                               glob_matches_any(&inc.source(), &exclude_globs) ||
176				                               glob_matches_any(&target, &exclude_globs)
177			                               })
178			                               .collect();
179
180			// skip if no exclusions:
181			if excluded.is_empty() {
182				continue;
183			}
184
185			*mapping = Mapping::ManyInto { sources: resolved,
186			                               target: target.into(),
187			                               exprs: exprs.to_owned(),
188			                               #[cfg(feature = "assets-report")]
189			                               excluded };
190		}
191	}
192
193
194	for k in include_unresolved {
195		let resolved = resolve_includes(&k, &crate_root, &exclude_exprs, link_behavior)?;
196		mappings.extend(resolved.into_iter()
197		                        .map(|inc| Mapping::AsIs(inc, (k.clone(), "true".into()))));
198	}
199
200
201	// TODO: sort before dedup?
202	mappings.dedup_by(|a, b| a.eq_ignore_expr(b));
203
204	// TODO: find source duplicates and warn!
205
206
207	// get all used env vars:
208	let vars = enver.into_cache().unwrap_or_default();
209
210	Ok(BuildPlan { plan: mappings,
211	               crate_root: crate_root.to_path_buf(),
212	               compile_target_agnostic,
213	               vars })
214}
215
216
217/// Make path relative to `crate_root` if it isn't absolute, checking existence.
218/// Returns `None` if path doesn't exist.
219///
220/// Input `path` must be absolute or relative to the `root`.
221pub fn abs_if_existing<'t, P1, P2>(path: P1, root: P2) -> std::io::Result<Option<Cow<'t, Path>>>
222	where P1: 't + AsRef<Path> + Into<Cow<'t, Path>>,
223	      P2: AsRef<Path> {
224	let p = if path.as_ref().is_absolute() && path.as_ref().try_exists()? {
225		Some(path.into())
226	} else {
227		let abs = root.as_ref().join(path);
228		if abs.try_exists()? {
229			Some(Cow::Owned(abs))
230		} else {
231			None
232		}
233	};
234	Ok(p)
235}
236
237/// Same as [`abs_or_rel_crate_existing`], but returns given `path` as fallback.
238#[inline]
239pub fn abs_if_existing_any<'t, P1, P2>(path: P1, root: P2) -> Cow<'t, Path>
240	where P1: 't + AsRef<Path> + Into<Cow<'t, Path>> + Clone,
241	      P2: AsRef<Path> {
242	abs_if_existing(path.clone(), root).ok()
243	                                   .flatten()
244	                                   .unwrap_or(path.into())
245}
246
247
248fn glob_matches_any<'a, I: IntoIterator<Item = &'a Glob<'a>>>(path: &Path, exprs: I) -> bool {
249	exprs.into_iter().any(|glob| glob.is_match(path))
250}
251
252
253/// Compare (apply) each `expr` with `path` using exact same or more number of [`components`] as in `path`.
254/// Returns `true` if any of `exprs` matches the `path`.
255///
256/// Uses [`possibly_matching`].
257///
258/// [`components`]: PathBuf::components
259fn possibly_matching_any<P: Into<PathBuf>, I: IntoIterator<Item = P>>(path: &Path, exprs: I) -> bool {
260	exprs.into_iter().any(|expr| possibly_matching(path, expr))
261}
262
263
264// TODO: tests for `possibly_matching`
265/// Check that filter (possibly) pattern `expr` matches the `path`.
266fn possibly_matching<P: Into<PathBuf>>(path: &Path, expr: P) -> bool {
267	// TODO: remove {crate_root} part if it is from filter (or both?).
268
269	let len = path.components().count();
270	let filter: PathBuf = expr.into()
271	                          .components()
272	                          .enumerate() // TODO: just `skip`
273	                          .filter(|(i, _)| *i < len)
274	                          .map(|(_, p)| p)
275	                          .collect();
276
277	let glob = Glob::new(filter.as_os_str().to_str().unwrap()).unwrap();
278	glob.is_match(path)
279}
280
281
282/// Assets Build Plan for a crate.
283#[derive(Debug, PartialEq, Eq, Hash)]
284#[cfg_attr(feature = "serde", derive(serde::Serialize))]
285pub struct BuildPlan<'left, 'right> {
286	/// Instructions - what file where to put
287	plan: Vec<Mapping<'left, 'right>>,
288	/// Root directory of associated crate
289	crate_root: PathBuf,
290
291	/// `true` if assets requires `TARGET` env var
292	compile_target_agnostic: bool,
293	/// Environment variables used by assets
294	vars: BTreeMap<String, String>,
295}
296
297impl<'left, 'right> BuildPlan<'left, 'right> {
298	pub fn into_inner(self) -> Vec<Mapping<'left, 'right>> { self.plan }
299	pub fn as_inner(&self) -> &[Mapping<'left, 'right>] { &self.plan[..] }
300	pub fn into_parts(self) -> (Vec<Mapping<'left, 'right>>, PathBuf) { (self.plan, self.crate_root) }
301
302	pub fn crate_root(&self) -> &Path { &self.crate_root }
303	pub fn set_crate_root<T: Into<PathBuf>>(&mut self, path: T) -> PathBuf {
304		let old = std::mem::replace(&mut self.crate_root, path.into());
305		old
306	}
307
308	pub fn compile_target_agnostic(&self) -> bool { self.compile_target_agnostic }
309	pub fn used_env_vars(&self) -> &BTreeMap<String, String> { &self.vars }
310}
311
312impl<'left, 'right> AsRef<[Mapping<'left, 'right>]> for BuildPlan<'left, 'right> {
313	fn as_ref(&self) -> &[Mapping<'left, 'right>] { &self.plan[..] }
314}
315
316
317impl std::fmt::Display for BuildPlan<'_, '_> {
318	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319		let align = |f: &mut std::fmt::Formatter<'_>| -> std::fmt::Result {
320			if !matches!(f.align(), Some(std::fmt::Alignment::Right)) {
321				return Ok(());
322			}
323
324			let c = f.fill();
325			if let Some(width) = f.width() {
326				for _ in 0..width {
327					write!(f, "{c}")?;
328				}
329				Ok(())
330			} else {
331				write!(f, "{c}")
332			}
333		};
334
335
336		let print = |f: &mut std::fmt::Formatter<'_>,
337		             inc: &Match,
338		             (left, right): &(Expr, Expr),
339		             br: bool|
340		 -> std::fmt::Result {
341			let target = inc.target();
342			let source = inc.source();
343			let left = left.original();
344			let right = right.original();
345			align(f)?;
346			write!(f, "{target:#?} <- {source:#?}  ({left} = {right})")?;
347			if br { writeln!(f) } else { Ok(()) }
348		};
349
350		let items = self.as_inner();
351		let len = items.len();
352		for (i, item) in items.into_iter().enumerate() {
353			let last = i == len - 1;
354			match item {
355				Mapping::AsIs(inc, exprs) => print(f, inc, exprs, !last)?,
356				Mapping::Into(inc, exprs) => print(f, inc, exprs, !last)?,
357				Mapping::ManyInto { sources,
358				                    target,
359				                    exprs,
360				                    .. } => {
361					let len = sources.len();
362					for (i_in, inc) in sources.iter().enumerate() {
363						let last = last && i_in == len - 1;
364						let m = Match::new(inc.source(), target.join(inc.target()));
365						print(f, &m, exprs, !last)?;
366					}
367				},
368			}
369		}
370
371		Ok(())
372	}
373}
374
375impl BuildPlan<'_, '_> {
376	pub fn targets(&self) -> impl Iterator<Item = Cow<'_, Path>> {
377		self.as_inner().iter().flat_map(|mapping| {
378			                      match mapping {
379				                      Mapping::AsIs(inc, ..) => vec![inc.target()].into_iter(),
380			                         Mapping::Into(inc, ..) => vec![inc.target()].into_iter(),
381			                         Mapping::ManyInto { sources, target, .. } => {
382				                         sources.iter()
383				                                .map(|inc| Cow::from(target.join(inc.target())))
384				                                .collect::<Vec<_>>()
385				                                .into_iter()
386			                         },
387			                      }
388		                      })
389	}
390
391	pub fn iter_flatten(&self) -> impl Iterator<Item = (MappingKind, PathBuf, PathBuf)> + '_ {
392		let pair = |inc: &Match| {
393			(inc.target().to_path_buf(), abs_if_existing_any(inc.source(), &self.crate_root).to_path_buf())
394		};
395
396		self.as_inner().iter().flat_map(move |mapping| {
397			                      let mut rows = Vec::new();
398			                      let kind = mapping.kind();
399			                      match mapping {
400				                      Mapping::AsIs(inc, _) | Mapping::Into(inc, _) => rows.push(pair(inc)),
401			                         Mapping::ManyInto { sources, target, .. } => {
402				                         rows.extend(sources.iter().map(|inc| {
403					                             pair(&Match::new(inc.source(), target.join(inc.target())))
404				                             }));
405			                         },
406			                      };
407			                      rows.into_iter().map(move |(l, r)| (kind, l, r))
408		                      })
409	}
410
411	pub fn iter_flatten_meta(
412		&self)
413		-> impl Iterator<Item = (MappingKind, PathBuf, (PathBuf, Option<std::time::SystemTime>))> + '_ {
414		self.iter_flatten().map(|(k, t, p)| {
415			                   let time = p.metadata().ok().and_then(|m| m.modified().ok());
416			                   (k, t, (p, time))
417		                   })
418	}
419}
420
421
422#[derive(Debug, PartialEq, Eq, Hash)]
423#[cfg_attr(feature = "serde", derive(serde::Serialize))]
424pub enum Mapping<'left, 'right>
425	where Self: 'left + 'right {
426	// if right part exact path to ONE existing fs item
427	/// Copy source to target as-is.
428	AsIs(Match, (Expr<'left>, Expr<'right>)), // left part without trailing /
429
430	/// Copy source into target as-is.
431	Into(Match, (Expr<'left>, Expr<'right>)),
432	// if right part not exact (exist one)
433	ManyInto {
434		sources: Vec<Match>,
435
436		/// Target __directory__. Related path that should be preserved in the output.
437		target: PathBuf,
438
439		#[cfg(feature = "assets-report")]
440		excluded: Vec<Match>, // TODO: add reason for exclusions
441
442		exprs: (Expr<'left>, Expr<'right>),
443	},
444}
445
446impl Mapping<'_, '_> {
447	// TODO: tests for `Mapping::eq_ignore_expr`
448	pub fn eq_ignore_expr(&self, other: &Self) -> bool {
449		match (self, other) {
450			(Mapping::AsIs(a, _), Mapping::AsIs(b, _)) | (Mapping::Into(a, _), Mapping::Into(b, _)) => a.eq(b),
451			(Mapping::AsIs(a, _), Mapping::Into(b, _)) | (Mapping::Into(b, _), Mapping::AsIs(a, _)) => a.eq(b),
452
453			(Mapping::AsIs(..), Mapping::ManyInto { .. }) => false,
454			(Mapping::Into(..), Mapping::ManyInto { .. }) => false,
455			(Mapping::ManyInto { .. }, Mapping::AsIs(..)) => false,
456			(Mapping::ManyInto { .. }, Mapping::Into(..)) => false,
457
458			(
459			 Mapping::ManyInto { sources: sa,
460			                     target: ta,
461			                     .. },
462			 Mapping::ManyInto { sources: sb,
463			                     target: tb,
464			                     .. },
465			) => sa.eq(sb) && ta.eq(tb),
466		}
467	}
468
469	pub fn exprs(&self) -> (&Expr<'_>, &Expr<'_>) {
470		match self {
471			Mapping::AsIs(_, (left, right)) | Mapping::Into(_, (left, right)) => (left, right),
472			Mapping::ManyInto { exprs: (left, right), .. } => (left, right),
473		}
474	}
475
476	pub fn sources(&self) -> Vec<&Match> {
477		match self {
478			Mapping::AsIs(source, ..) | Mapping::Into(source, ..) => vec![source],
479			Mapping::ManyInto { sources, .. } => sources.iter().collect(),
480		}
481	}
482
483	pub fn kind(&self) -> MappingKind {
484		match self {
485			Mapping::AsIs(..) => MappingKind::AsIs,
486			Mapping::Into(..) => MappingKind::Into,
487			Mapping::ManyInto { .. } => MappingKind::ManyInto,
488		}
489	}
490
491
492	pub fn pretty_print_compact(&self) -> String {
493		let (k, l, r) = match self {
494			Mapping::AsIs(_, (l, r)) => ('=', l, r),
495			Mapping::Into(_, (l, r)) => ('I', l, r),
496			Mapping::ManyInto { exprs: (l, r), .. } => ('M', l, r),
497		};
498		format!("{{{k}:{:?}={:?}}}", l.original(), r.original())
499	}
500}
501
502
503#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
504#[cfg_attr(feature = "serde", derive(serde::Serialize))]
505pub enum MappingKind {
506	/// Copy source __to__ target.
507	AsIs,
508	/// Copy source __into__ target as-is, preserving related path.
509	Into,
510	/// Copy sources __into__ target as-is, preserving matched path.
511	ManyInto,
512}
513
514impl std::fmt::Display for MappingKind {
515	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516		match self {
517			Self::AsIs => "as-is".fmt(f),
518			Self::Into => "into".fmt(f),
519			Self::ManyInto => "many-into".fmt(f),
520		}
521	}
522}
523
524
525pub trait EnvRequired<'t> {
526	type Item;
527	type Output: IntoIterator<Item = Self::Item>;
528
529	fn env_required(&'t self) -> Self::Output;
530}
531
532
533impl<'t> EnvRequired<'t> for Mapping<'_, '_> where Self: 't {
534	type Item = &'t str;
535	type Output = Vec<Self::Item>;
536
537	fn env_required(&'t self) -> Self::Output {
538		let resolver = EnvResolver::new();
539
540		let keys = match self {
541			Mapping::AsIs(_, (l, r)) => [l.original(), r.original()],
542			Mapping::Into(_, (l, r)) => [l.original(), r.original()],
543			Mapping::ManyInto { exprs: (l, r), .. } => [l.original(), r.original()],
544		};
545
546		keys.into_iter()
547		    .flat_map(|s| resolver.matches(s))
548		    .map(|m| m.as_str())
549		    .collect()
550	}
551}
552
553
554impl<'s, S: 's + Eq + Hash + AsRef<str>> EnvRequired<'s> for AssetsRules<S> {
555	type Item = &'s str;
556	type Output = Vec<Self::Item>;
557
558	fn env_required(&'s self) -> Self::Output {
559		let resolver = EnvResolver::new();
560		self.env_required_with(&resolver).collect()
561	}
562}
563
564
565impl<S: Eq + Hash + AsRef<str>> AssetsRules<S> {
566	pub fn all_keys(&self) -> impl Iterator<Item = &str> {
567		let mut a = None;
568		let mut b = None;
569		match self {
570			AssetsRules::List(list) => {
571				a = Some(list.into_iter().map(AsRef::as_ref));
572			},
573			AssetsRules::Map(map) => {
574				let keys = map.keys().into_iter().map(AsRef::as_ref);
575				let values = map.values().into_iter().filter_map(|v| {
576					                                     match v {
577						                                     RuleValue::String(s) => Some(s.as_str()),
578					                                        RuleValue::Boolean(_) => None,
579					                                     }
580				                                     });
581				b = Some(keys.chain(values));
582			},
583		};
584
585		a.into_iter().flatten().chain(b.into_iter().flatten())
586	}
587
588	pub fn env_required_with<'t: 'r, 'r>(&'t self,
589	                                     resolver: &'r EnvResolver)
590	                                     -> impl Iterator<Item = &'t str> + 'r {
591		self.all_keys()
592		    .flat_map(|s| resolver.matches(s))
593		    .map(|m| m.as_str())
594	}
595}
596
597
598#[cfg(test)]
599mod tests {
600	use std::collections::HashMap;
601	use std::collections::HashSet;
602	use std::path::{PathBuf, Path};
603
604	use crate::config::Env;
605	use crate::assets::resolver::Expr;
606	use crate::metadata::format::RuleValue;
607	use crate::metadata::format::AssetsRules;
608	use super::*;
609
610
611	fn crate_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) }
612
613
614	mod abs_if_existing {
615		use std::borrow::Cow;
616
617		use super::*;
618		use super::abs_if_existing;
619
620
621		#[test]
622		fn local() {
623			let roots = [
624			             Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))),
625			             crate_root().into(),
626			];
627			let paths = ["Cargo.toml", "src/lib.rs"];
628
629			for root in roots {
630				for test in paths {
631					let path = Path::new(test);
632
633					let crated = abs_if_existing(path, &root).unwrap();
634					assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})");
635
636					let crated = crated.unwrap();
637					let expected = root.join(path);
638					assert_eq!(expected, crated);
639				}
640			}
641		}
642
643		#[test]
644		fn external_rel() {
645			let roots = [
646			             Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))),
647			             crate_root().into(),
648			];
649			let paths = ["../utils/Cargo.toml", "./../utils/src/lib.rs"];
650
651			for root in roots {
652				for test in paths {
653					let path = Path::new(test);
654
655					let crated = abs_if_existing(path, &root).unwrap();
656					assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})");
657
658					let crated = crated.unwrap();
659					let expected = root.join(path);
660					assert_eq!(expected, crated);
661				}
662			}
663		}
664
665		#[test]
666		fn external_abs() {
667			let roots = [
668			             Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))),
669			             crate_root().into(),
670			];
671			let paths = ["utils", "utils/Cargo.toml"];
672
673			for root in roots {
674				for test in paths {
675					let path = root.parent().unwrap().join(test);
676
677					let crated = abs_if_existing(&path, &root).unwrap();
678					assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})");
679
680					let crated = crated.unwrap();
681					let expected = path.as_path();
682					assert_eq!(expected, crated);
683				}
684			}
685		}
686	}
687
688
689	mod plan {
690		use super::*;
691		use std::env::temp_dir;
692
693
694		fn prepared_tmp(test_name: &str) -> (PathBuf, PathBuf, [&'static str; 4], Env) {
695			let temp = temp_dir().join(env!("CARGO_PKG_NAME"))
696			                     .join(env!("CARGO_PKG_VERSION"))
697			                     .join(test_name);
698
699			let sub = temp.join("dir");
700
701			if !temp.exists() {
702				println!("creating temp dir: {temp:?}")
703			} else {
704				println!("temp dir: {temp:?}")
705			}
706			std::fs::create_dir_all(&temp).unwrap();
707			std::fs::create_dir_all(&sub).unwrap();
708
709			// add temp files
710			let files = ["foo.txt", "bar.txt", "dir/baz.txt", "dir/boo.txt"];
711			for name in files {
712				std::fs::write(temp.join(name), []).unwrap();
713			}
714
715			let env = {
716				let mut env = Env::try_default().unwrap();
717				env.vars.insert("TMP".into(), temp.to_string_lossy().into_owned());
718				env.vars.insert("SUB".into(), sub.to_string_lossy().into_owned());
719				env
720			};
721
722			(temp, sub, files, env)
723		}
724
725
726		mod list {
727			use super::*;
728
729
730			mod as_is {
731				use super::*;
732
733
734				#[test]
735				fn local_exact() {
736					let env = Env::try_default().unwrap();
737					let opts = AssetsOptions::default();
738
739					let root = crate_root();
740					let root = Some(Cow::Borrowed(root.as_path()));
741
742					let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect();
743
744					let exprs = tests.iter().map(|s| s.to_string()).collect();
745					let assets = AssetsRules::List(exprs);
746
747					let plan = build_plan(&env, &assets, &opts, root).unwrap();
748
749					for pair in plan.as_inner() {
750						assert!(matches!(
751							pair,
752							Mapping::AsIs(_, (Expr::Original(left), Expr::Original(right)))
753							if right == "true" && tests.contains(left.as_str())
754						));
755					}
756				}
757
758
759				#[test]
760				fn resolve_local_abs() {
761					let env = {
762						let mut env = Env::try_default().unwrap();
763						env.vars.insert(
764						                "SRC_ABS".into(),
765						                concat!(env!("CARGO_MANIFEST_DIR"), "/src").into(),
766						);
767						env
768					};
769
770					let opts = AssetsOptions::default();
771
772					let root = crate_root();
773					let root = Some(root.as_path().into());
774
775					let tests: HashMap<_, _> = {
776						let man_abs = PathBuf::from("Cargo.toml").canonicalize()
777						                                         .unwrap()
778						                                         .to_string_lossy()
779						                                         .to_string();
780						let lib_abs = PathBuf::from("src/lib.rs").canonicalize()
781						                                         .unwrap()
782						                                         .to_string_lossy()
783						                                         .to_string();
784						vec![
785						     ("${CARGO_MANIFEST_DIR}/Cargo.toml", man_abs),
786						     ("${SRC_ABS}/lib.rs", lib_abs),
787						].into_iter()
788						.collect()
789					};
790
791					let exprs = tests.keys().map(|s| s.to_string()).collect();
792					let assets = AssetsRules::List(exprs);
793
794					let plan = build_plan(&env, &assets, &opts, root).unwrap();
795
796					for pair in plan.as_inner() {
797						assert!(matches!(
798							pair,
799							Mapping::AsIs(matched, (Expr::Modified{original, actual}, Expr::Original(right)))
800							if right == "true"
801							&& tests[original.as_str()] == actual.as_ref()
802							&& matched.source() == Path::new(&tests[original.as_str()]).canonicalize().unwrap()
803						));
804					}
805				}
806
807
808				#[test]
809				fn resolve_local() {
810					let env = {
811						let mut env = Env::try_default().unwrap();
812						env.vars.insert("SRC".into(), "src".into());
813						env
814					};
815
816					let opts = AssetsOptions::default();
817
818					let root = crate_root();
819					let root = Some(root.as_path().into());
820
821					let tests: HashMap<_, _> = { vec![("${SRC}/lib.rs", "src/lib.rs"),].into_iter().collect() };
822
823					let exprs = tests.keys().map(|s| s.to_string()).collect();
824					let assets = AssetsRules::List(exprs);
825
826					let plan = build_plan(&env, &assets, &opts, root).unwrap();
827
828					for pair in plan.as_inner() {
829						if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) =
830							pair
831						{
832							assert_eq!("true", right);
833							assert_eq!(tests[original.as_str()], actual.as_ref());
834							assert_eq!(
835							           matched.source().canonicalize().unwrap(),
836							           Path::new(&tests[original.as_str()]).canonicalize().unwrap()
837							);
838							assert_eq!(matched.target(), Path::new(&tests[original.as_str()]));
839						} else {
840							panic!("pair is not matching: {pair:#?}");
841						}
842					}
843				}
844
845
846				#[test]
847				#[cfg_attr(windows, should_panic)]
848				fn resolve_exact_external_abs() {
849					let (temp, sub, _files, env) = prepared_tmp("as_is-resolve_external");
850
851					let opts = AssetsOptions::default();
852
853					let root = crate_root();
854					let root = Some(root.as_path().into());
855
856
857					// tests:
858
859					let tests: HashMap<_, _> = {
860						vec![
861						     ("${TMP}/foo.txt", (temp.join("foo.txt"), "foo.txt")),
862						     ("${TMP}/bar.txt", (temp.join("bar.txt"), "bar.txt")),
863						     ("${SUB}/baz.txt", (sub.join("baz.txt"), "baz.txt")),
864						     ("${TMP}/dir/boo.txt", (sub.join("boo.txt"), "boo.txt")),
865						].into_iter()
866						.collect()
867					};
868
869					let exprs = tests.keys().map(|s| s.to_string()).collect();
870					let assets = AssetsRules::List(exprs);
871
872					let plan = build_plan(&env, &assets, &opts, root).unwrap();
873
874					// check targets len
875					{
876						let targets = plan.targets().collect::<Vec<_>>();
877						let expected = tests.values().map(|(_, name)| name).collect::<Vec<_>>();
878						assert_eq!(expected.len(), targets.len());
879					}
880
881					// full check
882					for pair in plan.as_inner() {
883						if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) =
884							pair
885						{
886							assert_eq!("true", right);
887							assert_eq!(tests[original.as_str()].0.to_string_lossy(), actual.as_ref());
888							assert_eq!(matched.source(), tests[original.as_str()].0);
889							assert_eq!(matched.target().to_string_lossy(), tests[original.as_str()].1);
890						} else {
891							panic!("pair is not matching: {pair:#?}");
892						}
893					}
894				}
895
896
897				#[test]
898				#[cfg_attr(windows, should_panic)]
899				fn resolve_glob_external_many() {
900					let (_, _, files, env) = prepared_tmp("as_is-resolve_external_many");
901
902					let opts = AssetsOptions::default();
903
904					let root = crate_root();
905					let root = Some(root.as_path().into());
906
907					let exprs = ["${TMP}/*.txt", "${SUB}/*.txt"];
908
909					let assets = AssetsRules::List(exprs.iter().map(|s| s.to_string()).collect());
910
911					let plan = build_plan(&env, &assets, &opts, root).unwrap();
912
913					// check targets len
914					{
915						let targets = plan.targets().collect::<Vec<_>>();
916						assert_eq!(files.len(), targets.len());
917					}
918
919					// full check
920					for pair in plan.as_inner() {
921						if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) =
922							pair
923						{
924							assert!(exprs.contains(&original.as_str()));
925							assert!(Path::new(actual.as_ref()).is_absolute());
926							assert_eq!("true", right);
927
928							if let Match::Pair { source, target } = matched {
929								// target is just filename:
930								assert_eq!(1, target.components().count());
931								assert_eq!(target.file_name(), source.file_name());
932							} else {
933								panic!("pair.matched is not matching: {matched:#?}");
934							}
935						} else {
936							panic!("pair is not matching: {pair:#?}");
937						}
938					}
939				}
940			}
941		}
942
943
944		mod map {
945			use super::*;
946
947
948			mod as_is {
949				use super::*;
950
951
952				#[test]
953				fn local_exact() {
954					let env = Env::try_default().unwrap();
955					let opts = AssetsOptions::default();
956
957					let root = crate_root();
958					let root = Some(root.as_path().into());
959
960					let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect();
961
962					let exprs = tests.iter()
963					                 .map(|s| (s.to_string(), RuleValue::Boolean(true)))
964					                 .collect();
965
966					let assets = AssetsRules::Map(exprs);
967
968					let plan = build_plan(&env, &assets, &opts, root).unwrap();
969
970					for pair in plan.as_inner() {
971						if let Mapping::AsIs(matched, (Expr::Original(left), Expr::Original(right))) = pair {
972							assert_eq!("true", right);
973							assert!(tests.contains(left.as_str()));
974							assert_eq!(
975							           left.as_str(),
976							           sanitize_path_pattern(matched.target().to_string_lossy().as_ref())
977							);
978						} else {
979							panic!("pair is not matching: {pair:#?}");
980						}
981					}
982				}
983
984
985				#[test]
986				fn local_exact_target() {
987					let env = Env::try_default().unwrap();
988					let opts = AssetsOptions::default();
989
990					let root = crate_root();
991					let root = Some(root.as_path().into());
992
993					// left hand of rule:
994					let targets = ["trg", "/trg", "//trg"];
995					// right hand of rule:
996					let tests = vec!["Cargo.toml", "src/lib.rs"];
997					// latest because there is no to files into one target, so "into" will be used
998
999					for trg in targets {
1000						let stripped_trg = &trg.replace('/', "").trim().to_owned();
1001
1002						let exprs = tests.iter()
1003						                 .map(|s| (trg.to_string(), RuleValue::String(s.to_string())))
1004						                 .collect();
1005
1006						let assets = AssetsRules::Map(exprs);
1007
1008						let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1009
1010						for pair in plan.as_inner() {
1011							if let Mapping::AsIs(
1012							                     Match::Pair { source, target },
1013							                     (Expr::Original(left), Expr::Original(right)),
1014							) = pair
1015							{
1016								assert_eq!(left, stripped_trg);
1017								assert!(tests.contains(&right.as_str()));
1018								assert_eq!(source, Path::new(right));
1019								assert_eq!(target, Path::new(stripped_trg));
1020							} else {
1021								panic!("pair is not matching: {pair:#?}");
1022							}
1023						}
1024					}
1025				}
1026
1027				#[test]
1028				fn local_exact_exclude() {
1029					let env = Env::try_default().unwrap();
1030					let opts = AssetsOptions::default();
1031
1032					let root = crate_root();
1033					let root = Some(root.as_path().into());
1034
1035					// left hand of rule:
1036					let tests = vec!["Cargo.toml", "src/lib.rs"];
1037
1038					let exprs = tests.iter()
1039					                 .map(|s| (*s, RuleValue::Boolean(true)))
1040					                 .chain([("*.toml", RuleValue::Boolean(false))])
1041					                 .collect();
1042
1043					let assets = AssetsRules::Map(exprs);
1044
1045					let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1046
1047					let pairs = plan.as_inner();
1048					assert_eq!(1, pairs.len());
1049
1050					let pair = pairs.first().unwrap();
1051
1052					if let Mapping::AsIs(_, (Expr::Original(left), _)) = pair {
1053						assert_eq!(tests[1], left);
1054					} else {
1055						panic!("pair is not matching: {pair:#?}");
1056					}
1057				}
1058			}
1059
1060
1061			mod one_into {
1062				use super::*;
1063
1064
1065				#[test]
1066				fn local_exact_target() {
1067					let env = Env::try_default().unwrap();
1068					let opts = AssetsOptions::default();
1069
1070					let root = crate_root();
1071					let root = Some(root.as_path().into());
1072
1073					// left hand of rule:
1074					let targets = ["trg/", "trg//", "/trg/", "//trg/"];
1075					let targets_rel = ["trg/", "trg//"]; // non-abs targets
1076					// right hand of rule:
1077					let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect();
1078
1079					for trg in targets {
1080						let exprs = tests.iter()
1081						                 .map(|s| (trg.to_string(), RuleValue::String(s.to_string())))
1082						                 .collect();
1083
1084						let assets = AssetsRules::Map(exprs);
1085
1086						let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1087
1088						for pair in plan.as_inner() {
1089							if let Mapping::Into(
1090							                     Match::Pair { source, target },
1091							                     (Expr::Original(left), Expr::Original(right)),
1092							) = pair
1093							{
1094								assert_eq!(left, target.to_string_lossy().as_ref());
1095								assert!(targets_rel.contains(&left.as_str()));
1096								assert!(tests.contains(right.as_str()));
1097								assert_eq!(source, Path::new(right));
1098							} else {
1099								panic!("pair is not matching: {pair:#?}");
1100							}
1101						}
1102					}
1103				}
1104			}
1105
1106
1107			mod many_into {
1108				use super::*;
1109
1110				#[test]
1111				#[cfg_attr(windows, should_panic)]
1112				fn glob_local_target() {
1113					let env = Env::try_default().unwrap();
1114					let opts = AssetsOptions::default();
1115
1116					let root = crate_root();
1117					let root = Some(root.as_path().into());
1118
1119					// left hand of rule:
1120					let targets = ["/trg/", "//trg/", "/trg", "trg"];
1121					let targets_rel = ["trg/", "trg"]; // non-abs targets
1122					// right hand of rule:
1123					let tests = vec!["Cargo.tom*", "src/lib.*"];
1124					// latest because there is no to files into one target, so "into" will be used
1125
1126					for trg in targets {
1127						let exprs = tests.iter()
1128						                 .map(|s| (trg.to_string(), RuleValue::String(s.to_string())))
1129						                 .collect();
1130
1131						let assets = AssetsRules::Map(exprs);
1132
1133						let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1134
1135						for pair in plan.as_inner() {
1136							if let Mapping::ManyInto { sources,
1137							                           target,
1138							                           #[cfg(feature = "assets-report")]
1139							                           excluded,
1140							                           exprs: (Expr::Original(left), Expr::Original(right)), } = pair
1141							{
1142								assert!(targets_rel.contains(&target.to_string_lossy().as_ref()));
1143								assert_eq!(&target.to_string_lossy(), left);
1144
1145								assert_eq!(1, sources.len());
1146								assert!(tests.contains(&right.as_str()));
1147
1148								#[cfg(feature = "assets-report")]
1149								assert_eq!(0, excluded.len());
1150							} else {
1151								panic!("pair is not matching: {pair:#?}");
1152							}
1153						}
1154					}
1155				}
1156
1157
1158				#[test]
1159				#[cfg_attr(windows, should_panic)]
1160				fn glob_local_exclude() {
1161					let env = Env::try_default().unwrap();
1162					let opts = AssetsOptions::default();
1163
1164					let root = crate_root();
1165					let root = Some(root.as_path().into());
1166
1167					// exclude:
1168					let exclude = "src/*";
1169					// left hand of rule:
1170					let trg = "/trg";
1171					// right hand of rule:
1172					let tests: HashMap<_, _> = vec![("Cargo.tom*", 1), ("src/lib.*", 0)].into_iter().collect();
1173					// left + right:
1174					let exprs = tests.iter()
1175					                 .enumerate()
1176					                 .map(|(i, (s, _))| (trg.to_string() + &"/".repeat(i), RuleValue::String(s.to_string())))
1177					                 .chain([(exclude.to_string(), RuleValue::Boolean(false))])
1178					                 .collect();
1179
1180					let assets = AssetsRules::Map(exprs);
1181					let plan = build_plan(&env, &assets, &opts, root.clone()).unwrap();
1182
1183					let pairs = plan.as_inner();
1184					assert_eq!(2, pairs.len());
1185
1186					for pair in pairs.iter() {
1187						if let Mapping::ManyInto { sources,
1188						                           target,
1189						                           exprs: (Expr::Original(left), Expr::Original(right)),
1190						                           .. } = pair
1191						{
1192							assert_eq!(&target.to_string_lossy(), left);
1193
1194							let sources_expected = tests[right.as_str()];
1195							assert_eq!(sources_expected, sources.len());
1196						} else {
1197							panic!("pair is not matching: {pair:#?}");
1198						}
1199					}
1200				}
1201			}
1202		}
1203	}
1204}