lux-lib 0.67.1

Library for the lux package manager for Lua
Documentation
use std::collections::{HashMap, HashSet};

use bon::Builder;
use miette::Diagnostic;
use thiserror::Error;

use crate::{
    build::Deploy,
    config::Config,
    lockfile::{LockedPackage, LockedPackageId},
    lua_installation::{LuaInstallation, LuaInstallationError},
    lua_rockspec::BuildBackendSpec,
    luarocks::{
        install_binary_rock::{BinaryRockInstall, InstallBinaryRockError},
        luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
    },
    rockspec::Rockspec,
    tree::{InstallTree, TreeError},
};

use super::materialise::{MaterialisedPackage, PackageSource};

#[derive(Error, Debug, Diagnostic)]
#[non_exhaustive]
pub enum BuildError {
    #[error("failed to build '{0}'")]
    Build(String, #[source] Box<crate::build::BuildError>),
    #[error(transparent)]
    #[diagnostic(transparent)]
    Tree(#[from] TreeError),
    #[error("failed to install pre-built rock '{0}'")]
    InstallBinaryRock(String, #[source] Box<InstallBinaryRockError>),
    #[error(transparent)]
    #[diagnostic(transparent)]
    LuaInstallation(#[from] LuaInstallationError),
    #[error("error instantiating LuaRocks compatibility layer")]
    #[diagnostic(forward(0))]
    LuaRocks(#[from] LuaRocksError),
    #[error("error installing LuaRocks compatibility layer")]
    #[diagnostic(forward(0))]
    LuaRocksInstall(#[from] Box<LuaRocksInstallError>),
}

impl From<LuaRocksInstallError> for BuildError {
    fn from(source: LuaRocksInstallError) -> Self {
        Self::LuaRocksInstall(Box::new(source))
    }
}

/// A rocks package builder, providing fine-grained control
/// over how a package should be built.
#[derive(Builder)]
#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
pub(crate) struct Build<'a, T: InstallTree + Send + Sync> {
    #[builder(start_fn)]
    pub(crate) config: &'a Config,
    #[builder(start_fn)]
    pub(crate) tree: &'a T,
    #[builder(field)]
    pub(crate) packages: Vec<MaterialisedPackage>,
}

impl<T, State> BuildBuilder<'_, T, State>
where
    T: InstallTree + Send + Sync,
    State: build_builder::State,
{
    pub(crate) fn packages(mut self, packages: Vec<MaterialisedPackage>) -> Self {
        self.packages = packages;
        self
    }
}

impl<T, State> BuildBuilder<'_, T, State>
where
    T: InstallTree + Send + Sync,
    State: build_builder::State + build_builder::IsComplete,
{
    pub(crate) async fn build(self) -> Result<Vec<LockedPackage>, BuildError> {
        let args = self._build();
        let lua = LuaInstallation::new_from_config(args.config).await?;

        let needs_luarocks = args.packages.iter().any(|package| {
            matches!(
                package.rockspec.build().current_platform().build_backend,
                Some(BuildBackendSpec::LuaRock(_))
            )
        });
        if needs_luarocks {
            LuaRocksInstallation::new(args.config, args.tree.build_tree(args.config)?)?
                .ensure_installed(&lua)
                .await?;
        }

        let mut installed = Vec::new();

        for downloaded in order_by_build_dependencies(args.packages) {
            let MaterialisedPackage {
                package,
                rockspec,
                entry_type,
                build_behaviour: package_behaviour,
                artifact,
                temp_dir: _temp_dir,
            } = downloaded;

            let is_entrypoint = entry_type.is_entrypoint();
            let constraint = package.spec.constraint();
            let pin = package.spec.pinned();
            let opt = package.spec.opt();

            let pkg = match artifact {
                PackageSource::SourceDir(path) => Deploy::new(&rockspec, args.tree)
                    .package(package)
                    .lua(&lua)
                    .source_root(&path)
                    .entry_type(entry_type)
                    .config(args.config)
                    .behaviour(package_behaviour)
                    .deploy()
                    .await
                    .map_err(|err| {
                        BuildError::Build(
                            format!("{}-{}", rockspec.package(), rockspec.version()),
                            Box::new(err),
                        )
                    })?,
                PackageSource::Archive(bytes) => BinaryRockInstall::new(
                    &rockspec,
                    package.source().clone(),
                    bytes,
                    entry_type,
                    args.config,
                    args.tree,
                )
                .pin(pin)
                .opt(opt)
                .constraint(constraint)
                .behaviour(package_behaviour)
                .install()
                .await
                .map_err(|err| {
                    BuildError::InstallBinaryRock(rockspec.package().to_string(), Box::new(err))
                })?,
            };

            // Record the installed package in the tree lockfile so that its paths are visible to
            // the packages built after it (e.g. LuaRocks build backends), as well as future builds.
            {
                let mut lockfile = args.tree.lockfile()?.write_guard();
                if is_entrypoint {
                    lockfile.add_entrypoint(&pkg);
                } else {
                    lockfile.add(&pkg);
                }
            }

            installed.push(pkg);
        }

        Ok(installed)
    }
}

fn order_by_build_dependencies(packages: Vec<MaterialisedPackage>) -> Vec<MaterialisedPackage> {
    fn visit(
        id: LockedPackageId,
        by_id: &mut HashMap<LockedPackageId, MaterialisedPackage>,
        visited: &mut HashSet<LockedPackageId>,
        ordered: &mut Vec<MaterialisedPackage>,
    ) {
        if !visited.insert(id.clone()) {
            return;
        }

        let build_dependencies: Vec<LockedPackageId> = by_id
            .get(&id)
            .map(|package| {
                package
                    .package
                    .spec
                    .build_dependencies()
                    .into_iter()
                    .cloned()
                    .collect()
            })
            .unwrap_or_default();

        for dependency in build_dependencies {
            visit(dependency, by_id, visited, ordered);
        }

        if let Some(package) = by_id.remove(&id) {
            ordered.push(package);
        }
    }

    let mut by_id: HashMap<LockedPackageId, MaterialisedPackage> = packages
        .into_iter()
        .map(|package| (package.package.id(), package))
        .collect();

    let mut ordered = Vec::new();
    let mut visited = HashSet::new();

    let ids: Vec<LockedPackageId> = by_id.keys().cloned().collect();
    for id in ids {
        visit(id, &mut by_id, &mut visited, &mut ordered);
    }

    ordered
}