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