1#![cfg_attr(feature = "documentation", feature(get_mut_unchecked))]
2pub extern crate bindgen;
3
4use std::env;
5use std::path::{Path, PathBuf};
6use bindgen::callbacks::DeriveInfo;
7use bindgen::{EnumVariation, RustTarget, Builder, MacroTypeVariation};
8use utils::consts::*;
9use utils::toolchain::gcc::{ArmToolchain, Gcc};
10use utils::toolchain::sdk::Sdk;
11pub use bindgen_cfg as cfg;
12
13
14pub mod error;
15pub mod gen;
16
17
18type Result<T, E = error::Error> = std::result::Result<T, E>;
19
20
21pub const SDK_VER_SUPPORTED: &str = ">=2.1.0, <3.0.0";
22
23
24pub enum Bindings {
26 Bindgen(bindgen::Bindings),
27 #[cfg(feature = "extra-codegen")]
28 Engaged(gen::Bindings),
29}
30
31
32impl Bindings {
33 #[inline(always)]
34 pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
36 match self {
37 Bindings::Bindgen(this) => this.write_to_file(path),
38 #[cfg(feature = "extra-codegen")]
39 Bindings::Engaged(this) => this.write_to_file(path),
40 }
41 }
42
43 #[inline(always)]
44 pub fn write<'a>(&self, writer: Box<dyn std::io::Write + 'a>) -> std::io::Result<()> {
46 match self {
47 Bindings::Bindgen(this) => this.write(writer),
48 #[cfg(feature = "extra-codegen")]
49 Bindings::Engaged(this) => this.write(writer),
50 }
51 }
52}
53
54impl std::fmt::Display for Bindings {
55 #[inline(always)]
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 Bindings::Bindgen(this) => std::fmt::Display::fmt(this, f),
59 #[cfg(feature = "extra-codegen")]
60 Bindings::Engaged(this) => std::fmt::Display::fmt(this, f),
61 }
62 }
63}
64
65
66pub struct Generator {
67 pub sdk: Sdk,
69 pub version: semver::Version,
71
72 pub gcc: ArmToolchain,
74
75 pub filename: cfg::Filename,
77 pub builder: Builder,
79
80 pub derives: cfg::Derive,
82 pub features: cfg::Features,
83}
84
85
86impl Generator {
87 pub fn new(cfg: cfg::Cfg) -> Result<Self> { create_generator(cfg) }
88
89 pub fn generate(mut self) -> Result<Bindings> {
90 if cfg!(feature = "extra-codegen") {
92 self.builder = self.builder.formatter(bindgen::Formatter::None);
93 }
94
95 let bindings = self.builder.generate()?;
97
98 #[cfg(not(feature = "extra-codegen"))]
99 return Ok(Bindings::Bindgen(bindings));
100
101 #[cfg(feature = "extra-codegen")]
102 gen::engage(&bindings, &self.features, &self.sdk, None).map(Bindings::Engaged)
103 }
104}
105
106
107fn create_generator(cfg: cfg::Cfg) -> Result<Generator, error::Error> {
108 println!("cargo::rerun-if-env-changed=TARGET");
109 let cargo_target_triple = env::var("TARGET").expect("TARGET cargo env var");
110
111 println!("cargo::rerun-if-env-changed=PROFILE");
112 let cargo_profile = env::var("PROFILE").expect("PROFILE cargo env var");
113 let is_debug = cargo_profile == "debug" || env_cargo_feature("DEBUG");
114
115 let sdk = cfg.sdk
116 .map(|p| Sdk::try_new_exact(p).or_else(|_| Sdk::try_new()))
117 .unwrap_or_else(Sdk::try_new)?;
118 let version_path = sdk.version_file();
119 let version_raw = sdk.read_version()?;
120 let version = check_sdk_version(&version_raw)?;
121 println!("cargo::rerun-if-changed={}", version_path.display());
122 let sdk_c_api = sdk.c_api();
123
124 let main_header = sdk_c_api.join("pd_api.h");
125 println!("cargo::rerun-if-changed={}", main_header.display());
126 println!("cargo::rerun-if-env-changed={SDK_ENV_VAR}");
127 println!("cargo::metadata=include={}", sdk_c_api.display());
128
129
130 let gcc = cfg.gcc
132 .map(|p| {
133 Gcc::try_from_path(p).and_then(ArmToolchain::try_new_with)
134 .or_else(|_| ArmToolchain::try_new())
135 })
136 .unwrap_or_else(ArmToolchain::try_new)?;
137 let mut builder = create_builder(&cargo_target_triple, &sdk_c_api, &main_header, &cfg.derive);
138 builder = apply_profile(builder, is_debug);
139 builder = apply_target(builder, &cargo_target_triple, &gcc);
140
141
142 let filename = cfg::Filename::new(version.to_owned(), cfg.derive)?;
143
144 Ok(Generator { sdk,
145 gcc,
146 version,
147 filename,
148 builder,
149 derives: cfg.derive,
150 features: cfg.features })
151}
152
153
154fn check_sdk_version(version: &str) -> Result<semver::Version, error::Error> {
155 is_version_matches(version)
156 .map(|(ver, res, req)| {
157 if res {
158 const PKG: &str = env!("CARGO_PKG_NAME");
159 const VER: &str = env!("CARGO_PKG_VERSION");
160 println!("cargo:warning=Playdate SDK v{ver} may not be compatible with {PKG} v{VER} which hasn't been tested with it. Supported '{req}' does not matches current '{ver}'.")
161 }
162 ver
163 })
164}
165
166fn is_version_matches(version: &str) -> Result<(semver::Version, bool, semver::VersionReq), error::Error> {
167 let requirement =
168 semver::VersionReq::parse(SDK_VER_SUPPORTED).expect("Builtin supported version requirement is invalid.");
169 let version = semver::Version::parse(version.trim())?;
170 let matches = requirement.matches(&version);
171 Ok((version, matches, requirement))
172}
173
174
175pub fn env_var(name: &'static str) -> Result<String> {
176 env::var(name).map_err(|err| error::Error::Env { err, ctx: name })
177}
178
179pub fn env_cargo_feature(feature: &str) -> bool { env::var(format!("CARGO_FEATURE_{feature}")).is_ok() }
180
181
182fn create_builder(_target: &str, capi: &Path, header: &Path, derive: &cfg::Derive) -> Builder {
183 let mut builder = bindgen::builder()
184 .header(format!("{}", header.display()))
185 .rust_target(RustTarget::nightly())
186
187 .allowlist_recursively(true)
189 .allowlist_type("PlaydateAPI")
190 .allowlist_type("PDSystemEvent")
191 .allowlist_type("LCDSolidColor")
192 .allowlist_type("LCDColor")
193 .allowlist_type("LCDPattern")
194 .allowlist_type("PDEventHandler")
195
196 .allowlist_var("LCD_COLUMNS")
197 .allowlist_var("LCD_ROWS")
198 .allowlist_var("LCD_ROWSIZE")
199 .allowlist_var("LCD_SCREEN_RECT")
200 .allowlist_var("SEEK_SET")
201 .allowlist_var("SEEK_CUR")
202 .allowlist_var("SEEK_END")
203 .allowlist_var("AUDIO_FRAMES_PER_CYCLE")
204 .allowlist_var("NOTE_C4")
205
206 .default_macro_constant_type(MacroTypeVariation::Unsigned)
208 .allowlist_var("LCDMakePattern")
209 .allowlist_type("LCDMakePattern")
210 .allowlist_var("LCDOpaquePattern")
211 .allowlist_type("LCDOpaquePattern")
212
213 .bitfield_enum("FileOptions")
214 .bitfield_enum("PDButtons")
215
216 .use_core()
218 .ctypes_prefix("core::ffi")
219 .size_t_is_usize(true)
220 .no_convert_floats()
221 .translate_enum_integer_types(true)
222 .array_pointers_in_arguments(true)
223 .explicit_padding(false)
224
225 .default_enum_style(EnumVariation::Rust { non_exhaustive: false })
226
227 .layout_tests(true)
228 .enable_function_attribute_detection()
229 .detect_include_paths(true)
230
231 .clang_args(&["--include-directory", &capi.display().to_string()])
232 .clang_arg("-DTARGET_EXTENSION=1")
233
234 .dynamic_link_require_all(true)
235
236 .derive_default(derive.default)
238 .derive_eq(derive.eq)
239 .derive_copy(derive.copy)
240 .derive_debug(derive.debug)
241 .derive_hash(derive.hash)
242 .derive_ord(derive.ord)
243 .derive_partialeq(derive.partialeq)
244 .derive_partialord(derive.partialord)
245
246 .must_use_type("playdate_*")
247 .must_use_type(".*")
248 .generate_comments(true);
249
250
251 builder = builder.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
252 if !derive.copy {
253 builder = builder.parse_callbacks(Box::new(DeriveCopyToPrimitives));
254 }
255 if derive.constparamty {
256 builder = builder.parse_callbacks(Box::new(DeriveConstParamTy));
257 }
258
259
260 if !derive.default {
262 builder = builder.no_default(".*");
263 }
264 if !derive.copy {
265 builder = builder.no_copy(".*");
266 }
267 if !derive.debug {
268 builder = builder.no_debug(".*");
269 }
270 if !derive.hash {
271 builder = builder.no_hash(".*");
272 }
273 if !derive.partialeq {
274 builder = builder.no_partialeq(".*");
275 }
276
277 builder
278}
279
280
281fn apply_profile(mut builder: Builder, debug: bool) -> Builder {
282 if debug {
284 builder = builder.clang_arg("-D_DEBUG=1").derive_debug(true);
285 } else {
286 }
289 builder
290}
291
292
293fn apply_target(mut builder: Builder, target: &str, gcc: &ArmToolchain) -> Builder {
296 builder = if DEVICE_TARGET == target {
297 let arm_eabi_include = gcc.include();
298 println!("cargo::metadata=include={}", arm_eabi_include.display());
300
301 builder.clang_arg("-DTARGET_PLAYDATE=1")
305 .blocklist_file("stdlib.h")
306 .clang_args(&["-target", DEVICE_TARGET])
307 .clang_arg("-fshort-enums")
308 .clang_args(&["--include-directory", &arm_eabi_include.display().to_string()])
309 .clang_arg(format!("-I{}", arm_eabi_include.display()))
310 } else {
311 builder.clang_arg("-DTARGET_SIMULATOR=1")
312 };
313 builder
314}
315
316
317#[derive(Debug)]
319struct DeriveCopyToPrimitives;
320impl bindgen::callbacks::ParseCallbacks for DeriveCopyToPrimitives {
321 fn add_derives(&self, info: &DeriveInfo<'_>) -> Vec<String> {
322 const TYPES: &[&str] = &[
323 "PDButtons",
324 "FileOptions",
325 "LCDBitmapDrawMode",
326 "LCDBitmapFlip",
327 "LCDSolidColor",
328 "LCDLineCapStyle",
329 "PDStringEncoding",
330 "LCDPolygonFillRule",
331 "PDLanguage",
332 "PDPeripherals",
333 "l_valtype",
334 "LuaType",
335 "json_value_type",
336 "SpriteCollisionResponseType",
337 "SoundFormat",
338 "LFOType",
339 "SoundWaveform",
340 "TwoPoleFilterType",
341 "PDSystemEvent",
342 ];
343
344 if TYPES.contains(&info.name) {
345 vec!["Copy".to_string()]
346 } else {
347 vec![]
348 }
349 }
350}
351
352
353#[derive(Debug)]
354struct DeriveConstParamTy;
356
357impl bindgen::callbacks::ParseCallbacks for DeriveConstParamTy {
358 fn add_derives(&self, info: &DeriveInfo<'_>) -> Vec<String> {
359 const TYPES: &[&str] = &[
360 "PDButtons",
361 "FileOptions",
362 "LCDBitmapDrawMode",
363 "LCDBitmapFlip",
364 "LCDSolidColor",
365 "LCDLineCapStyle",
366 "PDStringEncoding",
367 "LCDPolygonFillRule",
368 "PDLanguage",
369 "PDPeripherals",
370 "l_valtype",
371 "LuaType",
372 "json_value_type",
373 "SpriteCollisionResponseType",
374 "SoundFormat",
375 "LFOType",
376 "SoundWaveform",
377 "TwoPoleFilterType",
378 "PDSystemEvent",
379 ];
380
381 if TYPES.contains(&info.name) {
382 vec!["::core::marker::ConstParamTy".to_string()]
383 } else {
384 vec![]
385 }
386 }
387}
388
389
390pub fn rustfmt<'out>(mut rustfmt_path: Option<PathBuf>,
391 source: String,
392 config_path: Option<&Path>)
393 -> std::io::Result<String> {
394 use std::io::Write;
395 use std::process::{Command, Stdio};
396
397 rustfmt_path = rustfmt_path.or_else(|| std::env::var("RUSTFMT").map(PathBuf::from).ok());
398 #[cfg(feature = "which-rustfmt")]
399 {
400 rustfmt_path = rustfmt_path.or_else(|| which::which("rustfmt").ok());
401 }
402 let rustfmt = rustfmt_path.as_deref().unwrap_or(Path::new("rustfmt"));
403
404
405 let mut cmd = Command::new(rustfmt);
406
407 cmd.stdin(Stdio::piped()).stdout(Stdio::piped());
408
409 if let Some(path) = config_path {
410 cmd.arg("--config-path");
411 cmd.arg(path);
412 }
413
414 let mut child = cmd.spawn()?;
415 let mut child_stdin = child.stdin.take().unwrap();
416 let mut child_stdout = child.stdout.take().unwrap();
417
418 let stdin_handle = std::thread::spawn(move || {
422 let _ = child_stdin.write_all(source.as_bytes());
423 source
424 });
425
426 let mut output = vec![];
427 std::io::copy(&mut child_stdout, &mut output)?;
428
429 let status = child.wait()?;
430 let source = stdin_handle.join()
431 .expect("The thread writing to rustfmt's stdin doesn't do anything that could panic");
432
433 match String::from_utf8(output) {
434 Ok(bindings) => {
435 match status.code() {
436 Some(0) => Ok(bindings),
437 Some(2) => Err(std::io::Error::new(std::io::ErrorKind::Other, "Rustfmt parsing errors.".to_string())),
438 Some(3) => {
439 println!("cargo:warning=Rustfmt could not format some lines.");
440 Ok(bindings)
441 },
442 _ => Err(std::io::Error::new(std::io::ErrorKind::Other, "Internal rustfmt error".to_string())),
443 }
444 },
445 _ => Ok(source),
446 }
447}
448
449
450#[cfg(test)]
451mod tests {
452 #[test]
453 fn same_env_var() {
454 assert_eq!(utils::consts::SDK_ENV_VAR, bindgen_cfg::Cfg::ENV_SDK_PATH);
455 }
456
457 #[test]
458 fn version_matches() {
459 use super::is_version_matches as check;
460
461 let map = |(_, res, _)| res;
462
463 assert!(check("0.0").map(map).is_err());
464 assert!(!check("0.0.0").map(map).unwrap());
465 assert!(check("2.1.0").map(map).unwrap());
466 assert!(check("2.7.0").map(map).unwrap());
467 assert!(!check("2.7.0-beta.3").map(map).unwrap());
468 assert!(!check("3.1.0").map(map).unwrap());
469 }
470}