playdate_build_utils/toolchain/
gcc.rs

1//! ARM GNU toolchain
2
3use std::borrow::Cow;
4use std::ffi::OsStr;
5use std::io::ErrorKind;
6use std::path::Path;
7use std::path::PathBuf;
8use std::process::Command;
9use std::io::Error as IoError;
10use std::process::Stdio;
11
12use self::err::Error;
13
14
15/// Env var name that points to the arm-gcc executable.
16pub const ARM_GCC_PATH_ENV_VAR: &str = "ARM_GCC_PATH";
17
18/// Variants of the compile's name - actual and old.
19pub const ARM_NONE_EABI_GCC: &[&str] = &["arm-none-eabi-gcc", "gcc-arm-none-eabi"];
20
21
22pub struct Gcc {
23	path: PathBuf,
24}
25
26pub struct ArmToolchain {
27	gcc: Gcc,
28	sysroot: PathBuf,
29}
30
31
32impl Gcc {
33	pub fn path(&self) -> &Path { self.path.as_path() }
34
35
36	/// Automatically determine the gcc
37	pub fn try_new() -> Result<Self, Error> {
38		let try_with = |f: fn() -> Result<Self, Error>| {
39			move |err: Error| {
40				let result = f();
41				if result.is_err() {
42					crate::error!("{err}");
43				}
44				result
45			}
46		};
47
48		Self::try_from_default_env().or_else(try_with(Self::try_from_env_path))
49		                            .or_else(try_with(Self::try_from_default_path))
50	}
51
52
53	pub fn try_new_exact_path<P: Into<PathBuf>>(path: P) -> Result<Self, Error> {
54		let path = path.into().canonicalize()?;
55		if path.try_exists()? {
56			Ok(Self { path })
57		} else {
58			Err(IoError::new(
59				ErrorKind::NotFound,
60				format!("Could not find ARM GCC at '{}'", path.display()),
61			).into())
62		}
63	}
64
65
66	/// Create new with default env var
67	pub fn try_from_default_env() -> Result<Self, Error> {
68		let res = std::env::var_os(ARM_GCC_PATH_ENV_VAR).map(PathBuf::from)
69		                                                .map(Self::try_new_exact_path);
70		res.ok_or(IoError::new(ErrorKind::NotFound, format!("Missed env {ARM_GCC_PATH_ENV_VAR}")))?
71	}
72
73	/// Create new with executable in PATH
74	pub fn try_from_env_path() -> Result<Self, Error> {
75		for name in ARM_NONE_EABI_GCC {
76			if let Ok(result) = Self::try_from_path(name) {
77				return Ok(result);
78			}
79		}
80
81		Err(Error::Err("Could not find ARM GCC in PATH"))
82	}
83
84	/// Create new with executable name or path
85	pub fn try_from_path<S: AsRef<OsStr>>(path: S) -> Result<Self, Error> {
86		let mut proc = Command::new(path.as_ref());
87		proc.arg("--version");
88		let output = proc.output()?;
89		if !output.status.success() {
90			return Err(Error::exit_status_error(&proc, output.stderr, output.status));
91		}
92		Ok(Self { path: path.as_ref().into() })
93	}
94
95	/// Create new with default path of executable
96	pub fn try_from_default_path() -> Result<Self, Error> {
97		#[cfg(unix)]
98		{
99			let paths = ["/usr/local/bin/", "/usr/bin/"].into_iter()
100			                                            .map(Path::new)
101			                                            .flat_map(|p| ARM_NONE_EABI_GCC.iter().map(|name| p.join(name)))
102			                                            .filter(|p| p.try_exists().ok().unwrap_or_default());
103			for path in paths {
104				match Self::try_from_path(&path) {
105					Ok(gcc) => return Ok(gcc),
106					Err(err) => crate::debug!("{}: {err:?}", path.display()),
107				}
108			}
109
110			// Not found, so err:
111			Err(Error::Err("Could not find ARM toolchain in default paths"))
112		}
113
114		#[cfg(windows)]
115		{
116			let path =
117				PathBuf::from(r"C:\Program Files (x86)\GNU Tools Arm Embedded\9 2019-q4-major\bin\").join(ARM_NONE_EABI_GCC[0])
118				                                                                                    .with_extension("exe");
119			Self::try_from_path(path).map_err(|_| Error::Err("Could not find ARM toolchain in default paths"))
120		}
121	}
122
123
124	/// Determine sysroot.
125	// There're another ways to do this.
126	// For example we can parse makefile in PlaydateSDK to get the path, but that's ugly way.
127	fn sysroot(&self) -> Result<PathBuf, Error> { self.sysroot_by_output().or_else(|_| self.sysroot_fallback()) }
128
129	/// Determine by asking gcc.
130	fn sysroot_by_output(&self) -> Result<PathBuf, Error> {
131		let mut proc = Command::new(&self.path);
132		proc.arg("-print-sysroot");
133
134		let output = proc.output()?;
135		if !output.status.success() {
136			return Err(Error::exit_status_error(&proc, output.stderr, output.status));
137		}
138		let path = std::str::from_utf8(&output.stdout).map(str::trim)
139		                                              .map(PathBuf::from)?;
140
141		if path.as_os_str().is_empty() {
142			Err(Error::Err("gcc returns empty string for sysroot"))
143		} else {
144			Ok(path.canonicalize()?)
145		}
146	}
147
148	/// Determine by path, relative to the gcc path.
149	fn sysroot_fallback(&self) -> Result<PathBuf, Error> {
150		// just name in PATH | full path
151		let path = if self.path.is_relative() || self.path.components().count() == 1 {
152			           let mut proc = Command::new("which");
153			           proc.arg(&self.path);
154			           let output = proc.output()?;
155			           if !output.status.success() {
156				           return Err(Error::exit_status_error(&proc, output.stderr, output.status));
157			           }
158			           crate::debug!("path by which: {:?}", std::str::from_utf8(&output.stdout));
159			           Cow::from(std::str::from_utf8(&output.stdout).map(str::trim)
160			                                                        .map(PathBuf::from)?)
161		           } else {
162			           Cow::from(self.path.as_path())
163		           }.canonicalize()?;
164
165
166		let path = path.parent()
167		               .and_then(|p| p.parent())
168		               .map(|p| p.join("arm-none-eabi"))
169		               .ok_or(IoError::new(ErrorKind::NotFound, "GCC sysroot not found"))?;
170
171		if !path.exists() && path == PathBuf::from("/usr/arm-none-eabi") {
172			let path = PathBuf::from("/usr/lib/arm-none-eabi");
173			if path.exists() {
174				return Ok(path);
175			}
176		}
177
178		crate::trace!("trying canonicalize this: {}", path.display());
179		let path = path.canonicalize()?;
180		Ok(path)
181	}
182}
183
184
185impl ArmToolchain {
186	pub fn gcc(&self) -> &Gcc { &self.gcc }
187	pub fn bin(&self) -> PathBuf { self.sysroot.join("bin") }
188	pub fn lib(&self) -> PathBuf { self.sysroot.join("lib") }
189	pub fn include(&self) -> PathBuf { self.sysroot.join("include") }
190	pub fn sysroot(&self) -> &Path { self.sysroot.as_ref() }
191
192
193	/// Specialized search-path for target
194	// e.g.: arm-none-eabi-gcc -mthumb -mcpu=cortex-m7 -mfloat-abi=hard -mfpu=fpv5-sp-d16 -print-search-dirs
195	pub fn lib_search_paths_for<S: AsRef<OsStr>, I: IntoIterator<Item = S>>(&self,
196	                                                                        args: I)
197	                                                                        -> Result<Vec<PathBuf>, Error> {
198		let mut proc = Command::new(self.gcc().path());
199		proc.args(args);
200		proc.arg("-print-search-dirs");
201		proc.stderr(Stdio::inherit());
202		proc.stdout(Stdio::piped());
203
204		let output = proc.output()?;
205		if !output.status.success() {
206			return Err(Error::exit_status_error(&proc, output.stderr, output.status));
207		}
208
209		#[cfg(not(windows))]
210		const SEP: &str = ":";
211		#[cfg(windows)]
212		const SEP: &str = ";";
213
214		Ok(std::str::from_utf8(&output.stdout)?.lines()
215		                                       .filter_map(|s| s.strip_prefix("libraries: ="))
216		                                       .flat_map(|s| s.split(SEP).map(|s| s.trim()).map(PathBuf::from))
217		                                       .collect())
218	}
219
220	pub fn lib_search_paths_for_playdate(&self) -> Result<Vec<PathBuf>, Error> {
221		self.lib_search_paths_for([
222			"-mthumb",
223			"-mcpu=cortex-m7",
224			"-mfloat-abi=hard",
225			"-mfpu=fpv5-sp-d16",
226		])
227	}
228
229	pub fn lib_search_paths_default(&self) -> Result<Vec<PathBuf>, Error> {
230		match self.lib_search_paths_for::<&str, _>([]) {
231			Ok(paths) if !paths.is_empty() => Ok(paths),
232			Ok(_) | Err(_) => Ok(vec![self.gcc().sysroot().map(|p| p.join("lib"))?]),
233		}
234	}
235
236
237	/// Create auto-determine the toolchain
238	pub fn try_new() -> Result<Self, Error> { Self::try_new_with(Gcc::try_new()?) }
239
240	/// Create auto-determine the toolchain by specified gcc
241	pub fn try_new_with(gcc: Gcc) -> Result<Self, Error> {
242		let sysroot = gcc.sysroot()?;
243		let bin = sysroot.join("bin");
244		let lib = sysroot.join("lib");
245		let include = sysroot.join("include");
246
247		if !bin.try_exists()? || !lib.try_exists()? || !include.try_exists()? {
248			Err(IoError::new(
249				ErrorKind::NotFound,
250				format!("ARM toolchain not found in '{}'", sysroot.display()),
251			).into())
252		} else {
253			Ok(Self { gcc, sysroot })
254		}
255	}
256}
257
258
259pub mod err {
260	use std::io::Error as IoError;
261	use std::process::Command;
262	use std::process::ExitStatus;
263	use std::str::Utf8Error;
264
265	#[derive(Debug)]
266	pub enum Error {
267		Io(IoError),
268		Utf8(Utf8Error),
269		Err(&'static str),
270		ExitStatusError {
271			cmd: String,
272			stderr: Vec<u8>,
273			status: ExitStatus,
274		},
275		// TODO: from `std::process::ExitStatusError` when stabilized `exit_status_error`
276	}
277
278	impl From<&'static str> for Error {
279		fn from(s: &'static str) -> Self { Self::Err(s) }
280	}
281
282	impl From<IoError> for Error {
283		fn from(err: IoError) -> Self { Self::Io(err) }
284	}
285	impl From<Utf8Error> for Error {
286		fn from(err: Utf8Error) -> Self { Self::Utf8(err) }
287	}
288
289	impl Error {
290		pub fn exit_status_error(cmd: &Command, stderr: Vec<u8>, status: ExitStatus) -> Self {
291			let cmd = format!(
292			                  "{} {}",
293			                  cmd.get_program().to_string_lossy(),
294			                  cmd.get_args()
295			                     .map(|s| s.to_string_lossy())
296			                     .collect::<Vec<_>>()
297			                     .join(" ")
298			);
299			Self::ExitStatusError { cmd, stderr, status }
300		}
301	}
302
303	impl std::error::Error for Error {
304		fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
305			match self {
306				Error::Io(err) => Some(err),
307				Error::Utf8(err) => Some(err),
308				Error::Err(_) => None,
309				Error::ExitStatusError { .. } => None,
310			}
311		}
312	}
313
314	impl std::fmt::Display for Error {
315		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316			match self {
317				Error::Io(err) => err.fmt(f),
318				Error::Utf8(err) => err.fmt(f),
319				Error::Err(err) => err.fmt(f),
320				Error::ExitStatusError { cmd, status, stderr } => {
321					let stderr = std::str::from_utf8(stderr).map(str::trim)
322					                                        .map(|s| format!("with output: {s}"))
323					                                        .ok()
324					                                        .unwrap_or_else(|| {
325						                                        if stderr.is_empty() {
326							                                        "without output".into()
327						                                        } else {
328							                                        "with not decodable output".into()
329						                                        }
330					                                        });
331					write!(f, "ExitStatusError: ({status}) {cmd} {stderr}.",)
332				},
333			}
334		}
335	}
336}
337
338
339// TODO: Maybe move this tests to integration tests dir and run if arm-gcc exists only.
340#[cfg(test)]
341mod tests {
342	use super::*;
343
344
345	#[test]
346	fn gcc_from_env_path() { Gcc::try_from_env_path().unwrap(); }
347
348	#[test]
349	#[cfg(unix)]
350	fn gcc_from_default_path() { Gcc::try_from_default_path().unwrap(); }
351
352
353	#[test]
354	#[cfg(unix)]
355	fn gcc_sysroot_fallback() {
356		let gcc = Gcc::try_new().unwrap();
357		let res = gcc.sysroot_fallback().unwrap();
358		assert!(res.exists());
359	}
360
361	#[test]
362	#[ignore = "sysroot can be empty"]
363	fn gcc_sysroot_by_output() {
364		let gcc = Gcc::try_new().unwrap();
365		let res = gcc.sysroot_by_output().unwrap();
366		assert!(res.exists());
367	}
368
369
370	#[test]
371	fn toolchain_new() {
372		let toolchain = ArmToolchain::try_new().unwrap();
373		assert!(toolchain.bin().exists());
374		assert!(toolchain.lib().exists());
375		assert!(toolchain.include().exists());
376	}
377}