rattler_build/source/
mod.rs

1//! Module for fetching sources and applying patches
2
3use 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
99/// Fetches all sources in a list of sources and applies specified patches
100pub 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    // Figure out the directories we need
112    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                // Create folder if it doesn't exist
168                if !dest_dir.exists() {
169                    fs::create_dir_all(&dest_dir)?;
170                }
171
172                // Copy source code to work dir
173                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                // Create folder if it doesn't exist
216                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                // check if the source path is a directory
225                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    /// Fetches the sources for the given output and returns a new output with the finalized sources attached
284    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}