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