guartcl 0.6.0-beta1

Enhanced Jim Tcl.
Documentation
use std::{
    env::current_exe,
    fs, io,
    path::{Path, PathBuf},
};

use jimtcl::JimError;
use jimtcl::{Interp, JimObject, JimResult, args::ProcArgParser, tcl_error};
use log::*;
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub const TCL_SECTION: &str = "tcl_embedded";

/// Errors resulting from compilation.
#[derive(Error, Debug)]
pub enum CompileError {
    #[error("I/O error: {0}")]
    IO(#[from] io::Error),
    #[error("SUI error: {0}")]
    SUI(#[from] libsui::Error),
}

#[derive(Debug, Serialize, Deserialize)]
pub struct EmbeddedSources {
    pub name: String,
    pub source: String,
    pub options: EmbeddedOptions,
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct EmbeddedOptions {
    pub usage: bool,
}

impl EmbeddedSources {
    fn serialize(&self) -> JimResult<Vec<u8>> {
        serde_json::to_vec(self).map_err(|e| tcl_error!("encoding failed: {}", e))
    }
}

/// Compile TCL command.
pub(crate) fn compile_command(interp: &Interp, args: &[JimObject<'_>]) -> JimResult<PathBuf> {
    let mut parser = ProcArgParser::new();
    parser.add_flag("-usage");
    parser.add_option("-out", "exe");
    parser.add_required_arg("script");
    let args = parser.parse(interp, args)?;

    let exe = current_exe()?;

    let src = args.require_arg("script")?.as_str()?;
    let src = PathBuf::from(src);
    let dst: PathBuf = if let Some(out) = args.get_option("-out") {
        out.as_str()?.into()
    } else {
        src.with_extension("")
    };
    let name = src
        .file_name()
        .ok_or(tcl_error!("{}: no file name", src.display()))?
        .to_str()
        .ok_or_else(|| tcl_error!("{}: invalid UTF-8", src.display()))?;

    let source = fs::read_to_string(&src)?;
    let embed = EmbeddedSources {
        name: name.to_owned(),
        source,
        options: EmbeddedOptions {
            usage: args.get_flag("-usage"),
        },
    };

    info!("reading executable {}", exe.display());
    let image = fs::read(exe)?;
    if libsui::utils::is_macho(&image) {
        embed_tcl_macho(image, &embed, &dst).map_err(JimError::wrap)?;
    } else if libsui::utils::is_elf(&image) {
        embed_tcl_elf(image, &embed, &dst).map_err(JimError::wrap)?;
    } else {
        return Err(tcl_error!(
            "{}: unsupported executable format",
            src.display()
        ));
    }

    Ok(dst)
}

fn embed_tcl_macho(image: Vec<u8>, embed: &EmbeddedSources, dst: &Path) -> JimResult<()> {
    use std::os::unix::fs::OpenOptionsExt;

    let mach = libsui::Macho::from(image).map_err(|e| tcl_error!("Mach-O load failed: {}", e))?;

    info!("embedding {} into Mach-O executable", embed.name);
    debug!("embedding into Macho-O");
    let mach = mach
        .write_section(TCL_SECTION, embed.serialize()?)
        .map_err(|e| tcl_error!("Mach-O embed failed: {}", e))?;

    if dst.exists() {
        debug!("removing existing executable");
        std::fs::remove_file(dst)?;
    }
    let mut out = fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .mode(0o777)
        .open(dst)?;
    debug!("building and signing executable");
    mach.build_and_sign(&mut out)
        .map_err(|e| tcl_error!("Mach-O finalization failed: {}", e))?;
    Ok(())
}

fn embed_tcl_elf(image: Vec<u8>, embed: &EmbeddedSources, dst: &Path) -> JimResult<()> {
    use std::os::unix::fs::OpenOptionsExt;

    info!("embedding {} into Elf executable", embed.name);
    let elf = libsui::Elf::new(&image);

    let mut out = fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .mode(0o777)
        .open(dst)?;
    debug!("saving section");
    elf.append(TCL_SECTION, &embed.serialize()?, &mut out)
        .map_err(|e| tcl_error!("Elf embed failed: {}", e))?;

    Ok(())
}