diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cd12c62..44d79dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -194,7 +194,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] rust: [stable] - lua: [lua54, lua53, lua52, lua51, luajit, luau] + lua: [lua54, lua53, lua52, lua51, luajit] include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu diff --git a/Cargo.toml b/Cargo.toml index 84fd595..f120343 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ lua52 = ["ffi/lua52"] lua51 = ["ffi/lua51"] luajit = ["ffi/luajit"] luajit52 = ["luajit", "ffi/luajit52"] -luau = ["ffi/luau", "dep:libloading"] +luau = ["ffi/luau"] luau-jit = ["luau", "ffi/luau-codegen"] luau-vector4 = ["luau", "ffi/luau-vector4"] vendored = ["ffi/vendored"] @@ -61,9 +61,6 @@ rustversion = "1.0" ffi = { package = "mlua-sys", version = "0.6.6", path = "mlua-sys" } -[target.'cfg(unix)'.dependencies] -libloading = { version = "0.8", optional = true } - [dev-dependencies] trybuild = "1.0" hyper = { version = "1.2", features = ["full"] } diff --git a/mlua-sys/Cargo.toml b/mlua-sys/Cargo.toml index a88019b..d52c419 100644 --- a/mlua-sys/Cargo.toml +++ b/mlua-sys/Cargo.toml @@ -40,7 +40,7 @@ cfg-if = "1.0" pkg-config = "0.3.17" lua-src = { version = ">= 547.0.0, < 547.1.0", optional = true } luajit-src = { version = ">= 210.5.0, < 210.6.0", optional = true } -luau0-src = { git = "https://github.com/mlua-rs/luau-src-rs", rev = "f89e9f2", optional = true } +luau0-src = { git = "https://github.com/mlua-rs/luau-src-rs", rev = "37e1e34", optional = true } [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(raw_dylib)'] } diff --git a/mlua-sys/build/main_inner.rs b/mlua-sys/build/main_inner.rs index 05ac53b..4f293e1 100644 --- a/mlua-sys/build/main_inner.rs +++ b/mlua-sys/build/main_inner.rs @@ -11,8 +11,8 @@ cfg_if::cfg_if! { } fn main() { - #[cfg(all(feature = "luau", feature = "module", windows))] - compile_error!("Luau does not support `module` mode on Windows"); + #[cfg(all(feature = "luau", feature = "module"))] + compile_error!("Luau does not support `module` mode"); #[cfg(all(feature = "module", feature = "vendored"))] compile_error!("`vendored` and `module` features are mutually exclusive"); diff --git a/mlua-sys/src/luau/luarequire.rs b/mlua-sys/src/luau/luarequire.rs index d372610..0061307 100644 --- a/mlua-sys/src/luau/luarequire.rs +++ b/mlua-sys/src/luau/luarequire.rs @@ -101,7 +101,7 @@ pub struct luarequire_Configuration { // Executes the module and places the result on the stack. Returns the number of results placed on the // stack. - pub load: unsafe extern "C" fn( + pub load: unsafe extern "C-unwind" fn( L: *mut lua_State, ctx: *mut c_void, chunkname: *const c_char, diff --git a/src/lib.rs b/src/lib.rs index 22a3b36..ea1d8f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,7 +76,7 @@ mod conversion; mod error; mod function; mod hook; -#[cfg(feature = "luau")] +#[cfg(any(feature = "luau", doc))] mod luau; mod memory; mod multi; @@ -130,6 +130,7 @@ pub use crate::{ buffer::Buffer, chunk::{CompileConstant, Compiler}, function::CoverageInfo, + luau::{NavigateError, Require}, vector::Vector, }; diff --git a/src/luau/mod.rs b/src/luau/mod.rs index 29427ed..20ec6c7 100644 --- a/src/luau/mod.rs +++ b/src/luau/mod.rs @@ -2,12 +2,14 @@ use std::ffi::CStr; use std::os::raw::c_int; use crate::error::Result; -use crate::state::Lua; +use crate::state::{ExtraData, Lua, LuaOptions}; + +pub use require::{NavigateError, Require}; // Since Luau has some missing standard functions, we re-implement them here impl Lua { - pub(crate) unsafe fn configure_luau(&self) -> Result<()> { + pub(crate) unsafe fn configure_luau(&self, mut options: LuaOptions) -> Result<()> { let globals = self.globals(); globals.raw_set("collectgarbage", self.create_c_function(lua_collectgarbage)?)?; @@ -18,11 +20,13 @@ impl Lua { globals.raw_set("_VERSION", format!("Luau {version}"))?; } - Ok(()) - } + // Enable `require` function + let requirer = (options.requirer.take()).unwrap_or_else(|| Box::new(require::TextRequirer::new())); + self.exec_raw::<()>((), |state| { + let requirer_ptr = (*ExtraData::get(state)).set_requirer(requirer); + ffi::luaopen_require(state, require::init_config, requirer_ptr as *mut _); + })?; - pub(crate) fn disable_c_modules(&self) -> Result<()> { - package::disable_dylibs(self); Ok(()) } } @@ -64,6 +68,4 @@ unsafe extern "C-unwind" fn lua_collectgarbage(state: *mut ffi::lua_State) -> c_ } } -pub(crate) use package::register_package_module; - -mod package; +mod require; diff --git a/src/luau/package.rs b/src/luau/package.rs deleted file mode 100644 index 1db11bc..0000000 --- a/src/luau/package.rs +++ /dev/null @@ -1,270 +0,0 @@ -use std::ffi::CStr; -use std::fmt::Write; -use std::os::raw::c_int; -use std::path::{PathBuf, MAIN_SEPARATOR_STR}; -use std::string::String as StdString; -use std::{env, fs}; - -use crate::chunk::ChunkMode; -use crate::error::Result; -use crate::state::Lua; -use crate::table::Table; -use crate::traits::IntoLua; -use crate::value::Value; - -#[cfg(unix)] -use {libloading::Library, rustc_hash::FxHashMap}; - -// -// Luau package module -// - -#[cfg(unix)] -const TARGET_MLUA_LUAU_ABI_VERSION: u32 = 2; - -#[cfg(all(unix, feature = "module"))] -#[no_mangle] -#[used] -pub static MLUA_LUAU_ABI_VERSION: u32 = TARGET_MLUA_LUAU_ABI_VERSION; - -// We keep reference to the loaded dylibs in application data -#[cfg(unix)] -struct LoadedDylibs(FxHashMap); - -#[cfg(unix)] -impl std::ops::Deref for LoadedDylibs { - type Target = FxHashMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg(unix)] -impl std::ops::DerefMut for LoadedDylibs { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -pub(crate) fn register_package_module(lua: &Lua) -> Result<()> { - // Create the package table - let package = lua.create_table()?; - - // Set `package.path` - let mut search_path = env::var("LUAU_PATH") - .or_else(|_| env::var("LUA_PATH")) - .unwrap_or_default(); - if search_path.is_empty() { - search_path = "?.luau;?.lua".to_string(); - } - package.raw_set("path", search_path)?; - - // Set `package.cpath` - #[cfg(unix)] - { - let mut search_cpath = env::var("LUAU_CPATH") - .or_else(|_| env::var("LUA_CPATH")) - .unwrap_or_default(); - if search_cpath.is_empty() { - if cfg!(any(target_os = "macos", target_os = "ios")) { - search_cpath = "?.dylib".to_string(); - } else { - search_cpath = "?.so".to_string(); - } - } - package.raw_set("cpath", search_cpath)?; - } - - // Set `package.loaded` (table with a list of loaded modules) - let loaded = if let Ok(Some(loaded)) = lua.named_registry_value::>("_LOADED") { - package.raw_set("loaded", &loaded)?; - loaded - } else { - let loaded = lua.create_table()?; - package.raw_set("loaded", &loaded)?; - lua.set_named_registry_value("_LOADED", &loaded)?; - loaded - }; - - // Set `package.loaders` - let loaders = lua.create_sequence_from([lua.create_function(lua_loader)?])?; - package.raw_set("loaders", &loaders)?; - #[cfg(unix)] - { - loaders.push(lua.create_function(dylib_loader)?)?; - lua.set_app_data(LoadedDylibs(FxHashMap::default())); - } - lua.set_named_registry_value("_LOADERS", loaders)?; - - // Register the module and `require` function in globals - let globals = lua.globals(); - globals.raw_set("package", &package)?; - loaded.raw_set("package", package)?; - globals.raw_set("require", unsafe { lua.create_c_function(lua_require)? })?; - - Ok(()) -} - -#[allow(unused_variables)] -pub(crate) fn disable_dylibs(lua: &Lua) { - // Presence of `LoadedDylibs` in app data is used as a flag - // to check whether binary modules are enabled - #[cfg(unix)] - lua.remove_app_data::(); -} - -unsafe extern "C-unwind" fn lua_require(state: *mut ffi::lua_State) -> c_int { - ffi::lua_settop(state, 1); - let name = ffi::luaL_checkstring(state, 1); - ffi::luaL_getsubtable(state, ffi::LUA_REGISTRYINDEX, cstr!("_LOADED")); // _LOADED is at index 2 - if ffi::lua_rawgetfield(state, 2, name) != ffi::LUA_TNIL { - return 1; // module is already loaded - } - ffi::lua_pop(state, 1); // remove nil - - // load the module - let err_buf = ffi::lua_newuserdata_t(state, StdString::new()); - ffi::luaL_getsubtable(state, ffi::LUA_REGISTRYINDEX, cstr!("_LOADERS")); // _LOADERS is at index 3 - for i in 1.. { - if ffi::lua_rawgeti(state, -1, i) == ffi::LUA_TNIL { - // no more loaders? - if (*err_buf).is_empty() { - ffi::luaL_error(state, cstr!("module '%s' not found"), name); - } else { - let bytes = (*err_buf).as_bytes(); - let extra = ffi::lua_pushlstring(state, bytes.as_ptr() as *const _, bytes.len()); - ffi::luaL_error(state, cstr!("module '%s' not found:%s"), name, extra); - } - } - ffi::lua_pushvalue(state, 1); // name arg - ffi::lua_call(state, 1, 2); // call loader - match ffi::lua_type(state, -2) { - ffi::LUA_TFUNCTION => break, // loader found - ffi::LUA_TSTRING => { - // error message - let msg = ffi::lua_tostring(state, -2); - let msg = CStr::from_ptr(msg).to_string_lossy(); - _ = write!(&mut *err_buf, "\n\t{msg}"); - } - _ => {} - } - ffi::lua_pop(state, 2); // remove both results - } - ffi::lua_pushvalue(state, 1); // name is 1st argument to module loader - ffi::lua_rotate(state, -2, 1); // loader data <-> name - - // stack: ...; loader function; module name; loader data - ffi::lua_call(state, 2, 1); - // stack: ...; result from loader function - if ffi::lua_isnil(state, -1) != 0 { - ffi::lua_pop(state, 1); - ffi::lua_pushboolean(state, 1); // use true as result - } - ffi::lua_pushvalue(state, -1); // make copy of entrypoint result - ffi::lua_setfield(state, 2, name); /* _LOADED[name] = returned value */ - 1 -} - -/// Searches for the given `name` in the given `path`. -/// -/// `path` is a string containing a sequence of templates separated by semicolons. -fn package_searchpath(name: &str, search_path: &str, try_prefix: bool) -> Option { - let mut names = vec![name.replace('.', MAIN_SEPARATOR_STR)]; - if try_prefix && name.contains('.') { - let prefix = name.split_once('.').map(|(prefix, _)| prefix).unwrap(); - names.push(prefix.to_string()); - } - for path in search_path.split(';') { - for name in &names { - let file_path = PathBuf::from(path.replace('?', name)); - if let Ok(true) = fs::metadata(&file_path).map(|m| m.is_file()) { - return Some(file_path); - } - } - } - None -} - -// -// Module loaders -// - -/// Tries to load a lua (text) file -fn lua_loader(lua: &Lua, modname: StdString) -> Result { - let package = { - let loaded = lua.named_registry_value::("_LOADED")?; - loaded.raw_get::
("package") - }?; - let search_path = package.get::("path").unwrap_or_default(); - - if let Some(file_path) = package_searchpath(&modname, &search_path, false) { - match fs::read(&file_path) { - Ok(buf) => { - return lua - .load(buf) - .set_name(format!("={}", file_path.display())) - .set_mode(ChunkMode::Text) - .into_function() - .map(Value::Function); - } - Err(err) => { - return format!("cannot open '{}': {err}", file_path.display()).into_lua(lua); - } - } - } - - Ok(Value::Nil) -} - -/// Tries to load a dynamic library -#[cfg(unix)] -fn dylib_loader(lua: &Lua, modname: StdString) -> Result { - let package = { - let loaded = lua.named_registry_value::
("_LOADED")?; - loaded.raw_get::
("package") - }?; - let search_cpath = package.get::("cpath").unwrap_or_default(); - - let find_symbol = |lib: &Library| unsafe { - if let Ok(entry) = lib.get::(format!("luaopen_{modname}\0").as_bytes()) { - return lua.create_c_function(*entry).map(Value::Function); - } - // Try all in one mode - if let Ok(entry) = - lib.get::(format!("luaopen_{}\0", modname.replace('.', "_")).as_bytes()) - { - return lua.create_c_function(*entry).map(Value::Function); - } - "cannot find module entrypoint".into_lua(lua) - }; - - if let Some(file_path) = package_searchpath(&modname, &search_cpath, true) { - let file_path = file_path.canonicalize()?; - // Load the library and check for symbol - unsafe { - let mut loaded_dylibs = match lua.app_data_mut::() { - Some(loaded_dylibs) => loaded_dylibs, - None => return "dynamic libraries are disabled in safe mode".into_lua(lua), - }; - // Check if it's already loaded - if let Some(lib) = loaded_dylibs.get(&file_path) { - return find_symbol(lib); - } - if let Ok(lib) = Library::new(&file_path) { - // Check version - let mod_version = lib.get::<*const u32>(b"MLUA_LUAU_ABI_VERSION"); - let mod_version = mod_version.map(|v| **v).unwrap_or_default(); - if mod_version != TARGET_MLUA_LUAU_ABI_VERSION { - let err = format!("wrong module ABI version (expected {TARGET_MLUA_LUAU_ABI_VERSION}, got {mod_version})"); - return err.into_lua(lua); - } - let symbol = find_symbol(&lib); - loaded_dylibs.insert(file_path, lib); - return symbol; - } - } - } - - Ok(Value::Nil) -} diff --git a/src/luau/require.rs b/src/luau/require.rs new file mode 100644 index 0000000..2270fa9 --- /dev/null +++ b/src/luau/require.rs @@ -0,0 +1,536 @@ +use std::cell::RefCell; +use std::collections::VecDeque; +use std::ffi::CStr; +use std::io::Result as IoResult; +use std::os::raw::{c_char, c_int, c_void}; +use std::path::{Component, Path, PathBuf}; +use std::result::Result as StdResult; +use std::{env, fmt, fs, ptr}; + +use crate::error::Result; +use crate::state::{callback_error_ext, Lua}; +use crate::value::Value; + +/// An error that can occur during navigation in the Luau `require` system. +pub enum NavigateError { + Ambiguous, + NotFound, +} + +#[cfg(feature = "luau")] +trait IntoNavigateResult { + fn into_nav_result(self) -> ffi::luarequire_NavigateResult; +} + +#[cfg(feature = "luau")] +impl IntoNavigateResult for StdResult<(), NavigateError> { + fn into_nav_result(self) -> ffi::luarequire_NavigateResult { + match self { + Ok(()) => ffi::luarequire_NavigateResult::Success, + Err(NavigateError::Ambiguous) => ffi::luarequire_NavigateResult::Ambiguous, + Err(NavigateError::NotFound) => ffi::luarequire_NavigateResult::NotFound, + } + } +} + +#[cfg(feature = "luau")] +type WriteResult = ffi::luarequire_WriteResult; + +/// A trait for handling modules loading and navigation in the Luau `require` system. +pub trait Require { + /// Returns `true` if "require" is permitted for the given chunk name. + fn is_require_allowed(&self, chunk_name: &str) -> bool; + + /// Resets the internal state to point at the requirer module. + fn reset(&self, chunk_name: &str) -> StdResult<(), NavigateError>; + + /// Resets the internal state to point at an aliased module. + /// + /// This function received an exact path from a configuration file. + /// It's only called when an alias's path cannot be resolved relative to its + /// configuration file. + fn jump_to_alias(&self, path: &str) -> StdResult<(), NavigateError>; + + // Navigate to parent directory + fn to_parent(&self) -> StdResult<(), NavigateError>; + + /// Navigate to the given child directory. + fn to_child(&self, name: &str) -> StdResult<(), NavigateError>; + + /// Returns whether the context is currently pointing at a module + fn is_module_present(&self) -> bool; + + /// Returns the contents of the current module + /// + /// This function is only called if `is_module_present` returns true. + fn contents(&self) -> IoResult>; + + /// Returns a chunk name for the current module. + /// + /// This function is only called if `is_module_present` returns true. + /// The chunk name is used to identify the module using the debug library. + fn chunk_name(&self) -> String; + + /// Provides a cache key representing the current module. + /// + /// This function is only called if `is_module_present` returns true. + fn cache_key(&self) -> Vec; + + /// Returns whether a configuration file is present in the current context. + fn is_config_present(&self) -> bool; + + /// Returns the contents of the configuration file in the current context. + /// + /// This function is only called if `is_config_present` returns true. + fn config(&self) -> IoResult>; + + /// Loads the module and returns the result (function or table). + fn load(&self, lua: &Lua, chunk_name: &str, content: &[u8]) -> Result { + lua.load(content).set_name(chunk_name).call(()) + } +} + +impl fmt::Debug for dyn Require { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "") + } +} + +/// The standard implementation of Luau `require` navigation. +#[derive(Default)] +pub(super) struct TextRequirer { + abs_path: RefCell, + rel_path: RefCell, + module_path: RefCell, +} + +impl TextRequirer { + pub(super) fn new() -> Self { + Self::default() + } + + fn normalize_chunk_name(chunk_name: &str) -> &str { + if let Some((path, line)) = chunk_name.split_once(':') { + if line.parse::().is_ok() { + return path; + } + } + chunk_name + } + + // Normalizes the path by removing unnecessary components + fn normalize_path(path: &Path) -> PathBuf { + let mut components = VecDeque::new(); + + for comp in path.components() { + match comp { + Component::Prefix(..) | Component::RootDir => { + components.push_back(comp); + } + Component::CurDir => {} + Component::ParentDir => { + if matches!(components.back(), None | Some(Component::ParentDir)) { + components.push_back(Component::ParentDir); + } else if matches!(components.back(), Some(Component::Normal(..))) { + components.pop_back(); + } + } + Component::Normal(..) => components.push_back(comp), + } + } + + if matches!(components.front(), None | Some(Component::Normal(..))) { + components.push_front(Component::CurDir); + } + + // Join the components back together + components.into_iter().collect() + } + + fn find_module_path(path: &Path) -> StdResult { + let mut found_path = None; + + let current_ext = (path.extension().and_then(|s| s.to_str())) + .map(|s| format!("{s}.")) + .unwrap_or_default(); + for ext in ["luau", "lua"] { + let candidate = path.with_extension(format!("{current_ext}{ext}")); + if candidate.is_file() { + if found_path.is_some() { + return Err(NavigateError::Ambiguous); + } + found_path = Some(candidate); + } + } + if path.is_dir() { + if found_path.is_some() { + return Err(NavigateError::Ambiguous); + } + + for component in ["init.luau", "init.lua"] { + let candidate = path.join(component); + if candidate.is_file() { + if found_path.is_some() { + return Err(NavigateError::Ambiguous); + } + found_path = Some(candidate); + } + } + + if found_path.is_none() { + found_path = Some(PathBuf::new()); + } + } + + found_path.ok_or(NavigateError::NotFound) + } +} + +impl Require for TextRequirer { + fn is_require_allowed(&self, chunk_name: &str) -> bool { + chunk_name.starts_with('@') + } + + fn reset(&self, chunk_name: &str) -> StdResult<(), NavigateError> { + if !chunk_name.starts_with('@') { + return Err(NavigateError::NotFound); + } + let chunk_name = &Self::normalize_chunk_name(chunk_name)[1..]; + let path = Self::normalize_path(chunk_name.as_ref()); + + if path.extension() == Some("rs".as_ref()) { + let cwd = match env::current_dir() { + Ok(cwd) => cwd, + Err(_) => return Err(NavigateError::NotFound), + }; + self.abs_path.replace(Self::normalize_path(&cwd.join(&path))); + self.rel_path.replace(path); + self.module_path.replace(PathBuf::new()); + + return Ok(()); + } + + if path.is_absolute() { + let module_path = Self::find_module_path(&path)?; + self.abs_path.replace(path.clone()); + self.rel_path.replace(path); + self.module_path.replace(module_path); + } else { + // Relative path + let cwd = match env::current_dir() { + Ok(cwd) => cwd, + Err(_) => return Err(NavigateError::NotFound), + }; + let abs_path = cwd.join(&path); + let module_path = Self::find_module_path(&abs_path)?; + self.abs_path.replace(Self::normalize_path(&abs_path)); + self.rel_path.replace(path); + self.module_path.replace(module_path); + } + + Ok(()) + } + + fn jump_to_alias(&self, path: &str) -> StdResult<(), NavigateError> { + let path = Self::normalize_path(path.as_ref()); + let module_path = Self::find_module_path(&path)?; + + self.abs_path.replace(path.clone()); + self.rel_path.replace(path); + self.module_path.replace(module_path); + + Ok(()) + } + + fn to_parent(&self) -> StdResult<(), NavigateError> { + let mut abs_path = self.abs_path.borrow().clone(); + if !abs_path.pop() { + return Err(NavigateError::NotFound); + } + let mut rel_parent = self.rel_path.borrow().clone(); + rel_parent.pop(); + let module_path = Self::find_module_path(&abs_path)?; + + self.abs_path.replace(abs_path); + self.rel_path.replace(Self::normalize_path(&rel_parent)); + self.module_path.replace(module_path); + + Ok(()) + } + + fn to_child(&self, name: &str) -> StdResult<(), NavigateError> { + let abs_path = self.abs_path.borrow().join(name); + let rel_path = self.rel_path.borrow().join(name); + let module_path = Self::find_module_path(&abs_path)?; + + self.abs_path.replace(abs_path); + self.rel_path.replace(rel_path); + self.module_path.replace(module_path); + + Ok(()) + } + + fn is_module_present(&self) -> bool { + self.module_path.borrow().is_file() + } + + fn contents(&self) -> IoResult> { + fs::read(&*self.module_path.borrow()) + } + + fn chunk_name(&self) -> String { + format!("@{}", self.rel_path.borrow().display()) + } + + fn cache_key(&self) -> Vec { + self.module_path.borrow().display().to_string().into_bytes() + } + + fn is_config_present(&self) -> bool { + self.abs_path.borrow().join(".luaurc").is_file() + } + + fn config(&self) -> IoResult> { + fs::read(self.abs_path.borrow().join(".luaurc")) + } +} + +#[cfg(feature = "luau")] +pub(super) unsafe extern "C" fn init_config(config: *mut ffi::luarequire_Configuration) { + if config.is_null() { + return; + } + + unsafe extern "C" fn is_require_allowed( + _state: *mut ffi::lua_State, + ctx: *mut c_void, + requirer_chunkname: *const c_char, + ) -> bool { + if requirer_chunkname.is_null() { + return false; + } + + let this = &*(ctx as *const Box); + let chunk_name = CStr::from_ptr(requirer_chunkname).to_string_lossy(); + this.is_require_allowed(&chunk_name) + } + + unsafe extern "C" fn reset( + _state: *mut ffi::lua_State, + ctx: *mut c_void, + requirer_chunkname: *const c_char, + ) -> ffi::luarequire_NavigateResult { + let this = &*(ctx as *const Box); + let chunk_name = CStr::from_ptr(requirer_chunkname).to_string_lossy(); + this.reset(&chunk_name).into_nav_result() + } + + unsafe extern "C" fn jump_to_alias( + _state: *mut ffi::lua_State, + ctx: *mut c_void, + path: *const c_char, + ) -> ffi::luarequire_NavigateResult { + let this = &*(ctx as *const Box); + let path = CStr::from_ptr(path).to_string_lossy(); + this.jump_to_alias(&path).into_nav_result() + } + + unsafe extern "C" fn to_parent( + _state: *mut ffi::lua_State, + ctx: *mut c_void, + ) -> ffi::luarequire_NavigateResult { + let this = &*(ctx as *const Box); + this.to_parent().into_nav_result() + } + + unsafe extern "C" fn to_child( + _state: *mut ffi::lua_State, + ctx: *mut c_void, + name: *const c_char, + ) -> ffi::luarequire_NavigateResult { + let this = &*(ctx as *const Box); + let name = CStr::from_ptr(name).to_string_lossy(); + this.to_child(&name).into_nav_result() + } + + unsafe extern "C" fn is_module_present(_state: *mut ffi::lua_State, ctx: *mut c_void) -> bool { + let this = &*(ctx as *const Box); + this.is_module_present() + } + + unsafe extern "C" fn get_contents( + state: *mut ffi::lua_State, + ctx: *mut c_void, + buffer: *mut c_char, + buffer_size: usize, + size_out: *mut usize, + ) -> WriteResult { + let this = &*(ctx as *const Box); + write_to_buffer(state, buffer, buffer_size, size_out, || this.contents()) + } + + unsafe extern "C" fn get_chunkname( + state: *mut ffi::lua_State, + ctx: *mut c_void, + buffer: *mut c_char, + buffer_size: usize, + size_out: *mut usize, + ) -> WriteResult { + let this = &*(ctx as *const Box); + write_to_buffer(state, buffer, buffer_size, size_out, || { + Ok(this.chunk_name().into_bytes()) + }) + } + + unsafe extern "C" fn get_cache_key( + state: *mut ffi::lua_State, + ctx: *mut c_void, + buffer: *mut c_char, + buffer_size: usize, + size_out: *mut usize, + ) -> WriteResult { + let this = &*(ctx as *const Box); + write_to_buffer(state, buffer, buffer_size, size_out, || Ok(this.cache_key())) + } + + unsafe extern "C" fn is_config_present(_state: *mut ffi::lua_State, ctx: *mut c_void) -> bool { + let this = &*(ctx as *const Box); + this.is_config_present() + } + + unsafe extern "C" fn get_config( + state: *mut ffi::lua_State, + ctx: *mut c_void, + buffer: *mut c_char, + buffer_size: usize, + size_out: *mut usize, + ) -> WriteResult { + let this = &*(ctx as *const Box); + write_to_buffer(state, buffer, buffer_size, size_out, || this.config()) + } + + unsafe extern "C-unwind" fn load( + state: *mut ffi::lua_State, + ctx: *mut c_void, + chunk_name: *const c_char, + contents: *const c_char, + ) -> c_int { + let this = &*(ctx as *const Box); + let chunk_name = CStr::from_ptr(chunk_name).to_string_lossy(); + let contents = CStr::from_ptr(contents).to_bytes(); + let lua = Lua::get_or_init_from_ptr(state); + callback_error_ext(state, ptr::null_mut(), false, move |_extra, _| { + match this.load(lua, &chunk_name, contents)? { + Value::Nil => lua.lock().push(true)?, + value => lua.lock().push(value)?, + }; + Ok(1) + }) + } + + (*config).is_require_allowed = is_require_allowed; + (*config).reset = reset; + (*config).jump_to_alias = jump_to_alias; + (*config).to_parent = to_parent; + (*config).to_child = to_child; + (*config).is_module_present = is_module_present; + (*config).get_contents = get_contents; + (*config).get_chunkname = get_chunkname; + (*config).get_cache_key = get_cache_key; + (*config).is_config_present = is_config_present; + (*config).get_config = get_config; + (*config).load = load; +} + +/// Helper function to write data to a buffer +#[cfg(feature = "luau")] +unsafe fn write_to_buffer( + state: *mut ffi::lua_State, + buffer: *mut c_char, + buffer_size: usize, + size_out: *mut usize, + data_fetcher: impl Fn() -> IoResult>, +) -> WriteResult { + struct DataCache(Vec); + + // The initial buffer size can be too small, to avoid making a second data fetch call, + // we cache the content in the first call, and then re-use it. + + let lua = Lua::get_or_init_from_ptr(state); + if let Some(data_cache) = lua.app_data_ref::() { + let data_len = data_cache.0.len(); + mlua_assert!(data_len <= buffer_size, "buffer is too small"); + *size_out = data_len; + ptr::copy_nonoverlapping(data_cache.0.as_ptr(), buffer as *mut _, data_len); + drop(data_cache); + lua.remove_app_data::(); + return WriteResult::Success; + } + + match data_fetcher() { + Ok(data) => { + let data_len = data.len(); + *size_out = data_len; + if data_len > buffer_size { + // Cache the data for the next call to avoid getting the contents again + lua.set_app_data(DataCache(data)); + *size_out = data_len; + return WriteResult::BufferTooSmall; + } + ptr::copy_nonoverlapping(data.as_ptr(), buffer as *mut _, data_len); + *size_out = data_len; + WriteResult::Success + } + Err(_) => WriteResult::Failure, + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::TextRequirer; + + #[test] + fn test_path_normalize() { + for (input, expected) in [ + // Basic formatting checks + ("", "./"), + (".", "./"), + ("a/relative/path", "./a/relative/path"), + // Paths containing extraneous '.' and '/' symbols + ("./remove/extraneous/symbols/", "./remove/extraneous/symbols"), + ("./remove/extraneous//symbols", "./remove/extraneous/symbols"), + ("./remove/extraneous/symbols/.", "./remove/extraneous/symbols"), + ("./remove/extraneous/./symbols", "./remove/extraneous/symbols"), + ("../remove/extraneous/symbols/", "../remove/extraneous/symbols"), + ("../remove/extraneous//symbols", "../remove/extraneous/symbols"), + ("../remove/extraneous/symbols/.", "../remove/extraneous/symbols"), + ("../remove/extraneous/./symbols", "../remove/extraneous/symbols"), + ("/remove/extraneous/symbols/", "/remove/extraneous/symbols"), + ("/remove/extraneous//symbols", "/remove/extraneous/symbols"), + ("/remove/extraneous/symbols/.", "/remove/extraneous/symbols"), + ("/remove/extraneous/./symbols", "/remove/extraneous/symbols"), + // Paths containing '..' + ("./remove/me/..", "./remove"), + ("./remove/me/../", "./remove"), + ("../remove/me/..", "../remove"), + ("../remove/me/../", "../remove"), + ("/remove/me/..", "/remove"), + ("/remove/me/../", "/remove"), + ("./..", "../"), + ("./../", "../"), + ("../..", "../../"), + ("../../", "../../"), + // '..' disappears if path is absolute and component is non-erasable + ("/../", "/"), + ] { + let path = TextRequirer::normalize_path(input.as_ref()); + assert_eq!( + &path, + expected.as_ref() as &Path, + "wrong normalization for {input}" + ); + } + } +} diff --git a/src/prelude.rs b/src/prelude.rs index fc96723..aaa94f5 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -23,7 +23,8 @@ pub use crate::HookTriggers as LuaHookTriggers; #[cfg(feature = "luau")] #[doc(no_inline)] pub use crate::{ - CompileConstant as LuaCompileConstant, CoverageInfo as LuaCoverageInfo, Vector as LuaVector, + CompileConstant as LuaCompileConstant, CoverageInfo as LuaCoverageInfo, + NavigateError as LuaNavigateError, Require as LuaRequire, Vector as LuaVector, }; #[cfg(feature = "async")] diff --git a/src/state.rs b/src/state.rs index b8e472b..5e20bb6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -46,7 +46,8 @@ use serde::Serialize; pub(crate) use extra::ExtraData; pub use raw::RawLua; -use util::{callback_error_ext, StateGuard}; +pub(crate) use util::callback_error_ext; +use util::StateGuard; /// Top level Lua struct which represents an instance of Lua VM. pub struct Lua { @@ -81,7 +82,7 @@ pub enum GCMode { } /// Controls Lua interpreter behavior such as Rust panics handling. -#[derive(Clone, Debug)] +#[derive(Debug)] #[non_exhaustive] pub struct LuaOptions { /// Catch Rust panics when using [`pcall`]/[`xpcall`]. @@ -106,6 +107,11 @@ pub struct LuaOptions { #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] pub thread_pool_size: usize, + + /// A custom [`crate::Require`] trait object to load Luau modules. + #[cfg(feature = "luau")] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + pub requirer: Option>, } impl Default for LuaOptions { @@ -121,6 +127,8 @@ impl LuaOptions { catch_rust_panics: true, #[cfg(feature = "async")] thread_pool_size: 0, + #[cfg(feature = "luau")] + requirer: None, } } @@ -143,6 +151,17 @@ impl LuaOptions { self.thread_pool_size = size; self } + + /// Sets a custom [`crate::Require`] trait object to load Luau modules. + /// + /// By default, the standard Luau `ReplRequirer` implementation is used. + #[cfg(feature = "luau")] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + #[must_use] + pub fn with_requirer(mut self, requirer: R) -> Self { + self.requirer = Some(Box::new(requirer)); + self + } } impl Drop for Lua { @@ -224,6 +243,7 @@ impl Lua { let lua = unsafe { Self::inner_new(libs, options) }; + #[cfg(not(feature = "luau"))] if libs.contains(StdLib::PACKAGE) { mlua_expect!(lua.disable_c_modules(), "Error disabling C modules"); } @@ -263,12 +283,12 @@ impl Lua { /// Creates a new Lua state with required `libs` and `options` unsafe fn inner_new(libs: StdLib, options: LuaOptions) -> Lua { let lua = Lua { - raw: RawLua::new(libs, options), + raw: RawLua::new(libs, &options), collect_garbage: true, }; #[cfg(feature = "luau")] - mlua_expect!(lua.configure_luau(), "Error configuring Luau"); + mlua_expect!(lua.configure_luau(options), "Error configuring Luau"); lua } diff --git a/src/state/extra.rs b/src/state/extra.rs index 7567669..af911dd 100644 --- a/src/state/extra.rs +++ b/src/state/extra.rs @@ -93,6 +93,8 @@ pub(crate) struct ExtraData { pub(super) compiler: Option, #[cfg(feature = "luau-jit")] pub(super) enable_jit: bool, + #[cfg(feature = "luau")] + pub(super) requirer: Option>, } impl Drop for ExtraData { @@ -194,6 +196,8 @@ impl ExtraData { enable_jit: true, #[cfg(feature = "luau")] running_gc: false, + #[cfg(feature = "luau")] + requirer: None, })); // Store it in the registry @@ -210,7 +214,7 @@ impl ExtraData { self.weak.write(WeakLua(XRc::downgrade(raw))); } - pub(super) unsafe fn get(state: *mut ffi::lua_State) -> *mut Self { + pub(crate) unsafe fn get(state: *mut ffi::lua_State) -> *mut Self { #[cfg(feature = "luau")] if cfg!(not(feature = "module")) { // In the main app we can use `lua_callbacks` to access ExtraData @@ -257,4 +261,13 @@ impl ExtraData { pub(super) unsafe fn weak(&self) -> &WeakLua { self.weak.assume_init_ref() } + + #[cfg(feature = "luau")] + pub(crate) fn set_requirer( + &mut self, + requirer: Box, + ) -> *mut Box { + self.requirer.replace(requirer); + self.requirer.as_mut().unwrap() + } } diff --git a/src/state/raw.rs b/src/state/raw.rs index c8f3add..0b700ca 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -121,7 +121,7 @@ impl RawLua { unsafe { (*self.extra.get()).ref_thread } } - pub(super) unsafe fn new(libs: StdLib, options: LuaOptions) -> XRc> { + pub(super) unsafe fn new(libs: StdLib, options: &LuaOptions) -> XRc> { let mem_state: *mut MemoryState = Box::into_raw(Box::default()); let mut state = ffi::lua_newstate(ALLOCATOR, mem_state as *mut c_void); // If state is null then switch to Lua internal allocator @@ -293,10 +293,15 @@ impl RawLua { let res = load_std_libs(self.main_state(), libs); // If `package` library loaded into a safe lua state then disable C modules - let curr_libs = (*self.extra.get()).libs; - if is_safe && (curr_libs ^ (curr_libs | libs)).contains(StdLib::PACKAGE) { - mlua_expect!(self.lua().disable_c_modules(), "Error during disabling C modules"); + #[cfg(not(feature = "luau"))] + if is_safe { + let curr_libs = (*self.extra.get()).libs; + if (curr_libs ^ (curr_libs | libs)).contains(StdLib::PACKAGE) { + mlua_expect!(self.lua().disable_c_modules(), "Error during disabling C modules"); + } } + #[cfg(feature = "luau")] + let _ = is_safe; unsafe { (*self.extra.get()).libs |= libs }; res @@ -1478,11 +1483,6 @@ unsafe fn load_std_libs(state: *mut ffi::lua_State, libs: StdLib) -> Result<()> if libs.contains(StdLib::PACKAGE) { requiref(state, ffi::LUA_LOADLIBNAME, ffi::luaopen_package, 1)?; } - #[cfg(feature = "luau")] - if libs.contains(StdLib::PACKAGE) { - let lua = (*ExtraData::get(state)).lua(); - crate::luau::register_package_module(lua)?; - } #[cfg(feature = "luajit")] if libs.contains(StdLib::JIT) { diff --git a/src/state/util.rs b/src/state/util.rs index ea482e0..ba9a7d4 100644 --- a/src/state/util.rs +++ b/src/state/util.rs @@ -24,7 +24,7 @@ impl Drop for StateGuard<'_> { // An optimized version of `callback_error` that does not allocate `WrappedFailure` userdata // and instead reuses unused values from previous calls (or allocates new). -pub(super) unsafe fn callback_error_ext( +pub(crate) unsafe fn callback_error_ext( state: *mut ffi::lua_State, mut extra: *mut ExtraData, wrap_error: bool, diff --git a/src/stdlib.rs b/src/stdlib.rs index 787b2fc..c6a26af 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -41,6 +41,8 @@ impl StdLib { pub const MATH: StdLib = StdLib(1 << 7); /// [`package`](https://www.lua.org/manual/5.4/manual.html#6.3) library + #[cfg(not(feature = "luau"))] + #[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))] pub const PACKAGE: StdLib = StdLib(1 << 8); /// [`buffer`](https://luau.org/library#buffer-library) library diff --git a/tests/luau.rs b/tests/luau.rs index 590125d..52a42da 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -2,7 +2,6 @@ use std::cell::Cell; use std::fmt::Debug; -use std::fs; use std::os::raw::c_void; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering}; @@ -17,83 +16,6 @@ fn test_version() -> Result<()> { Ok(()) } -#[test] -fn test_require() -> Result<()> { - // Ensure that require() is not available if package module is not loaded - let mut lua = Lua::new_with(StdLib::NONE, LuaOptions::default())?; - assert!(lua.globals().get::>("require")?.is_none()); - assert!(lua.globals().get::>("package")?.is_none()); - - if cfg!(target_arch = "wasm32") { - // TODO: figure out why emscripten fails on file operations - // Also see https://github.com/rust-lang/rust/issues/119250 - return Ok(()); - } - - lua = Lua::new(); - - // Check that require() can load stdlib modules (including `package`) - lua.load( - r#" - local math = require("math") - assert(math == _G.math, "math module does not match _G.math") - local package = require("package") - assert(package == _G.package, "package module does not match _G.package") - "#, - ) - .exec()?; - - let temp_dir = tempfile::tempdir().unwrap(); - fs::write( - temp_dir.path().join("module.luau"), - r#" - counter = (counter or 0) + 1 - return { - counter = counter, - error = function() error("test") end, - } - "#, - )?; - - lua.globals() - .get::
("package")? - .set("path", temp_dir.path().join("?.luau").to_string_lossy())?; - - lua.load( - r#" - local module = require("module") - assert(module.counter == 1) - module = require("module") - assert(module.counter == 1) - - local ok, err = pcall(module.error) - assert(not ok and string.find(err, "module.luau") ~= nil) - "#, - ) - .exec()?; - - // Require non-existent module - match lua.load("require('non-existent')").exec() { - Err(Error::RuntimeError(e)) if e.contains("module 'non-existent' not found") => {} - r => panic!("expected RuntimeError(...) with a specific message, got {r:?}"), - } - - // Require binary module in safe mode - lua.globals() - .get::
("package")? - .set("cpath", temp_dir.path().join("?.so").to_string_lossy())?; - fs::write(temp_dir.path().join("dylib.so"), "")?; - match lua.load("require('dylib')").exec() { - Err(Error::RuntimeError(e)) if cfg!(unix) && e.contains("module 'dylib' not found") => { - assert!(e.contains("dynamic libraries are disabled in safe mode")) - } - Err(Error::RuntimeError(e)) if e.contains("module 'dylib' not found") => {} - r => panic!("expected RuntimeError(...) with a specific message, got {r:?}"), - } - - Ok(()) -} - #[cfg(not(feature = "luau-vector4"))] #[test] fn test_vectors() -> Result<()> { @@ -492,3 +414,6 @@ fn test_thread_events() -> Result<()> { Ok(()) } + +#[path = "luau/require.rs"] +mod require; diff --git a/tests/luau/require.rs b/tests/luau/require.rs new file mode 100644 index 0000000..ad07735 --- /dev/null +++ b/tests/luau/require.rs @@ -0,0 +1,100 @@ +use mlua::{IntoLua, Lua, Result, Value}; + +fn run_require(lua: &Lua, path: &str) -> Result { + lua.load(r#"return require(...)"#).call(path) +} + +#[track_caller] +fn get_str(value: &Value, key: impl IntoLua) -> String { + value.as_table().unwrap().get::(key).unwrap() +} + +#[test] +fn test_require_errors() { + let lua = Lua::new(); + + // RequireAbsolutePath + let res = run_require(&lua, "/an/absolute/path"); + assert!(res.is_err()); + assert!( + (res.unwrap_err().to_string()).contains("require path must start with a valid prefix: ./, ../, or @") + ); + + // RequireUnprefixedPath + let res = run_require(&lua, "an/unprefixed/path"); + assert!(res.is_err()); + assert!( + (res.unwrap_err().to_string()).contains("require path must start with a valid prefix: ./, ../, or @") + ); +} + +#[test] +fn test_require_without_config() { + let lua = Lua::new(); + + // RequireSimpleRelativePath + let res = run_require(&lua, "./require/without_config/dependency").unwrap(); + assert_eq!("result from dependency", get_str(&res, 1)); + + // RequireRelativeToRequiringFile + let res = run_require(&lua, "./require/without_config/module").unwrap(); + assert_eq!("result from dependency", get_str(&res, 1)); + assert_eq!("required into module", get_str(&res, 2)); + + // RequireLua + let res = run_require(&lua, "./require/without_config/lua_dependency").unwrap(); + assert_eq!("result from lua_dependency", get_str(&res, 1)); + + // RequireInitLuau + let res = run_require(&lua, "./require/without_config/luau").unwrap(); + assert_eq!("result from init.luau", get_str(&res, 1)); + + // RequireInitLua + let res = run_require(&lua, "./require/without_config/lua").unwrap(); + assert_eq!("result from init.lua", get_str(&res, 1)); + + // RequireSubmoduleUsingSelf + let res = run_require(&lua, "./require/without_config/nested_module_requirer").unwrap(); + assert_eq!("result from submodule", get_str(&res, 1)); + + // RequireWithFileAmbiguity + let res = run_require(&lua, "./require/without_config/ambiguous_file_requirer"); + assert!(res.is_err()); + assert!((res.unwrap_err().to_string()).contains("require path could not be resolved to a unique file")); + + // RequireWithDirectoryAmbiguity + let res = run_require(&lua, "./require/without_config/ambiguous_directory_requirer"); + assert!(res.is_err()); + assert!((res.unwrap_err().to_string()).contains("require path could not be resolved to a unique file")); + + // CheckCachedResult + let res = run_require(&lua, "./require/without_config/validate_cache").unwrap(); + assert!(res.is_table()); +} + +#[test] +fn test_require_with_config() { + let lua = Lua::new(); + + // RequirePathWithAlias + let res = run_require(&lua, "./require/with_config/src/alias_requirer").unwrap(); + assert_eq!("result from dependency", get_str(&res, 1)); + + // RequirePathWithParentAlias + let res = run_require(&lua, "./require/with_config/src/parent_alias_requirer").unwrap(); + assert_eq!("result from other_dependency", get_str(&res, 1)); + + // RequirePathWithAliasPointingToDirectory + let res = run_require(&lua, "./require/with_config/src/directory_alias_requirer").unwrap(); + assert_eq!("result from subdirectory_dependency", get_str(&res, 1)); + + // RequireAliasThatDoesNotExist + let res = run_require(&lua, "@this.alias.does.not.exist"); + assert!(res.is_err()); + assert!((res.unwrap_err().to_string()).contains("@this.alias.does.not.exist is not a valid alias")); + + // IllegalAlias + let res = run_require(&lua, "@"); + assert!(res.is_err()); + assert!((res.unwrap_err().to_string()).contains("@ is not a valid alias")); +} diff --git a/tests/luau/require/with_config/.luaurc b/tests/luau/require/with_config/.luaurc new file mode 100644 index 0000000..2b64ad0 --- /dev/null +++ b/tests/luau/require/with_config/.luaurc @@ -0,0 +1,6 @@ +{ + "aliases": { + "dep": "./this_should_be_overwritten_by_child_luaurc", + "otherdep": "./src/other_dependency" + } +} diff --git a/tests/luau/require/with_config/GlobalLuauLibraries/global_library.luau b/tests/luau/require/with_config/GlobalLuauLibraries/global_library.luau new file mode 100644 index 0000000..0508e0b --- /dev/null +++ b/tests/luau/require/with_config/GlobalLuauLibraries/global_library.luau @@ -0,0 +1 @@ +return {"result from global_library"} diff --git a/tests/luau/require/with_config/ProjectLuauLibraries/library.luau b/tests/luau/require/with_config/ProjectLuauLibraries/library.luau new file mode 100644 index 0000000..9470401 --- /dev/null +++ b/tests/luau/require/with_config/ProjectLuauLibraries/library.luau @@ -0,0 +1 @@ +return {"result from library"} diff --git a/tests/luau/require/with_config/src/.luaurc b/tests/luau/require/with_config/src/.luaurc new file mode 100644 index 0000000..2726333 --- /dev/null +++ b/tests/luau/require/with_config/src/.luaurc @@ -0,0 +1,6 @@ +{ + "aliases": { + "dep": "./dependency", + "subdir": "./subdirectory" + } +} diff --git a/tests/luau/require/with_config/src/alias_requirer.luau b/tests/luau/require/with_config/src/alias_requirer.luau new file mode 100644 index 0000000..4375a78 --- /dev/null +++ b/tests/luau/require/with_config/src/alias_requirer.luau @@ -0,0 +1 @@ +return require("@dep") diff --git a/tests/luau/require/with_config/src/dependency.luau b/tests/luau/require/with_config/src/dependency.luau new file mode 100644 index 0000000..07466f4 --- /dev/null +++ b/tests/luau/require/with_config/src/dependency.luau @@ -0,0 +1 @@ +return {"result from dependency"} diff --git a/tests/luau/require/with_config/src/directory_alias_requirer.luau b/tests/luau/require/with_config/src/directory_alias_requirer.luau new file mode 100644 index 0000000..3b19d4f --- /dev/null +++ b/tests/luau/require/with_config/src/directory_alias_requirer.luau @@ -0,0 +1 @@ +return(require("@subdir/subdirectory_dependency")) diff --git a/tests/luau/require/with_config/src/other_dependency.luau b/tests/luau/require/with_config/src/other_dependency.luau new file mode 100644 index 0000000..8c582dc --- /dev/null +++ b/tests/luau/require/with_config/src/other_dependency.luau @@ -0,0 +1 @@ +return {"result from other_dependency"} diff --git a/tests/luau/require/with_config/src/parent_alias_requirer.luau b/tests/luau/require/with_config/src/parent_alias_requirer.luau new file mode 100644 index 0000000..a8e8de0 --- /dev/null +++ b/tests/luau/require/with_config/src/parent_alias_requirer.luau @@ -0,0 +1 @@ +return require("@otherdep") diff --git a/tests/luau/require/with_config/src/subdirectory/subdirectory_dependency.luau b/tests/luau/require/with_config/src/subdirectory/subdirectory_dependency.luau new file mode 100644 index 0000000..8bbd0be --- /dev/null +++ b/tests/luau/require/with_config/src/subdirectory/subdirectory_dependency.luau @@ -0,0 +1 @@ +return {"result from subdirectory_dependency"} diff --git a/tests/luau/require/without_config/ambiguous/directory/dependency.luau b/tests/luau/require/without_config/ambiguous/directory/dependency.luau new file mode 100644 index 0000000..07466f4 --- /dev/null +++ b/tests/luau/require/without_config/ambiguous/directory/dependency.luau @@ -0,0 +1 @@ +return {"result from dependency"} diff --git a/tests/luau/require/without_config/ambiguous/directory/dependency/init.luau b/tests/luau/require/without_config/ambiguous/directory/dependency/init.luau new file mode 100644 index 0000000..07466f4 --- /dev/null +++ b/tests/luau/require/without_config/ambiguous/directory/dependency/init.luau @@ -0,0 +1 @@ +return {"result from dependency"} diff --git a/tests/luau/require/without_config/ambiguous/file/dependency.lua b/tests/luau/require/without_config/ambiguous/file/dependency.lua new file mode 100644 index 0000000..07466f4 --- /dev/null +++ b/tests/luau/require/without_config/ambiguous/file/dependency.lua @@ -0,0 +1 @@ +return {"result from dependency"} diff --git a/tests/luau/require/without_config/ambiguous/file/dependency.luau b/tests/luau/require/without_config/ambiguous/file/dependency.luau new file mode 100644 index 0000000..07466f4 --- /dev/null +++ b/tests/luau/require/without_config/ambiguous/file/dependency.luau @@ -0,0 +1 @@ +return {"result from dependency"} diff --git a/tests/luau/require/without_config/ambiguous_directory_requirer.luau b/tests/luau/require/without_config/ambiguous_directory_requirer.luau new file mode 100644 index 0000000..e46be80 --- /dev/null +++ b/tests/luau/require/without_config/ambiguous_directory_requirer.luau @@ -0,0 +1,3 @@ +local result = require("./ambiguous/directory/dependency") +result[#result+1] = "required into module" +return result diff --git a/tests/luau/require/without_config/ambiguous_file_requirer.luau b/tests/luau/require/without_config/ambiguous_file_requirer.luau new file mode 100644 index 0000000..8e3a576 --- /dev/null +++ b/tests/luau/require/without_config/ambiguous_file_requirer.luau @@ -0,0 +1,3 @@ +local result = require("./ambiguous/file/dependency") +result[#result+1] = "required into module" +return result diff --git a/tests/luau/require/without_config/dependency.luau b/tests/luau/require/without_config/dependency.luau new file mode 100644 index 0000000..07466f4 --- /dev/null +++ b/tests/luau/require/without_config/dependency.luau @@ -0,0 +1 @@ +return {"result from dependency"} diff --git a/tests/luau/require/without_config/lua/init.lua b/tests/luau/require/without_config/lua/init.lua new file mode 100644 index 0000000..7c28b73 --- /dev/null +++ b/tests/luau/require/without_config/lua/init.lua @@ -0,0 +1 @@ +return {"result from init.lua"} diff --git a/tests/luau/require/without_config/lua_dependency.lua b/tests/luau/require/without_config/lua_dependency.lua new file mode 100644 index 0000000..aec2d82 --- /dev/null +++ b/tests/luau/require/without_config/lua_dependency.lua @@ -0,0 +1 @@ +return {"result from lua_dependency"} diff --git a/tests/luau/require/without_config/luau/init.luau b/tests/luau/require/without_config/luau/init.luau new file mode 100644 index 0000000..7246346 --- /dev/null +++ b/tests/luau/require/without_config/luau/init.luau @@ -0,0 +1 @@ +return {"result from init.luau"} diff --git a/tests/luau/require/without_config/module.luau b/tests/luau/require/without_config/module.luau new file mode 100644 index 0000000..1d1393f --- /dev/null +++ b/tests/luau/require/without_config/module.luau @@ -0,0 +1,3 @@ +local result = require("./dependency") +result[#result+1] = "required into module" +return result diff --git a/tests/luau/require/without_config/nested/init.luau b/tests/luau/require/without_config/nested/init.luau new file mode 100644 index 0000000..75b9617 --- /dev/null +++ b/tests/luau/require/without_config/nested/init.luau @@ -0,0 +1,2 @@ +local result = require("@self/submodule") +return result diff --git a/tests/luau/require/without_config/nested/submodule.luau b/tests/luau/require/without_config/nested/submodule.luau new file mode 100644 index 0000000..9221587 --- /dev/null +++ b/tests/luau/require/without_config/nested/submodule.luau @@ -0,0 +1 @@ +return {"result from submodule"} diff --git a/tests/luau/require/without_config/nested_module_requirer.luau b/tests/luau/require/without_config/nested_module_requirer.luau new file mode 100644 index 0000000..fc8d5e7 --- /dev/null +++ b/tests/luau/require/without_config/nested_module_requirer.luau @@ -0,0 +1,3 @@ +local result = require("./nested") +result[#result+1] = "required into module" +return result diff --git a/tests/luau/require/without_config/validate_cache.luau b/tests/luau/require/without_config/validate_cache.luau new file mode 100644 index 0000000..dad139b --- /dev/null +++ b/tests/luau/require/without_config/validate_cache.luau @@ -0,0 +1,4 @@ +local result1 = require("./dependency") +local result2 = require("./dependency") +assert(result1 == result2, "expect the same result when requiring the same module twice") +return {} \ No newline at end of file diff --git a/tests/module/Cargo.toml b/tests/module/Cargo.toml index f107ad7..c2e0da8 100644 --- a/tests/module/Cargo.toml +++ b/tests/module/Cargo.toml @@ -18,7 +18,6 @@ lua53 = ["mlua/lua53"] lua52 = ["mlua/lua52"] lua51 = ["mlua/lua51"] luajit = ["mlua/luajit"] -luau = ["mlua/luau"] [dependencies] mlua = { path = "../..", features = ["module"] } diff --git a/tests/module/loader/Cargo.toml b/tests/module/loader/Cargo.toml index 64b196f..b51f002 100644 --- a/tests/module/loader/Cargo.toml +++ b/tests/module/loader/Cargo.toml @@ -10,7 +10,6 @@ lua53 = ["mlua/lua53"] lua52 = ["mlua/lua52"] lua51 = ["mlua/lua51"] luajit = ["mlua/luajit"] -luau = ["mlua/luau"] vendored = ["mlua/vendored"] [dependencies]