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