Skip to main content

playdate_bindgen_cfg/
lib.rs

1#![feature(slice_split_once)]
2
3use core::str;
4use std::borrow::Cow;
5use std::convert::Infallible;
6use std::env::VarError;
7use std::ffi::OsString;
8use std::io::stderr;
9use std::path::Path;
10use std::path::PathBuf;
11use std::process::Command;
12use std::process::Stdio;
13use std::str::FromStr;
14
15
16/// Executable name of the `playdate-bindgen`.
17pub const BIN_NAME: &str = "pdbindgen";
18pub const FIND_SDK_VERSION_CMD: &str = "find-sdk-version";
19
20
21mod mask;
22pub use mask::DerivesMask;
23
24
25/// Playdate-bindgen executable path.
26#[derive(Clone, Debug)]
27#[cfg_attr(feature = "clap", derive(clap::Parser))]
28pub struct Bin {
29	/// Path to the playdate-bindgen (pdbindgen) executable.
30	#[cfg_attr(feature = "clap", arg(skip))]
31	pub path: PathBuf,
32}
33
34
35/// Playdate-bindgen configuration.
36#[derive(Clone, Debug)]
37#[cfg_attr(feature = "clap", derive(clap::Parser))]
38#[cfg_attr(feature = "clap", command(author, version, about, name = BIN_NAME, verbatim_doc_comment))]
39pub struct Cfg {
40	/// Path to the playdate-bindgen (pdbindgen) executable.
41	#[cfg_attr(feature = "clap", clap(flatten))]
42	pub bin: Bin,
43
44	/// Path to the Playdate SDK.
45	#[cfg_attr(feature = "clap", arg(long, value_name = "SDK", env = Cfg::ENV_SDK_PATH))]
46	pub sdk: Option<PathBuf>,
47
48	/// Path to gnu-arm-gcc executable, usually 'arm-none-eabi-gcc' or 'gcc-arm-none-eabi'.
49	#[cfg_attr(feature = "clap", arg(long, value_name = "GCC", env = Cfg::ENV_ARM_GCC_PATH))]
50	pub gcc: Option<PathBuf>,
51
52	/// Comma separated list of types to derive. Possible values: debug, default, eq, copy, hash, ord, partialeq, partialord, constparamty.
53	#[cfg_attr(feature = "clap", arg(long, value_name = "TY[,TY...]", default_value_t = Derive::default(), verbatim_doc_comment))]
54	pub derive: Derive,
55
56	/// Comma separated list of features to use. Possible values: documentation, rustify.
57	#[cfg_attr(feature = "clap", arg(long, value_name = "FEATURE[,FEATURE...]", default_value_t = Features::default(), verbatim_doc_comment))]
58	pub features: Features,
59
60	/// Output file path.
61	#[cfg_attr(feature = "clap", arg(long, value_name = "FILE"))]
62	pub output: Option<PathBuf>,
63}
64
65
66pub struct Runner;
67
68impl Runner {
69	/// Returns path and version of the `pdbindgen` executable if found.
70	pub fn find_tool(bin: &Bin) -> Option<(&Path, String)> {
71		Command::new(&bin.path).arg("-V")
72		                       .stdout(Stdio::piped())
73		                       .stderr(Stdio::inherit())
74		                       .output()
75		                       .ok()
76		                       .and_then(|out| {
77			                       out.status
78			                          .success()
79			                          .then(|| {
80				                          std::str::from_utf8(&out.stdout).ok()?
81				                                                          .strip_prefix(BIN_NAME)
82				                                                          .map(|s| s.trim().to_owned())
83				                                                          .filter(|s| !s.is_empty())
84			                          })
85			                          .flatten()
86		                       })
87		                       .map(|ver| (bin.path.as_path(), ver))
88	}
89
90
91	/// Prepare `Command` to run `pdbindgen`,
92	/// but without content of the `cfg`.
93	pub fn cmd(bin: &Bin) -> Option<Command> {
94		Self::find_tool(bin).and_then(|(path, _)| {
95			                    let mut proc = Command::new(path);
96			                    std::env::current_dir().map(|pwd| proc.current_dir(pwd)).ok();
97			                    proc.envs(std::env::vars());
98			                    proc.stderr(stderr());
99			                    proc.into()
100		                    })
101	}
102
103
104	pub fn gen_cmd(cfg: &Cfg) -> Option<Command> {
105		Self::cmd(&cfg.bin).and_then(|mut proc| {
106			                   proc.args(cfg.cli_args());
107			                   proc.into()
108		                   })
109	}
110
111
112	pub fn find_sdk_version(cfg: &Cfg) -> Option<String> {
113		// Path of the SDK:
114		let path =
115			cfg.sdk.clone().or_else(|| {
116				               std::env::var(Cfg::ENV_SDK_PATH).ok()
117				                                               .filter(|s| !s.trim().is_empty())
118				                                               .filter(|s| {
119					                                               Path::new(s).try_exists().ok().unwrap_or(false)
120				                                               })
121				                                               .map(PathBuf::from)
122			               });
123
124		// Easiest way to get existing SDK version:
125		let sdk_version = path.and_then(|path| {
126			                      std::fs::read_to_string(path.join("VERSION.txt")).ok()
127			                                                                       .map(|s| s.trim().to_string())
128		                      })
129		                      .filter(|s| !s.is_empty());
130
131		// Alternative way is to execute the tool:
132		if sdk_version.is_none() {
133			Self::cmd(&cfg.bin)?.arg(FIND_SDK_VERSION_CMD)
134			                    .args(cfg.cli_args())
135			                    .stdout(Stdio::piped())
136			                    .output()
137			                    .ok()
138			                    .and_then(|out| {
139				                    out.status.success().then(|| {
140					                                        std::str::from_utf8(&out.stdout).ok()
141					                                                                        .map(|s| {
142						                                                                        s.trim().to_owned()
143					                                                                        })
144					                                                                        .filter(|s| !s.is_empty())
145				                                        })
146			                    })
147			                    .flatten()
148		} else {
149			sdk_version
150		}
151	}
152}
153
154
155impl Cfg {
156	/// Path of the `playdate-bindgen` executable.
157	pub const ENV_BIN_PATH: &'static str = "PDBINDGEN_PATH";
158	pub const ENV_ARM_GCC_PATH: &'static str = "ARM_GCC_PATH";
159	pub const ENV_SDK_PATH: &'static str = "PLAYDATE_SDK_PATH";
160}
161
162
163impl Default for Cfg {
164	fn default() -> Self {
165		let tool = std::env::var_os(Self::ENV_BIN_PATH).map(PathBuf::from)
166		                                               .unwrap_or_else(|| PathBuf::from(BIN_NAME));
167		Self { sdk: Default::default(),
168		       gcc: Default::default(),
169		       derive: Default::default(),
170		       features: Default::default(),
171		       output: None,
172		       bin: Bin { path: tool } }
173	}
174}
175
176
177impl Cfg {
178	pub fn cli_args(&self) -> Vec<String> {
179		let mut args = vec![];
180
181		if let Some(ref sdk) = self.sdk {
182			args.push(format!("--sdk={}", sdk.display()));
183		}
184
185		if let Some(ref gcc) = self.gcc {
186			args.push(format!("--gcc={}", gcc.display()));
187		}
188
189		args.extend(self.derive.cli_args());
190		args.extend(self.features.cli_args());
191
192		if let Some(ref path) = self.output {
193			args.push(format!("--output={}", path.display()));
194		}
195
196		args
197	}
198}
199
200
201#[derive(Debug, Clone, Copy)]
202pub struct Derive {
203	// standard
204	pub default: bool,
205	pub eq: bool,
206	pub copy: bool,
207	pub debug: bool,
208	pub hash: bool,
209	pub ord: bool,
210	pub partialeq: bool,
211	pub partialord: bool,
212	// extra
213	pub constparamty: bool,
214}
215
216impl Derive {
217	pub const fn empty() -> Self {
218		Self { default: false,
219		       eq: false,
220		       copy: false,
221		       debug: false,
222		       hash: false,
223		       ord: false,
224		       partialeq: false,
225		       partialord: false,
226		       constparamty: false }
227	}
228
229	pub fn cli_args(&self) -> Vec<String> {
230		let words = self.arg_feature_list();
231		if words.is_empty() {
232			vec!["--derive=".to_string()]
233		} else {
234			vec![format!("--derive={words}")]
235		}
236	}
237}
238
239
240impl FromStr for Derive {
241	type Err = Infallible;
242
243	fn from_str(s: &str) -> Result<Self, Self::Err> {
244		let mut this = Derive::empty();
245
246		if s.trim().is_empty() {
247			return Ok(this);
248		}
249
250		for word in s.to_ascii_lowercase().split(',') {
251			match word {
252				"default" => this.default = true,
253				"eq" => this.eq = true,
254				"copy" => this.copy = true,
255				"debug" => this.debug = true,
256				"hash" => this.hash = true,
257				"ord" => this.ord = true,
258				"partialeq" => this.partialeq = true,
259				"partialord" => this.partialord = true,
260				"constparamty" => this.constparamty = true,
261				_ => println!("cargo::warning=Unknown derive '{word}'."),
262			}
263		}
264
265		Ok(this)
266	}
267}
268
269impl Derive {
270	#[rustfmt::skip]
271	fn arg_feature_list(&self) -> String {
272		let mut out = Vec::new();
273		if self.default { out.push("default") }
274		if self.eq {out.push("eq")}
275		if self.copy {out.push("copy")}
276		if self.debug {out.push("debug")}
277		if self.hash {out.push("hash")}
278		if self.ord {out.push("ord")}
279		if self.partialeq {out.push("partialeq")}
280		if self.partialord {out.push("partialord")}
281		if self.constparamty {out.push("constparamty")}
282		out.join(",")
283	}
284}
285
286impl std::fmt::Display for Derive {
287	#[inline]
288	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.arg_feature_list().fmt(f) }
289}
290
291impl Default for Derive {
292	fn default() -> Self {
293		Self { debug: true,
294		       default: Default::default(),
295		       eq: Default::default(),
296		       copy: Default::default(),
297		       hash: Default::default(),
298		       ord: Default::default(),
299		       partialeq: Default::default(),
300		       partialord: Default::default(),
301		       constparamty: Default::default() }
302	}
303}
304
305
306#[derive(Debug, Clone, Copy)]
307pub struct Features {
308	pub documentation: bool,
309	pub rustify: bool,
310}
311
312impl Features {
313	pub const fn empty() -> Self {
314		Self { documentation: false,
315		       rustify: false }
316	}
317
318	pub fn cli_args(&self) -> Vec<String> {
319		let words = self.arg_feature_list();
320		if words.is_empty() {
321			vec!["--features=".to_string()]
322		} else {
323			vec![format!("--features={words}")]
324		}
325	}
326}
327
328
329impl FromStr for Features {
330	type Err = Infallible;
331
332	fn from_str(s: &str) -> Result<Self, Self::Err> {
333		let mut this = Features::empty();
334
335		if s.trim().is_empty() {
336			return Ok(this);
337		}
338
339		for word in s.to_ascii_lowercase().split([',', ' ']).filter(|s| !s.is_empty()) {
340			match word {
341				"documentation" => this.documentation = true,
342				"rustify" => this.rustify = true,
343				_ => println!("cargo::warning=Unknown feature '{word}' ({}).", word == "rustify"),
344			}
345		}
346
347		Ok(this)
348	}
349}
350
351impl Features {
352	#[rustfmt::skip]
353	fn arg_feature_list(&self) -> String {
354		let mut out = Vec::new();
355		if self.documentation { out.push("documentation") }
356		if self.rustify { out.push("rustify") }
357		out.join(",")
358	}
359}
360
361impl std::fmt::Display for Features {
362	#[inline]
363	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.arg_feature_list().fmt(f) }
364}
365
366impl Default for Features {
367	fn default() -> Self {
368		Self { documentation: true,
369		       rustify: false }
370	}
371}
372
373
374/// Bindings output filename components.
375#[derive(Debug, Clone)]
376pub struct Filename {
377	/// Version of the Playdate SDK.
378	pub sdk: String,
379
380	/// String representation of enabled features/derives.
381	pub mask: DerivesMask,
382
383	/// Current target.
384	pub target: Target,
385}
386
387impl Filename {
388	pub fn new<T: Into<DerivesMask>>(sdk: impl ToString, derives: T) -> Result<Self, VarError> {
389		let target = Target::from_env_target()?;
390		Ok(Self::new_for(sdk, derives, target))
391	}
392
393	#[inline(never)]
394	pub fn new_for<T: Into<DerivesMask>>(sdk: impl ToString, derives: T, target: Target) -> Self {
395		Self { sdk: sdk.to_string(),
396		       target,
397		       mask: derives.into() }
398	}
399
400	/// Returns formatted prefix+mid part of the filename,
401	/// excluding derives mask and ext.
402	pub fn trim_suffix(&self) -> String {
403		let target = &self.target;
404		let sdk = &self.sdk;
405		format!("pd{sdk}-{target}-")
406	}
407
408	/// Extract SDK version from rendered filename.
409	/// If `target` is set, able to extract full version with suffix (e.g. "-beta.1").
410	/// Otherwise without suffix.
411	///
412	/// Usefull for multiple usage with single `target` instead of [`get_sdk_version_from_filename`].
413	pub fn get_sdk_version_from_filename_with_target<'t>(s: &'t std::ffi::OsStr,
414	                                                     target: Option<&'_ str>)
415	                                                     -> Option<Cow<'t, std::ffi::OsStr>> {
416		let s = s.as_encoded_bytes();
417		if !s.starts_with(Self::PREFIX.as_bytes()) || !s.ends_with(Self::DOT_EXT.as_bytes()) {
418			None
419		} else {
420			use std::ffi::OsStr;
421
422			if let Some(target) = target {
423				let s = unsafe { OsStr::from_encoded_bytes_unchecked(&s[Self::PREFIX.len()..]) };
424				s.to_string_lossy()
425				 .split_once(target)
426				 .and_then(|(version, _)| version.get(..(version.len() - 1)))
427				 .map(OsString::from)
428				 .map(Cow::Owned)
429			} else if let Some((prefix, _)) = &s[Self::PREFIX.len()..].split_once(|c| *c == b'-') {
430				let os = unsafe { OsStr::from_encoded_bytes_unchecked(prefix) };
431				Some(os.into())
432			} else {
433				None
434			}
435		}
436	}
437
438	pub fn get_sdk_version_from_filename<'t>(s: &'t std::ffi::OsStr,
439	                                         target: Option<&Target>)
440	                                         -> Option<Cow<'t, std::ffi::OsStr>> {
441		let target = target.map(ToString::to_string);
442		Self::get_sdk_version_from_filename_with_target(s, target.as_deref())
443	}
444
445
446	pub const PREFIX: &str = "pd";
447	pub const DOT_EXT: &str = ".rs";
448}
449
450
451impl std::fmt::Display for Filename {
452	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453		let derives = &self.mask;
454		let target = &self.target;
455		let sdk = &self.sdk;
456		write!(f, "pd{sdk}-{target}-{derives}{}", Self::DOT_EXT)
457	}
458}
459
460
461/// Target representation.
462/// There is not needed abi and arch because rust's arch in the corelib gives great abstraction.
463#[derive(Debug, Clone)]
464pub enum Target {
465	Playdate,
466	Other {
467		/// Target pointer width in bits.
468		ptr: String,
469
470		/// Target arch.
471		arch: String,
472
473		/// Target OS.
474		os: String,
475
476		/// Target c_int size in bits.
477		/// For playdate usually it should be 1 byte if compiled with `-fshort-enums`.
478		/// For other targets it should be between size of `16` and `64` bits, usually `32`,
479		/// before optimizations but it doesn't matter at all.
480		c_int: usize,
481	},
482}
483
484impl Target {
485	/// Retrieve by cargo env vars.
486	pub fn from_env_target() -> Result<Self, VarError> {
487		use std::env::{var, var_os};
488
489		let target = var("TARGET")?;
490		let is_pdos = var_os("CARGO_CFG_TARGET_OS").filter(|v| {
491			                                           let v = v.to_ascii_lowercase();
492			                                           v == "playdate" || v == "playdateos" || v == "pdos"
493		                                           })
494		                                           .is_some();
495		// let is_panic = var_os("CARGO_CFG_TARGET_VENDOR").filter(|v| {
496		// 	                                                let v = v.to_ascii_lowercase();
497		// 	                                                v == "panic" || v == "playdate"
498		//                                                 })
499		//                                                 .is_some();
500		// XXX: "sim" may conflict with "simd" for example.
501		let is_sim = target.contains("sim");
502		if target == "thumbv7em-none-eabihf" || (is_pdos && !is_sim) || (target.contains("playdate") && !is_sim) {
503			Ok(Self::Playdate)
504		} else {
505			use core::ffi::c_int;
506			let ptr = var("CARGO_CFG_TARGET_POINTER_WIDTH")?;
507			let arch = var("CARGO_CFG_TARGET_ARCH")?;
508			let os = var("CARGO_CFG_TARGET_OS")?;
509			Ok(Self::Other { os,
510			                 arch,
511			                 ptr,
512			                 c_int: c_int::BITS as usize })
513		}
514	}
515
516	pub fn is_playdate(&self) -> bool { matches!(self, Target::Playdate) }
517}
518
519impl std::fmt::Display for Target {
520	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521		match self {
522			Target::Playdate => write!(f, "pd"),
523			Target::Other { os, ptr, arch, c_int } => write!(f, "{os}-{arch}-{ptr}-i{c_int}"),
524		}
525	}
526}