From 03c773fd7928a8afa39c0515e97a5cdc0436d95b Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Thu, 18 Apr 2024 20:54:21 +0200 Subject: [PATCH 01/22] Fix headers in net.serve being raw bytes instead of strings --- src/lune/builtins/net/server/request.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/lune/builtins/net/server/request.rs b/src/lune/builtins/net/server/request.rs index dde529f..bab7a5d 100644 --- a/src/lune/builtins/net/server/request.rs +++ b/src/lune/builtins/net/server/request.rs @@ -18,21 +18,30 @@ impl LuaRequest { let path = self.head.uri.path().to_string(); let body = lua.create_string(&self.body)?; - let query: HashMap = self + let query: HashMap = self .head .uri .query() .unwrap_or_default() .split('&') .filter_map(|q| q.split_once('=')) - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - let headers: HashMap> = self + .map(|(k, v)| { + let k = lua.create_string(k)?; + let v = lua.create_string(v)?; + Ok((k, v)) + }) + .collect::>()?; + + let headers: HashMap = self .head .headers .iter() - .map(|(k, v)| (k.as_str().to_string(), v.as_bytes().to_vec())) - .collect(); + .map(|(k, v)| { + let k = lua.create_string(k.as_str())?; + let v = lua.create_string(v.as_bytes())?; + Ok((k, v)) + }) + .collect::>()?; TableBuilder::new(lua)? .with_value("method", method)? From 4a28499aaa1d9669636ffc6b0da2cd7c7549533d Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Thu, 18 Apr 2024 20:56:40 +0200 Subject: [PATCH 02/22] Fix net.serve no longer accepting ipv6 --- src/lune/builtins/net/config.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lune/builtins/net/config.rs b/src/lune/builtins/net/config.rs index 5d02d2b..d801c47 100644 --- a/src/lune/builtins/net/config.rs +++ b/src/lune/builtins/net/config.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, net::Ipv4Addr}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr}, +}; use mlua::prelude::*; @@ -6,7 +9,7 @@ use reqwest::Method; use super::util::table_to_hash_map; -const DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); +const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#" return { @@ -155,7 +158,7 @@ impl FromLua<'_> for RequestConfig { #[derive(Debug)] pub struct ServeConfig<'a> { - pub address: Ipv4Addr, + pub address: IpAddr, pub handle_request: LuaFunction<'a>, pub handle_web_socket: Option>, } @@ -175,7 +178,7 @@ impl<'lua> FromLua<'lua> for ServeConfig<'lua> { let handle_request: Option = t.get("handleRequest")?; let handle_web_socket: Option = t.get("handleWebSocket")?; if handle_request.is_some() || handle_web_socket.is_some() { - let address: Ipv4Addr = match &address { + let address: IpAddr = match &address { Some(addr) => { let addr_str = addr.to_str()?; From 753897222ab8b02101d483d3d88384b6a992771d Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Thu, 18 Apr 2024 22:10:08 +0200 Subject: [PATCH 03/22] Implement stdio.readToEnd API --- src/lune/builtins/stdio/mod.rs | 18 +++++++++++++++++- types/stdio.luau | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/lune/builtins/stdio/mod.rs b/src/lune/builtins/stdio/mod.rs index 14927f2..f851bc7 100644 --- a/src/lune/builtins/stdio/mod.rs +++ b/src/lune/builtins/stdio/mod.rs @@ -2,7 +2,7 @@ use mlua::prelude::*; use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select}; use mlua_luau_scheduler::LuaSpawnExt; -use tokio::io::{self, AsyncWriteExt}; +use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; use crate::lune::util::{ formatting::{ @@ -21,6 +21,7 @@ pub fn create(lua: &Lua) -> LuaResult> { .with_function("format", stdio_format)? .with_async_function("write", stdio_write)? .with_async_function("ewrite", stdio_ewrite)? + .with_async_function("readToEnd", stdio_read_to_end)? .with_async_function("prompt", stdio_prompt)? .build_readonly() } @@ -53,6 +54,21 @@ async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { Ok(()) } +/* + FUTURE: Figure out how to expose some kind of "readLine" function using a buffered reader. + + This is a bit tricky since we would want to be able to use **both** readLine and readToEnd + in the same script, doing something like readLine, readLine, readToEnd from lua, and + having that capture the first two lines and then read the rest of the input. +*/ + +async fn stdio_read_to_end(lua: &Lua, _: ()) -> LuaResult { + let mut input = Vec::new(); + let mut stdin = io::stdin(); + stdin.read_to_end(&mut input).await?; + lua.create_string(&input) +} + async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult { lua.spawn_blocking(move || prompt(options)) .await diff --git a/types/stdio.luau b/types/stdio.luau index 3c5198e..70a73e0 100644 --- a/types/stdio.luau +++ b/types/stdio.luau @@ -58,6 +58,11 @@ end stdio.write("World! ") stdio.write("All on the same line") stdio.ewrite("\nAnd some error text, too") + + -- Reading from stdin, either line-by-line or the entire input + local firstLine = stdio.readLine() + local secondLine = stdio.readLine() + local remaining = stdio.readToEnd() ``` ]=] local stdio = {} @@ -143,4 +148,16 @@ function stdio.write(s: string) end ]=] function stdio.ewrite(s: string) end +--[=[ + @within Stdio + @tag must_use + + Reads the entire input from stdin. + + @return The input from stdin +]=] +function stdio.readToEnd(): string + return nil :: any +end + return stdio From fa7f6c6f5101f6a28beedf29104709fa4054e613 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Thu, 18 Apr 2024 22:14:54 +0200 Subject: [PATCH 04/22] Fix doc example for new stdio API --- types/stdio.luau | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/types/stdio.luau b/types/stdio.luau index 70a73e0..e6e88a4 100644 --- a/types/stdio.luau +++ b/types/stdio.luau @@ -59,10 +59,8 @@ end stdio.write("All on the same line") stdio.ewrite("\nAnd some error text, too") - -- Reading from stdin, either line-by-line or the entire input - local firstLine = stdio.readLine() - local secondLine = stdio.readLine() - local remaining = stdio.readToEnd() + -- Reading the entire input from stdin + local input = stdio.readToEnd() ``` ]=] local stdio = {} From 7fb48dfa1f6090428b6038c02a3f2e8113b939e2 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 20 Apr 2024 20:00:47 +0530 Subject: [PATCH 05/22] Implement cross-compilation of standalone binaries (#162) --- Cargo.lock | 19 ++++ Cargo.toml | 9 ++ src/cli/build.rs | 201 ++++++++++++++++++++++++++++++++++--- src/main.rs | 4 +- src/standalone/metadata.rs | 23 +++-- 5 files changed, 233 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5669047..6432cc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,6 +150,7 @@ dependencies = [ "brotli", "flate2", "futures-core", + "futures-io", "memchr", "pin-project-lite", "tokio", @@ -196,6 +197,21 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "async_zip" +version = "0.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527207465fb6dcafbf661b0d4a51d0d2306c9d0c2975423079a6caa807930daf" +dependencies = [ + "async-compression", + "crc32fast", + "futures-lite", + "pin-project", + "thiserror", + "tokio", + "tokio-util", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1349,6 +1365,7 @@ dependencies = [ "anyhow", "async-compression", "async-trait", + "async_zip", "blocking", "chrono", "chrono_lc", @@ -1391,6 +1408,7 @@ dependencies = [ "thiserror", "tokio", "tokio-tungstenite", + "tokio-util", "toml", "tracing", "tracing-subscriber", @@ -2770,6 +2788,7 @@ checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 90e22c2..8552e1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ cli = [ "dep:include_dir", "dep:regex", "dep:rustyline", + "dep:async_zip", + "dep:tokio-util", ] roblox = [ "dep:glam", @@ -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"] } diff --git a/src/cli/build.rs b/src/cli/build.rs index efe1738..a9b6bc8 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -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; +use crate::standalone::metadata::{Metadata, CURRENT_EXE}; -/// Build a standalone executable +const TARGET_BASE_DIR: Lazy = Lazy::new(|| { + BaseDirs::new() + .unwrap() + .home_dir() + .to_path_buf() + .join(".lune") + .join("target") + .join(env!("CARGO_PKG_VERSION")) +}); + +// Build a standalone executable #[derive(Debug, Clone, Parser)] pub struct BuildCommand { /// The path to the input file @@ -21,37 +40,45 @@ pub struct BuildCommand { /// input file path with an executable extension #[clap(short, long)] pub output: Option, + + /// The target to compile for - defaults to the host triple + #[clap(short, long)] + pub target: Option, } impl BuildCommand { pub async fn run(self) -> Result { 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) } @@ -71,3 +98,153 @@ async fn write_executable_file_to(path: impl AsRef, 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, + 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(()) +} diff --git a/src/main.rs b/src/main.rs index ca28c76..734436d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; diff --git a/src/standalone/metadata.rs b/src/standalone/metadata.rs index 65249a9..f2ddfd9 100644 --- a/src/standalone/metadata.rs +++ b/src/standalone/metadata.rs @@ -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 = +pub const CURRENT_EXE: Lazy = 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 @@ -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>) -> Result> { - 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>, + ) -> Result> { + 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 }; From f830ce7fad1894cde13d8287c51f933f9fe1037e Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 20 Apr 2024 20:14:19 +0530 Subject: [PATCH 06/22] Add support for buffers as arguments in builtin APIs (#148) --- Cargo.lock | 2 + Cargo.toml | 3 +- src/lune/builtins/fs/mod.rs | 6 +- src/lune/builtins/net/config.rs | 4 +- src/lune/builtins/net/mod.rs | 3 +- src/lune/builtins/net/server/response.rs | 3 +- src/lune/builtins/net/websocket.rs | 3 +- src/lune/builtins/serde/encode_decode.rs | 3 +- src/lune/builtins/serde/mod.rs | 7 ++- tests/fs/copy.luau | 6 +- tests/fs/files.luau | 4 +- tests/fs/metadata.luau | 2 +- tests/fs/move.luau | 2 +- tests/fs/utils.luau | 2 +- tests/net/socket/wss_rw.luau | 4 +- tests/roblox/rbx-test-files | 2 +- tests/serde/compression/files.luau | 79 +++++++++++------------- types/fs.luau | 2 +- types/net.luau | 6 +- types/serde.luau | 6 +- 20 files changed, 78 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6432cc7..de30793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,6 +346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", + "regex-automata 0.4.6", "serde", ] @@ -1367,6 +1368,7 @@ dependencies = [ "async-trait", "async_zip", "blocking", + "bstr", "chrono", "chrono_lc", "clap", diff --git a/Cargo.toml b/Cargo.toml index 8552e1e..2eace8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ path-clean = "1.0" pathdiff = "0.2" pin-project = "1.0" urlencoding = "2.1" +bstr = "1.9.1" ### RUNTIME @@ -87,7 +88,7 @@ tokio = { version = "1.24", features = ["full", "tracing"] } os_str_bytes = { version = "7.0", features = ["conversions"] } mlua-luau-scheduler = { version = "0.0.2" } -mlua = { version = "0.9.6", features = [ +mlua = { version = "0.9.7", features = [ "luau", "luau-jit", "async", diff --git a/src/lune/builtins/fs/mod.rs b/src/lune/builtins/fs/mod.rs index 2c7b9fb..0db1f7f 100644 --- a/src/lune/builtins/fs/mod.rs +++ b/src/lune/builtins/fs/mod.rs @@ -1,6 +1,7 @@ use std::io::ErrorKind as IoErrorKind; use std::path::{PathBuf, MAIN_SEPARATOR}; +use bstr::{BString, ByteSlice}; use mlua::prelude::*; use tokio::fs; @@ -32,6 +33,7 @@ pub fn create(lua: &Lua) -> LuaResult { async fn fs_read_file(lua: &Lua, path: String) -> LuaResult { let bytes = fs::read(&path).await.into_lua_err()?; + lua.create_string(bytes) } @@ -64,8 +66,8 @@ async fn fs_read_dir(_: &Lua, path: String) -> LuaResult> { Ok(dir_strings_no_prefix) } -async fn fs_write_file(_: &Lua, (path, contents): (String, LuaString<'_>)) -> LuaResult<()> { - fs::write(&path, &contents.as_bytes()).await.into_lua_err() +async fn fs_write_file(_: &Lua, (path, contents): (String, BString)) -> LuaResult<()> { + fs::write(&path, contents.as_bytes()).await.into_lua_err() } async fn fs_write_dir(_: &Lua, path: String) -> LuaResult<()> { diff --git a/src/lune/builtins/net/config.rs b/src/lune/builtins/net/config.rs index d801c47..1abd121 100644 --- a/src/lune/builtins/net/config.rs +++ b/src/lune/builtins/net/config.rs @@ -3,6 +3,7 @@ use std::{ net::{IpAddr, Ipv4Addr}, }; +use bstr::{BString, ByteSlice}; use mlua::prelude::*; use reqwest::Method; @@ -107,10 +108,11 @@ impl FromLua<'_> for RequestConfig { Err(_) => HashMap::new(), }; // Extract body - let body = match tab.get::<_, LuaString>("body") { + let body = match tab.get::<_, BString>("body") { Ok(config_body) => Some(config_body.as_bytes().to_owned()), Err(_) => None, }; + // Convert method string into proper enum let method = method.trim().to_ascii_uppercase(); let method = match method.as_ref() { diff --git a/src/lune/builtins/net/mod.rs b/src/lune/builtins/net/mod.rs index 6253dec..d41b047 100644 --- a/src/lune/builtins/net/mod.rs +++ b/src/lune/builtins/net/mod.rs @@ -1,5 +1,6 @@ #![allow(unused_variables)] +use bstr::BString; use mlua::prelude::*; use mlua_luau_scheduler::LuaSpawnExt; @@ -45,7 +46,7 @@ fn net_json_encode<'lua>( .serialize_to_string(lua, val) } -fn net_json_decode<'lua>(lua: &'lua Lua, json: LuaString<'lua>) -> LuaResult> { +fn net_json_decode<'lua>(lua: &'lua Lua, json: BString) -> LuaResult> { EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json) } diff --git a/src/lune/builtins/net/server/response.rs b/src/lune/builtins/net/server/response.rs index f575990..240a7cd 100644 --- a/src/lune/builtins/net/server/response.rs +++ b/src/lune/builtins/net/server/response.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use bstr::{BString, ByteSlice}; use http_body_util::Full; use hyper::{ body::Bytes, @@ -56,7 +57,7 @@ impl FromLua<'_> for LuaResponse { LuaValue::Table(t) => { let status: Option = t.get("status")?; let headers: Option = t.get("headers")?; - let body: Option = t.get("body")?; + let body: Option = t.get("body")?; let mut headers_map = HeaderMap::new(); if let Some(headers) = headers { diff --git a/src/lune/builtins/net/websocket.rs b/src/lune/builtins/net/websocket.rs index fb7233c..5dba4ec 100644 --- a/src/lune/builtins/net/websocket.rs +++ b/src/lune/builtins/net/websocket.rs @@ -3,6 +3,7 @@ use std::sync::{ Arc, }; +use bstr::{BString, ByteSlice}; use mlua::prelude::*; use futures_util::{ @@ -160,7 +161,7 @@ where methods.add_async_method( "send", - |_, this, (string, as_binary): (LuaString, Option)| async move { + |_, this, (string, as_binary): (BString, Option)| async move { this.send(if as_binary.unwrap_or_default() { WsMessage::Binary(string.as_bytes().to_vec()) } else { diff --git a/src/lune/builtins/serde/encode_decode.rs b/src/lune/builtins/serde/encode_decode.rs index 1f83ce4..d8bae5c 100644 --- a/src/lune/builtins/serde/encode_decode.rs +++ b/src/lune/builtins/serde/encode_decode.rs @@ -1,3 +1,4 @@ +use bstr::{BString, ByteSlice}; use mlua::prelude::*; use serde_json::Value as JsonValue; @@ -89,7 +90,7 @@ impl EncodeDecodeConfig { pub fn deserialize_from_string<'lua>( self, lua: &'lua Lua, - string: LuaString<'lua>, + string: BString, ) -> LuaResult> { let bytes = string.as_bytes(); match self.format { diff --git a/src/lune/builtins/serde/mod.rs b/src/lune/builtins/serde/mod.rs index c149467..910cb59 100644 --- a/src/lune/builtins/serde/mod.rs +++ b/src/lune/builtins/serde/mod.rs @@ -1,3 +1,4 @@ +use bstr::BString; use mlua::prelude::*; pub(super) mod compress_decompress; @@ -27,7 +28,7 @@ fn serde_encode<'lua>( fn serde_decode<'lua>( lua: &'lua Lua, - (format, str): (EncodeDecodeFormat, LuaString<'lua>), + (format, str): (EncodeDecodeFormat, BString), ) -> LuaResult> { let config = EncodeDecodeConfig::from(format); config.deserialize_from_string(lua, str) @@ -35,7 +36,7 @@ fn serde_decode<'lua>( async fn serde_compress<'lua>( lua: &'lua Lua, - (format, str): (CompressDecompressFormat, LuaString<'lua>), + (format, str): (CompressDecompressFormat, BString), ) -> LuaResult> { let bytes = compress(format, str).await?; lua.create_string(bytes) @@ -43,7 +44,7 @@ async fn serde_compress<'lua>( async fn serde_decompress<'lua>( lua: &'lua Lua, - (format, str): (CompressDecompressFormat, LuaString<'lua>), + (format, str): (CompressDecompressFormat, BString), ) -> LuaResult> { let bytes = decompress(format, str).await?; lua.create_string(bytes) diff --git a/tests/fs/copy.luau b/tests/fs/copy.luau index c7ed6dd..1b52320 100644 --- a/tests/fs/copy.luau +++ b/tests/fs/copy.luau @@ -50,15 +50,15 @@ assert(fs.isFile(TEMP_ROOT_PATH_2 .. "/foo/buzz"), "Missing copied file - root/f -- Make sure the copied files are correct assert( - fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/bar/baz") == utils.binaryBlob, + fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/bar/baz") == buffer.tostring(utils.binaryBlob), "Invalid copied file - root/foo/bar/baz" ) assert( - fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/fizz") == utils.binaryBlob, + fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/fizz") == buffer.tostring(utils.binaryBlob), "Invalid copied file - root/foo/fizz" ) assert( - fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/buzz") == utils.binaryBlob, + fs.readFile(TEMP_ROOT_PATH_2 .. "/foo/buzz") == buffer.tostring(utils.binaryBlob), "Invalid copied file - root/foo/buzz" ) diff --git a/tests/fs/files.luau b/tests/fs/files.luau index a49412e..38b7485 100644 --- a/tests/fs/files.luau +++ b/tests/fs/files.luau @@ -11,6 +11,8 @@ fs.writeDir(TEMP_ROOT_PATH) -- Write both of our files +-- binaryBlob is of type buffer to make sure fs.writeFile +-- works with both strings and buffers fs.writeFile(TEMP_ROOT_PATH .. "/test_binary", utils.binaryBlob) fs.writeFile(TEMP_ROOT_PATH .. "/test_json.json", utils.jsonBlob) @@ -18,7 +20,7 @@ fs.writeFile(TEMP_ROOT_PATH .. "/test_json.json", utils.jsonBlob) -- wrote gets us back the original strings assert( - fs.readFile(TEMP_ROOT_PATH .. "/test_binary") == utils.binaryBlob, + fs.readFile(TEMP_ROOT_PATH .. "/test_binary") == buffer.tostring(utils.binaryBlob), "Binary file round-trip resulted in different strings" ) diff --git a/tests/fs/metadata.luau b/tests/fs/metadata.luau index e926a2c..8a5a795 100644 --- a/tests/fs/metadata.luau +++ b/tests/fs/metadata.luau @@ -45,7 +45,7 @@ assert(metaFile.kind == "file", "File metadata kind was invalid") local metaBefore = fs.metadata(TEMP_FILE_PATH) task.wait(1) -fs.writeFile(TEMP_FILE_PATH, utils.binaryBlob .. "\n") +fs.writeFile(TEMP_FILE_PATH, buffer.tostring(utils.binaryBlob) .. "\n") local metaAfter = fs.metadata(TEMP_FILE_PATH) assert( diff --git a/tests/fs/move.luau b/tests/fs/move.luau index 868a095..749c8cb 100644 --- a/tests/fs/move.luau +++ b/tests/fs/move.luau @@ -20,7 +20,7 @@ fs.move("bin/move_test_json.json", "bin/moved_test_json.json") -- wrote gets us back the original strings assert( - fs.readFile("bin/moved_test_binary") == utils.binaryBlob, + fs.readFile("bin/moved_test_binary") == buffer.tostring(utils.binaryBlob), "Binary file round-trip resulted in different strings" ) diff --git a/tests/fs/utils.luau b/tests/fs/utils.luau index a5a947e..89a6964 100644 --- a/tests/fs/utils.luau +++ b/tests/fs/utils.luau @@ -16,6 +16,6 @@ local jsonBlob = serde.encode("json", { -- Return testing data and utils return { - binaryBlob = binaryBlob, + binaryBlob = buffer.fromstring(binaryBlob), jsonBlob = jsonBlob, } diff --git a/tests/net/socket/wss_rw.luau b/tests/net/socket/wss_rw.luau index 8ae6066..fefb244 100644 --- a/tests/net/socket/wss_rw.luau +++ b/tests/net/socket/wss_rw.luau @@ -22,7 +22,9 @@ end) task.wait(1) -socket.send('{"op":1,"d":null}') +local payload = '{"op":1,"d":null}' +socket.send(payload) +socket.send(buffer.fromstring(payload)) socket.close(1000) task.cancel(delayedThread) diff --git a/tests/roblox/rbx-test-files b/tests/roblox/rbx-test-files index 52f2c1a..655b5cc 160000 --- a/tests/roblox/rbx-test-files +++ b/tests/roblox/rbx-test-files @@ -1 +1 @@ -Subproject commit 52f2c1a686e7b67d996005eeddf63b97b170a741 +Subproject commit 655b5cc6a64024709d3662cc45ec4319c87de5a2 diff --git a/tests/serde/compression/files.luau b/tests/serde/compression/files.luau index 8d41492..6f32ed0 100644 --- a/tests/serde/compression/files.luau +++ b/tests/serde/compression/files.luau @@ -33,69 +33,60 @@ local TESTS: { Test } = { } local failed = false -for _, test in TESTS do - local source = fs.readFile(test.Source) - local target = fs.readFile(test.Target) - - local success, compressed = pcall(serde.compress, test.Format, source) +local function testOperation( + operationName: "Compress" | "Decompress", + operation: ( + format: serde.CompressDecompressFormat, + s: buffer | string + ) -> string, + format: serde.CompressDecompressFormat, + source: string | buffer, + target: string +) + local success, res = pcall(operation, format, source) if not success then stdio.ewrite( string.format( - "Compressing source using '%s' format threw an error!\n%s", - tostring(test.Format), - tostring(compressed) + "%sing source using '%s' format threw an error!\n%s", + operationName, + tostring(format), + tostring(res) ) ) failed = true - continue - elseif compressed ~= target then + elseif res ~= target then stdio.ewrite( string.format( - "Compressing source using '%s' format did not produce target!\n", - tostring(test.Format) + "%sing source using '%s' format did not produce target!\n", + operationName, + tostring(format) ) ) stdio.ewrite( string.format( - "Compressed (%d chars long):\n%s\nTarget (%d chars long):\n%s\n\n", - #compressed, - tostring(compressed), + "%sed (%d chars long):\n%s\nTarget (%d chars long):\n%s\n\n", + operationName, + #res, + tostring(res), #target, tostring(target) ) ) failed = true - continue end +end - local success2, decompressed = pcall(serde.decompress, test.Format, target) - if not success2 then - stdio.ewrite( - string.format( - "Decompressing source using '%s' format threw an error!\n%s", - tostring(test.Format), - tostring(decompressed) - ) - ) - failed = true - continue - elseif decompressed ~= source then - stdio.ewrite( - string.format( - "Decompressing target using '%s' format did not produce source!\n", - tostring(test.Format) - ) - ) - stdio.ewrite( - string.format( - "Decompressed (%d chars long):\n%s\n\n", - #decompressed, - tostring(decompressed) - ) - ) - failed = true - continue - end +for _, test in TESTS do + local source = fs.readFile(test.Source) + local target = fs.readFile(test.Target) + + -- Compression + testOperation("Compress", serde.compress, test.Format, source, target) + testOperation("Compress", serde.compress, test.Format, buffer.fromstring(source), target) + + -- Decompression + testOperation("Decompress", serde.decompress, test.Format, target, source) + testOperation("Decompress", serde.decompress, test.Format, buffer.fromstring(target), source) end if failed then diff --git a/types/fs.luau b/types/fs.luau index 1d80604..823f6f7 100644 --- a/types/fs.luau +++ b/types/fs.luau @@ -144,7 +144,7 @@ end @param path The path of the file @param contents The contents of the file ]=] -function fs.writeFile(path: string, contents: string) end +function fs.writeFile(path: string, contents: buffer | string) end --[=[ @within FS diff --git a/types/net.luau b/types/net.luau index 47c6249..e9b793e 100644 --- a/types/net.luau +++ b/types/net.luau @@ -36,7 +36,7 @@ export type FetchParamsOptions = { export type FetchParams = { url: string, method: HttpMethod?, - body: string?, + body: (string | buffer)?, query: HttpQueryMap?, headers: HttpHeaderMap?, options: FetchParamsOptions?, @@ -101,7 +101,7 @@ export type ServeRequest = { export type ServeResponse = { status: number?, headers: { [string]: string }?, - body: string?, + body: (string | buffer)?, } type ServeHttpHandler = (request: ServeRequest) -> string | ServeResponse @@ -174,7 +174,7 @@ export type ServeHandle = { export type WebSocket = { closeCode: number?, close: (code: number?) -> (), - send: (message: string, asBinaryMessage: boolean?) -> (), + send: (message: (string | buffer)?, asBinaryMessage: boolean?) -> (), next: () -> string?, } diff --git a/types/serde.luau b/types/serde.luau index 6bd63db..c4a21d8 100644 --- a/types/serde.luau +++ b/types/serde.luau @@ -70,7 +70,7 @@ end @param encoded The string to decode @return The decoded lua value ]=] -function serde.decode(format: EncodeDecodeFormat, encoded: string): any +function serde.decode(format: EncodeDecodeFormat, encoded: buffer | string): any return nil :: any end @@ -93,7 +93,7 @@ end @param s The string to compress @return The compressed string ]=] -function serde.compress(format: CompressDecompressFormat, s: string): string +function serde.compress(format: CompressDecompressFormat, s: buffer | string): string return nil :: any end @@ -116,7 +116,7 @@ end @param s The string to decompress @return The decompressed string ]=] -function serde.decompress(format: CompressDecompressFormat, s: string): string +function serde.decompress(format: CompressDecompressFormat, s: buffer | string): string return nil :: any end From 66ed1a0b7224cc3677e1fdc1589a083b5ced595f Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Sat, 20 Apr 2024 20:25:54 +0530 Subject: [PATCH 07/22] Use a more descriptive User-Agent header in net.request (#186) --- src/lune/builtins/net/mod.rs | 2 +- src/lune/builtins/net/util.rs | 19 +++++++++++++------ src/lune/globals/version.rs | 2 +- tests/net/request/user_agent.luau | 9 +++++++++ 4 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 tests/net/request/user_agent.luau diff --git a/src/lune/builtins/net/mod.rs b/src/lune/builtins/net/mod.rs index d41b047..9943c1f 100644 --- a/src/lune/builtins/net/mod.rs +++ b/src/lune/builtins/net/mod.rs @@ -24,7 +24,7 @@ use super::serde::encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat}; pub fn create(lua: &Lua) -> LuaResult { NetClientBuilder::new() - .headers(&[("User-Agent", create_user_agent_header())])? + .headers(&[("User-Agent", create_user_agent_header(lua)?)])? .build()? .into_registry(lua); TableBuilder::new(lua)? diff --git a/src/lune/builtins/net/util.rs b/src/lune/builtins/net/util.rs index 4603547..e18235e 100644 --- a/src/lune/builtins/net/util.rs +++ b/src/lune/builtins/net/util.rs @@ -7,12 +7,19 @@ use mlua::prelude::*; use crate::lune::util::TableBuilder; -pub fn create_user_agent_header() -> String { - let (github_owner, github_repo) = env!("CARGO_PKG_REPOSITORY") - .trim_start_matches("https://github.com/") - .split_once('/') - .unwrap(); - format!("{github_owner}-{github_repo}-cli") +pub fn create_user_agent_header(lua: &Lua) -> LuaResult { + let version_global = lua + .globals() + .get::<_, LuaString>("_VERSION") + .expect("Missing _VERSION global"); + + let version_global_str = version_global + .to_str() + .context("Invalid utf8 found in _VERSION global")?; + + let (package_name, full_version) = version_global_str.split_once(' ').unwrap(); + + Ok(format!("{}/{}", package_name.to_lowercase(), full_version)) } pub fn header_map_to_table( diff --git a/src/lune/globals/version.rs b/src/lune/globals/version.rs index 4904144..1228fd0 100644 --- a/src/lune/globals/version.rs +++ b/src/lune/globals/version.rs @@ -13,7 +13,7 @@ pub fn create(lua: &Lua) -> LuaResult> { // If this function runs more than once, we // may get an already formatted lune version. - if luau_version_str.starts_with(&lune_version) { + if luau_version_str.starts_with(lune_version.as_str()) { return Ok(luau_version_full); } diff --git a/tests/net/request/user_agent.luau b/tests/net/request/user_agent.luau new file mode 100644 index 0000000..04491b7 --- /dev/null +++ b/tests/net/request/user_agent.luau @@ -0,0 +1,9 @@ +local net = require("@lune/net") + +local runtime, version = table.unpack(_VERSION:split(" ")) +local expectedUserAgent = runtime:lower() .. "/" .. version + +local userAgent: string = + net.jsonDecode(net.request("https://www.whatsmyua.info/api/v1/ua").body)[1].ua.rawUa + +assert(userAgent == expectedUserAgent, "Expected user agent to be " .. expectedUserAgent) From 53463641b8588c62a9d253db71f90017a9fcb5ef Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 16:48:28 +0200 Subject: [PATCH 08/22] Fix clippy lints in serde builtin --- src/lune/builtins/serde/encode_decode.rs | 6 +----- src/lune/builtins/serde/mod.rs | 17 +++++++---------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/lune/builtins/serde/encode_decode.rs b/src/lune/builtins/serde/encode_decode.rs index d8bae5c..8457a25 100644 --- a/src/lune/builtins/serde/encode_decode.rs +++ b/src/lune/builtins/serde/encode_decode.rs @@ -87,11 +87,7 @@ impl EncodeDecodeConfig { lua.create_string(bytes) } - pub fn deserialize_from_string<'lua>( - self, - lua: &'lua Lua, - string: BString, - ) -> LuaResult> { + pub fn deserialize_from_string(self, lua: &Lua, string: BString) -> LuaResult { let bytes = string.as_bytes(); match self.format { EncodeDecodeFormat::Json => { diff --git a/src/lune/builtins/serde/mod.rs b/src/lune/builtins/serde/mod.rs index 910cb59..de351a3 100644 --- a/src/lune/builtins/serde/mod.rs +++ b/src/lune/builtins/serde/mod.rs @@ -26,26 +26,23 @@ fn serde_encode<'lua>( config.serialize_to_string(lua, val) } -fn serde_decode<'lua>( - lua: &'lua Lua, - (format, str): (EncodeDecodeFormat, BString), -) -> LuaResult> { +fn serde_decode(lua: &Lua, (format, str): (EncodeDecodeFormat, BString)) -> LuaResult { let config = EncodeDecodeConfig::from(format); config.deserialize_from_string(lua, str) } -async fn serde_compress<'lua>( - lua: &'lua Lua, +async fn serde_compress( + lua: &Lua, (format, str): (CompressDecompressFormat, BString), -) -> LuaResult> { +) -> LuaResult { let bytes = compress(format, str).await?; lua.create_string(bytes) } -async fn serde_decompress<'lua>( - lua: &'lua Lua, +async fn serde_decompress( + lua: &Lua, (format, str): (CompressDecompressFormat, BString), -) -> LuaResult> { +) -> LuaResult { let bytes = decompress(format, str).await?; lua.create_string(bytes) } From 0685e62a8f81da55194d5862e2864a6f0398b1da Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 17:19:08 +0200 Subject: [PATCH 09/22] Refactor downloading lune binary to cache, some fixes + formatting --- Cargo.lock | 251 +++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 11 +-- src/cli/build.rs | 180 +++++++++++++++------------------ 3 files changed, 311 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de30793..8ff4e5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -110,6 +121,15 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -150,7 +170,6 @@ dependencies = [ "brotli", "flate2", "futures-core", - "futures-io", "memchr", "pin-project-lite", "tokio", @@ -197,21 +216,6 @@ dependencies = [ "syn 2.0.59", ] -[[package]] -name = "async_zip" -version = "0.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527207465fb6dcafbf661b0d4a51d0d2306c9d0c2975423079a6caa807930daf" -dependencies = [ - "async-compression", - "crc32fast", - "futures-lite", - "pin-project", - "thiserror", - "tokio", - "tokio-util", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -374,11 +378,36 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cc" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +dependencies = [ + "jobserver", + "libc", +] [[package]] name = "cfg-if" @@ -421,6 +450,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.4" @@ -470,6 +509,15 @@ dependencies = [ "error-code", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -557,6 +605,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.0" @@ -588,6 +651,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "deflate64" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" + [[package]] name = "deranged" version = "0.3.11" @@ -597,6 +666,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.59", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -631,6 +711,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -824,6 +905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", + "libz-ng-sys", "miniz_oxide", ] @@ -1020,6 +1102,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.9" @@ -1260,6 +1351,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1281,6 +1381,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1322,6 +1431,16 @@ dependencies = [ "libc", ] +[[package]] +name = "libz-ng-sys" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6409efc61b12687963e602df8ecf70e8ddacf95bc6576bcf16e3ac6328083c5" +dependencies = [ + "cmake", + "libc", +] + [[package]] name = "line-wrap" version = "0.2.0" @@ -1366,7 +1485,6 @@ dependencies = [ "anyhow", "async-compression", "async-trait", - "async_zip", "blocking", "bstr", "chrono", @@ -1410,11 +1528,11 @@ dependencies = [ "thiserror", "tokio", "tokio-tungstenite", - "tokio-util", "toml", "tracing", "tracing-subscriber", "urlencoding", + "zip_next", ] [[package]] @@ -1446,6 +1564,16 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder 1.5.0", + "crc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1691,6 +1819,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2432,6 +2570,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -2790,7 +2934,6 @@ checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -2959,6 +3102,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.17.0" @@ -3391,3 +3540,67 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zip_next" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9519d1479ea50c3b79f1e00eacbb58e311c72c721e08313ebe64d8617a31b5d1" +dependencies = [ + "aes", + "arbitrary", + "byteorder 1.5.0", + "bzip2", + "constant_time_eq 0.3.0", + "crc32fast", + "crossbeam-utils", + "deflate64", + "flate2", + "hmac", + "lzma-rs", + "pbkdf2", + "sha1 0.10.6", + "time 0.3.36", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736" +dependencies = [ + "crc32fast", + "log", + "simd-adler32", + "typed-arena", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 2eace8d..9e35a0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,7 @@ cli = [ "dep:include_dir", "dep:regex", "dep:rustyline", - "dep:async_zip", - "dep:tokio-util", + "dep:zip_next", ] roblox = [ "dep:glam", @@ -139,6 +138,7 @@ regex = { optional = true, version = "1.7", default-features = false, features = "unicode-perl", ] } rustyline = { optional = true, version = "14.0" } +zip_next = { optional = true, version = "1.1" } ### ROBLOX @@ -152,10 +152,3 @@ 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"] } diff --git a/src/cli/build.rs b/src/cli/build.rs index a9b6bc8..f5471d1 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -1,22 +1,17 @@ use std::{ env::consts, - io::Cursor, + io::{Cursor, Read}, path::{Path, PathBuf}, process::ExitCode, }; use anyhow::{Context, Result}; -use async_zip::base::read::seek::ZipFileReader; use clap::Parser; use console::style; use directories::BaseDirs; use once_cell::sync::Lazy; use thiserror::Error; -use tokio::{ - fs, - io::{AsyncReadExt, AsyncWriteExt}, -}; -use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; +use tokio::{fs, io::AsyncWriteExt, task::spawn_blocking}; use crate::standalone::metadata::{Metadata, CURRENT_EXE}; @@ -30,7 +25,7 @@ const TARGET_BASE_DIR: Lazy = Lazy::new(|| { .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 @@ -64,9 +59,8 @@ impl BuildCommand { // Read the contents of the lune interpreter as our starting point println!( - "{} standalone binary using {}", - style("Compile").green().bold(), - style(input_path_displayed).underlined() + "Compiling standalone binary from {}", + style(input_path_displayed).green() ); let patched_bin = Metadata::create_env_patched_bin(base_exe_path, source_code.clone()) .await @@ -74,9 +68,8 @@ impl BuildCommand { // And finally write the patched binary to the output file println!( - " {} standalone binary to {}", - style("Write").blue().bold(), - style(output_path.display()).underlined() + "Writing standalone binary to {}", + style(output_path.display()).blue() ); write_executable_file_to(output_path, patched_bin).await?; // Read & execute for all, write for owner @@ -84,7 +77,10 @@ impl BuildCommand { } } -async fn write_executable_file_to(path: impl AsRef, bytes: impl AsRef<[u8]>) -> Result<()> { +async fn write_executable_file_to( + path: impl AsRef, + bytes: impl AsRef<[u8]>, +) -> Result<(), std::io::Error> { let mut options = fs::OpenOptions::new(); options.write(true).create(true).truncate(true); @@ -99,27 +95,30 @@ async fn write_executable_file_to(path: impl AsRef, bytes: impl AsRef<[u8] Ok(()) } -/// Possible ways in which the discovery and/or download of a base binary's path can error +/// Errors that may occur when building a standalone binary #[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), +pub enum BuildError { + #[error("failed to find lune target '{0}' in GitHub release")] + ReleaseTargetNotFound(String), + #[error("failed to find lune binary '{0}' in downloaded zip file")] + ZippedBinaryNotFound(String), + #[error("failed to download lune binary: {0}")] + Download(#[from] reqwest::Error), + #[error("failed to unzip lune binary: {0}")] + Unzip(#[from] zip_next::result::ZipError), + #[error("panicked while unzipping lune binary: {0}")] + UnzipJoin(#[from] tokio::task::JoinError), + #[error("io error: {0}")] + IoError(#[from] std::io::Error), } +pub type BuildResult = std::result::Result; + /// Discovers the path to the base executable to use for cross-compilation async fn get_base_exe_path( target: Option, output_path: PathBuf, -) -> Result<(PathBuf, PathBuf), BasePathDiscoveryError> { +) -> BuildResult<(PathBuf, PathBuf)> { if let Some(target_inner) = target { let current_target = format!("{}-{}", consts::OS, consts::ARCH); @@ -140,16 +139,13 @@ async fn get_base_exe_path( // 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)?; + fs::create_dir_all(TARGET_BASE_DIR.to_path_buf()).await?; } // 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?; + println!("Requested target does not exist in cache and must be downloaded"); + download_target_to_cache(target_inner, target_exe_extension, &path).await?; } Ok((path, output_path.with_extension(target_exe_extension))) @@ -162,89 +158,67 @@ async fn get_base_exe_path( } } -async fn cache_target( +/// Downloads the target base executable to the cache directory +async fn download_target_to_cache( target: String, target_exe_extension: &str, path: &PathBuf, -) -> Result<(), BasePathDiscoveryError> { +) -> BuildResult<()> { + let version = env!("CARGO_PKG_VERSION"); + let target_triple = format!("lune-{version}-{target}"); + 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 + "{base_url}/v{version}/{target_triple}.zip", + base_url = "https://github.com/lune-org/lune/releases/download", ); + println!("Downloading {target_triple}"); - 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 }); + // Try to request to download the zip file from the target url, + // making sure transient errors are handled gracefully and + // with a different error message than "not found" + let response = reqwest::get(release_url).await?; + if !response.status().is_success() { + if response.status().as_u16() == 404 { + return Err(BuildError::ReleaseTargetNotFound(target)); + } + return Err(BuildError::Download( + response.error_for_status().unwrap_err(), + )); } - // 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(), + // Receive the full zip file + let zip_bytes = response.bytes().await?.to_vec(); + let zip_file = Cursor::new(zip_bytes); + + // Look for and extract the binary file from the zip file + let binary_file_name = format!( + "lune{}{target_exe_extension}", + if target_exe_extension.is_empty() { + "" + } else { + "." + } ); - // Construct a decoder and decompress the ZIP file using deflate - let mut decoder = ZipFileReader::new(compressed_data.compat()) - .await - .map_err(BasePathDiscoveryError::Decompression)?; + // NOTE: We use spawn_blocking here since reading a + // zip archive is a somewhat slow / blocking operation + let binary_file_handle = spawn_blocking(move || { + let mut archive = zip_next::ZipArchive::new(zip_file)?; - let mut decompressed = vec![]; + let mut binary = Vec::new(); + archive + .by_name(&binary_file_name) + .or(Err(BuildError::ZippedBinaryNotFound(binary_file_name)))? + .read_to_end(&mut binary)?; - 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)?; + Ok::<_, BuildError>(binary) + }); + let binary_file_contents = binary_file_handle.await??; // Finally write the decompressed data to the target base directory - write_executable_file_to(&path, decompressed) - .await - .map_err(BasePathDiscoveryError::IoError)?; + write_executable_file_to(&path, binary_file_contents).await?; - println!( - " {} {}", - style("Downloaded").blue(), - style(target_full_display).underlined() - ); + println!("Downloaded {target_triple} successfully"); Ok(()) } From cec77a9bd95de90bb7bc86d65354f1aabaafd076 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 17:19:38 +0200 Subject: [PATCH 10/22] Move standalone build into its own module --- src/cli/{build.rs => build/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/cli/{build.rs => build/mod.rs} (100%) diff --git a/src/cli/build.rs b/src/cli/build/mod.rs similarity index 100% rename from src/cli/build.rs rename to src/cli/build/mod.rs From a11c1558ed6b7f5e33384d64606747b53aca7c83 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 21:49:58 +0200 Subject: [PATCH 11/22] Implement strict target enum for standalone builds, improve paths logic --- src/cli/build/mod.rs | 155 +++++++++++++++++---------------------- src/cli/build/target.rs | 159 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 89 deletions(-) create mode 100644 src/cli/build/target.rs diff --git a/src/cli/build/mod.rs b/src/cli/build/mod.rs index f5471d1..6c03644 100644 --- a/src/cli/build/mod.rs +++ b/src/cli/build/mod.rs @@ -1,29 +1,20 @@ use std::{ - env::consts, io::{Cursor, Read}, path::{Path, PathBuf}, process::ExitCode, }; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use clap::Parser; use console::style; -use directories::BaseDirs; -use once_cell::sync::Lazy; use thiserror::Error; use tokio::{fs, io::AsyncWriteExt, task::spawn_blocking}; use crate::standalone::metadata::{Metadata, CURRENT_EXE}; -const TARGET_BASE_DIR: Lazy = Lazy::new(|| { - BaseDirs::new() - .unwrap() - .home_dir() - .to_path_buf() - .join(".lune") - .join("target") - .join(env!("CARGO_PKG_VERSION")) -}); +mod target; + +use self::target::{Target, CACHE_DIR}; /// Build a standalone executable #[derive(Debug, Clone, Parser)] @@ -36,33 +27,45 @@ pub struct BuildCommand { #[clap(short, long)] pub output: Option, - /// The target to compile for - defaults to the host triple + /// The target to compile for in the format `os-arch` - + /// defaults to the os and arch of the current system #[clap(short, long)] - pub target: Option, + pub target: Option, } impl BuildCommand { pub async fn run(self) -> Result { + // Derive target spec to use, or default to the current host system + let target = self.target.unwrap_or_else(Target::current_system); + + // Derive paths to use, and make sure the output path is + // not the same as the input, so that we don't overwrite it let output_path = self .output - .unwrap_or_else(|| self.input.with_extension(consts::EXE_EXTENSION)); - - let input_path_displayed = self.input.display(); + .clone() + .unwrap_or_else(|| remove_source_file_ext(&self.input)); + let output_path = output_path.with_extension(target.exe_extension()); + if output_path == self.input { + if self.output.is_some() { + bail!("output path cannot be the same as input path"); + } + bail!("output path cannot be the same as input path, please specify a different output path"); + } // 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?; + // Derive the base executable path based on the arguments provided + let base_exe_path = get_or_download_base_executable(target).await?; // Read the contents of the lune interpreter as our starting point println!( "Compiling standalone binary from {}", - style(input_path_displayed).green() + style(self.input.display()).green() ); - let patched_bin = Metadata::create_env_patched_bin(base_exe_path, source_code.clone()) + let patched_bin = Metadata::create_env_patched_bin(base_exe_path, source_code) .await .context("failed to create patched binary")?; @@ -77,7 +80,21 @@ impl BuildCommand { } } -async fn write_executable_file_to( +/// Removes the source file extension from the given path, if it has one +/// A source file extension is an extension such as `.lua` or `.luau` +pub fn remove_source_file_ext(path: &Path) -> PathBuf { + if path + .extension() + .is_some_and(|ext| matches!(ext.to_str(), Some("lua" | "luau"))) + { + path.with_extension("") + } else { + path.to_path_buf() + } +} + +/// Writes the given bytes to a file at the specified path, and makes sure it has permissions to be executed +pub async fn write_executable_file_to( path: impl AsRef, bytes: impl AsRef<[u8]>, ) -> Result<(), std::io::Error> { @@ -99,7 +116,7 @@ async fn write_executable_file_to( #[derive(Debug, Error)] pub enum BuildError { #[error("failed to find lune target '{0}' in GitHub release")] - ReleaseTargetNotFound(String), + ReleaseTargetNotFound(Target), #[error("failed to find lune binary '{0}' in downloaded zip file")] ZippedBinaryNotFound(String), #[error("failed to download lune binary: {0}")] @@ -114,56 +131,20 @@ pub enum BuildError { pub type BuildResult = std::result::Result; -/// Discovers the path to the base executable to use for cross-compilation -async fn get_base_exe_path( - target: Option, - output_path: PathBuf, -) -> BuildResult<(PathBuf, PathBuf)> { - 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?; - } - - // If a cached target base executable doesn't exist, attempt to download it - if !path.exists() { - println!("Requested target does not exist in cache and must be downloaded"); - download_target_to_cache(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), - )) +/// Discovers the path to the base executable to use for cross-compilation, and downloads it if necessary +pub async fn get_or_download_base_executable(target: Target) -> BuildResult { + // If the target matches the current system, just use the current executable + if target.is_current_system() { + return Ok(CURRENT_EXE.to_path_buf()); } -} -/// Downloads the target base executable to the cache directory -async fn download_target_to_cache( - target: String, - target_exe_extension: &str, - path: &PathBuf, -) -> BuildResult<()> { + // If a cached target base executable doesn't exist, attempt to download it + if !target.cache_path().exists() { + return Ok(target.cache_path()); + } + + // The target is not cached, we must download it + println!("Requested target '{target}' does not exist in cache"); let version = env!("CARGO_PKG_VERSION"); let target_triple = format!("lune-{version}-{target}"); @@ -171,7 +152,9 @@ async fn download_target_to_cache( "{base_url}/v{version}/{target_triple}.zip", base_url = "https://github.com/lune-org/lune/releases/download", ); - println!("Downloading {target_triple}"); + + // NOTE: This is not entirely accurate, but it is clearer for a user + println!("Downloading {target_triple}{}...", target.exe_suffix()); // Try to request to download the zip file from the target url, // making sure transient errors are handled gracefully and @@ -191,17 +174,9 @@ async fn download_target_to_cache( let zip_file = Cursor::new(zip_bytes); // Look for and extract the binary file from the zip file - let binary_file_name = format!( - "lune{}{target_exe_extension}", - if target_exe_extension.is_empty() { - "" - } else { - "." - } - ); - - // NOTE: We use spawn_blocking here since reading a - // zip archive is a somewhat slow / blocking operation + // NOTE: We use spawn_blocking here since reading a zip + // archive is a somewhat slow / blocking operation + let binary_file_name = format!("lune{}", target.exe_suffix()); let binary_file_handle = spawn_blocking(move || { let mut archive = zip_next::ZipArchive::new(zip_file)?; @@ -215,10 +190,12 @@ async fn download_target_to_cache( }); let binary_file_contents = binary_file_handle.await??; - // Finally write the decompressed data to the target base directory - write_executable_file_to(&path, binary_file_contents).await?; + // Finally, write the extracted binary to the cache + if !CACHE_DIR.exists() { + fs::create_dir_all(CACHE_DIR.as_path()).await?; + } + write_executable_file_to(target.cache_path(), binary_file_contents).await?; + println!("Downloaded successfully and added to cache"); - println!("Downloaded {target_triple} successfully"); - - Ok(()) + Ok(target.cache_path()) } diff --git a/src/cli/build/target.rs b/src/cli/build/target.rs new file mode 100644 index 0000000..993c5c7 --- /dev/null +++ b/src/cli/build/target.rs @@ -0,0 +1,159 @@ +use std::{env::consts::ARCH, fmt, path::PathBuf, str::FromStr}; + +use directories::BaseDirs; +use once_cell::sync::Lazy; + +const HOME_DIR: Lazy = Lazy::new(|| { + BaseDirs::new() + .expect("could not find home directory") + .home_dir() + .to_path_buf() +}); + +pub const CACHE_DIR: Lazy = Lazy::new(|| HOME_DIR.join(".lune").join("target")); + +/// A target operating system supported by Lune +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TargetOS { + Windows, + Linux, + MacOS, +} + +impl TargetOS { + fn current_system() -> Self { + match std::env::consts::OS { + "windows" => Self::Windows, + "linux" => Self::Linux, + "macos" => Self::MacOS, + _ => panic!("unsupported target OS"), + } + } + + fn exe_extension(self) -> &'static str { + // NOTE: We can't use the constants from std since + // they are only accessible for the current target + match self { + Self::Windows => "exe", + _ => "", + } + } + + fn exe_suffix(self) -> &'static str { + match self { + Self::Windows => ".exe", + _ => "", + } + } +} + +impl fmt::Display for TargetOS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Windows => write!(f, "windows"), + Self::Linux => write!(f, "linux"), + Self::MacOS => write!(f, "macos"), + } + } +} + +impl FromStr for TargetOS { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_str() { + "windows" => Ok(Self::Windows), + "linux" => Ok(Self::Linux), + "macos" => Ok(Self::MacOS), + _ => Err("invalid target OS"), + } + } +} + +/// A target architecture supported by Lune +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TargetArch { + X86_64, + Aarch64, +} + +impl TargetArch { + fn current_system() -> Self { + match ARCH { + "x86_64" => Self::X86_64, + "aarch64" => Self::Aarch64, + _ => panic!("unsupported target architecture"), + } + } +} + +impl fmt::Display for TargetArch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::X86_64 => write!(f, "x86_64"), + Self::Aarch64 => write!(f, "aarch64"), + } + } +} + +impl FromStr for TargetArch { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_str() { + "x86_64" | "x64" => Ok(Self::X86_64), + "aarch64" | "arm64" => Ok(Self::Aarch64), + _ => Err("invalid target architecture"), + } + } +} + +/// A full target description for cross-compilation (OS + Arch) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Target { + pub os: TargetOS, + pub arch: TargetArch, +} + +impl Target { + pub fn current_system() -> Self { + Self { + os: TargetOS::current_system(), + arch: TargetArch::current_system(), + } + } + + pub fn is_current_system(&self) -> bool { + self.os == TargetOS::current_system() && self.arch == TargetArch::current_system() + } + + pub fn exe_extension(&self) -> &'static str { + self.os.exe_extension() + } + + pub fn exe_suffix(&self) -> &'static str { + self.os.exe_suffix() + } + + pub fn cache_path(&self) -> PathBuf { + CACHE_DIR.join(format!("{self}{}", self.os.exe_extension())) + } +} + +impl fmt::Display for Target { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}-{}", self.os, self.arch) + } +} + +impl FromStr for Target { + type Err = &'static str; + fn from_str(s: &str) -> Result { + let (left, right) = s + .split_once('-') + .ok_or("target must be in the form `os-arch`")?; + + let os = left.parse()?; + let arch = right.parse()?; + + Ok(Self { os, arch }) + } +} From b8196d6284b161bc647ae8d33730f8e30185f0b3 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 21:59:17 +0200 Subject: [PATCH 12/22] Organize everything neatly into files --- src/cli/build/base_exe.rs | 83 ++++++++++++++++++++++ src/cli/build/files.rs | 36 ++++++++++ src/cli/build/mod.rs | 142 +++----------------------------------- src/cli/build/result.rs | 22 ++++++ src/cli/build/target.rs | 38 +++++----- 5 files changed, 171 insertions(+), 150 deletions(-) create mode 100644 src/cli/build/base_exe.rs create mode 100644 src/cli/build/files.rs create mode 100644 src/cli/build/result.rs diff --git a/src/cli/build/base_exe.rs b/src/cli/build/base_exe.rs new file mode 100644 index 0000000..39dfd12 --- /dev/null +++ b/src/cli/build/base_exe.rs @@ -0,0 +1,83 @@ +use std::{ + io::{Cursor, Read}, + path::PathBuf, +}; + +use tokio::{fs, task}; + +use crate::standalone::metadata::CURRENT_EXE; + +use super::{ + files::write_executable_file_to, + result::{BuildError, BuildResult}, + target::{BuildTarget, CACHE_DIR}, +}; + +/// Discovers the path to the base executable to use for cross-compilation, and downloads it if necessary +pub async fn get_or_download_base_executable(target: BuildTarget) -> BuildResult { + // If the target matches the current system, just use the current executable + if target.is_current_system() { + return Ok(CURRENT_EXE.to_path_buf()); + } + + // If a cached target base executable doesn't exist, attempt to download it + if !target.cache_path().exists() { + return Ok(target.cache_path()); + } + + // The target is not cached, we must download it + println!("Requested target '{target}' does not exist in cache"); + let version = env!("CARGO_PKG_VERSION"); + let target_triple = format!("lune-{version}-{target}"); + + let release_url = format!( + "{base_url}/v{version}/{target_triple}.zip", + base_url = "https://github.com/lune-org/lune/releases/download", + ); + + // NOTE: This is not entirely accurate, but it is clearer for a user + println!("Downloading {target_triple}{}...", target.exe_suffix()); + + // Try to request to download the zip file from the target url, + // making sure transient errors are handled gracefully and + // with a different error message than "not found" + let response = reqwest::get(release_url).await?; + if !response.status().is_success() { + if response.status().as_u16() == 404 { + return Err(BuildError::ReleaseTargetNotFound(target)); + } + return Err(BuildError::Download( + response.error_for_status().unwrap_err(), + )); + } + + // Receive the full zip file + let zip_bytes = response.bytes().await?.to_vec(); + let zip_file = Cursor::new(zip_bytes); + + // Look for and extract the binary file from the zip file + // NOTE: We use spawn_blocking here since reading a zip + // archive is a somewhat slow / blocking operation + let binary_file_name = format!("lune{}", target.exe_suffix()); + let binary_file_handle = task::spawn_blocking(move || { + let mut archive = zip_next::ZipArchive::new(zip_file)?; + + let mut binary = Vec::new(); + archive + .by_name(&binary_file_name) + .or(Err(BuildError::ZippedBinaryNotFound(binary_file_name)))? + .read_to_end(&mut binary)?; + + Ok::<_, BuildError>(binary) + }); + let binary_file_contents = binary_file_handle.await??; + + // Finally, write the extracted binary to the cache + if !CACHE_DIR.exists() { + fs::create_dir_all(CACHE_DIR.as_path()).await?; + } + write_executable_file_to(target.cache_path(), binary_file_contents).await?; + println!("Downloaded successfully and added to cache"); + + Ok(target.cache_path()) +} diff --git a/src/cli/build/files.rs b/src/cli/build/files.rs new file mode 100644 index 0000000..f35af21 --- /dev/null +++ b/src/cli/build/files.rs @@ -0,0 +1,36 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use tokio::{fs, io::AsyncWriteExt}; + +/// Removes the source file extension from the given path, if it has one +/// A source file extension is an extension such as `.lua` or `.luau` +pub fn remove_source_file_ext(path: &Path) -> PathBuf { + if path + .extension() + .is_some_and(|ext| matches!(ext.to_str(), Some("lua" | "luau"))) + { + path.with_extension("") + } else { + path.to_path_buf() + } +} + +/// Writes the given bytes to a file at the specified path, and makes sure it has permissions to be executed +pub async fn write_executable_file_to( + path: impl AsRef, + bytes: impl AsRef<[u8]>, +) -> Result<(), std::io::Error> { + 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(()) +} diff --git a/src/cli/build/mod.rs b/src/cli/build/mod.rs index 6c03644..8079131 100644 --- a/src/cli/build/mod.rs +++ b/src/cli/build/mod.rs @@ -1,20 +1,20 @@ -use std::{ - io::{Cursor, Read}, - path::{Path, PathBuf}, - process::ExitCode, -}; +use std::{path::PathBuf, process::ExitCode}; use anyhow::{bail, Context, Result}; use clap::Parser; use console::style; -use thiserror::Error; -use tokio::{fs, io::AsyncWriteExt, task::spawn_blocking}; +use tokio::fs; -use crate::standalone::metadata::{Metadata, CURRENT_EXE}; +use crate::standalone::metadata::Metadata; +mod base_exe; +mod files; +mod result; mod target; -use self::target::{Target, CACHE_DIR}; +use self::base_exe::get_or_download_base_executable; +use self::files::{remove_source_file_ext, write_executable_file_to}; +use self::target::BuildTarget; /// Build a standalone executable #[derive(Debug, Clone, Parser)] @@ -30,13 +30,13 @@ pub struct BuildCommand { /// The target to compile for in the format `os-arch` - /// defaults to the os and arch of the current system #[clap(short, long)] - pub target: Option, + pub target: Option, } impl BuildCommand { pub async fn run(self) -> Result { // Derive target spec to use, or default to the current host system - let target = self.target.unwrap_or_else(Target::current_system); + let target = self.target.unwrap_or_else(BuildTarget::current_system); // Derive paths to use, and make sure the output path is // not the same as the input, so that we don't overwrite it @@ -79,123 +79,3 @@ impl BuildCommand { Ok(ExitCode::SUCCESS) } } - -/// Removes the source file extension from the given path, if it has one -/// A source file extension is an extension such as `.lua` or `.luau` -pub fn remove_source_file_ext(path: &Path) -> PathBuf { - if path - .extension() - .is_some_and(|ext| matches!(ext.to_str(), Some("lua" | "luau"))) - { - path.with_extension("") - } else { - path.to_path_buf() - } -} - -/// Writes the given bytes to a file at the specified path, and makes sure it has permissions to be executed -pub async fn write_executable_file_to( - path: impl AsRef, - bytes: impl AsRef<[u8]>, -) -> Result<(), std::io::Error> { - 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(()) -} - -/// Errors that may occur when building a standalone binary -#[derive(Debug, Error)] -pub enum BuildError { - #[error("failed to find lune target '{0}' in GitHub release")] - ReleaseTargetNotFound(Target), - #[error("failed to find lune binary '{0}' in downloaded zip file")] - ZippedBinaryNotFound(String), - #[error("failed to download lune binary: {0}")] - Download(#[from] reqwest::Error), - #[error("failed to unzip lune binary: {0}")] - Unzip(#[from] zip_next::result::ZipError), - #[error("panicked while unzipping lune binary: {0}")] - UnzipJoin(#[from] tokio::task::JoinError), - #[error("io error: {0}")] - IoError(#[from] std::io::Error), -} - -pub type BuildResult = std::result::Result; - -/// Discovers the path to the base executable to use for cross-compilation, and downloads it if necessary -pub async fn get_or_download_base_executable(target: Target) -> BuildResult { - // If the target matches the current system, just use the current executable - if target.is_current_system() { - return Ok(CURRENT_EXE.to_path_buf()); - } - - // If a cached target base executable doesn't exist, attempt to download it - if !target.cache_path().exists() { - return Ok(target.cache_path()); - } - - // The target is not cached, we must download it - println!("Requested target '{target}' does not exist in cache"); - let version = env!("CARGO_PKG_VERSION"); - let target_triple = format!("lune-{version}-{target}"); - - let release_url = format!( - "{base_url}/v{version}/{target_triple}.zip", - base_url = "https://github.com/lune-org/lune/releases/download", - ); - - // NOTE: This is not entirely accurate, but it is clearer for a user - println!("Downloading {target_triple}{}...", target.exe_suffix()); - - // Try to request to download the zip file from the target url, - // making sure transient errors are handled gracefully and - // with a different error message than "not found" - let response = reqwest::get(release_url).await?; - if !response.status().is_success() { - if response.status().as_u16() == 404 { - return Err(BuildError::ReleaseTargetNotFound(target)); - } - return Err(BuildError::Download( - response.error_for_status().unwrap_err(), - )); - } - - // Receive the full zip file - let zip_bytes = response.bytes().await?.to_vec(); - let zip_file = Cursor::new(zip_bytes); - - // Look for and extract the binary file from the zip file - // NOTE: We use spawn_blocking here since reading a zip - // archive is a somewhat slow / blocking operation - let binary_file_name = format!("lune{}", target.exe_suffix()); - let binary_file_handle = spawn_blocking(move || { - let mut archive = zip_next::ZipArchive::new(zip_file)?; - - let mut binary = Vec::new(); - archive - .by_name(&binary_file_name) - .or(Err(BuildError::ZippedBinaryNotFound(binary_file_name)))? - .read_to_end(&mut binary)?; - - Ok::<_, BuildError>(binary) - }); - let binary_file_contents = binary_file_handle.await??; - - // Finally, write the extracted binary to the cache - if !CACHE_DIR.exists() { - fs::create_dir_all(CACHE_DIR.as_path()).await?; - } - write_executable_file_to(target.cache_path(), binary_file_contents).await?; - println!("Downloaded successfully and added to cache"); - - Ok(target.cache_path()) -} diff --git a/src/cli/build/result.rs b/src/cli/build/result.rs new file mode 100644 index 0000000..f987c9e --- /dev/null +++ b/src/cli/build/result.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +use super::target::BuildTarget; + +/// Errors that may occur when building a standalone binary +#[derive(Debug, Error)] +pub enum BuildError { + #[error("failed to find lune target '{0}' in GitHub release")] + ReleaseTargetNotFound(BuildTarget), + #[error("failed to find lune binary '{0}' in downloaded zip file")] + ZippedBinaryNotFound(String), + #[error("failed to download lune binary: {0}")] + Download(#[from] reqwest::Error), + #[error("failed to unzip lune binary: {0}")] + Unzip(#[from] zip_next::result::ZipError), + #[error("panicked while unzipping lune binary: {0}")] + UnzipJoin(#[from] tokio::task::JoinError), + #[error("io error: {0}")] + IoError(#[from] std::io::Error), +} + +pub type BuildResult = std::result::Result; diff --git a/src/cli/build/target.rs b/src/cli/build/target.rs index 993c5c7..0cbff94 100644 --- a/src/cli/build/target.rs +++ b/src/cli/build/target.rs @@ -14,13 +14,13 @@ pub const CACHE_DIR: Lazy = Lazy::new(|| HOME_DIR.join(".lune").join("t /// A target operating system supported by Lune #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TargetOS { +pub enum BuildTargetOS { Windows, Linux, MacOS, } -impl TargetOS { +impl BuildTargetOS { fn current_system() -> Self { match std::env::consts::OS { "windows" => Self::Windows, @@ -47,7 +47,7 @@ impl TargetOS { } } -impl fmt::Display for TargetOS { +impl fmt::Display for BuildTargetOS { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Windows => write!(f, "windows"), @@ -57,13 +57,13 @@ impl fmt::Display for TargetOS { } } -impl FromStr for TargetOS { +impl FromStr for BuildTargetOS { type Err = &'static str; fn from_str(s: &str) -> Result { match s.trim().to_ascii_lowercase().as_str() { - "windows" => Ok(Self::Windows), + "win" | "windows" => Ok(Self::Windows), "linux" => Ok(Self::Linux), - "macos" => Ok(Self::MacOS), + "mac" | "macos" | "darwin" => Ok(Self::MacOS), _ => Err("invalid target OS"), } } @@ -71,12 +71,12 @@ impl FromStr for TargetOS { /// A target architecture supported by Lune #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TargetArch { +pub enum BuildTargetArch { X86_64, Aarch64, } -impl TargetArch { +impl BuildTargetArch { fn current_system() -> Self { match ARCH { "x86_64" => Self::X86_64, @@ -86,7 +86,7 @@ impl TargetArch { } } -impl fmt::Display for TargetArch { +impl fmt::Display for BuildTargetArch { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::X86_64 => write!(f, "x86_64"), @@ -95,7 +95,7 @@ impl fmt::Display for TargetArch { } } -impl FromStr for TargetArch { +impl FromStr for BuildTargetArch { type Err = &'static str; fn from_str(s: &str) -> Result { match s.trim().to_ascii_lowercase().as_str() { @@ -108,21 +108,21 @@ impl FromStr for TargetArch { /// A full target description for cross-compilation (OS + Arch) #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Target { - pub os: TargetOS, - pub arch: TargetArch, +pub struct BuildTarget { + pub os: BuildTargetOS, + pub arch: BuildTargetArch, } -impl Target { +impl BuildTarget { pub fn current_system() -> Self { Self { - os: TargetOS::current_system(), - arch: TargetArch::current_system(), + os: BuildTargetOS::current_system(), + arch: BuildTargetArch::current_system(), } } pub fn is_current_system(&self) -> bool { - self.os == TargetOS::current_system() && self.arch == TargetArch::current_system() + self.os == BuildTargetOS::current_system() && self.arch == BuildTargetArch::current_system() } pub fn exe_extension(&self) -> &'static str { @@ -138,13 +138,13 @@ impl Target { } } -impl fmt::Display for Target { +impl fmt::Display for BuildTarget { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}-{}", self.os, self.arch) } } -impl FromStr for Target { +impl FromStr for BuildTarget { type Err = &'static str; fn from_str(s: &str) -> Result { let (left, right) = s From 476125cc74a01ff3cd8522a0fee2a9d50496d904 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 21:59:57 +0200 Subject: [PATCH 13/22] Fix clippy lint in net builtin --- src/lune/builtins/net/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lune/builtins/net/mod.rs b/src/lune/builtins/net/mod.rs index 9943c1f..9449e6b 100644 --- a/src/lune/builtins/net/mod.rs +++ b/src/lune/builtins/net/mod.rs @@ -46,7 +46,7 @@ fn net_json_encode<'lua>( .serialize_to_string(lua, val) } -fn net_json_decode<'lua>(lua: &'lua Lua, json: BString) -> LuaResult> { +fn net_json_decode(lua: &Lua, json: BString) -> LuaResult { EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json) } From 70f0c55d354dbd23b6fcc589c1b1d80dba0f1cf9 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 22:08:18 +0200 Subject: [PATCH 14/22] Do a pass over all doc comments in new build modules --- src/cli/build/base_exe.rs | 13 ++++++++----- src/cli/build/files.rs | 12 +++++++++--- src/cli/build/mod.rs | 4 +++- src/cli/build/result.rs | 4 +++- src/cli/build/target.rs | 24 +++++++++++++++++++++--- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/cli/build/base_exe.rs b/src/cli/build/base_exe.rs index 39dfd12..d8507cc 100644 --- a/src/cli/build/base_exe.rs +++ b/src/cli/build/base_exe.rs @@ -13,15 +13,18 @@ use super::{ target::{BuildTarget, CACHE_DIR}, }; -/// Discovers the path to the base executable to use for cross-compilation, and downloads it if necessary +/** + Discovers the path to the base executable to use for cross-compilation. + + If the target is the same as the current system, the current executable is used. + + If no binary exists at the target path, it will attempt to download it from the internet. +*/ pub async fn get_or_download_base_executable(target: BuildTarget) -> BuildResult { - // If the target matches the current system, just use the current executable if target.is_current_system() { return Ok(CURRENT_EXE.to_path_buf()); } - - // If a cached target base executable doesn't exist, attempt to download it - if !target.cache_path().exists() { + if target.cache_path().exists() { return Ok(target.cache_path()); } diff --git a/src/cli/build/files.rs b/src/cli/build/files.rs index f35af21..cf1d003 100644 --- a/src/cli/build/files.rs +++ b/src/cli/build/files.rs @@ -3,8 +3,11 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use tokio::{fs, io::AsyncWriteExt}; -/// Removes the source file extension from the given path, if it has one -/// A source file extension is an extension such as `.lua` or `.luau` +/** + Removes the source file extension from the given path, if it has one. + + A source file extension is an extension such as `.lua` or `.luau`. +*/ pub fn remove_source_file_ext(path: &Path) -> PathBuf { if path .extension() @@ -16,7 +19,10 @@ pub fn remove_source_file_ext(path: &Path) -> PathBuf { } } -/// Writes the given bytes to a file at the specified path, and makes sure it has permissions to be executed +/** + Writes the given bytes to a file at the specified path, + and makes sure it has permissions to be executed. +*/ pub async fn write_executable_file_to( path: impl AsRef, bytes: impl AsRef<[u8]>, diff --git a/src/cli/build/mod.rs b/src/cli/build/mod.rs index 8079131..6546a16 100644 --- a/src/cli/build/mod.rs +++ b/src/cli/build/mod.rs @@ -52,7 +52,9 @@ impl BuildCommand { bail!("output path cannot be the same as input path, please specify a different output path"); } - // Try to read the input file + // Try to read the given input file + // FUTURE: We should try and resolve a full require file graph using the input + // path here instead, see the notes in the `standalone` module for more details let source_code = fs::read(&self.input) .await .context("failed to read input file")?; diff --git a/src/cli/build/result.rs b/src/cli/build/result.rs index f987c9e..1131c34 100644 --- a/src/cli/build/result.rs +++ b/src/cli/build/result.rs @@ -2,7 +2,9 @@ use thiserror::Error; use super::target::BuildTarget; -/// Errors that may occur when building a standalone binary +/** + Errors that may occur when building a standalone binary +*/ #[derive(Debug, Error)] pub enum BuildError { #[error("failed to find lune target '{0}' in GitHub release")] diff --git a/src/cli/build/target.rs b/src/cli/build/target.rs index 0cbff94..7ed9dd1 100644 --- a/src/cli/build/target.rs +++ b/src/cli/build/target.rs @@ -12,7 +12,9 @@ const HOME_DIR: Lazy = Lazy::new(|| { pub const CACHE_DIR: Lazy = Lazy::new(|| HOME_DIR.join(".lune").join("target")); -/// A target operating system supported by Lune +/** + A target operating system supported by Lune +*/ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BuildTargetOS { Windows, @@ -69,7 +71,9 @@ impl FromStr for BuildTargetOS { } } -/// A target architecture supported by Lune +/** + A target architecture supported by Lune +*/ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BuildTargetArch { X86_64, @@ -106,7 +110,21 @@ impl FromStr for BuildTargetArch { } } -/// A full target description for cross-compilation (OS + Arch) +/** + A full target description that Lune supports (OS + Arch) + + This is used to determine the target to build for standalone binaries, + and to download the correct base executable for cross-compilation. + + The target may be parsed from and displayed in the form `os-arch`. + Examples of valid targets are: + + - `linux-aarch64` + - `linux-x86_64` + - `macos-aarch64` + - `macos-x86_64` + - `windows-x86_64` +*/ #[derive(Debug, Clone, PartialEq, Eq)] pub struct BuildTarget { pub os: BuildTargetOS, From e11302766bfa8fc69d087bb7e17754056ad8a869 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 22:09:48 +0200 Subject: [PATCH 15/22] Add scaffolding for new regex builtin --- src/lune/builtins/mod.rs | 5 +++++ src/lune/builtins/regex/mod.rs | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 src/lune/builtins/regex/mod.rs diff --git a/src/lune/builtins/mod.rs b/src/lune/builtins/mod.rs index 51d0290..8006a80 100644 --- a/src/lune/builtins/mod.rs +++ b/src/lune/builtins/mod.rs @@ -7,6 +7,7 @@ mod fs; mod luau; mod net; mod process; +mod regex; mod serde; mod stdio; mod task; @@ -22,6 +23,7 @@ pub enum LuneBuiltin { Net, Task, Process, + Regex, Serde, Stdio, #[cfg(feature = "roblox")] @@ -37,6 +39,7 @@ impl LuneBuiltin { Self::Net => "net", Self::Task => "task", Self::Process => "process", + Self::Regex => "regex", Self::Serde => "serde", Self::Stdio => "stdio", #[cfg(feature = "roblox")] @@ -52,6 +55,7 @@ impl LuneBuiltin { Self::Net => net::create(lua), Self::Task => task::create(lua), Self::Process => process::create(lua), + Self::Regex => regex::create(lua), Self::Serde => serde::create(lua), Self::Stdio => stdio::create(lua), #[cfg(feature = "roblox")] @@ -77,6 +81,7 @@ impl FromStr for LuneBuiltin { "net" => Ok(Self::Net), "task" => Ok(Self::Task), "process" => Ok(Self::Process), + "regex" => Ok(Self::Regex), "serde" => Ok(Self::Serde), "stdio" => Ok(Self::Stdio), #[cfg(feature = "roblox")] diff --git a/src/lune/builtins/regex/mod.rs b/src/lune/builtins/regex/mod.rs new file mode 100644 index 0000000..471062c --- /dev/null +++ b/src/lune/builtins/regex/mod.rs @@ -0,0 +1,7 @@ +use mlua::prelude::*; + +use crate::lune::util::TableBuilder; + +pub fn create(lua: &Lua) -> LuaResult { + TableBuilder::new(lua)?.build_readonly() +} From 12f5824da9b4763b5ddbb1a95f2c1235c94c1372 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 22:47:27 +0200 Subject: [PATCH 16/22] Port over regex implementation from fork, do some cleanup --- Cargo.lock | 7 +++ Cargo.toml | 9 ++-- src/lune/builtins/regex/captures.rs | 76 +++++++++++++++++++++++++++++ src/lune/builtins/regex/matches.rs | 48 ++++++++++++++++++ src/lune/builtins/regex/mod.rs | 16 +++++- src/lune/builtins/regex/regex.rs | 66 +++++++++++++++++++++++++ 6 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 src/lune/builtins/regex/captures.rs create mode 100644 src/lune/builtins/regex/matches.rs create mode 100644 src/lune/builtins/regex/regex.rs diff --git a/Cargo.lock b/Cargo.lock index 8ff4e5f..e2bb8e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1522,6 +1522,7 @@ dependencies = [ "regex", "reqwest", "rustyline", + "self_cell", "serde", "serde_json", "serde_yaml", @@ -2423,6 +2424,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "self_cell" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" + [[package]] name = "semver" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 9e35a0a..8199082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ cli = [ "dep:env_logger", "dep:clap", "dep:include_dir", - "dep:regex", "dep:rustyline", "dep:zip_next", ] @@ -76,7 +75,9 @@ path-clean = "1.0" pathdiff = "0.2" pin-project = "1.0" urlencoding = "2.1" -bstr = "1.9.1" +bstr = "1.9" +regex = "1.10" +self_cell = "1.0" ### RUNTIME @@ -133,10 +134,6 @@ env_logger = { optional = true, version = "0.11" } itertools = "0.12" clap = { optional = true, version = "4.1", features = ["derive"] } include_dir = { optional = true, version = "0.7", features = ["glob"] } -regex = { optional = true, version = "1.7", default-features = false, features = [ - "std", - "unicode-perl", -] } rustyline = { optional = true, version = "14.0" } zip_next = { optional = true, version = "1.1" } diff --git a/src/lune/builtins/regex/captures.rs b/src/lune/builtins/regex/captures.rs new file mode 100644 index 0000000..4dc67da --- /dev/null +++ b/src/lune/builtins/regex/captures.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use mlua::prelude::*; +use regex::{Captures, Regex}; +use self_cell::self_cell; + +use super::matches::LuaMatch; + +type OptionalCaptures<'a> = Option>; + +self_cell! { + struct LuaCapturesInner { + owner: Arc, + #[covariant] + dependent: OptionalCaptures, + } +} + +pub struct LuaCaptures { + inner: LuaCapturesInner, +} + +impl LuaCaptures { + pub fn new(pattern: &Regex, text: String) -> Self { + Self { + inner: LuaCapturesInner::new(Arc::from(text), |owned| pattern.captures(owned.as_str())), + } + } + + fn captures(&self) -> &Captures { + self.inner + .borrow_dependent() + .as_ref() + .expect("None captures should not be used") + } + + fn num_captures(&self) -> usize { + // NOTE: Here we exclude the match for the entire regex + // pattern, only counting the named and numbered captures + self.captures().len() - 1 + } + + fn text(&self) -> Arc { + Arc::clone(self.inner.borrow_owner()) + } +} + +impl LuaUserData for LuaCaptures { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("get", |_, this, n: usize| { + Ok(this + .captures() + .get(n) + .map(|m| LuaMatch::new(this.text(), m))) + }); + + methods.add_method("group", |_, this, group: String| { + Ok(this + .captures() + .name(&group) + .map(|m| LuaMatch::new(this.text(), m))) + }); + + methods.add_method("format", |_, this, format: String| { + let mut new = String::new(); + this.captures().expand(&format, &mut new); + Ok(new) + }); + + methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("RegexCaptures")); + methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures())); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("RegexCaptures({})", this.num_captures())) + }); + } +} diff --git a/src/lune/builtins/regex/matches.rs b/src/lune/builtins/regex/matches.rs new file mode 100644 index 0000000..60348e8 --- /dev/null +++ b/src/lune/builtins/regex/matches.rs @@ -0,0 +1,48 @@ +use std::{ops::Range, sync::Arc}; + +use mlua::prelude::*; +use regex::Match; + +pub struct LuaMatch { + text: Arc, + start: usize, + end: usize, +} + +impl LuaMatch { + pub fn new(text: Arc, matched: Match) -> Self { + Self { + text, + start: matched.start(), + end: matched.end(), + } + } + + fn range(&self) -> Range { + self.start..self.end + } + + fn slice(&self) -> &str { + &self.text[self.range()] + } +} + +impl LuaUserData for LuaMatch { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + // NOTE: Strings are 0 based in Rust but 1 based in Luau, and end of range in Rust is exclusive + fields.add_field_method_get("start", |_, this| Ok(this.start.saturating_add(1))); + fields.add_field_method_get("finish", |_, this| Ok(this.end)); + fields.add_field_method_get("len", |_, this| Ok(this.range().len())); + fields.add_field_method_get("text", |_, this| Ok(this.slice().to_string())); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("isEmpty", |_, this, ()| Ok(this.range().is_empty())); + + methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("RegexMatch")); + methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len())); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("RegexMatch({})", this.slice())) + }); + } +} diff --git a/src/lune/builtins/regex/mod.rs b/src/lune/builtins/regex/mod.rs index 471062c..bb674c2 100644 --- a/src/lune/builtins/regex/mod.rs +++ b/src/lune/builtins/regex/mod.rs @@ -1,7 +1,21 @@ +#![allow(clippy::module_inception)] + use mlua::prelude::*; use crate::lune::util::TableBuilder; +mod captures; +mod matches; +mod regex; + +use self::regex::LuaRegex; + pub fn create(lua: &Lua) -> LuaResult { - TableBuilder::new(lua)?.build_readonly() + TableBuilder::new(lua)? + .with_function("new", new_regex)? + .build_readonly() +} + +fn new_regex(_: &Lua, pattern: String) -> LuaResult { + LuaRegex::new(pattern) } diff --git a/src/lune/builtins/regex/regex.rs b/src/lune/builtins/regex/regex.rs new file mode 100644 index 0000000..b255caa --- /dev/null +++ b/src/lune/builtins/regex/regex.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use mlua::prelude::*; +use regex::Regex; + +use super::{captures::LuaCaptures, matches::LuaMatch}; + +pub struct LuaRegex { + inner: Regex, +} + +impl LuaRegex { + pub fn new(pattern: String) -> LuaResult { + Regex::new(&pattern) + .map(|inner| Self { inner }) + .map_err(LuaError::external) + } +} + +impl LuaUserData for LuaRegex { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("isMatch", |_, this, text: String| { + Ok(this.inner.is_match(&text)) + }); + + methods.add_method("find", |_, this, text: String| { + let arc = Arc::new(text); + Ok(this + .inner + .find(&arc) + .map(|m| LuaMatch::new(Arc::clone(&arc), m))) + }); + + methods.add_method("captures", |_, this, text: String| { + Ok(LuaCaptures::new(&this.inner, text)) + }); + + methods.add_method("split", |_, this, text: String| { + Ok(this + .inner + .split(&text) + .map(|s| s.to_string()) + .collect::>()) + }); + + // TODO: Determine whether it's desirable and / or feasible to support + // using a function or table for `replace` like in the lua string library + methods.add_method( + "replace", + |_, this, (haystack, replacer): (String, String)| { + Ok(this.inner.replace(&haystack, replacer).to_string()) + }, + ); + methods.add_method( + "replaceAll", + |_, this, (haystack, replacer): (String, String)| { + Ok(this.inner.replace_all(&haystack, replacer).to_string()) + }, + ); + + methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("Regex")); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("Regex({})", this.inner.as_str())) + }); + } +} From 54081c1b0f2b88386de85e4243fb27426af3abd3 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 22:54:12 +0200 Subject: [PATCH 17/22] Add docs on public functions, fix captures being returned even if not found --- src/lune/builtins/regex/captures.rs | 18 +++++++++++++++--- src/lune/builtins/regex/matches.rs | 6 ++++++ src/lune/builtins/regex/regex.rs | 7 +++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/lune/builtins/regex/captures.rs b/src/lune/builtins/regex/captures.rs index 4dc67da..a1283e9 100644 --- a/src/lune/builtins/regex/captures.rs +++ b/src/lune/builtins/regex/captures.rs @@ -16,14 +16,26 @@ self_cell! { } } +/** + A wrapper over the `regex::Captures` struct that can be used from Lua. +*/ pub struct LuaCaptures { inner: LuaCapturesInner, } impl LuaCaptures { - pub fn new(pattern: &Regex, text: String) -> Self { - Self { - inner: LuaCapturesInner::new(Arc::from(text), |owned| pattern.captures(owned.as_str())), + /** + Create a new `LuaCaptures` instance from a `Regex` pattern and a `String` text. + + Returns `Some(_)` if captures were found, `None` if no captures were found. + */ + pub fn new(pattern: &Regex, text: String) -> Option { + let inner = + LuaCapturesInner::new(Arc::from(text), |owned| pattern.captures(owned.as_str())); + if inner.borrow_dependent().is_some() { + Some(Self { inner }) + } else { + None } } diff --git a/src/lune/builtins/regex/matches.rs b/src/lune/builtins/regex/matches.rs index 60348e8..d3b1fa9 100644 --- a/src/lune/builtins/regex/matches.rs +++ b/src/lune/builtins/regex/matches.rs @@ -3,6 +3,9 @@ use std::{ops::Range, sync::Arc}; use mlua::prelude::*; use regex::Match; +/** + A wrapper over the `regex::Match` struct that can be used from Lua. +*/ pub struct LuaMatch { text: Arc, start: usize, @@ -10,6 +13,9 @@ pub struct LuaMatch { } impl LuaMatch { + /** + Create a new `LuaMatch` instance from a `String` text and a `regex::Match`. + */ pub fn new(text: Arc, matched: Match) -> Self { Self { text, diff --git a/src/lune/builtins/regex/regex.rs b/src/lune/builtins/regex/regex.rs index b255caa..b40331d 100644 --- a/src/lune/builtins/regex/regex.rs +++ b/src/lune/builtins/regex/regex.rs @@ -5,11 +5,18 @@ use regex::Regex; use super::{captures::LuaCaptures, matches::LuaMatch}; +/** + A wrapper over the `regex::Regex` struct that can be used from Lua. +*/ +#[derive(Debug, Clone)] pub struct LuaRegex { inner: Regex, } impl LuaRegex { + /** + Create a new `LuaRegex` instance from a `String` pattern. + */ pub fn new(pattern: String) -> LuaResult { Regex::new(&pattern) .map(|inner| Self { inner }) From abe7217a58d560870d6693cab0396c1ed15c669b Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 23:19:21 +0200 Subject: [PATCH 18/22] Add full type definitions for new regex builtin --- src/lune/builtins/regex/captures.rs | 4 +- src/lune/builtins/regex/matches.rs | 2 - types/regex.luau | 218 ++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 types/regex.luau diff --git a/src/lune/builtins/regex/captures.rs b/src/lune/builtins/regex/captures.rs index a1283e9..412370d 100644 --- a/src/lune/builtins/regex/captures.rs +++ b/src/lune/builtins/regex/captures.rs @@ -59,10 +59,10 @@ impl LuaCaptures { impl LuaUserData for LuaCaptures { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method("get", |_, this, n: usize| { + methods.add_method("get", |_, this, index: usize| { Ok(this .captures() - .get(n) + .get(index) .map(|m| LuaMatch::new(this.text(), m))) }); diff --git a/src/lune/builtins/regex/matches.rs b/src/lune/builtins/regex/matches.rs index d3b1fa9..42984a5 100644 --- a/src/lune/builtins/regex/matches.rs +++ b/src/lune/builtins/regex/matches.rs @@ -43,8 +43,6 @@ impl LuaUserData for LuaMatch { } fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method("isEmpty", |_, this, ()| Ok(this.range().is_empty())); - methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("RegexMatch")); methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len())); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { diff --git a/types/regex.luau b/types/regex.luau new file mode 100644 index 0000000..e148c73 --- /dev/null +++ b/types/regex.luau @@ -0,0 +1,218 @@ +--[=[ + @class RegexMatch + + A match from a regular expression. + + Contains the following values: + + - `start` -- The start index of the match in the original string. + - `finish` -- The end index of the match in the original string. + - `text` -- The text that was matched. + - `len` -- The length of the text that was matched. +]=] +local RegexMatch = { + start = 0, + finish = 0, + text = "", + len = 0, +} + +type RegexMatch = typeof(RegexMatch) + +--[=[ + @class RegexCaptures + + Captures from a regular expression. +]=] +local RegexCaptures = {} + +--[=[ + @within RegexCaptures + @tag Method + + Returns the match at the given index, if one exists. + + @param index -- The index of the match to get + @return RegexMatch -- The match, if one exists +]=] +function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch? + return nil :: any +end + +--[=[ + @within RegexCaptures + @tag Method + + Returns the match for the given named match group, if one exists. + + @param group -- The name of the group to get + @return RegexMatch -- The match, if one exists +]=] +function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch? + return nil :: any +end + +--[=[ + @within RegexCaptures + @tag Method + + Formats the captures using the given format string. + + ### Example usage + + ```lua + local regex = require("@lune/regex") + + local re = regex.new("(?[0-9]{2})-(?[0-9]{2})-(?[0-9]{4})") + + local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb."); + assert(caps ~= nil, "Example pattern should match example text") + + local formatted = caps:format("year=$year, month=$month, day=$day") + print(formatted) -- "year=2010, month=03, day=14" + ``` + + @param format -- The format string to use + @return string -- The formatted string +]=] +function RegexCaptures.format(self: RegexCaptures, format: string): string + return nil :: any +end + +export type RegexCaptures = typeof(RegexCaptures) + +local Regex = {} + +--[=[ + @within Regex + @tag Method + + Check if the given text matches the regular expression. + + This method may be slightly more efficient than calling `find` + if you only need to know if the text matches the pattern. + + @param text -- The text to search + @return boolean -- Whether the text matches the pattern +]=] +function Regex.isMatch(self: Regex, text: string): boolean + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Finds the first match in the given text. + + Returns `nil` if no match was found. + + @param text -- The text to search + @return RegexMatch? -- The match object +]=] +function Regex.find(self: Regex, text: string): RegexMatch? + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Finds all matches in the given text as a `RegexCaptures` object. + + Returns `nil` if no matches are found. + + @param text -- The text to search + @return RegexCaptures? -- The captures object +]=] +function Regex.captures(self: Regex, text: string): RegexCaptures? + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Splits the given text using the regular expression. + + @param text -- The text to split + @return { string } -- The split text +]=] +function Regex.split(self: Regex, text: string): { string } + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Replaces the first match in the given text with the given replacer string. + + @param haystack -- The text to search + @param replacer -- The string to replace matches with + @return string -- The text with the first match replaced +]=] +function Regex.replace(self: Regex, haystack: string, replacer: string): string + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Replaces all matches in the given text with the given replacer string. + + @param haystack -- The text to search + @param replacer -- The string to replace matches with + @return string -- The text with all matches replaced +]=] +function Regex.replaceAll(self: Regex, haystack: string, replacer: string): string + return nil :: any +end + +export type Regex = typeof(Regex) + +--[=[ + @class Regex + + Built-in library for regular expressions + + ### Example usage + + ```lua + local Regex = require("@lune/regex") + + local re = Regex.new("hello") + + if re:isMatch("hello, world!") then + print("Matched!") + end + + local caps = re:captures("hello, world! hello, again!") + + print(#caps) -- 2 + print(caps:get(1)) -- "hello" + print(caps:get(2)) -- "hello" + print(caps:get(3)) -- nil + ``` +]=] +local regex = {} + +--[=[ + @within Regex + @tag Constructor + + Creates a new `Regex` from a given string pattern. + + ### Errors + + This constructor throws an error if the given pattern is invalid. + + @param pattern -- The string pattern to use + @return Regex -- The new Regex object +]=] +function regex.new(pattern: string): Regex + return nil :: any +end + +return regex From 96eed54a65ce70ee916d08cc0c05432a8454be5e Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 23:30:08 +0200 Subject: [PATCH 19/22] Port test suite for regex from fork --- src/tests.rs | 4 +++ tests/regex/general.luau | 39 ++++++++++++++++++++++++++ tests/regex/metamethods.luau | 15 ++++++++++ tests/regex/replace.luau | 53 ++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 tests/regex/general.luau create mode 100644 tests/regex/metamethods.luau create mode 100644 tests/regex/replace.luau diff --git a/src/tests.rs b/src/tests.rs index fad6026..b14566f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -83,6 +83,10 @@ create_tests! { process_spawn_stdin: "process/spawn/stdin", process_spawn_stdio: "process/spawn/stdio", + regex_general: "regex/general", + regex_metamethods: "regex/metamethods", + regex_replace: "regex/replace", + require_aliases: "require/tests/aliases", require_async: "require/tests/async", require_async_concurrent: "require/tests/async_concurrent", diff --git a/tests/regex/general.luau b/tests/regex/general.luau new file mode 100644 index 0000000..24a73b5 --- /dev/null +++ b/tests/regex/general.luau @@ -0,0 +1,39 @@ +--!nocheck + +local regex = require("@lune/regex") + +local re = regex.new("[0-9]+") + +assert(re:isMatch("look, a number: 1241425") == true) + +local mtch = re:find("1337 wow") +assert(mtch ~= nil) +assert(mtch.start == 1) +assert(mtch.finish == 4) +assert(mtch.len == 4) +assert(mtch.text == "1337") +assert(#mtch == mtch.len) + +re = regex.new([[([0-9]+) (\d+) \D+ \d+ (\d)]]) +local captures = re:captures("1337 125600 wow! 1984 0") +assert(captures ~= nil) +assert(#captures == 3) +assert(captures:get(0).text == "1337 125600 wow! 1984 0") +assert(captures:get(1).text == "1337") +assert(captures:get(2).text == "125600") +assert(captures:get(3).text == "0") +assert(captures:get(4) == nil) + +assert(captures:format("$0") == "1337 125600 wow! 1984 0") +assert(captures:format("$3 $2 $1") == "0 125600 1337") + +re = regex.new("(?P[^ ]+)[ ]+(?P[^ ]+)(?P[ ]*)") +captures = re:captures("w1 w2 w3 w4") +assert(captures:format("$last $first$space") == "w2 w1 ") + +local split = regex.new([[,]]):split("1,2,3,4") +assert(#split == 4) +assert(split[1] == "1") +assert(split[2] == "2") +assert(split[3] == "3") +assert(split[4] == "4") diff --git a/tests/regex/metamethods.luau b/tests/regex/metamethods.luau new file mode 100644 index 0000000..66741d8 --- /dev/null +++ b/tests/regex/metamethods.luau @@ -0,0 +1,15 @@ +--!nolint + +local regex = require("@lune/regex") + +local re = regex.new("[0-9]+") +assert(tostring(re) == "Regex([0-9]+)") +assert(typeof(re) == "Regex") + +local mtch = re:find("1337 wow") +assert(tostring(mtch) == "RegexMatch(1337)") +assert(typeof(mtch) == "RegexMatch") + +local captures = re:captures("1337 125600 wow! 1984 0") +assert(tostring(captures) == "RegexCaptures(3)") +assert(typeof(captures) == "RegexCaptures") diff --git a/tests/regex/replace.luau b/tests/regex/replace.luau new file mode 100644 index 0000000..dc6b9f4 --- /dev/null +++ b/tests/regex/replace.luau @@ -0,0 +1,53 @@ +local regex = require("@lune/regex") + +-- Tests taken from the Regex crate + +local function replace( + name: string, + pattern: string, + find: string, + replace: string, + expected: string +) + local re = regex.new(pattern) + local replaced = re:replace(find, replace) + if replaced ~= expected then + error(`test '{name}' did not return expected result (expected {expected} got {replaced})`) + end +end + +local function replaceAll( + name: string, + pattern: string, + find: string, + replace: string, + expected: string +) + local re = regex.new(pattern) + local replaced = re:replaceAll(find, replace) + if replaced ~= expected then + error(`test '{name}' did not return expected result (expected {expected} got {replaced})`) + end +end + +replace("first", "[0-9]", "age: 26", "Z", "age: Z6") +replace("plus", "[0-9]+", "age: 26", "Z", "age: Z") +replaceAll("all", "[0-9]", "age: 26", "Z", "age: ZZ") +replace("groups", "([^ ]+)[ ]+([^ ]+)", "w1 w2", "$2 $1", "w2 w1") +replace("double dollar", "([^ ]+)[ ]+([^ ]+)", "w1 w2", "$2 $$1", "w2 $1") + +replaceAll( + "named", + "(?P[^ ]+)[ ]+(?P[^ ]+)(?P[ ]*)", + "w1 w2 w3 w4", + "$last $first$space", + "w2 w1 w4 w3" +) +replaceAll("trim", "^[ \t]+|[ \t]+$", " \t trim me\t \t", "", "trim me") +replace("number hypen", "(.)(.)", "ab", "$1-$2", "a-b") +replaceAll("simple expand", "([a-z]) ([a-z])", "a b", "$2 $1", "b a") +replaceAll("literal dollar 1", "([a-z]+) ([a-z]+)", "a b", "$$1", "$1") +replaceAll("literal dollar 2", "([a-z]+) ([a-z]+)", "a b", "$2 $$c $1", "b $c a") + +replaceAll("match at start replace with empty", "foo", "foobar", "", "bar") +replace("single empty match", "^", "bar", "foo", "foobar") From 7a46f12c02701ba539a54d09f03c07c1444365cd Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 23:38:06 +0200 Subject: [PATCH 20/22] Fix type metamethod and test case for regex --- src/lune/builtins/regex/captures.rs | 5 ++++- src/lune/builtins/regex/matches.rs | 3 ++- src/lune/builtins/regex/regex.rs | 5 ++++- tests/regex/metamethods.luau | 5 +++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/lune/builtins/regex/captures.rs b/src/lune/builtins/regex/captures.rs index 412370d..5dbea74 100644 --- a/src/lune/builtins/regex/captures.rs +++ b/src/lune/builtins/regex/captures.rs @@ -79,10 +79,13 @@ impl LuaUserData for LuaCaptures { Ok(new) }); - methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("RegexCaptures")); methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures())); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { Ok(format!("RegexCaptures({})", this.num_captures())) }); } + + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_meta_field(LuaMetaMethod::Type, "RegexCaptures"); + } } diff --git a/src/lune/builtins/regex/matches.rs b/src/lune/builtins/regex/matches.rs index 42984a5..bc109f8 100644 --- a/src/lune/builtins/regex/matches.rs +++ b/src/lune/builtins/regex/matches.rs @@ -40,10 +40,11 @@ impl LuaUserData for LuaMatch { fields.add_field_method_get("finish", |_, this| Ok(this.end)); fields.add_field_method_get("len", |_, this| Ok(this.range().len())); fields.add_field_method_get("text", |_, this| Ok(this.slice().to_string())); + + fields.add_meta_field(LuaMetaMethod::Type, "RegexMatch"); } fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("RegexMatch")); methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len())); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { Ok(format!("RegexMatch({})", this.slice())) diff --git a/src/lune/builtins/regex/regex.rs b/src/lune/builtins/regex/regex.rs index b40331d..3325e5d 100644 --- a/src/lune/builtins/regex/regex.rs +++ b/src/lune/builtins/regex/regex.rs @@ -65,9 +65,12 @@ impl LuaUserData for LuaRegex { }, ); - methods.add_meta_method(LuaMetaMethod::Type, |_, _, ()| Ok("Regex")); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { Ok(format!("Regex({})", this.inner.as_str())) }); } + + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_meta_field(LuaMetaMethod::Type, "Regex"); + } } diff --git a/tests/regex/metamethods.luau b/tests/regex/metamethods.luau index 66741d8..f14231c 100644 --- a/tests/regex/metamethods.luau +++ b/tests/regex/metamethods.luau @@ -10,6 +10,7 @@ local mtch = re:find("1337 wow") assert(tostring(mtch) == "RegexMatch(1337)") assert(typeof(mtch) == "RegexMatch") -local captures = re:captures("1337 125600 wow! 1984 0") -assert(tostring(captures) == "RegexCaptures(3)") +local re2 = regex.new("([0-9]+) ([0-9]+) wow! ([0-9]+) ([0-9]+)") +local captures = re2:captures("1337 125600 wow! 1984 0") +assert(tostring(captures) == "RegexCaptures(4)") assert(typeof(captures) == "RegexCaptures") From 089ecb3a4c1069c0d2bed90ea7c102ee84de6e7c Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 23:54:34 +0200 Subject: [PATCH 21/22] Update dependencies --- CHANGELOG.md | 6 +++ Cargo.lock | 120 ++++++++++++++++++++++++++++----------------------- 2 files changed, 71 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091b0c9..f599551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Updated to Luau version `0.622`. + ## `0.8.3` - April 15th, 2024 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index e2bb8e8..2bc2755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,7 +213,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -401,12 +401,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -491,7 +492,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -502,9 +503,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "clipboard-win" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ "error-code", ] @@ -674,7 +675,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -966,7 +967,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1220,9 +1221,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f24ce812868d86d19daa79bf3bf9175bc44ea323391147a5e3abde2a283871b" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", @@ -1248,7 +1249,7 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper 0.14.28", - "rustls 0.21.10", + "rustls 0.21.11", "tokio", "tokio-rustls 0.24.1", ] @@ -1260,7 +1261,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a343d17fe7885302ed7252767dc7bb83609a874b6ff581142241ec4b73957ad" dependencies = [ "http-body-util", - "hyper 1.3.0", + "hyper 1.3.1", "hyper-util", "pin-project-lite", "tokio", @@ -1279,7 +1280,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.3.0", + "hyper 1.3.1", "pin-project-lite", "socket2", "tokio", @@ -1471,9 +1472,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "luau0-src" -version = "0.8.5+luau617" +version = "0.8.6+luau622" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652e36b8c35d807ec76a4931fe7c62883c62cc93311fb49bf5b76084647078ea" +checksum = "07758c1f5908f7f9dd9109efaf8c66907cc38acf312db03287e7ad2a64b5de1c" dependencies = [ "cc", ] @@ -1499,7 +1500,7 @@ dependencies = [ "glam", "http 1.1.0", "http-body-util", - "hyper 1.3.0", + "hyper 1.3.1", "hyper-tungstenite", "hyper-util", "include_dir", @@ -1853,7 +1854,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1919,9 +1920,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -1942,7 +1943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2199,7 +2200,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.10", + "rustls 0.21.11", "rustls-pemfile", "serde", "serde_json", @@ -2234,9 +2235,9 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder 1.5.0", "num-traits", @@ -2245,9 +2246,9 @@ dependencies = [ [[package]] name = "rmp-serde" -version = "1.1.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +checksum = "938a142ab806f18b88a97b0dea523d39e0fd730a064b035726adcfc58a8a5188" dependencies = [ "byteorder 1.5.0", "rmp", @@ -2311,9 +2312,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" dependencies = [ "log", "ring", @@ -2323,9 +2324,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", @@ -2453,9 +2454,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] @@ -2472,20 +2473,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "indexmap", "itoa", @@ -2703,9 +2704,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.59" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -2753,22 +2754,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2893,7 +2894,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2902,7 +2903,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.10", + "rustls 0.21.11", "tokio", ] @@ -2912,7 +2913,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "tokio", ] @@ -2925,7 +2926,7 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", @@ -2971,9 +2972,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ "indexmap", "serde", @@ -3030,7 +3031,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3091,7 +3092,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "sha1 0.10.6", "thiserror", @@ -3259,7 +3260,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -3293,7 +3294,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3549,10 +3550,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] -name = "zip_next" +name = "zip" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9519d1479ea50c3b79f1e00eacbb58e311c72c721e08313ebe64d8617a31b5d1" +checksum = "f21968e6da56f847a155a89581ba846507afa14854e041f3053edb6ddd19f807" dependencies = [ "aes", "arbitrary", @@ -3572,6 +3573,15 @@ dependencies = [ "zstd", ] +[[package]] +name = "zip_next" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8b818e7a619a85006cbdf263036c50cddd97d907887173b7ef743b4fb5b5e" +dependencies = [ + "zip", +] + [[package]] name = "zopfli" version = "0.8.0" From 3f53fc983cd37e9f75c7c8bcc18d3abbefa9848e Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sun, 21 Apr 2024 00:03:23 +0200 Subject: [PATCH 22/22] Update changelog --- CHANGELOG.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f599551..0b1d435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Added a builtin API for regular expressions. + + Example basic usage: + + ```lua + local Regex = require("@lune/regex") + + local re = Regex.new("hello") + + if re:isMatch("hello, world!") then + print("Matched!") + end + + local caps = re:captures("hello, world! hello, again!") + + print(#caps) -- 2 + print(caps:get(1)) -- "hello" + print(caps:get(2)) -- "hello" + print(caps:get(3)) -- nil + ``` + + Check out the documentation for more details. + +- Added support for buffers as arguments in builtin APIs ([#148]) + + This includes APIs such as `fs.writeFile`, `serde.encode`, and more. + +- Added support for cross-compilation of standalone binaries ([#162]) + + You can now compile standalone binaries for other platforms by passing + an additional `target` argument to the `build` subcommand: + + ```sh + lune build my-file.luau --output my-bin --target windows-x86_64 + ``` + + Currently supported targets are the same as the ones included with each + release of Lune on GitHub. Check releases for a full list of targets. + +- Added `stdio.readToEnd()` for reading the entire stdin passed to Lune +- Changed the `User-Agent` header in `net.request` to be more descriptive ([#186]) - Updated to Luau version `0.622`. +### Fixed + +- Fixed stack overflow for tables with circular keys ([#183]) +- Fixed `net.serve` no longer accepting ipv6 addresses +- Fixed headers in `net.serve` being raw bytes instead of strings + +[#148]: https://github.com/lune-org/lune/pull/148 +[#162]: https://github.com/lune-org/lune/pull/162 +[#183]: https://github.com/lune-org/lune/pull/183 +[#186]: https://github.com/lune-org/lune/pull/186 + ## `0.8.3` - April 15th, 2024 ### Fixed