|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# SPDX-License-Identifier: BSD-3-Clause |
| 3 | +# |
| 4 | +# Creates new module based on template module. |
| 5 | + |
| 6 | +import os |
| 7 | +import sys |
| 8 | +import shutil |
| 9 | +import uuid |
| 10 | +import re |
| 11 | + |
| 12 | +def process_directory(directory_path, old_text, new_text): |
| 13 | + """ |
| 14 | + Recursively walks through a directory to rename files and replace content. |
| 15 | + This function processes files first, then directories, to allow for renaming |
| 16 | + of the directories themselves after their contents are handled. |
| 17 | + """ |
| 18 | + # Walk through the directory tree |
| 19 | + for dirpath, dirnames, filenames in os.walk(directory_path, topdown=False): |
| 20 | + # --- Process Files --- |
| 21 | + for filename in filenames: |
| 22 | + original_filepath = os.path.join(dirpath, filename) |
| 23 | + |
| 24 | + # a) Replace content within files |
| 25 | + # Only process C/H files for content replacement |
| 26 | + if filename.endswith(('.c', '.h', '.cpp', '.hpp', '.txt', '.toml', 'Kconfig')): |
| 27 | + replace_text_in_file(original_filepath, old_text, new_text) |
| 28 | + if filename.endswith(('.c', '.h', '.cpp', '.hpp', '.txt', '.toml', 'Kconfig')): |
| 29 | + replace_text_in_file(original_filepath, old_text.upper(), new_text.upper()) |
| 30 | + if filename.endswith(('.c', '.h', '.cpp', '.hpp', '.txt', '.toml', 'Kconfig')): |
| 31 | + replace_text_in_file(original_filepath, "template", new_text) |
| 32 | + if filename.endswith(('.c', '.h', '.cpp', '.hpp', '.txt', '.toml', 'Kconfig')): |
| 33 | + replace_text_in_file(original_filepath, "TEMPLATE", new_text.upper()) |
| 34 | + |
| 35 | + # b) Rename the file if its name contains the template text |
| 36 | + if "template" in filename: |
| 37 | + new_filename = filename.replace("template", new_text) |
| 38 | + new_filepath = os.path.join(dirpath, new_filename) |
| 39 | + print(f" -> Renaming file: '{original_filepath}' to '{new_filepath}'") |
| 40 | + os.rename(original_filepath, new_filepath) |
| 41 | + |
| 42 | +def replace_text_in_file(filepath, old_text, new_text): |
| 43 | + """ |
| 44 | + Replaces all occurrences of old_text with new_text in a given file. |
| 45 | + """ |
| 46 | + try: |
| 47 | + # Read the file content |
| 48 | + with open(filepath, 'r', encoding='utf-8') as file: |
| 49 | + content = file.read() |
| 50 | + |
| 51 | + # Perform the replacement |
| 52 | + new_content = content.replace(old_text, new_text) |
| 53 | + |
| 54 | + # If content has changed, write it back |
| 55 | + if new_content != content: |
| 56 | + print(f" -> Updating content in: '{filepath}'") |
| 57 | + with open(filepath, 'w', encoding='utf-8') as file: |
| 58 | + file.write(new_content) |
| 59 | + |
| 60 | + except Exception as e: |
| 61 | + print(f" -> [WARNING] Could not process file '{filepath}'. Reason: {e}") |
| 62 | + |
| 63 | +def insert_uuid_name(filepath: str, new_name: str): |
| 64 | + """ |
| 65 | + Inserts a newly generated UUID and a given name into a file, maintaining |
| 66 | + alphabetical order by name. Ignores and preserves lines starting with '#'. |
| 67 | +
|
| 68 | + Args: |
| 69 | + filepath (str): The path to the file to be updated. |
| 70 | + new_name (str): The name to associate with the new UUID. |
| 71 | + """ |
| 72 | + data_entries = [] |
| 73 | + other_lines = [] # To store comments and blank lines |
| 74 | + |
| 75 | + # --- 1. Read existing entries and comments from the file --- |
| 76 | + try: |
| 77 | + with open(filepath, 'r', encoding='utf-8') as f: |
| 78 | + for line in f: |
| 79 | + stripped_line = line.strip() |
| 80 | + # Check for comments or blank lines |
| 81 | + if not stripped_line or stripped_line.startswith('#'): |
| 82 | + other_lines.append(line) # Preserve the original line with newline |
| 83 | + continue |
| 84 | + |
| 85 | + # It's a data line, so process it |
| 86 | + # Split only on the first space to handle names that might contain spaces |
| 87 | + parts = stripped_line.split(' ', 1) |
| 88 | + if len(parts) == 2: |
| 89 | + data_entries.append((parts[0], parts[1])) |
| 90 | + except FileNotFoundError: |
| 91 | + print(f"File '{filepath}' not found. A new file will be created.") |
| 92 | + except Exception as e: |
| 93 | + print(f"An error occurred while reading the file: {e}") |
| 94 | + return |
| 95 | + |
| 96 | + # --- 2. Check if the name already exists in data entries --- |
| 97 | + if any(name == new_name for _, name in data_entries): |
| 98 | + print(f"Name '{new_name}' already exists in the file. No changes made.") |
| 99 | + return |
| 100 | + |
| 101 | + # --- 3. Add the new entry to the data list --- |
| 102 | + new_uuid = str(uuid.uuid4()) |
| 103 | + |
| 104 | + # We split the string from the right at the last hyphen and then join the parts. |
| 105 | + parts = new_uuid.rsplit('-', 1) |
| 106 | + custom_format_uuid = ''.join(parts) |
| 107 | + |
| 108 | + data_entries.append((custom_format_uuid, new_name)) |
| 109 | + print(f"Generated new entry: {new_uuid} {new_name}") |
| 110 | + |
| 111 | + # --- 4. Sort the list of data entries by name (the second element of the tuple) --- |
| 112 | + data_entries.sort(key=lambda item: item[1]) |
| 113 | + |
| 114 | + # --- 5. Write the comments and then the sorted data back to the file --- |
| 115 | + try: |
| 116 | + with open(filepath, 'w', encoding='utf-8') as f: |
| 117 | + # Write all the comments and other non-data lines first |
| 118 | + for line in other_lines: |
| 119 | + f.write(line) |
| 120 | + |
| 121 | + # Write the sorted data entries |
| 122 | + for entry_uuid, entry_name in data_entries: |
| 123 | + f.write(f"{entry_uuid} {entry_name}\n") |
| 124 | + print(f"Successfully updated '{filepath}'.") |
| 125 | + except Exception as e: |
| 126 | + print(f"An error occurred while writing to the file: {e}") |
| 127 | + |
| 128 | +# Define the markers for the managed block in the CMakeLists.txt file. |
| 129 | +CMAKE_START_MARKER = " # directories and files included conditionally (alphabetical order)" |
| 130 | +CMAKE_END_MARKER = " # end of directories and files included conditionally (alphabetical order)" |
| 131 | + |
| 132 | +def insert_cmake_rule(filepath: str, kconfig_option: str, subdir_name: str): |
| 133 | + """ |
| 134 | + Reads a CMakeLists.txt file, adds a new rule, and writes it back. |
| 135 | +
|
| 136 | + Args: |
| 137 | + filepath (str): Path to the CMakeLists.txt file. |
| 138 | + kconfig_option (str): The Kconfig flag to check. |
| 139 | + subdir_name (str): The subdirectory to add. |
| 140 | + """ |
| 141 | + if not os.path.exists(filepath): |
| 142 | + print(f"[ERROR] File not found at: '{filepath}'") |
| 143 | + return |
| 144 | + |
| 145 | + # --- 1. Read the entire file content --- |
| 146 | + with open(filepath, 'r', encoding='utf-8') as f: |
| 147 | + lines = f.readlines() |
| 148 | + |
| 149 | + # --- 2. Find the start and end of the managed block --- |
| 150 | + try: |
| 151 | + start_index = lines.index(CMAKE_START_MARKER + '\n') |
| 152 | + end_index = lines.index(CMAKE_END_MARKER + '\n') |
| 153 | + except ValueError: |
| 154 | + print(f"[ERROR] Could not find the required marker blocks in '{filepath}'.") |
| 155 | + print(f"Please ensure the file contains both '{CMAKE_START_MARKER}' and '{CMAKE_END_MARKER}'.") |
| 156 | + return |
| 157 | + |
| 158 | + # --- 3. Extract the lines before, during, and after the block --- |
| 159 | + lines_before = lines[:start_index + 1] |
| 160 | + block_lines = lines[start_index + 1 : end_index] |
| 161 | + lines_after = lines[end_index:] |
| 162 | + |
| 163 | + # --- 4. Parse the existing rules within the block --- |
| 164 | + rules = [] |
| 165 | + # Regex to find: if(CONFIG_NAME) ... add_subdirectory(subdir) ... endif() |
| 166 | + # This is robust against extra whitespace and blank lines. |
| 167 | + block_content = "".join(block_lines) |
| 168 | + pattern = re.compile( |
| 169 | + r"if\s*\((?P<kconfig>CONFIG_[A-Z0-9_]+)\)\s*" |
| 170 | + r"add_subdirectory\s*\((?P<subdir>[a-zA-Z0-9_]+)\)\s*" |
| 171 | + r"endif\(\)", |
| 172 | + re.DOTALL |
| 173 | + ) |
| 174 | + |
| 175 | + for match in pattern.finditer(block_content): |
| 176 | + rules.append(match.groupdict()) |
| 177 | + |
| 178 | + # --- 5. Check if the rule already exists --- |
| 179 | + if any(rule['kconfig'] == kconfig_option for rule in rules): |
| 180 | + print(f"[INFO] Rule for '{kconfig_option}' already exists. No changes made.") |
| 181 | + return |
| 182 | + |
| 183 | + # --- 6. Add the new rule and sort alphabetically --- |
| 184 | + rules.append({'kconfig': kconfig_option, 'subdir': subdir_name}) |
| 185 | + rules.sort(key=lambda r: r['kconfig']) |
| 186 | + print(f"Adding rule for '{kconfig_option}' -> '{subdir_name}' and re-sorting.") |
| 187 | + |
| 188 | + # --- 7. Rebuild the block content from the sorted rules --- |
| 189 | + new_block_lines = [] |
| 190 | + for i, rule in enumerate(rules): |
| 191 | + new_block_lines.append(f"\tif({rule['kconfig']})\n") |
| 192 | + new_block_lines.append(f"\t\tadd_subdirectory({rule['subdir']})\n") |
| 193 | + new_block_lines.append("\tendif()\n") |
| 194 | + |
| 195 | + # --- 8. Assemble the new file content and write it back --- |
| 196 | + new_content = "".join(lines_before + new_block_lines + lines_after) |
| 197 | + with open(filepath, 'w', encoding='utf-8') as f: |
| 198 | + f.write(new_content) |
| 199 | + |
| 200 | + print(f"Successfully updated '{filepath}'.") |
| 201 | + |
| 202 | +# Define the markers for the managed block in the Kconfig file. |
| 203 | +KCONFIG_START_MARKER = "# --- Kconfig Sources (alphabetical order) ---" |
| 204 | +KCONFIG_END_MARKER = "# --- End Kconfig Sources (alphabetical order) ---" |
| 205 | + |
| 206 | +def insert_kconfig_source(filepath: str, source_path: str): |
| 207 | + """ |
| 208 | + Reads a Kconfig file, adds a new rsource rule, and writes it back. |
| 209 | +
|
| 210 | + Args: |
| 211 | + filepath (str): Path to the Kconfig file. |
| 212 | + source_path (str): The path to the Kconfig file to be sourced. |
| 213 | + """ |
| 214 | + if not os.path.exists(filepath): |
| 215 | + print(f"[ERROR] File not found at: '{filepath}'") |
| 216 | + return |
| 217 | + |
| 218 | + # --- 1. Read the entire file content --- |
| 219 | + try: |
| 220 | + with open(filepath, 'r', encoding='utf-8') as f: |
| 221 | + lines = f.readlines() |
| 222 | + except Exception as e: |
| 223 | + print(f"[ERROR] Could not read file: {e}") |
| 224 | + return |
| 225 | + |
| 226 | + # --- 2. Find the start and end of the managed block --- |
| 227 | + try: |
| 228 | + start_index = lines.index(KCONFIG_START_MARKER + '\n') |
| 229 | + end_index = lines.index(KCONFIG_END_MARKER + '\n') |
| 230 | + except ValueError: |
| 231 | + print(f"[ERROR] Could not find the required marker blocks in '{filepath}'.") |
| 232 | + print(f"Please ensure the file contains both '{KCONFIG_START_MARKER}' and '{KCONFIG_END_MARKER}'.") |
| 233 | + return |
| 234 | + |
| 235 | + # --- 3. Extract the lines before, during, and after the block --- |
| 236 | + lines_before = lines[:start_index + 1] |
| 237 | + block_lines = lines[start_index + 1 : end_index] |
| 238 | + lines_after = lines[end_index:] |
| 239 | + |
| 240 | + # --- 4. Parse the existing rsource rules within the block --- |
| 241 | + source_paths = [] |
| 242 | + # Regex to find: rsource "path/to/file" |
| 243 | + pattern = re.compile(r'rsource\s+"(?P<path>.*?)"') |
| 244 | + |
| 245 | + for line in block_lines: |
| 246 | + match = pattern.search(line) |
| 247 | + if match: |
| 248 | + source_paths.append(match.group('path')) |
| 249 | + |
| 250 | + # --- 5. Check if the source path already exists --- |
| 251 | + if source_path in source_paths: |
| 252 | + print(f"[INFO] Source path '{source_path}' already exists. No changes made.") |
| 253 | + return |
| 254 | + |
| 255 | + # --- 6. Add the new path and sort alphabetically --- |
| 256 | + source_paths.append(source_path) |
| 257 | + source_paths.sort() |
| 258 | + print(f"Adding source '{source_path}' and re-sorting.") |
| 259 | + |
| 260 | + # --- 7. Rebuild the block content from the sorted paths --- |
| 261 | + new_block_lines = [] |
| 262 | + for path in source_paths: |
| 263 | + new_block_lines.append(f'rsource "{path}"\n') |
| 264 | + |
| 265 | + # --- 8. Assemble the new file content and write it back --- |
| 266 | + new_content = "".join(lines_before + new_block_lines + lines_after) |
| 267 | + with open(filepath, 'w', encoding='utf-8') as f: |
| 268 | + f.write(new_content) |
| 269 | + |
| 270 | + print(f"Successfully updated '{filepath}'.") |
| 271 | + |
| 272 | +def main(): |
| 273 | + """ |
| 274 | + Main function to drive the script logic. |
| 275 | + """ |
| 276 | + print("--- SOF SDK New Module Creator ---") |
| 277 | + |
| 278 | + # Argument Validation --- |
| 279 | + if len(sys.argv) != 7: |
| 280 | + print("\n[ERROR] Invalid number of arguments.") |
| 281 | + print("Usage: sdk_create_module.py <modules_root_dir> <template_name> <new_module_name> uuid") |
| 282 | + sys.exit(1) |
| 283 | + |
| 284 | + modules_root = sys.argv[1] |
| 285 | + template_name = sys.argv[2] |
| 286 | + new_module_name = sys.argv[3] |
| 287 | + uuid_file = sys.argv[4] |
| 288 | + cmake_file = sys.argv[5] |
| 289 | + kconfig_file = sys.argv[6] |
| 290 | + |
| 291 | + template_dir = os.path.join(modules_root, template_name) |
| 292 | + new_module_dir = os.path.join(modules_root, new_module_name) |
| 293 | + |
| 294 | + print(f"\nConfiguration:") |
| 295 | + print(f" - Modules Root: '{modules_root}'") |
| 296 | + print(f" - Template Name: '{template_name}'") |
| 297 | + print(f" - New Module Name:'{new_module_name}'") |
| 298 | + print(f" - UUID file: '{uuid_file}'") |
| 299 | + print(f" - Cmake file: '{cmake_file}'") |
| 300 | + print(f" - Kconfig file: '{kconfig_file}'") |
| 301 | + |
| 302 | + # Check for Pre-existing Directories --- |
| 303 | + if not os.path.isdir(template_dir): |
| 304 | + print(f"\n[ERROR] Template directory not found at: '{template_dir}'") |
| 305 | + sys.exit(1) |
| 306 | + |
| 307 | + if os.path.exists(new_module_dir): |
| 308 | + print(f"\n[ERROR] A directory with the new module name already exists: '{new_module_dir}'") |
| 309 | + sys.exit(1) |
| 310 | + |
| 311 | + # Copy Template Directory --- |
| 312 | + try: |
| 313 | + print(f"\n[1/6] Copying template directory...") |
| 314 | + shutil.copytree(template_dir, new_module_dir) |
| 315 | + print(f" -> Successfully copied to '{new_module_dir}'") |
| 316 | + except OSError as e: |
| 317 | + print(f"\n[ERROR] Could not copy directory. Reason: {e}") |
| 318 | + sys.exit(1) |
| 319 | + |
| 320 | + # Rename Files and Replace Content --- |
| 321 | + # We walk through the newly created directory. |
| 322 | + print(f"\n[2/6] Renaming files and replacing content...") |
| 323 | + process_directory(new_module_dir, template_name, new_module_name) |
| 324 | + |
| 325 | + # Generate UUID for the new module --- |
| 326 | + print("\n[3/6] Generating UUID for module...") |
| 327 | + insert_uuid_name(uuid_file, new_module_name) |
| 328 | + |
| 329 | + # Add CMake rule for new module --- |
| 330 | + print("\n[4/6] Module creation process finished successfully!") |
| 331 | + kconfig_option = f"CONFIG_COMP_{new_module_name.upper()}" |
| 332 | + print(f" -> Adding CMake rule for '{kconfig_option}'") |
| 333 | + insert_cmake_rule(cmake_file, kconfig_option, new_module_name) |
| 334 | + |
| 335 | + # Add Kconfig rsource for new module |
| 336 | + print("\n[5/6] Module creation process finished successfully!") |
| 337 | + insert_kconfig_source(kconfig_file, f"{new_module_name}/Kconfig") |
| 338 | + |
| 339 | + print("\n[6/6] Module creation process finished successfully!") |
| 340 | + print("--- Done ---") |
| 341 | + |
| 342 | +if __name__ == "__main__": |
| 343 | + main() |
| 344 | + |
0 commit comments