1use std::{
2 io::{self, Cursor},
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use bon::Builder;
8use bytes::Bytes;
9use futures::StreamExt;
10use itertools::Itertools;
11use miette::Diagnostic;
12use path_slash::PathExt;
13use strum::IntoEnumIterator;
14use thiserror::Error;
15use tokio::{fs::File, io::AsyncWriteExt};
16
17use crate::{
18 build::{RemotePackageSourceSpec, SrcRockSource},
19 config::Config,
20 lockfile::{LocalPackageLockType, ReadOnly},
21 lua_rockspec::RemoteLuaRockspec,
22 operations::{
23 self,
24 resolve::{PackageInstallData, Resolve, ResolveDependenciesError},
25 DownloadedRockspec, FetchSrcError, PackageInstallSpec, UnpackError,
26 },
27 package::PackageReq,
28 progress::{MultiProgress, Progress, ProgressBar},
29 project::project_toml::LocalProjectTomlValidationError,
30 remote_package_db::{RemotePackageDB, RemotePackageDBError},
31 rockspec::Rockspec,
32 tree::EntryType,
33 workspace::{Workspace, WorkspaceError},
34};
35
36#[allow(clippy::large_enum_variant)]
37pub enum VendorTarget {
38 Workspace(Workspace),
40
41 Rockspec(RemoteLuaRockspec),
43}
44
45#[derive(Builder)]
49#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
50pub struct Vendor<'a> {
51 target: VendorTarget,
52
53 vendor_dir: PathBuf,
55
56 no_lock: Option<bool>,
58
59 no_delete: Option<bool>,
62
63 config: &'a Config,
64
65 progress: Option<Arc<Progress<MultiProgress>>>,
66}
67
68#[derive(Error, Debug, Diagnostic)]
69pub enum VendorError {
70 #[error(transparent)]
71 #[diagnostic(transparent)]
72 Workspace(#[from] WorkspaceError),
73 #[error("project validation failed:\n{0}")]
74 #[diagnostic(forward(0))]
75 LocalProjectTomlValidation(#[from] LocalProjectTomlValidationError),
76 #[error("error initialising remote package DB:\n{0}")]
77 #[diagnostic(forward(0))]
78 RemotePackageDB(#[from] RemotePackageDBError),
79 #[error("failed to resolve dependencies:\n{0}")]
80 #[diagnostic(forward(0))]
81 ResolveDependencies(#[from] ResolveDependenciesError),
82 #[error("failed to delete vendor directory {0}:\n{1}")]
83 DeleteVendorDir(String, io::Error),
84 #[error("failed to create vendor directory {0}:\n{1}")]
85 CreateVendorDir(String, io::Error),
86 #[error("failed to create {0}:\n{1}")]
87 CreateSrcRock(String, io::Error),
88 #[error("failed to vendor Lua RockSpec:\n{0}")]
89 LuaRockSpec(String),
90 #[error("failed to write Lua RockSpec {0}:\n{1}")]
91 WriteLuaRockSpec(String, io::Error),
92 #[error("failed to unpack src.rock:\n{0}")]
93 #[diagnostic(forward(0))]
94 Unpack(#[from] UnpackError),
95 #[error("failed to fetch rock source:\n{0}")]
96 #[diagnostic(forward(0))]
97 FetchSrc(#[from] FetchSrcError),
98}
99
100impl<State> VendorBuilder<'_, State>
101where
102 State: vendor_builder::State + vendor_builder::IsComplete,
103{
104 pub async fn vendor_dependencies(self) -> Result<(), VendorError> {
105 do_vendor_dependencies(self._build()).await
106 }
107}
108
109async fn do_vendor_dependencies(args: Vendor<'_>) -> Result<(), VendorError> {
110 let vendor_dir = args.vendor_dir;
111 let no_delete = args.no_delete.unwrap_or(false);
112 let no_lock = args.no_lock.unwrap_or(false);
113 let target = args.target;
114 let config = args.config;
115 let progress = match args.progress {
116 Some(p) => p,
117 None => MultiProgress::new_arc(args.config),
118 };
119 let mut all_packages = Vec::new();
120
121 for lock_type in LocalPackageLockType::iter() {
122 let (package_db, install_specs) =
123 mk_resolve_args(lock_type, no_lock, &target, config, progress.clone()).await?;
124
125 let (dep_tx, mut dep_rx) = tokio::sync::mpsc::unbounded_channel();
126 Resolve::<'_, ReadOnly>::new()
127 .dependencies_tx(dep_tx.clone())
128 .build_dependencies_tx(dep_tx)
129 .packages(install_specs)
130 .package_db(Arc::new(package_db))
131 .config(config)
132 .progress(progress.clone())
133 .get_all_dependencies()
134 .await?;
135
136 while let Some(dep) = dep_rx.recv().await {
137 all_packages.push(dep);
138 }
139 }
140
141 if !no_delete && vendor_dir.exists() {
142 tokio::fs::remove_dir_all(&vendor_dir)
143 .await
144 .map_err(|err| {
145 VendorError::DeleteVendorDir(vendor_dir.to_slash_lossy().to_string(), err)
146 })?;
147 }
148
149 vendor_sources(Arc::new(vendor_dir), progress, config.clone(), all_packages).await
150}
151
152async fn mk_resolve_args(
153 lock_type: LocalPackageLockType,
154 no_lock: bool,
155 target: &VendorTarget,
156 config: &Config,
157 progress: Arc<Progress<MultiProgress>>,
158) -> Result<(RemotePackageDB, Vec<PackageInstallSpec>), VendorError> {
159 match &target {
160 VendorTarget::Workspace(workspace) => {
161 let lockfile = workspace.lockfile()?;
162 let package_db = if !no_lock {
163 lockfile.local_pkg_lock(&lock_type).clone().into()
164 } else {
165 let bar = progress.map(|p| p.new_bar());
166 RemotePackageDB::from_config(config, &bar).await?
167 };
168 let mut install_specs = Vec::new();
169 for project in workspace.members() {
170 let toml = project.toml().into_local()?;
171 push_dependencies(&lock_type, &toml, &mut install_specs)?;
172 if lock_type == LocalPackageLockType::Test {
173 for test_spec_dependency in toml
174 .test()
175 .current_platform()
176 .test_dependencies(project)
177 .iter()
178 .cloned()
179 .map(|dep| PackageInstallSpec::new(dep, EntryType::Entrypoint).build())
180 {
181 install_specs.push(test_spec_dependency);
182 }
183 }
184 }
185 Ok((package_db, install_specs))
186 }
187 VendorTarget::Rockspec(remote_lua_rockspec) => {
188 let bar = progress.map(|p| p.new_bar());
189 let package_db = RemotePackageDB::from_config(config, &bar).await?;
190 let mut install_specs = Vec::new();
191 push_dependencies(&lock_type, remote_lua_rockspec, &mut install_specs)?;
192 Ok((package_db, install_specs))
193 }
194 }
195}
196
197fn push_dependencies<R: Rockspec>(
198 lock_type: &LocalPackageLockType,
199 rockspec: &R,
200 install_specs: &mut Vec<PackageInstallSpec>,
201) -> Result<(), LocalProjectTomlValidationError> {
202 let dependencies: Vec<&PackageReq> = match lock_type {
203 LocalPackageLockType::Regular => rockspec
204 .dependencies()
205 .current_platform()
206 .iter()
207 .map(|dep| dep.package_req())
208 .collect_vec(),
209 LocalPackageLockType::Test => rockspec
210 .test_dependencies()
211 .current_platform()
212 .iter()
213 .map(|dep| dep.package_req())
214 .collect_vec(),
215 LocalPackageLockType::Build => rockspec
216 .build_dependencies()
217 .current_platform()
218 .iter()
219 .map(|dep| dep.package_req())
220 .collect_vec(),
221 };
222 install_specs.extend(
223 dependencies
224 .into_iter()
225 .unique()
226 .cloned()
227 .map(|dep| PackageInstallSpec::new(dep, EntryType::Entrypoint).build())
228 .collect_vec(),
229 );
230 Ok(())
231}
232
233async fn vendor_sources(
234 vendor_dir: Arc<PathBuf>,
235 progress: Arc<Progress<MultiProgress>>,
236 config: Config,
237 packages: Vec<PackageInstallData>,
238) -> Result<(), VendorError> {
239 futures::stream::iter(packages.into_iter().map(|dep| {
240 let vendor_dir = Arc::clone(&vendor_dir);
241 let progress = Arc::clone(&progress);
242 let config = config.clone();
243 tokio::spawn(async move {
244 match dep.downloaded_rock {
245 crate::operations::RemoteRockDownload::RockspecOnly { rockspec_download } => {
246 vendor_rockspec_sources(
247 &vendor_dir,
248 rockspec_download,
249 None,
250 &config,
251 &progress,
252 )
253 .await?
254 }
255 crate::operations::RemoteRockDownload::BinaryRock {
256 rockspec_download,
257 packed_rock,
258 } => {
259 vendor_binary_rock(&vendor_dir, rockspec_download, packed_rock, &progress)
260 .await?
261 }
262 crate::operations::RemoteRockDownload::SrcRock {
263 rockspec_download,
264 src_rock,
265 source_url,
266 } => {
267 let src_rock_source = SrcRockSource {
268 bytes: src_rock,
269 source_url,
270 };
271 vendor_rockspec_sources(
272 &vendor_dir,
273 rockspec_download,
274 Some(src_rock_source),
275 &config,
276 &progress,
277 )
278 .await?
279 }
280 };
281 Ok::<_, VendorError>(())
282 })
283 }))
284 .buffered(config.max_jobs())
285 .collect::<Vec<_>>()
286 .await
287 .into_iter()
288 .flatten()
289 .try_collect()
290}
291
292async fn vendor_rockspec_sources(
293 vendor_dir: &Path,
294 rockspec_download: DownloadedRockspec,
295 src_rock_source: Option<SrcRockSource>,
296 config: &Config,
297 progress: &Progress<MultiProgress>,
298) -> Result<(), VendorError> {
299 let rockspec = rockspec_download.rockspec;
300 let package = rockspec.package();
301 let version = rockspec.version();
302 let package_version_str = format!("{}@{}", package, version);
303 let bar = progress.map(|p| {
304 p.add(ProgressBar::from(format!(
305 "💼 Vendoring source of {}",
306 &package_version_str,
307 )))
308 });
309 let source_spec = match src_rock_source {
310 Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
311 None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
312 };
313
314 let package_vendor_dir = vendor_dir.join(&package_version_str);
315
316 tokio::fs::create_dir_all(&package_vendor_dir)
317 .await
318 .map_err(|err| {
319 VendorError::CreateVendorDir(package_vendor_dir.to_slash_lossy().to_string(), err)
320 })?;
321
322 let rockspec_lua_content = rockspec
323 .to_lua_remote_rockspec_string()
324 .map_err(|err| VendorError::LuaRockSpec(err.to_string()))?;
325
326 let rockspec_file_name = format!("{}-{}.rockspec", package, version);
327 let rockspec_path = vendor_dir.join(rockspec_file_name);
328 tokio::fs::write(&rockspec_path, rockspec_lua_content)
329 .await
330 .map_err(|err| {
331 VendorError::WriteLuaRockSpec(rockspec_path.to_slash_lossy().to_string(), err)
332 })?;
333
334 match source_spec {
335 RemotePackageSourceSpec::SrcRock(SrcRockSource {
336 bytes,
337 source_url: _,
338 }) => {
339 let cursor = Cursor::new(&bytes);
340 operations::unpack_src_rock(cursor, package_vendor_dir, &bar).await?;
341 }
342 RemotePackageSourceSpec::RockSpec(source_url) => {
343 operations::FetchSrc::new(&package_vendor_dir, &rockspec, config, &bar)
344 .maybe_source_url(source_url)
345 .fetch_internal()
346 .await?;
347 }
348 }
349
350 bar.map(|bar| bar.finish_and_clear());
351
352 Ok(())
353}
354
355async fn vendor_binary_rock(
356 vendor_dir: &Path,
357 rockspec_download: DownloadedRockspec,
358 packed_rock: Bytes,
359 progress: &Progress<MultiProgress>,
360) -> Result<(), VendorError> {
361 let rockspec = rockspec_download.rockspec;
362 let package = rockspec.package();
363 let version = rockspec.version();
364
365 let file_name = format!("{}@{}.rock", package, version);
366
367 let bar = progress.map(|p| {
368 p.add(ProgressBar::from(format!(
369 "💼 Vendoring pre-built binary .rock: {}",
370 &file_name,
371 )))
372 });
373
374 tokio::fs::create_dir_all(&vendor_dir)
375 .await
376 .map_err(|err| {
377 VendorError::CreateVendorDir(vendor_dir.to_slash_lossy().to_string(), err)
378 })?;
379
380 let dest_file = vendor_dir.join(&file_name);
381 let mut file = File::create(&dest_file)
382 .await
383 .map_err(|err| VendorError::CreateSrcRock(dest_file.to_slash_lossy().to_string(), err))?;
384 file.write_all(&packed_rock)
385 .await
386 .map_err(|err| VendorError::CreateSrcRock(dest_file.to_slash_lossy().to_string(), err))?;
387
388 bar.map(|bar| bar.finish_and_clear());
389
390 Ok(())
391}