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";
#[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))
}
}
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(())
}