avr-device 0.8.0

Register access crate for AVR microcontrollers
Documentation
use anyhow::Context;
use std::{
    collections::{BTreeMap, HashSet},
    env,
    ffi::OsString,
    fs::{self, File},
    io::{self, Read},
    path::{Path, PathBuf},
};
use svd2rust::config::IdentFormats;
use svd_rs::Device;

fn main() -> anyhow::Result<()> {
    let mut cargo_directives = vec![];

    let crate_root = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").ok_or(anyhow::anyhow!(
        "env variable CARGO_MANIFEST_DIR is missing"
    ))?);
    let out_dir = PathBuf::from(
        env::var_os("OUT_DIR").ok_or(anyhow::anyhow!("env variable OUT_DIR is missing"))?,
    );

    let svd_generator =
        SvdGenerator::from_out_dir(&out_dir).context("failed to create SVD output directory")?;
    let code_generator = CodeGenerator::from_out_dir(&out_dir)
        .context("failed to create codegen output directory")?;

    let mcu_inputs = InputFinder::find(&crate_root, &mut cargo_directives)
        .context("failed finding and selecting MCU inputs")?;
    let mut svds = BTreeMap::new();
    for (mcu, inputs) in mcu_inputs.get_map() {
        let _ = svds.insert(
            mcu.as_str(),
            svd_generator
                .generate(mcu, inputs)
                .context(format!("failed to generate SVD for MCU {}", mcu))?,
        );
    }
    for (mcu, svd) in &svds {
        code_generator
            .generate_module(mcu, svd)
            .context(format!("failed to generate PAC code for MCU {}", mcu))?;
    }
    code_generator
        .generate_vector(&svds)
        .context("failed to generate interrupt vector definitions")?;

    for directive in cargo_directives {
        println!("{}", directive);
    }

    Ok(())
}

#[derive(Debug)]
struct McuInputs {
    atdf_path: PathBuf,
    atdf: atdf2svd::chip::Chip,
    patch: Option<yaml_rust2::Yaml>,
}

#[derive(Debug)]
struct InputFinder {
    inputs: BTreeMap<String, McuInputs>,
}

impl InputFinder {
    pub fn get_map(&self) -> &BTreeMap<String, McuInputs> {
        &self.inputs
    }

    pub fn find(crate_root: &Path, cargo_directives: &mut Vec<String>) -> anyhow::Result<Self> {
        let packs_dir = crate_root.join("vendor");
        let patches_dir = crate_root.join("patch");

        Self::track_path(&packs_dir, cargo_directives)?;
        Self::track_path(&patches_dir, cargo_directives)?;

        let mut inputs = BTreeMap::new();
        let mut all_mcus = Vec::new();
        for result in fs::read_dir(&packs_dir)
            .map_err(io_error_in_path(&packs_dir))
            .context("could not scan vendor/ directory")?
        {
            let entry = result
                .map_err(io_error_in_path(&packs_dir))
                .context("could not read entry from vendor/ directory")?;
            let atdf_path = entry.path();
            if atdf_path.extension() != Some(&OsString::from("atdf")) {
                continue;
            }
            let mcu_name = atdf_path
                .file_stem()
                .ok_or(anyhow::anyhow!(
                    "file with no stem in `{}`",
                    atdf_path.display()
                ))?
                .to_str()
                .ok_or(anyhow::anyhow!(
                    "file with non UTF-8 name in `{}`",
                    atdf_path.display()
                ))?
                .to_owned();
            all_mcus.push(mcu_name.clone());
            if env::var_os(format!("CARGO_FEATURE_{}", mcu_name.to_uppercase())).is_none() {
                continue;
            }

            let atdf_file = File::open(&atdf_path)
                .map_err(io_error_in_path(&atdf_path))
                .context("could not open ATDF file")?;
            let atdf = atdf2svd::atdf::parse(atdf_file, &HashSet::new())
                .map_err(atdf_error(&atdf_path))?;
            let patch_path = patches_dir.join(&mcu_name).with_extension("yaml");
            let patch = if patch_path
                .try_exists()
                .map_err(io_error_in_path(&patch_path))
                .context("could not test for patch path existence")?
            {
                Some(
                    svdtools::patch::load_patch(&patch_path)
                        .context("failed to parse patch YAML")?,
                )
            } else {
                None
            };
            inputs.insert(
                mcu_name,
                McuInputs {
                    atdf_path,
                    atdf,
                    patch,
                },
            );
        }
        if inputs.is_empty() {
            return Err(anyhow::anyhow!(
                "at least one MCU feature must be selected; choose from {}",
                all_mcus.join(", ")
            ))
            .context("no crate-features for MCUs were selected");
        }

        Ok(Self { inputs })
    }

    fn track_path(path: &Path, cargo_directives: &mut Vec<String>) -> anyhow::Result<()> {
        if !path
            .try_exists()
            .map_err(io_error_in_path(path))
            .context("could not test for path existence")?
        {
            return Err(anyhow::anyhow!(
                "required path `{}` is missing",
                path.display()
            ));
        } else if path.is_symlink() {
            return Err(anyhow::anyhow!(
                "required path `{}` being a symlink is invalid",
                path.display()
            ));
        }

        if path.is_dir() {
            for result in fs::read_dir(path)
                .map_err(io_error_in_path(path))
                .context("failed to scan directory")?
            {
                let subpath = result
                    .map_err(io_error_in_path(path))
                    .context("failed to read scanned directory entry")?
                    .path();
                Self::track_path(&subpath, cargo_directives)?;
            }
        }

        cargo_directives.push(format!(
            "cargo::rerun-if-changed={}",
            path.to_str().ok_or(anyhow::anyhow!(
                "file with non UTF-8 name in `{}`",
                path.display()
            ))?
        ));

        Ok(())
    }
}

#[derive(Debug)]
struct SvdGenerator {
    unpatched_svd: PathBuf,
    patched_svd: PathBuf,
}

impl SvdGenerator {
    pub fn from_out_dir(out_dir: &Path) -> anyhow::Result<Self> {
        let svd_dir = out_dir.join("svd");
        let unpatched_svd = svd_dir.join("unpatched");
        let patched_svd = svd_dir.join("patched");

        ensure_out_dir(&unpatched_svd)?;
        ensure_out_dir(&patched_svd)?;

        Ok(Self {
            unpatched_svd,
            patched_svd,
        })
    }

    pub fn generate(&self, mcu: &str, inputs: &McuInputs) -> anyhow::Result<Device> {
        let unpatched_svd_path = self.unpatched_svd.join(mcu).with_extension("svd");
        let unpatched_writer = File::create(&unpatched_svd_path)
            .map_err(io_error_in_path(&unpatched_svd_path))
            .context("failed to create file for unpatched SVD")?;
        atdf2svd::svd::generate(&inputs.atdf, &unpatched_writer)
            .map_err(atdf_error(&inputs.atdf_path))
            .context("failed to generate SVD from ATDF")?;
        let unpatched_reader = File::open(&unpatched_svd_path)
            .map_err(io_error_in_path(&unpatched_svd_path))
            .context("failed to open unpatched SVD for reading")?;
        let patched_svd_path = self.patched_svd.join(mcu).with_extension("svd");
        let svd_path = if let Some(patch) = &inputs.patch {
            let mut reader = svdtools::patch::process_reader(
                unpatched_reader,
                patch,
                &Default::default(),
                &Default::default(),
            )
            .context("failed to start SVD patcher")?;
            let mut b = Vec::new();
            reader
                .read_to_end(&mut b)
                .map_err(io_error_in_path(&patched_svd_path))
                .context("failed to read patched SVD from patcher process")?;

            fs::write(&patched_svd_path, b)
                .map_err(io_error_in_path(&patched_svd_path))
                .context("failed to write patched SVD")?;
            patched_svd_path
        } else {
            unpatched_svd_path
        };

        let svd_str = fs::read_to_string(&svd_path)
            .map_err(io_error_in_path(&svd_path))
            .context("failed to read final SVD")?;

        svd2rust::load_from(&svd_str, &svd2rust::Config::default())
            .context("failed to parse SVD with svd2rust")
    }
}

#[derive(Debug)]
struct CodeGenerator {
    module: PathBuf,
}

impl CodeGenerator {
    pub fn generate_module(&self, mcu: &str, svd: &Device) -> anyhow::Result<()> {
        let mut svd2rust_config = svd2rust::Config::default();
        svd2rust_config.target = svd2rust::Target::None;
        svd2rust_config.generic_mod = true;
        svd2rust_config.make_mod = true;
        svd2rust_config.strict = true;
        svd2rust_config.output_dir = Some(self.module.clone());
        svd2rust_config.skip_crate_attributes = true;
        svd2rust_config.reexport_core_peripherals = false;
        svd2rust_config.reexport_interrupt = false;
        svd2rust_config.ident_formats = IdentFormats::legacy_theme();

        let generated_stream =
            svd2rust::generate::device::render(svd, &svd2rust_config, &mut String::new())
                .context("failed to generate svd2rust module")?;

        let mut syntax_tree: syn::File = syn::parse2(generated_stream)
            .map_err(|e| anyhow::anyhow!("{}", e))
            .context("failed to parse svd2rust module code")?;

        self.patch_device_peripherals_singleton(&mut syntax_tree)
            .context("failed to patch svd2rust module code")?;

        let formatted = prettyplease::unparse(&syntax_tree);

        let module_path = self.module.join(mcu).with_extension("rs");
        fs::write(&module_path, &formatted)
            .map_err(io_error_in_path(&module_path))
            .context("failed to write svd2rust module code to file")
    }

    fn patch_device_peripherals_singleton(
        &self,
        syntax_tree: &mut syn::File,
    ) -> anyhow::Result<()> {
        for item in syntax_tree.items.iter_mut() {
            if let syn::Item::Static(statik) = item {
                if statik.ident == "DEVICE_PERIPHERALS" {
                    *item = syn::parse_quote! {use super::DEVICE_PERIPHERALS;};
                    return Ok(());
                }
            }
        }
        Err(anyhow::anyhow!(
            "Could not find `DEVICE_PERIPHERALS` static"
        ))
    }

    fn generate_vector(&self, devices: &BTreeMap<&str, Device>) -> anyhow::Result<()> {
        let mut specific_matchers = Vec::new();
        for (mcu, device) in devices {
            for p in &device.peripherals {
                for i in &p.interrupt {
                    specific_matchers.push(format!(
                        r#"(@{0}, {1}, $it:item) => {{
        #[export_name = "__vector_{2}"]
        $it
    }};"#,
                        mcu, i.name, i.value,
                    ));
                }
            }
        }

        let content = format!(
            r#"#[doc(hidden)]
#[macro_export]
macro_rules! __avr_device_trampoline {{
    {}
    (@$mcu:ident, $name:ident, $it:item) => {{
        compile_error!(concat!("Couldn't find interrupt ", stringify!($name), ", for MCU ", stringify!($mcu), "."));
    }}
}}
"#,
            specific_matchers.join("\n    "),
        );
        let vector_path = self.module.join("vector.rs");
        fs::write(&vector_path, content)
            .map_err(io_error_in_path(&vector_path))
            .context("failed to write vector module to file")
    }

    pub fn from_out_dir(out_dir: &Path) -> anyhow::Result<Self> {
        let module = out_dir.join("pac");
        ensure_out_dir(&module).context("failed preparing PAC directory")?;
        Ok(Self { module })
    }
}

fn ensure_out_dir(dir: &Path) -> anyhow::Result<()> {
    if dir.is_file() || dir.is_symlink() {
        return Err(anyhow::anyhow!(
            "path `{}` not being a directory is invalid",
            dir.display()
        ));
    }
    if !dir.is_dir() {
        fs::create_dir_all(dir)
            .map_err(io_error_in_path(dir))
            .context("failed creating directory")?;
    }
    Ok(())
}

fn atdf_error(path: &Path) -> impl Fn(atdf2svd::Error) -> anyhow::Error {
    let path = path.to_owned();
    move |e: atdf2svd::Error| {
        let mut v = Vec::new();
        if let Err(e) = e.format(&mut v).map_err(io_error_in_path(&path)) {
            return e;
        }
        let s = v.utf8_chunks().fold(String::new(), |mut s, chunk| {
            s.push_str(chunk.valid());
            s
        });
        anyhow::anyhow!("{}", s)
    }
}

fn io_error_in_path(p: &Path) -> impl Fn(io::Error) -> anyhow::Error {
    let p = p.to_owned();
    move |e| anyhow::anyhow!("{}: {}", p.display(), e)
}