
From: Simon Glass <sjg@chromium.org> Add a Python script to convert Hardware ID files (as produced by 'fwupd hwids') into a devicetree format suitable for use within U-Boot. Provide a simple test as well. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <sjg@chromium.org> --- scripts/hwids_to_dtsi.py | 795 +++++++++++++++++++++++++++++ test/scripts/test_hwids_to_dtsi.py | 306 +++++++++++ 2 files changed, 1101 insertions(+) create mode 100755 scripts/hwids_to_dtsi.py create mode 100644 test/scripts/test_hwids_to_dtsi.py diff --git a/scripts/hwids_to_dtsi.py b/scripts/hwids_to_dtsi.py new file mode 100755 index 00000000000..d1f138687fe --- /dev/null +++ b/scripts/hwids_to_dtsi.py @@ -0,0 +1,795 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0+ +""" +Convert HWIDS txt files to devicetree source (.dtsi) format + +This script converts hardware ID files from board/efi/hwids/ into devicetree +source files. The output includes SMBIOS computer information as properties +and Hardware IDs as binary GUID arrays. +""" + +import argparse +import glob +from io import StringIO +import os +import re +import sys +import traceback +import uuid +from enum import IntEnum + + +DTSI_HEADER = """// SPDX-License-Identifier: GPL-2.0+ + +// Computer Hardware IDs for multiple boards +// Generated from source_path + +/ { +\tchid: chid {}; +}; + +&chid { +""" + +DTSI_FOOTER = """}; +""" + +# Constants for magic numbers +GUID_LENGTH = 36 # Length of GUID string without braces (8-4-4-4-12 format) +HWIDS_SECTION_HEADER_LINES = 2 # Lines for 'Hardware IDs' header and dashes +CHID_BINARY_LENGTH = 16 # Length of CHID binary data in bytes +HEX_BASE = 16 # Base for hexadecimal conversions + +# Field types for special handling +VERSION_FIELDS = { + 'BiosMajorRelease', 'BiosMinorRelease', + 'FirmwareMajorRelease', 'FirmwareMinorRelease' +} +HEX_ENCLOSURE_FIELD = 'EnclosureKind' + +class CHIDField(IntEnum): + """CHID field types matching the C enum chid_field_t.""" + MANUF = 0 + FAMILY = 1 + PRODUCT_NAME = 2 + PRODUCT_SKU = 3 + BOARD_MANUF = 4 + BOARD_PRODUCT = 5 + BIOS_VENDOR = 6 + BIOS_VERSION = 7 + BIOS_MAJOR = 8 + BIOS_MINOR = 9 + ENCLOSURE_TYPE = 10 + + +class CHIDVariant(IntEnum): + """CHID variant IDs matching the Microsoft specification.""" + CHID_00 = 0 # Most specific + CHID_01 = 1 + CHID_02 = 2 + CHID_03 = 3 + CHID_04 = 4 + CHID_05 = 5 + CHID_06 = 6 + CHID_07 = 7 + CHID_08 = 8 + CHID_09 = 9 + CHID_10 = 10 + CHID_11 = 11 + CHID_12 = 12 + CHID_13 = 13 + CHID_14 = 14 # Least specific + + +# Field mapping from HWIDS file field names to CHIDField bits and devicetree +# properties +# Format: 'HWIDSFieldName': (CHIDField.BIT or None, 'devicetree-property-name') +# Note: Firmware fields don't map to CHIDField bits +FIELD_MAP = { + 'Manufacturer': (CHIDField.MANUF, 'manufacturer'), + 'Family': (CHIDField.FAMILY, 'family'), + 'ProductName': (CHIDField.PRODUCT_NAME, 'product-name'), + 'ProductSku': (CHIDField.PRODUCT_SKU, 'product-sku'), + 'BaseboardManufacturer': (CHIDField.BOARD_MANUF, 'baseboard-manufacturer'), + 'BaseboardProduct': (CHIDField.BOARD_PRODUCT, 'baseboard-product'), + 'BiosVendor': (CHIDField.BIOS_VENDOR, 'bios-vendor'), + 'BiosVersion': (CHIDField.BIOS_VERSION, 'bios-version'), + 'BiosMajorRelease': (CHIDField.BIOS_MAJOR, 'bios-major-release'), + 'BiosMinorRelease': (CHIDField.BIOS_MINOR, 'bios-minor-release'), + 'EnclosureKind': (CHIDField.ENCLOSURE_TYPE, 'enclosure-kind'), + 'FirmwareMajorRelease': (None, 'firmware-major-release'), + 'FirmwareMinorRelease': (None, 'firmware-minor-release'), +} + + +# CHID variants table matching the C code variants array +CHID_VARIANTS = { + CHIDVariant.CHID_00: { + 'name': 'HardwareID-00', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.FAMILY) | + (1 << CHIDField.PRODUCT_NAME) | (1 << CHIDField.PRODUCT_SKU) | + (1 << CHIDField.BIOS_VENDOR) | (1 << CHIDField.BIOS_VERSION) | + (1 << CHIDField.BIOS_MAJOR) | (1 << CHIDField.BIOS_MINOR) + }, + CHIDVariant.CHID_01: { + 'name': 'HardwareID-01', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.FAMILY) | + (1 << CHIDField.PRODUCT_NAME) | (1 << CHIDField.BIOS_VENDOR) | + (1 << CHIDField.BIOS_VERSION) | (1 << CHIDField.BIOS_MAJOR) | + (1 << CHIDField.BIOS_MINOR) + }, + CHIDVariant.CHID_02: { + 'name': 'HardwareID-02', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.PRODUCT_NAME) | + (1 << CHIDField.BIOS_VENDOR) | (1 << CHIDField.BIOS_VERSION) | + (1 << CHIDField.BIOS_MAJOR) | (1 << CHIDField.BIOS_MINOR) + }, + CHIDVariant.CHID_03: { + 'name': 'HardwareID-03', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.FAMILY) | + (1 << CHIDField.PRODUCT_NAME) | (1 << CHIDField.PRODUCT_SKU) | + (1 << CHIDField.BOARD_MANUF) | (1 << CHIDField.BOARD_PRODUCT) + }, + CHIDVariant.CHID_04: { + 'name': 'HardwareID-04', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.FAMILY) | + (1 << CHIDField.PRODUCT_NAME) | (1 << CHIDField.PRODUCT_SKU) + }, + CHIDVariant.CHID_05: { + 'name': 'HardwareID-05', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.FAMILY) | + (1 << CHIDField.PRODUCT_NAME) + }, + CHIDVariant.CHID_06: { + 'name': 'HardwareID-06', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.PRODUCT_SKU) | + (1 << CHIDField.BOARD_MANUF) | (1 << CHIDField.BOARD_PRODUCT) + }, + CHIDVariant.CHID_07: { + 'name': 'HardwareID-07', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.PRODUCT_SKU) + }, + CHIDVariant.CHID_08: { + 'name': 'HardwareID-08', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.PRODUCT_NAME) | + (1 << CHIDField.BOARD_MANUF) | (1 << CHIDField.BOARD_PRODUCT) + }, + CHIDVariant.CHID_09: { + 'name': 'HardwareID-09', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.PRODUCT_NAME) + }, + CHIDVariant.CHID_10: { + 'name': 'HardwareID-10', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.FAMILY) | + (1 << CHIDField.BOARD_MANUF) | (1 << CHIDField.BOARD_PRODUCT) + }, + CHIDVariant.CHID_11: { + 'name': 'HardwareID-11', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.FAMILY) + }, + CHIDVariant.CHID_12: { + 'name': 'HardwareID-12', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.ENCLOSURE_TYPE) + }, + CHIDVariant.CHID_13: { + 'name': 'HardwareID-13', + 'fields': (1 << CHIDField.MANUF) | (1 << CHIDField.BOARD_MANUF) | + (1 << CHIDField.BOARD_PRODUCT) + }, + CHIDVariant.CHID_14: { + 'name': 'HardwareID-14', + 'fields': (1 << CHIDField.MANUF) + } +} + + +def load_compatible_map(hwids_dir): + """Load the compatible string mapping from compatible-map file + + Args: + hwids_dir (str): Directory containing the compatible-map file + + Returns: + dict: Mapping from filename to compatible string, empty if there is no + map + """ + compatible_map = {} + map_file = os.path.join(hwids_dir, 'compatible-map') + + if not os.path.exists(map_file): + return compatible_map + + with open(map_file, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + parts = line.split(': ', 1) + if len(parts) == 2: + compatible_map[parts[0]] = parts[1] + + return compatible_map + + +def parse_variant_description(description): + """Parse variant description to determine CHID variant ID and fields mask + + Args: + description (str): Description text after '<-' in HWIDS file + + Returns: + tuple: (variant_id, fields_mask) where variant_id is int (0-14) or None, + and fields_mask is the bitmask or None if not parseable + + Examples: + >>> parse_variant_description('Manufacturer + Family + ProductName') + (5, 7) # HardwareID-05 with fields 0x1|0x2|0x4 = 0x7 + + >>> parse_variant_description('Manufacturer') + (14, 1) # HardwareID-14 with field 0x1 + + >>> parse_variant_description('Invalid + Field') + (None, 0) # Unknown fields, no variant match + """ + # Parse field names and match to variants + desc = description.strip() + + # Parse the field list + fields_mask = 0 + field_parts = desc.split(' + ') + for part in field_parts: + part = part.strip() + if part in FIELD_MAP: + # Get CHIDField, ignore dt property name + chid_field, _ = FIELD_MAP[part] + if chid_field is not None: # Only add to mask if it's a CHIDField + fields_mask |= (1 << chid_field) + + # If no fields were parsed, return None for both + if not fields_mask: + return None, None + + # Match against known variants + for variant_id, variant_info in CHID_VARIANTS.items(): + if variant_info['fields'] == fields_mask: + return int(variant_id), fields_mask + + # Fields were parsed but don't match a known variant + return None, fields_mask + + +def _parse_hardware_ids_section(content, filepath): + """Parse the Hardware IDs section from HWIDS file content + + Args: + content (str): Full file content + filepath (str): Path to the file for error reporting + + Returns: + list: List of (guid_string, variant_id, bitmask) tuples + """ + hardware_ids = [] + ids_pattern = r'Hardware IDs\n-+\n(.*?)(?:\n\n|$)' + ids_section = re.search(ids_pattern, content, re.DOTALL) + if not ids_section: + raise ValueError(f'{filepath}: Missing "Hardware IDs" section') + + for linenum, line in enumerate(ids_section.group(1).strip().split('\n'), 1): + if not line.strip(): + continue + + # Extract GUID and variant info from line like: + # '{810e34c6-cc69-5e36-8675-2f6e354272d3}' <- HardwareID-00 + guid_match = re.search(rf'\{{([0-9a-f-]{{{GUID_LENGTH}}})\}}', line) + if not guid_match: + continue + + guid = guid_match.group(1) + + # The '<-' separator is required for valid HWIDS files + if '<-' not in line: + # Calculate actual line number in file + # (need to account for lines before Hardware IDs section) + before = content[:content.find('Hardware IDs')].count('\n') + # +2 for header and dashes + actual_line = before + linenum + HWIDS_SECTION_HEADER_LINES + raise ValueError( + f"{filepath}:{actual_line}: Missing '<-' separator in " + f'Hardware ID line: {line.strip()}') + + description = line.split('<-', 1)[1].strip() + variant_id, bitmask = parse_variant_description(description) + + hardware_ids.append((guid, variant_id, bitmask)) + + return hardware_ids + + +def parse_hwids_file(filepath): + """Parse a HWIDS txt file and return computer info and hardware IDs + + Args: + filepath (str): Path to the HWIDS txt file + + Returns: + tuple: (computer_info dict, hardware_ids list of tuples) + hardware_ids contains (guid_string, variant_id, bitmask) tuples + """ + info = {} + hardware_ids = [] + + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Extract computer information section + info_section = re.search(r'Computer Information\n-+\n(.*?)\nHardware IDs', + content, re.DOTALL) + if not info_section: + raise ValueError(f'{filepath}: Missing "Computer Information" section') + + for line in info_section.group(1).strip().split('\n'): + if ':' in line: + key, value = line.split(':', 1) + info[key.strip()] = value.strip() + + # Extract hardware IDs with variant information + hardware_ids = _parse_hardware_ids_section(content, filepath) + + return info, hardware_ids + + +def _add_header(out, basename, source_path=None): + """Add header section to devicetree source + + Args: + out (StringIO): StringIO object to write to + basename (str): Base filename for the header comment + source_path (str): Path to the source file or directory + """ + out.write('// SPDX-License-Identifier: GPL-2.0+\n') + out.write('\n') + out.write(f'// Computer Hardware IDs for {basename}\n') + if source_path: + out.write(f'// Generated from {source_path}\n') + else: + out.write('// Generated from board/efi/hwids/\n') + out.write('\n') + + +def _add_computer_info(out, computer_info, indent=2): + """Add computer information properties to devicetree source + + Args: + out (StringIO): StringIO object to write to + computer_info (dict): Dictionary of computer information fields + indent (int): Number of tab indentations (default 2 for &chid + structure) + """ + indent = '\t' * indent + out.write(f'{indent}// SMBIOS Computer Information\n') + for key, value in computer_info.items(): + # Look up the devicetree property name from FIELD_MAP + if key in FIELD_MAP: + _, prop_name = FIELD_MAP[key] + else: + # Fallback for fields not in FIELD_MAP (e.g. FirmwareMajorRelease, + # FirmwareMinorRelease) + prop_name = key.lower().replace('release', '-release') + + # Handle numeric values vs strings + if key in VERSION_FIELDS and value.isdigit(): + out.write(f'{indent}{prop_name} = <{value}>;\n') + elif key == HEX_ENCLOSURE_FIELD: + # Value is already a hex string, convert directly + hex_val = int(value, HEX_BASE) + out.write(f'{indent}{prop_name} = <0x{hex_val:x}>;\n') + else: + out.write(f'{indent}{prop_name} = "{value}";\n') + + +def _add_hardware_ids(out, hardware_ids, indent=2): + """Add hardware IDs as subnodes to devicetree source + + Args: + out (StringIO): StringIO object to write to + hardware_ids (list): List of (guid_string, variant_id, bitmask) tuples + indent (int): Number of tab indentations (default 2 for &chid + structure) + """ + indent = '\t' * indent + out.write(f'{indent}// Hardware IDs (CHIDs)\n') + + extra_counter = 0 + for guid, variant_id, bitmask in hardware_ids: + # Convert GUID string to binary array for devicetree + guid_obj = uuid.UUID(guid) + binary_data = guid_obj.bytes # Raw 16 bytes, no endian conversion + hex_bytes = ' '.join(f'{b:02x}' for b in binary_data) + hex_array = f'[{hex_bytes}]' + + # Create node name - use variant number if available, otherwise extra-N + if variant_id is not None: + node_name = f'hardware-id-{variant_id:02d}' + variant_info = CHID_VARIANTS.get(variant_id, {}) + comment = variant_info.get('name', f'Unknown-{variant_id}') + else: + node_name = f'extra-{extra_counter}' + comment = 'unknown variant' + extra_counter += 1 + + out.write('\n') + out.write(f'{indent}{node_name} {{ // {comment}\n') + + # Add variant property if known + if variant_id is not None: + out.write(f'{indent}\tvariant = <{variant_id}>;\n') + + # Add fields property if bitmask is known + if bitmask is not None: + out.write(f'{indent}\tfields = <0x{bitmask:x}>;\n') + + # Add CHID bytes + out.write(f'{indent}\tchid = {hex_array};\n') + + out.write(f'{indent}}};\n') + + +def generate_dtsi(basename, compatible, computer_info, hardware_ids, + source_path=None): + """Generate devicetree source content + + Args: + basename (str): Base filename for comments and node name + compatible (str): Compatible string for the node + computer_info (dict): Dictionary of computer information + hardware_ids (list): List of (guid_string, variant_id, bitmask) tuples + source_path (str): Path to the source file or directory + + Returns: + str: Complete devicetree source content + + Examples: + >>> info = {'Manufacturer': 'ACME', 'ProductName': 'Device'} + >>> hwids = [('12345678-1234-5678-9abc-123456789abc', 14, 1)] + >>> dtsi = generate_dtsi('acme-device', 'acme,device', info, hwids) + >>> '// Computer Hardware IDs for acme-device' in dtsi + True + >>> 'compatible = "acme,device"' in dtsi + True + """ + out = StringIO() + + _add_header(out, basename, source_path) + + # Start root node with chid declaration + out.write('/ {\n') + out.write('\tchid: chid {};\n') + out.write('};\n') + out.write('\n') + + # Add device content to chid node using reference + out.write('&chid {\n') + node_name = basename.replace('.', '-') + out.write(f'\t{node_name} {{\n') + out.write(f'\t\tcompatible = "{compatible}";\n') + out.write('\n') + + _add_computer_info(out, computer_info, indent=2) + out.write('\n') + + _add_hardware_ids(out, hardware_ids, indent=2) + + out.write('\t};\n') + out.write('};\n') + + return out.getvalue() + + +def parse_arguments(): + """Parse command line arguments + + Returns: + argparse.Namespace: Parsed command line arguments + """ + parser = argparse.ArgumentParser( + description='Convert HWIDS txt files to devicetree source (.dtsi)' + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + 'input_file', + nargs='?', + help='Path to HWIDS txt file (e.g., board/efi/hwids/filename.txt)' + ) + group.add_argument( + '-m', '--map-file', + help='compatible.hwidmap file (processes all .txt files in same dir)' + ) + parser.add_argument( + '-o', '--output', + help='Output file (default: basename.dtsi or hwids.dtsi for dir mode)' + ) + parser.add_argument( + '-v', '--verbose', + action='store_true', + help='Show verbose output with conversion details' + ) + parser.add_argument( + '-D', '--debug', + action='store_true', + help='Show debug traceback on errors' + ) + + return parser.parse_args() + + +def _process_board_file(out, compatible, txt_file, boards_processed, verbose): + """Process a single board HWIDS file and add to output + + Args: + out (StringIO): StringIO object to write to + compatible (str): Compatible string for the board + txt_file (str): Full path to the HWIDS txt file + boards_processed (int): Number of boards processed so far + verbose (bool): Whether to show verbose output + + Returns: + bool: True if board was successfully processed, False otherwise + """ + basename = os.path.splitext(os.path.basename(txt_file))[0] + if verbose: + print(f'Processing {basename}...') + + computer, hardware_ids = parse_hwids_file(txt_file) + + if not hardware_ids: + if verbose: + print(f' Warning: No hardware IDs found in {basename}') + return False + + # Add blank line between boards + if boards_processed > 0: + out.write('\n') + + # Generate board node directly (combining all boards) + node_name = basename.replace('.', '-') + out.write(f'\t{node_name} {{\n') + out.write(f'\t\tcompatible = "{compatible}";\n') + out.write('\n') + + # Add computer info and hardware IDs + _add_computer_info(out, computer, indent=2) + out.write('\n') + + _add_hardware_ids(out, hardware_ids, indent=2) + + out.write('\t};\n') + + if verbose: + print(f' Added {len(hardware_ids)} hardware IDs') + + return True + + +def _load_and_validate_map_file(map_file_path, verbose=False): + """Load and validate compatible map file + + Args: + map_file_path (str): Path to the compatible.hwidmap file + verbose (bool): Show verbose output + + Returns: + tuple: (hwids_dir, compatible_map) + """ + # Get directory containing the map file + hwids_dir = os.path.dirname(map_file_path) + + # Load compatible string mapping from the specified file + compatible_map = {} + if os.path.exists(map_file_path): + with open(map_file_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + parts = line.split(': ', 1) + if len(parts) == 2: + compatible_map[parts[0]] = parts[1] + + if not compatible_map: + raise ValueError(f'No valid mappings found in {map_file_path}') + + # Find all .txt files in the same directory + txt_files = glob.glob(os.path.join(hwids_dir, '*.txt')) + txt_basenames = {os.path.splitext(os.path.basename(f))[0] + for f in txt_files} + + # Check for files in map that don't exist + map_files = set(compatible_map.keys()) + missing_files = map_files - txt_basenames + if missing_files: + raise ValueError('Files in map but not found in directory: ' + f"{', '.join(sorted(missing_files))}") + + # Check for files in directory that aren't in map + extra_files = txt_basenames - map_files + if extra_files: + file_list = ', '.join(sorted(extra_files)) + raise ValueError(f'Files in directory but not in map: {file_list}') + + if verbose: + print(f'Using compatible map: {map_file_path}') + print(f'Processing {len(compatible_map)} HWIDs files from map') + print() + + return hwids_dir, compatible_map + + +def _finalise_combined_dtsi(out, hwids_dir, processed, skipped, verbose): + """Finalize the combined DTSI output with validation and reporting + + Args: + out (StringIO): StringIO object containing the main content + hwids_dir (str): Directory path for header generation + processed (int): Number of successfully processed files + skipped (list): List of skipped board names + verbose (bool): Whether to show verbose output + + Returns: + str: Final DTSI content with header and footer + """ + if not processed: + raise ValueError('No valid HWIDS files could be processed') + + if verbose: + print(f'\nProcessed {processed} boards successfully') + + # Print warning about skipped boards + if skipped: + print(f'Warning: Skipped {len(skipped)} unmapped boards: ' + f"{', '.join(skipped)}") + + header = DTSI_HEADER.replace('source_path', hwids_dir) + return ''.join([header, out.getvalue(), DTSI_FOOTER]) + + +def process_map_file(map_file_path, verbose=False): + """Process HWIDS files specified in the map file and generate combined DTSI + + Args: + map_file_path (str): Path to the compatible.hwidmap file + verbose (bool): Show verbose output + + Returns: + str: Combined DTSI content for all boards + """ + hwids_dir, compatible_map = _load_and_validate_map_file(map_file_path, + verbose) + + out = StringIO() + processed = 0 + skipped = [] + for basename in sorted(compatible_map.keys()): + compatible = compatible_map[basename] + + # Skip files with 'none' mapping + if compatible == 'none': + skipped.append(basename) + if verbose: + print(f'Skipping {basename} (mapping: none)') + continue + + # Process this board file + txt_file = os.path.join(hwids_dir, f'{basename}.txt') + if _process_board_file(out, compatible, txt_file, processed, verbose): + processed += 1 + + return _finalise_combined_dtsi(out, hwids_dir, processed, skipped, + verbose) + + +def handle_map_file_mode(args): + """Handle map file mode processing + + Args: + args (argparse.Namespace): Parsed command line arguments + + Returns: + str: Generated DTSI content + """ + if not os.path.exists(args.map_file): + raise FileNotFoundError(f'Map file {args.map_file} not found') + + dtsi_content = process_map_file(args.map_file, args.verbose) + outfile = args.output or 'hwids.dtsi' + + if args.verbose: + print(f'Generated combined DTSI -> {outfile}') + print() + + return dtsi_content + + +def handle_single_file_mode(args): + """Handle single file mode processing + + Args: + args (argparse.Namespace): Parsed command line arguments + + Returns: + str: Generated DTSI content + """ + if not args.input_file: + raise ValueError('input_file is required when not using --map-file') + + if not os.path.exists(args.input_file): + raise FileNotFoundError(f"File '{args.input_file}' not found") + + # Get the directory and basename + hwids_dir = os.path.dirname(args.input_file) + basename = os.path.splitext(os.path.basename(args.input_file))[0] + + # Load compatible string mapping + compatible_map = load_compatible_map(hwids_dir) + compatible = compatible_map.get(basename, f'unknown,{basename}') + + # Parse the input file + info, hardware_ids = parse_hwids_file(args.input_file) + + if not hardware_ids and args.verbose: + print(f"Warning: No hardware IDs found in '{args.input_file}'") + + # Generate devicetree source + content = generate_dtsi(basename, compatible, info, + hardware_ids, args.input_file) + + outfile = args.output or f'{basename}.dtsi' + if args.verbose: + print(f'Converting {args.input_file} -> {outfile}') + print(f'Compatible: {compatible}') + print(f'Computer info fields: {len(info)}') + print(f'Hardware IDs: {len(hardware_ids)}') + print() + + return content + + +def main(): + """Main function + + Returns: + int: Exit code (0 for success, 1 for error) + """ + args = parse_arguments() + try: + # Choose processing mode and handle + if args.map_file: + content = handle_map_file_mode(args) + outfile = args.output or 'hwids.dtsi' + else: + content = handle_single_file_mode(args) + basename = os.path.splitext(os.path.basename(args.input_file))[0] + outfile = args.output or f'{basename}.dtsi' + + # Write to file if output specified, otherwise print to stdout + if args.output: + try: + with open(outfile, 'w', encoding='utf-8') as f: + f.write(content) + if args.verbose: + print(f'Written to {outfile}') + except (IOError, OSError) as e: + print(f'Error writing to {outfile}: {e}') + return 1 + else: + print(content, end='') + + return 0 + + except (ValueError, FileNotFoundError, IOError, OSError) as e: + if args.debug: + traceback.print_exc() + else: + print(f'Error: {e}') + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/test/scripts/test_hwids_to_dtsi.py b/test/scripts/test_hwids_to_dtsi.py new file mode 100644 index 00000000000..c88604631a8 --- /dev/null +++ b/test/scripts/test_hwids_to_dtsi.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0+ +""" +Test for hwids-to-dtsi.py script + +Validates that the HWIDS to devicetree conversion script correctly parses +hardware ID files and generates proper devicetree-source output +""" + +import os +import sys +import tempfile +import unittest +import uuid +from io import StringIO + +# Add the scripts directory to the path +script_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'scripts') +sys.path.insert(0, script_dir) + +# pylint: disable=wrong-import-position,import-error +from hwids_to_dtsi import ( + load_compatible_map, + parse_hwids_file, + generate_dtsi, + parse_variant_description, + VERSION_FIELDS, + HEX_ENCLOSURE_FIELD, + _finalise_combined_dtsi +) + + +class TestHwidsToDeviceTree(unittest.TestCase): + """Test cases for HWIDS to devicetree conversion""" + + def setUp(self): + """Set up test fixtures""" + self.test_hwids_content = """Computer Information +-------------------- +BiosVendor: Insyde Corp. +BiosVersion: V1.24 +BiosMajorRelease: 0 +BiosMinorRelease: 0 +FirmwareMajorRelease: 01 +FirmwareMinorRelease: 15 +Manufacturer: Acer +Family: Swift 14 AI +ProductName: Swift SF14-11 +ProductSku: +EnclosureKind: a +BaseboardManufacturer: SX1 +BaseboardProduct: Bluetang_SX1 +Hardware IDs +------------ +{27d2dba8-e6f1-5c19-ba1c-c25a4744c161} <- Manufacturer + Family + ProductName + ProductSku + BiosVendor + BiosVersion + BiosMajorRelease + BiosMinorRelease +{676172cd-d185-53ed-aac6-245d0caa02c4} <- Manufacturer + Family + ProductName + BiosVendor + BiosVersion + BiosMajorRelease + BiosMinorRelease +{20c2cf2f-231c-5d02-ae9b-c837ab5653ed} <- Manufacturer + ProductName + BiosVendor + BiosVersion + BiosMajorRelease + BiosMinorRelease +{f2ea7095-999d-5e5b-8f2a-4b636a1e399f} <- Manufacturer + Family + ProductName + ProductSku + BaseboardManufacturer + BaseboardProduct +{331d7526-8b88-5923-bf98-450cf3ea82a4} <- Manufacturer + Family + ProductName + ProductSku +{98ad068a-f812-5f13-920c-3ff3d34d263f} <- Manufacturer + Family + ProductName +{3f49141c-d8fb-5a6f-8b4a-074a2397874d} <- Manufacturer + ProductSku + BaseboardManufacturer + BaseboardProduct +{7c107a7f-2d77-51aa-aef8-8d777e26ffbc} <- Manufacturer + ProductSku +{6a12c9bc-bcfa-5448-9f66-4159dbe8c326} <- Manufacturer + ProductName + BaseboardManufacturer + BaseboardProduct +{f55122fb-303f-58bc-b342-6ef653956d1d} <- Manufacturer + ProductName +{ee8fa049-e5f4-51e4-89d8-89a0140b8f38} <- Manufacturer + Family + BaseboardManufacturer + BaseboardProduct +{4cdff732-fd0c-5bac-b33e-9002788ea557} <- Manufacturer + Family +{92dcc94d-48f7-5ee8-b9ec-a6393fb7a484} <- Manufacturer + EnclosureKind +{32f83b0f-1fad-5be2-88be-5ab020e7a70e} <- Manufacturer + BaseboardManufacturer + BaseboardProduct +{1e301734-5d49-5df4-9ed2-aa1c0a9dddda} <- Manufacturer +Extra Hardware IDs +------------------ +{058c0739-1843-5a10-bab7-fae8aaf30add} <- Manufacturer + Family + ProductName + ProductSku + BiosVendor +{100917f4-9c0a-5ac3-a297-794222da9bc9} <- Manufacturer + Family + ProductName + BiosVendor +{86654360-65f0-5935-bc87-81102c6a022b} <- Manufacturer + BiosVendor +""" + + self.test_compatible_map = """# SPDX-License-Identifier: GPL-2.0+ +# compatible map +test-device: test,example-device +""" + + def test_parse_hwids_file(self): + """Test parsing of HWIDS file content""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', + delete=False) as outf: + outf.write(self.test_hwids_content) + outf.flush() + + try: + info, hardware_ids = parse_hwids_file(outf.name) + expected_info = { + 'BiosVendor': 'Insyde Corp.', + 'BiosVersion': 'V1.24', + 'BiosMajorRelease': '0', + 'BiosMinorRelease': '0', + 'FirmwareMajorRelease': '01', + 'FirmwareMinorRelease': '15', + 'Manufacturer': 'Acer', + 'Family': 'Swift 14 AI', + 'ProductName': 'Swift SF14-11', + 'ProductSku': '', + 'EnclosureKind': 'a', + 'BaseboardManufacturer': 'SX1', + 'BaseboardProduct': 'Bluetang_SX1' + } + self.assertEqual(info, expected_info) + + # Check hardware IDs (now tuples with variant info and bitmask) + expected_ids = [ + # Variant 0: All main fields + ('27d2dba8-e6f1-5c19-ba1c-c25a4744c161', 0, 0x3cf), + # Variant 1: Without SKU + ('676172cd-d185-53ed-aac6-245d0caa02c4', 1, 0x3c7), + # Variant 2: Without family + ('20c2cf2f-231c-5d02-ae9b-c837ab5653ed', 2, 0x3c5), + # Variant 3: With baseboard, no BIOS version + ('f2ea7095-999d-5e5b-8f2a-4b636a1e399f', 3, 0x3f), + # Variant 4: Basic product ID + ('331d7526-8b88-5923-bf98-450cf3ea82a4', 4, 0xf), + # Variant 5: Without SKU + ('98ad068a-f812-5f13-920c-3ff3d34d263f', 5, 0x7), + # Variant 6: SKU with baseboard + ('3f49141c-d8fb-5a6f-8b4a-074a2397874d', 6, 0x39), + # Variant 7: Manufacturer and SKU + ('7c107a7f-2d77-51aa-aef8-8d777e26ffbc', 7, 0x9), + # Variant 8: Product name with baseboard + ('6a12c9bc-bcfa-5448-9f66-4159dbe8c326', 8, 0x35), + # Variant 9: Manufacturer and product name + ('f55122fb-303f-58bc-b342-6ef653956d1d', 9, 0x5), + # Variant 10: Family with baseboard + ('ee8fa049-e5f4-51e4-89d8-89a0140b8f38', 10, 0x33), + # Variant 11: Manufacturer and family + ('4cdff732-fd0c-5bac-b33e-9002788ea557', 11, 0x3), + # Variant 12: Manufacturer and enclosure + ('92dcc94d-48f7-5ee8-b9ec-a6393fb7a484', 12, 0x401), + # Variant 13: Manufacturer with baseboard + ('32f83b0f-1fad-5be2-88be-5ab020e7a70e', 13, 0x31), + # Variant 14: Manufacturer only + ('1e301734-5d49-5df4-9ed2-aa1c0a9dddda', 14, 0x1), + # Extra Hardware IDs (non-standard variants) + # Extra: Manufacturer + Family + ProductName + ProductSku + BiosVendor + ('058c0739-1843-5a10-bab7-fae8aaf30add', None, 0x4f), + # Extra: Manufacturer + Family + ProductName + BiosVendor + ('100917f4-9c0a-5ac3-a297-794222da9bc9', None, 0x47), + # Extra: Manufacturer + BiosVendor + ('86654360-65f0-5935-bc87-81102c6a022b', None, 0x41), + ] + self.assertEqual(hardware_ids, expected_ids) + + finally: + os.unlink(outf.name) + + def test_load_compatible_map(self): + """Test loading compatible string mapping""" + with tempfile.TemporaryDirectory() as tmpdir: + map_file = os.path.join(tmpdir, 'compatible-map') + with open(map_file, 'w', encoding='utf-8') as f: + f.write(self.test_compatible_map) + + compatible_map = load_compatible_map(tmpdir) + self.assertEqual(compatible_map['test-device'], + 'test,example-device') + + def test_guid_to_binary(self): + """Test GUID to binary conversion""" + test_guid = '810e34c6-cc69-5e36-8675-2f6e354272d3' + guid_obj = uuid.UUID(test_guid) + binary_data = guid_obj.bytes + + # Should be 16 bytes + self.assertEqual(len(binary_data), 16) + + # Test known conversion (raw bytes in string order) + expected = bytearray([ + 0x81, 0x0e, 0x34, 0xc6, # time_low (raw bytes) + 0xcc, 0x69, # time_mid (raw bytes) + 0x5e, 0x36, # time_hi (raw bytes) + 0x86, 0x75, # clock_seq (raw bytes) + 0x2f, 0x6e, 0x35, 0x42, 0x72, 0xd3 # node (raw bytes) + ]) + self.assertEqual(binary_data, bytes(expected)) + + + def test_generate_dtsi(self): + """Test devicetree source generation""" + info = { + 'Manufacturer': 'LENOVO', + 'ProductName': '21BXCTO1WW', + 'BiosMajorRelease': '1', + 'EnclosureKind': 'a' + } + hardware_ids = [('810e34c6-cc69-5e36-8675-2f6e354272d3', 0, 0x3cf)] + + content = generate_dtsi('test-device', 'test,example-device', + info, hardware_ids) + + self.assertIn('// SPDX-License-Identifier: GPL-2.0+', content) + self.assertIn('test-device {', content) + self.assertIn('compatible = "test,example-device";', content) + self.assertIn('manufacturer = "LENOVO";', content) + self.assertIn('product-name = "21BXCTO1WW";', content) + self.assertIn('bios-major-release = <1>;', content) + self.assertIn('enclosure-kind = <0xa>;', content) + self.assertIn('// Hardware IDs (CHIDs)', content) + self.assertIn('hardware-id-00 {', content) + self.assertIn('variant = <0>;', content) + self.assertIn('fields = <0x3cf>;', content) + self.assertIn( + 'chid = [81 0e 34 c6 cc 69 5e 36 86 75 2f 6e 35 42 72 d3];', + content) + + def test_invalid_guid_format(self): + """Test error handling for invalid GUID format""" + with self.assertRaises(ValueError): + uuid.UUID('invalid-guid-format') + + def test_missing_compatible_map(self): + """Test behavior when compatible-map file is missing""" + with tempfile.TemporaryDirectory() as tmpdir: + compatible_map = load_compatible_map(tmpdir) + self.assertEqual(compatible_map, {}) + + def test_enclosure_kind_conversion(self): + """Test enclosure kind hex conversion""" + info = {'EnclosureKind': 'a'} + hardware_ids = [] + + content = generate_dtsi('test', 'test,device', info, hardware_ids) + self.assertIn('enclosure-kind = <0xa>;', content) + + # Test numeric enclosure kind ('10' is interpreted as hex 0x10) + info = {'EnclosureKind': '10'} + content = generate_dtsi('test', 'test,device', info, hardware_ids) + self.assertIn('enclosure-kind = <0x10>;', content) + + def test_empty_hardware_ids(self): + """Test handling of empty hardware IDs list""" + info = {'Manufacturer': 'TEST'} + hardware_ids = [] + + content = generate_dtsi('test', 'test,device', info, hardware_ids) + self.assertIn('// Hardware IDs (CHIDs)', content) + + # Should have no hardware-id-XX or extra-X nodes + self.assertNotIn('hardware-id-', content) + self.assertNotIn('extra-', content) + + def test_parse_variant_from_field_description(self): + """Test parsing variant ID from field descriptions""" + # Test variant 0 - most specific + desc = ('Manufacturer + Family + ProductName + ProductSku + ' + 'BiosVendor + BiosVersion + BiosMajorRelease + ' + 'BiosMinorRelease') + variant_id, fields_mask = parse_variant_description(desc) + self.assertEqual(variant_id, 0) + self.assertEqual(fields_mask, 0x3cf) + + # Test variant 14 - least specific (manufacturer only) + desc = 'Manufacturer' + variant_id, fields_mask = parse_variant_description(desc) + self.assertEqual(variant_id, 14) + self.assertEqual(fields_mask, 0x1) + + # Test variant 5 - manufacturer, family, product name + desc = 'Manufacturer + Family + ProductName' + variant_id, fields_mask = parse_variant_description(desc) + self.assertEqual(variant_id, 5) + self.assertEqual(fields_mask, 0x7) + + def test_constants_usage(self): + """Test that magic number constants are used correctly""" + # Test GUID_LENGTH constant in regex pattern + test_guid = '12345678-1234-5678-9abc-123456789abc' + content = f'''Computer Information +-------------------- +Manufacturer: Test + +Hardware IDs +------------ +{{{test_guid}}} <- Manufacturer +''' + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write(content) + f.flush() + try: + _info, hardware_ids = parse_hwids_file(f.name) + # Should successfully parse the GUID + self.assertEqual(len(hardware_ids), 1) + self.assertEqual(hardware_ids[0][0], test_guid) + finally: + os.unlink(f.name) + + def test_version_fields_constants(self): + """Test that VERSION_FIELDS constant is used correctly""" + + # Test that all expected version fields are in the constant + expected_fields = {'BiosMajorRelease', 'BiosMinorRelease', + 'FirmwareMajorRelease', 'FirmwareMinorRelease'} + self.assertTrue(expected_fields.issubset(VERSION_FIELDS)) + + # Test HEX_ENCLOSURE_FIELD constant + self.assertEqual(HEX_ENCLOSURE_FIELD, 'EnclosureKind') + + +if __name__ == '__main__': + unittest.main() -- 2.43.0