Skip to main content

lux_lib/operations/
vendor.rs

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    /// Vendor dependencies of a Lux workspace
39    Workspace(Workspace),
40
41    /// Vendor dependencies of a Lua RockSpec
42    Rockspec(RemoteLuaRockspec),
43}
44
45/// Vendor a project's dependencies into the specified directory at `<vendor_dir>`.
46/// After this command completes the vendor directory specified by `<vendor_dir>`
47/// will contain all remote sources from dependencies specified.
48#[derive(Builder)]
49#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
50pub struct Vendor<'a> {
51    target: VendorTarget,
52
53    /// The directory in which to vendor the dependencies.
54    vendor_dir: PathBuf,
55
56    /// Ignore the project's lockfile.
57    no_lock: Option<bool>,
58
59    /// Don't delete the `<vendor-dir>` when vendoring,{n}
60    /// but rather keep all existing contents of the vendor directory.
61    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}