Skip to content

Commit 2946297

Browse files
committed
scripts: module: helper script to create a new module from template
This scripts creates a new module from template and renames files, code to the new module name alongside creating uuid and Cmake and Kconfig entries. Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
1 parent cf01b6c commit 2946297

1 file changed

Lines changed: 397 additions & 0 deletions

File tree

scripts/sdk-create-module.py

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
#
4+
# Create a new module directory with a basic structure
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+
# ======================================================================================
203+
#
204+
# Title: add_kconfig_source.py
205+
# Description: A script to insert a new 'rsource' entry into a Kconfig file.
206+
# The script maintains alphabetical order of the Kconfig options
207+
# within a specially marked, managed block.
208+
#
209+
# Usage: python3 add_kconfig_source.py <path_to_kconfig> <KCONFIG_OPTION> <path_to_source_kconfig>
210+
#
211+
# Arguments:
212+
# path_to_kconfig: The full path to the main Kconfig file to be edited.
213+
# KCONFIG_OPTION: The Kconfig boolean (e.g., CONFIG_USE_ADC_DRIVER).
214+
# path_to_source_kconfig: The path to the Kconfig file to be sourced,
215+
# typically relative (e.g., "drivers/adc/Kconfig").
216+
#
217+
# Example:
218+
# python3 add_kconfig_source.py project/Kconfig CONFIG_USE_GPIO_DRIVER "drivers/gpio/Kconfig"
219+
#
220+
# Pre-requisite:
221+
# The target Kconfig file must contain a managed block, like this:
222+
#
223+
# # --- Kconfig Sources (Managed by script) ---
224+
# # (Content is managed here)
225+
# # --- End Kconfig Sources ---
226+
#
227+
# ======================================================================================
228+
229+
# Define the markers for the managed block in the Kconfig file.
230+
KCONFIG_START_MARKER = "# --- Kconfig Sources (alphabetical order) ---"
231+
KCONFIG_END_MARKER = "# --- End Kconfig Sources (alphabetical order) ---"
232+
233+
def insert_kconfig_source(filepath: str, source_path: str):
234+
"""
235+
Reads a Kconfig file, adds a new rsource rule, and writes it back.
236+
237+
Args:
238+
filepath (str): Path to the Kconfig file.
239+
source_path (str): The path to the Kconfig file to be sourced.
240+
"""
241+
if not os.path.exists(filepath):
242+
print(f"[ERROR] File not found at: '{filepath}'")
243+
return
244+
245+
# --- 1. Read the entire file content ---
246+
try:
247+
with open(filepath, 'r', encoding='utf-8') as f:
248+
lines = f.readlines()
249+
except Exception as e:
250+
print(f"[ERROR] Could not read file: {e}")
251+
return
252+
253+
# --- 2. Find the start and end of the managed block ---
254+
try:
255+
start_index = lines.index(KCONFIG_START_MARKER + '\n')
256+
end_index = lines.index(KCONFIG_END_MARKER + '\n')
257+
except ValueError:
258+
print(f"[ERROR] Could not find the required marker blocks in '{filepath}'.")
259+
print(f"Please ensure the file contains both '{KCONFIG_START_MARKER}' and '{KCONFIG_END_MARKER}'.")
260+
return
261+
262+
# --- 3. Extract the lines before, during, and after the block ---
263+
lines_before = lines[:start_index + 1]
264+
block_lines = lines[start_index + 1 : end_index]
265+
lines_after = lines[end_index:]
266+
267+
# --- 4. Parse the existing rsource rules within the block ---
268+
source_paths = []
269+
# Regex to find: rsource "path/to/file"
270+
pattern = re.compile(r'rsource\s+"(?P<path>.*?)"')
271+
272+
for line in block_lines:
273+
match = pattern.search(line)
274+
if match:
275+
source_paths.append(match.group('path'))
276+
277+
# --- 5. Check if the source path already exists ---
278+
if source_path in source_paths:
279+
print(f"[INFO] Source path '{source_path}' already exists. No changes made.")
280+
return
281+
282+
# --- 6. Add the new path and sort alphabetically ---
283+
source_paths.append(source_path)
284+
source_paths.sort()
285+
print(f"Adding source '{source_path}' and re-sorting.")
286+
287+
# --- 7. Rebuild the block content from the sorted paths ---
288+
new_block_lines = []
289+
for path in source_paths:
290+
new_block_lines.append(f'rsource "{path}"\n')
291+
292+
# --- 8. Assemble the new file content and write it back ---
293+
new_content = "".join(lines_before + new_block_lines + lines_after)
294+
with open(filepath, 'w', encoding='utf-8') as f:
295+
f.write(new_content)
296+
297+
print(f"Successfully updated '{filepath}'.")
298+
299+
300+
# ======================================================================================
301+
#
302+
# Title: create_c_module.py
303+
# Description: A script to create a new C module by copying a template
304+
# directory. It renames the directory, renames files within it,
305+
# and replaces all occurrences of the template name inside
306+
# the relevant C source and header files.
307+
#
308+
# Usage: python3 create_c_module.py <modules_root_dir> <template_name> <new_module_name>
309+
#
310+
# Arguments:
311+
# modules_root_dir: The top-level directory where all modules are stored.
312+
# template_name: The name of the template directory to be copied.
313+
# new_module_name: The name for the new module.
314+
#
315+
# Example:
316+
# # Creates a new module 'adc_driver' from the 'template' module
317+
# # located in the 'src/drivers' directory.
318+
# python3 create_c_module.py src/drivers template adc_driver
319+
#
320+
# To run with command-line arguments:
321+
# python3 insert_sorted_uuid.py data/users.txt "My Name"
322+
#
323+
# ======================================================================================
324+
325+
def main():
326+
"""
327+
Main function to drive the script logic.
328+
"""
329+
print("--- C Module Creator ---")
330+
331+
# Argument Validation ---
332+
if len(sys.argv) != 7:
333+
print("\n[ERROR] Invalid number of arguments.")
334+
print("Usage: python3 create_c_module.py <modules_root_dir> <template_name> <new_module_name> uuid")
335+
sys.exit(1)
336+
337+
modules_root = sys.argv[1]
338+
template_name = sys.argv[2]
339+
new_module_name = sys.argv[3]
340+
uuid_file = sys.argv[4]
341+
cmake_file = sys.argv[5]
342+
kconfig_file = sys.argv[6]
343+
344+
template_dir = os.path.join(modules_root, template_name)
345+
new_module_dir = os.path.join(modules_root, new_module_name)
346+
347+
print(f"\nConfiguration:")
348+
print(f" - Modules Root: '{modules_root}'")
349+
print(f" - Template Name: '{template_name}'")
350+
print(f" - New Module Name:'{new_module_name}'")
351+
print(f" - UUID file: '{uuid_file}'")
352+
print(f" - Cmake file: '{cmake_file}'")
353+
print(f" - Kconfig file: '{kconfig_file}'")
354+
355+
# Check for Pre-existing Directories ---
356+
if not os.path.isdir(template_dir):
357+
print(f"\n[ERROR] Template directory not found at: '{template_dir}'")
358+
sys.exit(1)
359+
360+
if os.path.exists(new_module_dir):
361+
print(f"\n[ERROR] A directory with the new module name already exists: '{new_module_dir}'")
362+
sys.exit(1)
363+
364+
# Copy Template Directory ---
365+
try:
366+
print(f"\n[1/6] Copying template directory...")
367+
shutil.copytree(template_dir, new_module_dir)
368+
print(f" -> Successfully copied to '{new_module_dir}'")
369+
except OSError as e:
370+
print(f"\n[ERROR] Could not copy directory. Reason: {e}")
371+
sys.exit(1)
372+
373+
# Rename Files and Replace Content ---
374+
# We walk through the newly created directory.
375+
print(f"\n[2/6] Renaming files and replacing content...")
376+
process_directory(new_module_dir, template_name, new_module_name)
377+
378+
# Generate UUID for the new module ---
379+
print("\n[3/6] Generating UUID for module...")
380+
insert_uuid_name(uuid_file, new_module_name)
381+
382+
# Add CMake rule for new module ---
383+
print("\n[4/6] Module creation process finished successfully!")
384+
kconfig_option = f"CONFIG_COMP_{new_module_name.upper()}"
385+
print(f" -> Adding CMake rule for '{kconfig_option}'")
386+
insert_cmake_rule(cmake_file, kconfig_option, new_module_name)
387+
388+
# Add Kconfig rsource for new module
389+
print("\n[5/6] Module creation process finished successfully!")
390+
insert_kconfig_source(kconfig_file, f"{new_module_name}/Kconfig")
391+
392+
print("\n[6/6] Module creation process finished successfully!")
393+
print("--- Done ---")
394+
395+
if __name__ == "__main__":
396+
main()
397+

0 commit comments

Comments
 (0)