mirror of
https://github.com/lune-org/lune.git
synced 2025-04-19 19:34:02 +01:00
Make standalone compilation more minimal for initial release, minor polish & fixes
This commit is contained in:
parent
55fe033f21
commit
ddff5364b7
5 changed files with 84 additions and 233 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1126,7 +1126,6 @@ dependencies = [
|
||||||
"itertools",
|
"itertools",
|
||||||
"lz4_flex",
|
"lz4_flex",
|
||||||
"mlua",
|
"mlua",
|
||||||
"num-traits",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"os_str_bytes",
|
"os_str_bytes",
|
||||||
"path-clean",
|
"path-clean",
|
||||||
|
|
|
@ -110,7 +110,6 @@ tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
|
||||||
### DATETIME
|
### DATETIME
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono_lc = "0.1"
|
chrono_lc = "0.1"
|
||||||
num-traits = "0.2"
|
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
|
|
|
@ -1,78 +1,64 @@
|
||||||
use console::Style;
|
|
||||||
use std::{env, path::Path, process::ExitCode};
|
use std::{env, path::Path, process::ExitCode};
|
||||||
use tokio::{
|
|
||||||
fs::{self, OpenOptions},
|
|
||||||
io::AsyncWriteExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use console::style;
|
||||||
use mlua::Compiler as LuaCompiler;
|
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,
|
Compiles and embeds the bytecode of a given lua file to form a standalone
|
||||||
then writes it to an output file, with the required permissions.
|
binary, then writes it to an output file, with the required permissions.
|
||||||
*/
|
*/
|
||||||
#[allow(clippy::similar_names)]
|
#[allow(clippy::similar_names)]
|
||||||
pub async fn build_standalone<T: AsRef<Path>>(
|
pub async fn build_standalone(
|
||||||
script_path: String,
|
input_path: impl AsRef<Path>,
|
||||||
output_path: T,
|
output_path: impl AsRef<Path>,
|
||||||
code: impl AsRef<[u8]>,
|
source_code: impl AsRef<[u8]>,
|
||||||
) -> Result<ExitCode> {
|
) -> Result<ExitCode> {
|
||||||
let log_output_path = output_path.as_ref().display();
|
let input_path_displayed = input_path.as_ref().display();
|
||||||
|
let output_path_displayed = 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}");
|
|
||||||
|
|
||||||
// First, we read the contents of the lune interpreter as our starting point
|
// 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 mut patched_bin = fs::read(env::current_exe()?).await?;
|
||||||
let base_bin_offset = u64::try_from(patched_bin.len())?;
|
|
||||||
|
|
||||||
// Compile luau input into bytecode
|
// Compile luau input into bytecode
|
||||||
let bytecode = LuaCompiler::new()
|
let bytecode = LuaCompiler::new()
|
||||||
.set_optimization_level(2)
|
.set_optimization_level(2)
|
||||||
.set_coverage_level(0)
|
.set_coverage_level(0)
|
||||||
.set_debug_level(0)
|
.set_debug_level(1)
|
||||||
.compile(code);
|
.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);
|
// And finally write the patched binary to the output file
|
||||||
|
println!(
|
||||||
let meta = MetaChunk::new()
|
"Writing standalone binary to {}",
|
||||||
.with_bytecode(bytecode)
|
style(output_path_displayed).blue()
|
||||||
.with_bytecode_offset(base_bin_offset)
|
);
|
||||||
.with_file_count(1_u64); // Start with the base bytecode offset
|
write_executable_file_to(output_path, patched_bin).await?;
|
||||||
|
|
||||||
// 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}");
|
|
||||||
|
|
||||||
Ok(ExitCode::SUCCESS)
|
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(())
|
||||||
|
}
|
||||||
|
|
|
@ -180,8 +180,6 @@ impl Cli {
|
||||||
let output_path =
|
let output_path =
|
||||||
PathBuf::from(script_path.clone()).with_extension(env::consts::EXE_EXTENSION);
|
PathBuf::from(script_path.clone()).with_extension(env::consts::EXE_EXTENSION);
|
||||||
|
|
||||||
println!("Building {script_path} to {}...\n", output_path.display());
|
|
||||||
|
|
||||||
return Ok(
|
return Ok(
|
||||||
match build_standalone(script_path, output_path, script_contents).await {
|
match build_standalone(script_path, output_path, script_contents).await {
|
||||||
Ok(exitcode) => exitcode,
|
Ok(exitcode) => exitcode,
|
||||||
|
|
213
src/executor.rs
213
src/executor.rs
|
@ -1,207 +1,76 @@
|
||||||
use std::{env, ops::ControlFlow, process::ExitCode};
|
use std::{env, process::ExitCode};
|
||||||
|
|
||||||
use lune::Lune;
|
use lune::Lune;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Result};
|
||||||
use num_traits::{FromBytes, ToBytes};
|
use tokio::fs;
|
||||||
use tokio::fs::read as read_to_vec;
|
|
||||||
|
|
||||||
// The signature which separates indicates the presence of bytecode to execute
|
const MAGIC: &[u8; 8] = b"cr3sc3nt";
|
||||||
// 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";
|
|
||||||
|
|
||||||
/// 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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MetaChunk {
|
pub struct MetaChunk {
|
||||||
/// Compiled lua bytecode of the entrypoint script.
|
|
||||||
pub bytecode: Vec<u8>,
|
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 {
|
impl MetaChunk {
|
||||||
/// Creates an emtpy `MetaChunk` instance.
|
/**
|
||||||
pub fn new() -> Self {
|
Tries to read a standalone binary from the given bytes.
|
||||||
Self {
|
*/
|
||||||
bytecode: Vec::new(),
|
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
|
||||||
bytecode_offset: None,
|
let bytes = bytes.as_ref();
|
||||||
file_count: None,
|
if bytes.len() < 16 || !bytes.ends_with(MAGIC) {
|
||||||
}
|
bail!("not a standalone binary")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder method to include the bytecode, **mandatory** before build.
|
// Extract bytecode size
|
||||||
pub fn with_bytecode(&mut self, bytecode: Vec<u8>) -> Self {
|
let bytecode_size_bytes = &bytes[bytes.len() - 16..bytes.len() - 8];
|
||||||
self.bytecode = bytecode;
|
let bytecode_size =
|
||||||
|
usize::try_from(u64::from_be_bytes(bytecode_size_bytes.try_into().unwrap()))?;
|
||||||
|
|
||||||
self.clone()
|
// Extract bytecode
|
||||||
|
let bytecode = bytes[bytes.len() - 16 - bytecode_size..].to_vec();
|
||||||
|
|
||||||
|
Ok(Self { bytecode })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder method to include the bytecode offset, **mandatory** before build.
|
/**
|
||||||
pub fn with_bytecode_offset(&mut self, offset: u64) -> Self {
|
Writes the metadata chunk to a byte vector, to later bet read using `from_bytes`.
|
||||||
self.bytecode_offset = Some(offset);
|
*/
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
self.clone()
|
let mut bytes = Vec::new();
|
||||||
}
|
bytes.extend_from_slice(&self.bytecode);
|
||||||
|
bytes.extend_from_slice(&(self.bytecode.len() as u64).to_be_bytes());
|
||||||
/// Builder method to include the file count, **mandatory** before build.
|
bytes.extend_from_slice(MAGIC);
|
||||||
|
|
||||||
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
|
|
||||||
bytes
|
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
|
Returns whether or not the currently executing Lune binary
|
||||||
or not, the standalone binary signature, and the contents of the binary.
|
is a standalone binary, and if so, the bytes of the binary.
|
||||||
*/
|
*/
|
||||||
pub async fn check_env() -> (bool, Vec<u8>) {
|
pub async fn check_env() -> (bool, Vec<u8>) {
|
||||||
// Read the current lune binary to memory
|
let path = env::current_exe().expect("failed to get path to current running lune executable");
|
||||||
let bin = if let Ok(contents) = read_to_vec(
|
let contents = fs::read(path).await.unwrap_or_default();
|
||||||
env::current_exe().expect("failed to get path to current running lune executable"),
|
let is_standalone = contents.ends_with(MAGIC);
|
||||||
)
|
(is_standalone, contents)
|
||||||
.await
|
|
||||||
{
|
|
||||||
contents
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_standalone =
|
|
||||||
!bin.is_empty() && bin[bin.len() - MAGIC.len()..bin.len()] == MAGIC.to_vec();
|
|
||||||
|
|
||||||
(is_standalone, bin)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Discovers, loads and executes the bytecode contained in a standalone binary.
|
Discovers, loads and executes the bytecode contained in a standalone binary.
|
||||||
*/
|
*/
|
||||||
pub async fn run_standalone(bin: Vec<u8>) -> Result<ExitCode> {
|
pub async fn run_standalone(patched_bin: impl AsRef<[u8]>) -> Result<ExitCode> {
|
||||||
// If we were able to retrieve the required metadata, we load
|
// The first argument is the path to the current executable
|
||||||
// and execute the bytecode
|
|
||||||
let MetaChunk { bytecode, .. } = MetaChunk::from_le_bytes(&bin);
|
|
||||||
|
|
||||||
// Skip the first argument which is the path to current executable
|
|
||||||
let args = env::args().skip(1).collect::<Vec<_>>();
|
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()
|
let result = Lune::new()
|
||||||
.with_args(args)
|
.with_args(args)
|
||||||
.run("STANDALONE", bytecode)
|
.run("STANDALONE", meta.bytecode)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(match result {
|
Ok(match result {
|
||||||
|
|
Loading…
Add table
Reference in a new issue