Skip to content

Commit f4eb6a0

Browse files
committed
Add support for PlatformIO code generation
1 parent 8587fbd commit f4eb6a0

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed

generator/platformio_generator.py

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
"""
2+
TcMenu Automated Code Generator for PlatformIO
3+
==============================================
4+
5+
This script automates TcMenu code generation in a PlatformIO project. It is automatically run before each build,
6+
checking if the `.emf` file has changed and only regenerating code when necessary.
7+
8+
Available Options (platformio.ini)
9+
----------------------------------
10+
- **tcmenu_disable_generator**: (boolean/string, optional)
11+
If set to `true` (or `1`, `yes`), the script is disabled entirely. No generation occurs.
12+
Example:
13+
tcmenu_disable_generator = true
14+
15+
- **tcmenu_force_generation**: (boolean/string, optional)
16+
If set to `true` (or `1`, `yes`), the script always regenerates the code regardless of the file’s hash.
17+
Example:
18+
tcmenu_force_generation = true
19+
20+
- **tcmenu_generator_path**: (string, optional)
21+
Path to the TcMenu Designer generator executable. Example:
22+
tcmenu_generator_path = "C:/MyTools/TcMenuDesigner/tcMenuDesigner.exe"
23+
24+
- **tcmenu_project_file**: (string, optional)
25+
Custom path to the `.emf` (or project) file. Example:
26+
tcmenu_project_file = "/home/user/customMenus/myMenu.emf"
27+
28+
"""
29+
30+
import os
31+
import platform
32+
import pathlib
33+
import subprocess
34+
import hashlib
35+
36+
from platformio import fs
37+
from SCons.Script import Import
38+
39+
Import("env")
40+
41+
def find_tcmenu_generator():
42+
"""
43+
Determine the path to the TcMenu Designer generator executable based on:
44+
1) platformio.ini override (tcmenu_generator_path)
45+
2) host operating system defaults
46+
Return the executable path or None if not found.
47+
"""
48+
custom_generator_path = env.GetProjectOption("tcmenu_generator_path", default=None)
49+
if custom_generator_path and os.path.isfile(custom_generator_path):
50+
return custom_generator_path
51+
52+
system_name = platform.system().lower()
53+
if system_name.startswith("win"):
54+
default_path = "C:\\Program Files (x86)\\TcMenuDesigner\\tcmenu.exe"
55+
elif system_name.startswith("darwin"):
56+
# macOS
57+
default_path = "/Applications/tcMenuDesigner.app/Contents/MacOS/tcMenuDesigner/tcmenu"
58+
else:
59+
# Linux
60+
default_path = "/opt/tcmenudesigner/bin/tcMenuDesigner/tcmenu"
61+
62+
return default_path if os.path.isfile(default_path) else None
63+
64+
65+
def find_project_file():
66+
"""
67+
Locate the .emf (or project) file in the project root or via user-specified path in platformio.ini:
68+
tcmenu_project_file=<path>
69+
"""
70+
custom_emf = env.GetProjectOption("tcmenu_project_file", default=None)
71+
if custom_emf and os.path.isfile(custom_emf):
72+
return custom_emf
73+
74+
project_dir = env.subst("$PROJECT_DIR")
75+
emf_candidates = fs.match_src_files(project_dir, "+<*.emf>")
76+
if emf_candidates:
77+
return os.path.join(project_dir, emf_candidates[0])
78+
return None
79+
80+
81+
def compute_file_sha256(file_path):
82+
"""
83+
Compute the SHA-256 hash of the given file.
84+
"""
85+
with open(file_path, "rb") as f:
86+
data = f.read()
87+
return hashlib.sha256(data).hexdigest()
88+
89+
90+
def generate_code(tcmenu_generator, project_file):
91+
"""
92+
Run the TcMenu Designer command, generating code into .pio/build/<env>/tcmenu.
93+
"""
94+
build_dir = env.subst("$BUILD_DIR")
95+
tcmenu_output_dir = os.path.join(build_dir, "tcmenu")
96+
97+
os.makedirs(tcmenu_output_dir, exist_ok=True)
98+
99+
old_cwd = os.getcwd()
100+
try:
101+
# Change directory to the output directory
102+
os.chdir(tcmenu_output_dir)
103+
104+
cmd = [
105+
tcmenu_generator,
106+
"generate",
107+
"--emf-file",
108+
project_file
109+
]
110+
print(f"[TcMenu] Generating code with command: {' '.join(cmd)}")
111+
112+
result = subprocess.run(cmd, check=True, capture_output=True)
113+
stdout_str = result.stdout.decode("utf-8")
114+
if stdout_str.strip():
115+
print("[TcMenu] Output:\n", stdout_str)
116+
117+
except subprocess.CalledProcessError as e:
118+
print(f"[TcMenu] Warning: TcMenu generation failed: {e}")
119+
print("[TcMenu] Continuing build anyway...")
120+
121+
finally:
122+
# Always restore the original working directory
123+
os.chdir(old_cwd)
124+
125+
def remove_duplicates(tcmenu_output_dir):
126+
"""
127+
Remove or skip duplicates if user code is in 'src/'.
128+
The user code always takes precedence over generated code.
129+
"""
130+
project_src = os.path.join(env.subst("$PROJECT_DIR"), "src")
131+
if not os.path.isdir(tcmenu_output_dir) or not os.path.isdir(project_src):
132+
return
133+
134+
for root, _, files in os.walk(tcmenu_output_dir):
135+
for f in files:
136+
generated_file = os.path.join(root, f)
137+
user_file = os.path.join(project_src, f)
138+
if os.path.isfile(user_file):
139+
print(f"[TcMenu] Skipping generated file because user code takes precedence: {generated_file}")
140+
# Optionally remove or rename the generated file here:
141+
# os.remove(generated_file)
142+
143+
144+
def main():
145+
# Check if script is disabled
146+
disable_generator_str = env.GetProjectOption("tcmenu_disable_generator", default="false").lower()
147+
if disable_generator_str in ["true", "1", "yes"]:
148+
print("[TcMenu] Script is disabled via 'tcmenu_disable_generator'. Skipping code generation.")
149+
return
150+
151+
print("[TcMenu] Starting code generation script (SHA-256 check).")
152+
153+
# Locate the TcMenu generator executable
154+
tcmenu_generator = find_tcmenu_generator()
155+
if not tcmenu_generator:
156+
print("[TcMenu] WARNING: TcMenu generator not found. Code generation will be skipped.")
157+
return
158+
159+
# Locate the project file (i.e., .emf)
160+
project_file = find_project_file()
161+
if not project_file:
162+
print("[TcMenu] WARNING: No project (.emf) file found. Code generation will be skipped.")
163+
return
164+
165+
# Determine if we should force generation
166+
force_generation_str = env.GetProjectOption("tcmenu_force_generation", default="false").lower()
167+
force_generation = force_generation_str in ["true", "1", "yes"]
168+
169+
# Compute SHA-256 of the project file
170+
project_sha = compute_file_sha256(project_file)
171+
172+
build_dir = env.subst("$BUILD_DIR")
173+
tcmenu_output_dir = os.path.join(build_dir, "tcmenu")
174+
os.makedirs(tcmenu_output_dir, exist_ok=True)
175+
176+
# Store the last known SHA-256 in a file
177+
sha_file_path = os.path.join(tcmenu_output_dir, "tcmenu.project.sha256")
178+
179+
# Determine if we need to regenerate
180+
need_generate = True
181+
if not force_generation:
182+
try:
183+
last_sha = pathlib.Path(sha_file_path).read_text().strip()
184+
if last_sha == project_sha:
185+
need_generate = False
186+
print("[TcMenu] Skipping code generation: Project file unchanged.")
187+
except FileNotFoundError:
188+
pass
189+
190+
if need_generate:
191+
generate_code(tcmenu_generator, project_file)
192+
# Write the new SHA-256
193+
pathlib.Path(sha_file_path).write_text(project_sha)
194+
# Remove duplicates (skip or remove existing user code)
195+
remove_duplicates(tcmenu_output_dir)
196+
else:
197+
# If skipping generation, still remove duplicates
198+
remove_duplicates(tcmenu_output_dir)
199+
200+
print("[TcMenu] Finished code generation script.")
201+
202+
203+
# Run the generator script
204+
main()

library.json

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
"authors": "tcmenu"
2727
}
2828
],
29+
"build": {
30+
"extraScript": "generator/platformio_generator.py"
31+
},
2932
"version": "4.4.0",
3033
"license": "Apache-2.0",
3134
"frameworks": "arduino, mbed",

0 commit comments

Comments
 (0)