Skip to content

Commit d9df40c

Browse files
committed
sdk: modules: Add script to generate a new module from template.
Add a convenience script that will generate new module sources, uuid, Kconfig and Cmake to speed up module development. Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
1 parent 1663c73 commit d9df40c

File tree

1 file changed

+344
-0
lines changed

1 file changed

+344
-0
lines changed

scripts/sdk-create-module.py

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
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

Comments
 (0)