Add luaurc to lune-utils crate, resolve issues with require in lune-std

This commit is contained in:
Filip Tibell 2024-04-21 21:06:14 +02:00
parent cf7e1f3cd9
commit a714efdac4
No known key found for this signature in database
9 changed files with 196 additions and 26 deletions

3
Cargo.lock generated
View file

@ -1651,6 +1651,9 @@ dependencies = [
"once_cell",
"path-clean",
"pathdiff",
"serde",
"serde_json",
"tokio",
]
[[package]]

View file

@ -1,8 +1,8 @@
use mlua::prelude::*;
use lune_utils::{
luaurc::LuauRc,
path::{clean_path_and_make_absolute, diff_path, get_current_dir},
LuauRc,
};
use super::context::*;

View file

@ -27,9 +27,9 @@ use crate::library::LuneStandardLibrary;
*/
#[derive(Debug, Clone)]
pub(super) struct RequireContext {
cache_libraries: Arc<AsyncMutex<HashMap<LuneStandardLibrary, LuaResult<LuaRegistryKey>>>>,
cache_results: Arc<AsyncMutex<HashMap<PathBuf, LuaResult<LuaRegistryKey>>>>,
cache_pending: Arc<AsyncMutex<HashMap<PathBuf, Sender<()>>>>,
libraries: Arc<AsyncMutex<HashMap<LuneStandardLibrary, LuaResult<LuaRegistryKey>>>>,
results: Arc<AsyncMutex<HashMap<PathBuf, LuaResult<LuaRegistryKey>>>>,
pending: Arc<AsyncMutex<HashMap<PathBuf, Sender<()>>>>,
}
impl RequireContext {
@ -42,9 +42,9 @@ impl RequireContext {
*/
pub fn new() -> Self {
Self {
cache_libraries: Arc::new(AsyncMutex::new(HashMap::new())),
cache_results: Arc::new(AsyncMutex::new(HashMap::new())),
cache_pending: Arc::new(AsyncMutex::new(HashMap::new())),
libraries: Arc::new(AsyncMutex::new(HashMap::new())),
results: Arc::new(AsyncMutex::new(HashMap::new())),
pending: Arc::new(AsyncMutex::new(HashMap::new())),
}
}
@ -57,7 +57,6 @@ impl RequireContext {
absolute path by prepending the current working directory.
*/
pub fn resolve_paths(
&self,
source: impl AsRef<str>,
path: impl AsRef<str>,
) -> LuaResult<(PathBuf, PathBuf)> {
@ -66,7 +65,7 @@ impl RequireContext {
.ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))?
.join(path.as_ref());
let abs_path = clean_path_and_make_absolute(path);
let abs_path = clean_path_and_make_absolute(&path);
let rel_path = clean_path(path);
Ok((abs_path, rel_path))
@ -77,7 +76,7 @@ impl RequireContext {
*/
pub fn is_cached(&self, abs_path: impl AsRef<Path>) -> LuaResult<bool> {
let is_cached = self
.cache_results
.results
.try_lock()
.expect("RequireContext may not be used from multiple threads")
.contains_key(abs_path.as_ref());
@ -89,7 +88,7 @@ impl RequireContext {
*/
pub fn is_pending(&self, abs_path: impl AsRef<Path>) -> LuaResult<bool> {
let is_pending = self
.cache_pending
.pending
.try_lock()
.expect("RequireContext may not be used from multiple threads")
.contains_key(abs_path.as_ref());
@ -107,7 +106,7 @@ impl RequireContext {
abs_path: impl AsRef<Path>,
) -> LuaResult<LuaMultiValue<'lua>> {
let results = self
.cache_results
.results
.try_lock()
.expect("RequireContext may not be used from multiple threads");
@ -137,7 +136,7 @@ impl RequireContext {
) -> LuaResult<LuaMultiValue<'lua>> {
let mut thread_recv = {
let pending = self
.cache_pending
.pending
.try_lock()
.expect("RequireContext may not be used from multiple threads");
let thread_id = pending
@ -200,7 +199,7 @@ impl RequireContext {
// Set this abs path as currently pending
let (broadcast_tx, _) = broadcast::channel(1);
self.cache_pending
self.pending
.try_lock()
.expect("RequireContext may not be used from multiple threads")
.insert(abs_path.to_path_buf(), broadcast_tx);
@ -220,7 +219,7 @@ impl RequireContext {
// NOTE: We use the async lock and not try_lock here because
// some other thread may be wanting to insert into the require
// cache at the same time, and that's not an actual error case
self.cache_results
self.results
.lock()
.await
.insert(abs_path.to_path_buf(), load_res);
@ -229,7 +228,7 @@ impl RequireContext {
// broadcast a message to let any listeners know that this
// path has now finished the require process and is cached
let broadcast_tx = self
.cache_pending
.pending
.try_lock()
.expect("RequireContext may not be used from multiple threads")
.remove(abs_path)
@ -253,7 +252,7 @@ impl RequireContext {
};
let mut cache = self
.cache_libraries
.libraries
.try_lock()
.expect("RequireContext may not be used from multiple threads");

View file

@ -2,7 +2,7 @@ use mlua::prelude::*;
use super::context::*;
pub(super) async fn require<'lua, 'ctx>(
pub(super) fn require<'lua, 'ctx>(
lua: &'lua Lua,
ctx: &'ctx RequireContext,
name: &str,

View file

@ -6,7 +6,7 @@ mod context;
use context::RequireContext;
mod alias;
mod builtin;
mod library;
mod path;
const REQUIRE_IMPL: &str = r"
@ -36,7 +36,7 @@ pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
*/
let require_fn = lua.create_async_function(require)?;
let get_source_fn = lua.create_function(move |lua, _: ()| match lua.inspect_stack(2) {
let get_source_fn = lua.create_function(move |lua, (): ()| match lua.inspect_stack(2) {
None => Err(LuaError::runtime(
"Failed to get stack info for require source",
)),
@ -80,11 +80,8 @@ async fn require<'lua>(
.app_data_ref()
.expect("Failed to get RequireContext from app data");
if let Some(builtin_name) = path
.strip_prefix("@lune/")
.map(|name| name.to_ascii_lowercase())
{
builtin::require(lua, &context, &builtin_name).await
if let Some(builtin_name) = path.strip_prefix("@lune/").map(str::to_ascii_lowercase) {
library::require(lua, &context, &builtin_name)
} else if let Some(aliased_path) = path.strip_prefix('@') {
let (alias, path) = aliased_path.split_once('/').ok_or(LuaError::runtime(
"Require with custom alias must contain '/' delimiter",

View file

@ -14,7 +14,7 @@ pub(super) async fn require<'lua, 'ctx>(
where
'lua: 'ctx,
{
let (abs_path, rel_path) = ctx.resolve_paths(source, path)?;
let (abs_path, rel_path) = RequireContext::resolve_paths(source, path)?;
require_abs_rel(lua, ctx, abs_path, rel_path).await
}

View file

@ -13,7 +13,12 @@ workspace = true
[dependencies]
mlua = { version = "0.9.7", features = ["async"] }
tokio = { version = "1", default-features = false, features = ["fs"] }
dunce = "1.0"
once_cell = "1.17"
path-clean = "1.0"
pathdiff = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View file

@ -1,9 +1,11 @@
#![allow(clippy::cargo_common_metadata)]
mod luaurc;
mod table_builder;
mod version_string;
pub mod path;
pub use self::luaurc::LuauRc;
pub use self::table_builder::TableBuilder;
pub use self::version_string::get_version_string;

View file

@ -0,0 +1,164 @@
use std::{
collections::HashMap,
path::{Path, PathBuf, MAIN_SEPARATOR},
sync::Arc,
};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use tokio::fs::read;
use crate::path::{clean_path, clean_path_and_make_absolute};
const LUAURC_FILE: &str = ".luaurc";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum LuauLanguageMode {
NoCheck,
NonStrict,
Strict,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LuauRcConfig {
#[serde(skip_serializing_if = "Option::is_none")]
language_mode: Option<LuauLanguageMode>,
#[serde(skip_serializing_if = "Option::is_none")]
lint: Option<HashMap<String, JsonValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
lint_errors: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
type_errors: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
globals: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
paths: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
aliases: Option<HashMap<String, String>>,
}
/**
A deserialized `.luaurc` file.
Contains utility methods for validating and searching for aliases.
*/
#[derive(Debug, Clone)]
pub struct LuauRc {
dir: Arc<Path>,
config: LuauRcConfig,
}
impl LuauRc {
/**
Reads a `.luaurc` file from the given directory.
If the file does not exist, or if it is invalid, this function returns `None`.
*/
pub async fn read(dir: impl AsRef<Path>) -> Option<Self> {
let dir = clean_path_and_make_absolute(dir);
let path = dir.join(LUAURC_FILE);
let bytes = read(&path).await.ok()?;
let config = serde_json::from_slice(&bytes).ok()?;
Some(Self {
dir: dir.into(),
config,
})
}
/**
Reads a `.luaurc` file from the given directory, and then recursively searches
for a `.luaurc` file in the parent directories if a predicate is not satisfied.
If no `.luaurc` file exists, or if they are invalid, this function returns `None`.
*/
pub async fn read_recursive(
dir: impl AsRef<Path>,
mut predicate: impl FnMut(&Self) -> bool,
) -> Option<Self> {
let mut current = clean_path_and_make_absolute(dir);
loop {
if let Some(rc) = Self::read(&current).await {
if predicate(&rc) {
return Some(rc);
}
}
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
return None;
}
}
}
/**
Validates that the `.luaurc` file is correct.
This primarily validates aliases since they are not
validated during creation of the [`LuauRc`] struct.
# Errors
If an alias key is invalid.
*/
pub fn validate(&self) -> Result<(), String> {
if let Some(aliases) = &self.config.aliases {
for alias in aliases.keys() {
if !is_valid_alias_key(alias) {
return Err(format!("invalid alias key: {alias}"));
}
}
}
Ok(())
}
/**
Gets a copy of all aliases in the `.luaurc` file.
Will return an empty map if there are no aliases.
*/
#[must_use]
pub fn aliases(&self) -> HashMap<String, String> {
self.config.aliases.clone().unwrap_or_default()
}
/**
Finds an alias in the `.luaurc` file by name.
If the alias does not exist, this function returns `None`.
*/
#[must_use]
pub fn find_alias(&self, name: &str) -> Option<PathBuf> {
self.config.aliases.as_ref().and_then(|aliases| {
aliases.iter().find_map(|(alias, path)| {
if alias
.trim_end_matches(MAIN_SEPARATOR)
.eq_ignore_ascii_case(name)
&& is_valid_alias_key(alias)
{
Some(clean_path(self.dir.join(path)))
} else {
None
}
})
})
}
}
fn is_valid_alias_key(alias: impl AsRef<str>) -> bool {
let alias = alias.as_ref();
if alias.is_empty()
|| alias.starts_with('.')
|| alias.starts_with("..")
|| alias.chars().any(|c| c == MAIN_SEPARATOR)
{
false // Paths are not valid alias keys
} else {
alias.chars().all(is_valid_alias_char)
}
}
fn is_valid_alias_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'
}