#!/usr/bin/env python3 # pylint: disable=redefined-outer-name,too-many-branches ''' Update autogenerated source files from yaml database. Copyright (c) 2019, vit9696 ''' import fnmatch import json import operator import os import sys import unicodedata import yaml import update_products def remove_accents(input_str): nfkd_form = unicodedata.normalize('NFKD', input_str) return ''.join([c for c in nfkd_form if not unicodedata.combining(c)]) def load_db(dbpath): ''' Load yaml database and return in a list. ''' if not os.path.exists(dbpath): print(f'Cannot find {dbpath} directory, rerun from AppleModels directory!') sys.exit(1) db = [] for root, _, files in os.walk(dbpath): for file in fnmatch.filter(files, '*.yaml'): path = os.path.join(root, file) with open(path, 'r', encoding='utf-8') as fh: try: r = yaml.safe_load(fh) if r.get('SystemProductName', None) is None: print(f'WARN: Missing SystemProductName in {path}, skipping!') continue db.append(r) except yaml.YAMLError as e: print(f'Failed to parse file {path} - {e}') sys.exit(1) if len(db) == 0: print('Empty database!') sys.exit(1) # Sorting is required for fast lookup. return sorted(db, key=operator.itemgetter('SystemProductName')) def gather_products(db, ptype='AppleModelCode', empty_valid=False, shared_valid=False, fatal=True): ''' Obtain all product codes from the database ''' products = [] for info in db: pp = info.get(ptype, None) if pp is None: continue for p in pp: if p == '': if not empty_valid: print(f"ERROR: {info['SystemProductName']} in contains empty {ptype}, skipping!") if fatal: sys.exit(1) continue if p in ['000', '0000']: print(f"WARN: {info['SystemProductName']} in contains zero {ptype}, skipping!") continue if p in products: if not shared_valid: print(f"ERROR: {info['SystemProductName']} shares {ptype} {p} with other model!") if fatal: sys.exit(1) continue products.append(p) return products def validate_products(db, dbpd): usedproducts = gather_products(db) # FIXME: Empty is not valid, but we let it be for now. gather_products(db, 'AppleBoardCode', True, True, False) knownproducts = dbpd for product in usedproducts: if knownproducts.get(product, None) is None: print(f'ERROR: Model {product} is used in DataBase but not present in Products!') sys.exit(1) if knownproducts[product][update_products.KEY_STATUS] != update_products.STATUS_OK: print(f'ERROR: Model {product} is used in DataBase but not valid in Products!') sys.exit(1) to_add = {} for product in knownproducts: if knownproducts[product][update_products.KEY_STATUS] != update_products.STATUS_OK: continue name = knownproducts[product][update_products.KEY_NAME] if name.find('Mac') < 0 and name.find('Xserve') < 0: continue if name.find('M1') >= 0: continue if len(product) > 3 and product not in usedproducts: print(f'WARN: Model {product} ({name}) is known but is not used in DataBase!') if to_add.get(name, None) is None: to_add[name] = [] to_add[name].append(product) continue if len(to_add) > 0: for sysname in to_add.items(): for info in db: if sysname in info['Specifications']['SystemReportName']: print(f"New AppleModelCode for {info['SystemProductName']}:") for model in to_add[sysname]: print(f" - \"{model}\"") def export_db_macinfolib(db, path, year=0): ''' Export yaml database to MacInfoLib format. TODO: use jinja2? ''' with open(path, 'w', encoding='utf-8') as fh: print('// DO NOT EDIT! This is an autogenerated file.', file=fh) print('#include "MacInfoInternal.h"', file=fh) print('CONST MAC_INFO_INTERNAL_ENTRY gMacInfoModels[] = {', file=fh) for info in db: if max(info['AppleModelYear']) < year: continue sb_model = info.get('AppleModelId') sb_model = f'"{sb_model.lower()}"' if sb_model else 'NULL' print( ' {\n' f''' .SystemProductName = "{info['SystemProductName']}",\n''' f''' .BoardProduct = "{info['BoardProduct'][0] if isinstance(info['BoardProduct'], list) else info['BoardProduct']}",\n''' f''' .BoardRevision = {f"0x{info['BoardRevision']:X}" if 'BoardRevision' in info else 'MAC_INFO_BOARD_REVISION_MISSING'},\n''' f''' .SmcRevision = {{{', '.join(map(str, info.get('SmcRevision', [0x00])))}}},\n''' f''' .SmcBranch = {{{', '.join(map(str, info.get('SmcBranch', [0x00])))}}},\n''' f''' .SmcPlatform = {{{', '.join(map(str, info.get('SmcPlatform', [0x00])))}}},\n''' f''' .BIOSVersion = "{info['BIOSVersion']}",\n''' f''' .BIOSReleaseDate = "{info['BIOSReleaseDate']}",\n''' f''' .SystemVersion = "{info['SystemVersion']}",\n''' f''' .SystemSKUNumber = "{info['SystemSKUNumber']}",\n''' f''' .SystemFamily = "{info['SystemFamily']}",\n''' f''' .BoardVersion = "{info['BoardVersion']}",\n''' f''' .BoardAssetTag = "{info['BoardAssetTag']}",\n''' f''' .BoardLocationInChassis = "{info['BoardLocationInChassis']}",\n''' f''' .SmcGeneration = 0x{info['SmcGeneration']:X},\n''' f''' .BoardType = 0x{info['BoardType']:X},\n''' f''' .ChassisType = 0x{info['ChassisType']:X},\n''' f''' .MemoryFormFactor = 0x{info['MemoryFormFactor']:X},\n''' f''' .PlatformFeature = {f"0x{info['PlatformFeature']:X}" if 'PlatformFeature' in info else 'MAC_INFO_PLATFORM_FEATURE_MISSING'},\n''' f''' .ChassisAssetTag = "{info['ChassisAssetTag']}",\n''' f''' .FirmwareFeatures = 0x{info.get('ExtendedFirmwareFeatures', info.get('FirmwareFeatures', 0)):X}ULL,\n''' f''' .FirmwareFeaturesMask = 0x{info.get('ExtendedFirmwareFeaturesMask', info.get('FirmwareFeaturesMask', 0)):X}ULL,\n''' f' .SecureBootModel = {sb_model},\n' f' }},', file=fh) print('};', file=fh) print('CONST UINTN gMacInfoModelCount = ARRAY_SIZE (gMacInfoModels);', file=fh) print('CONST UINTN gMacInfoDefaultModel = 0;', file=fh) def export_db_macserial(db, dbpd, path, year=0): ''' Export yaml database to macserial format. TODO: use jinja2? ''' with open(path, 'w', encoding='utf-8') as fh: print('#ifndef GENSERIAL_MODELINFO_AUTOGEN_H', file=fh) print('#define GENSERIAL_MODELINFO_AUTOGEN_H\n', file=fh) print('// DO NOT EDIT! This is an autogenerated file.\n', file=fh) print('#include "macserial.h"\n', file=fh) print('typedef enum {', file=fh) for info in db: print(f" {info['SystemProductName'].replace(',', '_')}, // {info['Specifications']['CPU'][0]}", file=fh) print('} AppleModel;\n', file=fh) print(f'#define APPLE_MODEL_MAX {len(db)}\n', file=fh) print('static PLATFORMDATA ApplePlatformData[] = {', file=fh) for info in db: print(f''' {{ "{info['SystemProductName']}", "{info['SystemSerialNumber']}" }},''', file=fh) print('};\n', file=fh) print(f"#define APPLE_MODEL_CODE_MAX {max(len(info['AppleModelCode']) for info in db)}", file=fh) print('static const char *AppleModelCode[][APPLE_MODEL_CODE_MAX] = {', file=fh) for info in db: print(f""" /* {info['SystemProductName']:14} */ {{"{'", "'.join(info['AppleModelCode'])}"}},""", file=fh) print('};\n', file=fh) print(f"#define APPLE_BOARD_CODE_MAX {max(len(info['AppleBoardCode']) for info in db)}", file=fh) print('static const char *AppleBoardCode[][APPLE_BOARD_CODE_MAX] = {', file=fh) for info in db: print(f""" /* {info['SystemProductName']:14} */ {{"{'", "'.join(info['AppleBoardCode'])}"}},""", file=fh) print('};\n', file=fh) print(f"#define APPLE_MODEL_YEAR_MAX {max(len(info['AppleModelYear']) for info in db)}", file=fh) print('static uint32_t AppleModelYear[][APPLE_MODEL_YEAR_MAX] = {', file=fh) for info in db: print(f" /* {info['SystemProductName']:14} */ {{{', '.join(str(year) for year in info['AppleModelYear'])}}},", file=fh) print('};\n', file=fh) print('static uint32_t ApplePreferredModelYear[] = {', file=fh) for info in db: print(f" /* {info['SystemProductName']:14} */ {info.get('MacserialModelYear', 0)},", file=fh) print('};\n', file=fh) print('static APPLE_MODEL_DESC AppleModelDesc[] = {', file=fh) models = sorted(dbpd.keys()) models.sort(key=len) for model in models: if dbpd[model][update_products.KEY_STATUS] == update_products.STATUS_OK: print(f""" {{"{model}", "{remove_accents(dbpd[model][update_products.KEY_NAME])}"}},""", file=fh) print('};\n', file=fh) print('#endif // GENSERIAL_MODELINFO_AUTOGEN_H', file=fh) def export_mlb_boards(db, boards): mlb = {} for info in db: if len(info['SystemSerialNumber']) == 12: models = [info['BoardProduct']] if not isinstance(info['BoardProduct'], list) else info['BoardProduct'] for model in models: mlb[model] = 'latest' if not info['MaximumOSVersion'] else info['MaximumOSVersion'] with open(boards, 'w', encoding='utf-8') as fh: json.dump(mlb, fh, indent=1) if __name__ == '__main__': db = load_db('DataBase') dbpd = update_products.load_products() # Run test phase to validate the library validate_products(db, dbpd) export_db_macinfolib(db, os.devnull) export_db_macserial(db, dbpd, os.devnull) # Export new models export_db_macinfolib(db, '../Library/OcMacInfoLib/AutoGenerated.c') export_db_macserial(db, dbpd, '../Utilities/macserial/modelinfo_autogen.h') # Export MLB models export_mlb_boards(db, '../Utilities/macrecovery/boards.json')