librelane 2.4.0.dev11__py3-none-any.whl → 2.4.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.

Potentially problematic release.


This version of librelane might be problematic. Click here for more details.

librelane/env_info.py CHANGED
@@ -23,17 +23,16 @@ import os
23
23
  import re
24
24
  import sys
25
25
  import json
26
+ import shutil
26
27
  import tempfile
27
28
  import platform
28
29
  import subprocess
29
30
 
30
31
  try:
31
- from typing import Optional, Dict, List # noqa: F401
32
+ from typing import Union, Optional, Dict, List # noqa: F401
32
33
  except ImportError:
33
34
  pass
34
35
 
35
- CONTAINER_ENGINE = os.getenv("OPENLANE_CONTAINER_ENGINE", "docker")
36
-
37
36
 
38
37
  class StringRepresentable(object):
39
38
  def __str__(self):
@@ -44,6 +43,7 @@ class StringRepresentable(object):
44
43
 
45
44
 
46
45
  class ContainerInfo(StringRepresentable):
46
+ path = None # type: Optional[str]
47
47
  engine = "UNKNOWN" # type: str
48
48
  version = "UNKNOWN" # type: str
49
49
  conmon = False # type: bool
@@ -54,63 +54,84 @@ class ContainerInfo(StringRepresentable):
54
54
  self.version = "UNKNOWN"
55
55
  self.conmon = False
56
56
  self.rootless = False
57
+ self.seccomp = False
58
+ self.selinux = False
59
+ self.apparmor = False
57
60
 
58
61
  @staticmethod
59
62
  def get():
60
- # type: () -> Optional['ContainerInfo']
63
+ # type: () -> Union[ContainerInfo, str]
64
+ cinfo = ContainerInfo()
65
+ # Here are the rules:
66
+ # 1. If LIBRELANE_CONTAINER_ENGINE exists, use that uncritically.
67
+ # 2. Else, if OPENLANE_CONTAINER_ENGINE exists, use that uncritically.
68
+ # 3. Else, if "docker" is in PATH, always use it.
69
+ # 4. Else, see if "podman" is in PATH, and use THAT.
70
+ # 5. If none exist, halt and return early.
71
+
72
+ container_engine = os.getenv(
73
+ "LIBRELANE_CONTAINER_ENGINE", os.getenv("OPENLANE_CONTAINER_ENGINE")
74
+ )
75
+ if container_engine is None or container_engine == "":
76
+ container_engine = shutil.which("docker")
77
+ if container_engine is None:
78
+ container_engine = shutil.which("podman")
79
+ if container_engine is None:
80
+ return "no compatible container engine found in PATH (tried docker, podman)"
61
81
  try:
62
- cinfo = ContainerInfo()
82
+ info_str = subprocess.check_output(
83
+ [container_engine, "info", "--format", "{{json .}}"]
84
+ ).decode("utf8")
85
+ except Exception as e:
86
+ return "failed to get container engine info: %s" % str(e)
87
+ cinfo.path = container_engine
63
88
 
89
+ try:
90
+ info = json.loads(info_str)
91
+ except Exception as e:
92
+ return "result from '%s info' was not valid JSON: %s" % (
93
+ container_engine,
94
+ str(e),
95
+ )
96
+
97
+ if (
98
+ info.get("Docker Root Dir") is not None
99
+ or info.get("DockerRootDir") is not None
100
+ ):
101
+ cinfo.engine = "docker"
102
+
103
+ # Get Version
64
104
  try:
65
- info_str = subprocess.check_output(
66
- [CONTAINER_ENGINE, "info", "--format", "{{json .}}"]
67
- ).decode("utf8")
68
- except Exception as e:
69
- raise Exception("Failed to get Docker info: %s" % str(e)) from None
105
+ version_output = (
106
+ subprocess.check_output([container_engine, "--version"])
107
+ .decode("utf8")
108
+ .strip()
109
+ )
110
+ cinfo.version = re.split(r"\s", version_output)[2].strip(",")
111
+ except Exception:
112
+ pass
70
113
 
71
- try:
72
- info = json.loads(info_str)
73
- except Exception as e:
74
- raise Exception(
75
- "Result from 'docker info' was not valid JSON: %s" % str(e)
76
- ) from None
77
-
78
- if info.get("host") is not None:
79
- if info["host"].get("conmon") is not None:
80
- cinfo.conmon = True
81
- if (
82
- info["host"].get("remoteSocket") is not None
83
- and "podman" in info["host"]["remoteSocket"]["path"]
84
- ):
85
- cinfo.engine = "podman"
86
-
87
- cinfo.version = info["version"]["Version"]
88
- elif (
89
- info.get("Docker Root Dir") is not None
90
- or info.get("DockerRootDir") is not None
91
- ):
92
- cinfo.engine = "docker"
93
-
94
- # Get Version
95
- try:
96
- version_output = (
97
- subprocess.check_output([CONTAINER_ENGINE, "--version"])
98
- .decode("utf8")
99
- .strip()
100
- )
101
- cinfo.version = re.split(r"\s", version_output)[2].strip(",")
102
- except Exception:
103
- print("Could not extract Docker version.", file=sys.stderr)
104
-
105
- security_options = info.get("SecurityOptions")
106
- for option in security_options:
107
- if "rootless" in option:
108
- cinfo.rootless = True
109
-
110
- return cinfo
111
- except Exception as e:
112
- print(e, file=sys.stderr)
113
- return None
114
+ security_options = info.get("SecurityOptions")
115
+ for option in security_options:
116
+ if "rootless" in option:
117
+ cinfo.rootless = True
118
+ elif info.get("host") is not None:
119
+ host = info["host"]
120
+ conmon = host.get("conmon")
121
+ remote_socket = host.get("remoteSocket")
122
+ security = host.get("security")
123
+ if conmon is not None:
124
+ cinfo.conmon = True
125
+ if remote_socket is not None and "podman" in remote_socket["path"]:
126
+ cinfo.engine = "podman"
127
+ cinfo.version = info["version"]["Version"]
128
+ if security is not None:
129
+ cinfo.rootless = security.get("rootless", False)
130
+ cinfo.apparmor = security.get("apparmorEnabled", False)
131
+ cinfo.seccomp = security.get("seccompEnabled", False)
132
+ cinfo.selinux = security.get("selinuxEnabled", False)
133
+
134
+ return cinfo
114
135
 
115
136
 
116
137
  class NixInfo(StringRepresentable):
@@ -127,73 +148,65 @@ class NixInfo(StringRepresentable):
127
148
 
128
149
  @staticmethod
129
150
  def get():
130
- # type: () -> Optional['NixInfo']
151
+ # type: () -> Union[NixInfo, str]
131
152
  ninfo = NixInfo()
153
+ if shutil.which("nix") is None:
154
+ return "nix not found in PATH"
132
155
  try:
133
- try:
134
- version_str = subprocess.check_output(
135
- ["nix", "--version"], encoding="utf8"
136
- )
137
- ninfo.version_string = version_str.strip()
138
- except Exception as e:
139
- raise Exception("Failed to get Nix info: %s" % str(e)) from None
156
+ version_str = subprocess.check_output(["nix", "--version"], encoding="utf8")
157
+ ninfo.version_string = version_str.strip()
158
+ except Exception as e:
159
+ return "could not get nix version: %s" % str(e)
140
160
 
141
- try:
142
- channels = {}
143
- channels_raw = subprocess.check_output(
144
- ["nix-channel", "--list"], encoding="utf8"
145
- )
146
- for channel in channels_raw.splitlines():
147
- name, url = channel.split(maxsplit=1)
148
- channels[name] = url
149
- ninfo.channels = channels
150
- except Exception as e:
161
+ try:
162
+ channels = {}
163
+ channels_raw = subprocess.check_output(
164
+ ["nix-channel", "--list"], encoding="utf8"
165
+ )
166
+ for channel in channels_raw.splitlines():
167
+ name, url = channel.split(maxsplit=1)
168
+ channels[name] = url
169
+ ninfo.channels = channels
170
+ except Exception:
171
+ pass
172
+
173
+ with tempfile.TemporaryDirectory(prefix="librelane_env_report_") as d:
174
+ with open(os.path.join(d, "flake.nix"), "w") as f:
175
+ f.write("{}")
176
+ nix_command = subprocess.run(
177
+ ["nix", "eval"],
178
+ stdout=subprocess.PIPE,
179
+ stderr=subprocess.STDOUT,
180
+ cwd=d,
181
+ encoding="utf8",
182
+ )
183
+ nix_command_result = nix_command.stdout
184
+ if "'nix-command'" in nix_command_result:
185
+ pass
186
+ elif "'flakes'" in nix_command_result:
187
+ ninfo.nix_command = True
188
+ elif "lacks attribute" in nix_command_result:
189
+ ninfo.nix_command = True
190
+ ninfo.flakes = True
191
+ else:
151
192
  print(
152
- "Failed to get nix channels: %s" % str(e),
193
+ "'nix flake' returned unexpected output: %s" % nix_command_result,
153
194
  file=sys.stderr,
154
195
  )
155
196
 
156
- with tempfile.TemporaryDirectory(prefix="librelane_env_report_") as d:
157
- with open(os.path.join(d, "flake.nix"), "w") as f:
158
- f.write("{}")
159
- nix_command = subprocess.run(
160
- ["nix", "eval"],
161
- stdout=subprocess.PIPE,
162
- stderr=subprocess.STDOUT,
163
- cwd=d,
164
- encoding="utf8",
165
- )
166
- nix_command_result = nix_command.stdout
167
- if "'nix-command'" in nix_command_result:
168
- pass
169
- elif "'flakes'" in nix_command_result:
170
- ninfo.nix_command = True
171
- elif "lacks attribute" in nix_command_result:
172
- ninfo.nix_command = True
173
- ninfo.flakes = True
174
- else:
175
- print(
176
- "'nix flake' returned unexpected output: %s"
177
- % nix_command_result,
178
- file=sys.stderr,
179
- )
180
-
181
- return ninfo
182
- except Exception as e:
183
- print(e, file=sys.stderr)
184
- return None
197
+ return ninfo
185
198
 
186
199
 
187
200
  class OSInfo(StringRepresentable):
188
201
  kernel = "" # type: str
189
202
  kernel_version = "" # type: str
190
203
  supported = False # type: bool
191
- distro = None # type: Optional[str]
192
- distro_version = None # type: Optional[str]
204
+ distro = "UNKNOWN" # type: str
205
+ distro_version = "UNKNOWN" # type: str
193
206
  python_version = "" # type: str
194
207
  python_path = [] # type: List[str]
195
- container_info = None # type: Optional[ContainerInfo]
196
- nix_info = None # type: Optional[NixInfo]
208
+ container_info = None # type: Union[ContainerInfo, str]
209
+ nix_info = None # type: Union[NixInfo, str]
197
210
 
198
211
  def __init__(self):
199
212
  self.kernel = platform.system()
@@ -201,8 +214,8 @@ class OSInfo(StringRepresentable):
201
214
  platform.release()
202
215
  ) # Unintuitively enough, it's the kernel's release
203
216
  self.supported = self.kernel in ["Darwin", "Linux"]
204
- self.distro = None
205
- self.distro_version = None
217
+ self.distro = "UNKNOWN"
218
+ self.distro_version = "UNKNOWN"
206
219
  self.python_version = platform.python_version()
207
220
  self.python_path = sys.path.copy()
208
221
  self.tkinter = False
@@ -212,8 +225,8 @@ class OSInfo(StringRepresentable):
212
225
  self.tkinter = True
213
226
  except ImportError:
214
227
  pass
215
- self.container_info = None
216
- self.nix_info = None
228
+ self.container_info = ""
229
+ self.nix_info = ""
217
230
 
218
231
  @staticmethod
219
232
  def get():
@@ -253,13 +266,14 @@ class OSInfo(StringRepresentable):
253
266
 
254
267
  config[key] = value
255
268
 
256
- osinfo.distro = config.get("ID") or config.get("DISTRIB_ID")
257
- osinfo.distro_version = config.get("VERSION_ID") or config.get(
258
- "DISTRIB_RELEASE"
269
+ osinfo.distro = (
270
+ config.get("ID") or config.get("DISTRIB_ID") or "UNKNOWN"
271
+ )
272
+ osinfo.distro_version = (
273
+ config.get("VERSION_ID")
274
+ or config.get("DISTRIB_RELEASE")
275
+ or "UNKNOWN"
259
276
  )
260
-
261
- else:
262
- print("Failed to get distribution info.", file=sys.stderr)
263
277
 
264
278
  osinfo.container_info = ContainerInfo.get()
265
279
  osinfo.nix_info = NixInfo.get()
librelane/flows/flow.py CHANGED
@@ -375,7 +375,7 @@ class Flow(ABC):
375
375
  self.progress_bar = FlowProgressBar(self.name)
376
376
 
377
377
  @classmethod
378
- def get_help_md(Self, myst_anchors: bool = True) -> str: # pragma: no cover
378
+ def get_help_md(Self, myst_anchors: bool = False) -> str: # pragma: no cover
379
379
  """
380
380
  :returns: rendered Markdown help for this Flow
381
381
  """
@@ -415,10 +415,10 @@ class Flow(ABC):
415
415
  flow_config_vars = Self.config_vars
416
416
 
417
417
  if len(flow_config_vars):
418
+ config_var_anchors = f"({slugify(Self.__name__, lower=True)}-config-vars)="
418
419
  result += textwrap.dedent(
419
420
  f"""
420
- ({slugify(Self.__name__, lower=True)}-config-vars)=
421
-
421
+ {config_var_anchors * myst_anchors}
422
422
  #### Flow-specific Configuration Variables
423
423
 
424
424
  | Variable Name | Type | Description | Default | Units |
@@ -435,18 +435,14 @@ class Flow(ABC):
435
435
  if len(Self.Steps):
436
436
  result += "#### Included Steps\n"
437
437
  for step in Self.Steps:
438
- if hasattr(step, "long_name"):
439
- name = step.long_name
440
- elif hasattr(step, "name"):
441
- name = step.name
442
- else:
443
- name = step.id
438
+ imp_id = step.get_implementation_id()
444
439
  if myst_anchors:
445
- result += (
446
- f"* [`{step.id}`](./step_config_vars.md#{slugify(name)})\n"
447
- )
440
+ result += f"* [`{step.id}`](./step_config_vars.md#step-{slugify(imp_id, lower=True)})\n"
448
441
  else:
449
- result += f"* {step.id}"
442
+ variant_str = ""
443
+ if imp_id != step.id:
444
+ variant_str = f" (implementation: `{imp_id}`)"
445
+ result += f"* `{step.id}`{variant_str}\n"
450
446
 
451
447
  return result
452
448
 
@@ -824,7 +820,6 @@ class Flow(ABC):
824
820
  DesignFormat.POWERED_NETLIST: (os.path.join("verilog", "gl"), "v"),
825
821
  DesignFormat.DEF: ("def", "def"),
826
822
  DesignFormat.LEF: ("lef", "lef"),
827
- DesignFormat.SDF: (os.path.join("sdf", "multicorner"), "sdf"),
828
823
  DesignFormat.SPEF: (os.path.join("spef", "multicorner"), "spef"),
829
824
  DesignFormat.LIB: (os.path.join("lib", "multicorner"), "lib"),
830
825
  DesignFormat.GDS: ("gds", "gds"),
@@ -883,38 +878,67 @@ class Flow(ABC):
883
878
  file_path, os.path.join(to_dir, file), follow_symlinks=True
884
879
  )
885
880
 
886
- signoff_folder = os.path.join(
887
- path, "signoff", self.config["DESIGN_NAME"], "librelane-signoff"
888
- )
889
- mkdirp(signoff_folder)
881
+ def find_one(pattern):
882
+ result = glob.glob(pattern)
883
+ if len(result) == 0:
884
+ return None
885
+ return result[0]
890
886
 
891
- # resolved.json
887
+ signoff_dir = os.path.join(path, "signoff", self.config["DESIGN_NAME"])
888
+ openlane_signoff_dir = os.path.join(signoff_dir, "openlane-signoff")
889
+ mkdirp(openlane_signoff_dir)
890
+
891
+ ## resolved.json
892
892
  shutil.copyfile(
893
893
  self.config_resolved_path,
894
- os.path.join(signoff_folder, "resolved.json"),
894
+ os.path.join(openlane_signoff_dir, "resolved.json"),
895
895
  follow_symlinks=True,
896
896
  )
897
897
 
898
- # Logs
899
- mkdirp(signoff_folder)
900
- copy_dir_contents(self.run_dir, signoff_folder, "*.log")
898
+ ## metrics
899
+ with open(os.path.join(signoff_dir, "metrics.csv"), "w", encoding="utf8") as f:
900
+ last_state.metrics_to_csv(f)
901
+
902
+ ## flow logs
903
+ mkdirp(openlane_signoff_dir)
904
+ copy_dir_contents(self.run_dir, openlane_signoff_dir, "*.log")
901
905
 
902
- # Step-specific
906
+ ### step-specific signoff logs and reports
903
907
  for step in self.step_objects:
904
908
  reports_dir = os.path.join(step.step_dir, "reports")
905
909
  step_imp_id = step.get_implementation_id()
910
+ if step_imp_id == "Magic.DRC":
911
+ if drc_rpt := find_one(os.path.join(reports_dir, "*.rpt")):
912
+ shutil.copyfile(
913
+ drc_rpt, os.path.join(openlane_signoff_dir, "drc.rpt")
914
+ )
915
+ if drc_xml := find_one(os.path.join(reports_dir, "*.xml")):
916
+ # Despite the name, this is the Magic DRC report simply
917
+ # converted into a KLayout-compatible format. Confusing!
918
+ drc_xml_out = os.path.join(openlane_signoff_dir, "drc.klayout.xml")
919
+ with open(drc_xml, encoding="utf8") as i, open(
920
+ drc_xml_out, "w", encoding="utf8"
921
+ ) as o:
922
+ o.write(
923
+ "<!-- Despite the name, this is the Magic DRC report in KLayout format. -->\n"
924
+ )
925
+ shutil.copyfileobj(i, o)
926
+ if step_imp_id == "Netgen.LVS":
927
+ if lvs_rpt := find_one(os.path.join(reports_dir, "*.rpt")):
928
+ shutil.copyfile(
929
+ lvs_rpt, os.path.join(openlane_signoff_dir, "lvs.rpt")
930
+ )
906
931
  if step_imp_id.endswith("DRC") or step_imp_id.endswith("LVS"):
907
- if os.path.exists(reports_dir):
908
- copy_dir_contents(reports_dir, signoff_folder)
909
- if step_imp_id.endswith("LVS"):
910
- copy_dir_contents(step.step_dir, signoff_folder, "*.log")
932
+ copy_dir_contents(step.step_dir, openlane_signoff_dir, "*.log")
911
933
  if step_imp_id.endswith("CheckAntennas"):
912
934
  if os.path.exists(reports_dir):
913
935
  copy_dir_contents(
914
- reports_dir, signoff_folder, "antenna_summary.rpt"
936
+ reports_dir, openlane_signoff_dir, "antenna_summary.rpt"
915
937
  )
916
938
  if step_imp_id.endswith("STAPostPNR"):
917
- timing_report_folder = os.path.join(signoff_folder, "timing-reports")
939
+ timing_report_folder = os.path.join(
940
+ openlane_signoff_dir, "timing-reports"
941
+ )
918
942
  mkdirp(timing_report_folder)
919
943
  copy_dir_contents(step.step_dir, timing_report_folder, "*summary.rpt")
920
944
  for dir in os.listdir(step.step_dir):
@@ -925,6 +949,18 @@ class Flow(ABC):
925
949
  mkdirp(target)
926
950
  copy_dir_contents(dir_path, target, "*.rpt")
927
951
 
952
+ # 3. SDF
953
+ # (This one, as with many things in the Efabless format, is special)
954
+ if sdf := last_state[DesignFormat.SDF]:
955
+ assert isinstance(sdf, dict), "SDF is not a dictionary"
956
+ for corner, view in sdf.items():
957
+ assert isinstance(view, Path), "SDF state out returned multiple paths"
958
+ target_dir = os.path.join(signoff_dir, "sdf", corner)
959
+ mkdirp(target_dir)
960
+ shutil.copyfile(
961
+ view, os.path.join(target_dir, f"{self.config['DESIGN_NAME']}.sdf")
962
+ )
963
+
928
964
  @deprecated(
929
965
  version="2.0.0a46",
930
966
  reason="Use .progress_bar.set_max_stage_count",
@@ -0,0 +1,39 @@
1
+ # Copyright 2025 LibreLane Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from ..common.cli import formatter_settings
15
+ from ..flows import Flow
16
+ from ..steps import Step
17
+ from ..logging import console
18
+
19
+ import cloup
20
+
21
+
22
+ @cloup.command(formatter_settings=formatter_settings)
23
+ @cloup.argument("step_or_flow")
24
+ @cloup.pass_context
25
+ def cli(ctx, step_or_flow):
26
+ """
27
+ Displays rich help for the step or flow in question.
28
+ """
29
+ if TargetFlow := Flow.factory.get(step_or_flow):
30
+ TargetFlow.display_help()
31
+ elif TargetStep := Step.factory.get(step_or_flow):
32
+ TargetStep.display_help()
33
+ else:
34
+ console.log(f"Unknown Flow or Step '{step_or_flow}'.")
35
+ ctx.exit(-1)
36
+
37
+
38
+ if __name__ == "__main__":
39
+ cli()
@@ -77,5 +77,3 @@ if { $::env(MAGIC_GDS_POLYGON_SUBCELLS) } {
77
77
 
78
78
  gds write $::env(SAVE_MAG_GDS)
79
79
  puts "\[INFO\] GDS Write Complete"
80
-
81
- exit 0
@@ -76,4 +76,3 @@ puts stdout "\[INFO\] Saving mag view with DRC errors ($mag_view)"
76
76
  # WARNING: changes the name of the cell; keep as last step
77
77
  save $mag_view
78
78
  puts stdout "\[INFO\] Saved"
79
- exit 0
@@ -43,5 +43,3 @@ if { [info exist ::env(EXTRA_GDS_FILES)] } {
43
43
  puts "\[INFO\] Saved mag view from $gds_file under $::env(STEP_DIR)"
44
44
  }
45
45
  }
46
-
47
- exit 0
@@ -29,4 +29,3 @@ set final_filepath $::env(signoff_tmpfiles)/gds_ptrs.mag
29
29
  file rename -force $::env(signoff_tmpfiles)/$::env(DESIGN_NAME).mag $final_filepath
30
30
 
31
31
  puts "\[INFO\] Wrote $final_filepath including GDS pointers."
32
- exit 0
@@ -59,5 +59,3 @@ foreach design_name [cellname list allcells] {
59
59
  puts $fp [join $new_mag_lines "\n"]
60
60
  close $fp
61
61
  }
62
-
63
- exit 0
@@ -24,4 +24,3 @@ cellname filepath $::env(DESIGN_NAME).lef $::env(signoff_results)
24
24
  save
25
25
 
26
26
  puts "\[INFO\] DONE GENERATING MAGLEF VIEW"
27
- exit 0
@@ -17,3 +17,5 @@ if {[catch {source $::env(_MAGIC_SCRIPT)} err]} {
17
17
  puts "Error: $err"
18
18
  exit 1
19
19
  }
20
+
21
+ exit 0
@@ -17,7 +17,6 @@ import utl
17
17
 
18
18
  import re
19
19
  import json
20
- import functools
21
20
  from dataclasses import dataclass
22
21
  from typing import Dict, List, Optional
23
22
 
@@ -49,11 +48,14 @@ class Design(object):
49
48
  def get_verilog_net_name_by_bit(self, top_module: str, target_bit: int):
50
49
  yosys_design_object = self.yosys_dict["modules"][top_module]
51
50
  if top_module not in self.verilog_net_names_by_bit_by_module:
52
- self.verilog_net_names_by_bit_by_module[top_module] = functools.reduce(
53
- lambda a, b: {**a, **{bit: b[0] for bit in b[1]["bits"]}},
54
- yosys_design_object["netnames"].items(),
55
- {},
56
- )
51
+ # check git history for a version of this loop that is drunk on power
52
+ netname_by_bit = {}
53
+
54
+ for netname, info in yosys_design_object["netnames"].items():
55
+ for bit in info["bits"]:
56
+ netname_by_bit[bit] = netname
57
+
58
+ self.verilog_net_names_by_bit_by_module[top_module] = netname_by_bit
57
59
  return self.verilog_net_names_by_bit_by_module[top_module][target_bit]
58
60
 
59
61
  def get_pins(self, module_name: str) -> Dict[str, odb.dbMTerm]:
@@ -20,7 +20,7 @@ import sys
20
20
  import json
21
21
  import locale
22
22
  import inspect
23
- import functools
23
+ from functools import wraps
24
24
  from decimal import Decimal
25
25
  from fnmatch import fnmatch
26
26
  from typing import Callable, Dict
@@ -188,7 +188,7 @@ class OdbReader(object):
188
188
 
189
189
 
190
190
  def click_odb(function):
191
- @functools.wraps(function)
191
+ @wraps(function)
192
192
  def wrapper(input_db, input_lefs, config_path, **kwargs):
193
193
  reader = OdbReader(input_db, config_path=config_path)
194
194
 
librelane/state/state.py CHANGED
@@ -13,7 +13,9 @@
13
13
  # limitations under the License.
14
14
  from __future__ import annotations
15
15
 
16
+ import io
16
17
  import os
18
+ import csv
17
19
  import sys
18
20
  import json
19
21
  import shutil
@@ -214,14 +216,20 @@ class State(GenericImmutableDict[str, StateElement]):
214
216
  self._walk(self, path, visitor)
215
217
  metrics_csv_path = os.path.join(path, "metrics.csv")
216
218
  with open(metrics_csv_path, "w", encoding="utf8") as f:
217
- f.write("Metric,Value\n")
218
- for metric in self.metrics:
219
- f.write(f"{metric},{self.metrics[metric]}\n")
219
+ self.metrics_to_csv(f)
220
220
 
221
221
  metrics_json_path = os.path.join(path, "metrics.json")
222
222
  with open(metrics_json_path, "w", encoding="utf8") as f:
223
223
  f.write(self.metrics.dumps())
224
224
 
225
+ def metrics_to_csv(
226
+ self, fp: io.TextIOWrapper, metrics_object: Optional[Dict[str, Any]] = None
227
+ ):
228
+ w = csv.writer(fp)
229
+ w.writerow(("Metric", "Value"))
230
+ for entry in (metrics_object or self.metrics).items():
231
+ w.writerow(entry)
232
+
225
233
  def validate(self):
226
234
  """
227
235
  Ensures that all paths exist in a State.
@@ -15,7 +15,6 @@ import os
15
15
  import shlex
16
16
  import shutil
17
17
  import datetime
18
- import functools
19
18
  import subprocess
20
19
  from functools import partial
21
20
  from typing import IO, Any, Dict, Optional, Sequence, Union
@@ -239,7 +238,7 @@ def eject(ctx, output, state_in, config, id):
239
238
  found_stdin_data = found_stdin.read()
240
239
  raise Stop()
241
240
 
242
- step.run_subprocess = functools.partial(
241
+ step.run_subprocess = partial(
243
242
  step.run_subprocess,
244
243
  _popen_callable=popen_substitute,
245
244
  )