bile 0.1.0

A simple build system, structured around Lua scripts.
//! Structure for gathering build steps from a build.lua file.

use std::collections::VecDeque;
use std::fmt::{Display, Error as FmtError};
use std::io::Error as IoError;
use std::sync::Arc;
use std::{error, process};

use clap::Command;
use mlua::{Error as LuaError, FromLua, Function, Lua, Table, UserData, UserDataMethods, Value};

/// A structure for building and collecting build steps.
#[derive(Debug, Clone)]
pub struct Module {
    /// Description for the command.
    pub description: Option<String>,
    /// Name of the subcommand.
    pub name: String,
    steps: VecDeque<Step>,
}

impl Module {
    /// Creates a [`Command`] from this [`Module`].
    #[must_use]
    pub fn make_command(&self) -> Command {
        self.description.as_ref().map_or_else(
            || Command::new(&self.name),
            |description| Command::new(&self.name).about(description),
        )
    }

    /// Creates an empty [`Module`].
    #[must_use]
    pub const fn new() -> Self {
        Self {
            description: None,
            name: String::new(),
            steps: VecDeque::new(),
        }
    }

    /// Creates a new [`Module`] with the provided name.
    #[must_use]
    pub fn with_name(name: String) -> Self {
        Self {
            name,
            ..Self::new()
        }
    }

    /// Extend this [`Module`] by applying the lua [function] `func` to it.
    ///
    /// # Errors
    ///
    /// This function will return an error if the called Lua function doesn't
    /// successfully execute, or returns something that isn't a [`Module`].
    ///
    /// [function]: `mlua::Function`
    pub fn apply_through_lua(self, func: &Function) -> Result<Self, LuaError> {
        func.call::<Self>(self)
    }

    /// Adds a shell script [step] to this `Module`.
    ///
    /// [step]: `Step`
    pub fn add_sh_step(&mut self, sh_str: String) {
        self.steps.push_front(Step::Sh(sh_str));
    }

    /// Sets the description of this [`Module`].
    pub fn set_description(&mut self, description: String) {
        self.description = Some(description);
    }

    /// Like [`Self::set_description`], but it can only be called once.
    pub fn provide_description(&mut self, description: String) -> bool {
        if self.description.is_none() {
            self.description = Some(description);
            true
        } else {
            false
        }
    }

    /// Executes every [`Step`] from `steps`. This function is very effectful, be wary.
    ///
    /// # Errors
    ///
    /// See the individual error sections for each [`Step`].
    pub fn execute(self) -> Result<(), IoError> {
        for step in self.steps {
            step.execute()?;
        }
        Ok(())
    }
}

impl Default for Module {
    fn default() -> Self {
        Self::new()
    }
}

/// Error for when casting to a [`Module`] from a [`mlua::Value`] fails.
///
/// Contains the value which the cast failed on.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct ModuleFromLuaError(pub &'static str);

impl From<Value> for ModuleFromLuaError {
    fn from(found: Value) -> Self {
        Self(found.type_name())
    }
}

impl error::Error for ModuleFromLuaError {}
impl Display for ModuleFromLuaError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), FmtError> {
        write!(f, "Expected a builder.Module found a {}", self.0)
    }
}

impl FromLua for Module {
    fn from_lua(value: Value, _lua: &Lua) -> Result<Self, LuaError> {
        match value {
            Value::UserData(any_user_data) => any_user_data.take(),
            found => Err(LuaError::ExternalError(Arc::new(ModuleFromLuaError(
                found.type_name(),
            )))),
        }
    }
}

impl UserData for Module {
    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
        methods.add_method_mut("add_sh_step", |_lua, module, shell: String| {
            module.add_sh_step(shell);
            Ok(())
        });
        methods.add_method_mut(
            "provide_description",
            |_lua, module, description: String| {
                module
                    .provide_description(description)
                    .ok_or(LuaError::external(
                        "Called provide_description on a Module with a description",
                    ))
            },
        );
    }
}

/// A build step, such as running a shell script or moving a file.
#[derive(Debug, Clone)]
pub enum Step {
    /// Exeuction of a shell script.
    Sh(String),
}

impl Step {
    /// Executes this [`Step`].
    ///
    /// # Errors
    ///
    /// Very contextual to the type of step.
    pub fn execute(&self) -> Result<(), IoError> {
        match self {
            Self::Sh(script) => {
                let _exit = process::Command::new("sh")
                    .arg("-c")
                    .arg(script)
                    .spawn()?
                    .wait()?;
                // TODO: Handle the exit code
                Ok(())
            }
        }
    }
}

/// Collects the module outputs from a Lua [table] of modules.
///
/// > Note: The output is sorted by the `Module`'s `name` fields.
///
/// [table]: `Table`
pub fn collect_modules_from_table(table: &Table) -> Vec<Module> {
    fn collect_module((key, func): (String, Function)) -> Option<Module> {
        func.call::<Module>(Module::with_name(key)).ok()
    }

    // ISSUE: Fails silently on a malformed module
    let mut res = table
        .pairs::<String, Function>()
        .flatten()
        .filter_map(collect_module)
        .collect::<Vec<Module>>();

    res.sort_by(|a, b| a.name.cmp(&b.name));

    res
}