use crate::select::Selector;
use luaux::{CompileError, Config};
use std::path::{Path, PathBuf};
pub struct Options {
pub source_root: PathBuf,
pub out_root: Option<PathBuf>,
pub write: bool,
}
impl Options {
pub fn output_for(&self, input: &Path) -> PathBuf {
match &self.out_root {
None => input.with_extension("luau"),
Some(root) => {
let relative = input.strip_prefix(&self.source_root).unwrap_or(input);
root.join(relative).with_extension("luau")
}
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Report {
pub compiled: usize,
pub unchanged: usize,
pub copied: usize,
pub removed: usize,
pub warnings: usize,
pub failures: usize,
}
impl Report {
pub fn ok(&self) -> bool {
self.failures == 0
}
pub fn summary(&self) -> String {
format!(
"{} compiled, {} unchanged, {} copied, {} removed, {} warning(s), {} failed",
self.compiled, self.unchanged, self.copied, self.removed, self.warnings, self.failures
)
}
}
pub fn configure(source_root: &Path) -> Result<Config, String> {
find_config(source_root)
}
pub fn run(options: &Options, config: &Config) -> Report {
let mut report = Report::default();
let selector = match Selector::new(&config.build.include, &config.build.exclude) {
Ok(selector) => selector,
Err(error) => {
eprintln!("luaux: luaux.toml: {error}");
report.failures += 1;
return report;
}
};
let mut inputs = Vec::new();
if let Err(error) = collect_files(&options.source_root, &mut inputs) {
eprintln!("luaux: {}: {error}", options.source_root.display());
report.failures += 1;
return report;
}
inputs.retain(|path| {
path.extension().and_then(|e| e.to_str()) == Some("luaux")
&& selector.allows(relative(path, &options.source_root))
});
inputs.sort();
for input in &inputs {
let source = match std::fs::read_to_string(input) {
Ok(source) => source,
Err(error) => {
eprintln!("luaux: {}: {error}", input.display());
report.failures += 1;
continue;
}
};
let path = input.display().to_string();
let (compiled, warnings) = match luaux::compile_verified(&source, &luaux::Vide, config) {
Ok(result) => result,
Err(error) => {
diagnose(&path, &source, &error, true);
report.failures += 1;
continue;
}
};
for warning in warnings {
diagnose(
&path,
&source,
&CompileError {
message: warning.message,
offset: warning.offset,
length: warning.length,
help: warning.help,
},
false,
);
report.warnings += 1;
}
if !options.write {
report.compiled += 1;
continue;
}
let output = options.output_for(input);
if reads_same(&output, compiled.as_bytes()) {
report.unchanged += 1;
continue;
}
if let Err(error) = write_file(&output, compiled.as_bytes()) {
eprintln!("luaux: {error}");
report.failures += 1;
continue;
}
println!("{} -> {}", input.display(), output.display());
report.compiled += 1;
}
if options.write {
if let Some(root) = &options.out_root {
match passthrough(&options.source_root, root, &selector) {
Ok(count) => report.copied = count,
Err(error) => {
eprintln!("luaux: {error}");
report.failures += 1;
}
}
if config.build.clean {
match clean(&options.source_root, root) {
Ok(count) => report.removed = count,
Err(error) => {
eprintln!("luaux: {error}");
report.failures += 1;
}
}
}
}
}
report
}
fn clean(source_root: &Path, out_root: &Path) -> Result<usize, String> {
let mut outputs = Vec::new();
collect_files(out_root, &mut outputs)
.map_err(|error| format!("{}: {error}", out_root.display()))?;
let mut removed = 0usize;
for output in outputs {
let relative = relative(&output, out_root).to_path_buf();
let from_source = source_root.join(&relative);
let has_source = from_source.is_file()
|| (relative.extension().and_then(|e| e.to_str()) == Some("luau")
&& from_source.with_extension("luaux").is_file());
if has_source {
continue;
}
std::fs::remove_file(&output).map_err(|error| format!("{}: {error}", output.display()))?;
println!("{} (removed, no source)", output.display());
removed += 1;
}
Ok(removed)
}
fn passthrough(source_root: &Path, out_root: &Path, selector: &Selector) -> Result<usize, String> {
let mut sources = Vec::new();
collect_files(source_root, &mut sources)
.map_err(|error| format!("{}: {error}", source_root.display()))?;
let mut copied = 0usize;
for source in sources {
if source.extension().and_then(|e| e.to_str()) == Some("luaux") {
continue;
}
if source.with_extension("luaux").is_file() {
continue;
}
let relative = relative(&source, source_root);
if !selector.allows(relative) {
continue;
}
let destination = out_root.join(relative);
let contents =
std::fs::read(&source).map_err(|error| format!("{}: {error}", source.display()))?;
if reads_same(&destination, &contents) {
continue;
}
write_file(&destination, &contents)?;
println!("{} -> {} (copied)", source.display(), destination.display());
copied += 1;
}
Ok(copied)
}
fn reads_same(path: &Path, contents: &[u8]) -> bool {
std::fs::read(path).is_ok_and(|existing| existing == contents)
}
fn write_file(path: &Path, contents: &[u8]) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|error| format!("{}: {error}", parent.display()))?;
}
std::fs::write(path, contents).map_err(|error| format!("{}: {error}", path.display()))
}
pub fn collect_files(path: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
if path.is_file() {
out.push(path.to_path_buf());
return Ok(());
}
if !path.is_dir() {
return Ok(());
}
for entry in std::fs::read_dir(path)? {
let entry = entry?.path();
if entry.is_dir() {
collect_files(&entry, out)?;
} else {
out.push(entry);
}
}
Ok(())
}
fn relative<'a>(path: &'a Path, root: &Path) -> &'a Path {
path.strip_prefix(root).unwrap_or(path)
}
fn find_config(start: &Path) -> Result<Config, String> {
for directory in ancestors(start) {
if !directory.join("luaux.toml").is_file() {
continue;
}
let (config, warnings) =
Config::load_reporting(&directory).map_err(|error| error.message)?;
for warning in warnings {
eprintln!("luaux: {warning}");
}
return Ok(config);
}
Ok(Config::default())
}
pub fn ancestors(start: &Path) -> Vec<PathBuf> {
let absolute = std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf());
let first = if absolute.is_dir() {
absolute.as_path()
} else {
absolute.parent().unwrap_or(absolute.as_path())
};
first.ancestors().map(Path::to_path_buf).collect()
}
fn diagnose(path: &str, source: &str, error: &CompileError, fatal: bool) {
let offset = error.offset.min(source.len());
let length = error
.length
.clamp(1, source.len().saturating_sub(offset).max(1));
let label = miette::LabeledSpan::new(error.help.clone(), offset, length);
let diagnostic = miette::miette!(
severity = if fatal {
miette::Severity::Error
} else {
miette::Severity::Warning
},
labels = vec![label],
"{}",
error.message
)
.with_source_code(miette::NamedSource::new(path, source.to_string()).with_language("Lua"));
eprintln!("{diagnostic:?}");
}