qs.xiong | df2e8c0 | 2025-07-02 16:50:13 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # ASR1806 Partition Configuration Generator (Final Correct Format) |
| 3 | |
| 4 | import re |
| 5 | import json |
| 6 | import sys |
| 7 | import os |
| 8 | import argparse |
| 9 | from collections import OrderedDict |
| 10 | from pathlib import Path |
| 11 | |
| 12 | def validate_file(path, description): |
| 13 | """Validate input file exists and is readable""" |
| 14 | path_obj = Path(path) |
| 15 | if not path_obj.exists(): |
| 16 | raise FileNotFoundError(f"{description} not found: {path}") |
| 17 | if not path_obj.is_file(): |
| 18 | raise IOError(f"{description} path is not a file: {path}") |
| 19 | if not os.access(str(path_obj), os.R_OK): |
| 20 | raise PermissionError(f"Cannot read {description}: {path}") |
| 21 | return path_obj |
| 22 | |
| 23 | def parse_dtsi_partitions(dtsi_file): |
| 24 | """Parse partition information from DTSI file""" |
| 25 | partitions = OrderedDict() |
| 26 | try: |
| 27 | with open(dtsi_file, 'r', encoding='utf-8') as f: |
| 28 | content = f.read() |
| 29 | |
| 30 | pattern = re.compile( |
| 31 | r'partition@([0-9A-Fa-f]+)\s*{\s*.*?label\s*=\s*"([^"]+)";\s*.*?reg\s*=\s*<([^>]+)>;\s*.*?};', |
| 32 | re.DOTALL |
| 33 | ) |
| 34 | |
| 35 | for match in pattern.finditer(content): |
| 36 | try: |
| 37 | start_addr = int(match.group(1), 16) |
| 38 | label = match.group(2).strip() |
| 39 | reg_values = [int(x, 16) for x in match.group(3).split()] |
| 40 | |
| 41 | if not reg_values: |
| 42 | print(f"Invalid reg value for partition {label}", file=sys.stderr) |
| 43 | continue |
| 44 | |
| 45 | size = reg_values[-1] if len(reg_values) > 1 else reg_values[0] |
| 46 | |
| 47 | partitions[label] = { |
| 48 | 'start_addr': f"0x{start_addr:08X}", |
| 49 | 'size': f"0x{size:08X}", |
| 50 | 'size_bytes': size |
| 51 | } |
| 52 | except ValueError as e: |
| 53 | print(f"Failed to parse partition data: {e}", file=sys.stderr) |
| 54 | continue |
| 55 | |
| 56 | if not partitions: |
| 57 | raise ValueError("No valid partitions found in DTSI file") |
| 58 | |
| 59 | print(f"Parsed {len(partitions)} partitions from {dtsi_file}") |
| 60 | return partitions |
| 61 | except Exception as e: |
| 62 | raise RuntimeError(f"DTSI parse failed: {str(e)}") from e |
| 63 | |
| 64 | def load_config(config_file): |
| 65 | """Load and validate partition configuration""" |
| 66 | try: |
| 67 | with open(config_file, 'r', encoding='utf-8') as f: |
| 68 | config = json.load(f, object_pairs_hook=OrderedDict) |
| 69 | |
| 70 | required = { |
| 71 | 'mtdparts_order': list, |
| 72 | 'partition_aliases': dict |
| 73 | } |
| 74 | for key, typ in required.items(): |
| 75 | if key not in config: |
| 76 | raise KeyError(f"Missing required key: {key}") |
| 77 | if not isinstance(config[key], typ): |
| 78 | raise TypeError(f"{key} must be {typ.__name__}") |
| 79 | |
| 80 | print(f"Loaded config from {config_file}") |
| 81 | return config |
| 82 | except json.JSONDecodeError as e: |
| 83 | raise RuntimeError(f"Invalid JSON format: {str(e)}") from e |
| 84 | except Exception as e: |
| 85 | raise RuntimeError(f"Config load failed: {str(e)}") from e |
| 86 | |
| 87 | def generate_complete_config(partitions, config): |
| 88 | """Generate the complete configuration block with exact formatting""" |
| 89 | # 1. Generate partition defines |
| 90 | defines = [] |
| 91 | for part_name, alias in config['partition_aliases'].items(): |
| 92 | if part_name not in partitions: |
| 93 | print(f"Config references unknown partition: {part_name}", file=sys.stderr) |
| 94 | continue |
| 95 | defines.append(f'#define {alias} "{part_name}"') |
| 96 | defines_section = "\n".join(defines) |
| 97 | |
| 98 | # 2. Generate MTDPARTS content with exact formatting |
| 99 | mtdparts = [] |
| 100 | valid_parts = [p for p in config['mtdparts_order'] if p in partitions and p in config['partition_aliases']] |
| 101 | num_parts = len(valid_parts) |
| 102 | |
| 103 | for i, part_name in enumerate(valid_parts): |
| 104 | part = partitions[part_name] |
| 105 | alias = config['partition_aliases'][part_name] |
| 106 | |
| 107 | # 构建每个分区的定义,格式为 "0x00180000@0x02600000("UBOOT_MTD_PART_A")," |
| 108 | mtd_entry = f'0x{part["size"][2:]}@0x{part["start_addr"][2:]}("{alias}")' |
| 109 | if i < num_parts - 1: |
| 110 | mtd_entry += ',' # 除最后一个分区外,每个分区后加逗号 |
| 111 | |
| 112 | # 每个分区定义用双引号包裹,并添加行继续符和缩进 |
| 113 | if i == 0: |
| 114 | # 第一个分区不需要额外缩进 |
| 115 | mtdparts.append(f'"mtdparts=nand_mtd:{mtd_entry}"\\') |
| 116 | else: |
| 117 | # 后续分区需要 5 个空格缩进 |
| 118 | mtdparts.append(f' "{mtd_entry}"\\') |
| 119 | |
| 120 | # 最后一行需要单独处理,确保没有双引号和反斜杠 |
| 121 | if mtdparts: |
| 122 | last_line = mtdparts.pop() # 移除最后一行 |
| 123 | # 构建不带双引号和反斜杠的最后一行 |
| 124 | mtdparts.append(last_line.replace('\\"', '').replace('\\', '')) |
| 125 | |
| 126 | # 连接所有分区定义 |
| 127 | mtdparts_joined = '\n'.join(mtdparts) |
| 128 | |
| 129 | # 构建最终的 MTDPARTS 部分 |
| 130 | mtdparts_section = f"""#ifdef CONFIG_CMD_FASTBOOT |
| 131 | #define CONFIG_MTDPARTS \\ |
| 132 | {mtdparts_joined} |
| 133 | #endif /* CONFIG_CMD_FASTBOOT */""" |
| 134 | |
| 135 | # 3. Combine into complete block |
| 136 | complete_content = f"""{defines_section} |
| 137 | |
| 138 | {mtdparts_section}""" |
| 139 | |
| 140 | return complete_content |
| 141 | |
| 142 | def update_header_file(header_file, complete_content): |
| 143 | """Replace ONLY the content between dynamic markers""" |
| 144 | try: |
| 145 | header_file.parent.mkdir(parents=True, exist_ok=True) |
| 146 | |
| 147 | if not header_file.exists(): |
| 148 | raise RuntimeError(f"Header file does not exist: {header_file}") |
| 149 | |
| 150 | # Read existing content |
| 151 | with open(header_file, 'r', encoding='utf-8') as f: |
| 152 | content = f.read() |
| 153 | original_mode = header_file.stat().st_mode |
| 154 | |
| 155 | # Pattern to find the exact block to replace |
| 156 | pattern = re.compile( |
| 157 | r'(/\* add mtd parts dynamically \*/\n)(.*?)(\n#else)', |
| 158 | re.DOTALL |
| 159 | ) |
| 160 | |
| 161 | match = pattern.search(content) |
| 162 | if not match: |
| 163 | raise RuntimeError("Could not find the replacement markers ('/* add mtd parts dynamically */' to '#else')") |
| 164 | |
| 165 | # Replace only the middle part |
| 166 | updated_content = ( |
| 167 | content[:match.start(2)] + |
| 168 | complete_content + |
| 169 | content[match.end(2):] |
| 170 | ) |
| 171 | |
| 172 | # Write updated content |
| 173 | with open(header_file, 'w', encoding='utf-8') as f: |
| 174 | f.write(updated_content) |
| 175 | header_file.chmod(original_mode) |
| 176 | |
| 177 | print(f"Successfully updated {header_file}") |
| 178 | return True |
| 179 | |
| 180 | except Exception as e: |
| 181 | raise RuntimeError(f"Header update failed: {str(e)}") from e |
| 182 | |
| 183 | def main(): |
| 184 | parser = argparse.ArgumentParser( |
| 185 | description='Generate partition configuration between markers', |
| 186 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 187 | |
| 188 | parser.add_argument('--dtsi', required=True, |
| 189 | help='Input DTSI file with partition layout') |
| 190 | parser.add_argument('--config', required=True, |
| 191 | help='JSON config file with partition aliases') |
| 192 | parser.add_argument('--header', required=True, |
| 193 | help='Output header file path') |
| 194 | parser.add_argument('--verbose', action='store_true', |
| 195 | help='Show detailed processing info') |
| 196 | |
| 197 | args = parser.parse_args() |
| 198 | |
| 199 | try: |
| 200 | dtsi_path = validate_file(args.dtsi, "DTSI file") |
| 201 | config_path = validate_file(args.config, "Config file") |
| 202 | header_path = Path(args.header).resolve() |
| 203 | |
| 204 | if args.verbose: |
| 205 | print(f"Processing:\n- DTSI: {dtsi_path}\n- Config: {config_path}\n- Output: {header_path}") |
| 206 | |
| 207 | partitions = parse_dtsi_partitions(dtsi_path) |
| 208 | config = load_config(config_path) |
| 209 | |
| 210 | # Verify all configured partitions exist |
| 211 | missing = [p for p in config['mtdparts_order'] if p not in partitions] |
| 212 | if missing: |
| 213 | print(f"Missing partitions in DTSI: {', '.join(missing)}", file=sys.stderr) |
| 214 | |
| 215 | complete_content = generate_complete_config(partitions, config) |
| 216 | |
| 217 | if args.verbose: |
| 218 | print("\nGenerated content to insert:") |
| 219 | print("-" * 40) |
| 220 | print(complete_content) |
| 221 | print("-" * 40) |
| 222 | |
| 223 | updated = update_header_file(header_path, complete_content) |
| 224 | |
| 225 | if updated: |
| 226 | print("Update completed successfully") |
| 227 | |
| 228 | return 0 |
| 229 | |
| 230 | except Exception as e: |
| 231 | print(f"Error: {str(e)}", file=sys.stderr) |
| 232 | return 1 |
| 233 | |
| 234 | if __name__ == "__main__": |
| 235 | sys.exit(main()) |