riot-sys 0.7.9

Rust FFI wrappers for the RIOT operating system
extern crate bindgen;
extern crate shlex;

use bindgen::builder;
use std::env;
use std::fmt::Write;
use std::path::PathBuf;

use serde_json::json;

fn main() {
    let cc;
    let mut cflags;

    #[cfg(not(feature = "riot-rs"))]
    if env::var("BUILDING_RIOT_RS").is_ok() {
        println!("");
        println!("ERROR: riot-sys seems to be built for RIOT-rs (BUILDING_RIOT_RS is set). Please enable the 'riot-rs' feature.");
        println!(
            "To do this, make the main application crate depend on `riot-sys` with feature `riot-rs`."
        );
        println!("");
        std::process::exit(1);
    }

    #[cfg(not(feature = "riot-rs"))]
    let compile_commands_json = "RIOT_COMPILE_COMMANDS_JSON";
    #[cfg(feature = "riot-rs")]
    let compile_commands_json = "DEP_RIOT_BUILD_COMPILE_COMMANDS_JSON";

    println!("cargo:rerun-if-env-changed=BUILDING_RIOT_RS");
    println!("cargo:rerun-if-env-changed=RIOT_CC");
    println!("cargo:rerun-if-env-changed=RIOT_CFLAGS");
    println!("cargo:rerun-if-env-changed={}", &compile_commands_json);

    if let Ok(commands_json) = env::var(compile_commands_json) {
        println!("cargo:rerun-if-changed={}", commands_json);
        let commands_file = std::fs::File::open(&commands_json)
            .expect(&format!("Failed to open {}", &commands_json));

        #[derive(Debug, serde::Deserialize)]
        struct Entry {
            arguments: Vec<String>,
        }
        let parsed: Vec<Entry> = serde_json::from_reader(commands_file)
            .expect(&format!("Failed to parse {}", &compile_commands_json));

        // We need to find a consensus list -- otherwise single modules like stdio_uart that
        // defines anything odd for its own purpose can throw things off. (It's not like the actual
        // ABI compatibility should suffer from them, for any flags like enum packing need to be
        // the same systemwide anyway for things to to go very wrong) -- but at any rate, finding
        // some consensus is to some extent necessary here).
        //
        // This is relatively brittle, but still better than the previous approach of just taking
        // the first entry.
        //
        // A good long-term solution might be to take CFLAGS as the build system produces them, but
        // pass them through the LLVMization process of create_compile_commands without actually
        // turning them into compile commands.
        let mut consensus_cc: Option<&str> = None;
        let mut consensus_cflag_groups: Option<Vec<Vec<&str>>> = None;
        for entry in parsed.iter() {
            if let Some(consensus_cc) = consensus_cc.as_ref() {
                assert!(consensus_cc == &entry.arguments[0])
            } else {
                consensus_cc = Some(&entry.arguments[0]);
            }
            let arg_iter = entry.arguments[1..]
                .iter()
                .map(|s| s.as_str())
                // Anything after -c is not CFLAGS but concrete input/output stuff.
                .take_while(|&s| s != "-c" && s != "-MQ");
            // Heuristically grouping them to drop different arguments as whole group
            let mut cflag_groups = vec![];
            for mut arg in arg_iter {
                if arg.starts_with("-I") {
                    // -I arguments are given inconsistently with and without trailing slashes;
                    // removing them keeps them from being pruned from the consensus set
                    arg = arg.trim_end_matches('/');
                }
                if arg.starts_with('-') {
                    cflag_groups.push(vec![arg]);
                } else {
                    cflag_groups
                        .last_mut()
                        .expect("CFLAG options all start with a dash")
                        .push(arg);
                }
            }
            if let Some(consensus_cflag_groups) = consensus_cflag_groups.as_mut() {
                if &cflag_groups != consensus_cflag_groups {
                    // consensus is in a good ordering, so we'll just strip it down
                    *consensus_cflag_groups = consensus_cflag_groups
                        .drain(..)
                        .filter(|i| {
                            let mut keep = cflag_groups.contains(i);
                            // USEMODULE_INCLUDES are sometimes not in all of the entries; see note
                            // on brittleness above.
                            keep |= i[0].starts_with("-I");
                            // Left as multiple lines to ease hooking in with debug statements when
                            // something goes wrong again...
                            keep
                        })
                        .collect();
                }
            } else {
                consensus_cflag_groups = Some(cflag_groups);
            }
        }
        cc = consensus_cc
            .expect("Entries are present in compile_commands.json")
            .to_string();
        cflags = shlex::join(consensus_cflag_groups.unwrap().iter().flatten().map(|s| *s));

        let usemodule = {
            #[cfg(not(feature = "riot-rs"))]
            {
                println!("cargo:rerun-if-env-changed=RIOT_USEMODULE");
                env::var("RIOT_USEMODULE").expect(&format!(
                    "RIOT_USEMODULE is required when {} is given",
                    &compile_commands_json,
                ))
            }
            #[cfg(feature = "riot-rs")]
            {
                println!("cargo:rerun-if-env-changed=DEP_RIOT_BUILD_DIR");
                let riot_builddir =
                    env::var("DEP_RIOT_BUILD_DIR").expect("DEP_RIOT_BUILD_DIR unset?");
                get_riot_var(&riot_builddir, "USEMODULE")
            }
        };

        for m in usemodule.split(" ") {
            // Hack around https://github.com/RIOT-OS/RIOT/pull/16129#issuecomment-805810090
            write!(
                cflags,
                " -DMODULE_{}",
                m.to_uppercase()
                    // avoid producing MODULE_BOARDS_COMMON_SAMDX1-ARDUINO-BOOTLOADER
                    .replace('-', "_")
            )
            .unwrap();
        }
    } else {
        cc = env::var("RIOT_CC")
            .expect("Please pass in RIOT_CC; see README.md for details.")
            .clone();
        cflags = env::var("RIOT_CFLAGS")
            .expect("Please pass in RIOT_CFLAGS; see README.md for details.");
    }

    // pass CC and CFLAGS to dependees
    // this requires a `links = "riot-sys"` directive in Cargo.toml.
    // Dependees can then access these as DEP_RIOT_SYS_CC and DEP_RIOT_SYS_CFLAGS.
    println!("cargo:CC={}", &cc);
    println!("cargo:CFLAGS={}", &cflags);

    println!("cargo:rerun-if-changed=riot-bindgen.h");

    let cflags = shlex::split(&cflags).expect("Odd shell escaping in RIOT_CFLAGS");
    let cflags: Vec<String> = cflags
        .into_iter()
        .filter(|x| {
            match x.as_ref() {
                // These will be in riotbuild.h as well, and better there because bindgen emits
                // consts for data from files but not from defines (?)
                x if x.starts_with("-D") => false,
                // Don't pollute the riot-sys source directory -- cargo is run unconditionally
                // in the Makefiles, and this script tracks on its own which files to depend on
                // for rebuilding.
                "-MD" => false,
                // accept all others
                _ => true,
            }
        })
        .collect();

    let bindings = builder()
        .header("riot-bindgen.h")
        .clang_args(&cflags)
        .use_core()
        .ctypes_prefix("libc")
        .impl_debug(true)
        // Structs listed here are Packed and thus need impl_debug, but also contain non-Copy
        // members.
        //
        // This is a workaround for <https://github.com/rust-lang/rust-bindgen/issues/2221>; once
        // that is fixed and our bindgen is updated, these can just go away again.
        //
        // If you see any errors like
        //
        // ```
        // error: reference to packed field is unaligned
        //      --> .../out/bindings.rs:79797:13
        //       |
        // 79797 |             self.opcode, self.length, self.data
        //       |             ^^^^^^^^^^^
        //       |
        //       = note: `#[deny(unaligned_references)]` on by default
        //       = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
        //       = note: for more information, see issue #82523 <https://github.com/rust-lang/rust/issues/82523>
        //       = note: fields of packed structs are not properly aligned, and creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
        //       = help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers)
        //       = note: this error originates in the macro `$crate::format_args` (in Nightly builds, run with -Z macro-backtrace for more info)
        // ```
        //
        // please add the offending struct in here; if existing code depends on the Debug
        // implementation, you may add a Debug implementation (that possibly is just a dummy, for
        // in these cases it *is* hard to implement showing all details) to this crate for the
        // duration of these workarounds.
        .no_debug("ble_hci_cmd")
        .no_debug("ble_hci_ev_command_complete")
        .no_debug("ble_hci_ev_le_subev_big_complete")
        .no_debug("ble_hci_ev_le_subev_big_sync_established")
        .no_debug("ble_hci_ev_le_subev_periodic_adv_rpt")
        .no_debug("ext_adv_report")
        .derive_default(true)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
    // Store for inspection for markers; see there
    let mut bindgen_output = Vec::<u8>::new();
    bindings
        .write(Box::new(&mut bindgen_output))
        .expect("String writing never fails");
    let bindgen_output = std::str::from_utf8(&bindgen_output).expect("Rust source code is UTF-8");

    // Build a compile_commands.json, and run C2Rust
    //
    // The output is cleared beforehand (for c2rust no-ops when an output file is present), and the
    // input is copied to OUT_DIR as that's the easiest way to get c2rust to put the output file in
    // a different place -- and because some additions are generated anyway.

    let c2rust_infile = "riot-c2rust.h";
    // Follows from c2rust_infile and C2Rust's file name translation scheme
    let c2rust_output = out_path.join("riot_c2rust.rs");
    let headercopy = out_path.join(c2rust_infile);
    println!("cargo:rerun-if-changed=riot-c2rust.h");

    std::fs::copy("riot-headers.h", out_path.join("riot-headers.h"))
        .expect("Failed to copy over header file");

    // These constant initializers are unusable without knowledge of which type they're for; adding
    // the information here to build explicit consts
    let macro_functions = [
        ("SOCK_IPV4_EP_ANY", "sock_udp_ep_t", None, true),
        ("SOCK_IPV6_EP_ANY", "sock_udp_ep_t", None, true),
        ("MUTEX_INIT", "mutex_t", None, true),
        // neither C2Rust nor bindgen understand the cast without help
        ("STATUS_NOT_FOUND", "thread_status_t", None, true),
        // If any board is ever added that works completely differently, this'll have to go behind
        // a feature-gate
        (
            "GPIO_PIN",
            "gpio_t",
            Some("unsigned port, unsigned pin"),
            // would be nice to have them const, but on boards like samd21-xpro that'd require
            // several nightly features (const_ptr_offset, const_mut_refs).
            false,
        ),
    ];
    let mut macro_functions: Vec<_> = macro_functions
        .iter()
        .map(|(macro_name, return_type, args, is_const)| {
            (macro_name.to_string(), *return_type, *args, *is_const)
        })
        .collect();
    for i in 0..8 {
        macro_functions.push((format!("LED{}_ON", i), "void", None, false));
        macro_functions.push((format!("LED{}_OFF", i), "void", None, false));
        macro_functions.push((format!("LED{}_TOGGLE", i), "void", None, false));
    }

    let mut c_code = String::new();
    std::fs::File::open("riot-c2rust.h")
        .expect("Failed to open riot-c2rust.h")
        .read_to_string(&mut c_code)
        .expect("Failed to read riot-c2rust.h");

    for (macro_name, return_type, args, _is_const) in macro_functions.iter() {
        let argnames = match args {
            None => "".to_string(),
            Some("void") => "()".to_string(),
            Some(args) => format!(
                "({})",
                args.split(", ")
                    .map(|s| &s[s.find(" ").expect("Non-void args need to have names")..])
                    // Not really essential concepturally, but .join is only available on
                    // slices, not on Iterator
                    .collect::<Vec<&str>>()
                    .join(", ")
            ),
        };

        // The ifdef guards make errors easier to spot: A "cannot find function
        // `macro_SOCK_IPV6_EP_ANY` in crate `riot_sys`" can lead one to check whether
        // SOCK_IPV6_EP_ANY is really defined, whereas if the macro is missing, C2Rust would
        // produce a run-time panic, and the compiler would reject that in a const function.
        //
        // This is more reliable than the previous approach of trying to defined a `-DSOME_MODULE`
        // condition, also because there may not even be a module that gives a precise condition.
        if *return_type == "void" {
            // in C, assigning and returning void is special
            write!(
                c_code,
                r"

#ifdef {macro_name}
{return_type} macro_{macro_name}({args}) {{
    {macro_name}{argnames};
}}
#endif
                ",
                return_type = return_type,
                macro_name = macro_name,
                args = args.unwrap_or("void"),
                argnames = argnames,
            )
        } else {
            write!(
                c_code,
                r"

#ifdef {macro_name}
{return_type} macro_{macro_name}({args}) {{
    {return_type} result = {macro_name}{argnames};
    return result;
}}
#endif
                ",
                return_type = return_type,
                macro_name = macro_name,
                args = args.unwrap_or("void"),
                argnames = argnames,
            )
        }
        .unwrap();
    }

    let mut outfile =
        std::fs::File::create(&headercopy).expect("Failed to open temporary riot-c2rust.h");
    outfile
        .write_all(c_code.as_bytes())
        .expect("Failed to write to riot-c2rust.h");
    outfile
        .sync_all()
        .expect("failed to write to riot-c2rust.h");

    if cc.find("clang") == None {
        panic!("riot-sys only accepts clang style CFLAGS. RIOT can produce them using the compile_commands tool even when using a non-clang compiler, such as GCC.");
    };

    let arguments: Vec<_> = core::iter::once("any-cc".to_string())
        .chain(cflags.into_iter())
        .chain(core::iter::once(c2rust_infile.to_string()))
        .collect();
    let compile_commands = json!([{
        "arguments": arguments,
        "directory": out_path,
        "file": c2rust_infile,
    }]);
    let compile_commands_name = out_path.join("compile_commands.json");

    let mut compile_commands_file = std::fs::File::create(compile_commands_name.clone())
        .expect("Failed to create compile_commands.json");
    serde_json::to_writer_pretty(&mut compile_commands_file, &compile_commands)
        .expect("Failed to write to compile_commands.json");
    compile_commands_file
        .sync_all()
        .expect("Failed to write to compile_commands.json");

    let compile_commands_name = compile_commands_name
        .to_str()
        .expect("Inexpressible path name");

    println!("cargo:rerun-if-env-changed=C2RUST");
    println!("cargo:rerun-if-env-changed=PATH");
    let c2rust = std::env::var("C2RUST").unwrap_or_else(|_| "c2rust".to_string());
    let c2rust_version = std::process::Command::new(&c2rust)
        .args(&["--version"])
        .output()
        .expect("C2Rust version check did not complete")
        .stdout;
    let c2rust_version = String::from_utf8_lossy(&c2rust_version);
    print!("C2Rust binary {}, version: {}", c2rust, c2rust_version);
    // FIXME: This does not rat on the used files. Most are probably included from riot-bindgen.h
    // anyway, tough.
    println!("Running C2Rust on {}", compile_commands_name);
    let status = std::process::Command::new(&c2rust)
        .args(&[
            "transpile",
            compile_commands_name,
            "--preserve-unused-functions",
            "--emit-modules",
            "--emit-no-std",
            "--translate-const-macros",
            "--overwrite-existing",
            "--fail-on-error",
        ])
        .status()
        .expect("C2Rust failed");
    if !status.success() {
        println!(
            "cargo:warning=C2Rust failed with error code {}, exiting",
            status
        );
        std::process::exit(status.code().unwrap_or(1));
    }

    // Some fix-ups to the C2Rust output
    // (could just as well call sed...)

    use std::io::{Read, Write};

    let mut rustcode = String::new();
    std::fs::File::open(c2rust_output)
        .expect("Failed to open riot_c2rust.rs")
        .read_to_string(&mut rustcode)
        .expect("Failed to read from riot_c2rust.rs");

    rustcode = rustcode.replace("use ::libc;\n", "");

    if !c2rust_version.contains("+git-for-riot") && c2rust_version.contains("C2Rust 0.15") {
        // Old C2Rust still generate old-style ASM -- workaround for https://github.com/immunant/c2rust/issues/306
        rustcode = rustcode.replace(" asm!(", " llvm_asm!(");
    }

    // Workaround for https://github.com/immunant/c2rust/issues/372
    rustcode = rustcode.replace("::core::intrinsics::", "crate::intrinsics_replacements::");

    println!("cargo:rerun-if-env-changed=CARGO_FEATURE_KEEP_EXTERN_TYPES");
    if env::var("CARGO_FEATURE_KEEP_EXTERN_TYPES").is_err() {
        // There's only one `pub type` usually, and that breaks use on stable, and src/inline.rs has a
        // workaround for that
        rustcode = rustcode.replace("\n    pub type __locale_t;", "");
        rustcode = rustcode.replace("\n    pub type _IO_wide_data;", "");
        rustcode = rustcode.replace("\n    pub type _IO_codecvt;", "");
        rustcode = rustcode.replace("\n    pub type _IO_marker;", "");
        rustcode = rustcode.replace("\n    pub type __lock;", "");
    }

    // Replace the function declarations with ... usually something pub, but special considerations
    // may apply
    let mut rustcode_functionsreplaced = String::new();
    let function_original_prefix = r#"unsafe extern "C" fn "#;
    let mut functionchunks = rustcode.split(function_original_prefix);
    rustcode_functionsreplaced.push_str(
        functionchunks
            .next()
            .expect("Split produces at least a hit"),
    );

    for chunk in functionchunks {
        let funcname = &chunk[..chunk.find('(').expect("Function has parentheses somewhere")];
        let macro_details = if funcname.len() > 5 && &funcname[..6] == "macro_" {
            macro_functions
                .iter()
                .filter(|(macro_name, _, _, _)| &funcname[6..] == *macro_name)
                .next()
        } else {
            None
        };
        let new_prefix = match (funcname, macro_details) {
            // used as a callback, therefore does need the extern "C" -- FIXME probably worth a RIOT issue
            ("_evtimer_msg_handler" | "_evtimer_mbox_handler", _) => function_original_prefix,

            // Assigned by CMSIS to the const that is being overridden and thus needs its original
            // "C" type; see also riot-c2rust.h. (Actually using it would cause a linker error
            // anyway).
            ("__masked_builtin_arm_get_fpscr" | "__masked_builtin_arm_set_fpscr", _) => {
                function_original_prefix
            }

            // same problem but from C2Rust's --translate-const-macros
            ("__NVIC_SetPriority", _) => function_original_prefix,

            // As below (no need for extern), and they are const as declared ni the macro_functions
            // list.
            (_, Some((_, _, _, is_const))) => {
                // No "pub" because that's already a "pub" in front of it, they were never static
                match is_const {
                    // FIXME: These should be unsafe -- just because most of them are const doesn't
                    // necessrily mean they're safe (just the first few happened to be, but that's
                    // not this crate's place to assert)
                    true => "const unsafe fn ",
                    false => "unsafe fn ",
                }
            }

            // C2Rust transpiles these into Rust with conflicting lifetimes, see
            // https://github.com/immunant/c2rust/issues/309
            //
            // Simply disabling them here because they aren't used by any other inline code (and
            // will, when the manual llvm_asm to asm changes are added to riot-sys, not have manual
            // asm conversions on top of that).
            ("__SMLALD" | "__SMLALDX" | "__SMLSLD" | "__SMLSLDX", _) => {
                "#[cfg(c2rust_fixed_309)]\npub unsafe fn "
            }

            // The rest we don't need to call through the extern convention, but let's please make
            // them pub to be usable
            _ => "pub unsafe fn ",
        };
        rustcode_functionsreplaced.push_str(new_prefix);
        rustcode_functionsreplaced.push_str(chunk);
    }

    rustcode = rustcode_functionsreplaced;

    let output_replaced = out_path.join("riot_c2rust_replaced.rs");
    std::fs::File::create(output_replaced)
        .expect("Failed to create riot_c2rust_replaced.rs")
        .write(rustcode.as_bytes())
        .expect("Failed to write to riot_c2rust_replaced.rs");

    // Pub uses of inline right into the main lib.rs
    //
    // This is primarily for things that can really come from either backend (eg. irq functions
    // that are regular on native but static inline on others), and for convenience stuff like
    // macro_.
    //
    // Some functions are also in because they're innocuous enough.
    //
    // If (eg. on some platform but not on others) any function here is not an inline function,
    // that does not hurt; the entry doesn't do anything on these then. (But it is especially
    // valuable, as it ensures that on the *other* platforms it's still available with the same
    // Rust name).
    let mut toplevel_from_inline: Vec<String> = [
        "bluetil_ad_add_flags",
        "coap_get_code_raw",
        "coap_get_total_hdr_len",
        "gnrc_netapi_dispatch_send",
        "gnrc_netif_ipv6_addrs_get",
        "gnrc_netreg_entry_init_pid",
        "gpio_is_valid",
        "irq_disable",
        "irq_is_enabled",
        "irq_is_in",
        "irq_restore",
        "mutex_trylock",
        "mutex_lock",
        "pid_is_valid",
        "shell_run_forever",
        "sock_udp_recv",
        "sock_udp_send",
        "thread_get",
        "thread_getpid",
        "thread_get_unchecked",
        "ztimer_spin",
        // because when defined through RIOT's af.h these are enums and thus unix_af_t prefixed.
        "AF_UNSPEC",
        "AF_UNIX",
        "AF_PACKET",
        "AF_INET",
        "AF_INET6",
    ]
    .iter()
    .map(|name| name.to_string())
    .collect();
    for (macro_name, _, _, _) in macro_functions.iter() {
        toplevel_from_inline.push(format!("macro_{}", macro_name));
    }
    let toplevel_from_inline: Vec<String> = toplevel_from_inline
        .drain(..)
        .filter(|s: &String| {
            // Not just matching on `pub fn`: `irq_disable` on native is visible as `extern "C" {
            // fn irq_disable(); }`, and that should not trigger going through C2Rust.
            rustcode.contains(&format!("pub fn {}(", s))
                || rustcode.contains(&format!("unsafe fn {}(", s))
                || rustcode.contains(&format!(" {}: ", s))
        })
        .collect();
    let toplevel_from_inline_filename = out_path.join("toplevel_from_inline.rs");
    std::fs::File::create(toplevel_from_inline_filename)
        .expect("Failed to create toplevel_from_inline.rs")
        .write(
            format!(
                "
               pub use inline::{{ {} }};
           ",
                toplevel_from_inline.join(",\n")
            )
            .as_bytes(),
        )
        .expect("Failed to write to toplevel_from_inline.rs");

    // Some decisions of downstream crates need to depend on whether some feature is around in
    // RIOT. For many things that's best checked on module level, but some minor items have no
    // module to mark the feature, and checking for versions by numers is not fine-grained enough,
    // so it's easiest to check for concrete strings in the bindgen output.
    //
    // These lead to `MARKER_foo=1` items emitted that are usable as `DEP_RIOT_SYS_MARKER_foo=1` by
    // crates that explicitly `links = "riot-sys"`. They are stable in that they'll only go away in
    // a breaking riot-sys version; downstream users likely stop using them earlier because they
    // sooner or later stop supporting old RIOT versions.
    //
    // These markers are currently checked against bindgen's output, but could query any property
    // that riot-sys has access to. The markers are defined in terms of some change having happened
    // in RIOT; the way they are tested for can change. (In particular, when riot-sys stops
    // supporting an older RIOT version, it can just always emit that marker).
    //
    // Crates building on this should preferably not alter their own APIs depending on these,
    // because that would add extra (and hard-to-track) dimensions to them. If they can, they
    // should provide a unified view and degrade gracefully. (For example, riot-wrappers has the
    // unit `T` of the `phydat_unit_t` in its enum, but converts it to the generic unspecified unit
    // on RIOT versions that don't have the T type yet -- at least for as long as it supports
    // 2022.01).

    enum MarkerCondition {
        /// This has been around for long enough that no actual check is performed any more, the
        /// marker is just always set. Markers are set to that when the oldest supported RIOT
        /// version has the new behavior; users of riot-sys may stop checking for the marker when
        /// they depend on a riot-sys version that has it on Always.
        Always,
        /// A marker that has been around for some time during while preparing some PRs, but never
        /// was merged, and the PR was abandoned.
        ///
        /// This is equivalent to not having the marker in the first place, except that their
        /// presence serves as a reminder to not reuse that marker name.
        Never,
        /// A marker that is set if the given string is found in the bindgen output.
        InCode(&'static str),
        /// A marker that is set if its name is found in the bindgen output. Shorthand for
        /// Text(name).
        NameInCode,
    }

    use MarkerCondition::*;

    let markers = [
        // See https://github.com/RIOT-OS/RIOT/pull/17569, available after 2022.01
        (Always, "phydat_unit_t"),
        // See https://github.com/RIOT-OS/RIOT/pull/17660, available after 2022.01
        (Always, "vfs_iterate_mount_dirs"),
        // See https://github.com/RIOT-OS/RIOT/pull/17758 retrofitting it for the change in
        // https://github.com/RIOT-OS/RIOT/pull/17351, available in 2022.04
        (Always, "ztimer_periodic_callback_t"),
        // Experimental markers
        //
        // These are not merged in RIOT yet, but promising candidates; if there are any substantial
        // changes to them, their marker name will be bumped, but it is expected that they will be
        // moved up and get an "available after" release once merged.

        // See https://github.com/RIOT-OS/RIOT/pull/17544
        (Never, "coap_build_pkt_t"),
        (Never, "gcoap_resource_t"),
        // See https://github.com/RIOT-OS/RIOT/pull/17957, available TBD
        (NameInCode, "coap_request_ctx_t"),
    ];
    for (needle, name) in markers {
        let found = match needle {
            InCode(s) => bindgen_output.contains(s),
            NameInCode => bindgen_output.contains(name),
            Always => true,
            Never => false,
        };
        if found {
            println!("cargo:MARKER_{}=1", name);
        }
    }

    // let downstream crates know we're building for riot-rs
    #[cfg(feature = "riot-rs")]
    println!("cargo:MARKER_riot_rs=1");
}

#[cfg(feature = "riot-rs")]
fn get_riot_var(riot_builddir: &str, var: &str) -> String {
    let output = std::process::Command::new("sh")
        .arg("-c")
        .arg(format!(
            "{} make --no-print-directory -C {} TOOLCHAIN=llvm info-debug-variable-{}",
            "WARNING_EXTERNAL_MODULE_DIRS=0", riot_builddir, var
        ))
        .output()
        .unwrap()
        .stdout;
    String::from_utf8_lossy(output.as_slice()).trim_end().into()
}