Give standalone compilation its own dir, leave some future notes

This commit is contained in:
Filip Tibell 2024-01-14 11:14:27 +01:00
parent e5e83d1ad6
commit 0392c163a0
No known key found for this signature in database
6 changed files with 165 additions and 105 deletions

View file

@ -1,5 +1,5 @@
use std::{
env::{self, consts::EXE_EXTENSION},
env::consts::EXE_EXTENSION,
path::{Path, PathBuf},
process::ExitCode,
};
@ -7,10 +7,9 @@ use std::{
use anyhow::{Context, Result};
use clap::Parser;
use console::style;
use mlua::Compiler as LuaCompiler;
use tokio::{fs, io::AsyncWriteExt as _};
use crate::executor::MetaChunk;
use crate::standalone::metadata::Metadata;
/// Build a standalone executable
#[derive(Debug, Clone, Parser)]
@ -43,18 +42,9 @@ impl BuildCommand {
"Creating standalone binary using {}",
style(input_path_displayed).green()
);
let mut patched_bin = fs::read(env::current_exe()?).await?;
// Compile luau input into bytecode
let bytecode = LuaCompiler::new()
.set_optimization_level(2)
.set_coverage_level(0)
.set_debug_level(1)
.compile(source_code);
// Append the bytecode / metadata to the end
let meta = MetaChunk { bytecode };
patched_bin.extend_from_slice(&meta.to_bytes());
let patched_bin = Metadata::create_env_patched_bin(source_code.clone())
.await
.context("failed to create patched binary")?;
// And finally write the patched binary to the output file
println!(

View file

@ -1,83 +0,0 @@
use std::{env, process::ExitCode};
use lune::Runtime;
use anyhow::{bail, Result};
use tokio::fs;
const MAGIC: &[u8; 8] = b"cr3sc3nt";
/**
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 {
pub bytecode: Vec<u8>,
}
impl MetaChunk {
/**
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 })
}
/**
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
}
}
/**
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>) {
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(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 = Runtime::new()
.with_args(args)
.run("STANDALONE", meta.bytecode)
.await;
Ok(match result {
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
Ok(code) => code,
})
}

View file

@ -11,7 +11,7 @@
use std::process::ExitCode;
pub(crate) mod cli;
pub(crate) mod executor;
pub(crate) mod standalone;
use cli::Cli;
use console::style;
@ -26,12 +26,8 @@ async fn main() -> ExitCode {
.with_level(true)
.init();
let (is_standalone, bin) = executor::check_env().await;
if is_standalone {
// It's fine to unwrap here since we don't want to continue
// if something fails
return executor::run_standalone(bin).await.unwrap();
if let Some(bin) = standalone::check().await {
return standalone::run(bin).await.unwrap();
}
match Cli::new().run().await {

View file

@ -0,0 +1,99 @@
use std::{env, path::PathBuf};
use anyhow::{bail, Result};
use mlua::Compiler as LuaCompiler;
use once_cell::sync::Lazy;
use tokio::fs;
const MAGIC: &[u8; 8] = b"cr3sc3nt";
static CURRENT_EXE: Lazy<PathBuf> =
Lazy::new(|| env::current_exe().expect("failed to get current exe"));
/*
TODO: Right now all we do is append the bytecode to the end
of the binary, but we will need a more flexible solution in
the future to store many files as well as their metadata.
The best solution here is most likely to use a well-supported
and rust-native binary serialization format with a stable
specification, one that also supports byte arrays well without
overhead, so the best solution seems to currently be Postcard:
https://github.com/jamesmunns/postcard
https://crates.io/crates/postcard
*/
/**
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 Metadata {
pub bytecode: Vec<u8>,
}
impl Metadata {
/**
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>) {
let contents = fs::read(CURRENT_EXE.to_path_buf())
.await
.unwrap_or_default();
let is_standalone = contents.ends_with(MAGIC);
(is_standalone, contents)
}
/**
Creates a patched standalone binary from the given script contents.
*/
pub async fn create_env_patched_bin(script_contents: impl Into<Vec<u8>>) -> Result<Vec<u8>> {
let mut patched_bin = fs::read(CURRENT_EXE.to_path_buf()).await?;
// Compile luau input into bytecode
let bytecode = LuaCompiler::new()
.set_optimization_level(2)
.set_coverage_level(0)
.set_debug_level(1)
.compile(script_contents.into());
// Append the bytecode / metadata to the end
let meta = Self { bytecode };
patched_bin.extend_from_slice(&meta.to_bytes());
Ok(patched_bin)
}
/**
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 })
}
/**
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
}
}

44
src/standalone/mod.rs Normal file
View file

@ -0,0 +1,44 @@
use std::{env, process::ExitCode};
use anyhow::Result;
use lune::Runtime;
pub(crate) mod metadata;
pub(crate) mod tracer;
use self::metadata::Metadata;
/**
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() -> Option<Vec<u8>> {
let (is_standalone, patched_bin) = Metadata::check_env().await;
if is_standalone {
Some(patched_bin)
} else {
None
}
}
/**
Discovers, loads and executes the bytecode contained in a standalone binary.
*/
pub async fn run(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 = Metadata::from_bytes(patched_bin).expect("must be a standalone binary");
let result = Runtime::new()
.with_args(args)
.run("STANDALONE", meta.bytecode)
.await;
Ok(match result {
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
Ok(code) => code,
})
}

14
src/standalone/tracer.rs Normal file
View file

@ -0,0 +1,14 @@
/*
TODO: Implement tracing of requires here
Rough steps / outline:
1. Create a new tracer struct using a main entrypoint script path
2. Some kind of discovery mechanism that goes through all require chains (failing on recursive ones)
2a. Conversion of script-relative paths to cwd-relative paths + normalization
2b. Cache all found files in a map of file path -> file contents
2c. Prepend some kind of symbol to paths that can tell our runtime `require` function that it
should look up a bundled/standalone script, a good symbol here is probably a dollar sign ($)
3. ???
4. Profit
*/