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