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