siliconcompiler 0.33.2__py3-none-any.whl → 0.34.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. siliconcompiler/__init__.py +2 -0
  2. siliconcompiler/_metadata.py +1 -1
  3. siliconcompiler/apps/sc_issue.py +5 -3
  4. siliconcompiler/apps/sc_remote.py +0 -17
  5. siliconcompiler/checklist.py +1 -1
  6. siliconcompiler/core.py +34 -47
  7. siliconcompiler/dependencyschema.py +392 -0
  8. siliconcompiler/design.py +664 -0
  9. siliconcompiler/flowgraph.py +32 -1
  10. siliconcompiler/package/__init__.py +383 -223
  11. siliconcompiler/package/git.py +75 -77
  12. siliconcompiler/package/github.py +70 -97
  13. siliconcompiler/package/https.py +77 -93
  14. siliconcompiler/packageschema.py +260 -0
  15. siliconcompiler/pdk.py +2 -2
  16. siliconcompiler/remote/client.py +15 -3
  17. siliconcompiler/report/dashboard/cli/board.py +1 -1
  18. siliconcompiler/scheduler/__init__.py +3 -1382
  19. siliconcompiler/scheduler/docker.py +268 -0
  20. siliconcompiler/scheduler/run_node.py +10 -16
  21. siliconcompiler/scheduler/scheduler.py +308 -0
  22. siliconcompiler/scheduler/schedulernode.py +934 -0
  23. siliconcompiler/scheduler/slurm.py +147 -163
  24. siliconcompiler/scheduler/taskscheduler.py +39 -52
  25. siliconcompiler/schema/__init__.py +3 -3
  26. siliconcompiler/schema/baseschema.py +234 -10
  27. siliconcompiler/schema/editableschema.py +4 -0
  28. siliconcompiler/schema/journal.py +210 -0
  29. siliconcompiler/schema/namedschema.py +31 -2
  30. siliconcompiler/schema/parameter.py +14 -1
  31. siliconcompiler/schema/parametervalue.py +1 -34
  32. siliconcompiler/schema/schema_cfg.py +210 -349
  33. siliconcompiler/tool.py +61 -20
  34. siliconcompiler/tools/builtin/concatenate.py +2 -2
  35. siliconcompiler/tools/builtin/verify.py +1 -2
  36. siliconcompiler/tools/openroad/scripts/common/procs.tcl +27 -25
  37. siliconcompiler/tools/vpr/route.py +69 -0
  38. siliconcompiler/toolscripts/_tools.json +4 -4
  39. siliconcompiler/utils/__init__.py +2 -23
  40. siliconcompiler/utils/flowgraph.py +5 -5
  41. siliconcompiler/utils/logging.py +2 -1
  42. {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.0.dist-info}/METADATA +4 -3
  43. {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.0.dist-info}/RECORD +47 -42
  44. siliconcompiler/scheduler/docker_runner.py +0 -254
  45. siliconcompiler/schema/journalingschema.py +0 -242
  46. {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.0.dist-info}/WHEEL +0 -0
  47. {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.0.dist-info}/entry_points.txt +0 -0
  48. {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.0.dist-info}/licenses/LICENSE +0 -0
  49. {siliconcompiler-0.33.2.dist-info → siliconcompiler-0.34.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,934 @@
1
+ import logging
2
+ import os
3
+ import shutil
4
+ import sys
5
+ import time
6
+
7
+ import os.path
8
+
9
+ from logging.handlers import QueueHandler
10
+
11
+ from siliconcompiler import utils, sc_open
12
+ from siliconcompiler import Schema
13
+ from siliconcompiler import NodeStatus
14
+
15
+ from siliconcompiler.tools._common import input_file_node_name, record_metric
16
+
17
+ from siliconcompiler.record import RecordTime, RecordTool
18
+ from siliconcompiler.schema import Journal
19
+ from siliconcompiler.scheduler import send_messages
20
+
21
+
22
+ class SchedulerNode:
23
+ def __init__(self, chip, step, index, replay=False):
24
+ self.__step = step
25
+ self.__index = index
26
+ self.__chip = chip
27
+
28
+ self.__design = self.__chip.design
29
+
30
+ self.__job = self.__chip.get('option', 'jobname')
31
+ self.__record_user_info = self.__chip.get("option", "track",
32
+ step=self.__step, index=self.__index)
33
+ self.__pipe = None
34
+ self.__failed_log_lines = 20
35
+ self.__error = False
36
+ self.__generate_test_case = not replay
37
+ self.__replay = replay
38
+ self.__hash = self.__chip.get("option", "hash")
39
+ self.__builtin = False
40
+
41
+ self.__enforce_inputfiles = self.__chip.get('option', 'strict')
42
+ self.__enforce_outputfiles = self.__chip.get('option', 'strict')
43
+
44
+ flow = self.__chip.get('option', 'flow')
45
+ self.__is_entry_node = (self.__step, self.__index) in \
46
+ self.__chip.get("flowgraph", flow, field="schema").get_entry_nodes()
47
+
48
+ self.__jobworkdir = self.__chip.getworkdir(jobname=self.__job)
49
+ self.__workdir = self.__chip.getworkdir(jobname=self.__job,
50
+ step=self.__step, index=self.__index)
51
+ self.__manifests = {
52
+ "input": os.path.join(self.__workdir, "inputs", f"{self.__design}.pkg.json"),
53
+ "output": os.path.join(self.__workdir, "outputs", f"{self.__design}.pkg.json")
54
+ }
55
+ self.__logs = {
56
+ "sc": os.path.join(self.__workdir, f"sc_{self.__step}{self.__index}.log"),
57
+ "exe": os.path.join(self.__workdir, f"{self.__step}.log")
58
+ }
59
+ self.__replay_script = os.path.join(self.__workdir, "replay.sh")
60
+
61
+ self.set_queue(None, None)
62
+ self.init_state()
63
+
64
+ def init_state(self, assign_runtime=False):
65
+ self.__setup_schema_access()
66
+ if assign_runtime:
67
+ self.__task.set_runtime(self.__chip, step=self.__step, index=self.__index)
68
+
69
+ @staticmethod
70
+ def init(chip):
71
+ pass
72
+
73
+ @property
74
+ def is_local(self):
75
+ return True
76
+
77
+ @property
78
+ def has_error(self):
79
+ return self.__error
80
+
81
+ def set_builtin(self):
82
+ self.__builtin = True
83
+
84
+ @property
85
+ def is_builtin(self):
86
+ return self.__builtin
87
+
88
+ @property
89
+ def logger(self):
90
+ return self.__chip.logger
91
+
92
+ @property
93
+ def chip(self):
94
+ return self.__chip
95
+
96
+ @property
97
+ def step(self):
98
+ return self.__step
99
+
100
+ @property
101
+ def index(self):
102
+ return self.__index
103
+
104
+ @property
105
+ def design(self):
106
+ return self.__design
107
+
108
+ @property
109
+ def workdir(self):
110
+ return self.__workdir
111
+
112
+ @property
113
+ def jobworkdir(self):
114
+ return self.__jobworkdir
115
+
116
+ @property
117
+ def is_replay(self):
118
+ return self.__replay
119
+
120
+ @property
121
+ def task(self):
122
+ return self.__task
123
+
124
+ def get_manifest(self, input=False):
125
+ if input:
126
+ return self.__manifests["input"]
127
+ return self.__manifests["output"]
128
+
129
+ def get_log(self, type="exe"):
130
+ if type not in self.__logs:
131
+ raise ValueError(f"{type} is not a log")
132
+ return self.__logs[type]
133
+
134
+ @property
135
+ def replay_script(self):
136
+ return self.__replay_script
137
+
138
+ @property
139
+ def threads(self):
140
+ self.__task.set_runtime(self.__chip, step=self.__step, index=self.__index)
141
+ thread_count = self.__task.get("task", self.__task.task(), "threads",
142
+ step=self.__step, index=self.__index)
143
+ self.__task.set_runtime(None)
144
+ return thread_count
145
+
146
+ def set_queue(self, pipe, queue):
147
+ self.__pipe = pipe
148
+ self.__queue = queue
149
+
150
+ def __setup_schema_access(self):
151
+ flow = self.__chip.get('option', 'flow')
152
+ self.__flow = self.__chip.get("flowgraph", flow, field="schema")
153
+
154
+ tool = self.__flow.get(self.__step, self.__index, 'tool')
155
+ self.__task = self.__chip.schema.get("tool", tool, field="schema")
156
+ self.__record = self.__chip.schema.get("record", field="schema")
157
+ self.__metrics = self.__chip.schema.get("metric", field="schema")
158
+
159
+ def halt(self, msg=None):
160
+ if msg:
161
+ self.logger.error(msg)
162
+
163
+ self.__record.set("status", NodeStatus.ERROR, step=self.__step, index=self.__index)
164
+ try:
165
+ self.__chip.schema.write_manifest(self.__manifests["output"])
166
+ except FileNotFoundError:
167
+ self.logger.error(f"Failed to write manifest for {self.__step}{self.__index}.")
168
+
169
+ self.logger.error(f"Halting {self.__step}{self.__index} due to errors.")
170
+ send_messages.send(self.__chip, "fail", self.__step, self.__index)
171
+ sys.exit(1)
172
+
173
+ def setup(self):
174
+ self.__task.set_runtime(self.__chip, step=self.__step, index=self.__index)
175
+
176
+ # Run node setup.
177
+ self.logger.info(f'Setting up node {self.__step}{self.__index} with '
178
+ f'{self.__task.tool()}/{self.__task.task()}')
179
+ setup_ret = None
180
+ try:
181
+ setup_ret = self.__task.setup()
182
+ except Exception as e:
183
+ self.logger.error(f'Failed to run setup() for {self.__step}{self.__index} '
184
+ f'with {self.__task.tool()}/{self.__task.task()}')
185
+ self.__task.set_runtime(None)
186
+ raise e
187
+
188
+ self.__task.set_runtime(None)
189
+
190
+ if setup_ret is not None:
191
+ self.logger.warning(f'Removing {self.__step}{self.__index} due to {setup_ret}')
192
+ self.__record.set('status', NodeStatus.SKIPPED, step=self.__step, index=self.__index)
193
+
194
+ return False
195
+
196
+ return True
197
+
198
+ def check_previous_run_status(self, previous_run):
199
+ # Assume modified if flow does not match
200
+ if self.__flow.name() != previous_run.__flow.name():
201
+ self.logger.debug("Flow name changed")
202
+ return False
203
+
204
+ # Tool name
205
+ if self.__task.tool() != previous_run.__task.tool():
206
+ self.logger.debug("Tool name changed")
207
+ return False
208
+
209
+ # Task name
210
+ if self.__task.task() != previous_run.__task.task():
211
+ self.logger.debug("Task name changed")
212
+ return False
213
+
214
+ previous_status = previous_run.__chip.get("record", "status",
215
+ step=self.__step, index=self.__index)
216
+ if not NodeStatus.is_done(previous_status):
217
+ self.logger.debug("Previous step did not complete")
218
+ # Not complete
219
+ return False
220
+
221
+ if not NodeStatus.is_success(previous_status):
222
+ self.logger.debug("Previous step was not successful")
223
+ # Not a success
224
+ return False
225
+
226
+ # Check input nodes
227
+ log_level = self.logger.level
228
+ self.logger.setLevel(logging.CRITICAL)
229
+ sel_inputs = self.__task.select_input_nodes()
230
+ self.logger.setLevel(log_level)
231
+ if set(previous_run.__chip.get("record", "inputnode",
232
+ step=self.__step, index=self.__index)) != set(sel_inputs):
233
+ self.logger.warning(f'inputs to {self.__step}{self.__index} has been modified from '
234
+ 'previous run')
235
+ return False
236
+
237
+ # Check that all output files are present?
238
+
239
+ return True
240
+
241
+ def check_values_changed(self, previous_run, keys):
242
+ def print_warning(key):
243
+ self.logger.warning(f'[{",".join(key)}] in {self.__step}{self.__index} has been '
244
+ 'modified from previous run')
245
+
246
+ for key in sorted(keys):
247
+ if not self.__chip.valid(*key) or not previous_run.__chip.valid(*key):
248
+ # Key is missing in either run
249
+ print_warning(key)
250
+ return True
251
+
252
+ param = self.__chip.get(*key, field=None)
253
+ step, index = self.__step, self.__index
254
+ if param.get(field='pernode').is_never():
255
+ step, index = None, None
256
+
257
+ check_val = param.get(step=step, index=index)
258
+ prev_val = previous_run.__chip.get(*key, step=step, index=index)
259
+
260
+ if check_val != prev_val:
261
+ print_warning(key)
262
+ return True
263
+
264
+ return False
265
+
266
+ def check_files_changed(self, previous_run, previous_time, keys):
267
+ use_hash = self.__hash and previous_run.__hash
268
+
269
+ def print_warning(key, reason):
270
+ self.logger.warning(f'[{",".join(key)}] ({reason}) in {self.__step}{self.__index} has '
271
+ 'been modified from previous run')
272
+
273
+ def get_file_time(path):
274
+ times = [os.path.getmtime(path)]
275
+ if os.path.isdir(path):
276
+ for path_root, _, files in os.walk(path):
277
+ for path_end in files:
278
+ times.append(os.path.getmtime(os.path.join(path_root, path_end)))
279
+
280
+ return max(times)
281
+
282
+ for key in sorted(keys):
283
+ param = self.__chip.get(*key, field=None)
284
+ step, index = self.__step, self.__index
285
+ if param.get(field='pernode').is_never():
286
+ step, index = None, None
287
+
288
+ if use_hash:
289
+ check_hash = self.__chip.hash_files(*key, update=False, check=False,
290
+ verbose=False, allow_cache=True,
291
+ step=step, index=index)
292
+ prev_hash = previous_run.__chip.get(*key, field='filehash',
293
+ step=step, index=index)
294
+
295
+ if check_hash != prev_hash:
296
+ print_warning(key, "file hash")
297
+ return True
298
+ else:
299
+ # check package values
300
+ check_val = self.__chip.get(*key, field='package', step=step, index=index)
301
+ prev_val = previous_run.__chip.get(*key, field='package', step=step, index=index)
302
+
303
+ if check_val != prev_val:
304
+ print_warning(key, "file package")
305
+ return True
306
+
307
+ for check_file in self.__chip.find_files(*key, step=step, index=index):
308
+ if get_file_time(check_file) > previous_time:
309
+ print_warning(key, "timestamp")
310
+ return True
311
+
312
+ return False
313
+
314
+ def get_check_changed_keys(self):
315
+ all_keys = set()
316
+
317
+ all_keys.update(self.__task.get('task', self.__task.task(), 'require',
318
+ step=self.__step, index=self.__index))
319
+
320
+ tool_task_prefix = ('tool', self.__task.tool(), 'task', self.__task.task())
321
+ for key in ('option', 'threads', 'prescript', 'postscript', 'refdir', 'script',):
322
+ all_keys.add(",".join([*tool_task_prefix, key]))
323
+
324
+ for env_key in self.__chip.getkeys(*tool_task_prefix, 'env'):
325
+ all_keys.add(",".join([*tool_task_prefix, 'env', env_key]))
326
+
327
+ value_keys = set()
328
+ path_keys = set()
329
+ for key in all_keys:
330
+ keypath = tuple(key.split(","))
331
+ if not self.__chip.valid(*keypath, default_valid=True):
332
+ raise KeyError(f"[{','.join(keypath)}] not found")
333
+ keytype = self.__chip.get(*keypath, field="type")
334
+ if 'file' in keytype or 'dir' in keytype:
335
+ path_keys.add(keypath)
336
+ else:
337
+ value_keys.add(keypath)
338
+
339
+ return value_keys, path_keys
340
+
341
+ def requires_run(self):
342
+ from siliconcompiler import Chip
343
+
344
+ # Load previous manifest
345
+ previous_node = None
346
+ previous_node_time = time.time()
347
+ if os.path.exists(self.__manifests["input"]):
348
+ previous_node_time = os.path.getmtime(self.__manifests["input"])
349
+ chip = Chip('')
350
+ try:
351
+ chip.schema = Schema(manifest=self.__manifests["input"], logger=self.logger)
352
+ except: # noqa E722
353
+ self.logger.debug("Input manifest failed to load")
354
+ return True
355
+ previous_node = SchedulerNode(chip, self.__step, self.__index)
356
+ previous_node.init_state(assign_runtime=True)
357
+ else:
358
+ # No manifest found so assume rerun is needed
359
+ self.logger.debug("Previous run did not generate input manifest")
360
+ return True
361
+
362
+ previous_node_end = None
363
+ if os.path.exists(self.__manifests["output"]):
364
+ chip = Chip('')
365
+ try:
366
+ chip.schema = Schema(manifest=self.__manifests["output"], logger=self.logger)
367
+ except: # noqa E722
368
+ self.logger.debug("Output manifest failed to load")
369
+ return True
370
+ previous_node_end = SchedulerNode(chip, self.__step, self.__index)
371
+ previous_node_end.init_state(assign_runtime=True)
372
+ else:
373
+ # No manifest found so assume rerun is needed
374
+ self.logger.debug("Previous run did not generate output manifest")
375
+ return True
376
+
377
+ self.init_state(assign_runtime=True)
378
+
379
+ if not self.check_previous_run_status(previous_node_end):
380
+ self.__task.set_runtime(None)
381
+ self.logger.debug("Previous run state failed")
382
+ return True
383
+
384
+ # Generate key paths to check
385
+ try:
386
+ value_keys, path_keys = self.get_check_changed_keys()
387
+ previous_value_keys, previous_path_keys = previous_node.get_check_changed_keys()
388
+ value_keys.update(previous_value_keys)
389
+ path_keys.update(previous_path_keys)
390
+ except KeyError:
391
+ self.__task.set_runtime(None)
392
+ self.logger.debug("Failed to acquire keys")
393
+ return True
394
+
395
+ self.__task.set_runtime(None)
396
+ if self.check_values_changed(previous_node, value_keys.union(path_keys)):
397
+ self.logger.debug("Key values changed")
398
+ return True
399
+
400
+ if self.check_files_changed(previous_node, previous_node_time, path_keys):
401
+ self.logger.debug("Files changed")
402
+ return True
403
+
404
+ return False
405
+
406
+ def setup_input_directory(self):
407
+ in_files = set(self.__task.get('task', self.__task.task(), 'input',
408
+ step=self.__step, index=self.__index))
409
+
410
+ for in_step, in_index in self.__record.get('inputnode',
411
+ step=self.__step, index=self.__index):
412
+ if NodeStatus.is_error(self.__record.get('status', step=in_step, index=in_index)):
413
+ self.halt(f'Halting step due to previous error in {in_step}{in_index}')
414
+
415
+ output_dir = os.path.join(
416
+ self.__chip.getworkdir(step=in_step, index=in_index), "outputs")
417
+ if not os.path.isdir(output_dir):
418
+ self.halt(f'Unable to locate outputs directory for {in_step}{in_index}: '
419
+ f'{output_dir}')
420
+
421
+ for outfile in os.scandir(output_dir):
422
+ if outfile.name == f'{self.__design}.pkg.json':
423
+ # Dont forward manifest
424
+ continue
425
+
426
+ new_name = input_file_node_name(outfile.name, in_step, in_index)
427
+ if self.__enforce_inputfiles:
428
+ if outfile.name not in in_files and new_name not in in_files:
429
+ continue
430
+
431
+ if outfile.is_file() or outfile.is_symlink():
432
+ utils.link_symlink_copy(outfile.path,
433
+ f'{self.__workdir}/inputs/{outfile.name}')
434
+ elif outfile.is_dir():
435
+ shutil.copytree(outfile.path,
436
+ f'{self.__workdir}/inputs/{outfile.name}',
437
+ dirs_exist_ok=True,
438
+ copy_function=utils.link_symlink_copy)
439
+
440
+ if new_name in in_files:
441
+ # perform rename
442
+ os.rename(f'{self.__workdir}/inputs/{outfile.name}',
443
+ f'{self.__workdir}/inputs/{new_name}')
444
+
445
+ def validate(self):
446
+ '''
447
+ Runtime checks called from _runtask().
448
+
449
+ - Make sure expected inputs exist.
450
+ - Make sure all required filepaths resolve correctly.
451
+ '''
452
+ error = False
453
+
454
+ required_inputs = self.__task.get('task', self.__task.task(), 'input',
455
+ step=self.__step, index=self.__index)
456
+
457
+ input_dir = os.path.join(self.__workdir, 'inputs')
458
+
459
+ for filename in required_inputs:
460
+ path = os.path.join(input_dir, filename)
461
+ if not os.path.exists(path):
462
+ self.logger.error(f'Required input {filename} not received for '
463
+ f'{self.__step}{self.__index}.')
464
+ error = True
465
+
466
+ all_required = self.__task.get('task', self.__task.task(), 'require',
467
+ step=self.__step, index=self.__index)
468
+ for item in all_required:
469
+ keypath = item.split(',')
470
+ if not self.__chip.valid(*keypath):
471
+ self.logger.error(f'Cannot resolve required keypath [{",".join(keypath)}].')
472
+ error = True
473
+ continue
474
+
475
+ param = self.__chip.get(*keypath, field=None)
476
+ check_step, check_index = self.__step, self.__index
477
+ if param.get(field='pernode').is_never():
478
+ check_step, check_index = None, None
479
+
480
+ value = self.__chip.get(*keypath, step=check_step, index=check_index)
481
+ if not value:
482
+ self.logger.error('No value set for required keypath '
483
+ f'[{",".join(keypath)}].')
484
+ error = True
485
+ continue
486
+
487
+ paramtype = param.get(field='type')
488
+ if ('file' in paramtype) or ('dir' in paramtype):
489
+ abspath = self.__chip.find_files(*keypath,
490
+ missing_ok=True,
491
+ step=check_step, index=check_index)
492
+
493
+ unresolved_paths = value
494
+ if not isinstance(abspath, list):
495
+ abspath = [abspath]
496
+ unresolved_paths = [unresolved_paths]
497
+
498
+ for path, setpath in zip(abspath, unresolved_paths):
499
+ if path is None:
500
+ self.logger.error(f'Cannot resolve path {setpath} in '
501
+ f'required file keypath [{",".join(keypath)}].')
502
+ error = True
503
+
504
+ return not error
505
+
506
+ def summarize(self):
507
+ for metric in ['errors', 'warnings']:
508
+ val = self.__metrics.get(metric, step=self.__step, index=self.__index)
509
+ if val is not None:
510
+ self.logger.info(f'Number of {metric}: {val}')
511
+
512
+ walltime = self.__metrics.get("tasktime", step=self.__step, index=self.__index)
513
+ self.logger.info(f"Finished task in {walltime:.2f}s")
514
+
515
+ def run(self):
516
+ '''
517
+ Private per node run method called by run().
518
+
519
+ The method takes in a step string and index string to indicate what
520
+ to run.
521
+
522
+ Note that since _runtask occurs in its own process with a separate
523
+ address space, any changes made to the `self` object will not
524
+ be reflected in the parent. We rely on reading/writing the chip manifest
525
+ to the filesystem to communicate updates between processes.
526
+ '''
527
+
528
+ # Setup chip
529
+ self.__chip._init_codecs()
530
+ self.__chip._init_logger(self.__step, self.__index, in_run=True)
531
+
532
+ if self.__queue:
533
+ self.logger.removeHandler(self.logger._console)
534
+ self.logger._console = QueueHandler(self.__queue)
535
+ self.logger.addHandler(self.logger._console)
536
+ self.__chip._init_logger_formats()
537
+
538
+ self.__chip.set('arg', 'step', self.__step)
539
+ self.__chip.set('arg', 'index', self.__index)
540
+
541
+ # Setup journaling
542
+ journal = Journal.access(self.__chip.schema)
543
+ journal.start()
544
+
545
+ # Must be after journaling to ensure journal is complete
546
+ self.init_state(assign_runtime=True)
547
+
548
+ # Make record of sc version and machine
549
+ self.__record.record_version(self.__step, self.__index)
550
+
551
+ # Record user information if enabled
552
+ if self.__record_user_info:
553
+ self.__record.record_userinformation(self.__step, self.__index)
554
+
555
+ # Start wall timer
556
+ self.__record.record_time(self.__step, self.__index, RecordTime.START)
557
+
558
+ # Setup run directory
559
+ self.__task.setup_work_directory(self.__workdir, remove_exist=not self.__replay)
560
+
561
+ cwd = os.getcwd()
562
+ os.chdir(self.__workdir)
563
+
564
+ # Attach siliconcompiler file log handler
565
+ self.__chip._add_file_logger(self.__logs["sc"])
566
+
567
+ # Select the inputs to this node
568
+ sel_inputs = self.__task.select_input_nodes()
569
+ if not self.__is_entry_node and not sel_inputs:
570
+ self.halt(f'No inputs selected for {self.__step}{self.__index}')
571
+ self.__record.set("inputnode", sel_inputs, step=self.__step, index=self.__index)
572
+
573
+ if self.__hash:
574
+ self.__hash_files_pre_execute()
575
+
576
+ # Forward data
577
+ if not self.__replay:
578
+ self.setup_input_directory()
579
+
580
+ # Write manifest prior to step running into inputs
581
+ self.__chip.write_manifest(self.__manifests["input"])
582
+
583
+ # Check manifest
584
+ if not self.validate():
585
+ self.halt("Failed to validate node setup. See previous errors")
586
+
587
+ try:
588
+ self.execute()
589
+ except Exception as e:
590
+ utils.print_traceback(self.logger, e)
591
+ self.halt()
592
+
593
+ # return to original directory
594
+ os.chdir(cwd)
595
+
596
+ # Stop journaling
597
+ journal.stop()
598
+
599
+ if self.__pipe:
600
+ self.__pipe.send(self.__chip.get("package", field="schema").get_path_cache())
601
+
602
+ def execute(self):
603
+ self.logger.info(f'Running in {self.__workdir}')
604
+
605
+ try:
606
+ self.__task.pre_process()
607
+ except Exception as e:
608
+ self.logger.error(
609
+ f"Pre-processing failed for {self.__task.tool()}/{self.__task.task()}")
610
+ utils.print_traceback(self.logger, e)
611
+ raise e
612
+
613
+ if self.__record.get('status', step=self.__step, index=self.__index) == NodeStatus.SKIPPED:
614
+ # copy inputs to outputs and skip execution
615
+ for in_step, in_index in self.__record.get('inputnode',
616
+ step=self.__step, index=self.__index):
617
+ in_workdir = self.__chip.getworkdir(step=in_step, index=in_index)
618
+ for outfile in os.scandir(f"{in_workdir}/outputs"):
619
+ if outfile.name == f'{self.__design}.pkg.json':
620
+ # Dont forward manifest
621
+ continue
622
+
623
+ if outfile.is_file() or outfile.is_symlink():
624
+ utils.link_symlink_copy(outfile.path,
625
+ f'outputs/{outfile.name}')
626
+ elif outfile.is_dir():
627
+ shutil.copytree(outfile.path,
628
+ f'outputs/{outfile.name}',
629
+ dirs_exist_ok=True,
630
+ copy_function=utils.link_symlink_copy)
631
+
632
+ send_messages.send(self.__chip, "skipped", self.__step, self.__index)
633
+ else:
634
+ org_env = os.environ.copy()
635
+ os.environ.update(self.__task.get_runtime_environmental_variables())
636
+
637
+ toolpath = self.__task.get_exe()
638
+ version = self.__task.get_exe_version()
639
+
640
+ if not self.__chip.get('option', 'novercheck', step=self.__step, index=self.__index):
641
+ if not self.__task.check_exe_version(version):
642
+ self.halt()
643
+
644
+ if version:
645
+ self.__record.record_tool(self.__step, self.__index, version, RecordTool.VERSION)
646
+
647
+ if toolpath:
648
+ self.__record.record_tool(self.__step, self.__index, toolpath, RecordTool.PATH)
649
+
650
+ send_messages.send(self.__chip, "begin", self.__step, self.__index)
651
+
652
+ try:
653
+ if not self.__replay:
654
+ self.__task.generate_replay_script(self.__replay_script, self.__workdir)
655
+ ret_code = self.__task.run_task(
656
+ self.__workdir,
657
+ self.__chip.get('option', 'quiet', step=self.__step, index=self.__index),
658
+ self.__chip.get('option', 'loglevel', step=self.__step, index=self.__index),
659
+ self.__chip.get('option', 'breakpoint', step=self.__step, index=self.__index),
660
+ self.__chip.get('option', 'nice', step=self.__step, index=self.__index),
661
+ self.__chip.get('option', 'timeout', step=self.__step, index=self.__index))
662
+ except Exception as e:
663
+ raise e
664
+
665
+ os.environ.clear()
666
+ os.environ.update(org_env)
667
+
668
+ if ret_code != 0:
669
+ msg = f'Command failed with code {ret_code}.'
670
+ if os.path.exists(self.__logs["exe"]):
671
+ if self.__chip.get('option', 'quiet', step=self.__step, index=self.__index):
672
+ # Print last N lines of log when in quiet mode
673
+ with sc_open(self.__logs["exe"]) as logfd:
674
+ loglines = logfd.read().splitlines()
675
+ for logline in loglines[-self.__failed_log_lines:]:
676
+ self.logger.error(logline)
677
+ # No log file for pure-Python tools.
678
+ msg += f' See log file {os.path.abspath(self.__logs["exe"])}'
679
+ self.logger.warning(msg)
680
+ self.__error = True
681
+
682
+ try:
683
+ self.__task.post_process()
684
+ except Exception as e:
685
+ self.logger.error(
686
+ f"Post-processing failed for {self.__task.tool()}/{self.__task.task()}")
687
+ utils.print_traceback(self.logger, e)
688
+ self.__error = True
689
+
690
+ self.check_logfile()
691
+
692
+ if not self.__error and self.__hash:
693
+ self.__hash_files_post_execute()
694
+
695
+ # Capture wall runtime
696
+ self.__record.record_time(self.__step, self.__index, RecordTime.END)
697
+ self.__metrics.record_tasktime(self.__step, self.__index, self.__record)
698
+ self.__metrics.record_totaltime(
699
+ self.__step, self.__index,
700
+ self.__flow,
701
+ self.__record)
702
+
703
+ # Save a successful manifest
704
+ if self.__record.get('status', step=self.__step, index=self.__index) != NodeStatus.SKIPPED:
705
+ self.__record.set('status', NodeStatus.SUCCESS, step=self.__step, index=self.__index)
706
+
707
+ self.__chip.write_manifest(self.__manifests["output"])
708
+
709
+ self.summarize()
710
+
711
+ if self.__error and self.__generate_test_case:
712
+ self.__generate_testcase()
713
+
714
+ # Stop if there are errors
715
+ errors = self.__metrics.get('errors', step=self.__step, index=self.__index)
716
+ if errors and not self.__chip.get('option', 'continue',
717
+ step=self.__step, index=self.__index):
718
+ self.halt(f'{self.__task.tool()}/{self.__task.task()} reported {errors} '
719
+ f'errors during {self.__step}{self.__index}')
720
+
721
+ if self.__error:
722
+ self.halt()
723
+
724
+ self.__report_output_files()
725
+
726
+ send_messages.send(self.__chip, "end", self.__step, self.__index)
727
+
728
+ def __generate_testcase(self):
729
+ from siliconcompiler.utils.issue import generate_testcase
730
+ import lambdapdk
731
+
732
+ generate_testcase(
733
+ self.__chip,
734
+ self.__step,
735
+ self.__index,
736
+ archive_directory=self.__jobworkdir,
737
+ include_pdks=False,
738
+ include_specific_pdks=lambdapdk.get_pdks(),
739
+ include_libraries=False,
740
+ include_specific_libraries=lambdapdk.get_libs(),
741
+ hash_files=self.__hash,
742
+ verbose_collect=False)
743
+
744
+ def check_logfile(self):
745
+ if self.__record.get('status', step=self.__step, index=self.__index) == NodeStatus.SKIPPED:
746
+ return
747
+
748
+ checks = {}
749
+ matches = {}
750
+ for suffix in self.__task.getkeys('task', self.__task.task(), 'regex'):
751
+ regexes = self.__task.get('task', self.__task.task(), 'regex', suffix,
752
+ step=self.__step, index=self.__index)
753
+ if not regexes:
754
+ continue
755
+
756
+ checks[suffix] = {
757
+ "report": open(f"{self.__step}.{suffix}", "w"),
758
+ "args": regexes,
759
+ "display": False
760
+ }
761
+ matches[suffix] = 0
762
+
763
+ def print_error(suffix, line):
764
+ self.logger.error(line)
765
+
766
+ def print_warning(suffix, line):
767
+ self.logger.warning(line)
768
+
769
+ def print_info(suffix, line):
770
+ self.logger.warning(f'{suffix}: {line}')
771
+
772
+ if not self.__chip.get('option', 'quiet', step=self.__step, index=self.__index):
773
+ for suffix, info in checks.items():
774
+ if suffix == 'errors':
775
+ info["display"] = print_error
776
+ elif suffix == "warnings":
777
+ info["display"] = print_warning
778
+ else:
779
+ info["display"] = print_info
780
+
781
+ # Order suffixes as follows: [..., 'warnings', 'errors']
782
+ ordered_suffixes = list(
783
+ filter(lambda key: key not in ['warnings', 'errors'], checks.keys()))
784
+ if 'warnings' in checks:
785
+ ordered_suffixes.append('warnings')
786
+ if 'errors' in checks:
787
+ ordered_suffixes.append('errors')
788
+
789
+ # Looping through patterns for each line
790
+ with sc_open(self.__logs["exe"]) as f:
791
+ line_count = sum(1 for _ in f)
792
+ right_align = len(str(line_count))
793
+ for suffix in ordered_suffixes:
794
+ # Start at the beginning of file again
795
+ f.seek(0)
796
+ for num, line in enumerate(f, start=1):
797
+ string = line
798
+ for item in checks[suffix]['args']:
799
+ if string is None:
800
+ break
801
+ else:
802
+ string = utils.grep(self.__chip, item, string)
803
+ if string is not None:
804
+ matches[suffix] += 1
805
+ # always print to file
806
+ line_with_num = f'{num: >{right_align}}: {string.strip()}'
807
+ print(line_with_num, file=checks[suffix]['report'])
808
+ # selectively print to display
809
+ if checks[suffix]["display"]:
810
+ checks[suffix]["display"](suffix, line_with_num)
811
+
812
+ for check in checks.values():
813
+ check['report'].close()
814
+
815
+ for metric in ("errors", "warnings"):
816
+ if metric in matches:
817
+ errors = self.__metrics.get(metric, step=self.__step, index=self.__index)
818
+ if errors is None:
819
+ errors = 0
820
+ errors += matches[metric]
821
+
822
+ sources = [os.path.basename(self.__logs["exe"])]
823
+ if self.__task.get('task', self.__task.task(), 'regex', metric,
824
+ step=self.__step, index=self.__index):
825
+ sources.append(f'{self.__step}.{metric}')
826
+
827
+ record_metric(self.__chip, self.__step, self.__index, metric, errors, sources)
828
+
829
+ def __hash_files_pre_execute(self):
830
+ for task_key in ('refdir', 'prescript', 'postscript', 'script'):
831
+ self.__chip.hash_files('tool', self.__task.tool(), 'task', self.__task.task(), task_key,
832
+ step=self.__step, index=self.__index, check=False,
833
+ allow_cache=True, verbose=False)
834
+
835
+ # hash all requirements
836
+ for item in set(self.__task.get('task', self.__task.task(), 'require',
837
+ step=self.__step, index=self.__index)):
838
+ args = item.split(',')
839
+ sc_type = self.__chip.get(*args, field='type')
840
+ if 'file' in sc_type or 'dir' in sc_type:
841
+ access_step, access_index = self.__step, self.__index
842
+ if self.__chip.get(*args, field='pernode').is_never():
843
+ access_step, access_index = None, None
844
+ self.__chip.hash_files(*args, step=access_step, index=access_index,
845
+ check=False, allow_cache=True, verbose=False)
846
+
847
+ def __hash_files_post_execute(self):
848
+ # hash all outputs
849
+ self.__chip.hash_files('tool', self.__task.tool(), 'task', self.__task.task(), 'output',
850
+ step=self.__step, index=self.__index, check=False, verbose=False)
851
+
852
+ # hash all requirements
853
+ for item in set(self.__task.get('task', self.__task.task(), 'require',
854
+ step=self.__step, index=self.__index)):
855
+ args = item.split(',')
856
+ sc_type = self.__chip.get(*args, field='type')
857
+ if 'file' in sc_type or 'dir' in sc_type:
858
+ access_step, access_index = self.__step, self.__index
859
+ if self.__chip.get(*args, field='pernode').is_never():
860
+ access_step, access_index = None, None
861
+ if self.__chip.get(*args, field='filehash'):
862
+ continue
863
+ self.__chip.hash_files(*args, step=access_step, index=access_index,
864
+ check=False, allow_cache=True, verbose=False)
865
+
866
+ def __report_output_files(self):
867
+ if self.__task.tool() == 'builtin':
868
+ return
869
+
870
+ error = False
871
+
872
+ try:
873
+ outputs = os.listdir(os.path.join(self.__workdir, "outputs"))
874
+ except FileNotFoundError:
875
+ self.halt("Output directory is missing")
876
+
877
+ try:
878
+ outputs.remove(os.path.basename(self.__manifests["output"]))
879
+ except ValueError:
880
+ self.logger.error(f"Output manifest ({os.path.basename(self.__manifests['output'])}) "
881
+ "is missing.")
882
+ error = True
883
+
884
+ outputs = set(outputs)
885
+
886
+ output_files = set(self.__task.get('task', self.__task.task(), 'output',
887
+ step=self.__step, index=self.__index))
888
+
889
+ missing = output_files.difference(outputs)
890
+ excess = outputs.difference(output_files)
891
+
892
+ if missing:
893
+ error = True
894
+ self.logger.error(f"Expected output files are missing: {', '.join(missing)}")
895
+
896
+ if excess:
897
+ error = True
898
+ self.logger.error(f"Unexpected output files found: {', '.join(excess)}")
899
+
900
+ if error and self.__enforce_outputfiles:
901
+ self.halt()
902
+
903
+ def copy_from(self, source):
904
+ copy_from = self.__chip.getworkdir(jobname=source, step=self.__step, index=self.__index)
905
+
906
+ if not os.path.exists(copy_from):
907
+ return
908
+
909
+ self.logger.info(f'Importing {self.__step}{self.__index} from {source}')
910
+ shutil.copytree(
911
+ copy_from, self.__workdir,
912
+ dirs_exist_ok=True,
913
+ copy_function=utils.link_copy)
914
+
915
+ # rewrite replay files
916
+ if os.path.exists(self.__replay_script):
917
+ # delete file as it might be a hard link
918
+ os.remove(self.__replay_script)
919
+
920
+ self.__task.set_runtime(self.__chip, step=self.__step, index=self.__index)
921
+ self.__task.generate_replay_script(self.__replay_script, self.__workdir)
922
+ self.__task.set_runtime(None)
923
+
924
+ for manifest in self.__manifests.values():
925
+ if os.path.exists(manifest):
926
+ schema = Schema.from_manifest(manifest)
927
+ # delete file as it might be a hard link
928
+ os.remove(manifest)
929
+ schema.set('option', 'jobname', self.__chip.get('option', 'jobname'))
930
+ schema.write_manifest(manifest)
931
+
932
+ def clean_directory(self):
933
+ if os.path.exists(self.__workdir):
934
+ shutil.rmtree(self.__workdir)