Skip to content

Commit

Permalink
Give standalone compilation its own dir, leave some future notes
Browse files Browse the repository at this point in the history
  • Loading branch information
filiptibell committed Jan 14, 2024
1 parent e5e83d1 commit 0392c16
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 105 deletions.
20 changes: 5 additions & 15 deletions src/cli/build.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
use std::{
env::{self, consts::EXE_EXTENSION},
env::consts::EXE_EXTENSION,
path::{Path, PathBuf},
process::ExitCode,
};

use anyhow::{Context, Result};
use clap::Parser;
use console::style;
use mlua::Compiler as LuaCompiler;
use tokio::{fs, io::AsyncWriteExt as _};

use crate::executor::MetaChunk;
use crate::standalone::metadata::Metadata;

/// Build a standalone executable
#[derive(Debug, Clone, Parser)]
Expand Down Expand Up @@ -43,18 +42,9 @@ impl BuildCommand {
"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());
let patched_bin = Metadata::create_env_patched_bin(source_code.clone())
.await
.context("failed to create patched binary")?;

// And finally write the patched binary to the output file
println!(
Expand Down
83 changes: 0 additions & 83 deletions src/executor.rs

This file was deleted.

10 changes: 3 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use std::process::ExitCode;

pub(crate) mod cli;
pub(crate) mod executor;
pub(crate) mod standalone;

use cli::Cli;
use console::style;
Expand All @@ -26,12 +26,8 @@ async fn main() -> ExitCode {
.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();
if let Some(bin) = standalone::check().await {
return standalone::run(bin).await.unwrap();
}

match Cli::new().run().await {
Expand Down
99 changes: 99 additions & 0 deletions src/standalone/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use std::{env, path::PathBuf};

use anyhow::{bail, Result};
use mlua::Compiler as LuaCompiler;
use once_cell::sync::Lazy;
use tokio::fs;

const MAGIC: &[u8; 8] = b"cr3sc3nt";

static CURRENT_EXE: Lazy<PathBuf> =
Lazy::new(|| env::current_exe().expect("failed to get current exe"));

/*
TODO: Right now all we do is append the bytecode to the end
of the binary, but we will need a more flexible solution in
the future to store many files as well as their metadata.
The best solution here is most likely to use a well-supported
and rust-native binary serialization format with a stable
specification, one that also supports byte arrays well without
overhead, so the best solution seems to currently be Postcard:
https://github.com/jamesmunns/postcard
https://crates.io/crates/postcard
*/

/**
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 Metadata {
pub bytecode: Vec<u8>,
}

impl Metadata {
/**
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 contents = fs::read(CURRENT_EXE.to_path_buf())
.await
.unwrap_or_default();
let is_standalone = contents.ends_with(MAGIC);
(is_standalone, contents)
}

/**
Creates a patched standalone binary from the given script contents.
*/
pub async fn create_env_patched_bin(script_contents: impl Into<Vec<u8>>) -> Result<Vec<u8>> {
let mut patched_bin = fs::read(CURRENT_EXE.to_path_buf()).await?;

// Compile luau input into bytecode
let bytecode = LuaCompiler::new()
.set_optimization_level(2)
.set_coverage_level(0)
.set_debug_level(1)
.compile(script_contents.into());

// Append the bytecode / metadata to the end
let meta = Self { bytecode };
patched_bin.extend_from_slice(&meta.to_bytes());

Ok(patched_bin)
}

/**
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
}
}
44 changes: 44 additions & 0 deletions src/standalone/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::{env, process::ExitCode};

use anyhow::Result;
use lune::Runtime;

pub(crate) mod metadata;
pub(crate) mod tracer;

use self::metadata::Metadata;

/**
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() -> Option<Vec<u8>> {
let (is_standalone, patched_bin) = Metadata::check_env().await;
if is_standalone {
Some(patched_bin)
} else {
None
}
}

/**
Discovers, loads and executes the bytecode contained in a standalone binary.
*/
pub async fn run(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 = Metadata::from_bytes(patched_bin).expect("must be a standalone binary");

let result = Runtime::new()
.with_args(args)
.run("STANDALONE", meta.bytecode)
.await;

Ok(match result {
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
Ok(code) => code,
})
}
14 changes: 14 additions & 0 deletions src/standalone/tracer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
TODO: Implement tracing of requires here

Rough steps / outline:

1. Create a new tracer struct using a main entrypoint script path
2. Some kind of discovery mechanism that goes through all require chains (failing on recursive ones)
2a. Conversion of script-relative paths to cwd-relative paths + normalization
2b. Cache all found files in a map of file path -> file contents
2c. Prepend some kind of symbol to paths that can tell our runtime `require` function that it
should look up a bundled/standalone script, a good symbol here is probably a dollar sign ($)
3. ???
4. Profit
*/

0 comments on commit 0392c16

Please sign in to comment.