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 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 => ©_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}