Implement standalone executable compilation (#140)

This commit is contained in:
Erica Marigold 2024-01-14 01:49:59 +05:30 committed by GitHub
parent 5040deddb6
commit 6f8b1e4896
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 186 additions and 5 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

64
src/cli/build.rs Normal file
View file

@ -0,0 +1,64 @@
use std::{env, path::Path, process::ExitCode};
use anyhow::Result;
use console::style;
use mlua::Compiler as LuaCompiler;
use tokio::{fs, io::AsyncWriteExt as _};
use crate::executor::MetaChunk;
/**
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(
input_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
source_code: impl AsRef<[u8]>,
) -> Result<ExitCode> {
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?;
// 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());
// 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

@ -1,4 +1,4 @@
use std::{fmt::Write as _, process::ExitCode};
use std::{env, fmt::Write as _, path::PathBuf, process::ExitCode};
use anyhow::{Context, Result};
use clap::Parser;
@ -9,6 +9,7 @@ use tokio::{
io::{stdin, AsyncReadExt},
};
pub(crate) mod build;
pub(crate) mod gen;
pub(crate) mod repl;
pub(crate) mod setup;
@ -20,6 +21,8 @@ use utils::{
listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list},
};
use self::build::build_standalone;
/// A Luau script runner
#[derive(Parser, Debug, Default, Clone)]
#[command(version, long_about = None)]
@ -44,6 +47,9 @@ pub struct Cli {
/// Generate a Lune documentation file for Luau LSP
#[clap(long, hide = true)]
generate_docs_file: bool,
/// Build a Luau file to an OS-Native standalone executable
#[clap(long)]
build: bool,
}
#[allow(dead_code)]
@ -116,6 +122,7 @@ impl Cli {
return Ok(ExitCode::SUCCESS);
}
// Generate (save) definition files, if wanted
let generate_file_requested = self.setup
|| self.generate_luau_types
@ -143,14 +150,17 @@ impl Cli {
if generate_file_requested {
return Ok(ExitCode::SUCCESS);
}
// If we did not generate any typedefs we know that the user did not
// provide any other options, and in that case we should enter the REPL
// If not in a standalone context and we don't have any arguments
// display the interactive REPL interface
return repl::show_interface().await;
}
// Figure out if we should read from stdin or from a file,
// reading from stdin is marked by passing a single "-"
// (dash) as the script name to run to the cli
let script_path = self.script_path.unwrap();
let (script_display_name, script_contents) = if script_path == "-" {
let mut stdin_contents = Vec::new();
stdin()
@ -165,6 +175,22 @@ impl Cli {
let file_display_name = file_path.with_extension("").display().to_string();
(file_display_name, file_contents)
};
if self.build {
let output_path =
PathBuf::from(script_path.clone()).with_extension(env::consts::EXE_EXTENSION);
return Ok(
match build_standalone(script_path, output_path, script_contents).await {
Ok(exitcode) => exitcode,
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
},
);
}
// Create a new lune object with all globals & run the script
let result = Lune::new()
.with_args(self.script_args)

83
src/executor.rs Normal file
View file

@ -0,0 +1,83 @@
use std::{env, process::ExitCode};
use lune::Lune;
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 = Lune::new()
.with_args(args)
.run("STANDALONE", meta.bytecode)
.await;
Ok(match result {
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
Ok(code) => code,
})
}

View file

@ -13,6 +13,7 @@ use std::process::ExitCode;
use clap::Parser;
pub(crate) mod cli;
pub(crate) mod executor;
use cli::Cli;
use console::style;
@ -26,6 +27,15 @@ async fn main() -> ExitCode {
.with_timer(tracing_subscriber::fmt::time::uptime())
.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();
}
match Cli::parse().run().await {
Ok(code) => code,
Err(err) => {