use std::collections::BTreeSet;
use std::path::Path;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
const SOURCE: &str = "src/android/framework.txt";
struct Attr<'a> {
id: u32,
name: &'a str,
format: u32,
manifest: bool,
symbols: Vec<(&'a str, u32)>,
}
struct Resource<'a> {
ty: &'a str,
id: u32,
name: &'a str,
}
fn main() {
println!("cargo::rerun-if-changed={SOURCE}");
let text = std::fs::read_to_string(SOURCE).unwrap_or_else(|error| panic!("{SOURCE}: {error}"));
let (api_level, mut attrs, mut resources) = parse(&text);
attrs.sort_by_key(|attr| attr.name);
resources.sort_by_key(|resource| (resource.ty, resource.name));
assert!(attrs.windows(2).all(|w| w[0].name < w[1].name), "{SOURCE} names an attribute twice");
assert!(
resources.windows(2).all(|w| (w[0].ty, w[0].name) < (w[1].ty, w[1].name)),
"{SOURCE} names a resource twice"
);
let ids: BTreeSet<u32> = attrs.iter().map(|a| a.id).chain(resources.iter().map(|r| r.id)).collect();
assert_eq!(ids.len(), attrs.len() + resources.len(), "{SOURCE} uses a resource ID twice");
let out = Path::new(&std::env::var_os("OUT_DIR").expect("cargo sets OUT_DIR")).join("framework.rs");
std::fs::write(out, generate(api_level, &attrs, &resources).to_string()).expect("write the generated tables");
}
fn malformed(number: usize, line: &str) -> ! {
panic!("{SOURCE}:{}: malformed line {line:?}", number + 1)
}
fn parse(text: &str) -> (u32, Vec<Attr<'_>>, Vec<Resource<'_>>) {
let mut api_level = None;
let mut attrs = Vec::new();
let mut resources = Vec::new();
for (number, line) in text.lines().enumerate() {
let hex = |field: Option<&str>| {
let value = field.and_then(|f| u32::from_str_radix(f.strip_prefix("0x")?, 16).ok());
value.unwrap_or_else(|| malformed(number, line))
};
let mut fields = line.split_ascii_whitespace();
match fields.next() {
None => {}
Some(comment) if comment.starts_with('#') => {}
Some("api") => {
api_level =
Some(fields.next().and_then(|level| level.parse().ok()).unwrap_or_else(|| malformed(number, line)));
}
Some("attr") => {
let id = hex(fields.next());
let name = fields.next().unwrap_or_else(|| malformed(number, line));
let format = hex(fields.next());
let mut attr = Attr { id, name, format, manifest: false, symbols: Vec::new() };
for field in fields {
match field.split_once('=') {
Some((symbol, value)) => attr.symbols.push((symbol, hex(Some(value)))),
None if field == "manifest" => attr.manifest = true,
None => malformed(number, line),
}
}
attrs.push(attr);
}
Some(ty) => {
let id = hex(fields.next());
let name = fields.next().unwrap_or_else(|| malformed(number, line));
if fields.next().is_some() {
malformed(number, line);
}
resources.push(Resource { ty, id, name });
}
}
}
(api_level.unwrap_or_else(|| panic!("{SOURCE} has no `api` line")), attrs, resources)
}
fn constant_name(name: &str) -> String {
let mut out = String::with_capacity(name.len() + 8);
for (i, c) in name.char_indices() {
if c.is_ascii_uppercase() && i > 0 && !out.ends_with('_') {
out.push('_');
}
out.push(c.to_ascii_uppercase());
}
out
}
fn by_id(ids: impl Iterator<Item = u32>) -> Vec<u16> {
let mut indices: Vec<(u32, u16)> = ids
.enumerate()
.map(|(index, id)| (id, u16::try_from(index).expect("framework index exceeds 16 bits")))
.collect();
indices.sort();
indices.into_iter().map(|(_, index)| index).collect()
}
fn generate(api_level: u32, attrs: &[Attr<'_>], resources: &[Resource<'_>]) -> TokenStream {
let attr_defs = attrs.iter().map(|Attr { id, name, format, symbols, .. }| {
let symbols = symbols.iter().map(|(symbol, value)| quote!(Symbol::new(#symbol, #value)));
quote!(AttrDef::new(#name, ResourceId(#id), AttrFormat::from_bits_retain(#format), &[#(#symbols),*]))
});
let attrs_by_id = by_id(attrs.iter().map(|attr| attr.id));
let resource_tuples = resources.iter().map(|Resource { ty, id, name }| quote!((#ty, #name, ResourceId(#id))));
let resources_by_id = by_id(resources.iter().map(|resource| resource.id));
let (attr_count, resource_count) = (attrs.len(), resources.len());
let mut constants = BTreeSet::new();
let attr_constants = attrs.iter().enumerate().map(|(index, attr)| {
let constant = constant_name(attr.name);
assert!(constants.insert(constant.clone()), "attribute constant {constant} collides");
let constant = format_ident!("{constant}");
let usage = if attr.manifest { " (used in manifests)" } else { "" };
let doc = format!("`android:{}`{usage}", attr.name);
quote! {
#[doc = #doc]
pub static #constant: &AttrDef = &ATTRS[#index];
}
});
quote! {
pub const PLATFORM_API_LEVEL: u32 = #api_level;
pub(super) static ATTRS: [AttrDef; #attr_count] = [#(#attr_defs),*];
pub(super) static ATTRS_BY_ID: [u16; #attr_count] = [#(#attrs_by_id),*];
pub(super) static RESOURCES: [(&str, &str, ResourceId); #resource_count] = [#(#resource_tuples),*];
pub(super) static RESOURCES_BY_ID: [u16; #resource_count] = [#(#resources_by_id),*];
pub mod attr {
use super::{ATTRS, AttrDef};
#(#attr_constants)*
}
}
}