zephyr_build/
lib.rs

1// Copyright (c) 2024 Linaro LTD
2// SPDX-License-Identifier: Apache-2.0
3
4// Pre-build code for zephyr module.
5
6// This module makes the values from the generated .config available as conditional compilation.
7// Note that this only applies to the zephyr module, and the user's application will not be able to
8// see these definitions.  To make that work, this will need to be moved into a support crate which
9// can be invoked by the user's build.rs.
10
11// This builds a program that is run on the compilation host before the code is compiled.  It can
12// output configuration settings that affect the compilation.
13
14use std::collections::HashSet;
15use std::env;
16use std::fs::File;
17use std::io::{BufRead, BufReader, Write};
18use std::path::Path;
19use std::process::{Command, Stdio};
20
21use proc_macro2::TokenStream;
22use regex::Regex;
23
24use devicetree::{Augment, DeviceTree};
25
26mod devicetree;
27
28/// Extract boolean Kconfig entries.  This must happen in any crate that wishes to access the
29/// configuration settings.
30pub fn extract_kconfig_bool_options(path: &str) -> anyhow::Result<HashSet<String>> {
31    let config_y = Regex::new(r"^(CONFIG_.*)=y$").expect("hardcoded regex is always valid");
32
33    let input = File::open(path)?;
34    let flags: HashSet<String> = BufReader::new(input)
35        .lines()
36        // If the line is an Err(_), just ignore it.
37        .map(|x| x.unwrap_or_default())
38        .filter_map(|line| config_y.captures(&line).map(|caps| caps[1].to_string()))
39        .collect();
40
41    Ok(flags)
42}
43
44/// Export boolean Kconfig entries.  This must happen in any crate that wishes to access the
45/// configuration settings.
46pub fn export_kconfig_bool_options() {
47    let dotconfig = env::var("DOTCONFIG").expect("DOTCONFIG must be set by wrapper");
48
49    // Ensure the build script is rerun when the dotconfig changes.
50    println!("cargo:rerun-if-env-changed=DOTCONFIG");
51    println!("cargo-rerun-if-changed={}", dotconfig);
52
53    extract_kconfig_bool_options(&dotconfig)
54        .expect("failed to extract flags from .config")
55        .iter()
56        .for_each(|flag| println!("cargo:rustc-cfg={flag}"));
57}
58
59/// Capture bool, numeric and string kconfig values in a 'kconfig' module.
60/// This is a little simplistic, and will make the entries numeric if they look like numbers.
61/// Ideally, this would be built on the types of the values, but that will require more
62/// introspection.
63pub fn build_kconfig_mod() {
64    let dotconfig = env::var("DOTCONFIG").expect("DOTCONFIG must be set by wrapper");
65    let outdir = env::var("OUT_DIR").expect("OUT_DIR must be set");
66
67    // The assumption is that hex values are unsigned, and decimal are signed.
68    let config_hex = Regex::new(r"^(CONFIG_.*)=(0x[0-9a-fA-F]+)$").unwrap();
69    let config_int = Regex::new(r"^(CONFIG_.*)=(-?[1-9][0-9]*|0)$").unwrap();
70    // It is unclear what quoting might be used in the .config.
71    let config_str = Regex::new(r#"^(CONFIG_.*)=(".*")$"#).unwrap();
72    let gen_path = Path::new(&outdir).join("kconfig.rs");
73
74    let mut f = File::create(&gen_path).unwrap();
75
76    let file = File::open(&dotconfig).expect("Unable to open dotconfig");
77    for line in BufReader::new(file).lines() {
78        let line = line.expect("reading line from dotconfig");
79        if let Some(caps) = config_hex.captures(&line) {
80            writeln!(&mut f, "#[allow(dead_code)]").unwrap();
81            writeln!(&mut f, "pub const {}: usize = {};", &caps[1], &caps[2]).unwrap();
82        } else if let Some(caps) = config_int.captures(&line) {
83            writeln!(&mut f, "#[allow(dead_code)]").unwrap();
84            writeln!(&mut f, "pub const {}: isize = {};", &caps[1], &caps[2]).unwrap();
85        } else if let Some(caps) = config_str.captures(&line) {
86            writeln!(&mut f, "#[allow(dead_code)]").unwrap();
87            writeln!(&mut f, "pub const {}: &str = {};", &caps[1], &caps[2]).unwrap();
88        }
89    }
90}
91
92/// Parse the finalized DTS file, generating the Rust devicetree file.
93fn import_dt() -> DeviceTree {
94    let zephyr_dts = env::var("ZEPHYR_DTS").expect("ZEPHYR_DTS must be set");
95    let gen_include =
96        env::var("BINARY_DIR_INCLUDE_GENERATED").expect("BINARY_DIR_INCLUDE_GENERATED must be set");
97
98    let generated = format!("{}/devicetree_generated.h", gen_include);
99    DeviceTree::new(&zephyr_dts, generated)
100}
101
102pub fn build_dts() {
103    let dt = import_dt();
104
105    let outdir = env::var("OUT_DIR").expect("OUT_DIR must be set");
106    let out_path = Path::new(&outdir).join("devicetree.rs");
107    let mut out = File::create(&out_path).expect("Unable to create devicetree.rs");
108
109    let augments = env::var("DT_AUGMENTS").expect("DT_AUGMENTS must be set");
110    let augments: Vec<String> = augments.split_whitespace().map(String::from).collect();
111
112    // Make sure that cargo knows to run if this changes, or any file mentioned changes.
113    println!("cargo:rerun-if-env-changed=DT_AUGMENTS");
114    for name in &augments {
115        println!("cargo:rerun-if-changed={}", name);
116    }
117
118    let mut augs = Vec::new();
119    for aug in &augments {
120        // println!("Load augment: {:?}", aug);
121        let mut aug = devicetree::load_augments(aug).expect("Loading augment file");
122        augs.append(&mut aug);
123    }
124    // For now, just print it out.
125    // println!("augments: {:#?}", augs);
126    let augs: Vec<_> = augs
127        .into_iter()
128        .map(|aug| Box::new(aug) as Box<dyn Augment>)
129        .collect();
130
131    let tokens = dt.to_tokens(&augs);
132    if has_rustfmt() {
133        write_formatted(out, tokens);
134    } else {
135        writeln!(out, "{}", tokens).unwrap();
136    };
137}
138
139/// Generate cfg directives for each of the nodes in the generated device tree.
140///
141/// This assumes that build_dts was already run by the `zephyr` crate, which should happen if this
142/// is called from a user application.
143pub fn dt_cfgs() {
144    let dt = import_dt();
145    dt.output_node_paths(&mut std::io::stdout()).unwrap();
146}
147
148/// Determine if `rustfmt` is in the path, and can be excecuted. Returns false on any kind of error.
149pub fn has_rustfmt() -> bool {
150    matches!(Command::new("rustfmt").arg("--version").status(), Ok(st) if st.success())
151}
152
153/// Attempt to write the contents to a file, using rustfmt. If there is an error running rustfmt,
154/// print a warning, and then just directly write the file.
155fn write_formatted(file: File, tokens: TokenStream) {
156    let mut rustfmt = Command::new("rustfmt")
157        .args(["--emit", "stdout"])
158        .stdin(Stdio::piped())
159        .stdout(file)
160        .stderr(Stdio::inherit())
161        .spawn()
162        .expect("Failed to run rustfmt");
163    // TODO: Handle the above failing.
164
165    let mut stdin = rustfmt
166        .stdin
167        .as_ref()
168        .expect("Stdin should have been opened by spawn");
169    writeln!(stdin, "{}", tokens).expect("Writing to rustfmt");
170
171    match rustfmt.wait() {
172        Ok(st) if st.success() => (),
173        _ => panic!("Failure running rustfmt"),
174    }
175}