lux_lib/operations/
install.rs

1use std::{collections::HashMap, io, sync::Arc};
2
3use crate::{
4    build::{Build, BuildBehaviour, BuildError, RemotePackageSourceSpec, SrcRockSource},
5    config::{Config, LuaVersionUnset},
6    lockfile::{
7        LocalPackage, LocalPackageId, LockConstraint, Lockfile, OptState, PinnedState, ReadOnly,
8        ReadWrite,
9    },
10    lua_rockspec::BuildBackendSpec,
11    luarocks::{
12        install_binary_rock::{BinaryRockInstall, InstallBinaryRockError},
13        luarocks_installation::{
14            InstallBuildDependenciesError, LuaRocksError, LuaRocksInstallError,
15            LuaRocksInstallation,
16        },
17    },
18    package::{PackageName, PackageNameList},
19    progress::{MultiProgress, Progress, ProgressBar},
20    project::{Project, ProjectTreeError},
21    remote_package_db::{RemotePackageDB, RemotePackageDBError, RemotePackageDbIntegrityError},
22    rockspec::Rockspec,
23    tree::{self, Tree, TreeError},
24};
25
26use bon::Builder;
27use bytes::Bytes;
28use futures::future::join_all;
29use itertools::Itertools;
30use thiserror::Error;
31
32use super::{
33    install_spec::PackageInstallSpec, resolve::get_all_dependencies, DownloadedRockspec,
34    RemoteRockDownload, SearchAndDownloadError,
35};
36
37/// A rocks package installer, providing fine-grained control
38/// over how packages should be installed.
39/// Can install multiple packages in parallel.
40#[derive(Builder)]
41#[builder(start_fn = new, finish_fn(name = _install, vis = ""))]
42pub struct Install<'a> {
43    #[builder(start_fn)]
44    config: &'a Config,
45    #[builder(field)]
46    packages: Vec<PackageInstallSpec>,
47    #[builder(setters(name = "_tree", vis = ""))]
48    tree: Tree,
49    package_db: Option<RemotePackageDB>,
50    progress: Option<Arc<Progress<MultiProgress>>>,
51}
52
53impl<'a, State> InstallBuilder<'a, State>
54where
55    State: install_builder::State,
56{
57    pub fn tree(self, tree: Tree) -> InstallBuilder<'a, install_builder::SetTree<State>>
58    where
59        State::Tree: install_builder::IsUnset,
60    {
61        self._tree(tree)
62    }
63
64    pub fn project(
65        self,
66        project: &'a Project,
67    ) -> Result<InstallBuilder<'a, install_builder::SetTree<State>>, ProjectTreeError>
68    where
69        State::Tree: install_builder::IsUnset,
70    {
71        let config = self.config;
72        Ok(self._tree(project.tree(config)?))
73    }
74
75    pub fn packages(self, packages: Vec<PackageInstallSpec>) -> Self {
76        Self { packages, ..self }
77    }
78
79    pub fn package(self, package: PackageInstallSpec) -> Self {
80        Self {
81            packages: self
82                .packages
83                .into_iter()
84                .chain(std::iter::once(package))
85                .collect(),
86            ..self
87        }
88    }
89}
90
91impl<State> InstallBuilder<'_, State>
92where
93    State: install_builder::State + install_builder::IsComplete,
94{
95    /// Install the packages.
96    pub async fn install(self) -> Result<Vec<LocalPackage>, InstallError> {
97        let install_built = self._install();
98        let progress = match install_built.progress {
99            Some(p) => p,
100            None => MultiProgress::new_arc(),
101        };
102        let package_db = match install_built.package_db {
103            Some(db) => db,
104            None => {
105                let bar = progress.map(|p| p.new_bar());
106                RemotePackageDB::from_config(install_built.config, &bar).await?
107            }
108        };
109
110        let duplicate_entrypoints = install_built
111            .packages
112            .iter()
113            .filter(|pkg| pkg.entry_type == tree::EntryType::Entrypoint)
114            .map(|pkg| pkg.package.name())
115            .duplicates()
116            .cloned()
117            .collect_vec();
118
119        if !duplicate_entrypoints.is_empty() {
120            return Err(InstallError::DuplicateEntrypoints(PackageNameList::new(
121                duplicate_entrypoints,
122            )));
123        }
124
125        install_impl(
126            install_built.packages,
127            Arc::new(package_db),
128            install_built.config,
129            &install_built.tree,
130            install_built.tree.lockfile()?,
131            progress,
132        )
133        .await
134    }
135}
136
137#[derive(Error, Debug)]
138pub enum InstallError {
139    #[error(transparent)]
140    SearchAndDownloadError(#[from] SearchAndDownloadError),
141    #[error(transparent)]
142    LuaVersionUnset(#[from] LuaVersionUnset),
143    #[error(transparent)]
144    Io(#[from] io::Error),
145    #[error(transparent)]
146    Tree(#[from] TreeError),
147    #[error("error instantiating LuaRocks compatibility layer: {0}")]
148    LuaRocksError(#[from] LuaRocksError),
149    #[error("error installing LuaRocks compatibility layer: {0}")]
150    LuaRocksInstallError(#[from] LuaRocksInstallError),
151    #[error("error installing LuaRocks build dependencies: {0}")]
152    InstallBuildDependenciesError(#[from] InstallBuildDependenciesError),
153    #[error("failed to build {0}: {1}")]
154    BuildError(PackageName, BuildError),
155    #[error("error initialising remote package DB: {0}")]
156    RemotePackageDB(#[from] RemotePackageDBError),
157    #[error("failed to install pre-built rock {0}: {1}")]
158    InstallBinaryRockError(PackageName, InstallBinaryRockError),
159    #[error("integrity error for package {0}: {1}\n")]
160    Integrity(PackageName, RemotePackageDbIntegrityError),
161    #[error(transparent)]
162    ProjectTreeError(#[from] ProjectTreeError),
163    #[error("cannot install duplicate entrypoints: {0}")]
164    DuplicateEntrypoints(PackageNameList),
165}
166
167// TODO(vhyrro): This function has too many arguments. Refactor it.
168#[allow(clippy::too_many_arguments)]
169async fn install_impl(
170    packages: Vec<PackageInstallSpec>,
171    package_db: Arc<RemotePackageDB>,
172    config: &Config,
173    tree: &Tree,
174    lockfile: Lockfile<ReadOnly>,
175    progress_arc: Arc<Progress<MultiProgress>>,
176) -> Result<Vec<LocalPackage>, InstallError> {
177    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
178
179    get_all_dependencies(
180        tx,
181        packages,
182        package_db.clone(),
183        Arc::new(lockfile.clone()),
184        config,
185        progress_arc.clone(),
186    )
187    .await?;
188
189    let mut all_packages = HashMap::with_capacity(rx.len());
190
191    while let Some(dep) = rx.recv().await {
192        all_packages.insert(dep.spec.id(), dep);
193    }
194
195    let installed_packages = join_all(all_packages.clone().into_values().map(|install_spec| {
196        let progress_arc = progress_arc.clone();
197        let downloaded_rock = install_spec.downloaded_rock;
198        let config = config.clone();
199        let tree = tree.clone();
200
201        tokio::spawn(async move {
202            let rockspec = downloaded_rock.rockspec();
203            if let Some(BuildBackendSpec::LuaRock(build_backend)) =
204                &rockspec.build().current_platform().build_backend
205            {
206                let luarocks_tree = tree.build_tree(&config)?;
207                let luarocks = LuaRocksInstallation::new(&config, luarocks_tree)?;
208                luarocks
209                    .install_build_dependencies(build_backend, rockspec, progress_arc.clone())
210                    .await?;
211            }
212
213            let pkg = match downloaded_rock {
214                RemoteRockDownload::RockspecOnly { rockspec_download } => {
215                    install_rockspec(
216                        rockspec_download,
217                        None,
218                        install_spec.spec.constraint(),
219                        install_spec.build_behaviour,
220                        install_spec.pin,
221                        install_spec.opt,
222                        install_spec.entry_type,
223                        &tree,
224                        &config,
225                        progress_arc,
226                    )
227                    .await?
228                }
229                RemoteRockDownload::BinaryRock {
230                    rockspec_download,
231                    packed_rock,
232                } => {
233                    install_binary_rock(
234                        rockspec_download,
235                        packed_rock,
236                        install_spec.spec.constraint(),
237                        install_spec.build_behaviour,
238                        install_spec.pin,
239                        install_spec.opt,
240                        install_spec.entry_type,
241                        &config,
242                        &tree,
243                        progress_arc,
244                    )
245                    .await?
246                }
247                RemoteRockDownload::SrcRock {
248                    rockspec_download,
249                    src_rock,
250                    source_url,
251                } => {
252                    let src_rock_source = SrcRockSource {
253                        bytes: src_rock,
254                        source_url,
255                    };
256                    install_rockspec(
257                        rockspec_download,
258                        Some(src_rock_source),
259                        install_spec.spec.constraint(),
260                        install_spec.build_behaviour,
261                        install_spec.pin,
262                        install_spec.opt,
263                        install_spec.entry_type,
264                        &tree,
265                        &config,
266                        progress_arc,
267                    )
268                    .await?
269                }
270            };
271
272            Ok::<_, InstallError>((pkg.id(), (pkg, install_spec.entry_type)))
273        })
274    }))
275    .await
276    .into_iter()
277    .flatten()
278    .try_collect::<_, HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>, _>()?;
279
280    let write_dependency = |lockfile: &mut Lockfile<ReadWrite>,
281                            id: &LocalPackageId,
282                            pkg: &LocalPackage,
283                            entry_type: tree::EntryType| {
284        if entry_type == tree::EntryType::Entrypoint {
285            lockfile.add_entrypoint(pkg);
286        }
287
288        all_packages
289            .get(id)
290            .map(|pkg| pkg.spec.dependencies())
291            .unwrap_or_default()
292            .into_iter()
293            .for_each(|dependency_id| {
294                lockfile.add_dependency(
295                    pkg,
296                    installed_packages
297                        .get(dependency_id)
298                        .map(|(pkg, _)| pkg)
299                        // NOTE: This can happen if an install thread panics
300                        .expect("required dependency not found [This is a bug!]"),
301                );
302            });
303    };
304
305    lockfile.map_then_flush(|lockfile| {
306        installed_packages
307            .iter()
308            .for_each(|(id, (pkg, is_entrypoint))| {
309                write_dependency(lockfile, id, pkg, *is_entrypoint)
310            });
311
312        Ok::<_, io::Error>(())
313    })?;
314
315    Ok(installed_packages
316        .into_values()
317        .map(|(pkg, _)| pkg)
318        .collect_vec())
319}
320
321#[allow(clippy::too_many_arguments)]
322async fn install_rockspec(
323    rockspec_download: DownloadedRockspec,
324    src_rock_source: Option<SrcRockSource>,
325    constraint: LockConstraint,
326    behaviour: BuildBehaviour,
327    pin: PinnedState,
328    opt: OptState,
329    entry_type: tree::EntryType,
330    tree: &Tree,
331    config: &Config,
332    progress_arc: Arc<Progress<MultiProgress>>,
333) -> Result<LocalPackage, InstallError> {
334    let progress = Arc::clone(&progress_arc);
335    let rockspec = rockspec_download.rockspec;
336    let source = rockspec_download.source;
337    let package = rockspec.package().clone();
338    let bar = progress.map(|p| p.add(ProgressBar::from(format!("💻 Installing {}", &package,))));
339
340    if let Some(BuildBackendSpec::LuaRock(build_backend)) =
341        &rockspec.build().current_platform().build_backend
342    {
343        let luarocks_tree = tree.build_tree(config)?;
344        let luarocks = LuaRocksInstallation::new(config, luarocks_tree)?;
345        luarocks.ensure_installed(&bar).await?;
346        luarocks
347            .install_build_dependencies(build_backend, &rockspec, progress_arc)
348            .await?;
349    }
350
351    let source_spec = match src_rock_source {
352        Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
353        None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
354    };
355
356    let pkg = Build::new(&rockspec, tree, entry_type, config, &bar)
357        .pin(pin)
358        .opt(opt)
359        .constraint(constraint)
360        .behaviour(behaviour)
361        .source(source)
362        .source_spec(source_spec)
363        .build()
364        .await
365        .map_err(|err| InstallError::BuildError(package, err))?;
366
367    bar.map(|b| b.finish_and_clear());
368
369    Ok(pkg)
370}
371
372#[allow(clippy::too_many_arguments)]
373async fn install_binary_rock(
374    rockspec_download: DownloadedRockspec,
375    packed_rock: Bytes,
376    constraint: LockConstraint,
377    behaviour: BuildBehaviour,
378    pin: PinnedState,
379    opt: OptState,
380    entry_type: tree::EntryType,
381    config: &Config,
382    tree: &Tree,
383    progress_arc: Arc<Progress<MultiProgress>>,
384) -> Result<LocalPackage, InstallError> {
385    let progress = Arc::clone(&progress_arc);
386    let rockspec = rockspec_download.rockspec;
387    let package = rockspec.package().clone();
388    let bar = progress.map(|p| {
389        p.add(ProgressBar::from(format!(
390            "💻 Installing {} (pre-built)",
391            &package,
392        )))
393    });
394    let pkg = BinaryRockInstall::new(
395        &rockspec,
396        rockspec_download.source,
397        packed_rock,
398        entry_type,
399        config,
400        tree,
401        &bar,
402    )
403    .pin(pin)
404    .opt(opt)
405    .constraint(constraint)
406    .behaviour(behaviour)
407    .install()
408    .await
409    .map_err(|err| InstallError::InstallBinaryRockError(package, err))?;
410
411    bar.map(|b| b.finish_and_clear());
412
413    Ok(pkg)
414}