1use itertools::Itertools;
2use miette::Diagnostic;
3use std::{
4 collections::{HashMap, HashSet},
5 path::{Path, PathBuf},
6 str::FromStr,
7};
8use thiserror::Error;
9use walkdir::WalkDir;
10
11use crate::{
12 build::{
13 backend::{BuildBackend, BuildInfo, RunBuildArgs},
14 compile::{c_files, c_modules},
15 },
16 fs,
17 lua_rockspec::{BuiltinBuildSpec, LuaModule, ModuleSpec, ParseLuaModuleError},
18 tree::{self, InstallTree},
19};
20
21#[cfg(not(feature = "luau-transpile"))]
22use crate::lua_version::LuaVersion;
23
24use super::{compile, install};
25
26#[derive(Error, Debug, Diagnostic)]
27#[non_exhaustive]
28pub enum Error {
29 #[error(transparent)]
30 #[diagnostic(transparent)]
31 CompileCFiles(#[from] compile::c_files::Error),
32 #[error(transparent)]
33 #[diagnostic(transparent)]
34 CompileCModules(#[from] compile::c_modules::Error),
35 #[error("failed to install binary '{file_name}'")]
36 InstallBinary {
37 file_name: String,
38 #[diagnostic_source]
39 source: install::Error,
40 },
41 #[error(transparent)]
42 #[diagnostic(transparent)]
43 Fs(#[from] fs::Error),
44 #[error(transparent)]
45 #[diagnostic(transparent)]
46 Tree(#[from] tree::Error),
47 #[error("module auto-detection failed")]
48 #[diagnostic(transparent)]
49 AutoDetectModules(#[from] AutoDetectModulesError),
50 #[error("luau does not support native dependencies")]
51 #[diagnostic(help(
52 "the luau runtime cannot load native libraries. use pure-Luau packages or Lua"
53 ))]
54 LuauNativeDepsUnsupported,
55 #[cfg(feature = "luau-transpile")]
56 #[error(transparent)]
57 #[diagnostic(transparent)]
58 LuauTranspile(#[from] super::luau::Error),
59 #[cfg(not(feature = "luau-transpile"))]
60 #[error("cannot install Luau module `{module}` on {lua_version}")]
61 #[diagnostic(help(
62 "this build of Lux was compiled without support for transpiling Luau to Lua"
63 ))]
64 LuauTranspileDisabled {
65 module: String,
66 lua_version: LuaVersion,
67 },
68}
69
70impl BuildBackend for BuiltinBuildSpec {
71 type Err = Error;
72
73 #[tracing::instrument(name = "builtin::run", skip_all, level = "debug")]
74 async fn run<T>(self, args: RunBuildArgs<'_, T>) -> Result<BuildInfo, Self::Err>
75 where
76 T: InstallTree + Sync,
77 {
78 let package = args.package;
79 let lua = args.lua;
80 let external_dependencies = args.external_dependencies;
81 let config = args.config;
82 let tree = args.tree;
83 let layout = tree.layout_for(package);
84 let build_dir = args.build_dir;
85
86 let modules = autodetect_modules(build_dir, source_paths(build_dir, &self.modules))?
88 .into_iter()
89 .chain(self.modules)
90 .collect::<HashMap<_, _>>();
91
92 if lua.version.is_luau() {
93 let has_native_modules = modules.values().any(|spec| match spec {
94 ModuleSpec::SourcePath(source) => source.extension().is_some_and(|ext| ext == "c"),
95 ModuleSpec::SourcePaths(_) | ModuleSpec::ModulePaths(_) => true,
96 });
97 if has_native_modules {
98 return Err(Error::LuauNativeDepsUnsupported);
99 }
100 }
101
102 for (destination_path, module_type) in modules.iter() {
103 match module_type {
104 ModuleSpec::SourcePath(source) => {
105 if source.extension().map(|ext| ext == "c").unwrap_or(false) {
106 let absolute_source_paths = vec![build_dir.join(source)];
107 c_files::compile(
108 &absolute_source_paths,
109 destination_path,
110 &layout.lib,
111 lua,
112 external_dependencies,
113 config,
114 )
115 .await?;
116 } else if source.extension().is_some_and(|ext| ext == "luau")
117 && !lua.version.is_luau()
118 {
119 #[cfg(feature = "luau-transpile")]
120 {
121 let absolute_source_path = build_dir.join(source);
122 super::luau::transpile_luau_to_lua_module(
123 &absolute_source_path,
124 destination_path,
125 &layout.src,
126 )
127 .await?;
128 }
129 #[cfg(not(feature = "luau-transpile"))]
130 return Err(Error::LuauTranspileDisabled {
131 module: destination_path.to_string(),
132 lua_version: lua.version.clone(),
133 });
134 } else {
135 let absolute_source_path = build_dir.join(source);
136 install::lua_module(&absolute_source_path, destination_path, &layout.src)?;
137 }
138 }
139 ModuleSpec::SourcePaths(files) => {
140 let absolute_source_paths =
141 files.iter().map(|file| build_dir.join(file)).collect();
142 c_files::compile(
143 &absolute_source_paths,
144 destination_path,
145 &layout.lib,
146 lua,
147 external_dependencies,
148 config,
149 )
150 .await?
151 }
152 ModuleSpec::ModulePaths(data) => {
153 c_modules::compile(
154 data,
155 build_dir,
156 destination_path,
157 &layout.lib,
158 lua,
159 external_dependencies,
160 config,
161 )
162 .await?
163 }
164 }
165 }
166
167 let mut binaries = Vec::new();
168 for bin_script in install::detect_binaries(build_dir) {
169 if let Some(target) = bin_script.file_name() {
170 let file_name = target.to_string_lossy().to_string();
171 let installed_bin_script =
172 install::binary(&bin_script, &file_name, tree, lua, args.deploy, config)
173 .await
174 .map_err(|err| Error::InstallBinary {
175 file_name: file_name.clone(),
176 source: err,
177 })?;
178 if let Some(bin_script_file_name) = installed_bin_script.file_name() {
179 binaries.push(bin_script_file_name.into());
180 }
181 }
182 }
183
184 Ok(BuildInfo { binaries })
185 }
186}
187
188fn source_paths(build_dir: &Path, modules: &HashMap<LuaModule, ModuleSpec>) -> HashSet<PathBuf> {
189 modules
190 .values()
191 .flat_map(|spec| match spec {
192 ModuleSpec::SourcePath(path_buf) => vec![path_buf],
193 ModuleSpec::SourcePaths(vec) => vec.iter().collect_vec(),
194 ModuleSpec::ModulePaths(module_paths) => module_paths.sources.iter().collect_vec(),
195 })
196 .map(|path| build_dir.join(path))
197 .collect()
198}
199
200#[derive(Error, Debug, Diagnostic)]
201#[non_exhaustive]
202pub enum AutoDetectModulesError {
203 #[error(transparent)]
204 #[diagnostic(transparent)]
205 ParseLuaModule(#[from] ParseLuaModuleError),
206}
207
208#[tracing::instrument(level = "trace")]
209pub(crate) fn autodetect_modules(
210 build_dir: &Path,
211 exclude: HashSet<PathBuf>,
212) -> Result<HashMap<LuaModule, ModuleSpec>, AutoDetectModulesError> {
213 WalkDir::new(build_dir.join("src"))
214 .into_iter()
215 .chain(WalkDir::new(build_dir.join("lua")))
216 .chain(WalkDir::new(build_dir.join("lib")))
217 .filter_map(|file| {
218 file.ok().and_then(|file| {
219 let is_lua_file = PathBuf::from(file.file_name())
220 .extension()
221 .is_some_and(|ext| ext == "lua" || ext == "luau");
222 if is_lua_file && !exclude.contains(&file.clone().into_path()) {
223 Some(file)
224 } else {
225 None
226 }
227 })
228 })
229 .map(|file| {
230 let diff: PathBuf = unsafe {
231 pathdiff::diff_paths(build_dir.join(file.clone().into_path()), build_dir)
232 .unwrap_unchecked()
233 };
234
235 let mut pathbuf = diff.components().skip(1).collect::<PathBuf>();
242 let lua_module = if pathbuf
243 .parent()
244 .is_none_or(|parent| parent.as_os_str().is_empty())
245 {
246 pathbuf.set_extension("");
247 Ok(LuaModule::from_pathbuf(pathbuf)?)
248 } else {
249 let mut lua_module = LuaModule::from_pathbuf(pathbuf)?;
250 if matches!(
253 file.file_name().to_string_lossy().as_bytes(),
254 b"init.lua" | b"init.luau"
255 ) {
256 unsafe {
257 lua_module =
258 lua_module.join(&LuaModule::from_str("init").unwrap_unchecked())
259 }
260 }
261 Ok(lua_module)
262 };
263 lua_module.map(|lua_module| (lua_module, ModuleSpec::SourcePath(diff)))
264 })
265 .try_collect()
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 fn write_file(path: &Path) {
273 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
274 std::fs::write(path, b"return {}").unwrap();
275 }
276
277 #[tokio::test]
278 async fn test_auto_detect_modules() {
279 let build_dir = tempfile::tempdir().unwrap();
280 let root = build_dir.path();
281 write_file(&root.join("src").join("foo.luau"));
282 write_file(&root.join("src").join("bar").join("baz.luau"));
283 write_file(&root.join("src").join("baz").join("init.luau"));
284 write_file(&root.join("lua").join("legacy.lua"));
285
286 let modules = autodetect_modules(root, Default::default()).unwrap();
287
288 let names = modules.keys().map(|m| m.to_string()).collect_vec();
289 assert!(names.contains(&"foo".to_string()), "{names:?}");
290 assert!(names.contains(&"bar.baz".to_string()), "{names:?}");
291 assert!(names.contains(&"baz.init".to_string()), "{names:?}");
292 assert!(names.contains(&"legacy".to_string()), "{names:?}");
293
294 let foo_spec = &modules[&"foo".parse().unwrap()];
295 match foo_spec {
296 ModuleSpec::SourcePath(path) => {
297 assert_eq!(path.extension().unwrap(), "luau");
298 }
299 _ => panic!("expected SourcePath for foo.luau"),
300 }
301 }
302}