Make standalone compilation more minimal for initial release, minor polish & fixes

This commit is contained in:
Filip Tibell 2024-01-13 21:10:57 +01:00
parent 55fe033f21
commit ddff5364b7
No known key found for this signature in database
5 changed files with 84 additions and 233 deletions

1
Cargo.lock generated
View file

@ -1126,7 +1126,6 @@ dependencies = [
"itertools",
"lz4_flex",
"mlua",
"num-traits",
"once_cell",
"os_str_bytes",
"path-clean",

View file

@ -110,7 +110,6 @@ tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
### DATETIME
chrono = "0.4"
chrono_lc = "0.1"
num-traits = "0.2"
### CLI

View file

@ -1,78 +1,64 @@
use console::Style;
use std::{env, path::Path, process::ExitCode};
use tokio::{
fs::{self, OpenOptions},
io::AsyncWriteExt,
};
use anyhow::Result;
use console::style;
use mlua::Compiler as LuaCompiler;
use tokio::{fs, io::AsyncWriteExt as _};
use crate::executor::{MetaChunk, MAGIC};
use crate::executor::MetaChunk;
/**
Compiles and embeds the bytecode of a requested lua file to form a standalone binary,
then writes it to an output file, with the required permissions.
Compiles and embeds the bytecode of a given lua file to form a standalone
binary, then writes it to an output file, with the required permissions.
*/
#[allow(clippy::similar_names)]
pub async fn build_standalone<T: AsRef<Path>>(
script_path: String,
output_path: T,
code: impl AsRef<[u8]>,
pub async fn build_standalone(
input_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
source_code: impl AsRef<[u8]>,
) -> Result<ExitCode> {
let log_output_path = output_path.as_ref().display();
let prefix_style = Style::new().green().bold();
let compile_prefix = prefix_style.apply_to("Compile");
let bytecode_prefix = prefix_style.apply_to("Bytecode");
let write_prefix = prefix_style.apply_to("Write");
let compiled_prefix = prefix_style.apply_to("Compiled");
println!("{compile_prefix} {script_path}");
let input_path_displayed = input_path.as_ref().display();
let output_path_displayed = output_path.as_ref().display();
// First, we read the contents of the lune interpreter as our starting point
println!(
"Creating standalone binary using {}",
style(input_path_displayed).green()
);
let mut patched_bin = fs::read(env::current_exe()?).await?;
let base_bin_offset = u64::try_from(patched_bin.len())?;
// Compile luau input into bytecode
let bytecode = LuaCompiler::new()
.set_optimization_level(2)
.set_coverage_level(0)
.set_debug_level(0)
.compile(code);
.set_debug_level(1)
.compile(source_code);
println!(" {bytecode_prefix} {script_path}");
// Append the bytecode / metadata to the end
let meta = MetaChunk { bytecode };
patched_bin.extend_from_slice(&meta.to_bytes());
patched_bin.extend(&bytecode);
let meta = MetaChunk::new()
.with_bytecode(bytecode)
.with_bytecode_offset(base_bin_offset)
.with_file_count(1_u64); // Start with the base bytecode offset
// Include metadata in the META chunk, each field is 8 bytes
patched_bin.extend(meta.build("little"));
// Append the magic signature to the base binary
patched_bin.extend(MAGIC);
// Write the compiled binary to file
#[cfg(target_family = "unix")]
OpenOptions::new()
.write(true)
.create(true)
.mode(0o770) // read, write and execute permissions for user and group
.open(&output_path)
.await?
.write_all(&patched_bin)
.await?;
#[cfg(target_family = "windows")]
fs::write(&output_path, &patched_bin).await?;
println!(" {write_prefix} {log_output_path}");
println!("{compiled_prefix} {log_output_path}");
// And finally write the patched binary to the output file
println!(
"Writing standalone binary to {}",
style(output_path_displayed).blue()
);
write_executable_file_to(output_path, patched_bin).await?;
Ok(ExitCode::SUCCESS)
}
async fn write_executable_file_to(path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) -> Result<()> {
let mut options = fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
options.mode(0o755); // Read & execute for all, write for owner
}
let mut file = options.open(path).await?;
file.write_all(bytes.as_ref()).await?;
Ok(())
}

View file

@ -180,8 +180,6 @@ impl Cli {
let output_path =
PathBuf::from(script_path.clone()).with_extension(env::consts::EXE_EXTENSION);
println!("Building {script_path} to {}...\n", output_path.display());
return Ok(
match build_standalone(script_path, output_path, script_contents).await {
Ok(exitcode) => exitcode,

View file

@ -1,207 +1,76 @@
use std::{env, ops::ControlFlow, process::ExitCode};
use std::{env, process::ExitCode};
use lune::Lune;
use anyhow::Result;
use num_traits::{FromBytes, ToBytes};
use tokio::fs::read as read_to_vec;
use anyhow::{bail, Result};
use tokio::fs;
// The signature which separates indicates the presence of bytecode to execute
// If a binary contains this magic signature as the last 8 bytes, that must mean
// it is a standalone binary
pub const MAGIC: &[u8; 8] = b"cr3sc3nt";
const MAGIC: &[u8; 8] = b"cr3sc3nt";
/// Utility struct to parse and generate bytes to the META chunk of standalone binaries.
/**
Metadata for a standalone Lune executable. Can be used to
discover and load the bytecode contained in a standalone binary.
*/
#[derive(Debug, Clone)]
pub struct MetaChunk {
/// Compiled lua bytecode of the entrypoint script.
pub bytecode: Vec<u8>,
/// Offset to the the beginning of the bytecode from the start of the lune binary.
pub bytecode_offset: Option<u64>,
/// Number of files present, currently unused. **For future use**.
pub file_count: Option<u64>,
}
impl MetaChunk {
/// Creates an emtpy `MetaChunk` instance.
pub fn new() -> Self {
Self {
bytecode: Vec::new(),
bytecode_offset: None,
file_count: None,
/**
Tries to read a standalone binary from the given bytes.
*/
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
let bytes = bytes.as_ref();
if bytes.len() < 16 || !bytes.ends_with(MAGIC) {
bail!("not a standalone binary")
}
// Extract bytecode size
let bytecode_size_bytes = &bytes[bytes.len() - 16..bytes.len() - 8];
let bytecode_size =
usize::try_from(u64::from_be_bytes(bytecode_size_bytes.try_into().unwrap()))?;
// Extract bytecode
let bytecode = bytes[bytes.len() - 16 - bytecode_size..].to_vec();
Ok(Self { bytecode })
}
/// Builder method to include the bytecode, **mandatory** before build.
pub fn with_bytecode(&mut self, bytecode: Vec<u8>) -> Self {
self.bytecode = bytecode;
self.clone()
}
/// Builder method to include the bytecode offset, **mandatory** before build.
pub fn with_bytecode_offset(&mut self, offset: u64) -> Self {
self.bytecode_offset = Some(offset);
self.clone()
}
/// Builder method to include the file count, **mandatory** before build.
pub fn with_file_count(&mut self, count: u64) -> Self {
self.file_count = Some(count);
self.clone()
}
/// Builds the final `Vec` of bytes, based on the endianness specified.
pub fn build(self, endianness: &str) -> Vec<u8> {
match endianness {
"big" => self.to_be_bytes(),
"little" => self.to_le_bytes(),
&_ => panic!("unexpected endianness"),
}
}
/// Internal method which implements endian independent bytecode discovery logic.
fn from_bytes(bytes: &[u8], int_handler: fn([u8; 8]) -> u64) -> Result<Self> {
let mut bytecode_offset = 0;
let mut bytecode_size = 0;
// standalone binary structure (reversed, 8 bytes per field)
// [0] => magic signature
// ----------------
// -- META Chunk --
// [1] => file count
// [2] => bytecode size
// [3] => bytecode offset
// ----------------
// -- MISC Chunk --
// [4..n] => bytecode (variable size)
// ----------------
// NOTE: All integers are 8 byte, padded, unsigned & 64 bit (u64's).
// The rchunks will have unequally sized sections in the beginning
// but that doesn't matter to us because we don't need anything past the
// middle chunks where the bytecode is stored
/**
Writes the metadata chunk to a byte vector, to later bet read using `from_bytes`.
*/
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.bytecode);
bytes.extend_from_slice(&(self.bytecode.len() as u64).to_be_bytes());
bytes.extend_from_slice(MAGIC);
bytes
.rchunks(MAGIC.len())
.enumerate()
.try_for_each(|(idx, chunk)| {
if bytecode_offset != 0 && bytecode_size != 0 {
return ControlFlow::Break(());
}
if idx == 0 && chunk != MAGIC {
// Binary is guaranteed to be standalone, we've confirmed this before
unreachable!("expected proper magic signature for standalone binary")
}
if idx == 3 {
bytecode_offset = int_handler(chunk.try_into().unwrap());
}
if idx == 2 {
bytecode_size = int_handler(chunk.try_into().unwrap());
}
ControlFlow::Continue(())
});
Ok(Self {
bytecode: bytes[usize::try_from(bytecode_offset)?
..usize::try_from(bytecode_offset + bytecode_size)?]
.to_vec(),
bytecode_offset: Some(bytecode_offset),
file_count: Some(1),
})
}
}
impl Default for MetaChunk {
fn default() -> Self {
Self {
bytecode: Vec::new(),
bytecode_offset: Some(0),
file_count: Some(1),
}
}
}
impl ToBytes for MetaChunk {
type Bytes = Vec<u8>;
fn to_be_bytes(&self) -> Self::Bytes {
// We start with the bytecode offset as the first field already filled in
let mut tmp = self.bytecode_offset.unwrap().to_be_bytes().to_vec();
// NOTE: The order of the fields here are reversed, which is on purpose
tmp.extend(self.bytecode.len().to_be_bytes());
tmp.extend(self.file_count.unwrap().to_be_bytes());
tmp
}
fn to_le_bytes(&self) -> Self::Bytes {
// We start with the bytecode offset as the first field already filled in
let mut tmp = self.bytecode_offset.unwrap().to_le_bytes().to_vec();
// NOTE: The order of the fields here are reversed, which is on purpose
tmp.extend(self.bytecode.len().to_le_bytes());
tmp.extend(self.file_count.unwrap().to_le_bytes());
tmp
}
}
impl FromBytes for MetaChunk {
type Bytes = Vec<u8>;
fn from_be_bytes(bytes: &Self::Bytes) -> Self {
Self::from_bytes(bytes, u64::from_be_bytes).unwrap()
}
fn from_le_bytes(bytes: &Self::Bytes) -> Self {
Self::from_bytes(bytes, u64::from_le_bytes).unwrap()
}
}
/**
Returns information about whether the execution environment is standalone
or not, the standalone binary signature, and the contents of the binary.
Returns whether or not the currently executing Lune binary
is a standalone binary, and if so, the bytes of the binary.
*/
pub async fn check_env() -> (bool, Vec<u8>) {
// Read the current lune binary to memory
let bin = if let Ok(contents) = read_to_vec(
env::current_exe().expect("failed to get path to current running lune executable"),
)
.await
{
contents
} else {
Vec::new()
};
let is_standalone =
!bin.is_empty() && bin[bin.len() - MAGIC.len()..bin.len()] == MAGIC.to_vec();
(is_standalone, bin)
let path = env::current_exe().expect("failed to get path to current running lune executable");
let contents = fs::read(path).await.unwrap_or_default();
let is_standalone = contents.ends_with(MAGIC);
(is_standalone, contents)
}
/**
Discovers, loads and executes the bytecode contained in a standalone binary.
*/
pub async fn run_standalone(bin: Vec<u8>) -> Result<ExitCode> {
// If we were able to retrieve the required metadata, we load
// and execute the bytecode
let MetaChunk { bytecode, .. } = MetaChunk::from_le_bytes(&bin);
// Skip the first argument which is the path to current executable
pub async fn run_standalone(patched_bin: impl AsRef<[u8]>) -> Result<ExitCode> {
// The first argument is the path to the current executable
let args = env::args().skip(1).collect::<Vec<_>>();
let meta = MetaChunk::from_bytes(patched_bin).expect("must be a standalone binary");
let result = Lune::new()
.with_args(args)
.run("STANDALONE", bytecode)
.run("STANDALONE", meta.bytecode)
.await;
Ok(match result {