playdate_build_utils/toolchain/
gcc.rs1use 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
15pub const ARM_GCC_PATH_ENV_VAR: &str = "ARM_GCC_PATH";
17
18pub 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 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 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 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 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 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 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 fn sysroot(&self) -> Result<PathBuf, Error> { self.sysroot_by_output().or_else(|_| self.sysroot_fallback()) }
128
129 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 fn sysroot_fallback(&self) -> Result<PathBuf, Error> {
150 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 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 pub fn try_new() -> Result<Self, Error> { Self::try_new_with(Gcc::try_new()?) }
239
240 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 }
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#[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}