mirror of
https://github.com/lune-org/lune.git
synced 2024-12-13 05:20: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",
|
"itertools",
|
||||||
"lz4_flex",
|
"lz4_flex",
|
||||||
"mlua",
|
"mlua",
|
||||||
"num-traits",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"os_str_bytes",
|
"os_str_bytes",
|
||||||
"path-clean",
|
"path-clean",
|
||||||
|
|
|
@ -110,7 +110,6 @@ tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
|
||||||
### DATETIME
|
### DATETIME
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono_lc = "0.1"
|
chrono_lc = "0.1"
|
||||||
num-traits = "0.2"
|
|
||||||
|
|
||||||
### CLI
|
### 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 anyhow::{Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
@ -9,6 +9,7 @@ use tokio::{
|
||||||
io::{stdin, AsyncReadExt},
|
io::{stdin, AsyncReadExt},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub(crate) mod build;
|
||||||
pub(crate) mod gen;
|
pub(crate) mod gen;
|
||||||
pub(crate) mod repl;
|
pub(crate) mod repl;
|
||||||
pub(crate) mod setup;
|
pub(crate) mod setup;
|
||||||
|
@ -20,6 +21,8 @@ use utils::{
|
||||||
listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list},
|
listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use self::build::build_standalone;
|
||||||
|
|
||||||
/// A Luau script runner
|
/// A Luau script runner
|
||||||
#[derive(Parser, Debug, Default, Clone)]
|
#[derive(Parser, Debug, Default, Clone)]
|
||||||
#[command(version, long_about = None)]
|
#[command(version, long_about = None)]
|
||||||
|
@ -44,6 +47,9 @@ pub struct Cli {
|
||||||
/// Generate a Lune documentation file for Luau LSP
|
/// Generate a Lune documentation file for Luau LSP
|
||||||
#[clap(long, hide = true)]
|
#[clap(long, hide = true)]
|
||||||
generate_docs_file: bool,
|
generate_docs_file: bool,
|
||||||
|
/// Build a Luau file to an OS-Native standalone executable
|
||||||
|
#[clap(long)]
|
||||||
|
build: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -116,6 +122,7 @@ impl Cli {
|
||||||
|
|
||||||
return Ok(ExitCode::SUCCESS);
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate (save) definition files, if wanted
|
// Generate (save) definition files, if wanted
|
||||||
let generate_file_requested = self.setup
|
let generate_file_requested = self.setup
|
||||||
|| self.generate_luau_types
|
|| self.generate_luau_types
|
||||||
|
@ -143,14 +150,17 @@ impl Cli {
|
||||||
if generate_file_requested {
|
if generate_file_requested {
|
||||||
return Ok(ExitCode::SUCCESS);
|
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;
|
return repl::show_interface().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Figure out if we should read from stdin or from a file,
|
// Figure out if we should read from stdin or from a file,
|
||||||
// reading from stdin is marked by passing a single "-"
|
// reading from stdin is marked by passing a single "-"
|
||||||
// (dash) as the script name to run to the cli
|
// (dash) as the script name to run to the cli
|
||||||
let script_path = self.script_path.unwrap();
|
let script_path = self.script_path.unwrap();
|
||||||
|
|
||||||
let (script_display_name, script_contents) = if script_path == "-" {
|
let (script_display_name, script_contents) = if script_path == "-" {
|
||||||
let mut stdin_contents = Vec::new();
|
let mut stdin_contents = Vec::new();
|
||||||
stdin()
|
stdin()
|
||||||
|
@ -165,6 +175,22 @@ impl Cli {
|
||||||
let file_display_name = file_path.with_extension("").display().to_string();
|
let file_display_name = file_path.with_extension("").display().to_string();
|
||||||
(file_display_name, file_contents)
|
(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
|
// Create a new lune object with all globals & run the script
|
||||||
let result = Lune::new()
|
let result = Lune::new()
|
||||||
.with_args(self.script_args)
|
.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;
|
use clap::Parser;
|
||||||
|
|
||||||
pub(crate) mod cli;
|
pub(crate) mod cli;
|
||||||
|
pub(crate) mod executor;
|
||||||
|
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
use console::style;
|
use console::style;
|
||||||
|
@ -26,6 +27,15 @@ async fn main() -> ExitCode {
|
||||||
.with_timer(tracing_subscriber::fmt::time::uptime())
|
.with_timer(tracing_subscriber::fmt::time::uptime())
|
||||||
.with_level(true)
|
.with_level(true)
|
||||||
.init();
|
.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 {
|
match Cli::parse().run().await {
|
||||||
Ok(code) => code,
|
Ok(code) => code,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
Loading…
Reference in a new issue