1use std::{
2 io::{self, Cursor},
3 path::{Path, PathBuf},
4 process::ExitStatus,
5 sync::Arc,
6};
7
8use bon::Builder;
9use bytes::Bytes;
10use futures::StreamExt;
11use itertools::Itertools;
12use miette::Diagnostic;
13use path_slash::PathExt;
14use strum::IntoEnumIterator;
15use thiserror::Error;
16use tokio::io::AsyncWriteExt;
17use tracing::Instrument;
18
19use crate::{
20 build::{resolve_source_dir, RemotePackageSourceSpec, SrcRockSource},
21 config::Config,
22 fs,
23 lockfile::{LocalPackageLockType, ReadOnly, RemotePackageSourceUrl},
24 lua_rockspec::{BuildBackendSpec, RemoteLuaRockspec},
25 operations::{
26 self,
27 resolve::{
28 luarocks_build_backend_name, PackageInstallData, Resolve, ResolveDependenciesError,
29 },
30 DownloadedRockspec, FetchSrcError, PackageInstallSpec, UnpackError,
31 },
32 package::{PackageReq, PackageSpec},
33 project::project_toml::LocalProjectTomlValidationError,
34 remote_package_db::{RemotePackageDB, RemotePackageDBError},
35 reqwest::RequestError,
36 rockspec::Rockspec,
37 tree::EntryType,
38 workspace::{Workspace, WorkspaceError},
39};
40
41#[allow(clippy::large_enum_variant)]
42pub enum VendorTarget {
43 Workspace(Workspace),
45
46 Rockspec(RemoteLuaRockspec),
48}
49
50#[derive(Builder)]
54#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
55pub struct Vendor<'a> {
56 target: VendorTarget,
57
58 vendor_dir: PathBuf,
60
61 no_lock: Option<bool>,
63
64 no_delete: Option<bool>,
67
68 config: &'a Config,
69}
70
71#[derive(Error, Debug, Diagnostic)]
72pub enum VendorError {
73 #[error(transparent)]
74 #[diagnostic(transparent)]
75 Workspace(#[from] WorkspaceError),
76 #[error("project validation failed")]
77 #[diagnostic(forward(0))]
78 LocalProjectTomlValidation(#[from] LocalProjectTomlValidationError),
79 #[error("error initialising remote package DB")]
80 #[diagnostic(forward(0))]
81 RemotePackageDB(#[from] RemotePackageDBError),
82 #[error("failed to resolve dependencies")]
83 #[diagnostic(forward(0))]
84 ResolveDependencies(#[from] Box<ResolveDependenciesError>),
85 #[error(transparent)]
86 #[diagnostic(transparent)]
87 Fs(#[from] fs::FsError),
88 #[error("failed to vendor Lua RockSpec:\n{0}")]
89 LuaRockSpec(String),
90 #[error("failed to unpack src.rock")]
91 #[diagnostic(forward(0))]
92 Unpack(#[from] UnpackError),
93 #[error("failed to fetch rock source")]
94 #[diagnostic(forward(0))]
95 FetchSrc(#[from] FetchSrcError),
96 #[error("failed to download rock source")]
97 #[diagnostic(forward(0))]
98 Request(#[from] RequestError),
99 #[error("failed to run `cargo vendor`")]
100 #[diagnostic(help("ensure cargo is installed"))]
101 CargoVendor { source: io::Error },
102 #[error("cargo vendor failed.\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}")]
103 #[diagnostic(help("check the output for details."))]
104 CargoVendorFailure {
105 status: ExitStatus,
106 stdout: String,
107 stderr: String,
108 },
109}
110
111impl From<ResolveDependenciesError> for VendorError {
112 fn from(source: ResolveDependenciesError) -> Self {
113 Self::ResolveDependencies(Box::new(source))
114 }
115}
116
117impl<State> VendorBuilder<'_, State>
118where
119 State: vendor_builder::State + vendor_builder::IsComplete,
120{
121 pub async fn vendor_dependencies(self) -> Result<(), VendorError> {
122 do_vendor_dependencies(self._build()).await
123 }
124}
125
126const CARGO_VENDOR_SUBDIR: &str = "cargo";
127
128async fn do_vendor_dependencies(args: Vendor<'_>) -> Result<(), VendorError> {
129 let vendor_dir = args.vendor_dir;
130 let no_delete = args.no_delete.unwrap_or(false);
131 let no_lock = args.no_lock.unwrap_or(false);
132 let target = args.target;
133 let config = args.config;
134 let mut all_packages = Vec::new();
135
136 for lock_type in LocalPackageLockType::iter() {
137 let (package_db, install_specs) =
138 mk_resolve_args(lock_type, no_lock, &target, config).await?;
139
140 let (dep_tx, mut dep_rx) = tokio::sync::mpsc::unbounded_channel();
141 Resolve::<'_, ReadOnly>::new()
142 .dependencies_tx(dep_tx.clone())
143 .build_dependencies_tx(dep_tx)
144 .packages(install_specs)
145 .package_db(Arc::new(package_db))
146 .config(config)
147 .get_all_dependencies()
148 .await?;
149
150 while let Some(dep) = dep_rx.recv().await {
151 all_packages.push(dep);
152 }
153 }
154
155 let all_packages = all_packages
158 .into_iter()
159 .unique_by(|pkg| (pkg.spec.name().clone(), pkg.spec.version().clone()))
160 .collect_vec();
161
162 let cargo_deps: Vec<(PackageSpec, Option<PathBuf>, Vec<PathBuf>)> = all_packages
163 .iter()
164 .filter_map(|pkg| {
165 let rockspec = pkg.downloaded_rock.rockspec();
166 match rockspec.build().current_platform().build_backend {
167 Some(BuildBackendSpec::RustMlua(_) | BuildBackendSpec::RustBinary(_)) => Some((
168 pkg.spec.to_package(),
169 rockspec.source().current_platform().unpack_dir.clone(),
170 rockspec.build().current_platform().copy_directories.clone(),
171 )),
172 _ => None,
173 }
174 })
175 .collect();
176
177 if !no_delete && vendor_dir.exists() {
178 fs::tokio::remove_dir_all(&vendor_dir).await?;
179 }
180
181 let vendor_dir = Arc::new(vendor_dir);
182 vendor_sources(vendor_dir.clone(), config.clone(), all_packages).await?;
183 vendor_target_cargo_deps(&vendor_dir, &target, config).await?;
184 for (dep, unpack_dir, copy_dirs) in cargo_deps {
185 vendor_package_cargo_deps(&vendor_dir, &dep, &unpack_dir, ©_dirs, config).await?;
186 }
187 Ok(())
188}
189
190async fn mk_resolve_args(
191 lock_type: LocalPackageLockType,
192 no_lock: bool,
193 target: &VendorTarget,
194 config: &Config,
195) -> Result<(RemotePackageDB, Vec<PackageInstallSpec>), VendorError> {
196 match &target {
197 VendorTarget::Workspace(workspace) => {
198 let lockfile = workspace.try_lockfile()?;
202 let package_db = match lockfile {
203 Some(lockfile) if !no_lock => lockfile.local_pkg_locks().into(),
204 _ => RemotePackageDB::from_config(config).await?,
205 };
206 let mut install_specs = Vec::new();
207 for project in workspace.members() {
208 let toml = project.toml().into_local()?;
209 push_dependencies(&lock_type, &toml, &mut install_specs)?;
210 if lock_type == LocalPackageLockType::Test {
211 for test_spec_dependency in toml
212 .test()
213 .current_platform()
214 .test_dependencies(project)
215 .iter()
216 .cloned()
217 .map(|dep| PackageInstallSpec::new(dep, EntryType::Entrypoint).build())
218 {
219 install_specs.push(test_spec_dependency);
220 }
221 }
222 }
223 Ok((package_db, install_specs))
224 }
225 VendorTarget::Rockspec(remote_lua_rockspec) => {
226 let package_db = RemotePackageDB::from_config(config).await?;
227 let mut install_specs = Vec::new();
228 push_dependencies(&lock_type, remote_lua_rockspec, &mut install_specs)?;
229 Ok((package_db, install_specs))
230 }
231 }
232}
233
234fn push_dependencies<R: Rockspec>(
235 lock_type: &LocalPackageLockType,
236 rockspec: &R,
237 install_specs: &mut Vec<PackageInstallSpec>,
238) -> Result<(), LocalProjectTomlValidationError> {
239 let mut dependencies: Vec<PackageReq> = match lock_type {
240 LocalPackageLockType::Regular => rockspec
241 .dependencies()
242 .current_platform()
243 .iter()
244 .map(|dep| dep.package_req().clone())
245 .collect_vec(),
246 LocalPackageLockType::Test => rockspec
247 .test_dependencies()
248 .current_platform()
249 .iter()
250 .map(|dep| dep.package_req().clone())
251 .collect_vec(),
252 LocalPackageLockType::Build => rockspec
253 .build_dependencies()
254 .current_platform()
255 .iter()
256 .map(|dep| dep.package_req().clone())
257 .collect_vec(),
258 };
259 if *lock_type == LocalPackageLockType::Build {
260 if let Some(backend) = luarocks_build_backend_name(rockspec) {
261 dependencies.insert(0, backend.into());
262 }
263 }
264 install_specs.extend(
265 dependencies
266 .into_iter()
267 .unique()
268 .map(|dep| PackageInstallSpec::new(dep, EntryType::Entrypoint).build())
269 .collect_vec(),
270 );
271 Ok(())
272}
273
274async fn vendor_sources(
275 vendor_dir: Arc<PathBuf>,
276 config: Config,
277 packages: Vec<PackageInstallData>,
278) -> Result<(), VendorError> {
279 futures::stream::iter(packages.into_iter().map(|dep| {
280 let vendor_dir = Arc::clone(&vendor_dir);
281 let config = config.clone();
282 tokio::spawn(
283 async move {
284 match dep.downloaded_rock {
285 crate::operations::RemoteRockDownload::RockspecOnly { rockspec_download } => {
286 vendor_rockspec_sources(&vendor_dir, rockspec_download, None, &config)
287 .await?
288 }
289 crate::operations::RemoteRockDownload::BinaryRock {
290 rockspec_download,
291 packed_rock,
292 } => vendor_binary_rock(&vendor_dir, rockspec_download, packed_rock).await?,
293 crate::operations::RemoteRockDownload::SrcRock {
294 rockspec_download,
295 src_rock,
296 source_url,
297 } => {
298 let src_rock_source = SrcRockSource {
299 bytes: src_rock,
300 source_url,
301 };
302 vendor_rockspec_sources(
303 &vendor_dir,
304 rockspec_download,
305 Some(src_rock_source),
306 &config,
307 )
308 .await?
309 }
310 };
311 Ok::<_, VendorError>(())
312 }
313 .instrument(tracing::trace_span!("vendor_worker")),
314 )
315 }))
316 .buffered(config.max_jobs())
317 .collect::<Vec<_>>()
318 .instrument(tracing::trace_span!("vendor_collector"))
319 .await
320 .into_iter()
321 .flatten()
322 .try_collect()
323}
324
325#[tracing::instrument(
326 name = "Vendoring source",
327 level = "info",
328 skip_all,
329 fields(
330 package = rockspec_download.rockspec.package().to_string(),
331 version = rockspec_download.rockspec.version().to_string(),
332 ),
333)]
334async fn vendor_rockspec_sources(
335 vendor_dir: &Path,
336 rockspec_download: DownloadedRockspec,
337 src_rock_source: Option<SrcRockSource>,
338 config: &Config,
339) -> Result<(), VendorError> {
340 let rockspec = rockspec_download.rockspec;
341 let package = rockspec.package();
342 let version = rockspec.version();
343 let package_version_str = format!("{}@{}", package, version);
344
345 let source_spec = match src_rock_source {
346 Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
347 None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
348 };
349
350 let source_path = vendor_dir.join(&package_version_str);
351
352 fs::tokio::create_dir_all(vendor_dir).await?;
353
354 let rockspec_lua_content = rockspec
355 .to_lua_remote_rockspec_string()
356 .map_err(|err| VendorError::LuaRockSpec(err.to_string()))?;
357
358 let rockspec_file_name = format!("{}-{}.rockspec", package, version);
359 let rockspec_path = vendor_dir.join(rockspec_file_name);
360 fs::tokio::write(&rockspec_path, rockspec_lua_content).await?;
361
362 match source_spec {
363 RemotePackageSourceSpec::SrcRock(SrcRockSource {
364 bytes,
365 source_url: _,
366 }) => {
367 fs::tokio::write(&source_path, &bytes).await?;
368 }
369 RemotePackageSourceSpec::RockSpec(source_url) => match source_url {
370 Some(RemotePackageSourceUrl::Url { url }) => {
371 let bytes = crate::reqwest::download_bytes(config, &url).await?;
372 fs::tokio::write(&source_path, &bytes).await?;
373 }
374 _ => {
375 fs::tokio::create_dir_all(&source_path).await?;
376 operations::FetchSrc::new(&source_path, &rockspec, config)
377 .maybe_source_url(source_url)
378 .fetch_internal()
379 .await?;
380 }
381 },
382 }
383
384 Ok(())
385}
386
387#[tracing::instrument(
388 name = "Vendoring pre-built binary",
389 level = "info",
390 skip_all,
391 fields(
392 package = rockspec_download.rockspec.package().to_string(),
393 version = rockspec_download.rockspec.version().to_string(),
394 ),
395)]
396async fn vendor_binary_rock(
397 vendor_dir: &Path,
398 rockspec_download: DownloadedRockspec,
399 packed_rock: Bytes,
400) -> Result<(), VendorError> {
401 let rockspec = rockspec_download.rockspec;
402 let package = rockspec.package();
403 let version = rockspec.version();
404
405 let file_name = format!("{}@{}.rock", package, version);
406
407 fs::tokio::create_dir_all(&vendor_dir).await?;
408
409 let dest_file = vendor_dir.join(&file_name);
410 let mut file = fs::tokio::create(&dest_file).await?;
411 file.write_all(&packed_rock)
412 .await
413 .map_err(|source| fs::FsError::Write {
414 path: dest_file.to_path_buf(),
415 source,
416 })?;
417
418 let rockspec_lua_content = rockspec
419 .to_lua_remote_rockspec_string()
420 .map_err(|err| VendorError::LuaRockSpec(err.to_string()))?;
421
422 let rockspec_file_name = format!("{}-{}.rockspec", package, version);
423 let rockspec_path = vendor_dir.join(rockspec_file_name);
424 fs::tokio::write(&rockspec_path, rockspec_lua_content).await?;
425
426 Ok(())
427}
428
429#[tracing::instrument(name = "Vendoring cargo dependencies", level = "info", skip_all)]
430async fn vendor_target_cargo_deps(
431 vendor_dir: &Path,
432 target: &VendorTarget,
433 config: &Config,
434) -> Result<(), VendorError> {
435 if is_cargo_build_backend(target) {
436 match target {
437 VendorTarget::Workspace(workspace) => {
438 cargo_vendor(vendor_dir, workspace.root().as_path(), config).await
439 }
440 VendorTarget::Rockspec(rockspec) => {
441 let temp_dir = fs::tempfile::tempdir()?;
442 operations::FetchSrc::new(temp_dir.path(), rockspec, config)
443 .fetch_internal()
444 .await?;
445 cargo_vendor(
446 vendor_dir,
447 &resolve_source_dir(
448 temp_dir.path(),
449 rockspec.source().current_platform().unpack_dir.as_deref(),
450 &rockspec.build().current_platform().copy_directories,
451 )?,
452 config,
453 )
454 .await
455 }
456 }
457 } else {
458 Ok(())
459 }
460}
461
462#[tracing::instrument(
463 name = "Vendoring cargo dependencies",
464 level = "info",
465 skip_all,
466 fields(
467 package = package.name().to_string(),
468 version = package.version().to_string(),
469 ),
470)]
471async fn vendor_package_cargo_deps(
472 vendor_dir: &Path,
473 package: &PackageSpec,
474 unpack_dir: &Option<PathBuf>,
475 copy_dirs: &[PathBuf],
476 config: &Config,
477) -> Result<(), VendorError> {
478 let source_dir = vendor_dir.join(format!("{}@{}", package.name(), package.version()));
479 if source_dir.is_dir() {
480 cargo_vendor(
481 vendor_dir,
482 &resolve_source_dir(&source_dir, unpack_dir.as_deref(), copy_dirs)?,
483 config,
484 )
485 .await?;
486 } else if source_dir.is_file() {
487 let temp_dir = fs::tempfile::tempdir()?;
490 extract_source_archive(&source_dir, unpack_dir.as_deref(), temp_dir.path()).await?;
491 cargo_vendor(
492 vendor_dir,
493 &resolve_source_dir(temp_dir.path(), unpack_dir.as_deref(), copy_dirs)?,
494 config,
495 )
496 .await?;
497 }
498
499 Ok(())
500}
501
502fn is_cargo_build_backend(target: &VendorTarget) -> bool {
503 match target {
504 VendorTarget::Workspace(workspace) => workspace.members().iter().any(|project| {
505 project.toml().into_local().is_ok_and(|toml| {
506 matches!(
507 toml.build().current_platform().build_backend.to_owned(),
508 Some(BuildBackendSpec::RustMlua(_) | BuildBackendSpec::RustBinary(_))
509 )
510 })
511 }),
512 VendorTarget::Rockspec(rockspec) => matches!(
513 rockspec.build().current_platform().build_backend.to_owned(),
514 Some(BuildBackendSpec::RustMlua(_) | BuildBackendSpec::RustBinary(_))
515 ),
516 }
517}
518
519async fn extract_source_archive(
520 source_file: &Path,
521 unpack_dir: Option<&Path>,
522 dest_dir: &Path,
523) -> Result<(), VendorError> {
524 let bytes = fs::tokio::read(source_file).await?;
525 let mime_type = infer::get(&bytes).map(|file_type| file_type.mime_type());
526 let file_name = source_file
527 .file_name()
528 .map(|file_name| file_name.to_string_lossy())
529 .unwrap_or(source_file.to_slash_lossy())
530 .to_string();
531 operations::unpack::unpack(
532 mime_type,
533 Cursor::new(bytes),
534 unpack_dir.is_none(),
535 file_name,
536 dest_dir,
537 )
538 .await?;
539 Ok(())
540}
541
542async fn cargo_vendor(
543 vendor_dir: &Path,
544 source_dir: &Path,
545 config: &Config,
546) -> Result<(), VendorError> {
547 let cargo_vendor_dir = fs::sync::absolute(vendor_dir.join(CARGO_VENDOR_SUBDIR))?;
548 fs::tokio::create_dir_all(&cargo_vendor_dir).await?;
549
550 let output = config
551 .wrapped_command("cargo", ["vendor", "--locked", "--versioned-dirs"])
552 .arg(&cargo_vendor_dir)
553 .current_dir(source_dir)
554 .output()
555 .await
556 .map_err(|source| VendorError::CargoVendor { source })?;
557
558 if output.status.success() {
559 Ok(())
560 } else {
561 Err(VendorError::CargoVendorFailure {
562 status: output.status,
563 stdout: String::from_utf8_lossy(&output.stdout).into(),
564 stderr: String::from_utf8_lossy(&output.stderr).into(),
565 })
566 }
567}