siliconcompiler 0.35.4__py3-none-any.whl → 0.36.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 (89) hide show
  1. siliconcompiler/_metadata.py +1 -1
  2. siliconcompiler/constraints/__init__.py +4 -1
  3. siliconcompiler/constraints/asic_timing.py +230 -38
  4. siliconcompiler/constraints/fpga_timing.py +209 -14
  5. siliconcompiler/constraints/timing_mode.py +82 -0
  6. siliconcompiler/data/templates/tcl/manifest.tcl.j2 +0 -6
  7. siliconcompiler/flowgraph.py +95 -42
  8. siliconcompiler/flows/generate_openroad_rcx.py +2 -2
  9. siliconcompiler/flows/highresscreenshotflow.py +37 -0
  10. siliconcompiler/library.py +2 -1
  11. siliconcompiler/package/__init__.py +39 -45
  12. siliconcompiler/project.py +4 -1
  13. siliconcompiler/scheduler/scheduler.py +64 -35
  14. siliconcompiler/scheduler/schedulernode.py +5 -2
  15. siliconcompiler/scheduler/slurm.py +7 -6
  16. siliconcompiler/scheduler/taskscheduler.py +19 -16
  17. siliconcompiler/schema/_metadata.py +1 -1
  18. siliconcompiler/schema/namedschema.py +2 -4
  19. siliconcompiler/schema_support/cmdlineschema.py +0 -3
  20. siliconcompiler/schema_support/dependencyschema.py +0 -6
  21. siliconcompiler/schema_support/record.py +4 -3
  22. siliconcompiler/tool.py +58 -27
  23. siliconcompiler/tools/_common/tcl/sc_schema_access.tcl +0 -6
  24. siliconcompiler/tools/chisel/convert.py +44 -0
  25. siliconcompiler/tools/ghdl/convert.py +37 -2
  26. siliconcompiler/tools/icarus/compile.py +14 -0
  27. siliconcompiler/tools/keplerformal/__init__.py +7 -0
  28. siliconcompiler/tools/keplerformal/lec.py +112 -0
  29. siliconcompiler/tools/klayout/drc.py +14 -0
  30. siliconcompiler/tools/klayout/export.py +40 -0
  31. siliconcompiler/tools/klayout/operations.py +40 -0
  32. siliconcompiler/tools/klayout/screenshot.py +66 -1
  33. siliconcompiler/tools/klayout/scripts/klayout_export.py +10 -40
  34. siliconcompiler/tools/klayout/scripts/klayout_show.py +4 -4
  35. siliconcompiler/tools/klayout/scripts/klayout_utils.py +13 -1
  36. siliconcompiler/tools/montage/tile.py +26 -12
  37. siliconcompiler/tools/openroad/__init__.py +11 -0
  38. siliconcompiler/tools/openroad/_apr.py +780 -11
  39. siliconcompiler/tools/openroad/antenna_repair.py +26 -0
  40. siliconcompiler/tools/openroad/fillmetal_insertion.py +14 -0
  41. siliconcompiler/tools/openroad/global_placement.py +67 -0
  42. siliconcompiler/tools/openroad/global_route.py +15 -0
  43. siliconcompiler/tools/openroad/init_floorplan.py +19 -2
  44. siliconcompiler/tools/openroad/macro_placement.py +252 -0
  45. siliconcompiler/tools/openroad/power_grid.py +43 -0
  46. siliconcompiler/tools/openroad/power_grid_analysis.py +1 -1
  47. siliconcompiler/tools/openroad/rcx_bench.py +28 -0
  48. siliconcompiler/tools/openroad/rcx_extract.py +14 -0
  49. siliconcompiler/tools/openroad/rdlroute.py +14 -0
  50. siliconcompiler/tools/openroad/repair_design.py +41 -0
  51. siliconcompiler/tools/openroad/repair_timing.py +54 -0
  52. siliconcompiler/tools/openroad/screenshot.py +31 -1
  53. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +8 -0
  54. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +54 -15
  55. siliconcompiler/tools/openroad/scripts/apr/sc_irdrop.tcl +6 -4
  56. siliconcompiler/tools/openroad/scripts/apr/sc_write_data.tcl +4 -4
  57. siliconcompiler/tools/openroad/scripts/common/procs.tcl +14 -5
  58. siliconcompiler/tools/openroad/scripts/common/read_liberty.tcl +2 -2
  59. siliconcompiler/tools/openroad/scripts/common/reports.tcl +6 -3
  60. siliconcompiler/tools/openroad/scripts/common/screenshot.tcl +1 -1
  61. siliconcompiler/tools/openroad/scripts/common/write_data_physical.tcl +8 -0
  62. siliconcompiler/tools/openroad/scripts/common/write_images.tcl +16 -12
  63. siliconcompiler/tools/openroad/scripts/sc_rdlroute.tcl +3 -1
  64. siliconcompiler/tools/openroad/write_data.py +78 -2
  65. siliconcompiler/tools/opensta/scripts/sc_check_library.tcl +2 -2
  66. siliconcompiler/tools/opensta/scripts/sc_report_libraries.tcl +2 -2
  67. siliconcompiler/tools/opensta/scripts/sc_timing.tcl +12 -14
  68. siliconcompiler/tools/opensta/timing.py +42 -3
  69. siliconcompiler/tools/slang/elaborate.py +16 -1
  70. siliconcompiler/tools/surelog/parse.py +54 -0
  71. siliconcompiler/tools/verilator/compile.py +120 -0
  72. siliconcompiler/tools/vivado/syn_fpga.py +27 -0
  73. siliconcompiler/tools/vpr/route.py +40 -0
  74. siliconcompiler/tools/xdm/convert.py +14 -0
  75. siliconcompiler/tools/xyce/simulate.py +26 -0
  76. siliconcompiler/tools/yosys/lec_asic.py +13 -0
  77. siliconcompiler/tools/yosys/syn_asic.py +332 -3
  78. siliconcompiler/tools/yosys/syn_fpga.py +32 -0
  79. siliconcompiler/toolscripts/_tools.json +9 -4
  80. siliconcompiler/toolscripts/ubuntu22/install-keplerformal.sh +72 -0
  81. siliconcompiler/toolscripts/ubuntu24/install-keplerformal.sh +72 -0
  82. siliconcompiler/utils/multiprocessing.py +11 -0
  83. siliconcompiler/utils/settings.py +70 -49
  84. {siliconcompiler-0.35.4.dist-info → siliconcompiler-0.36.1.dist-info}/METADATA +4 -4
  85. {siliconcompiler-0.35.4.dist-info → siliconcompiler-0.36.1.dist-info}/RECORD +89 -83
  86. {siliconcompiler-0.35.4.dist-info → siliconcompiler-0.36.1.dist-info}/WHEEL +0 -0
  87. {siliconcompiler-0.35.4.dist-info → siliconcompiler-0.36.1.dist-info}/entry_points.txt +0 -0
  88. {siliconcompiler-0.35.4.dist-info → siliconcompiler-0.36.1.dist-info}/licenses/LICENSE +0 -0
  89. {siliconcompiler-0.35.4.dist-info → siliconcompiler-0.36.1.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@ from datetime import datetime
14
14
 
15
15
  from typing import Union, Dict, Optional, Tuple, List, Set, TYPE_CHECKING
16
16
 
17
- from siliconcompiler import NodeStatus
17
+ from siliconcompiler import NodeStatus, Task
18
18
  from siliconcompiler.schema import Journal
19
19
  from siliconcompiler.flowgraph import RuntimeFlowgraph
20
20
  from siliconcompiler.scheduler import SchedulerNode
@@ -292,6 +292,10 @@ class Scheduler:
292
292
  self.__run_setup()
293
293
  self.configure_nodes()
294
294
 
295
+ # Verify task classes
296
+ if not self.__check_task_classes():
297
+ raise SCRuntimeError("Task classes are missing")
298
+
295
299
  # Verify tool setups
296
300
  if not self.__check_tool_versions():
297
301
  raise SCRuntimeError("Tools did not meet version requirements")
@@ -353,6 +357,33 @@ class Scheduler:
353
357
 
354
358
  return not error
355
359
 
360
+ def __check_task_classes(self) -> bool:
361
+ """
362
+ Verifies that all runtime nodes have loaded their specific Task implementation classes.
363
+
364
+ Iterates through all nodes in the execution flow and checks if the associated
365
+ task object is a generic instance of the base `Task` class. If so, it indicates
366
+ that the specific module for that tool/task was not loaded correctly.
367
+
368
+ Returns:
369
+ bool: `True` if all nodes are using specialized Task subclasses, `False` if any
370
+ node is using the base `Task` class.
371
+ """
372
+ nodes = self.__flow_runtime.get_nodes()
373
+ error = False
374
+
375
+ for (step, index) in nodes:
376
+ tool = self.__flow.get(step, index, "tool")
377
+ task = self.__flow.get(step, index, "task")
378
+
379
+ task_cls = self.project.get("tool", tool, "task", task, field="schema")
380
+ if type(task_cls) is Task:
381
+ self.__logger.error(f"Invalid task: {step}/{index} did not load "
382
+ "the correct class module")
383
+ error = True
384
+
385
+ return not error
386
+
356
387
  def __check_flowgraph_io(self) -> bool:
357
388
  """
358
389
  Validate that every runtime node will receive its required input files and that no
@@ -409,6 +440,8 @@ class Scheduler:
409
440
  node_inp = task.compute_input_file_node_name(inp, in_step, in_index)
410
441
  if node_inp in requirements:
411
442
  inp = node_inp
443
+ if inp not in requirements:
444
+ continue
412
445
  if inp in all_inputs:
413
446
  self.__logger.error(f'Invalid flow: {step}/{index} '
414
447
  f'receives {inp} from multiple input tasks')
@@ -908,43 +941,39 @@ class Scheduler:
908
941
 
909
942
  error = False
910
943
 
911
- cwd = os.getcwd()
912
- with tempfile.TemporaryDirectory(prefix="sc_tool_check") as d:
913
- try:
914
- versions: Dict[str, Optional[str]] = {}
944
+ with tempfile.TemporaryDirectory(
945
+ prefix=f"sc_tool_check_{self.project.option.get_jobname()}_") as d:
946
+ versions: Dict[str, Optional[str]] = {}
915
947
 
916
- self.__logger.debug(f"Executing tool checks in: {d}")
917
- os.chdir(d)
918
- for (step, index) in self.__flow_runtime.get_nodes():
919
- if self.__project.option.scheduler.get_name(step=step, index=index) is not None:
920
- continue
948
+ self.__logger.debug(f"Executing tool checks in: {d}")
949
+ for (step, index) in self.__flow_runtime.get_nodes():
950
+ if self.__project.option.scheduler.get_name(step=step, index=index) is not None:
951
+ continue
921
952
 
922
- node = self.__tasks[(step, index)]
923
- with node.runtime():
924
- try:
925
- exe = node.get_exe_path()
926
- except TaskExecutableNotReceived:
927
- continue
928
- except TaskExecutableNotFound:
929
- exe = node.task.get("exe")
930
- self.__logger.error(f"Executable for {step}/{index} could not "
931
- f"be found: {exe}")
932
- error = True
933
- continue
953
+ node = self.__tasks[(step, index)]
954
+ with node.runtime():
955
+ try:
956
+ exe = node.get_exe_path()
957
+ except TaskExecutableNotReceived:
958
+ continue
959
+ except TaskExecutableNotFound:
960
+ exe = node.task.get("exe")
961
+ self.__logger.error(f"Executable for {step}/{index} could not "
962
+ f"be found: {exe}")
963
+ error = True
964
+ continue
934
965
 
935
- try:
936
- if exe:
937
- version: Optional[str] = versions.get(exe, None)
938
- version, check = node.check_version(version)
939
- versions[exe] = version
940
- if not check:
941
- self.__logger.error(f"Executable for {step}/{index} did not "
942
- "meet version checks")
943
- error = True
944
- except NotImplementedError:
945
- self.__logger.error(f"Unable to process version for {step}/{index}")
946
- finally:
947
- os.chdir(cwd)
966
+ try:
967
+ if exe:
968
+ version: Optional[str] = versions.get(exe, None)
969
+ version, check = node.check_version(version, workdir=d)
970
+ versions[exe] = version
971
+ if not check:
972
+ self.__logger.error(f"Executable for {step}/{index} did not "
973
+ "meet version checks")
974
+ error = True
975
+ except NotImplementedError:
976
+ self.__logger.error(f"Unable to process version for {step}/{index}")
948
977
 
949
978
  return not error
950
979
 
@@ -895,7 +895,8 @@ class SchedulerNode:
895
895
  with self.__set_env():
896
896
  return self.__task.get_exe()
897
897
 
898
- def check_version(self, version: Optional[str] = None) -> Tuple[Optional[str], bool]:
898
+ def check_version(self, version: Optional[str] = None,
899
+ workdir: Optional[str] = None) -> Tuple[Optional[str], bool]:
899
900
  """Checks the version of the tool for this task.
900
901
 
901
902
  Compares a version string against the tool's requirements. This check
@@ -908,6 +909,8 @@ class SchedulerNode:
908
909
  Args:
909
910
  version: The version string to check. If None, the task's
910
911
  configured version is fetched and used.
912
+ workdir: The working directory to use for the version check. If None,
913
+ the current working directory is used.
911
914
 
912
915
  Returns:
913
916
  A tuple (version_str, check_passed):
@@ -921,7 +924,7 @@ class SchedulerNode:
921
924
 
922
925
  with self.__set_env():
923
926
  if version is None:
924
- version = self.__task.get_exe_version()
927
+ version = self.__task.get_exe_version(workdir=workdir)
925
928
 
926
929
  check = self.__task.check_exe_version(version)
927
930
 
@@ -209,8 +209,7 @@ class SlurmSchedulerNode(SchedulerNode):
209
209
  self._init_run_logger()
210
210
 
211
211
  # Determine which cluster parititon to use.
212
- partition = self.project.get('option', 'scheduler', 'queue',
213
- step=self.step, index=self.index)
212
+ partition = self.project.option.scheduler.get_queue(step=self.step, index=self.index)
214
213
  if not partition:
215
214
  partition = SlurmSchedulerNode.get_slurm_partition()
216
215
 
@@ -246,7 +245,6 @@ class SlurmSchedulerNode(SchedulerNode):
246
245
  os.stat(script_file).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
247
246
 
248
247
  schedule_cmd = ['srun',
249
- '--exclusive',
250
248
  '--partition', partition,
251
249
  '--chdir', self.project_cwd,
252
250
  '--job-name', SlurmSchedulerNode.get_job_name(self.__job_hash,
@@ -254,17 +252,20 @@ class SlurmSchedulerNode(SchedulerNode):
254
252
  '--output', log_file]
255
253
 
256
254
  # Only delay the starting time if the 'defer' Schema option is specified.
257
- defer_time = self.project.get('option', 'scheduler', 'defer',
258
- step=self.step, index=self.index)
255
+ defer_time = self.project.option.scheduler.get_defer(step=self.step, index=self.index)
259
256
  if defer_time:
260
257
  schedule_cmd.extend(['--begin', defer_time])
261
258
 
259
+ # Forward additional user options
260
+ options = self.project.option.scheduler.get_options(step=self.step, index=self.index)
261
+ if options:
262
+ schedule_cmd.extend(options)
263
+
262
264
  schedule_cmd.append(script_file)
263
265
 
264
266
  self.logger.debug(f"Executing slurm command: {shlex.join(schedule_cmd)}")
265
267
 
266
268
  # Run the 'srun' command, and track its output.
267
- # TODO: output should be fed to log, and stdout if quiet = False
268
269
  step_result = subprocess.Popen(schedule_cmd,
269
270
  stdin=subprocess.DEVNULL,
270
271
  stdout=subprocess.PIPE,
@@ -5,7 +5,7 @@ import time
5
5
 
6
6
  import os.path
7
7
 
8
- from typing import List, Dict, Tuple, Optional, Callable, ClassVar, Any, Literal, TYPE_CHECKING
8
+ from typing import List, Dict, Tuple, Optional, Callable, Any, Literal, TYPE_CHECKING
9
9
 
10
10
  from logging.handlers import QueueListener
11
11
 
@@ -35,12 +35,6 @@ class TaskScheduler:
35
35
  dependency checking. It operates on a set of pending tasks defined by the
36
36
  main Scheduler and executes them in a loop until the flow is complete.
37
37
  """
38
- __callbacks: ClassVar[Dict[str, Callable[..., None]]] = {
39
- "pre_run": lambda project: None,
40
- "pre_node": lambda project, step, index: None,
41
- "post_node": lambda project, step, index: None,
42
- "post_run": lambda project: None,
43
- }
44
38
 
45
39
  @staticmethod
46
40
  def register_callback(hook: Literal["pre_run", "pre_node", "post_node", "post_run"],
@@ -57,9 +51,9 @@ class TaskScheduler:
57
51
  Raises:
58
52
  ValueError: If the specified hook is not valid.
59
53
  """
60
- if hook not in TaskScheduler.__callbacks:
54
+ if hook not in ('pre_run', 'pre_node', 'post_node', 'post_run'):
61
55
  raise ValueError(f"{hook} is not a valid callback")
62
- TaskScheduler.__callbacks[hook] = func
56
+ MPManager.get_transient_settings().set('TaskScheduler', hook, func)
63
57
 
64
58
  def __init__(self, project: "Project", tasks: Dict[Tuple[str, str], "SchedulerNode"]):
65
59
  """Initializes the TaskScheduler.
@@ -139,9 +133,6 @@ class TaskScheduler:
139
133
  threads = self.__max_threads
140
134
  task["threads"] = max(1, min(threads, self.__max_threads))
141
135
 
142
- task["parent_pipe"], pipe = multiprocessing.Pipe()
143
- task["node"].set_queue(pipe, self.__log_queue)
144
-
145
136
  task["proc"] = multiprocessing.Process(target=task["node"].run)
146
137
  self.__nodes[(step, index)] = task
147
138
 
@@ -181,11 +172,13 @@ class TaskScheduler:
181
172
  if self.__dashboard:
182
173
  self.__dashboard.update_manifest()
183
174
 
184
- TaskScheduler.__callbacks["pre_run"](self.__project)
175
+ MPManager.get_transient_settings().get(
176
+ 'TaskScheduler', 'pre_run', lambda project: None)(self.__project)
185
177
 
186
178
  try:
187
179
  self.__run_loop()
188
- TaskScheduler.__callbacks["post_run"](self.__project)
180
+ MPManager.get_transient_settings().get(
181
+ 'TaskScheduler', 'post_run', lambda project: None)(self.__project)
189
182
  except KeyboardInterrupt:
190
183
  # exit immediately
191
184
  log_listener.stop()
@@ -304,6 +297,10 @@ class TaskScheduler:
304
297
  except: # noqa E722
305
298
  pass
306
299
 
300
+ # Remove pipe
301
+ info["parent_pipe"] = None
302
+ info["node"].set_queue(None, None)
303
+
307
304
  step, index = node
308
305
  if info["proc"].exitcode > 0:
309
306
  status = NodeStatus.ERROR
@@ -319,7 +316,9 @@ class TaskScheduler:
319
316
 
320
317
  changed = True
321
318
 
322
- TaskScheduler.__callbacks['post_node'](self.__project, step, index)
319
+ MPManager.get_transient_settings().get(
320
+ 'TaskScheduler', 'post_node',
321
+ lambda project, step, index: None)(self.__project, step, index)
323
322
 
324
323
  return changed
325
324
 
@@ -406,7 +405,9 @@ class TaskScheduler:
406
405
  if ready and self.__allow_start(node):
407
406
  self.__logger.debug(f'Launching {info["name"]}')
408
407
 
409
- TaskScheduler.__callbacks['pre_node'](self.__project, step, index)
408
+ MPManager.get_transient_settings().get(
409
+ 'TaskScheduler', 'pre_node',
410
+ lambda project, step, index: None)(self.__project, step, index)
410
411
 
411
412
  self.__record.set('status', NodeStatus.RUNNING, step=step, index=index)
412
413
  self.__startTimes[node] = time.time()
@@ -414,6 +415,8 @@ class TaskScheduler:
414
415
 
415
416
  # Start the process
416
417
  info["running"] = True
418
+ info["parent_pipe"], pipe = multiprocessing.Pipe()
419
+ info["node"].set_queue(pipe, self.__log_queue)
417
420
  info["proc"].start()
418
421
 
419
422
  return changed
@@ -1,2 +1,2 @@
1
1
  # Version number following semver standard.
2
- version = '0.52.1'
2
+ version = '0.53.1'
@@ -44,12 +44,10 @@ class NamedSchema(BaseSchema):
44
44
  """
45
45
 
46
46
  try:
47
- if self.__name is not None:
48
- raise RuntimeError("Cannot call set_name more than once.")
47
+ if self._parent() is not self:
48
+ raise RuntimeError("Cannot call set_name after it has been inserted into schema.")
49
49
  except AttributeError:
50
50
  pass
51
- if name is not None and "." in name:
52
- raise ValueError("Named schema object cannot contains: .")
53
51
  self.__name = name
54
52
 
55
53
  def type(self) -> str:
@@ -172,9 +172,6 @@ class CommandLineSchema(BaseSchema):
172
172
 
173
173
  # Iterate over all keys from an empty schema to add parser arguments
174
174
  for keypath in sorted(keyschema.allkeys()):
175
- if keypath == ("option", "cfg"): # TODO: remove this when cfg is removed from schema
176
- continue
177
-
178
175
  param: Parameter = keyschema.get(*keypath, field=None)
179
176
 
180
177
  dest, switches = param.add_commandline_arguments(
@@ -210,12 +210,6 @@ class DependencySchema(BaseSchema):
210
210
 
211
211
  if name:
212
212
  if not self.has_dep(name):
213
- if "." in name:
214
- name0, *name1 = name.split(".")
215
- subdep = self.get_dep(name0)
216
- if isinstance(subdep, DependencySchema):
217
- return subdep.get_dep(".".join(name1))
218
- raise KeyError(f"{name} does not contain dependency information")
219
213
  raise KeyError(f"{name} is not an imported module")
220
214
 
221
215
  return self.__deps[name]
@@ -15,6 +15,7 @@ from enum import Enum
15
15
  from siliconcompiler.schema import BaseSchema, LazyLoad
16
16
  from siliconcompiler.schema import EditableSchema, Parameter, PerNode, Scope
17
17
  from siliconcompiler.schema.utils import trim
18
+ from siliconcompiler.utils.multiprocessing import MPManager
18
19
 
19
20
  from siliconcompiler import _metadata
20
21
 
@@ -138,9 +139,9 @@ class RecordSchema(BaseSchema):
138
139
  "region": str
139
140
  }
140
141
  '''
141
- # TODO: add logic to figure out if we're running on a remote cluster and
142
- # extract the region in a provider-specific way.
143
- return {"region": "local"}
142
+
143
+ region = MPManager().get_settings().get("record", "region", default="local")
144
+ return {"region": region}
144
145
 
145
146
  @staticmethod
146
147
  def get_ip_information() -> Dict[str, Optional[str]]:
siliconcompiler/tool.py CHANGED
@@ -11,7 +11,6 @@ import shlex
11
11
  import shutil
12
12
  import subprocess
13
13
  import sys
14
- import threading
15
14
  import time
16
15
  import yaml
17
16
 
@@ -35,7 +34,7 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier
35
34
  from typing import List, Dict, Tuple, Union, Optional, Set, TextIO, Type, TypeVar, TYPE_CHECKING
36
35
  from pathlib import Path
37
36
 
38
- from siliconcompiler.schema import BaseSchema, NamedSchema, Journal, DocsSchema, LazyLoad
37
+ from siliconcompiler.schema import BaseSchema, NamedSchema, DocsSchema, LazyLoad
39
38
  from siliconcompiler.schema import EditableSchema, Parameter, PerNode, Scope
40
39
  from siliconcompiler.schema.parametertype import NodeType
41
40
  from siliconcompiler.schema.utils import trim
@@ -43,6 +42,7 @@ from siliconcompiler.schema.utils import trim
43
42
  from siliconcompiler import utils, NodeStatus, Flowgraph
44
43
  from siliconcompiler import sc_open
45
44
  from siliconcompiler.utils import paths
45
+ from siliconcompiler.utils.multiprocessing import MPManager
46
46
 
47
47
  from siliconcompiler.schema_support.pathschema import PathSchema
48
48
  from siliconcompiler.schema_support.record import RecordTool, RecordSchema
@@ -390,7 +390,7 @@ class Task(NamedSchema, PathSchema, DocsSchema):
390
390
  if self.tool() in tools:
391
391
  self.logger.info(f"Missing tool can be installed via: \"sc-install {self.tool()}\"")
392
392
 
393
- def get_exe_version(self) -> Optional[str]:
393
+ def get_exe_version(self, workdir: Optional[str] = None) -> Optional[str]:
394
394
  """
395
395
  Gets the version of the task's executable by running it with a version switch.
396
396
 
@@ -398,6 +398,10 @@ class Task(NamedSchema, PathSchema, DocsSchema):
398
398
  TaskExecutableNotFound: If the executable is not found.
399
399
  NotImplementedError: If the `parse_version` method is not implemented.
400
400
 
401
+ Args:
402
+ workdir (str): The working directory to use for the version check. If None,
403
+ the current working directory is used.
404
+
401
405
  Returns:
402
406
  str: The parsed version string.
403
407
  """
@@ -422,7 +426,8 @@ class Task(NamedSchema, PathSchema, DocsSchema):
422
426
  stdin=subprocess.DEVNULL,
423
427
  stdout=subprocess.PIPE,
424
428
  stderr=subprocess.STDOUT,
425
- universal_newlines=True)
429
+ universal_newlines=True,
430
+ cwd=workdir)
426
431
 
427
432
  if proc.returncode != 0:
428
433
  self.logger.warning(f"Version check on '{exe_base}' ended with "
@@ -736,9 +741,7 @@ class Task(NamedSchema, PathSchema, DocsSchema):
736
741
  fout.write(template.render(manifest_dict='\n'.join(tcl_set_cmds),
737
742
  scroot=os.path.abspath(
738
743
  os.path.join(os.path.dirname(__file__))),
739
- toolvars=self.get_tcl_variables(manifest),
740
- record_access="get" in Journal.access(self).get_types(),
741
- record_access_id="TODO"))
744
+ toolvars=self.get_tcl_variables(manifest)))
742
745
  else:
743
746
  for cmd in tcl_set_cmds:
744
747
  fout.write(cmd + '\n')
@@ -2084,9 +2087,6 @@ class ShowTask(Task):
2084
2087
  Subclasses should implement `get_supported_show_extentions` to declare
2085
2088
  which file types they can handle.
2086
2089
  """
2087
- __TASKS_LOCK = threading.Lock()
2088
- __TASKS = {}
2089
-
2090
2090
  def __init__(self):
2091
2091
  """Initializes a ShowTask, adding specific parameters for show tasks."""
2092
2092
  super().__init__()
@@ -2133,8 +2133,10 @@ class ShowTask(Task):
2133
2133
  if not cls.__check_task(task):
2134
2134
  raise TypeError(f"task must be a subclass of {cls.__name__}")
2135
2135
 
2136
- with cls.__TASKS_LOCK:
2137
- cls.__TASKS.setdefault(cls, set()).add(task)
2136
+ MPManager.get_transient_settings().set(
2137
+ cls.__name__,
2138
+ f"{task.__module__}/{task.__name__}",
2139
+ task)
2138
2140
 
2139
2141
  @classmethod
2140
2142
  def __populate_tasks(cls) -> None:
@@ -2146,6 +2148,9 @@ class ShowTask(Task):
2146
2148
  """
2147
2149
  cls.__check_task(None)
2148
2150
 
2151
+ if MPManager.get_transient_settings().get_category(cls.__name__):
2152
+ return # Already populated
2153
+
2149
2154
  def recurse(searchcls: Type["ShowTask"]):
2150
2155
  subclss = set()
2151
2156
  if not cls.__check_task(searchcls):
@@ -2160,20 +2165,25 @@ class ShowTask(Task):
2160
2165
  classes = recurse(cls)
2161
2166
 
2162
2167
  # Support non-SC defined tasks from plugins
2163
- for plugin in utils.get_plugins('showtask'): # TODO rename
2168
+ for plugin in utils.get_plugins('showtask'):
2164
2169
  plugin()
2165
2170
 
2166
2171
  if not classes:
2167
2172
  return
2168
2173
 
2169
- with ShowTask.__TASKS_LOCK:
2170
- ShowTask.__TASKS.setdefault(cls, set()).update(classes)
2174
+ for c in classes:
2175
+ cls.register_task(c)
2171
2176
 
2172
2177
  @classmethod
2173
2178
  def get_task(cls, ext: Optional[str]) -> Union[Optional["ShowTask"], Set[Type["ShowTask"]]]:
2174
2179
  """
2175
2180
  Retrieves a suitable show task instance for a given file extension.
2176
2181
 
2182
+ The method first checks the user's settings file (~/.sc/settings.json)
2183
+ under the 'showtask' category for a preferred tool. If no preference
2184
+ is found or the preferred tool is not available, it falls back to
2185
+ automatic discovery.
2186
+
2177
2187
  Args:
2178
2188
  ext (str): The file extension to find a viewer for.
2179
2189
 
@@ -2182,24 +2192,45 @@ class ShowTask(Task):
2182
2192
  no suitable task is found.
2183
2193
  """
2184
2194
  cls.__check_task(None)
2195
+ cls.__populate_tasks()
2185
2196
 
2186
- if cls not in ShowTask.__TASKS:
2187
- cls.__populate_tasks()
2188
-
2189
- with ShowTask.__TASKS_LOCK:
2190
- if cls not in ShowTask.__TASKS:
2191
- return None
2192
- tasks = ShowTask.__TASKS[cls].copy()
2193
-
2194
- # TODO: add user preference lookup (ext -> task)
2197
+ settings = MPManager.get_transient_settings()
2198
+ tasks = set(settings.get_category(cls.__name__).values())
2199
+ if not tasks:
2200
+ return None
2195
2201
 
2196
2202
  if ext is None:
2197
2203
  return tasks
2198
2204
 
2199
- for task in tasks:
2205
+ # 1. Check User Settings for Preference
2206
+ preference = MPManager.get_settings().get("showtask", ext)
2207
+
2208
+ if preference:
2209
+ # Preference format: "tool" or "tool/task"
2210
+ pref_parts = preference.split('/')
2211
+ pref_tool = pref_parts[0]
2212
+ pref_task = pref_parts[1] if len(pref_parts) > 1 else None
2213
+
2214
+ for task_cls in tasks:
2215
+ try:
2216
+ task_inst = task_cls()
2217
+ # Check if this task matches the preference
2218
+ if task_inst.tool() == pref_tool:
2219
+ if pref_task and task_inst.task() != pref_task:
2220
+ continue
2221
+
2222
+ # Verify the preferred tool actually supports the extension
2223
+ if ext in task_inst.get_supported_show_extentions():
2224
+ return task_inst
2225
+ except NotImplementedError:
2226
+ continue
2227
+
2228
+ # 2. Fallback to Automatic Discovery
2229
+ for task_cls in tasks:
2200
2230
  try:
2201
- if ext in task().get_supported_show_extentions():
2202
- return task()
2231
+ task_inst = task_cls()
2232
+ if ext in task_inst.get_supported_show_extentions():
2233
+ return task_inst
2203
2234
  except NotImplementedError:
2204
2235
  pass
2205
2236
 
@@ -1,11 +1,5 @@
1
- proc _sc_cfg_get_debug { args } {
2
-
3
- }
4
-
5
1
  # Shortcut to get values from configuration
6
2
  proc sc_cfg_get { args } {
7
- _sc_cfg_get_debug $args
8
-
9
3
  # Refer to global sc_cfg dictionary
10
4
  global sc_cfg
11
5
 
@@ -4,6 +4,8 @@ import glob
4
4
 
5
5
  import os.path
6
6
 
7
+ from typing import Optional, List, Union
8
+
7
9
  from siliconcompiler import sc_open
8
10
 
9
11
  from siliconcompiler import Task
@@ -18,6 +20,48 @@ class ConvertTask(Task):
18
20
  self.add_parameter("targetdir", "str", "Output target directory name",
19
21
  defvalue="chisel-output")
20
22
 
23
+ def set_chisel_application(self, application: str,
24
+ step: Optional[str] = None, index: Optional[str] = None) -> None:
25
+ """
26
+ Sets the application name of the chisel program.
27
+
28
+ Args:
29
+ application (str): The application name.
30
+ step (str, optional): The specific step to apply this configuration to.
31
+ index (str, optional): The specific index to apply this configuration to.
32
+ """
33
+ self.set("var", "application", application, step=step, index=index)
34
+
35
+ def add_chisel_argument(self, argument: Union[str, List[str]],
36
+ step: Optional[str] = None, index: Optional[str] = None,
37
+ clobber: bool = False) -> None:
38
+ """
39
+ Adds arguments for the chisel build.
40
+
41
+ Args:
42
+ argument (Union[str, List[str]]): The argument(s) to add.
43
+ step (str, optional): The specific step to apply this configuration to.
44
+ index (str, optional): The specific index to apply this configuration to.
45
+ clobber (bool, optional): If True, overwrites the existing list. Defaults to False.
46
+ """
47
+ if clobber:
48
+ self.set("var", "argument", argument, step=step, index=index)
49
+ else:
50
+ self.add("var", "argument", argument, step=step, index=index)
51
+
52
+ def set_chisel_targetdir(self, targetdir: str,
53
+ step: Optional[str] = None,
54
+ index: Optional[str] = None) -> None:
55
+ """
56
+ Sets the output target directory name.
57
+
58
+ Args:
59
+ targetdir (str): The target directory name.
60
+ step (str, optional): The specific step to apply this configuration to.
61
+ index (str, optional): The specific index to apply this configuration to.
62
+ """
63
+ self.set("var", "targetdir", targetdir, step=step, index=index)
64
+
21
65
  def tool(self):
22
66
  return "chisel"
23
67