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