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

Implement Cross Compilation #162

Merged
merged 21 commits into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
119eeca
feat: cross compilation target system
CompeyDev Mar 2, 2024
3d305f7
feat: implement base binary system for custom targets
CompeyDev Mar 2, 2024
7e40851
refactor: minor cleanup & formatting
CompeyDev Mar 4, 2024
52e7185
refactor: appease the clippy overlords
CompeyDev Mar 10, 2024
93b950c
refactor: improved error handling for base bin discovery process
CompeyDev Mar 11, 2024
b9b1ba7
feat(docs): add rustdoc comments
CompeyDev Mar 11, 2024
a40d267
feat: finalize target download and caching system
CompeyDev Mar 14, 2024
1c0bb47
refactor: include comments & rustdoc
CompeyDev Mar 14, 2024
894b994
merge: main -> feature/cross-compilation
CompeyDev Mar 14, 2024
556dbd8
Merge branch 'main' into feature/cross-compilation
CompeyDev Mar 14, 2024
6e07534
Merge branch 'main' into feature/cross-compilation
CompeyDev Apr 7, 2024
66d1031
Merge branch 'main' into feature/cross-compilation
CompeyDev Apr 11, 2024
48092d4
refactor: addressed review concerns in error handling
CompeyDev Apr 18, 2024
5ce0ee5
Merge branch 'main' into feature/cross-compilation
CompeyDev Apr 18, 2024
8c1f3e9
refactor: address some more review issues
CompeyDev Apr 18, 2024
8d09f4d
Merge branch 'main' into feature/cross-compilation
CompeyDev Apr 19, 2024
971eccf
refactor: remove support for `--base` flag
CompeyDev Apr 19, 2024
4f80e98
refactor: remove 'bin' extension for precompiled targets
CompeyDev Apr 19, 2024
903de08
refactor: revert back to write function with executable perms at all …
CompeyDev Apr 20, 2024
0070f13
refactor: specify when to cross compile more explicitly
CompeyDev Apr 20, 2024
d889031
refactor: let `Metadata::create_env_patched_bin` handle compiler
CompeyDev Apr 20, 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
19 changes: 19 additions & 0 deletions Cargo.lock

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

9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ cli = [
"dep:include_dir",
"dep:regex",
"dep:rustyline",
"dep:async_zip",
"dep:tokio-util",
]
roblox = [
"dep:glam",
Expand Down Expand Up @@ -149,3 +151,10 @@ rbx_dom_weak = { optional = true, version = "2.6.0" }
rbx_reflection = { optional = true, version = "4.4.0" }
rbx_reflection_database = { optional = true, version = "0.2.9" }
rbx_xml = { optional = true, version = "0.13.2" }

### CROSS COMPILATION
async_zip = { optional = true, version = "0.0.16", features = [
"tokio",
"deflate",
] }
tokio-util = { optional = true, version = "0.7", features = ["io-util"] }
201 changes: 189 additions & 12 deletions src/cli/build.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
use std::{
env::consts::EXE_EXTENSION,
env::consts,
io::Cursor,
path::{Path, PathBuf},
process::ExitCode,
};

use anyhow::{Context, Result};
use async_zip::base::read::seek::ZipFileReader;
use clap::Parser;
use console::style;
use tokio::{fs, io::AsyncWriteExt as _};
use directories::BaseDirs;
use once_cell::sync::Lazy;
use thiserror::Error;
use tokio::{
fs,
io::{AsyncReadExt, AsyncWriteExt},
};
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};

use crate::standalone::metadata::{Metadata, CURRENT_EXE};

use crate::standalone::metadata::Metadata;
const TARGET_BASE_DIR: Lazy<PathBuf> = Lazy::new(|| {
BaseDirs::new()
.unwrap()
.home_dir()
.to_path_buf()
.join(".lune")
.join("target")
.join(env!("CARGO_PKG_VERSION"))
});

/// Build a standalone executable
// Build a standalone executable
#[derive(Debug, Clone, Parser)]
pub struct BuildCommand {
/// The path to the input file
Expand All @@ -21,37 +40,45 @@ pub struct BuildCommand {
/// input file path with an executable extension
#[clap(short, long)]
pub output: Option<PathBuf>,

/// The target to compile for - defaults to the host triple
#[clap(short, long)]
pub target: Option<String>,
}

impl BuildCommand {
pub async fn run(self) -> Result<ExitCode> {
let output_path = self
.output
.unwrap_or_else(|| self.input.with_extension(EXE_EXTENSION));
.unwrap_or_else(|| self.input.with_extension(consts::EXE_EXTENSION));

let input_path_displayed = self.input.display();
let output_path_displayed = output_path.display();

// Try to read the input file
let source_code = fs::read(&self.input)
.await
.context("failed to read input file")?;

// Dynamically derive the base executable path based on the CLI arguments provided
let (base_exe_path, output_path) = get_base_exe_path(self.target, output_path).await?;

// Read the contents of the lune interpreter as our starting point
println!(
"Creating standalone binary using {}",
style(input_path_displayed).green()
"{} standalone binary using {}",
style("Compile").green().bold(),
style(input_path_displayed).underlined()
);
let patched_bin = Metadata::create_env_patched_bin(source_code.clone())
let patched_bin = Metadata::create_env_patched_bin(base_exe_path, source_code.clone())
.await
.context("failed to create patched binary")?;

// And finally write the patched binary to the output file
println!(
"Writing standalone binary to {}",
style(output_path_displayed).blue()
" {} standalone binary to {}",
style("Write").blue().bold(),
style(output_path.display()).underlined()
);
write_executable_file_to(output_path, patched_bin).await?;
write_executable_file_to(output_path, patched_bin).await?; // Read & execute for all, write for owner

Ok(ExitCode::SUCCESS)
}
Expand All @@ -71,3 +98,153 @@ async fn write_executable_file_to(path: impl AsRef<Path>, bytes: impl AsRef<[u8]

Ok(())
}

/// Possible ways in which the discovery and/or download of a base binary's path can error
#[derive(Debug, Error)]
pub enum BasePathDiscoveryError {
/// An error in the decompression of the precompiled target
#[error("decompression error")]
Decompression(#[from] async_zip::error::ZipError),
#[error("precompiled base for target not found for {target}")]
TargetNotFound { target: String },
/// An error in the precompiled target download process
#[error("failed to download precompiled binary base, reason: {0}")]
DownloadError(#[from] reqwest::Error),
/// An IO related error
#[error("a generic error related to an io operation occurred, details: {0}")]
IoError(#[from] anyhow::Error),
}

/// Discovers the path to the base executable to use for cross-compilation
async fn get_base_exe_path(
target: Option<String>,
output_path: PathBuf,
) -> Result<(PathBuf, PathBuf), BasePathDiscoveryError> {
if let Some(target_inner) = target {
let current_target = format!("{}-{}", consts::OS, consts::ARCH);

let target_exe_extension = match target_inner.as_str() {
"windows-x86_64" => "exe",
_ => "",
};

if target_inner == current_target {
// If the target is the host target, just use the current executable
return Ok((
CURRENT_EXE.to_path_buf(),
output_path.with_extension(consts::EXE_EXTENSION),
));
}

let path = TARGET_BASE_DIR.join(format!("lune-{target_inner}.{target_exe_extension}"));

// Create the target base directory in the lune home if it doesn't already exist
if !TARGET_BASE_DIR.exists() {
fs::create_dir_all(TARGET_BASE_DIR.to_path_buf())
.await
.map_err(anyhow::Error::from)
.map_err(BasePathDiscoveryError::IoError)?;
}

// If a cached target base executable doesn't exist, attempt to download it
if !path.exists() {
println!("Requested target hasn't been downloaded yet, attempting to download");
cache_target(target_inner, target_exe_extension, &path).await?;
}

Ok((path, output_path.with_extension(target_exe_extension)))
} else {
// If the target flag was not specified, just use the current executable
Ok((
CURRENT_EXE.to_path_buf(),
output_path.with_extension(consts::EXE_EXTENSION),
))
}
}

async fn cache_target(
target: String,
target_exe_extension: &str,
path: &PathBuf,
) -> Result<(), BasePathDiscoveryError> {
let release_url = format!(
"https://github.com/lune-org/lune/releases/download/v{ver}/lune-{ver}-{target}.zip",
ver = env!("CARGO_PKG_VERSION"),
target = target
);

let target_full_display = release_url
.split('/')
.last()
.unwrap_or("lune-UNKNOWN-UNKNOWN")
.replace(".zip", format!(".{target_exe_extension}").as_str());

println!(
"{} target {}",
style("Download").green().bold(),
target_full_display
);

let resp = reqwest::get(release_url).await.map_err(|err| {
eprintln!(
" {} Unable to download base binary found for target `{}`",
style("Download").red().bold(),
target,
);

BasePathDiscoveryError::DownloadError(err)
})?;

let resp_status = resp.status();

if resp_status != 200 && !resp_status.is_redirection() {
eprintln!(
" {} No precompiled base binary found for target `{}`",
style("Download").red().bold(),
target
);

return Err(BasePathDiscoveryError::TargetNotFound { target });
}

// Wrap the request response in bytes so that we can decompress it, since `async_zip`
// requires the underlying reader to implement `AsyncRead` and `Seek`, which `Bytes`
// doesn't implement
let compressed_data = Cursor::new(
resp.bytes()
.await
.map_err(anyhow::Error::from)
.map_err(BasePathDiscoveryError::IoError)?
.to_vec(),
);

// Construct a decoder and decompress the ZIP file using deflate
let mut decoder = ZipFileReader::new(compressed_data.compat())
.await
.map_err(BasePathDiscoveryError::Decompression)?;

let mut decompressed = vec![];

decoder
.reader_without_entry(0)
.await
.map_err(BasePathDiscoveryError::Decompression)?
.compat()
.read_to_end(&mut decompressed)
.await
.map_err(anyhow::Error::from)
.map_err(BasePathDiscoveryError::IoError)?;

// Finally write the decompressed data to the target base directory
write_executable_file_to(&path, decompressed)
.await
.map_err(BasePathDiscoveryError::IoError)?;

println!(
" {} {}",
style("Downloaded").blue(),
style(target_full_display).underlined()
);

Ok(())
}
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
clippy::match_bool,
clippy::module_name_repetitions,
clippy::multiple_crate_versions,
clippy::needless_pass_by_value
clippy::needless_pass_by_value,
clippy::declare_interior_mutable_const,
clippy::borrow_interior_mutable_const
)]

use std::process::ExitCode;
Expand Down
23 changes: 13 additions & 10 deletions src/standalone/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ use mlua::Compiler as LuaCompiler;
use once_cell::sync::Lazy;
use tokio::fs;

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

static CURRENT_EXE: Lazy<PathBuf> =
pub const CURRENT_EXE: Lazy<PathBuf> =
Lazy::new(|| env::current_exe().expect("failed to get current exe"));
const MAGIC: &[u8; 8] = b"cr3sc3nt";

/*
TODO: Right now all we do is append the bytecode to the end
Expand Down Expand Up @@ -49,15 +48,19 @@ impl Metadata {
/**
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()
pub async fn create_env_patched_bin(
base_exe_path: PathBuf,
script_contents: impl Into<Vec<u8>>,
) -> Result<Vec<u8>> {
let compiler = LuaCompiler::new()
.set_optimization_level(2)
.set_coverage_level(0)
.set_debug_level(1)
.compile(script_contents.into());
.set_debug_level(1);

let mut patched_bin = fs::read(base_exe_path).await?;

// Compile luau input into bytecode
let bytecode = compiler.compile(script_contents.into());

// Append the bytecode / metadata to the end
let meta = Self { bytecode };
Expand Down
Loading