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