Skip to content

Commit 6f8b1e4

Browse files
authoredJan 13, 2024
Implement standalone executable compilation (#140)
1 parent 5040ded commit 6f8b1e4

File tree

6 files changed

+186
-5
lines changed

6 files changed

+186
-5
lines changed
 

‎Cargo.lock

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
110110
### DATETIME
111111
chrono = "0.4"
112112
chrono_lc = "0.1"
113-
num-traits = "0.2"
114113

115114
### CLI
116115

‎src/cli/build.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::{env, path::Path, process::ExitCode};
2+
3+
use anyhow::Result;
4+
use console::style;
5+
use mlua::Compiler as LuaCompiler;
6+
use tokio::{fs, io::AsyncWriteExt as _};
7+
8+
use crate::executor::MetaChunk;
9+
10+
/**
11+
Compiles and embeds the bytecode of a given lua file to form a standalone
12+
binary, then writes it to an output file, with the required permissions.
13+
*/
14+
#[allow(clippy::similar_names)]
15+
pub async fn build_standalone(
16+
input_path: impl AsRef<Path>,
17+
output_path: impl AsRef<Path>,
18+
source_code: impl AsRef<[u8]>,
19+
) -> Result<ExitCode> {
20+
let input_path_displayed = input_path.as_ref().display();
21+
let output_path_displayed = output_path.as_ref().display();
22+
23+
// First, we read the contents of the lune interpreter as our starting point
24+
println!(
25+
"Creating standalone binary using {}",
26+
style(input_path_displayed).green()
27+
);
28+
let mut patched_bin = fs::read(env::current_exe()?).await?;
29+
30+
// Compile luau input into bytecode
31+
let bytecode = LuaCompiler::new()
32+
.set_optimization_level(2)
33+
.set_coverage_level(0)
34+
.set_debug_level(1)
35+
.compile(source_code);
36+
37+
// Append the bytecode / metadata to the end
38+
let meta = MetaChunk { bytecode };
39+
patched_bin.extend_from_slice(&meta.to_bytes());
40+
41+
// And finally write the patched binary to the output file
42+
println!(
43+
"Writing standalone binary to {}",
44+
style(output_path_displayed).blue()
45+
);
46+
write_executable_file_to(output_path, patched_bin).await?;
47+
48+
Ok(ExitCode::SUCCESS)
49+
}
50+
51+
async fn write_executable_file_to(path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) -> Result<()> {
52+
let mut options = fs::OpenOptions::new();
53+
options.write(true).create(true).truncate(true);
54+
55+
#[cfg(unix)]
56+
{
57+
options.mode(0o755); // Read & execute for all, write for owner
58+
}
59+
60+
let mut file = options.open(path).await?;
61+
file.write_all(bytes.as_ref()).await?;
62+
63+
Ok(())
64+
}

‎src/cli/mod.rs

+29-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{fmt::Write as _, process::ExitCode};
1+
use std::{env, fmt::Write as _, path::PathBuf, process::ExitCode};
22

33
use anyhow::{Context, Result};
44
use clap::Parser;
@@ -9,6 +9,7 @@ use tokio::{
99
io::{stdin, AsyncReadExt},
1010
};
1111

12+
pub(crate) mod build;
1213
pub(crate) mod gen;
1314
pub(crate) mod repl;
1415
pub(crate) mod setup;
@@ -20,6 +21,8 @@ use utils::{
2021
listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list},
2122
};
2223

24+
use self::build::build_standalone;
25+
2326
/// A Luau script runner
2427
#[derive(Parser, Debug, Default, Clone)]
2528
#[command(version, long_about = None)]
@@ -44,6 +47,9 @@ pub struct Cli {
4447
/// Generate a Lune documentation file for Luau LSP
4548
#[clap(long, hide = true)]
4649
generate_docs_file: bool,
50+
/// Build a Luau file to an OS-Native standalone executable
51+
#[clap(long)]
52+
build: bool,
4753
}
4854

4955
#[allow(dead_code)]
@@ -116,6 +122,7 @@ impl Cli {
116122

117123
return Ok(ExitCode::SUCCESS);
118124
}
125+
119126
// Generate (save) definition files, if wanted
120127
let generate_file_requested = self.setup
121128
|| self.generate_luau_types
@@ -143,14 +150,17 @@ impl Cli {
143150
if generate_file_requested {
144151
return Ok(ExitCode::SUCCESS);
145152
}
146-
// If we did not generate any typedefs we know that the user did not
147-
// provide any other options, and in that case we should enter the REPL
153+
154+
// If not in a standalone context and we don't have any arguments
155+
// display the interactive REPL interface
148156
return repl::show_interface().await;
149157
}
158+
150159
// Figure out if we should read from stdin or from a file,
151160
// reading from stdin is marked by passing a single "-"
152161
// (dash) as the script name to run to the cli
153162
let script_path = self.script_path.unwrap();
163+
154164
let (script_display_name, script_contents) = if script_path == "-" {
155165
let mut stdin_contents = Vec::new();
156166
stdin()
@@ -165,6 +175,22 @@ impl Cli {
165175
let file_display_name = file_path.with_extension("").display().to_string();
166176
(file_display_name, file_contents)
167177
};
178+
179+
if self.build {
180+
let output_path =
181+
PathBuf::from(script_path.clone()).with_extension(env::consts::EXE_EXTENSION);
182+
183+
return Ok(
184+
match build_standalone(script_path, output_path, script_contents).await {
185+
Ok(exitcode) => exitcode,
186+
Err(err) => {
187+
eprintln!("{err}");
188+
ExitCode::FAILURE
189+
}
190+
},
191+
);
192+
}
193+
168194
// Create a new lune object with all globals & run the script
169195
let result = Lune::new()
170196
.with_args(self.script_args)

‎src/executor.rs

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use std::{env, process::ExitCode};
2+
3+
use lune::Lune;
4+
5+
use anyhow::{bail, Result};
6+
use tokio::fs;
7+
8+
const MAGIC: &[u8; 8] = b"cr3sc3nt";
9+
10+
/**
11+
Metadata for a standalone Lune executable. Can be used to
12+
discover and load the bytecode contained in a standalone binary.
13+
*/
14+
#[derive(Debug, Clone)]
15+
pub struct MetaChunk {
16+
pub bytecode: Vec<u8>,
17+
}
18+
19+
impl MetaChunk {
20+
/**
21+
Tries to read a standalone binary from the given bytes.
22+
*/
23+
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
24+
let bytes = bytes.as_ref();
25+
if bytes.len() < 16 || !bytes.ends_with(MAGIC) {
26+
bail!("not a standalone binary")
27+
}
28+
29+
// Extract bytecode size
30+
let bytecode_size_bytes = &bytes[bytes.len() - 16..bytes.len() - 8];
31+
let bytecode_size =
32+
usize::try_from(u64::from_be_bytes(bytecode_size_bytes.try_into().unwrap()))?;
33+
34+
// Extract bytecode
35+
let bytecode = bytes[bytes.len() - 16 - bytecode_size..].to_vec();
36+
37+
Ok(Self { bytecode })
38+
}
39+
40+
/**
41+
Writes the metadata chunk to a byte vector, to later bet read using `from_bytes`.
42+
*/
43+
pub fn to_bytes(&self) -> Vec<u8> {
44+
let mut bytes = Vec::new();
45+
bytes.extend_from_slice(&self.bytecode);
46+
bytes.extend_from_slice(&(self.bytecode.len() as u64).to_be_bytes());
47+
bytes.extend_from_slice(MAGIC);
48+
bytes
49+
}
50+
}
51+
52+
/**
53+
Returns whether or not the currently executing Lune binary
54+
is a standalone binary, and if so, the bytes of the binary.
55+
*/
56+
pub async fn check_env() -> (bool, Vec<u8>) {
57+
let path = env::current_exe().expect("failed to get path to current running lune executable");
58+
let contents = fs::read(path).await.unwrap_or_default();
59+
let is_standalone = contents.ends_with(MAGIC);
60+
(is_standalone, contents)
61+
}
62+
63+
/**
64+
Discovers, loads and executes the bytecode contained in a standalone binary.
65+
*/
66+
pub async fn run_standalone(patched_bin: impl AsRef<[u8]>) -> Result<ExitCode> {
67+
// The first argument is the path to the current executable
68+
let args = env::args().skip(1).collect::<Vec<_>>();
69+
let meta = MetaChunk::from_bytes(patched_bin).expect("must be a standalone binary");
70+
71+
let result = Lune::new()
72+
.with_args(args)
73+
.run("STANDALONE", meta.bytecode)
74+
.await;
75+
76+
Ok(match result {
77+
Err(err) => {
78+
eprintln!("{err}");
79+
ExitCode::FAILURE
80+
}
81+
Ok(code) => code,
82+
})
83+
}

‎src/main.rs

+10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::process::ExitCode;
1313
use clap::Parser;
1414

1515
pub(crate) mod cli;
16+
pub(crate) mod executor;
1617

1718
use cli::Cli;
1819
use console::style;
@@ -26,6 +27,15 @@ async fn main() -> ExitCode {
2627
.with_timer(tracing_subscriber::fmt::time::uptime())
2728
.with_level(true)
2829
.init();
30+
31+
let (is_standalone, bin) = executor::check_env().await;
32+
33+
if is_standalone {
34+
// It's fine to unwrap here since we don't want to continue
35+
// if something fails
36+
return executor::run_standalone(bin).await.unwrap();
37+
}
38+
2939
match Cli::parse().run().await {
3040
Ok(code) => code,
3141
Err(err) => {

0 commit comments

Comments
 (0)