siliconcompiler 0.30.0__py3-none-any.whl → 0.31.1__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 (79) hide show
  1. siliconcompiler/_metadata.py +1 -1
  2. siliconcompiler/apps/sc_install.py +26 -4
  3. siliconcompiler/apps/sc_remote.py +1 -3
  4. siliconcompiler/core.py +24 -9
  5. siliconcompiler/flowgraph.py +11 -23
  6. siliconcompiler/{package.py → package/__init__.py} +62 -176
  7. siliconcompiler/package/git.py +81 -0
  8. siliconcompiler/package/https.py +93 -0
  9. siliconcompiler/remote/schema.py +9 -8
  10. siliconcompiler/report/report.py +4 -3
  11. siliconcompiler/scheduler/__init__.py +127 -113
  12. siliconcompiler/scheduler/docker_runner.py +4 -4
  13. siliconcompiler/scheduler/run_node.py +3 -3
  14. siliconcompiler/scheduler/send_messages.py +1 -1
  15. siliconcompiler/schema/schema_cfg.py +367 -357
  16. siliconcompiler/schema/schema_obj.py +39 -29
  17. siliconcompiler/schema/utils.py +19 -0
  18. siliconcompiler/sphinx_ext/schemagen.py +3 -1
  19. siliconcompiler/templates/replay/replay.sh.j2 +92 -0
  20. siliconcompiler/templates/tcl/manifest.tcl.j2 +1 -1
  21. siliconcompiler/tools/_common/__init__.py +8 -2
  22. siliconcompiler/tools/_common/asic.py +1 -1
  23. siliconcompiler/tools/_common/tcl/sc_pin_constraints.tcl +3 -5
  24. siliconcompiler/tools/genfasm/genfasm.py +1 -1
  25. siliconcompiler/tools/klayout/export.py +5 -0
  26. siliconcompiler/tools/klayout/klayout.py +18 -1
  27. siliconcompiler/tools/klayout/klayout_export.py +4 -1
  28. siliconcompiler/tools/klayout/klayout_operations.py +5 -2
  29. siliconcompiler/tools/klayout/klayout_utils.py +23 -0
  30. siliconcompiler/tools/klayout/operations.py +5 -0
  31. siliconcompiler/tools/magic/magic.py +1 -1
  32. siliconcompiler/tools/openroad/_apr.py +14 -3
  33. siliconcompiler/tools/openroad/antenna_repair.py +2 -1
  34. siliconcompiler/tools/openroad/clock_tree_synthesis.py +2 -1
  35. siliconcompiler/tools/openroad/detailed_placement.py +2 -1
  36. siliconcompiler/tools/openroad/detailed_route.py +8 -0
  37. siliconcompiler/tools/openroad/fillercell_insertion.py +2 -1
  38. siliconcompiler/tools/openroad/global_placement.py +2 -1
  39. siliconcompiler/tools/openroad/pin_placement.py +2 -1
  40. siliconcompiler/tools/openroad/rdlroute.py +4 -0
  41. siliconcompiler/tools/openroad/repair_design.py +2 -1
  42. siliconcompiler/tools/openroad/repair_timing.py +2 -1
  43. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +6 -0
  44. siliconcompiler/tools/openroad/scripts/apr/sc_clock_tree_synthesis.tcl +1 -0
  45. siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +8 -0
  46. siliconcompiler/tools/openroad/scripts/apr/sc_global_placement.tcl +1 -0
  47. siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +2 -0
  48. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +3 -3
  49. siliconcompiler/tools/openroad/scripts/apr/sc_macro_placement.tcl +5 -0
  50. siliconcompiler/tools/openroad/scripts/apr/sc_power_grid.tcl +1 -0
  51. siliconcompiler/tools/openroad/scripts/apr/sc_repair_design.tcl +1 -0
  52. siliconcompiler/tools/openroad/scripts/apr/sc_repair_timing.tcl +3 -0
  53. siliconcompiler/tools/openroad/scripts/common/procs.tcl +29 -12
  54. siliconcompiler/tools/openroad/scripts/common/reports.tcl +15 -0
  55. siliconcompiler/tools/openroad/scripts/common/write_images.tcl +28 -0
  56. siliconcompiler/tools/openroad/scripts/sc_rdlroute.tcl +3 -13
  57. siliconcompiler/tools/vpr/vpr.py +86 -6
  58. siliconcompiler/tools/yosys/__init__.py +7 -0
  59. siliconcompiler/tools/yosys/sc_syn.tcl +33 -24
  60. siliconcompiler/tools/yosys/syn_asic.py +27 -0
  61. siliconcompiler/tools/yosys/syn_asic.tcl +27 -0
  62. siliconcompiler/toolscripts/_tools.json +15 -3
  63. siliconcompiler/toolscripts/rhel8/install-yosys-moosic.sh +17 -0
  64. siliconcompiler/toolscripts/rhel8/install-yosys-slang.sh +22 -0
  65. siliconcompiler/toolscripts/rhel9/install-yosys-moosic.sh +17 -0
  66. siliconcompiler/toolscripts/rhel9/install-yosys-slang.sh +22 -0
  67. siliconcompiler/toolscripts/ubuntu20/install-yosys-moosic.sh +17 -0
  68. siliconcompiler/toolscripts/ubuntu20/install-yosys-slang.sh +22 -0
  69. siliconcompiler/toolscripts/ubuntu22/install-yosys-moosic.sh +17 -0
  70. siliconcompiler/toolscripts/ubuntu22/install-yosys-slang.sh +22 -0
  71. siliconcompiler/toolscripts/ubuntu24/install-yosys-moosic.sh +17 -0
  72. siliconcompiler/toolscripts/ubuntu24/install-yosys-slang.sh +22 -0
  73. siliconcompiler/utils/__init__.py +33 -5
  74. {siliconcompiler-0.30.0.dist-info → siliconcompiler-0.31.1.dist-info}/METADATA +21 -23
  75. {siliconcompiler-0.30.0.dist-info → siliconcompiler-0.31.1.dist-info}/RECORD +79 -66
  76. {siliconcompiler-0.30.0.dist-info → siliconcompiler-0.31.1.dist-info}/WHEEL +1 -1
  77. {siliconcompiler-0.30.0.dist-info → siliconcompiler-0.31.1.dist-info}/entry_points.txt +4 -0
  78. {siliconcompiler-0.30.0.dist-info → siliconcompiler-0.31.1.dist-info}/LICENSE +0 -0
  79. {siliconcompiler-0.30.0.dist-info → siliconcompiler-0.31.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,93 @@
1
+ import requests
2
+ import shutil
3
+ import tarfile
4
+ import zipfile
5
+
6
+ import os.path
7
+
8
+ from fasteners import InterProcessLock
9
+ from io import BytesIO
10
+ from urllib.parse import urlparse
11
+
12
+ from siliconcompiler import SiliconCompilerError
13
+ from siliconcompiler.package import get_download_cache_path
14
+ from siliconcompiler.package import aquire_data_lock, release_data_lock
15
+
16
+
17
+ def get_resolver(url):
18
+ if url.scheme in ("http", "https"):
19
+ return http_resolver
20
+
21
+ return None
22
+
23
+
24
+ def http_resolver(chip, package, path, ref, url):
25
+ data_path, data_path_lock = get_download_cache_path(chip, package, ref)
26
+
27
+ if os.path.exists(data_path):
28
+ return data_path, False
29
+
30
+ # Acquire lock
31
+ data_lock = InterProcessLock(data_path_lock)
32
+ aquire_data_lock(data_path, data_lock)
33
+
34
+ extract_from_url(chip, package, path, ref, url, data_path)
35
+
36
+ release_data_lock(data_lock)
37
+
38
+ return data_path, True
39
+
40
+
41
+ def extract_from_url(chip, package, path, ref, url, data_path):
42
+ data_url = path
43
+ headers = {}
44
+ if os.environ.get('GIT_TOKEN') or url.username:
45
+ headers['Authorization'] = f'token {os.environ.get("GIT_TOKEN") or url.username}'
46
+ if "github" in data_url:
47
+ headers['Accept'] = 'application/octet-stream'
48
+ data_url = path
49
+ if data_url.endswith('/'):
50
+ data_url = f"{data_url}{ref}.tar.gz"
51
+ chip.logger.info(f'Downloading {package} data from {data_url}')
52
+ response = requests.get(data_url, stream=True, headers=headers)
53
+ if not response.ok:
54
+ raise SiliconCompilerError(f'Failed to download {package} data source.', chip=chip)
55
+
56
+ fileobj = BytesIO(response.content)
57
+ try:
58
+ with tarfile.open(fileobj=fileobj, mode='r|gz') as tar_ref:
59
+ tar_ref.extractall(path=data_path)
60
+ except tarfile.ReadError:
61
+ fileobj.seek(0)
62
+ # Try as zip
63
+ with zipfile.ZipFile(fileobj) as zip_ref:
64
+ zip_ref.extractall(path=data_path)
65
+
66
+ if 'github' in url.netloc and len(os.listdir(data_path)) == 1:
67
+ # Github inserts one folder at the highest level of the tar file
68
+ # this compensates for this behavior
69
+ gh_url = urlparse(data_url)
70
+
71
+ repo = gh_url.path.split('/')[2]
72
+
73
+ gh_ref = gh_url.path.split('/')[-1]
74
+ if repo.endswith('.git'):
75
+ gh_ref = ref
76
+ elif gh_ref.endswith('.tar.gz'):
77
+ gh_ref = gh_ref[0:-7]
78
+ elif gh_ref.endswith('.tgz'):
79
+ gh_ref = gh_ref[0:-4]
80
+ else:
81
+ gh_ref = gh_ref.split('.')[0]
82
+
83
+ if gh_ref.startswith('v'):
84
+ gh_ref = gh_ref[1:]
85
+
86
+ github_folder = f"{repo}-{gh_ref}"
87
+
88
+ if github_folder in os.listdir(data_path):
89
+ # This moves all files one level up
90
+ git_path = os.path.join(data_path, github_folder)
91
+ for data_file in os.listdir(git_path):
92
+ shutil.move(os.path.join(git_path, data_file), data_path)
93
+ os.removedirs(git_path)
@@ -1,5 +1,6 @@
1
1
  from siliconcompiler.schema.schema_cfg import scparam
2
2
  from siliconcompiler.schema import Schema
3
+ from siliconcompiler.schema.utils import PerNode, Scope
3
4
 
4
5
 
5
6
  SCHEMA_VERSION = '0.0.2'
@@ -11,7 +12,7 @@ def schema_cfg():
11
12
 
12
13
  scparam(cfg, ['schemaversion'],
13
14
  sctype='str',
14
- scope='global',
15
+ scope=Scope.GLOBAL,
15
16
  defvalue=SCHEMA_VERSION,
16
17
  require='all',
17
18
  shorthelp="Schema version number",
@@ -22,7 +23,7 @@ def schema_cfg():
22
23
 
23
24
  scparam(cfg, ['option', 'port'],
24
25
  sctype='int',
25
- scope='global',
26
+ scope=Scope.GLOBAL,
26
27
  defvalue=8080,
27
28
  require='all',
28
29
  shorthelp="Port number to run the server on.",
@@ -34,7 +35,7 @@ def schema_cfg():
34
35
  scparam(cfg, ['option', 'cluster'],
35
36
  sctype='enum',
36
37
  enum=['local', 'slurm'],
37
- scope='global',
38
+ scope=Scope.GLOBAL,
38
39
  defvalue='local',
39
40
  require='all',
40
41
  shorthelp="Type of compute cluster to use.",
@@ -45,7 +46,7 @@ def schema_cfg():
45
46
 
46
47
  scparam(cfg, ['option', 'nfsmount'],
47
48
  sctype='dir',
48
- scope='global',
49
+ scope=Scope.GLOBAL,
49
50
  defvalue='/nfs/sc_compute',
50
51
  require='all',
51
52
  shorthelp="Directory of mounted shared NFS storage.",
@@ -56,7 +57,7 @@ def schema_cfg():
56
57
 
57
58
  scparam(cfg, ['option', 'auth'],
58
59
  sctype='bool',
59
- scope='global',
60
+ scope=Scope.GLOBAL,
60
61
  defvalue=False,
61
62
  require='all',
62
63
  shorthelp="Flag determining whether to enable authenticated and encrypted jobs.",
@@ -67,7 +68,7 @@ def schema_cfg():
67
68
 
68
69
  scparam(cfg, ['option', 'cfg'],
69
70
  sctype='[file]',
70
- scope='job',
71
+ scope=Scope.JOB,
71
72
  shorthelp="Configuration manifest",
72
73
  switch="-cfg <file>",
73
74
  example=["cli: -cfg mypdk.json",
@@ -82,8 +83,8 @@ def schema_cfg():
82
83
  scparam(cfg, ['option', 'loglevel'],
83
84
  sctype='enum',
84
85
  enum=["info", "warning", "error", "critical", "debug"],
85
- pernode='optional',
86
- scope='job',
86
+ pernode=PerNode.OPTIONAL,
87
+ scope=Scope.JOB,
87
88
  defvalue='info',
88
89
  shorthelp="Logging level",
89
90
  switch="-loglevel <str>",
@@ -2,6 +2,7 @@ import fnmatch
2
2
  import pandas
3
3
  import os
4
4
  from siliconcompiler import Schema
5
+ from siliconcompiler.schema.utils import PerNode
5
6
  from siliconcompiler.report import utils
6
7
  from siliconcompiler.flowgraph import nodes_to_execute
7
8
  from siliconcompiler.tools._common import get_tool_task
@@ -53,7 +54,7 @@ def get_flowgraph_nodes(chip, step, index):
53
54
  if task is not None:
54
55
  nodes['task'] = task
55
56
  for key in chip.getkeys('record'):
56
- if chip.get('record', key, field='pernode') == 'never':
57
+ if chip.get('record', key, field='pernode').is_never():
57
58
  value = chip.get('record', key)
58
59
  else:
59
60
  value = chip.get('record', key, step=step, index=index)
@@ -106,7 +107,7 @@ def make_manifest_helper(manifest_subsect, modified_manifest_subsect):
106
107
  '''
107
108
 
108
109
  def build_leaf(manifest_subsect):
109
- if manifest_subsect['pernode'] == 'never':
110
+ if PerNode(manifest_subsect['pernode']) == PerNode.NEVER:
110
111
  if Schema.GLOBAL_KEY in manifest_subsect['node'] and \
111
112
  Schema.GLOBAL_KEY in manifest_subsect['node'][Schema.GLOBAL_KEY]:
112
113
  value = manifest_subsect['node'][Schema.GLOBAL_KEY][Schema.GLOBAL_KEY]['value']
@@ -132,7 +133,7 @@ def make_manifest_helper(manifest_subsect, modified_manifest_subsect):
132
133
  return node_values
133
134
 
134
135
  if Schema._is_leaf(manifest_subsect):
135
- if manifest_subsect['pernode'] == 'never':
136
+ if PerNode(manifest_subsect['pernode']) == PerNode.NEVER:
136
137
  if Schema.GLOBAL_KEY in manifest_subsect['node']:
137
138
  value = manifest_subsect['node'][Schema.GLOBAL_KEY][Schema.GLOBAL_KEY]['value']
138
139
  else:
@@ -1,5 +1,4 @@
1
1
  import contextlib
2
- import copy
3
2
  import distro
4
3
  import getpass
5
4
  import multiprocessing
@@ -27,7 +26,7 @@ from siliconcompiler.scheduler import slurm
27
26
  from siliconcompiler.scheduler import docker_runner
28
27
  from siliconcompiler import NodeStatus, SiliconCompilerError
29
28
  from siliconcompiler.flowgraph import _get_flowgraph_nodes, _get_flowgraph_execution_order, \
30
- _get_pruned_node_inputs, _get_flowgraph_node_inputs, _get_flowgraph_entry_nodes, \
29
+ _get_pruned_node_inputs, _get_flowgraph_entry_nodes, \
31
30
  _unreachable_steps_to_execute, _nodes_to_execute, \
32
31
  get_nodes_from, nodes_to_execute, _check_flowgraph
33
32
  from siliconcompiler.tools._common import input_file_node_name
@@ -99,13 +98,6 @@ def run(chip):
99
98
  _reset_flow_nodes(chip, flow, nodes_to_execute(chip, flow))
100
99
  __record_packages(chip)
101
100
 
102
- # Save current environment
103
- environment = copy.deepcopy(os.environ)
104
- # Set env variables
105
- for envvar in chip.getkeys('option', 'env'):
106
- val = chip.get('option', 'env', envvar)
107
- os.environ[envvar] = val
108
-
109
101
  if chip.get('option', 'remote'):
110
102
  client = Client(chip)
111
103
  client.run()
@@ -113,23 +105,18 @@ def run(chip):
113
105
  _local_process(chip, flow)
114
106
 
115
107
  # Merge cfgs from last executed tasks, and write out a final manifest.
116
- _finalize_run(chip, environment)
108
+ _finalize_run(chip)
117
109
 
118
110
 
119
111
  ###########################################################################
120
- def _finalize_run(chip, environment):
112
+ def _finalize_run(chip):
121
113
  '''
122
114
  Helper function to finalize a job run after it completes:
123
- * Restore any environment variable changes made during the run.
124
115
  * Clear any -arg_step/-arg_index values in case only one node was run.
125
116
  * Store this run in the Schema's 'history' field.
126
117
  * Write out a final JSON manifest containing the full results and history.
127
118
  '''
128
119
 
129
- # Restore environment
130
- os.environ.clear()
131
- os.environ.update(environment)
132
-
133
120
  # Clear scratchpad args since these are checked on run() entry
134
121
  chip.set('arg', 'step', None, clobber=True)
135
122
  chip.set('arg', 'index', None, clobber=True)
@@ -502,10 +489,13 @@ def _haltstep(chip, flow, step, index, log=True):
502
489
  def _setupnode(chip, flow, step, index, replay):
503
490
  _hash_files(chip, step, index, setup=True)
504
491
 
492
+ # Select the inputs to this node
493
+ _select_inputs(chip, step, index)
494
+
505
495
  # Write manifest prior to step running into inputs
506
496
  chip.write_manifest(f'inputs/{chip.get("design")}.pkg.json')
507
497
 
508
- _select_inputs(chip, step, index)
498
+ # Forward data
509
499
  _copy_previous_steps_output_data(chip, step, index, replay)
510
500
 
511
501
  # Check manifest
@@ -541,7 +531,7 @@ def _setup_workdir(chip, step, index, replay):
541
531
  return workdir
542
532
 
543
533
 
544
- def _select_inputs(chip, step, index):
534
+ def _select_inputs(chip, step, index, trial=False):
545
535
 
546
536
  flow = chip.get('option', 'flow')
547
537
  tool, _ = get_tool_task(chip, step, index, flow)
@@ -551,15 +541,24 @@ def _select_inputs(chip, step, index):
551
541
  '_select_inputs',
552
542
  None)
553
543
  if select_inputs:
544
+ log_handlers = None
545
+ if trial:
546
+ log_handlers = chip.logger.handlers.copy()
547
+ chip.logger.handlers.clear()
554
548
  sel_inputs = select_inputs(chip, step, index)
549
+ if log_handlers:
550
+ chip.logger.handlers = log_handlers
555
551
  else:
556
- sel_inputs = _get_flowgraph_node_inputs(chip, flow, (step, index))
552
+ sel_inputs = _get_pruned_node_inputs(chip, flow, (step, index))
557
553
 
558
554
  if (step, index) not in _get_flowgraph_entry_nodes(chip, flow) and not sel_inputs:
559
555
  chip.logger.error(f'No inputs selected after running {tool}')
560
556
  _haltstep(chip, flow, step, index)
561
557
 
562
- chip.set('record', 'inputnode', sel_inputs, step=step, index=index)
558
+ if not trial:
559
+ chip.set('record', 'inputnode', sel_inputs, step=step, index=index)
560
+
561
+ return sel_inputs
563
562
 
564
563
 
565
564
  def copy_output_file(chip, outfile, folder='inputs'):
@@ -608,8 +607,14 @@ def _copy_previous_steps_output_data(chip, step, index, replay):
608
607
  # configuration into inputs/{design}.pkg.json earlier in _runstep.
609
608
  if not replay:
610
609
  in_workdir = chip.getworkdir(step=in_step, index=in_index)
610
+ output_dir = os.path.join(in_workdir, "outputs")
611
611
 
612
- for outfile in os.scandir(f"{in_workdir}/outputs"):
612
+ if not os.path.isdir(output_dir):
613
+ chip.logger.error(
614
+ f'Unable to locate outputs directory for {in_step}{in_index}: {output_dir}')
615
+ _haltstep(chip, flow, step, index)
616
+
617
+ for outfile in os.scandir(output_dir):
613
618
  new_name = input_file_node_name(outfile.name, in_step, in_index)
614
619
  if strict:
615
620
  if outfile.name not in in_files and new_name not in in_files:
@@ -649,13 +654,36 @@ def _getexe(chip, tool, step, index):
649
654
  syspath = os.getenv('PATH', os.defpath)
650
655
  if path:
651
656
  # Prepend 'path' schema var to system path
652
- syspath = utils._resolve_env_vars(chip, path) + os.pathsep + syspath
657
+ syspath = utils._resolve_env_vars(chip, path, step, index) + os.pathsep + syspath
653
658
 
654
659
  fullexe = shutil.which(exe, path=syspath)
655
660
 
656
661
  return fullexe
657
662
 
658
663
 
664
+ def _get_run_env_vars(chip, tool, task, step, index, include_path):
665
+ envvars = utils.get_env_vars(chip, step, index)
666
+ for item in chip.getkeys('tool', tool, 'licenseserver'):
667
+ license_file = chip.get('tool', tool, 'licenseserver', item, step=step, index=index)
668
+ if license_file:
669
+ envvars[item] = ':'.join(license_file)
670
+
671
+ if include_path:
672
+ path = chip.get('tool', tool, 'path', step=step, index=index)
673
+ if path:
674
+ envvars['PATH'] = path + os.pathsep + os.environ['PATH']
675
+ else:
676
+ envvars['PATH'] = os.environ['PATH']
677
+
678
+ # Forward additional variables
679
+ for var in ('LD_LIBRARY_PATH',):
680
+ val = os.getenv(var, None)
681
+ if val:
682
+ envvars[var] = val
683
+
684
+ return envvars
685
+
686
+
659
687
  #######################################
660
688
  def _makecmd(chip, tool, task, step, index, script_name='replay.sh', include_path=True):
661
689
  '''
@@ -707,32 +735,6 @@ def _makecmd(chip, tool, task, step, index, script_name='replay.sh', include_pat
707
735
  chip.logger.error(f'Failed to get runtime options for {tool}/{task}')
708
736
  raise e
709
737
 
710
- envvars = {}
711
- for key in chip.getkeys('option', 'env'):
712
- envvars[key] = chip.get('option', 'env', key)
713
- for item in chip.getkeys('tool', tool, 'licenseserver'):
714
- license_file = chip.get('tool', tool, 'licenseserver', item, step=step, index=index)
715
- if license_file:
716
- envvars[item] = ':'.join(license_file)
717
-
718
- if include_path:
719
- path = chip.get('tool', tool, 'path', step=step, index=index)
720
- if path:
721
- envvars['PATH'] = path + os.pathsep + os.environ['PATH']
722
- else:
723
- envvars['PATH'] = os.environ['PATH']
724
-
725
- # Forward additional variables
726
- for var in ('LD_LIBRARY_PATH',):
727
- val = os.getenv(var, None)
728
- if val:
729
- envvars[var] = val
730
-
731
- for key in chip.getkeys('tool', tool, 'task', task, 'env'):
732
- val = chip.get('tool', tool, 'task', task, 'env', key, step=step, index=index)
733
- if val:
734
- envvars[key] = val
735
-
736
738
  # Separate variables to be able to display nice name of executable
737
739
  cmd = os.path.basename(cmdlist[0])
738
740
  cmd_args = cmdlist[1:]
@@ -747,19 +749,23 @@ def _makecmd(chip, tool, task, step, index, script_name='replay.sh', include_pat
747
749
 
748
750
  # create replay file
749
751
  with open(script_name, 'w') as f:
750
- print('#!/usr/bin/env bash', file=f)
751
-
752
- envvar_cmd = 'export'
753
- for key, val in envvars.items():
754
- print(f'{envvar_cmd} {key}="{val}"', file=f)
755
-
756
752
  # Ensure execution runs from the same directory
753
+ replay_opts = {}
757
754
  work_dir = chip.getworkdir(step=step, index=index)
758
755
  if chip._relative_path:
759
756
  work_dir = os.path.relpath(work_dir, chip._relative_path)
760
- print(f'cd {work_dir}', file=f)
761
-
762
- format_cmd = [chip.get('tool', tool, 'exe')]
757
+ replay_opts["work_dir"] = work_dir
758
+ replay_opts["exports"] = _get_run_env_vars(chip,
759
+ tool, task,
760
+ step, index,
761
+ include_path=include_path)
762
+ replay_opts["executable"] = chip.get('tool', tool, 'exe')
763
+
764
+ vswitch = chip.get('tool', tool, 'vswitch')
765
+ if vswitch:
766
+ replay_opts["version_flag"] = " ".join(vswitch)
767
+
768
+ format_cmd = [replay_opts["executable"]]
763
769
  arg_test = re.compile(r'^[-+]')
764
770
  file_test = re.compile(r'^[/]')
765
771
  for cmdarg in cmd_args:
@@ -775,7 +781,11 @@ def _makecmd(chip, tool, task, step, index, script_name='replay.sh', include_pat
775
781
  format_cmd.append(cmdarg)
776
782
  else:
777
783
  format_cmd[-1] += f' {cmdarg}'
778
- print(" \\\n ".join(format_cmd), file=f)
784
+
785
+ replay_opts["cmds"] = format_cmd
786
+
787
+ f.write(utils.get_file_template("replay/replay.sh.j2").render(replay_opts))
788
+ f.write("\n")
779
789
 
780
790
  os.chmod(script_name, 0o755)
781
791
 
@@ -830,6 +840,7 @@ def _run_executable_or_builtin(chip, step, index, version, toolpath, workdir, ru
830
840
 
831
841
  # TODO: Currently no memory usage tracking in breakpoints, builtins, or unexpected errors.
832
842
  max_mem_bytes = 0
843
+ cpu_start = time.time()
833
844
 
834
845
  stdout_file, stderr_file = __get_stdio(chip, tool, task, flow, step, index)
835
846
  is_stdout_log = chip.get('tool', tool, 'task', task, 'stdout', 'destination',
@@ -881,7 +892,10 @@ def _run_executable_or_builtin(chip, step, index, version, toolpath, workdir, ru
881
892
 
882
893
  ##################
883
894
  # Make record of tool options
884
- __record_tool(chip, step, index, version, toolpath, cmd_args)
895
+ if cmd_args is not None:
896
+ chip.set('record', 'toolargs',
897
+ ' '.join(f'"{arg}"' if ' ' in arg else arg for arg in cmd_args),
898
+ step=step, index=index)
885
899
 
886
900
  chip.logger.info('%s', printable_cmd)
887
901
  timeout = chip.get('option', 'timeout', step=step, index=index)
@@ -1003,8 +1017,15 @@ def _run_executable_or_builtin(chip, step, index, version, toolpath, workdir, ru
1003
1017
  chip.logger.warning(msg)
1004
1018
  chip._error = True
1005
1019
 
1020
+ # Capture cpu runtime
1021
+ record_metric(chip, step, index, 'exetime', round((time.time() - cpu_start), 2),
1022
+ source=None,
1023
+ source_unit='s')
1024
+
1006
1025
  # Capture memory usage
1007
- record_metric(chip, step, index, 'memory', max_mem_bytes, source=None, source_unit='B')
1026
+ record_metric(chip, step, index, 'memory', max_mem_bytes,
1027
+ source=None,
1028
+ source_unit='B')
1008
1029
 
1009
1030
 
1010
1031
  def _post_process(chip, step, index):
@@ -1074,26 +1095,26 @@ def _executenode(chip, step, index, replay):
1074
1095
 
1075
1096
  send_messages.send(chip, "skipped", step, index)
1076
1097
  else:
1077
- _set_env_vars(chip, step, index)
1098
+ org_env = _set_env_vars(chip, step, index)
1078
1099
 
1079
1100
  run_func = getattr(chip._get_task_module(step, index, flow=flow), 'run', None)
1080
- (toolpath, version) = _check_tool_version(chip, step, index, run_func)
1101
+ toolpath, version = _check_tool_version(chip, step, index, run_func)
1102
+
1103
+ if version:
1104
+ chip.set('record', 'toolversion', version, step=step, index=index)
1105
+
1106
+ if toolpath:
1107
+ chip.set('record', 'toolpath', toolpath, step=step, index=index)
1081
1108
 
1082
1109
  # Write manifest (tool interface) (Don't move this!)
1083
1110
  _write_task_manifest(chip, tool)
1084
1111
 
1085
1112
  send_messages.send(chip, "begin", step, index)
1086
1113
 
1087
- # Start CPU Timer
1088
- chip.logger.debug("Starting executable")
1089
- cpu_start = time.time()
1090
-
1091
1114
  _run_executable_or_builtin(chip, step, index, version, toolpath, workdir, run_func)
1092
1115
 
1093
- # Capture cpu runtime
1094
- cpu_end = time.time()
1095
- cputime = round((cpu_end - cpu_start), 2)
1096
- record_metric(chip, step, index, 'exetime', cputime, source=None, source_unit='s')
1116
+ os.environ.clear()
1117
+ os.environ.update(org_env)
1097
1118
 
1098
1119
  _post_process(chip, step, index)
1099
1120
 
@@ -1120,24 +1141,18 @@ def _pre_process(chip, step, index):
1120
1141
 
1121
1142
 
1122
1143
  def _set_env_vars(chip, step, index):
1123
- flow = chip.get('option', 'flow')
1124
- tool, task = get_tool_task(chip, step, index, flow)
1144
+ org_env = os.environ.copy()
1145
+
1146
+ tool, task = get_tool_task(chip, step, index)
1125
1147
 
1126
1148
  chip.schema._start_record_access()
1127
- # License file configuration.
1128
- for item in chip.getkeys('tool', tool, 'licenseserver'):
1129
- license_file = chip.get('tool', tool, 'licenseserver', item, step=step, index=index)
1130
- if license_file:
1131
- os.environ[item] = ':'.join(license_file)
1132
1149
 
1133
- # Tool-specific environment variables for this task.
1134
- for item in chip.getkeys('tool', tool, 'task', task, 'env'):
1135
- val = chip.get('tool', tool, 'task', task, 'env', item, step=step, index=index)
1136
- if val:
1137
- os.environ[item] = val
1150
+ os.environ.update(_get_run_env_vars(chip, tool, task, step, index, include_path=True))
1138
1151
 
1139
1152
  chip.schema._stop_record_access()
1140
1153
 
1154
+ return org_env
1155
+
1141
1156
 
1142
1157
  def _check_tool_version(chip, step, index, run_func=None):
1143
1158
  '''
@@ -1214,8 +1229,7 @@ def _hash_files(chip, step, index, setup=False):
1214
1229
  args = item.split(',')
1215
1230
  sc_type = chip.get(*args, field='type')
1216
1231
  if 'file' in sc_type or 'dir' in sc_type:
1217
- pernode = chip.get(*args, field='pernode')
1218
- if pernode == 'never':
1232
+ if chip.get(*args, field='pernode').is_never():
1219
1233
  if not setup:
1220
1234
  if chip.get(*args, field='filehash'):
1221
1235
  continue
@@ -1355,7 +1369,7 @@ def assert_required_accesses(chip, step, index):
1355
1369
  gets.add(tuple(key))
1356
1370
 
1357
1371
  def get_value(*key):
1358
- if chip.get(*key, field='pernode') == 'never':
1372
+ if chip.get(*key, field='pernode').is_never():
1359
1373
  return chip.get(*key)
1360
1374
  else:
1361
1375
  return chip.get(*key, step=step, index=index)
@@ -1728,19 +1742,6 @@ def get_record_time(chip, step, index, timetype):
1728
1742
  '%Y-%m-%d %H:%M:%S').timestamp()
1729
1743
 
1730
1744
 
1731
- #######################################
1732
- def __record_tool(chip, step, index, toolversion=None, toolpath=None, cli_args=None):
1733
- if toolversion:
1734
- chip.set('record', 'toolversion', toolversion, step=step, index=index)
1735
-
1736
- if toolpath:
1737
- chip.set('record', 'toolpath', toolpath, step=step, index=index)
1738
-
1739
- if cli_args is not None:
1740
- toolargs = ' '.join(f'"{arg}"' if ' ' in arg else arg for arg in cli_args)
1741
- chip.set('record', 'toolargs', toolargs, step=step, index=index)
1742
-
1743
-
1744
1745
  #######################################
1745
1746
  def _get_cloud_region():
1746
1747
  # TODO: add logic to figure out if we're running on a remote cluster and
@@ -1864,6 +1865,22 @@ def kill_process(chip, proc, tool, poll_interval, msg=""):
1864
1865
  utils.terminate_process(proc.pid)
1865
1866
 
1866
1867
 
1868
+ def get_check_node_keys(chip, step, index):
1869
+ tool, task = get_tool_task(chip, step, index)
1870
+
1871
+ # Collect keys to check for changes
1872
+ required = chip.get('tool', tool, 'task', task, 'require', step=step, index=index)
1873
+
1874
+ tool_task_key = ('tool', tool, 'task', task)
1875
+ for key in ('option', 'threads', 'prescript', 'postscript', 'refdir', 'script',):
1876
+ required.append(",".join([*tool_task_key, key]))
1877
+
1878
+ for env_key in chip.getkeys(*tool_task_key, 'env'):
1879
+ required.append(",".join([*tool_task_key, 'env', env_key]))
1880
+
1881
+ return set(sorted(required))
1882
+
1883
+
1867
1884
  def check_node_inputs(chip, step, index):
1868
1885
  from siliconcompiler import Chip # import here to avoid circular import
1869
1886
 
@@ -1910,16 +1927,15 @@ def check_node_inputs(chip, step, index):
1910
1927
  if tool != input_tool or task != input_task:
1911
1928
  return False
1912
1929
 
1913
- # Collect keys to check for changes
1914
- required = chip.get('tool', tool, 'task', task, 'require', step=step, index=index)
1915
- required.extend(input_chip.get('tool', tool, 'task', task, 'require', step=step, index=index))
1930
+ # Check if inputs changed
1931
+ new_inputs = set(_select_inputs(chip, step, index, trial=True))
1932
+ if set(input_chip.get('record', 'inputnode', step=step, index=index)) != new_inputs:
1933
+ chip.logger.warning(f'inputs to {step}{index} has been modified from previous run')
1934
+ return False
1916
1935
 
1917
- tool_task_key = ('tool', tool, 'task', task)
1918
- for key in ('option', 'threads', 'prescript', 'postscript', 'refdir', 'script',):
1919
- required.append(",".join([*tool_task_key, key]))
1920
- for check_chip in (chip, input_chip):
1921
- for env_key in chip.getkeys(*tool_task_key, 'env'):
1922
- required.append(",".join([*tool_task_key, 'env', env_key]))
1936
+ # Collect keys to check for changes
1937
+ required = get_check_node_keys(chip, step, index)
1938
+ required.update(get_check_node_keys(input_chip, step, index))
1923
1939
 
1924
1940
  def print_warning(key, extra=None):
1925
1941
  if extra:
@@ -1930,18 +1946,16 @@ def check_node_inputs(chip, step, index):
1930
1946
  'from previous run')
1931
1947
 
1932
1948
  # Check if keys have been modified
1933
- for check_key in sorted(set(required)):
1949
+ for check_key in required:
1934
1950
  key = check_key.split(',')
1935
1951
 
1936
1952
  if not chip.valid(*key) or not input_chip.valid(*key):
1937
1953
  print_warning(key)
1938
1954
  return False
1939
1955
 
1940
- pernode = chip.get(*key, field='pernode')
1941
-
1942
1956
  check_step = step
1943
1957
  check_index = index
1944
- if pernode == 'never':
1958
+ if chip.get(*key, field='pernode').is_never():
1945
1959
  check_step = None
1946
1960
  check_index = None
1947
1961
 
@@ -2244,7 +2258,7 @@ def _check_manifest_dynamic(chip, step, index):
2244
2258
  error = True
2245
2259
  else:
2246
2260
  paramtype = chip.get(*keypath, field='type')
2247
- is_perstep = chip.get(*keypath, field='pernode') != 'never'
2261
+ is_perstep = not chip.get(*keypath, field='pernode').is_never()
2248
2262
  if ('file' in paramtype) or ('dir' in paramtype):
2249
2263
  for val, check_step, check_index in chip.schema._getvals(*keypath):
2250
2264
  if is_perstep:
@@ -2298,7 +2312,7 @@ def _clear_record(chip, step, index, record, preserve=None):
2298
2312
  if preserve and record in preserve:
2299
2313
  return
2300
2314
 
2301
- if chip.get('record', record, field='pernode') == 'never':
2315
+ if chip.get('record', record, field='pernode').is_never():
2302
2316
  chip.unset('record', record)
2303
2317
  else:
2304
2318
  chip.unset('record', record, step=step, index=index)