use std::path::{Path, PathBuf};
use thiserror::Error;
use tokio::process::Command;
#[derive(Error, Debug)]
pub enum DtcError {
#[error("device-tree compile I/O error at {path}: {source}")]
Io {
path: String,
source: std::io::Error,
},
#[error(
"`--linux` path {path} does not look like a kernel tree (no `include/` directory); \
point it at a Linux source checkout that provides dt-bindings headers"
)]
LinuxTreeInvalid {
path: String,
},
#[error("`gcc` not found on PATH; needed to preprocess the device tree before `dtc`")]
GccNotFound,
#[error("`dtc` not found on PATH; install the device-tree compiler to build `.dtb`s")]
DtcNotFound,
#[error("device tree {name} failed to compile:\n{message}")]
Compile {
name: String,
message: String,
},
}
fn io(path: impl AsRef<Path>, source: std::io::Error) -> DtcError {
DtcError::Io {
path: path.as_ref().display().to_string(),
source,
}
}
pub fn arch_dir(app_target: &str) -> &'static str {
if app_target.starts_with("aarch64") {
"arm64"
} else {
"arm"
}
}
pub async fn compile_device_tree(
dts_dir: &Path,
dts_name: &str,
closure: &[(String, String)],
linux_dir: &Path,
arch: &str,
) -> Result<PathBuf, DtcError> {
consortium_log::trace!("compiling device tree {} (arch {})", dts_name, arch);
let include = linux_dir.join("include");
if !include.is_dir() {
consortium_log::trace!("linux include dir missing at {}", include.display());
return Err(DtcError::LinuxTreeInvalid {
path: linux_dir.display().to_string(),
});
}
let work = dts_dir.join(".dtc-build");
let _ = tokio::fs::remove_dir_all(&work).await;
tokio::fs::create_dir_all(&work)
.await
.map_err(|e| io(&work, e))?;
for (name, contents) in closure {
let dest = work.join(name);
if let Some(parent) = dest.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| io(parent, e))?;
}
tokio::fs::write(&dest, contents)
.await
.map_err(|e| io(&dest, e))?;
}
let staged = dts_dir.join(dts_name);
let src = work.join(dts_name);
tokio::fs::copy(&staged, &src)
.await
.map_err(|e| io(&staged, e))?;
let stem = dts_name
.rsplit_once('.')
.map(|(s, _)| s)
.unwrap_or(dts_name);
let is_overlay = dts_name.ends_with(".dtso");
let out_name = format!("{stem}.{}", if is_overlay { "dtbo" } else { "dtb" });
let preprocessed = work.join(format!("{stem}.pp.dts"));
let boot_dts = linux_dir.join("arch").join(arch).join("boot").join("dts");
let gcc = Command::new("gcc")
.args(["-E", "-nostdinc"])
.arg("-I")
.arg(&work)
.arg("-I")
.arg(&include)
.arg("-I")
.arg(&boot_dts)
.args(["-undef", "-D__DTS__", "-x", "assembler-with-cpp"])
.arg(&src)
.arg("-o")
.arg(&preprocessed)
.output()
.await
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => DtcError::GccNotFound,
_ => io(&src, e),
})?;
if !gcc.status.success() {
consortium_log::trace!("gcc preprocess failed for {}", dts_name);
let _ = tokio::fs::remove_dir_all(&work).await;
return Err(DtcError::Compile {
name: dts_name.to_string(),
message: String::from_utf8_lossy(&gcc.stderr).into_owned(),
});
}
let out = dts_dir.join(&out_name);
let dtc = Command::new("dtc")
.args(["-@", "-I", "dts", "-O", "dtb", "-o"])
.arg(&out)
.arg(&preprocessed)
.output()
.await
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => DtcError::DtcNotFound,
_ => io(&preprocessed, e),
});
let _ = tokio::fs::remove_dir_all(&work).await;
let dtc = dtc?;
if !dtc.status.success() {
consortium_log::trace!("dtc compile failed for {}", dts_name);
return Err(DtcError::Compile {
name: dts_name.to_string(),
message: String::from_utf8_lossy(&dtc.stderr).into_owned(),
});
}
consortium_log::trace!("device tree compiled to {}", out.display());
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arch_dir_maps_triples() {
assert_eq!(arch_dir("aarch64-unknown-linux-gnu"), "arm64");
assert_eq!(arch_dir("armv7-unknown-linux-gnueabihf"), "arm");
}
#[test]
fn invalid_linux_tree_is_rejected() {
let dir = std::env::temp_dir().join(format!("dtc-nolinux-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
let err = rt
.block_on(compile_device_tree(
&dir,
"x.dts",
&[],
&dir.join("not-a-kernel"),
"arm64",
))
.unwrap_err();
assert!(matches!(err, DtcError::LinuxTreeInvalid { .. }));
let _ = std::fs::remove_dir_all(&dir);
}
fn tools_available() -> bool {
fn on_path(program: &str) -> bool {
std::process::Command::new(program)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
on_path("gcc") && on_path("dtc")
}
#[test]
fn compiles_dts_against_a_synthetic_kernel_tree() {
if !tools_available() {
eprintln!("skipping: `gcc`/`dtc` not on PATH");
return;
}
let base = std::env::temp_dir().join(format!("dtc-synth-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let dts_dir = base.join("dts");
std::fs::create_dir_all(&dts_dir).unwrap();
let bindings = base.join("linux/include/dt-bindings/test");
std::fs::create_dir_all(&bindings).unwrap();
std::fs::write(bindings.join("macros.h"), "#define TEST_VALUE 0x2a\n").unwrap();
let generated = "/dts-v1/;\n\
#include <dt-bindings/test/macros.h>\n\
#include \"vendor.dtsi\"\n\
/ {\n\tconsortium { value = <TEST_VALUE>; };\n};\n";
std::fs::write(dts_dir.join("consortium-x.dts"), generated).unwrap();
let closure = vec![(
"vendor.dtsi".to_string(),
"/ {\n\tvendor { ok; };\n};\n".to_string(),
)];
let rt = tokio::runtime::Runtime::new().unwrap();
let out = rt
.block_on(compile_device_tree(
&dts_dir,
"consortium-x.dts",
&closure,
&base.join("linux"),
"arm64",
))
.expect("synthetic compile succeeds");
assert_eq!(out, dts_dir.join("consortium-x.dtb"));
let dtb = std::fs::read(&out).unwrap();
assert_eq!(&dtb[..4], &[0xd0, 0x0d, 0xfe, 0xed]);
assert!(!dts_dir.join(".dtc-build").exists());
let _ = std::fs::remove_dir_all(&base);
}
}