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