forked from ArthurkaX/cds-text-sync
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProject_Build.py
More file actions
381 lines (314 loc) · 15.2 KB
/
Project_Build.py
File metadata and controls
381 lines (314 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# -*- coding: utf-8 -*-
"""
Project_Build.py - Trigger build in CODESYS IDE
Compiles the active application and reports errors/warnings.
"""
import os
import time
import sys
from codesys_utils import safe_str, init_logging, load_base_dir
def build_project(projects_obj=None, silent=False):
"""Build the active application in CODESYS and generate build.log"""
from System import Guid
# Resolve projects object
if projects_obj is None:
projects_obj = globals().get("projects")
if projects_obj is None:
try:
import __main__
projects_obj = getattr(__main__, "projects", None)
except:
pass
if projects_obj is None:
msg = "Error: 'projects' object not found."
if not silent:
system.ui.error(msg)
else:
print(msg)
return
if not projects_obj.primary:
msg = "Error: No project open!"
if not silent:
system.ui.error(msg)
else:
print(msg)
return
# Find active application
app = projects_obj.primary.active_application
if not app:
# Fallback: find first application in project
def find_app(obj):
for child in obj.get_children():
if str(child.type).lower() == "6394ad93-46a4-4927-8819-c1ca8654c6ad": # Application GUID
return child
res = find_app(child)
if res: return res
return None
app = find_app(projects_obj.primary)
if not app:
msg = "Error: No active application found to build."
if not silent:
system.ui.error(msg)
else:
print(msg)
return
# CODESYS Build GUID Category
BUILD_CATEGORY = Guid("97F48D64-A2A3-4856-B640-75C046E37EA9")
print("=== Starting Project Build ===")
print("Application: " + safe_str(app.get_name()))
# Clear previous build messages
try:
system.clear_messages(BUILD_CATEGORY)
except:
pass
start_time = time.time()
# Log Header for build.log
log_lines = []
log_lines.append("------ Build started: Application: {} ------".format(safe_str(app.get_name())))
log_lines.append("Typify code...") # Aesthetic phase marker
try:
# Trigger Build
app.build()
elapsed = time.time() - start_time
# Retrieve messages
messages = system.get_message_objects(BUILD_CATEGORY)
error_count = 0
warning_count = 0
# Try to get project name safely
project_name = "Unknown Project"
try:
# projects.primary on some versions returns a project object whose string
# representation is complex. Let's try to get a clean name.
p = projects_obj.primary
if hasattr(p, "name"):
project_name = safe_str(p.name)
elif hasattr(p, "get_name"):
project_name = safe_str(p.get_name())
# If it's still a long path or object string, take the filename
if "\\" in project_name or "/" in project_name:
project_name = os.path.basename(project_name).replace(".project", "")
if "Project(" in project_name:
# Fallback: try to find the path in the string
import re
match = re.search(r"stPath=([^,\)]+)", project_name)
if match:
project_name = os.path.basename(match.group(1)).replace(".project", "")
except:
pass
app_name = safe_str(app.get_name())
for msg in messages:
msg_text = safe_str(msg.text)
# Skip messages that are just headers/footers (avoid double logging)
if "Build started" in msg_text or "Compile complete" in msg_text:
continue
sev = str(msg.severity)
if "Error" in sev: error_count += 1
if "Warning" in sev: warning_count += 1
# Format ID using old-school % for maximum compatibility with IronPython
# prefix is usually 'C', number is the code like 18
prefix = safe_str(msg.prefix) if msg.prefix else ""
if msg.number and msg.number > 0:
msg_id = "%s%04d" % (prefix, msg.number)
else:
msg_id = prefix
desc = "{}: {}".format(msg_id, msg_text)
# Object information
obj_str = "N/A"
obj_ref = None
if hasattr(msg, "object") and msg.object:
try:
obj_ref = msg.object
obj_str = "{} [{}]".format(safe_str(obj_ref.get_name()), app_name)
except:
obj_str = str(msg.object) if msg.object else "N/A"
# Position information
pos_str = ""
msg_line = 0
msg_col = 0
section = ""
# --- Attempt 1: Default calculation from 'position' index ---
# Try to get position index
pos_index = getattr(msg, "position", -1)
decl_text = ""
impl_text = ""
if obj_ref:
if hasattr(obj_ref, "textual_declaration") and obj_ref.textual_declaration:
decl_text = safe_str(obj_ref.textual_declaration.text)
if hasattr(obj_ref, "textual_implementation") and obj_ref.textual_implementation:
impl_text = safe_str(obj_ref.textual_implementation.text)
if pos_index >= 0 and obj_ref:
try:
target_text = None
rel_index = pos_index
# Check if index is within Declaration
if pos_index < len(decl_text):
target_text = decl_text
section = "(Decl)"
else:
# Assume it is in Implementation
rel_index = pos_index - len(decl_text)
target_text = impl_text
section = "(Impl)"
if target_text is not None and rel_index >= 0:
if rel_index > len(target_text): rel_index = len(target_text)
part = target_text[:rel_index]
lines = part.split('\n')
msg_line = len(lines)
msg_col = len(lines[-1]) + 1
except:
pass
# --- Attempt 2: Heuristic Text Search (Override if found) ---
# If the default calculation seems suspect or purely to improve accuracy,
# we search for the offending code in the text.
try:
import re
candidates = []
# 1. Quoted text inside message
candidates.extend(re.findall(r"'([^']+)'", msg_text))
# 2. "instead of <Identifier>" pattern (common in syntax errors)
m_instead = re.search(r"instead of\s+([a-zA-Z0-9_]+)", msg_text)
if m_instead:
candidates.append(m_instead.group(1))
# Keywords that are valid standalone in Declaration (no colon needed)
decl_keywords = {'VAR', 'END_VAR', 'VAR_INPUT', 'VAR_OUTPUT', 'VAR_IN_OUT',
'VAR_TEMP', 'VAR_GLOBAL', 'VAR_CONFIG', 'VAR_EXTERNAL', 'VAR_STAT',
'PROGRAM', 'FUNCTION_BLOCK', 'FUNCTION', 'TYPE', 'END_TYPE',
'STRUCT', 'END_STRUCT', 'PROTECTED', 'INTERNAL'}
best_match = None # (line, col, section)
high_priority_found = False
min_dist = 999999999
for item in candidates:
# Allow length 1 items only if they were explicitly captured (e.g. "j" from "instead of j")
# But filter out extremely common delimiters if they slipped in (like , or ;) unless quoted
if len(item) < 1: continue
# Regex for whole word search to avoid partial matches
pattern = r"\b" + re.escape(item) + r"\b"
# --- Search Declaration ---
for m in re.finditer(pattern, decl_text):
idx = m.start()
# Calculate Line/Col
part = decl_text[:idx]
lines = part.split('\n')
match_line = len(lines)
match_col = len(lines[-1]) + 1
# Analyze content for High Priority (Code in Decl)
lines_all = decl_text.split('\n')
if match_line <= len(lines_all):
line_content = lines_all[match_line-1].strip()
# Check if line has colon (valid decl) or is a keyword (valid block marker)
has_colon = ":" in line_content
# Check if it starts with a keyword
is_keyword = any(line_content.startswith(k) for k in decl_keywords) or line_content in decl_keywords
if not has_colon and not is_keyword:
# High Priority: This looks like executable code in declaration!
msg_line = match_line
msg_col = match_col
section = "(Decl)"
high_priority_found = True
break
# Calculate distance to reported position (if valid)
# Decl index is absolute 0..len
dist = abs(idx - pos_index)
if dist < min_dist:
min_dist = dist
best_match = (match_line, match_col, "(Decl)")
if high_priority_found: break
# --- Search Implementation ---
# Only search impl if we haven't found a High Priority Decl error
offset = len(decl_text)
for m in re.finditer(pattern, impl_text):
idx = m.start()
# Calculate Line/Col
part = impl_text[:idx]
lines = part.split('\n')
match_line = len(lines)
match_col = len(lines[-1]) + 1
# Calculate distance (Impl matches start after Decl)
abs_pos = offset + idx
dist = abs(abs_pos - pos_index)
if dist < min_dist:
min_dist = dist
best_match = (match_line, match_col, "(Impl)")
if high_priority_found: break
# Apply best match if no high priority one was set directly
if not high_priority_found and best_match:
msg_line = best_match[0]
msg_col = best_match[1]
section = best_match[2]
except:
pass
# --- Attempt 3: Regex Parse from Message Text (Fallback) ---
if msg_line == 0:
import re
line_match = re.search(r'[Ll]ine[:\s]+(\d+)', msg_text)
if line_match:
msg_line = int(line_match.group(1))
col_match = re.search(r'[Cc]olumn[:\s]+(\d+)', msg_text)
if col_match:
msg_col = int(col_match.group(1))
if msg_line > 0:
pos_str = "Line {}, Col {} {}".format(msg_line, msg_col, section)
# Sanitize description for table formatting
# 1. Remove newlines that break the row structure
clean_desc = desc.replace('\r', '').replace('\n', ' ')
# 2. Truncate if too long to maintain column width (optional, but good for cleanliness)
# if len(clean_desc) > 90: clean_desc = clean_desc[:87] + "..."
# Actually, standard format specifier {:<90} will not truncate, it just pads.
# If string is longer, it overflows. Table alignment breaks.
# So truncation is recommended for strict table.
if len(clean_desc) > 90:
clean_desc = clean_desc[:87] + "..."
# Recreate table-like row for log (Removed Project Column)
log_lines.append("{:<90} | {:<40} | {}".format(clean_desc, obj_str, pos_str))
# --- Formatting for File Output ---
# Add Header Table
header = "{:<90} | {:<40} | {}".format("Description", "Object", "Position")
separator = "-" * 160
# Insert Header at the top
log_lines.insert(0, separator)
log_lines.insert(0, header)
# Add Footer with separator
footer = "Compile complete -- {} errors, {} warnings".format(error_count, warning_count)
log_lines.append(separator)
log_lines.append(footer)
# Write to build.log in base directory
base_dir, _ = load_base_dir()
if base_dir and os.path.exists(base_dir):
log_path = os.path.join(base_dir, "build.log")
try:
import codecs
with codecs.open(log_path, "w", "utf-8") as f:
f.write("\n".join(log_lines))
print("Build log saved to: " + log_path)
except Exception as e:
print("Error saving build.log: " + str(e))
status = "Success" if error_count == 0 else "Failed"
msg_title = "Build " + status
msg_body = "{}\nErrors: {}\nWarnings: {}\nTime: {:.2f}s".format(
app_name, error_count, warning_count, elapsed
)
print(footer + " (Time: {:.2f}s)".format(elapsed))
# Feedback
try:
from codesys_ui import show_toast
show_toast(msg_title, msg_body)
except:
if not silent:
if error_count == 0:
system.ui.info(msg_body)
else:
system.ui.error(msg_body)
except Exception as e:
print("Build Error: " + str(e))
if not silent:
system.ui.error("Build process failed: " + str(e))
def main():
base_dir, error = load_base_dir()
if error:
# Build doesn't strictly need base_dir, but we check for consistency
pass
# Check if we are being run in silent mode (e.g. from Daemon)
is_silent = globals().get("SILENT", False)
build_project(silent=is_silent)
if __name__ == "__main__":
main()