lux_lib/operations/
fetch.rs

1use bon::Builder;
2use git2::build::RepoBuilder;
3use git2::FetchOptions;
4use git_url_parse::GitUrlParseError;
5use ssri::Integrity;
6use std::fs::File;
7use std::io;
8use std::io::Cursor;
9use std::io::Read;
10use std::path::Path;
11use thiserror::Error;
12
13use crate::build::utils::recursive_copy_dir;
14use crate::config::Config;
15use crate::git::GitSource;
16use crate::hash::HasIntegrity;
17use crate::lockfile::RemotePackageSourceUrl;
18use crate::lua_rockspec::RockSourceSpec;
19use crate::operations;
20use crate::package::PackageSpec;
21use crate::progress::Progress;
22use crate::progress::ProgressBar;
23use crate::rockspec::Rockspec;
24
25use super::DownloadSrcRockError;
26use super::UnpackError;
27
28/// A rocks package source fetcher, providing fine-grained control
29/// over how a package should be fetched.
30#[derive(Builder)]
31#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
32pub struct FetchSrc<'a, R: Rockspec> {
33    #[builder(start_fn)]
34    dest_dir: &'a Path,
35    #[builder(start_fn)]
36    rockspec: &'a R,
37    #[builder(start_fn)]
38    config: &'a Config,
39    #[builder(start_fn)]
40    progress: &'a Progress<ProgressBar>,
41    source_url: Option<RemotePackageSourceUrl>,
42}
43
44#[derive(Debug)]
45pub(crate) struct RemotePackageSourceMetadata {
46    pub hash: Integrity,
47    pub source_url: RemotePackageSourceUrl,
48}
49
50impl<R: Rockspec, State> FetchSrcBuilder<'_, R, State>
51where
52    State: fetch_src_builder::State + fetch_src_builder::IsComplete,
53{
54    /// Fetch and unpack the source into the `dest_dir`.
55    pub async fn fetch(self) -> Result<(), FetchSrcError> {
56        self.fetch_internal().await?;
57        Ok(())
58    }
59
60    /// Fetch and unpack the source into the `dest_dir`,
61    /// returning the source `Integrity`.
62    pub(crate) async fn fetch_internal(self) -> Result<RemotePackageSourceMetadata, FetchSrcError> {
63        let fetch = self._build();
64        match do_fetch_src(&fetch).await {
65            Err(err) => match &fetch.rockspec.source().current_platform().source_spec {
66                RockSourceSpec::Git(_) | RockSourceSpec::Url(_) => {
67                    let package = PackageSpec::new(
68                        fetch.rockspec.package().clone(),
69                        fetch.rockspec.version().clone(),
70                    );
71                    fetch.progress.map(|p| {
72                        p.println(format!(
73                            "⚠️ WARNING: Failed to fetch source for {}: {}",
74                            &package, err
75                        ))
76                    });
77                    fetch
78                        .progress
79                        .map(|p| p.println("⚠️ Falling back to .src.rock archive"));
80                    let metadata =
81                        FetchSrcRock::new(&package, fetch.dest_dir, fetch.config, fetch.progress)
82                            .fetch()
83                            .await?;
84                    Ok(metadata)
85                }
86                RockSourceSpec::File(_) => Err(err),
87            },
88            Ok(metadata) => Ok(metadata),
89        }
90    }
91}
92
93#[derive(Error, Debug)]
94pub enum FetchSrcError {
95    #[error("failed to clone rock source: {0}")]
96    GitClone(#[from] git2::Error),
97    #[error("failed to parse git URL: {0}")]
98    GitUrlParse(#[from] GitUrlParseError),
99    #[error(transparent)]
100    Io(#[from] io::Error),
101    #[error(transparent)]
102    Request(#[from] reqwest::Error),
103    #[error(transparent)]
104    Unpack(#[from] UnpackError),
105    #[error(transparent)]
106    FetchSrcRock(#[from] FetchSrcRockError),
107}
108
109/// A rocks package source fetcher, providing fine-grained control
110/// over how a package should be fetched.
111#[derive(Builder)]
112#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
113struct FetchSrcRock<'a> {
114    #[builder(start_fn)]
115    package: &'a PackageSpec,
116    #[builder(start_fn)]
117    dest_dir: &'a Path,
118    #[builder(start_fn)]
119    config: &'a Config,
120    #[builder(start_fn)]
121    progress: &'a Progress<ProgressBar>,
122}
123
124impl<State> FetchSrcRockBuilder<'_, State>
125where
126    State: fetch_src_rock_builder::State + fetch_src_rock_builder::IsComplete,
127{
128    pub async fn fetch(self) -> Result<RemotePackageSourceMetadata, FetchSrcRockError> {
129        do_fetch_src_rock(self._build()).await
130    }
131}
132
133#[derive(Error, Debug)]
134#[error(transparent)]
135pub enum FetchSrcRockError {
136    DownloadSrcRock(#[from] DownloadSrcRockError),
137    Unpack(#[from] UnpackError),
138    Io(#[from] io::Error),
139}
140
141async fn do_fetch_src<R: Rockspec>(
142    fetch: &FetchSrc<'_, R>,
143) -> Result<RemotePackageSourceMetadata, FetchSrcError> {
144    let rockspec = fetch.rockspec;
145    let rock_source = rockspec.source().current_platform();
146    let progress = fetch.progress;
147    let dest_dir = fetch.dest_dir;
148    // prioritise lockfile source, if present
149    let source_spec = match &fetch.source_url {
150        Some(source_url) => match source_url {
151            RemotePackageSourceUrl::Git { url, checkout_ref } => RockSourceSpec::Git(GitSource {
152                url: url.parse()?,
153                checkout_ref: Some(checkout_ref.clone()),
154            }),
155            RemotePackageSourceUrl::Url { url } => RockSourceSpec::Url(url.clone()),
156            RemotePackageSourceUrl::File { path } => RockSourceSpec::File(path.clone()),
157        },
158        None => rock_source.source_spec.clone(),
159    };
160    let metadata = match &source_spec {
161        RockSourceSpec::Git(git) => {
162            let url = git.url.to_string();
163            progress.map(|p| p.set_message(format!("🦠 Cloning {}", url)));
164
165            let mut fetch_options = FetchOptions::new();
166            fetch_options.update_fetchhead(false);
167            if git.checkout_ref.is_none() {
168                fetch_options.depth(1);
169            };
170            let mut repo_builder = RepoBuilder::new();
171            repo_builder.fetch_options(fetch_options);
172            let repo = repo_builder.clone(&url, dest_dir)?;
173
174            let checkout_ref = match &git.checkout_ref {
175                Some(checkout_ref) => {
176                    let (object, _) = repo.revparse_ext(checkout_ref)?;
177                    repo.checkout_tree(&object, None)?;
178                    checkout_ref.clone()
179                }
180                None => {
181                    let head = repo.head()?;
182                    let commit = head.peel_to_commit()?;
183                    commit.id().to_string()
184                }
185            };
186            // The .git directory is not deterministic
187            std::fs::remove_dir_all(dest_dir.join(".git"))?;
188            let hash = fetch.dest_dir.hash()?;
189            RemotePackageSourceMetadata {
190                hash,
191                source_url: RemotePackageSourceUrl::Git { url, checkout_ref },
192            }
193        }
194        RockSourceSpec::Url(url) => {
195            progress.map(|p| p.set_message(format!("📥 Downloading {}", url.to_owned())));
196
197            let response = reqwest::get(url.to_owned())
198                .await?
199                .error_for_status()?
200                .bytes()
201                .await?;
202            let hash = response.hash()?;
203            let file_name = url
204                .path_segments()
205                .and_then(|mut segments| segments.next_back())
206                .and_then(|name| {
207                    if name.is_empty() {
208                        None
209                    } else {
210                        Some(name.to_string())
211                    }
212                })
213                .unwrap_or(url.to_string());
214            let cursor = Cursor::new(response);
215            let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
216            operations::unpack::unpack(
217                mime_type,
218                cursor,
219                rock_source.unpack_dir.is_none(),
220                file_name,
221                dest_dir,
222                progress,
223            )
224            .await?;
225            RemotePackageSourceMetadata {
226                hash,
227                source_url: RemotePackageSourceUrl::Url { url: url.clone() },
228            }
229        }
230        RockSourceSpec::File(path) => {
231            let hash = if path.is_dir() {
232                progress.map(|p| p.set_message(format!("📋 Copying {}", path.display())));
233                recursive_copy_dir(&path.to_path_buf(), dest_dir).await?;
234                progress.map(|p| p.finish_and_clear());
235                dest_dir.hash()?
236            } else {
237                let mut file = File::open(path)?;
238                let mut buffer = Vec::new();
239                file.read_to_end(&mut buffer)?;
240                let mime_type = infer::get(&buffer).map(|file_type| file_type.mime_type());
241                let file_name = path
242                    .file_name()
243                    .map(|os_str| os_str.to_string_lossy())
244                    .unwrap_or(path.to_string_lossy())
245                    .to_string();
246                operations::unpack::unpack(
247                    mime_type,
248                    file,
249                    rock_source.unpack_dir.is_none(),
250                    file_name,
251                    dest_dir,
252                    progress,
253                )
254                .await?;
255                path.hash()?
256            };
257            RemotePackageSourceMetadata {
258                hash,
259                source_url: RemotePackageSourceUrl::File { path: path.clone() },
260            }
261        }
262    };
263    Ok(metadata)
264}
265
266async fn do_fetch_src_rock(
267    fetch: FetchSrcRock<'_>,
268) -> Result<RemotePackageSourceMetadata, FetchSrcRockError> {
269    let package = fetch.package;
270    let dest_dir = fetch.dest_dir;
271    let config = fetch.config;
272    let progress = fetch.progress;
273    let src_rock = operations::download_src_rock(package, config.server(), progress).await?;
274    let hash = src_rock.bytes.hash()?;
275    let cursor = Cursor::new(src_rock.bytes);
276    let mime_type = infer::get(cursor.get_ref()).map(|file_type| file_type.mime_type());
277    operations::unpack::unpack(
278        mime_type,
279        cursor,
280        true,
281        src_rock.file_name,
282        dest_dir,
283        progress,
284    )
285    .await?;
286    Ok(RemotePackageSourceMetadata {
287        hash,
288        source_url: RemotePackageSourceUrl::Url { url: src_rock.url },
289    })
290}