use crate::{
config::Config,
lua_installation::LuaInstallation,
lua_rockspec::{DeploySpec, LuaModule, ModulePaths},
path::{Paths, PathsError},
project::project_files,
tree::{RockLayout, Tree},
variables::{self, Environment, VariableSubstitutionError},
};
use itertools::Itertools;
use path_slash::PathExt;
use shlex::try_quote;
use std::{
collections::HashMap,
io,
path::{Path, PathBuf},
process::{ExitStatus, Output, Stdio},
string::FromUtf8Error,
};
use target_lexicon::Triple;
use thiserror::Error;
use tokio::process::Command;
use which::which;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use super::external_dependency::ExternalDependencyInfo;
pub(crate) fn copy_lua_to_module_path(
source: &PathBuf,
target_module: &LuaModule,
target_dir: &Path,
) -> io::Result<()> {
let target = target_dir.join(target_module.to_lua_path());
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).map_err(|err| {
io::Error::other(format!(
"Failed to create directory {}:\n{}",
parent.display(),
err
))
})?;
}
std::fs::copy(source, &target).map_err(|err| {
io::Error::other(format!(
"Failed to copy {} to {}:\n{}",
source.display(),
target.display(),
err
))
})?;
Ok(())
}
pub(crate) async fn recursive_copy_dir(src: &PathBuf, dest: &Path) -> Result<(), io::Error> {
if src.exists() {
for file in project_files(src) {
let relative_src_path: PathBuf = pathdiff::diff_paths(src.join(&file), src).ok_or(
io::Error::other("failed to diff directories [THIS IS A BUG!]"),
)?;
let target = dest.join(relative_src_path);
if let Some(parent) = target.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::copy(&file, target).await?;
}
}
Ok(())
}
#[derive(Error, Debug)]
pub enum OutputValidationError {
#[error("compilation failed.\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}")]
CommandFailure {
status: ExitStatus,
stdout: String,
stderr: String,
},
}
fn validate_output(output: &Output) -> Result<(), OutputValidationError> {
if !output.status.success() {
return Err(OutputValidationError::CommandFailure {
status: output.status,
stdout: String::from_utf8_lossy(&output.stdout).into(),
stderr: String::from_utf8_lossy(&output.stderr).into(),
});
}
Ok(())
}
#[derive(Error, Debug)]
pub enum CompileCFilesError {
#[error("IO operation while compiling C files:\n{0}")]
Io(#[from] io::Error),
#[error("failed to compile intermediates from C files:\n{0}")]
CompileIntermediates(cc::Error),
#[error("error compiling C files (compilation failed):\n{0}")]
Compilation(#[from] cc::Error),
#[error("error compiling C files (linking failed):\n{0}")]
Link(#[from] LinkCModulesError),
}
pub(crate) async fn compile_c_files(
files: &Vec<PathBuf>,
target_module: &LuaModule,
target_dir: &Path,
lua: &LuaInstallation,
external_dependencies: &HashMap<String, ExternalDependencyInfo>,
config: &Config,
) -> Result<(), CompileCFilesError> {
let target = target_dir.join(target_module.to_lib_path());
let target_parent_dir = target.parent().ok_or(io::Error::other(format!(
"couldn't determine build target parent directory:\n{}",
target.display()
)))?;
let target_file_name = target
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
std::fs::create_dir_all(target_parent_dir)?;
let host = Triple::host();
let mut build = cc::Build::new();
let intermediate_dir = tempfile::tempdir()?;
let build = build
.cargo_output(config.verbose())
.cargo_metadata(config.verbose())
.cargo_warnings(config.verbose())
.warnings(config.verbose())
.files(files)
.host(&host.to_string())
.target(&host.to_string())
.includes(lua.includes())
.includes(
external_dependencies
.iter()
.filter_map(|(_, dep)| dep.include_dir.as_ref()),
)
.opt_level(3)
.out_dir(intermediate_dir);
let compiler = build.try_get_compiler()?;
if compiler.is_like_msvc() {
build.flag("-W0");
} else {
build.flag("-w");
}
for arg in lua.define_flags() {
build.flag(&arg);
}
let objects = build
.try_compile_intermediates()
.map_err(CompileCFilesError::CompileIntermediates)?;
link_c_artifacts(
build,
lua,
external_dependencies,
&target_file_name,
Vec::new(),
Vec::new(),
target_module,
target_parent_dir,
objects,
config,
)
.await?;
Ok(())
}
fn mk_def_file(
dir: &Path,
output_file_name: &str,
target_module: &LuaModule,
) -> io::Result<PathBuf> {
let mut def_file: PathBuf = dir.join(output_file_name);
def_file.set_extension(".def");
let exported_name = target_module.to_string().replace(".", "_");
let exported_name = exported_name
.split_once('-')
.map(|(_, after_hyphen)| after_hyphen.to_string())
.unwrap_or_else(|| exported_name.clone());
let content = format!(
r#"EXPORTS
luaopen_{exported_name}
"#,
);
std::fs::write(&def_file, content)?;
Ok(def_file)
}
pub(crate) fn c_dylib_extension() -> &'static str {
if cfg!(target_env = "msvc") {
"dll"
} else {
"so"
}
}
pub(crate) fn c_lib_extension() -> &'static str {
if cfg!(target_env = "msvc") {
"lib"
} else {
"a"
}
}
pub(crate) fn c_obj_extension() -> &'static str {
if cfg!(target_env = "msvc") {
"obj"
} else {
"o"
}
}
pub(crate) fn default_cflags() -> &'static str {
if cfg!(target_env = "msvc") {
"/NOLOGO /MD /O2"
} else {
"-O2"
}
}
pub(crate) fn default_libflag() -> &'static str {
if cfg!(target_os = "macos") {
"-bundle -undefined dynamic_lookup -all_load"
} else if cfg!(target_env = "msvc") {
"/NOLOGO /DLL"
} else {
"-shared"
}
}
#[derive(Error, Debug)]
pub enum CompileCModulesError {
#[error("IO operation failed while compiling C modules:\n{0}")]
Io(#[from] io::Error),
#[error("failed to compile intermediates from C modules: {0}")]
CompileIntermediates(cc::Error),
#[error("error compiling C modules (compilation failed):\n{0}")]
Compilation(#[from] cc::Error),
#[error("error compiling C modules (linking failed):\n{0}")]
Link(#[from] LinkCModulesError),
#[error(transparent)]
VariableSubstitution(#[from] VariableSubstitutionError),
}
#[derive(Error, Debug)]
pub enum LinkCModulesError {
#[error("IO operation failed while linking C modules:\n{0}")]
Io(#[from] io::Error),
#[error(transparent)]
CC(#[from] cc::Error),
#[error("error compiling C modules (output validation failed): {0}")]
OutputValidation(#[from] OutputValidationError),
#[error("compiling C modules succeeded, but the expected library {0} was not created")]
LibOutputNotCreated(String),
}
pub(crate) async fn compile_c_modules(
data: &ModulePaths,
source_dir: &Path,
target_module: &LuaModule,
target_dir: &Path,
lua: &LuaInstallation,
external_dependencies: &HashMap<String, ExternalDependencyInfo>,
config: &Config,
) -> Result<(), CompileCModulesError> {
let target = target_dir.join(target_module.to_lib_path());
let target_parent_dir = target.parent().ok_or(io::Error::other(format!(
"couldn't determine build target parent directory:\n{}",
target.display()
)))?;
tokio::fs::create_dir_all(target_parent_dir).await?;
let host = Triple::host();
let mut build = cc::Build::new();
let source_files = data
.sources
.iter()
.map(|dir| {
variables::substitute(
&[lua, external_dependencies, &Environment {}, config],
&dir.to_slash_lossy(),
)
})
.map(|dir| dir.map(|dir| source_dir.join(dir)))
.try_collect::<_, Vec<_>, _>()?;
let include_dirs = data
.incdirs
.iter()
.map(|dir| {
variables::substitute(
&[lua, external_dependencies, &Environment {}, config],
&dir.to_slash_lossy(),
)
})
.map(|dir| dir.map(|dir| source_dir.join(dir)))
.chain(
external_dependencies
.iter()
.filter_map(|(_, dep)| dep.include_dir.clone())
.map(Result::Ok),
)
.try_collect::<_, Vec<_>, _>()?
.into_iter()
.unique() .collect_vec();
let intermediate_dir = tempfile::tempdir()?;
let build = build
.cargo_output(config.verbose())
.cargo_metadata(config.verbose())
.cargo_warnings(config.verbose())
.warnings(config.verbose())
.files(source_files)
.host(&host.to_string())
.target(&host.to_string())
.includes(&include_dirs)
.includes(lua.includes())
.includes(
external_dependencies
.iter()
.filter_map(|(_, dep)| dep.include_dir.as_ref()),
)
.opt_level(3)
.out_dir(intermediate_dir);
let compiler = build.try_get_compiler()?;
let is_msvc = compiler.is_like_msvc();
if is_msvc {
build.flag("-W0");
} else {
build.flag("-w");
}
for arg in lua.define_flags() {
build.flag(&arg);
}
for (name, value) in &data.defines {
build.define(name, value.as_deref());
}
let target_file_name = target
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let objects = build
.try_compile_intermediates()
.map_err(CompileCModulesError::CompileIntermediates)?;
let libdir_args = data
.libdirs
.iter()
.map(|dir| {
variables::substitute(
&[lua, external_dependencies, &Environment {}, config],
&dir.to_slash_lossy(),
)
})
.map(|libdir| {
libdir.map(|libdir| {
if is_msvc {
format!("/LIBPATH:{}", source_dir.join(libdir).display())
} else {
format!("-L{}", source_dir.join(libdir).display())
}
})
})
.try_collect::<_, Vec<_>, _>()?;
let library_args = data
.libraries
.iter()
.map(|dir| {
variables::substitute(
&[lua, external_dependencies, &Environment {}, config],
&dir.to_slash_lossy(),
)
})
.map(|library| {
library.map(|library| {
if is_msvc {
format!("{}.lib", library)
} else {
format!("-l{}", library)
}
})
})
.try_collect::<_, Vec<_>, _>()?;
link_c_artifacts(
build,
lua,
external_dependencies,
&target_file_name,
libdir_args,
library_args,
target_module,
target_parent_dir,
objects,
config,
)
.await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn link_c_artifacts(
build: &mut cc::Build,
lua: &LuaInstallation,
external_dependencies: &HashMap<String, ExternalDependencyInfo>,
target_file_name: &str,
libdir_args: Vec<String>,
library_args: Vec<String>,
target_module: &LuaModule,
target_parent_dir: &Path,
objects: Vec<PathBuf>,
config: &Config,
) -> Result<(), LinkCModulesError> {
let output_path = target_parent_dir.join(target_file_name);
let compiler = build.try_get_compiler()?;
let temp_work_dir = tempfile::tempdir().map_err(|err| {
io::Error::other(format!(
"failed to create temporary directory for linking:\n{err}"
))
})?;
let is_msvc = compiler.is_like_msvc();
let cmd = build.try_get_compiler()?.to_command();
let mut cmd: tokio::process::Command = cmd.into();
cmd.current_dir(temp_work_dir.path());
add_variable_if_set(config, "LIBFLAG", &mut cmd);
let output = if is_msvc {
let def_file = mk_def_file(temp_work_dir.path(), target_file_name, target_module)?;
cmd.arg("/NOIMPLIB").arg("/NOEXP").args(&objects).arg("/LD");
add_variable_if_set(config, "LDFLAGS", &mut cmd);
cmd.arg("/link")
.arg(format!("/DEF:{}", def_file.display()))
.arg(format!("/OUT:{}", output_path.display()))
.args(lua.lib_link_args(&compiler))
.args(
external_dependencies
.iter()
.flat_map(|(_, dep)| dep.lib_link_args(&compiler)),
)
.args(libdir_args)
.args(library_args)
.output()
.await?
} else {
add_variable_if_set(config, "LDFLAGS", &mut cmd);
cmd.args(vec!["-o".into(), output_path.to_string_lossy().to_string()])
.args(lua.lib_link_args(&build.try_get_compiler()?))
.args(
external_dependencies
.iter()
.flat_map(|(_, dep)| dep.lib_link_args(&compiler)),
)
.args(&objects)
.args(libdir_args)
.args(library_args)
.output()
.await?
};
if config.verbose() {
if !&output.stdout.is_empty() {
println!("{}", String::from_utf8_lossy(&output.stdout));
}
if !&output.stderr.is_empty() {
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
}
validate_output(&output)?;
log_command_output(&output, config);
if output_path.exists() {
Ok(())
} else {
Err(LinkCModulesError::LibOutputNotCreated(
output_path.to_slash_lossy().to_string(),
))
}
}
fn add_variable_if_set(config: &Config, name: &str, cmd: &mut Command) {
let variables = config.variables();
if let Some(var_str) = variables.get(name) {
if !var_str.is_empty() {
let vars = var_str.split_whitespace().collect_vec();
cmd.args(vars);
}
}
}
#[derive(Debug, Error)]
pub enum InstallBinaryError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Paths(#[from] PathsError),
#[error("error wrapping binary: {0}")]
Wrap(#[from] WrapBinaryError),
}
#[derive(Debug, Error)]
pub enum WrapBinaryError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Utf8(#[from] FromUtf8Error),
#[error("no `lua` executable found")]
NoLuaBinary,
}
pub(crate) async fn install_binary(
source: &Path,
target: &str,
tree: &Tree,
lua: &LuaInstallation,
deploy: &DeploySpec,
config: &Config,
) -> Result<PathBuf, InstallBinaryError> {
tokio::fs::create_dir_all(&tree.bin()).await?;
let paths = Paths::new(tree)?;
let script =
if deploy.wrap_bin_scripts && is_compatible_lua_script(source, lua, &paths, config).await {
install_wrapped_binary(source, target, tree, lua, config).await?
} else {
let target = tree.bin().join(target);
tokio::fs::copy(source, &target).await?;
target
};
#[cfg(unix)]
set_executable_permissions(&script).await?;
Ok(script)
}
pub(crate) fn log_command_output(output: &Output, config: &Config) {
if config.verbose() {
if !output.stderr.is_empty() {
println!("{}", String::from_utf8_lossy(&output.stderr));
}
if !output.stdout.is_empty() {
println!("{}", String::from_utf8_lossy(&output.stdout));
}
}
}
async fn install_wrapped_binary(
source: &Path,
target: &str,
tree: &Tree,
lua: &LuaInstallation,
config: &Config,
) -> Result<PathBuf, WrapBinaryError> {
let unwrapped_bin_dir = tree.unwrapped_bin();
tokio::fs::create_dir_all(&unwrapped_bin_dir).await?;
let unwrapped_bin = unwrapped_bin_dir.join(target);
tokio::fs::copy(source, &unwrapped_bin).await?;
#[cfg(target_family = "unix")]
let target = tree.bin().join(target);
#[cfg(target_family = "windows")]
let target = tree.bin().join(format!("{}.bat", target));
let lua_bin = lua
.lua_binary_or_config_override(config)
.ok_or(WrapBinaryError::NoLuaBinary)?;
#[cfg(target_family = "unix")]
let content = format!(
r#"#!/bin/sh
exec "{0}" "{1}" "$@"
"#,
lua_bin,
unwrapped_bin.display(),
);
#[cfg(target_family = "windows")]
let content = format!(
r#"@echo off
setlocal
{0} "{1}" %*
exit /b %ERRORLEVEL%
"#,
lua_bin,
unwrapped_bin.display(),
);
tokio::fs::write(&target, content).await?;
Ok(target)
}
#[cfg(unix)]
async fn set_executable_permissions(script: &Path) -> std::io::Result<()> {
let mut perms = tokio::fs::metadata(&script).await?.permissions();
perms.set_mode(0o744);
tokio::fs::set_permissions(&script, perms).await?;
Ok(())
}
async fn is_compatible_lua_script(
file: &Path,
lua: &LuaInstallation,
paths: &Paths,
config: &Config,
) -> bool {
let lua_path = paths.package_path_prepended().joined();
let lua_cpath = paths.package_cpath_prepended().joined();
let path = paths.path_prepended().joined();
if let Some(lua_bin) = &lua.lua_binary_or_config_override(config) {
let lua_bin_path = PathBuf::from(lua_bin);
if lua_bin_path.is_file() || which(lua_bin).is_ok() {
Command::new(lua_bin)
.arg("-e")
.arg(format!(
"if loadfile('{}') then os.exit(0) else os.exit(1) end",
file.to_slash_lossy()
))
.stderr(Stdio::null())
.stdout(Stdio::null())
.env("LUA_PATH", lua_path)
.env("LUA_CPATH", lua_cpath)
.env("PATH", path)
.status()
.await
.is_ok_and(|status| status.success())
} else {
is_compatible_lua_script_fallback(file, lua, config).await
}
} else {
false
}
}
async fn is_compatible_lua_script_fallback(
file: &Path,
lua_installation: &LuaInstallation,
config: &Config,
) -> bool {
if let Ok(file_content) = tokio::fs::read_to_string(&file).await {
let file_content_without_comments = file_content
.lines()
.filter(|line| !line.trim_start().starts_with("#"))
.collect_vec()
.join("\n");
lua_installation
.lua_binary_or_config_override(config)
.is_some_and(|_| {
ottavino::Lua::core()
.try_enter(|ctx| {
ottavino::Closure::load(
ctx,
None,
file_content_without_comments.as_bytes(),
)?;
Ok(())
})
.is_ok()
})
} else {
false
}
}
pub(crate) fn substitute_variables(
input: &str,
output_paths: &RockLayout,
lua: &LuaInstallation,
external_dependencies: &HashMap<String, ExternalDependencyInfo>,
config: &Config,
) -> Result<String, VariableSubstitutionError> {
variables::substitute(
&[
output_paths,
lua,
external_dependencies,
&Environment {},
config,
],
input,
)
}
pub(crate) fn format_path(path: &Path) -> String {
let path_str = path.to_slash_lossy();
if cfg!(windows) {
path_str.to_string()
} else {
try_quote(&path_str)
.map(|str| str.to_string())
.unwrap_or(format!("'{path_str}'"))
}
}
#[cfg(test)]
mod tests {
use tokio::process::Command;
use crate::{
config::ConfigBuilder, lua_installation::detect_installed_lua_version,
lua_version::LuaVersion, progress::MultiProgress,
};
use super::*;
#[tokio::test]
async fn test_is_compatible_lua_script() {
let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
let config = ConfigBuilder::new()
.unwrap()
.lua_version(lua_version)
.build()
.unwrap();
let lua_version = config.lua_version().unwrap();
let progress = MultiProgress::new(&config);
let bar = progress.map(MultiProgress::new_bar);
let lua = LuaInstallation::new(lua_version, &config, &bar)
.await
.unwrap();
let valid_script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/sample_lua_bin_script_valid");
let temp_dir = assert_fs::TempDir::new().unwrap().to_path_buf();
let tree = Tree::new(temp_dir, lua_version.clone(), &config).unwrap();
let paths = Paths::new(&tree).unwrap();
assert!(is_compatible_lua_script(&valid_script, &lua, &paths, &config).await);
let invalid_script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/sample_lua_bin_script_invalid");
assert!(!is_compatible_lua_script(&invalid_script, &lua, &paths, &config).await);
}
#[tokio::test]
async fn test_install_wrapped_binary() {
let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
let temp = assert_fs::TempDir::new().unwrap();
let config = ConfigBuilder::new()
.unwrap()
.lua_version(lua_version)
.user_tree(Some(temp.to_path_buf()))
.build()
.unwrap();
let lua_version = config.lua_version().unwrap();
let progress = MultiProgress::new(&config);
let bar = progress.map(MultiProgress::new_bar);
let lua = LuaInstallation::new(lua_version, &config, &bar)
.await
.unwrap();
let tree = config.user_tree(lua_version.clone()).unwrap();
let valid_script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/sample_lua_bin_script_valid");
let script_name = "test_script";
let script_path = install_wrapped_binary(&valid_script, script_name, &tree, &lua, &config)
.await
.unwrap();
#[cfg(unix)]
set_executable_permissions(&script_path).await.unwrap();
assert!(Command::new(script_path.to_string_lossy().to_string())
.status()
.await
.is_ok_and(|status| status.success()));
}
}