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