lux-lib 0.67.0

Library for the lux package manager for Lua
Documentation
use std::collections::HashMap;
use std::io;
use std::io::Cursor;
use std::path::PathBuf;

use bon::Builder;
use bytes::Bytes;
use futures::{StreamExt, TryStreamExt};
use miette::Diagnostic;
use tempfile::TempDir;
use thiserror::Error;

use crate::{
    build::{BuildError, RemotePackageSourceSpec, SrcRockSource},
    config::Config,
    fs::{self, FsError},
    hash::HasIntegrity,
    lockfile::{
        LockedPackage, LockedPackageHashes, LockedPackageId, LockedPackageSpec,
        RemotePackageSourceUrl,
    },
    lua_rockspec::RemoteLuaRockspec,
    operations::{unpack_src_rock, FetchSrc, RemotePackageSourceMetadata},
    remote_package_source::RemotePackageSource,
    rockspec::Rockspec,
    tree::EntryType,
};

use super::{
    resolve::{ResolvedArtifacts, ResolvedPackage},
    Artifacts,
};

#[derive(Clone, Debug)]
pub(crate) enum PackageSource {
    SourceDir(PathBuf),
    Archive(Bytes),
}

pub(crate) struct MaterialisedPackage {
    pub(crate) package: LockedPackage,
    pub(crate) rockspec: RemoteLuaRockspec,
    pub(crate) entry_type: EntryType,
    pub(crate) build_behaviour: crate::build::BuildBehaviour,
    pub(crate) artifact: PackageSource,
    pub(crate) temp_dir: Option<TempDir>,
}

pub(crate) type MaterialiseArtifacts = Artifacts<HashMap<LockedPackageId, MaterialisedPackage>>;

#[derive(Error, Debug, Diagnostic)]
#[non_exhaustive]
pub enum Error {
    #[error(transparent)]
    #[diagnostic(transparent)]
    Fs(#[from] FsError),
    #[error(transparent)]
    #[diagnostic(transparent)]
    Build(#[from] Box<BuildError>),
    #[error("failed to hash the source for '{0}'")]
    Hash(String, #[source] io::Error),
    #[error("missing source url for '{0}'")]
    MissingSourceUrl(String),
}

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

#[derive(Builder)]
#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
pub(crate) struct Materialise<'a> {
    #[builder(start_fn)]
    pub(crate) config: &'a Config,
    pub(crate) resolved: ResolvedArtifacts,
}

impl<State> MaterialiseBuilder<'_, State>
where
    State: materialise_builder::State + materialise_builder::IsComplete,
{
    pub(crate) async fn materialise(self) -> Result<MaterialiseArtifacts, Error> {
        let args = self._build();
        let config = args.config;

        let artifacts: MaterialiseArtifacts = args
            .resolved
            .iter()
            .map(|(section, packages)| (section, packages.map(|_| HashMap::new())))
            .collect();

        let jobs = args.resolved.into_iter().flat_map(|(section, packages)| {
            packages
                .into_iter()
                .flatten()
                .map(move |(id, package)| (section, id, package))
        });

        futures::stream::iter(jobs)
            .map(|(section, id, package)| async move {
                materialise(config, package)
                    .await
                    .map(|package| (section, id, package))
            })
            .buffered(config.max_jobs())
            .try_fold(
                artifacts,
                |mut artifacts, (section, id, package)| async move {
                    artifacts
                        .get_mut(section)
                        .get_or_insert_with(HashMap::new)
                        .insert(id, package);
                    Ok(artifacts)
                },
            )
            .await
    }
}

async fn materialise(
    config: &Config,
    resolved: ResolvedPackage,
) -> Result<MaterialisedPackage, Error> {
    let ResolvedPackage {
        spec,
        rockspec,
        source,
        source_url,
        entry_type,
        build_behaviour,
        artifact,
    } = resolved;

    let name = spec.name().to_string();

    let (package, artifact, temp_dir) = match artifact {
        Some(PackageSource::Archive(bytes)) => match &source {
            RemotePackageSource::LuarocksBinaryRock(url) => {
                let binary_url = url.clone();
                let rockspec_hash = rockspec
                    .hash()
                    .await
                    .map_err(|err| Error::Hash(name.clone(), err))?;
                let source_hash = bytes
                    .hash()
                    .await
                    .map_err(|err| Error::Hash(name.clone(), err))?;
                let hashes = LockedPackageHashes {
                    rockspec: rockspec_hash,
                    source: source_hash,
                };
                let package = LockedPackage::new(
                    spec,
                    source,
                    Some(RemotePackageSourceUrl::Url { url: binary_url }),
                    hashes,
                );
                (package, PackageSource::Archive(bytes), None)
            }
            _ => {
                let source_url = source_url
                    .clone()
                    .ok_or_else(|| Error::MissingSourceUrl(name.clone()))?;
                let (package, dir) = fetch_and_hash_source(
                    &rockspec,
                    spec,
                    Some(RemotePackageSourceSpec::SrcRock(SrcRockSource {
                        bytes,
                        source_url,
                    })),
                    Some(source),
                    config,
                )
                .await?;
                let artifact = PackageSource::SourceDir(dir.path().to_path_buf());
                (package, artifact, Some(dir))
            }
        },
        Some(PackageSource::SourceDir(path)) => {
            let rockspec_hash = rockspec
                .hash()
                .await
                .map_err(|err| Error::Hash(name.clone(), err))?;
            let source_hash = path
                .hash()
                .await
                .map_err(|err| Error::Hash(name.clone(), err))?;
            let hashes = LockedPackageHashes {
                rockspec: rockspec_hash,
                source: source_hash,
            };
            let package = LockedPackage::new(spec, source, source_url, hashes);
            (package, PackageSource::SourceDir(path), None)
        }
        None => {
            let (package, dir) = fetch_and_hash_source(
                &rockspec,
                spec,
                Some(RemotePackageSourceSpec::RockSpec(source_url)),
                Some(source),
                config,
            )
            .await?;
            let artifact = PackageSource::SourceDir(dir.path().to_path_buf());
            (package, artifact, Some(dir))
        }
    };

    Ok(MaterialisedPackage {
        package,
        rockspec,
        entry_type,
        build_behaviour,
        artifact,
        temp_dir,
    })
}

pub(crate) async fn fetch_and_hash_source<R: Rockspec + HasIntegrity>(
    rockspec: &R,
    spec: LockedPackageSpec,
    source_spec: Option<RemotePackageSourceSpec>,
    source: Option<RemotePackageSource>,
    config: &Config,
) -> Result<(LockedPackage, TempDir), BuildError> {
    let temp_dir = fs::tempfile::tempdir()?;

    let source_metadata = match source_spec {
        Some(RemotePackageSourceSpec::SrcRock(SrcRockSource { bytes, source_url })) => {
            let hash = bytes.hash().await?;
            let cursor = Cursor::new(bytes);
            unpack_src_rock(cursor, temp_dir.path().to_path_buf())
                .await
                .map_err(BuildError::UnpackSrcRock)?;
            RemotePackageSourceMetadata { hash, source_url }
        }
        Some(RemotePackageSourceSpec::RockSpec(source_url)) => {
            FetchSrc::new(temp_dir.path(), rockspec, config)
                .maybe_source_url(source_url)
                .fetch_internal()
                .await?
        }
        None => {
            FetchSrc::new(temp_dir.path(), rockspec, config)
                .fetch_internal()
                .await?
        }
    };

    let hashes = LockedPackageHashes {
        rockspec: rockspec.hash().await?,
        source: source_metadata.hash.clone(),
    };

    let package = LockedPackage::new(
        spec,
        source
            .map(Result::Ok)
            .unwrap_or_else(|| {
                rockspec
                    .to_lua_remote_rockspec_string()
                    .map(RemotePackageSource::RockspecContent)
            })
            .unwrap_or(RemotePackageSource::Local),
        Some(source_metadata.source_url.clone()),
        hashes,
    );

    Ok((package, temp_dir))
}