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 ()
0 commit comments