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
16pub 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#[derive(Clone, Debug)]
27#[cfg_attr(feature = "clap", derive(clap::Parser))]
28pub struct Bin {
29 #[cfg_attr(feature = "clap", arg(skip))]
31 pub path: PathBuf,
32}
33
34
35#[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 #[cfg_attr(feature = "clap", clap(flatten))]
42 pub bin: Bin,
43
44 #[cfg_attr(feature = "clap", arg(long, value_name = "SDK", env = Cfg::ENV_SDK_PATH))]
46 pub sdk: Option<PathBuf>,
47
48 #[cfg_attr(feature = "clap", arg(long, value_name = "GCC", env = Cfg::ENV_ARM_GCC_PATH))]
50 pub gcc: Option<PathBuf>,
51
52 #[cfg_attr(feature = "clap", arg(long, value_name = "TY[,TY...]", default_value_t = Derive::default(), verbatim_doc_comment))]
54 pub derive: Derive,
55
56 #[cfg_attr(feature = "clap", arg(long, value_name = "FEATURE[,FEATURE...]", default_value_t = Features::default(), verbatim_doc_comment))]
58 pub features: Features,
59
60 #[cfg_attr(feature = "clap", arg(long, value_name = "FILE"))]
62 pub output: Option<PathBuf>,
63}
64
65
66pub struct Runner;
67
68impl Runner {
69 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
376pub struct Filename {
377 pub sdk: String,
379
380 pub mask: DerivesMask,
382
383 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 pub fn trim_suffix(&self) -> String {
403 let target = &self.target;
404 let sdk = &self.sdk;
405 format!("pd{sdk}-{target}-")
406 }
407
408 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#[derive(Debug, Clone)]
464pub enum Target {
465 Playdate,
466 Other {
467 ptr: String,
469
470 arch: String,
472
473 os: String,
475
476 c_int: usize,
481 },
482}
483
484impl Target {
485 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_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}