mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Implement standalone executable compilation (#140)
This commit is contained in:
parent
5040deddb6
commit
6f8b1e4896
6 changed files with 186 additions and 5 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1126,7 +1126,6 @@ dependencies = [
|
|||
"itertools",
|
||||
"lz4_flex",
|
||||
"mlua",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"os_str_bytes",
|
||||
"path-clean",
|
||||
|
|
|
@ -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
64
src/cli/build.rs
Normal 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(())
|
||||
}
|
|
@ -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
83
src/executor.rs
Normal 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,
|
||||
})
|
||||
}
|
10
src/main.rs
10
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) => {
|
||||
|
|
Loading…
Reference in a new issue