1use std::{
2 path::{Path, PathBuf},
3 process::Stdio,
4 string::FromUtf8Error,
5};
6
7use itertools::Itertools;
8use miette::Diagnostic;
9use path_slash::PathExt;
10use thiserror::Error;
11use tokio::process::Command;
12use walkdir::{DirEntry, WalkDir};
13use which::which;
14
15use crate::{
16 config::Config,
17 fs,
18 lua_installation::LuaInstallation,
19 lua_rockspec::{DeploySpec, LuaModule},
20 path::{self, Paths},
21 tree::InstallTree,
22};
23
24#[derive(Debug, Error, Diagnostic)]
25pub enum Error {
26 #[error(transparent)]
27 #[diagnostic(transparent)]
28 Fs(#[from] fs::Error),
29 #[error(transparent)]
30 #[diagnostic(transparent)]
31 Paths(#[from] path::Error),
32 #[error(transparent)]
33 Utf8(#[from] FromUtf8Error),
34 #[error("no Lua executable found")]
35 NoLuaBinary {
36 #[help]
37 help: String,
38 },
39}
40
41#[tracing::instrument(
44 name = "Copying Lua module",
45 level = "trace",
46 skip_all,
47 fields(
48 source = source.to_slash_lossy().to_string(),
49 target = target_module.to_string(),
50 target_dir = target_dir.to_slash_lossy().to_string(),
51 ),
52)]
53pub(crate) fn lua_module(
54 source: &PathBuf,
55 target_module: &LuaModule,
56 target_dir: &Path,
57) -> Result<(), fs::Error> {
58 let target_module_path = source
60 .extension()
61 .map(|ext| target_module.to_file_path(&format!(".{}", ext.to_string_lossy().as_ref())))
62 .unwrap_or_else(|| target_module.to_lua_path());
63 let target = target_dir.join(target_module_path);
64
65 if let Some(parent) = target.parent() {
66 fs::sync::create_dir_all(parent)?;
67 }
68
69 fs::sync::copy(source, &target)?;
70 fs::make_writable_sync(&target)?;
71
72 Ok(())
73}
74
75#[tracing::instrument(
77 name = "Installing binary",
78 level = "trace",
79 skip_all,
80 fields(
81 source = source.to_slash_lossy().to_string(),
82 target,
83 ),
84)]
85pub(crate) async fn binary(
86 source: &Path,
87 target: &str,
88 tree: &impl InstallTree,
89 lua: &LuaInstallation,
90 deploy: &DeploySpec,
91 config: &Config,
92) -> Result<PathBuf, Error> where
93{
94 fs::tokio::create_dir_all(&tree.bin()).await?;
95 let paths = Paths::new(tree)?;
96 let script = if config.wrap_bin_scripts()
97 && deploy.wrap_bin_scripts
98 && is_compatible_lua_script(source, lua, &paths, config).await
99 {
100 wrapped_binary(source, target, tree, lua, config).await?
101 } else {
102 let target = tree.bin().join(target);
103 fs::tokio::copy(source, &target).await?;
104 target
105 };
106
107 #[cfg(unix)]
108 fs::set_executable_permissions(&script).await?;
109
110 Ok(script)
111}
112
113#[tracing::instrument("Installing wrapped binary", level = "trace", skip(tree))]
114async fn wrapped_binary(
115 source: &Path,
116 target: &str,
117 tree: &impl InstallTree,
118 lua: &LuaInstallation,
119 config: &Config,
120) -> Result<PathBuf, Error> {
121 let unwrapped_bin_dir = tree.unwrapped_bin();
122 fs::tokio::create_dir_all(&unwrapped_bin_dir).await?;
123 let unwrapped_bin = unwrapped_bin_dir.join(target);
124 fs::tokio::copy(source, &unwrapped_bin).await?;
125 fs::make_writable(&unwrapped_bin).await?;
126
127 #[cfg(target_family = "unix")]
128 let target = tree.bin().join(target);
129 #[cfg(target_family = "windows")]
130 let target = tree.bin().join(format!("{}.bat", target));
131
132 let lua_bin = lua.lua_binary_or_config_override(config).ok_or({
133 let bin_name = config
134 .variables()
135 .get("LUA")
136 .cloned()
137 .or(lua
138 .bin()
139 .as_ref()
140 .map(|bin| bin.to_slash_lossy().to_string()))
141 .unwrap_or("lua".to_string());
142 Error::NoLuaBinary {
143 help: format!(
144 "make sure {bin_name} (version {}) is on the PATH or has been installed by Lux",
145 lua.version
146 ),
147 }
148 })?;
149
150 #[cfg(target_family = "unix")]
151 let content = format!(
152 r#"#!/bin/sh
153
154exec "{0}" "{1}" "$@"
155"#,
156 lua_bin,
157 unwrapped_bin.display(),
158 );
159 #[cfg(target_family = "windows")]
160 let content = format!(
161 r#"@echo off
162setlocal
163
164{0} "{1}" %*
165
166exit /b %ERRORLEVEL%
167"#,
168 lua_bin,
169 unwrapped_bin.display(),
170 );
171
172 fs::tokio::write(&target, content).await?;
173 Ok(target)
174}
175
176#[tracing::instrument(level = "trace")]
184async fn is_compatible_lua_script(
185 file: &Path,
186 lua: &LuaInstallation,
187 paths: &Paths,
188 config: &Config,
189) -> bool {
190 let lua_path = paths.package_path_prepended().joined();
191 let lua_cpath = paths.package_cpath_prepended().joined();
192 let path = paths.path_prepended().joined();
193 if let Some(lua_bin) = &lua.lua_binary_or_config_override(config) {
194 let lua_bin_path = PathBuf::from(lua_bin);
195 if lua_bin_path.is_file() || which(lua_bin).is_ok() {
196 Command::new(lua_bin)
197 .arg("-e")
198 .arg(format!(
199 "if loadfile('{}') then os.exit(0) else os.exit(1) end",
200 file.to_slash_lossy()
202 ))
203 .stderr(Stdio::null())
204 .stdout(Stdio::null())
205 .env("LUA_PATH", lua_path)
206 .env("LUA_CPATH", lua_cpath)
207 .env("PATH", path)
208 .status()
209 .await
210 .is_ok_and(|status| status.success())
211 } else {
212 is_compatible_lua_script_fallback(file, lua, config).await
213 }
214 } else {
215 false
216 }
217}
218
219#[tracing::instrument(level = "trace")]
224async fn is_compatible_lua_script_fallback(
225 file: &Path,
226 lua_installation: &LuaInstallation,
227 config: &Config,
228) -> bool {
229 if let Ok(file_content) = fs::tokio::read_to_string(&file).await {
230 let file_content_without_comments = file_content
231 .lines()
232 .filter(|line| !line.trim_start().starts_with("#"))
233 .collect_vec()
234 .join("\n");
235 lua_installation
236 .lua_binary_or_config_override(config)
237 .is_some_and(|_| {
238 ottavino::Lua::core()
240 .try_enter(|ctx| {
241 ottavino::Closure::load(
242 ctx,
243 None,
244 file_content_without_comments.as_bytes(),
245 )?;
246 Ok(())
247 })
248 .is_ok()
249 })
250 } else {
251 false
252 }
253}
254
255#[tracing::instrument(level = "trace")]
256pub(crate) fn detect_binaries(build_dir: &Path) -> Vec<PathBuf> {
257 WalkDir::new(build_dir.join("src").join("bin"))
258 .into_iter()
259 .chain(WalkDir::new(build_dir.join("bin")))
260 .filter_map(|file| file.ok())
261 .filter(|file| file.clone().into_path().is_file())
262 .map(DirEntry::into_path)
263 .collect()
264}
265
266#[cfg(test)]
267mod tests {
268 use tokio::process::Command;
269
270 use crate::{
271 config::ConfigBuilder, lua_installation::detect_installed_lua_version,
272 lua_version::LuaVersion, tree::Tree,
273 };
274
275 use super::*;
276
277 #[tokio::test]
278 async fn test_is_compatible_lua_script() {
279 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
280 let config = ConfigBuilder::new()
281 .unwrap()
282 .lua_version(lua_version)
283 .build()
284 .unwrap();
285 let lua_version = config.lua_version().unwrap();
286 let lua = LuaInstallation::new(lua_version, &config).await.unwrap();
287 let valid_script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
288 .join("resources/test/sample_lua_bin_script_valid");
289 let temp_dir = assert_fs::TempDir::new().unwrap().to_path_buf();
290 let tree = Tree::new(temp_dir, lua_version.clone(), &config).unwrap();
291 let paths = Paths::new(&tree).unwrap();
292 assert!(is_compatible_lua_script(&valid_script, &lua, &paths, &config).await);
293 let invalid_script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
294 .join("resources/test/sample_lua_bin_script_invalid");
295 assert!(!is_compatible_lua_script(&invalid_script, &lua, &paths, &config).await);
296 }
297
298 #[tokio::test]
299 async fn test_wrapped_binary() {
300 let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
301 let temp = assert_fs::TempDir::new().unwrap();
302 let config = ConfigBuilder::new()
303 .unwrap()
304 .lua_version(lua_version)
305 .user_tree(Some(temp.to_path_buf()))
306 .build()
307 .unwrap();
308 let lua_version = config.lua_version().unwrap();
309 let lua = LuaInstallation::new(lua_version, &config).await.unwrap();
310 let tree = config.user_tree(lua_version.clone()).unwrap();
311 let valid_script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
312 .join("resources/test/sample_lua_bin_script_valid");
313 let script_name = "test_script";
314 let script_path = wrapped_binary(&valid_script, script_name, &tree, &lua, &config)
315 .await
316 .unwrap();
317
318 #[cfg(unix)]
319 fs::set_executable_permissions(&script_path).await.unwrap();
320
321 assert!(Command::new(script_path.to_string_lossy().to_string())
322 .status()
323 .await
324 .is_ok_and(|status| status.success()));
325 }
326}