1use std::env;
2use std::error::Error;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, PartialEq, Eq)]
8pub enum Version {
9 Lua51,
10 Lua52,
11 Lua53,
12 Lua54,
13 Lua55,
14}
15pub use self::Version::*;
16
17pub struct Build {
19 out_dir: Option<PathBuf>,
20 target: Option<String>,
21 host: Option<String>,
22 opt_level: Option<String>,
23 debug: Option<bool>,
24}
25
26#[derive(Clone, Debug)]
28pub struct Artifacts {
29 include_dir: PathBuf,
30 lib_dir: PathBuf,
31 libs: Vec<String>,
32}
33
34impl Default for Build {
35 fn default() -> Build {
36 Build {
37 out_dir: env::var_os("OUT_DIR").map(PathBuf::from),
38 target: env::var("TARGET").ok(),
39 host: None,
40 opt_level: None,
41 debug: None,
42 }
43 }
44}
45
46impl Build {
47 pub fn new() -> Build {
49 Build::default()
50 }
51
52 pub fn out_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Build {
56 self.out_dir = Some(path.as_ref().to_path_buf());
57 self
58 }
59
60 pub fn target(&mut self, target: &str) -> &mut Build {
64 self.target = Some(target.to_string());
65 self
66 }
67
68 pub fn host(&mut self, host: &str) -> &mut Build {
73 self.host = Some(host.to_string());
74 self
75 }
76
77 pub fn opt_level(&mut self, opt_level: &str) -> &mut Build {
82 self.opt_level = Some(opt_level.to_string());
83 self
84 }
85
86 pub fn debug(&mut self, debug: bool) -> &mut Build {
91 self.debug = Some(debug);
92 self
93 }
94
95 pub fn build(&self, version: Version) -> Artifacts {
97 match self.try_build(version) {
98 Ok(artifacts) => artifacts,
99 Err(err) => panic!("{err}"),
100 }
101 }
102
103 pub fn try_build(&self, version: Version) -> Result<Artifacts, Box<dyn Error>> {
107 let target = self.target.as_ref().ok_or("TARGET is not set")?;
108 let out_dir = self.out_dir.as_ref().ok_or("OUT_DIR is not set")?;
109 let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
110 let mut source_dir = manifest_dir.join(version.source_dir());
111 let lib_dir = out_dir.join("lib");
112 let include_dir = out_dir.join("include");
113
114 if !include_dir.exists() {
115 fs::create_dir_all(&include_dir)
116 .context(|| format!("Cannot create '{}'", include_dir.display()))?;
117 }
118
119 let mut config = cc::Build::new();
120 config.warnings(false).cargo_metadata(false).target(target);
121
122 match &self.host {
123 Some(host) => {
124 config.host(host);
125 }
126 None if env::var("HOST").is_ok() => {}
128 None => {
129 config.host(target);
131 }
132 }
133
134 let mut libs = vec![version.lib_name().to_string()];
135 match target {
136 _ if target.contains("linux") => {
137 config.define("LUA_USE_LINUX", None);
138 }
139 _ if target.ends_with("bsd") => {
140 config.define("LUA_USE_LINUX", None);
141 }
142 _ if target.contains("apple-darwin") => {
143 match version {
144 Lua51 => config.define("LUA_USE_LINUX", None),
145 _ => config.define("LUA_USE_MACOSX", None),
146 };
147 }
148 _ if target.contains("apple-ios") => {
149 match version {
150 Lua54 | Lua55 => config.define("LUA_USE_IOS", None),
151 _ => config.define("LUA_USE_POSIX", None),
152 };
153 }
154 _ if target.contains("windows") => {
155 config.define("LUA_USE_WINDOWS", None);
157 }
158 _ if target.ends_with("emscripten") => {
159 config
160 .define("LUA_USE_POSIX", None)
161 .cpp(true)
162 .flag("-fexceptions"); let cpp_source_dir = out_dir.join("cpp_source");
165 if cpp_source_dir.exists() {
166 fs::remove_dir_all(&cpp_source_dir)
167 .context(|| format!("Cannot remove '{}'", cpp_source_dir.display()))?;
168 }
169 fs::create_dir_all(&cpp_source_dir)
170 .context(|| format!("Cannot create '{}'", cpp_source_dir.display()))?;
171
172 for file in fs::read_dir(&source_dir)
173 .context(|| format!("Cannot read '{}'", source_dir.display()))?
174 {
175 let file = file?;
176 let filename = file.file_name();
177 let filename = &*filename.to_string_lossy();
178 let src_file = source_dir.join(file.file_name());
179 let dst_file = cpp_source_dir.join(file.file_name());
180
181 let mut content = fs::read(&src_file)
182 .context(|| format!("Cannot read '{}'", src_file.display()))?;
183 if ["lauxlib.h", "lua.h", "lualib.h"].contains(&filename) {
184 content.splice(0..0, b"extern \"C\" {\n".to_vec());
185 content.extend(b"\n}".to_vec())
186 }
187 fs::write(&dst_file, content)
188 .context(|| format!("Cannot write to '{}'", dst_file.display()))?;
189 }
190 source_dir = cpp_source_dir
191 }
192 _ if target.contains("wasi") => {
193 config.define("LUA_USE_POSIX", None);
196
197 config.define("_WASI_EMULATED_SIGNAL", None);
201 libs.push("wasi-emulated-signal".to_string());
202
203 config.flag("-mllvm").flag("-wasm-enable-eh");
206 config.flag("-mllvm").flag("-wasm-use-legacy-eh=false");
207 config.flag("-mllvm").flag("-wasm-enable-sjlj");
208 libs.push("setjmp".to_string());
209 }
210 _ => Err(format!("don't know how to build Lua for {target}"))?,
211 }
212
213 if let Lua54 = version {
214 config.define("LUA_COMPAT_5_3", None);
215 }
216
217 #[cfg(feature = "ucid")]
218 if let Lua54 | Lua55 = version {
219 config.define("LUA_UCID", None);
220 }
221
222 let debug = self.debug.unwrap_or(cfg!(debug_assertions));
223 if debug {
224 config.define("LUA_USE_APICHECK", None);
225 config.debug(true);
226 }
227
228 match &self.opt_level {
229 Some(opt_level) => {
230 config.opt_level_str(opt_level);
231 }
232 None if env::var("OPT_LEVEL").is_ok() => {}
234 None => {
235 config.opt_level(if debug { 0 } else { 2 });
237 }
238 }
239
240 config
241 .include(&source_dir)
242 .warnings(false) .flag_if_supported("-fno-common") .add_files_by_ext(&source_dir, "c")?
245 .out_dir(&lib_dir)
246 .try_compile(version.lib_name())?;
247
248 for f in &["lauxlib.h", "lua.h", "luaconf.h", "lualib.h"] {
249 let from = source_dir.join(f);
250 let to = include_dir.join(f);
251 fs::copy(&from, &to)
252 .context(|| format!("Cannot copy '{}' to '{}'", from.display(), to.display()))?;
253 }
254
255 Ok(Artifacts {
256 include_dir,
257 lib_dir,
258 libs,
259 })
260 }
261}
262
263impl Version {
264 fn source_dir(&self) -> &'static str {
265 match self {
266 Lua51 => "lua-5.1.5",
267 Lua52 => "lua-5.2.4",
268 Lua53 => "lua-5.3.6",
269 Lua54 => "lua-5.4.8",
270 Lua55 => "lua-5.5.0",
271 }
272 }
273
274 fn lib_name(&self) -> &'static str {
275 match self {
276 Lua51 => "lua5.1",
277 Lua52 => "lua5.2",
278 Lua53 => "lua5.3",
279 Lua54 => "lua5.4",
280 Lua55 => "lua5.5",
281 }
282 }
283}
284
285impl Artifacts {
286 pub fn include_dir(&self) -> &Path {
288 &self.include_dir
289 }
290
291 pub fn lib_dir(&self) -> &Path {
293 &self.lib_dir
294 }
295
296 pub fn libs(&self) -> &[String] {
298 &self.libs
299 }
300
301 pub fn print_cargo_metadata(&self) {
306 println!("cargo:rustc-link-search=native={}", self.lib_dir.display());
307 for lib in self.libs.iter() {
308 println!("cargo:rustc-link-lib=static:-bundle={lib}");
309 }
310 }
311}
312
313trait ErrorContext<T> {
314 fn context(self, f: impl FnOnce() -> String) -> Result<T, Box<dyn Error>>;
315}
316
317impl<T, E: Error> ErrorContext<T> for Result<T, E> {
318 fn context(self, f: impl FnOnce() -> String) -> Result<T, Box<dyn Error>> {
319 self.map_err(|e| format!("{}: {e}", f()).into())
320 }
321}
322
323trait AddFilesByExt {
324 fn add_files_by_ext(&mut self, dir: &Path, ext: &str) -> Result<&mut Self, Box<dyn Error>>;
325}
326
327impl AddFilesByExt for cc::Build {
328 fn add_files_by_ext(&mut self, dir: &Path, ext: &str) -> Result<&mut Self, Box<dyn Error>> {
329 for entry in fs::read_dir(dir)
330 .context(|| format!("Cannot read '{}'", dir.display()))?
331 .filter_map(|e| e.ok())
332 .filter(|e| e.path().extension() == Some(ext.as_ref()))
333 {
334 self.file(entry.path());
335 }
336 Ok(self)
337 }
338}