playdate_build_utils/toolchain/
sdk.rs

1//! Playdate SDK
2
3use std::borrow::Cow;
4use std::io::Error;
5use std::io::ErrorKind;
6use std::path::Path;
7use std::path::PathBuf;
8
9use crate::consts::SDK_ENV_VAR;
10
11
12pub struct Sdk {
13	path: PathBuf,
14}
15
16
17// Paths:
18impl Sdk {
19	pub fn path(&self) -> &Path { &self.path }
20	pub fn c_api(&self) -> PathBuf { self.path.join("C_API") }
21	pub fn build_support(&self) -> BuildSupport { BuildSupport { path: self.c_api().join("buildsupport").into() } }
22	pub fn version_file(&self) -> PathBuf { self.path.join("VERSION.txt") }
23
24	pub fn bin(&self) -> PathBuf { self.path.join("bin") }
25
26	pub fn pdc(&self) -> PathBuf {
27		#[cfg(unix)]
28		const PDC: &str = "pdc";
29		#[cfg(windows)]
30		const PDC: &'static str = "PDC.EXE";
31		self.bin().join(PDC)
32	}
33
34	pub fn pdutil(&self) -> PathBuf {
35		#[cfg(unix)]
36		const PDUTIL: &str = "pdutil";
37		#[cfg(windows)]
38		const PDUTIL: &'static str = "PDUTIL.EXE";
39		self.bin().join(PDUTIL)
40	}
41}
42
43
44// Constructors:
45impl Sdk {
46	/// Create new `Sdk` with auto-determining the SDK path
47	pub fn try_new() -> Result<Self, Error> {
48		let try_with = move |f: fn() -> Result<Self, Error>| {
49			let result = f();
50			match result {
51				Err(ref result_error) => crate::error!("{result_error}"),
52				Ok(ref sdk) => {
53					crate::info!("Found SDK in {}", sdk.path().display())
54				},
55			}
56			result
57		};
58
59		let res = try_with(Self::try_from_default_env).or_else(|_| try_with(Self::try_from_default_config));
60		#[cfg(unix)]
61		let res = res.or_else(|_| try_with(Self::try_xdg_unix_path));
62
63		#[cfg(windows)]
64		let res = res.or_else(|_| try_with(Self::try_windows_registry));
65
66		res.or_else(|_| try_with(Self::try_from_default_path))
67	}
68
69	/// Create new `Sdk` with exact passed SDK path
70	pub fn try_new_exact<P: Into<PathBuf>>(root: P) -> Result<Self, Error> {
71		let path = root.into();
72		let err = |p: &Path| {
73			Error::new(
74			           ErrorKind::InvalidInput,
75			           format!("Invalid SDK path '{}'", p.display()),
76			)
77		};
78
79		if path.exists() && path.is_dir() {
80			let sdk = Self { path };
81			if sdk.build_support().link_map().exists() {
82				Ok(sdk)
83			} else {
84				Err(err(&sdk.path))
85			}
86		} else {
87			Err(err(&path))
88		}
89	}
90
91	#[cfg(windows)]
92	pub fn try_windows_registry() -> Result<Self, Error> {
93		let key: String = windows_registry::CURRENT_USER.open(r#"Software\\PlaydateSDK"#)?
94		                                                .get_string("")?;
95		return Self::try_new_exact(key);
96	}
97
98	/// Create new `Sdk` with default env var
99	pub fn try_from_default_env() -> Result<Self, Error> {
100		let sdk = std::env::var_os(SDK_ENV_VAR).map(PathBuf::from)
101		                                       .map(Self::try_new_exact);
102		sdk.ok_or(Error::new(ErrorKind::NotFound, format!("Missed env {SDK_ENV_VAR}")))?
103	}
104
105	/// Create new `Sdk` with default env var
106	pub fn try_from_default_config() -> Result<Self, Error> {
107		let cfg = config::Cfg::try_default()?;
108		let path = cfg.sdk_path()
109		              .ok_or(Error::new(ErrorKind::InvalidInput, "SDK path is not set"))?;
110		Self::try_new_exact(path)
111	}
112
113	/// Create new `Sdk` with default env var
114	pub fn try_from_default_path() -> Result<Self, Error> {
115		#[cfg(unix)]
116		const SDK_HOME_DIR: &str = "Developer";
117		#[cfg(windows)]
118		const SDK_HOME_DIR: &'static str = "Documents";
119
120		let home = utils::home_dir()?;
121		Self::try_new_exact(home.join(SDK_HOME_DIR).join("PlaydateSDK"))
122	}
123
124	#[cfg(unix)]
125	pub fn try_xdg_unix_path() -> Result<Self, Error> {
126		const XDG_CONFIG_DATA_ENV: &str = "XDG_CONFIG_DATA";
127
128		let xdg_data_path = match std::env::var(XDG_CONFIG_DATA_ENV) {
129			Ok(ref variable) => PathBuf::from(variable),
130			Err(_) => utils::home_dir()?.join(".local").join("share"),
131		};
132
133		Self::try_new_exact(xdg_data_path.join("playdate-sdk"))
134	}
135}
136
137
138// Read:
139impl Sdk {
140	pub fn read_version(&self) -> Result<String, Error> {
141		let value = std::fs::read_to_string(self.version_file())?;
142		Ok(value)
143	}
144}
145
146
147pub struct BuildSupport<'t> {
148	path: Cow<'t, Path>,
149}
150
151
152impl<'t> BuildSupport<'t> {
153	pub fn setup(&self) -> Cow<'t, Path> { self.path.join("setup.c").into() }
154	pub fn link_map(&self) -> Cow<'t, Path> { self.path.join("link_map.ld").into() }
155}
156
157
158mod utils {
159	use super::Error;
160	use super::ErrorKind;
161	use std::path::PathBuf;
162
163	pub fn home_dir() -> Result<PathBuf, Error> {
164		dirs::home_dir().ok_or(Error::new(ErrorKind::InvalidInput, "Could not find home dir"))
165	}
166}
167
168
169mod config {
170	use super::Error;
171	use super::utils;
172	use std::collections::HashMap;
173	use std::path::PathBuf;
174	use std::str::FromStr;
175
176	#[cfg(unix)]
177	const DEFAULT_XDG_CONFIG_DIR: &str = ".config";
178	#[cfg(unix)]
179	const XDG_CONFIG_HOME_ENV: &str = "XDG_CONFIG_HOME";
180	const CFG_DIR: &str = ".Playdate";
181	const CFG_FILENAME: &str = "config";
182	const CFG_KEY_SDK_ROOT: &str = "SDKRoot";
183
184	pub(super) struct Cfg(HashMap<String, String>);
185
186	impl Cfg {
187		pub fn try_default() -> Result<Self, Error> {
188			fn find_config_folder() -> Result<PathBuf, Error> {
189				#[cfg(unix)]
190				{
191					let xdg_cfg_path = match std::env::var(XDG_CONFIG_HOME_ENV) {
192						                   Ok(ref variable) => PathBuf::from(variable),
193					                      Err(_) => utils::home_dir()?.join(DEFAULT_XDG_CONFIG_DIR),
194					                   }.join(CFG_DIR)
195					                   .join(CFG_FILENAME);
196
197					if xdg_cfg_path.exists() {
198						return Ok(xdg_cfg_path);
199					}
200					// Fallback to ~/.Playdate/config
201				}
202
203				let cfg_path = utils::home_dir()?.join(CFG_DIR).join(CFG_FILENAME);
204
205				if cfg_path.exists() {
206					return Ok(cfg_path);
207				}
208
209				Err(Error::new(std::io::ErrorKind::NotFound, "Config file not found"))
210			}
211
212			std::fs::read_to_string(find_config_folder()?)?.parse()
213		}
214
215		pub fn sdk_path(&self) -> Option<PathBuf> { self.0.get(CFG_KEY_SDK_ROOT).map(PathBuf::from) }
216	}
217
218	impl FromStr for Cfg {
219		type Err = Error;
220
221		fn from_str(s: &str) -> Result<Self, Self::Err> {
222			Ok(Self(
223			        s.trim()
224			         .lines()
225			         .filter_map(|line| line.split_once('\t').map(|(k, v)| (k.to_owned(), v.to_owned())))
226			         .collect(),
227			))
228		}
229	}
230
231
232	#[cfg(test)]
233	mod tests {
234		use super::*;
235
236		#[test]
237		fn parse() {
238			let path = "/path/PlaydateSDK-dir";
239			let cfg: Cfg = format!("{k}\t{v}\n", k = CFG_KEY_SDK_ROOT, v = path).parse()
240			                                                                    .unwrap();
241			assert_eq!(cfg.sdk_path(), Some(PathBuf::from(path)));
242		}
243	}
244}
245
246
247// TODO: Move this tests to integration tests dir "tests" and run if sdk exists only.
248#[cfg(test)]
249mod tests {
250	use super::*;
251
252
253	#[test]
254	fn sdk() { Sdk::try_new().unwrap(); }
255}