Skip to main content

lux_lib/operations/
vendor.rs

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    /// 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(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}