#!/usr/bin/env python3 import argparse, os, re, sys from typing import Dict, List, Tuple RE_BLOCK = re.compile( br'(?mi)^(#\s*begin[^\n]*build\s+properties[^\n]*\n)(.*?)(^#\s*end[^\n]*build\s+properties[^\n]*$)', re.DOTALL, ) BASE_CANONICAL: List[Tuple[str, bool]] = [ ("# begin build properties", True), ("# autogenerated by buildinfo.sh", True), ("ro.build.id", False), ("ro.build.display.id", False), ("ro.build.version.incremental", False), ("ro.build.version.sdk", False), ("ro.build.version.preview_sdk", False), ("ro.build.version.codename", False), ("ro.build.version.all_codenames", False), ("ro.build.version.release", False), ("ro.build.version.security_patch", False), ("ro.build.version.base_os", False), ("ro.build.date", False), ("ro.build.date.utc", False), ("ro.build.type", False), ("ro.build.user", False), ("ro.build.host", False), ("ro.build.tags", False), ("ro.build.flavor", False), ("ro.build.system_root_image", False), ("ro.build.ab_update", False), ("ro.product.model", False), ("ro.product.brand", False), ("ro.product.name", False), ("ro.product.device", False), ("ro.product.board", False), ("# ro.product.cpu.abi and ro.product.cpu.abi2 are obsolete,", True), ("# use ro.product.cpu.abilist instead.", True), ("ro.product.cpu.abi", False), ("ro.product.cpu.abi2", False), ("ro.product.cpu.abilist", False), ("ro.product.cpu.abilist32", False), ("ro.product.cpu.abilist64", False), ("ro.product.manufacturer", False), ("ro.product.locale", False), ("ro.wifi.channels", False), ("ro.board.platform", False), ("# ro.build.product is obsolete; use ro.product.device", True), ("ro.build.product", False), ("# Do not try to parse description, fingerprint, or thumbprint", True), ("ro.build.description", False), ("ro.build.fingerprint", False), ("ro.build.thumbprint", False), ("ro.build.characteristics", False), ("# end build properties", True), ] GENERIC_TOKENS = {"", "unknown", "generic", "aosp", "android", "android_device"} def is_generic(v: str) -> bool: s = (v or "").strip().lower() return s in GENERIC_TOKENS or s.startswith("generic-") or s == "test-keys" def parse_props(text: str) -> Dict[str, str]: d: Dict[str, str] = {} for raw in text.splitlines(): line = raw.strip() if not line or line.startswith("#") or "=" not in line: continue k, v = line.split("=", 1) d[k.strip()] = v.strip() return d def block_kind(header: str, body: str) -> str: h = (header + body).lower() return "common" if "common build properties" in h or "buildinfo_common.sh" in h else "base" def find_blocks(data: bytes): blocks = [] for m in RE_BLOCK.finditer(data): header = m.group(1).decode(errors="ignore") body = m.group(2).decode(errors="ignore") footer = m.group(3).decode(errors="ignore") text = (header + body + footer).replace("\r\n","\n") props = parse_props(text) blocks.append({"text": text, "props": props, "kind": block_kind(header, body)}) return blocks SOC_VENDOR_PATTERNS = { "mstar": ["mstar", "cv6a", "msd6a", "mst", "cva"], "mediatek":["mediatek", "mtk", "mt5", "mt6", "mt8"], "amlogic": ["amlogic", "s905", "s912", "s922", "a113", "g12"], "qualcomm":["qcom", "qualcomm", "msm", "sdm", "sm"], "hisilicon":["hisilicon", "kirin", "hi37", "hi38", "hi35"], "rockchip":["rockchip", "rk3", "rk33", "rk35", "px3", "px5"], } def detect_soc_vendor(props: Dict[str,str]) -> str: hay = " ".join([ props.get("ro.board.platform",""), props.get("ro.hardware",""), props.get("ro.soc.manufacturer",""), props.get("ro.soc.model",""), props.get("ro.product.board",""), ]).lower() for vendor, pats in SOC_VENDOR_PATTERNS.items(): if any(p in hay for p in pats): return vendor.capitalize() return props.get("ro.soc.manufacturer","").strip() FINGERPRINT_KEYS = [ "ro.build.fingerprint", "ro.system.build.fingerprint", "ro.product.build.fingerprint", "ro.vendor.build.fingerprint", "ro.odm.build.fingerprint", "ro.oem.build.fingerprint", "ro.bootimage.build.fingerprint", ] def first_non_generic(*vals: str) -> str: for v in vals: if v and not is_generic(v): return v return vals[0] if vals else "" def derive_company_from_fp(fp: str) -> str: if not fp or "/" not in fp: return "" head = fp.split("/", 1)[0].strip() return head[:1].upper() + head[1:] if head else "" def detect_form_factor(all_props: Dict[str,str]) -> str: text = " ".join([ all_props.get("ro.build.characteristics",""), all_props.get("ro.build.flavor",""), all_props.get("ro.build.fingerprint",""), all_props.get("ro.product.model",""), all_props.get("ro.product.name",""), all_props.get("ro.product.device",""), all_props.get("ro.product.system.model",""), all_props.get("ro.product.system.name",""), ]).lower() if any(t in text for t in [" tv", "/tv", "mitv", "atv", "dvb", "android-tv", "google/atv"]): return "tv" if "tablet" in text or "pad" in text or "tab" in text: return "tablet" if "watch" in text or "wear" in text: return "watch" if "automotive" in text or "car" in text or "ivi" in text: return "automotive" return "" def compose_props(base: Dict[str,str], commons: Dict[str,str], prefer_soc: bool, prefer_board_device: bool, force_ff: str|None) -> Dict[str,str]: merged = dict(commons) merged.update(base) out = dict(base) fp = base.get("ro.build.fingerprint","") if not fp or is_generic(fp) or "/" not in fp: out["ro.build.fingerprint"] = first_non_generic(*(commons.get(k,"") for k in FINGERPRINT_KEYS[1:])) or fp for tail in ["brand","manufacturer","model","device","name","board"]: dst = f"ro.product.{tail}" cur = base.get(dst, "") cand = first_non_generic( commons.get(f"ro.product.system.{tail}",""), commons.get(f"ro.product.product.{tail}",""), commons.get(f"ro.product.vendor.{tail}",""), commons.get(f"ro.product.odm.{tail}",""), commons.get(f"ro.product.oem.{tail}",""), merged.get(dst,""), cur, ) out[dst] = cand or cur if prefer_board_device: dev = out.get("ro.product.device","") if is_generic(dev) or dev in {"croods","android"}: repl = first_non_generic( merged.get("ro.product.board",""), merged.get("ro.soc.model",""), merged.get("ro.hardware",""), dev, ) out["ro.product.device"] = repl or dev fp_final = out.get("ro.build.fingerprint","") fp_brand = derive_company_from_fp(fp_final) soc_vendor = detect_soc_vendor(merged) if prefer_soc and soc_vendor: out["ro.product.brand"] = soc_vendor out["ro.product.manufacturer"] = soc_vendor else: if is_generic(out.get("ro.product.brand","")) and fp_brand: out["ro.product.brand"] = fp_brand if is_generic(out.get("ro.product.manufacturer","")) and fp_brand: out["ro.product.manufacturer"] = fp_brand if is_generic(out.get("ro.product.board","")): out["ro.product.board"] = first_non_generic( merged.get("ro.product.board",""), merged.get("ro.soc.model",""), merged.get("ro.board.platform",""), out.get("ro.product.device",""), ) if not out.get("ro.board.platform",""): out["ro.board.platform"] = first_non_generic( merged.get("ro.board.platform",""), merged.get("ro.hardware",""), merged.get("ro.soc.model",""), ) if force_ff: out["ro.build.characteristics"] = force_ff else: cur_ff = out.get("ro.build.characteristics","").strip().lower() if cur_ff in {"", "default", "nosdcard"}: auto = detect_form_factor({**merged, **out}) if auto: out["ro.build.characteristics"] = auto return out def render_canonical(props: Dict[str,str]) -> str: lines: List[str] = [] for token, is_comment in BASE_CANONICAL: lines.append(token if is_comment else f"{token}={props.get(token,'')}") return "\n".join(lines).strip() + "\n" def main(): ap = argparse.ArgumentParser(description="Compose canonical build.prop (fills brand/device/fingerprint/form-factor).") ap.add_argument("file") ap.add_argument("--outdir", default="out") ap.add_argument("--prefer-soc", action="store_true") ap.add_argument("--prefer-board-device", action="store_true") ap.add_argument("--form-factor", choices=["auto","tv","tablet","phone","watch","automotive"], default="auto") args = ap.parse_args() with open(args.file, "rb") as f: data = f.read() blocks = find_blocks(data) if not blocks: print("No build properties blocks found.") sys.exit(1) base = next((b for b in blocks if b["kind"]=="base"), blocks[0]) commons: Dict[str,str] = {} for b in blocks: if b["kind"] == "common": commons.update(b["props"]) force_ff = None if args.form_factor == "auto" else args.form_factor composed = compose_props(base["props"], commons, args.prefer_soc, args.prefer_board_device, force_ff) txt = render_canonical(composed) os.makedirs(args.outdir, exist_ok=True) outp = os.path.join(args.outdir, "best_build_composed.prop") with open(outp, "w", encoding="utf-8") as fo: fo.write(txt) print("Done:", outp) if __name__ == "__main__": main()