lux-lib 0.65.0

Library for the lux package manager for Lua
Documentation
use crate::build::backend::{BuildBackend, BuildInfo, RunBuildArgs};
use crate::build::utils::{self, InstallBinaryError};
use crate::config::{build, Config};
use crate::fs;
use crate::lua_rockspec::RustBinaryBuildSpec;
use crate::tree::InstallTree;
use miette::Diagnostic;
use path_slash::PathBufExt;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use thiserror::Error;

use tracing::Instrument;

#[derive(Error, Debug, Diagnostic)]
#[non_exhaustive]
pub enum RustBinaryError {
    #[error("`cargo install` failed.\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}")]
    CargoInstall {
        status: ExitStatus,
        stdout: String,
        stderr: String,
    },
    #[error("failed to run `cargo`")]
    #[diagnostic(help("ensure cargo is installed"))]
    RustBuild { source: io::Error },
    #[error("`cargo metadata` failed.\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}")]
    CargoMetadata {
        status: ExitStatus,
        stdout: String,
        stderr: String,
    },
    #[error("failed to parse `cargo metadata` output")]
    CargoMetadataParse(#[from] serde_json::Error),
    #[error("could not locate cargo package '{package}' in the source")]
    CargoPackageNotFound { package: String },
    #[error("failed to install binary '{file_name}'")]
    InstallBinary {
        file_name: String,
        #[diagnostic_source]
        source: InstallBinaryError,
    },
    #[error(transparent)]
    #[diagnostic(transparent)]
    Fs(#[from] fs::FsError),
}

impl BuildBackend for RustBinaryBuildSpec {
    type Err = RustBinaryError;

    #[tracing::instrument(name = "rust_binary::run", skip_all, level = "debug")]
    async fn run<T>(self, args: RunBuildArgs<'_, T>) -> Result<BuildInfo, Self::Err>
    where
        T: InstallTree,
    {
        let config = args.config;
        let build_dir = args.build_dir;

        let mut install_args = vec!["install", "--root", ".", "--bins"];
        if config.build_profile() == build::Profile::Dev {
            install_args.push("--debug");
        }
        let features = self.features.join(",");
        if !features.is_empty() {
            install_args.push("--features");
            install_args.push(&features);
        }
        let cargo_vendor_dir = config
            .vendor_dir()
            .map(|vendor_dir| vendor_dir.join("cargo"))
            .filter(|dir| dir.is_dir());

        let crate_dir = if let Some(package) = &self.package {
            find_cargo_package_dir(config, build_dir, package)
                .await?
                .to_slash_lossy()
                .to_string()
        } else {
            format!("{}", build_dir.display())
        };

        install_args.push("--path");
        install_args.push(&crate_dir);

        if let Some(cargo_vendor_dir) = &cargo_vendor_dir {
            install_args.push("--offline");
            utils::prepare_cargo_vendor_config(config, build_dir, cargo_vendor_dir).await?;
        }

        match config
            .wrapped_command("cargo", install_args)
            .current_dir(build_dir)
            .output()
            .instrument(tracing::info_span!(
                "Compiling rust binary",
                profile = config.build_profile().to_string()
            ))
            .await
        {
            Ok(output) if output.status.success() => utils::trace_command_output(&output),
            Ok(output) => {
                return Err(RustBinaryError::CargoInstall {
                    status: output.status,
                    stdout: String::from_utf8_lossy(&output.stdout).into(),
                    stderr: String::from_utf8_lossy(&output.stderr).into(),
                });
            }
            Err(source) => return Err(RustBinaryError::RustBuild { source }),
        }

        let mut binaries = Vec::new();
        for bin_script in utils::detect_binaries(build_dir) {
            if let Some(target) = bin_script.file_name() {
                let file_name = target.to_string_lossy().to_string();
                let installed_bin_script = utils::install_binary(
                    &bin_script,
                    &file_name,
                    args.tree,
                    args.lua,
                    args.deploy,
                    config,
                )
                .await
                .map_err(|err| RustBinaryError::InstallBinary {
                    file_name: file_name.to_string(),
                    source: err,
                })?;
                if let Some(bin_script_file_name) = installed_bin_script.file_name() {
                    binaries.push(bin_script_file_name.into());
                }
            }
        }

        Ok(BuildInfo { binaries })
    }
}

async fn find_cargo_package_dir(
    config: &Config,
    source_root: &Path,
    package: &str,
) -> Result<PathBuf, RustBinaryError> {
    let output = config
        .wrapped_command("cargo", ["metadata", "--no-deps", "--format-version", "1"])
        .current_dir(source_root)
        .output()
        .await
        .map_err(|source| RustBinaryError::RustBuild { source })?;
    if !output.status.success() {
        return Err(RustBinaryError::CargoMetadata {
            status: output.status,
            stdout: String::from_utf8_lossy(&output.stdout).into(),
            stderr: String::from_utf8_lossy(&output.stderr).into(),
        });
    }
    let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)?;
    metadata
        .get("packages")
        .and_then(|packages| packages.as_array())
        .and_then(|packages| {
            packages
                .iter()
                .find(|pkg| pkg.get("name").and_then(|name| name.as_str()) == Some(package))
        })
        .and_then(|pkg| pkg.get("manifest_path").and_then(|path| path.as_str()))
        .and_then(|path| Path::new(path).parent())
        .map(Path::to_path_buf)
        .ok_or_else(|| RustBinaryError::CargoPackageNotFound {
            package: package.to_string(),
        })
}