playdate_build/assets/
mod.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::io::{Error as IoError, ErrorKind as IoErrorKind};
4
5use wax::{LinkBehavior, WalkError};
6use fs_extra::error::Error as FsExtraError;
7
8use crate::fs::soft_link_checked;
9use crate::metadata::format::AssetsBuildMethod;
10use crate::metadata::format::AssetsOptions;
11
12
13pub mod plan;
14pub mod resolver;
15use self::plan::*;
16
17
18pub fn apply_build_plan<'l, 'r, P: AsRef<Path>>(plan: BuildPlan<'l, 'r>,
19                                                target_root: P,
20                                                assets_options: &AssetsOptions)
21                                                -> Result<BuildReport<'l, 'r>, FsExtraError> {
22	use crate::fs::parent_of;
23	use crate::fs::ensure_dir_exists;
24
25	let target_root = target_root.as_ref();
26	let build_method = assets_options.method();
27	let overwrite = assets_options.overwrite();
28	info!("collecting assets:");
29	debug!("assets build method: {build_method:?}, overwrite: {overwrite}");
30
31
32	let def_options = fs_extra::dir::CopyOptions { overwrite,
33	                                               skip_exist: true,
34	                                               copy_inside: false,
35	                                               content_only: false,
36	                                               ..Default::default() };
37
38	// ensures that there's no symlink or any `..` component in path, just resolve and compare with root:
39	let ensure_out_of_root = |path: &Path| -> std::io::Result<()> {
40		if path.is_symlink() {
41			let real = path.read_link()
42			               .map_or_else(|err| format!("{err}"), |p| p.display().to_string());
43			return Err(IoError::new(
44				IoErrorKind::AlreadyExists,
45				format!("Scary to overwrite symlink ({real})"),
46			));
47		}
48
49		let real = path.canonicalize()?;
50		if !real.starts_with(target_root) {
51			return Err(IoError::new(
52				IoErrorKind::AlreadyExists,
53				format!("Target points out from target directory: {}", real.display()),
54			));
55		}
56
57		Ok(())
58	};
59
60
61	let copy_method = |source: &Path, target: &Path, to_inside| -> Result<OpRes, FsExtraError> {
62		let into = target_root.join(target);
63		let copied = if source.is_dir() {
64			ensure_dir_exists(&into, target_root)?;
65			ensure_out_of_root(&into)?;
66			let options = fs_extra::dir::CopyOptions { copy_inside: to_inside,
67			                                           ..def_options };
68			fs_extra::dir::copy(source, into, &options).map(OpRes::Write)?
69		} else if to_inside {
70			ensure_dir_exists(&into, target_root)?;
71			ensure_out_of_root(&into)?;
72			let filename = source.file_name().ok_or_else(|| {
73				                                  IoError::new(
74				                                               IoErrorKind::InvalidFilename,
75				                                               format!("Filename not found for {}", into.display()),
76				)
77			                                  })?;
78			let into = into.join(filename);
79			ensure_out_of_root(&into)?;
80			std::fs::copy(source, into).map(OpRes::Write)?
81		} else {
82			let into_parent = parent_of(&into)?;
83			ensure_dir_exists(into_parent, target_root)?;
84			ensure_out_of_root(into_parent)?;
85
86			if !into.try_exists()? || overwrite {
87				std::fs::copy(source, into).map(OpRes::Write)?
88			} else {
89				OpRes::Skip
90			}
91		};
92		info!("  {copied:?} copy: {} <- {}", target.display(), source.display());
93		Ok(copied)
94	};
95
96	let link_method = |source: &Path, target: &Path, to_inside| -> Result<OpRes, FsExtraError> {
97		let into = target_root.join(target);
98		let linked = if to_inside {
99			             ensure_dir_exists(&into, target_root)?;
100			             let filename =
101				             source.file_name().ok_or_else(|| {
102					                                let msg = format!("Filename not found for {}", into.display());
103					                                IoError::new(IoErrorKind::InvalidFilename, msg)
104				                                })?;
105			             let into = into.join(filename);
106			             soft_link_checked(source, into, overwrite, target_root)
107		             } else {
108			             let into_parent = parent_of(&into)?;
109			             ensure_dir_exists(into_parent, target_root)?;
110			             soft_link_checked(source, &into, overwrite, target_root)
111		             }.map(|was| if was { OpRes::Link } else { OpRes::Skip })?;
112		info!("  {linked:?} link: {} <- {}", target.display(), source.display());
113		Ok(linked)
114	};
115
116	let method: &dyn Fn(&Path, &Path, bool) -> Result<OpRes, FsExtraError> = match build_method {
117		AssetsBuildMethod::Copy => &copy_method,
118		AssetsBuildMethod::Link => &link_method,
119	};
120
121	let (mut plan, crate_root) = plan.into_parts();
122	let mut results = HashMap::with_capacity(plan.len());
123	for entry in plan.drain(..) {
124		let current: Vec<_> = match &entry {
125			Mapping::AsIs(inc, ..) => {
126				let source = abs_if_existing_any(inc.source(), &crate_root);
127				vec![method(&source, &inc.target(), false)]
128			},
129			Mapping::Into(inc, ..) => {
130				let source = abs_if_existing_any(inc.source(), &crate_root);
131				vec![method(&source, &inc.target(), true)]
132			},
133			Mapping::ManyInto { sources, target, .. } => {
134				sources.iter()
135				       .map(|inc| (abs_if_existing_any(inc.source(), &crate_root), target.join(inc.target())))
136				       .map(|(ref source, ref target)| method(source, target, false))
137				       .collect()
138			},
139		};
140
141		results.insert(entry, current);
142	}
143
144	Ok(BuildReport { results })
145}
146
147
148#[derive(Debug)]
149pub struct BuildReport<'left, 'right>
150	where Self: 'left + 'right {
151	pub results: HashMap<Mapping<'left, 'right>, Vec<Result<OpRes, FsExtraError>>>,
152}
153
154impl BuildReport<'_, '_> {
155	pub fn has_errors(&self) -> bool {
156		self.results
157		    .iter()
158		    .flat_map(|(_, results)| results.iter())
159		    .any(|result| result.is_err())
160	}
161}
162
163
164#[derive(Debug)]
165pub enum OpRes {
166	Write(u64),
167	Link,
168	Skip,
169}
170
171
172impl AssetsOptions {
173	fn link_behavior(&self) -> LinkBehavior {
174		if self.follow_symlinks() {
175			LinkBehavior::ReadTarget
176		} else {
177			LinkBehavior::ReadFile
178		}
179	}
180}
181
182
183fn log_err<Err: std::fmt::Display>(err: Err) -> Err {
184	error!("[package.metadata.playdate.assets]: {err}");
185	err
186}
187
188
189#[derive(Debug)]
190pub enum Error {
191	Io(std::io::Error),
192	Wax(wax::BuildError),
193	Walk(WalkError),
194	Error(String),
195}
196
197
198impl From<std::io::Error> for Error {
199	fn from(err: std::io::Error) -> Self { Self::Io(err) }
200}
201impl From<wax::BuildError> for Error {
202	fn from(err: wax::BuildError) -> Self { Self::Wax(err) }
203}
204impl From<WalkError> for Error {
205	fn from(err: WalkError) -> Self { Self::Walk(err) }
206}
207impl From<String> for Error {
208	fn from(value: String) -> Self { Self::Error(value) }
209}
210
211impl std::fmt::Display for Error {
212	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213		match self {
214			Error::Io(err) => err.fmt(f),
215			Error::Wax(err) => err.fmt(f),
216			Error::Walk(err) => err.fmt(f),
217			Error::Error(err) => err.fmt(f),
218		}
219	}
220}
221
222impl std::error::Error for Error {
223	fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
224		match self {
225			Error::Io(err) => Some(err),
226			Error::Wax(err) => Some(err),
227			Error::Walk(err) => Some(err),
228			Error::Error(_) => None,
229		}
230	}
231}