1use std::{
4 ffi::OsStr,
5 path::{PathBuf, StripPrefixError},
6};
7
8use crate::{
9 metadata::{Directories, Output},
10 recipe::parser::{GitRev, GitSource, Source},
11 source::{
12 checksum::Checksum,
13 extract::{extract_tar, extract_zip, is_tarball},
14 },
15 system_tools::ToolError,
16 tool_configuration,
17};
18
19use fs_err as fs;
20
21use crate::system_tools::SystemTools;
22pub mod checksum;
23pub mod copy_dir;
24pub mod extract;
25pub mod git_source;
26pub mod patch;
27pub mod url_source;
28
29#[allow(missing_docs)]
30#[derive(Debug, thiserror::Error)]
31pub enum SourceError {
32 #[error("IO Error: {0}")]
33 Io(#[from] std::io::Error),
34
35 #[error("Failed to download source from url: {0}")]
36 Url(#[from] reqwest::Error),
37
38 #[error("Url does not point to a file: {0}")]
39 UrlNotFile(url::Url),
40
41 #[error("WalkDir Error: {0}")]
42 WalkDir(#[from] walkdir::Error),
43
44 #[error("FileSystem error: '{0}'")]
45 FileSystemError(std::io::Error),
46
47 #[error("StripPrefixError Error: {0}")]
48 StripPrefixError(#[from] StripPrefixError),
49
50 #[error("Download could not be validated with checksum!")]
51 ValidationFailed,
52
53 #[error("File not found: {0}")]
54 FileNotFound(PathBuf),
55
56 #[error("Could not find `patch` executable")]
57 PatchExeNotFound,
58
59 #[error("Patch file not found: {0}")]
60 PatchNotFound(PathBuf),
61
62 #[error("Failed to apply patch: {0}")]
63 PatchFailed(String),
64
65 #[error("Failed to extract archive: {0}")]
66 TarExtractionError(String),
67
68 #[error("Failed to extract zip archive: {0}")]
69 ZipExtractionError(String),
70
71 #[error("Failed to read from zip: {0}")]
72 InvalidZip(String),
73
74 #[error("Failed to run git command: {0}")]
75 GitError(String),
76
77 #[error("Failed to run git command: {0}")]
78 GitErrorStr(&'static str),
79
80 #[error("{0}")]
81 UnknownError(String),
82
83 #[error("{0}")]
84 UnknownErrorStr(&'static str),
85
86 #[error("Could not walk dir")]
87 IgnoreError(#[from] ignore::Error),
88
89 #[error("Failed to parse glob pattern")]
90 Glob(#[from] globset::Error),
91
92 #[error("No checksum found for url: {0}")]
93 NoChecksum(String),
94
95 #[error("Failed to find git executable: {0}")]
96 GitNotFound(#[from] ToolError),
97}
98
99pub async fn fetch_sources(
101 sources: &[Source],
102 directories: &Directories,
103 system_tools: &SystemTools,
104 tool_configuration: &tool_configuration::Configuration,
105) -> Result<Vec<Source>, SourceError> {
106 if sources.is_empty() {
107 tracing::info!("No sources to fetch");
108 return Ok(Vec::new());
109 }
110
111 let work_dir = &directories.work_dir;
113 let recipe_dir = &directories.recipe_dir;
114 let cache_src = directories.output_dir.join("src_cache");
115 fs::create_dir_all(&cache_src)?;
116
117 let mut rendered_sources = Vec::new();
118
119 for src in sources {
120 match &src {
121 Source::Git(src) => {
122 tracing::info!("Fetching source from git repo: {}", src.url());
123 let result = git_source::git_src(system_tools, src, &cache_src, recipe_dir)?;
124 let dest_dir = if let Some(target_directory) = src.target_directory() {
125 work_dir.join(target_directory)
126 } else {
127 work_dir.to_path_buf()
128 };
129
130 rendered_sources.push(Source::Git(GitSource {
131 rev: GitRev::Commit(result.1),
132 ..src.clone()
133 }));
134
135 let copy_result = tool_configuration.fancy_log_handler.wrap_in_progress(
136 "copying source into isolated environment",
137 || {
138 copy_dir::CopyDir::new(&result.0, &dest_dir)
139 .use_gitignore(false)
140 .run()
141 },
142 )?;
143 tracing::info!(
144 "Copied {} files into isolated environment",
145 copy_result.copied_paths().len()
146 );
147
148 if !src.patches().is_empty() {
149 patch::apply_patches(system_tools, src.patches(), &dest_dir, recipe_dir)?;
150 }
151 }
152 Source::Url(src) => {
153 let first_url = src.urls().first().expect("we should have at least one URL");
154 let file_name_from_url = first_url
155 .path_segments()
156 .and_then(|segments| segments.last().map(|last| last.to_string()))
157 .ok_or_else(|| SourceError::UrlNotFile(first_url.clone()))?;
158
159 let res = url_source::url_src(src, &cache_src, tool_configuration).await?;
160
161 let dest_dir = if let Some(target_directory) = src.target_directory() {
162 work_dir.join(target_directory)
163 } else {
164 work_dir.to_path_buf()
165 };
166
167 if !dest_dir.exists() {
169 fs::create_dir_all(&dest_dir)?;
170 }
171
172 if res.is_dir() {
174 tracing::info!(
175 "Copying source from url: {} to {}",
176 res.display(),
177 dest_dir.display()
178 );
179 tool_configuration.fancy_log_handler.wrap_in_progress(
180 "copying source into isolated environment",
181 || {
182 copy_dir::CopyDir::new(&res, &dest_dir)
183 .use_gitignore(false)
184 .run()
185 },
186 )?;
187 } else {
188 tracing::info!(
189 "Copying source from url: {} to {}",
190 res.display(),
191 dest_dir.display()
192 );
193
194 let file_name = src.file_name().unwrap_or(&file_name_from_url);
195 let target = dest_dir.join(file_name);
196 fs::copy(&res, &target)?;
197 }
198
199 if !src.patches().is_empty() {
200 patch::apply_patches(system_tools, src.patches(), &dest_dir, recipe_dir)?;
201 }
202
203 rendered_sources.push(Source::Url(src.clone()));
204 }
205 Source::Path(src) => {
206 let src_path = recipe_dir.join(src.path()).canonicalize()?;
207 tracing::info!("Fetching source from path: {}", src_path.display());
208
209 let dest_dir = if let Some(target_directory) = src.target_directory() {
210 work_dir.join(target_directory)
211 } else {
212 work_dir.to_path_buf()
213 };
214
215 if !dest_dir.exists() {
217 fs::create_dir_all(&dest_dir)?;
218 }
219
220 if !src_path.exists() {
221 return Err(SourceError::FileNotFound(src_path));
222 }
223
224 if src_path.is_dir() {
226 let copy_result = tool_configuration.fancy_log_handler.wrap_in_progress(
227 "copying source into isolated environment",
228 || {
229 copy_dir::CopyDir::new(&src_path, &dest_dir)
230 .use_gitignore(src.use_gitignore())
231 .run()
232 },
233 )?;
234 tracing::info!(
235 "Copied {} files into isolated environment",
236 copy_result.copied_paths().len()
237 );
238 } else if is_tarball(
239 src_path
240 .file_name()
241 .unwrap_or_default()
242 .to_string_lossy()
243 .as_ref(),
244 ) {
245 extract_tar(&src_path, &dest_dir, &tool_configuration.fancy_log_handler)?;
246 tracing::info!("Extracted to {}", dest_dir.display());
247 } else if src_path.extension() == Some(OsStr::new("zip")) {
248 extract_zip(&src_path, &dest_dir, &tool_configuration.fancy_log_handler)?;
249 tracing::info!("Extracted zip to {}", dest_dir.display());
250 } else if let Some(file_name) = src
251 .file_name()
252 .cloned()
253 .or_else(|| src_path.file_name().map(PathBuf::from))
254 {
255 let dest = dest_dir.join(&file_name);
256 tracing::info!(
257 "Copying source from path: {} to {}",
258 src_path.display(),
259 dest.display()
260 );
261 if let Some(checksum) = Checksum::from_path_source(src) {
262 if !checksum.validate(&src_path) {
263 return Err(SourceError::ValidationFailed);
264 }
265 }
266 fs::copy(&src_path, dest)?;
267 } else {
268 return Err(SourceError::FileNotFound(src_path));
269 }
270
271 if !src.patches().is_empty() {
272 patch::apply_patches(system_tools, src.patches(), &dest_dir, recipe_dir)?;
273 }
274
275 rendered_sources.push(Source::Path(src.clone()));
276 }
277 }
278 }
279 Ok(rendered_sources)
280}
281
282impl Output {
283 pub async fn fetch_sources(
285 self,
286 tool_configuration: &tool_configuration::Configuration,
287 ) -> Result<Self, SourceError> {
288 let span = tracing::info_span!("Fetching source code");
289 let _enter = span.enter();
290
291 if let Some(finalized_sources) = &self.finalized_sources {
292 fetch_sources(
293 finalized_sources,
294 &self.build_configuration.directories,
295 &self.system_tools,
296 tool_configuration,
297 )
298 .await?;
299
300 Ok(self)
301 } else {
302 let rendered_sources = fetch_sources(
303 self.recipe.sources(),
304 &self.build_configuration.directories,
305 &self.system_tools,
306 tool_configuration,
307 )
308 .await?;
309
310 Ok(Output {
311 finalized_sources: Some(rendered_sources),
312 ..self
313 })
314 }
315 }
316}