Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone Compiler #140

Merged
merged 25 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5d58ba7
feat: initial standalone executable builder impl
CompeyDev Nov 20, 2023
dc2bab3
feat: initial unfinished rewrite for perf
CompeyDev Nov 21, 2023
441a1ea
fix: finalize updated standalone runtime system
CompeyDev Nov 22, 2023
2bf68c1
refactor: polish a few things and clean up code
CompeyDev Nov 22, 2023
4bb0eba
feat: disable unneeded CLI args for standalone
CompeyDev Nov 22, 2023
6087069
fix(windows): write file differently for windows
CompeyDev Nov 22, 2023
cf2f93d
fix: conditionally compile fs writing system for windows
CompeyDev Nov 23, 2023
2af8ed3
feat: proper args support for standalone binaries
CompeyDev Nov 23, 2023
1e43f70
feat: SUPER fast standalone binaries using jemalloc & rayon
CompeyDev Nov 23, 2023
9c615ad
refactor: cleanup code & include logging
CompeyDev Jan 2, 2024
207de2f
Merge branch 'lune-org:main' into feat/standalone-executable
CompeyDev Jan 2, 2024
a5d118d
fix: improper trait bounds
CompeyDev Jan 4, 2024
5f68fee
fix: add rustdoc comments for build arg
CompeyDev Jan 4, 2024
75152bd
fix: panic on failure to get current exe
CompeyDev Jan 4, 2024
53b53a2
fix: avoid collecting to unneeded VecDequeue
CompeyDev Jan 4, 2024
6f4b2f4
fix: remove redundant multi-threading code
CompeyDev Jan 4, 2024
94b27d8
fix: make build option visible to user
CompeyDev Jan 4, 2024
6f3d11c
Merge branch 'lune-org:main' into feat/standalone-executable
CompeyDev Jan 5, 2024
3c2464d
feat: store magic signature as a meaningful constant
CompeyDev Jan 5, 2024
e425581
Merge branch 'feat/standalone-executable' of https://github.com/0x5ea…
CompeyDev Jan 5, 2024
35c5a3c
fix: use fixed (little) endianness
CompeyDev Jan 5, 2024
b071db3
feat: initial META chunk (de)serialization impl
CompeyDev Jan 5, 2024
94fd549
refactor: impl discovery logic as trait
CompeyDev Jan 13, 2024
55fe033
refactor: move most shared logic to executor.rs
CompeyDev Jan 13, 2024
ddff536
Make standalone compilation more minimal for initial release, minor p…
filiptibell Jan 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
64 changes: 64 additions & 0 deletions src/cli/build.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
32 changes: 29 additions & 3 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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)]
Expand All @@ -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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down
83 changes: 83 additions & 0 deletions src/executor.rs
Original file line number Diff line number Diff line change
@@ -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,
})
}
10 changes: 10 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) => {
Expand Down