diff --git a/Cargo.lock b/Cargo.lock index f5e29e6..1f8c6e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1126,7 +1126,6 @@ dependencies = [ "itertools", "lz4_flex", "mlua", - "num-traits", "once_cell", "os_str_bytes", "path-clean", diff --git a/Cargo.toml b/Cargo.toml index 6474e9d..e7531d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/src/cli/build.rs b/src/cli/build.rs new file mode 100644 index 0000000..998077a --- /dev/null +++ b/src/cli/build.rs @@ -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, + output_path: impl AsRef, + source_code: impl AsRef<[u8]>, +) -> Result { + 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, 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(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index cb7daee..581d9a9 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -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) diff --git a/src/executor.rs b/src/executor.rs new file mode 100644 index 0000000..0b6f3f0 --- /dev/null +++ b/src/executor.rs @@ -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, +} + +impl MetaChunk { + /** + Tries to read a standalone binary from the given bytes. + */ + pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result { + 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 { + 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) { + 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 { + // The first argument is the path to the current executable + let args = env::args().skip(1).collect::>(); + 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, + }) +} diff --git a/src/main.rs b/src/main.rs index cd13300..3cdb821 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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) => {