librelane 2.4.0.dev2__py3-none-any.whl → 2.4.7__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 (55) hide show
  1. librelane/__init__.py +1 -1
  2. librelane/__main__.py +34 -27
  3. librelane/common/__init__.py +2 -0
  4. librelane/common/cli.py +1 -1
  5. librelane/common/drc.py +1 -0
  6. librelane/common/generic_dict.py +1 -1
  7. librelane/common/metrics/__main__.py +1 -1
  8. librelane/common/misc.py +58 -2
  9. librelane/common/tcl.py +2 -1
  10. librelane/common/types.py +2 -3
  11. librelane/config/__main__.py +1 -4
  12. librelane/config/flow.py +2 -2
  13. librelane/config/preprocessor.py +1 -1
  14. librelane/config/variable.py +136 -7
  15. librelane/container.py +55 -31
  16. librelane/env_info.py +129 -115
  17. librelane/examples/hold_eco_demo/config.yaml +18 -0
  18. librelane/examples/hold_eco_demo/demo.v +27 -0
  19. librelane/flows/cli.py +39 -23
  20. librelane/flows/flow.py +100 -36
  21. librelane/help/__main__.py +39 -0
  22. librelane/scripts/magic/def/mag_gds.tcl +0 -2
  23. librelane/scripts/magic/drc.tcl +0 -1
  24. librelane/scripts/magic/gds/extras_mag.tcl +0 -2
  25. librelane/scripts/magic/gds/mag_with_pointers.tcl +0 -1
  26. librelane/scripts/magic/lef/extras_maglef.tcl +0 -2
  27. librelane/scripts/magic/lef/maglef.tcl +0 -1
  28. librelane/scripts/magic/wrapper.tcl +2 -0
  29. librelane/scripts/odbpy/defutil.py +15 -10
  30. librelane/scripts/odbpy/eco_buffer.py +182 -0
  31. librelane/scripts/odbpy/eco_diode.py +140 -0
  32. librelane/scripts/odbpy/ioplace_parser/__init__.py +1 -1
  33. librelane/scripts/odbpy/ioplace_parser/parse.py +1 -1
  34. librelane/scripts/odbpy/power_utils.py +8 -6
  35. librelane/scripts/odbpy/reader.py +17 -13
  36. librelane/scripts/openroad/common/io.tcl +66 -2
  37. librelane/scripts/openroad/gui.tcl +23 -1
  38. librelane/state/design_format.py +16 -1
  39. librelane/state/state.py +11 -3
  40. librelane/steps/__init__.py +1 -1
  41. librelane/steps/__main__.py +4 -4
  42. librelane/steps/checker.py +7 -8
  43. librelane/steps/klayout.py +11 -1
  44. librelane/steps/magic.py +24 -14
  45. librelane/steps/misc.py +5 -0
  46. librelane/steps/odb.py +193 -28
  47. librelane/steps/openroad.py +64 -47
  48. librelane/steps/pyosys.py +18 -1
  49. librelane/steps/step.py +36 -17
  50. librelane/steps/yosys.py +9 -1
  51. {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/METADATA +10 -11
  52. {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/RECORD +54 -50
  53. {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/entry_points.txt +1 -0
  54. librelane/scripts/odbpy/exception_codes.py +0 -17
  55. {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/WHEEL +0 -0
librelane/container.py CHANGED
@@ -18,7 +18,6 @@ import re
18
18
  import uuid
19
19
  import shlex
20
20
  import pathlib
21
- import tempfile
22
21
  import subprocess
23
22
  from typing import List, NoReturn, Sequence, Optional, Union, Tuple
24
23
 
@@ -27,15 +26,15 @@ import semver
27
26
 
28
27
  from .common import mkdirp
29
28
  from .logging import err, info, warn
30
- from .env_info import OSInfo
29
+ from .env_info import ContainerInfo, OSInfo
31
30
 
32
- CONTAINER_ENGINE = os.getenv("OPENLANE_CONTAINER_ENGINE", "docker")
31
+ __file_dir__ = os.path.dirname(os.path.abspath(__file__))
33
32
 
34
33
 
35
34
  def permission_args(osinfo: OSInfo) -> List[str]:
36
35
  if (
37
36
  osinfo.kernel == "Linux"
38
- and osinfo.container_info is not None
37
+ and isinstance(osinfo.container_info, ContainerInfo)
39
38
  and osinfo.container_info.engine == "docker"
40
39
  and not osinfo.container_info.rootless
41
40
  ):
@@ -58,10 +57,13 @@ def gui_args(osinfo: OSInfo) -> List[str]:
58
57
  args += [
59
58
  "-e",
60
59
  f"DISPLAY={os.environ.get('DISPLAY')}",
61
- "-v",
62
- "/tmp/.X11-unix:/tmp/.X11-unix",
63
- "-v",
64
- f"{os.path.expanduser('~')}/.Xauthority:/.Xauthority",
60
+ ]
61
+ if os.path.isdir("/tmp/.X11-unix"):
62
+ args += ["-v", "/tmp/.X11-unix:/tmp/.X11-unix"]
63
+ homedir = os.path.expanduser("~")
64
+ if os.path.isfile(f"{homedir}/.Xauthority"):
65
+ args += ["-v", f"{homedir}/.Xauthority:/.Xauthority"]
66
+ args += [
65
67
  "--network",
66
68
  "host",
67
69
  "--security-opt",
@@ -70,9 +72,9 @@ def gui_args(osinfo: OSInfo) -> List[str]:
70
72
  return args
71
73
 
72
74
 
73
- def image_exists(image: str) -> bool:
75
+ def image_exists(ce_path: str, image: str) -> bool:
74
76
  images = (
75
- subprocess.check_output([CONTAINER_ENGINE, "images", image])
77
+ subprocess.check_output([ce_path, "images", image])
76
78
  .decode("utf8")
77
79
  .rstrip()
78
80
  .split("\n")[1:]
@@ -116,14 +118,14 @@ def remote_manifest_exists(image: str) -> bool:
116
118
  return True
117
119
 
118
120
 
119
- def ensure_image(image: str) -> bool:
120
- if image_exists(image):
121
+ def ensure_image(ce_path: str, image: str) -> bool:
122
+ if image_exists(ce_path, image):
121
123
  return True
122
124
 
123
125
  try:
124
- subprocess.check_call([CONTAINER_ENGINE, "pull", image])
126
+ subprocess.check_call([ce_path, "pull", image])
125
127
  except subprocess.CalledProcessError:
126
- err(f"Failed to pull image {image} from the container registries.")
128
+ err(f"Failed to pull image '{image}' from the container registries.")
127
129
  return False
128
130
 
129
131
  return True
@@ -150,6 +152,22 @@ def sanitize_path(path: Union[str, os.PathLike]) -> Tuple[str, str]:
150
152
  return (abspath, mountable_path)
151
153
 
152
154
 
155
+ def container_version_error(input: str, against: str) -> Optional[str]:
156
+ if input == "UNKNOWN":
157
+ return (
158
+ "Could not determine version for %s. You may encounter unexpected issues."
159
+ )
160
+ if semver.compare(input, against) < 0:
161
+ return f"Your %s version ({input}) is out of date. You may encounter unexpected issues."
162
+ return None
163
+
164
+
165
+ def ubuntu_version_at_least(current: str, minimum: str) -> bool:
166
+ if current == "UNKNOWN":
167
+ return False
168
+ return tuple(map(int, current.split("."))) >= tuple(map(int, minimum.split(".")))
169
+
170
+
153
171
  def run_in_container(
154
172
  image: str,
155
173
  args: Sequence[str],
@@ -169,20 +187,31 @@ def run_in_container(
169
187
  f"Unsupported host operating system '{osinfo.kernel}'. You may encounter unexpected issues."
170
188
  )
171
189
 
172
- if osinfo.container_info is None:
190
+ if not isinstance(osinfo.container_info, ContainerInfo):
173
191
  raise FileNotFoundError("No compatible container engine found.")
174
192
 
175
- if osinfo.container_info.engine.lower() == "docker":
176
- if semver.compare(osinfo.container_info.version, "25.0.5") < 0:
193
+ ce_path = osinfo.container_info.path
194
+ assert ce_path is not None
195
+
196
+ engine_name = osinfo.container_info.engine.lower()
197
+ if engine_name == "docker":
198
+ if error := container_version_error(osinfo.container_info.version, "25.0.5"):
199
+ warn(error % engine_name)
200
+ elif engine_name == "podman":
201
+ if osinfo.distro.lower() == "ubuntu" and not ubuntu_version_at_least(
202
+ osinfo.distro_version, "24.04"
203
+ ):
177
204
  warn(
178
- f"Your Docker engine version ({osinfo.container_info.version}) is out of date. You may encounter unexpected issues."
205
+ "Versions of Podman for Ubuntu before Ubuntu 24.04 are generally pretty buggy. We recommend using Docker instead if possible."
179
206
  )
207
+ elif error := container_version_error(osinfo.container_info.version, "4.1.0"):
208
+ warn(error % engine_name)
180
209
  else:
181
210
  warn(
182
- f"Unsupported container engine '{osinfo.container_info.engine}'. You may encounter unexpected issues."
211
+ f"Unsupported container engine referenced by '{osinfo.container_info.path}'. You may encounter unexpected issues."
183
212
  )
184
213
 
185
- if not ensure_image(image):
214
+ if not ensure_image(ce_path, image):
186
215
  raise ValueError(f"Failed to use image '{image}'.")
187
216
 
188
217
  terminal_args = ["-i"]
@@ -222,15 +251,6 @@ def run_in_container(
222
251
  mount_args += ["-v", f"{from_cwd}:{to_cwd}"]
223
252
  mount_args += ["-w", to_cwd]
224
253
 
225
- tempdir = tempfile.mkdtemp("librelane_docker")
226
-
227
- mount_args += [
228
- "-v",
229
- f"{tempdir}:/tmp",
230
- "-e",
231
- "TMPDIR=/tmp",
232
- ]
233
-
234
254
  if other_mounts is not None:
235
255
  for mount in other_mounts:
236
256
  if os.path.isdir(mount):
@@ -242,9 +262,13 @@ def run_in_container(
242
262
 
243
263
  container_id = str(uuid.uuid4())
244
264
 
265
+ if os.getenv("_MOUNT_HOST_LIBRELANE") == "1":
266
+ host_librelane_pythonpath = os.path.dirname(__file_dir__)
267
+ mount_args += ["-v", f"{host_librelane_pythonpath}:/host_librelane"]
268
+
245
269
  cmd = (
246
270
  [
247
- CONTAINER_ENGINE,
271
+ ce_path,
248
272
  "run",
249
273
  "--rm",
250
274
  "--name",
@@ -261,4 +285,4 @@ def run_in_container(
261
285
  info("Running containerized command:")
262
286
  print(shlex.join(cmd))
263
287
 
264
- os.execlp(CONTAINER_ENGINE, *cmd)
288
+ os.execlp(ce_path, *cmd)
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()
@@ -0,0 +1,18 @@
1
+ DESIGN_NAME: hold_violation
2
+ CLOCK_PORT: clk
3
+ CLOCK_PERIOD: 5
4
+ VERILOG_FILES: dir::demo.v
5
+ RUN_POST_CTS_RESIZER_TIMING: false
6
+ RUN_POST_GRT_RESIZER_TIMING: false
7
+ FP_SIZING: absolute
8
+ DIE_AREA: [0, 0, 100, 100]
9
+ INSERT_ECO_BUFFERS:
10
+ - target: u_ff1/Q
11
+ buffer: sky130_fd_sc_hd__buf_1
12
+ - target: u_ff1/Q
13
+ buffer: sky130_fd_sc_hd__buf_1
14
+ meta:
15
+ flow: Classic
16
+ substituting_steps:
17
+ "+OpenROAD.DetailedRouting": "Odb.InsertECOBuffers"
18
+ "+Odb.InsertECOBuffers": "OpenROAD.DetailedRouting"
@@ -0,0 +1,27 @@
1
+ module hold_violation(
2
+ input clk,
3
+ input d,
4
+ output q
5
+ );
6
+ wire intermediate;
7
+ wire clk_delayed;
8
+
9
+ sky130_fd_sc_hd__clkbuf_4 dly (
10
+ .A(clk),
11
+ .X(clk_delayed)
12
+ );
13
+
14
+ sky130_fd_sc_hd__dfrtp_4 u_ff1 (
15
+ .CLK(clk),
16
+ .D(d),
17
+ .RESET_B(1'b1),
18
+ .Q(intermediate)
19
+ );
20
+
21
+ sky130_fd_sc_hd__dfrtp_1 u_ff2 (
22
+ .CLK(clk_delayed),
23
+ .D(intermediate),
24
+ .RESET_B(1'b1),
25
+ .Q(q)
26
+ );
27
+ endmodule
librelane/flows/cli.py CHANGED
@@ -35,7 +35,7 @@ from cloup.typing import Decorator
35
35
 
36
36
  from .flow import Flow
37
37
  from ..common import set_tpe, cli, get_opdks_rev, _get_process_limit
38
- from ..logging import set_log_level, verbose, err, options, LogLevels
38
+ from ..logging import set_log_level, verbose, info, err, options, LogLevels
39
39
  from ..state import State, InvalidState
40
40
 
41
41
 
@@ -146,27 +146,30 @@ def cloup_flow_opts(
146
146
  function decorated with @cloup.command (https://cloup.readthedocs.io/en/stable/autoapi/cloup/index.html#cloup.command).
147
147
 
148
148
  The following keyword arguments will be passed to the decorated function.
149
+
149
150
  * Those postfixed ‡ are compatible with the constructor for :class:`Flow`.
150
151
  * Those postfixed § are compatible with the :meth:`Flow.start`.
151
152
 
153
+ ---
154
+
152
155
  * Flow configuration options (if parameter ``config_options`` is ``True``):
153
156
  * ``flow_name``: ``Optional[str]``: A valid flow ID to be used with :meth:`Flow.factory.get`
154
- * ``config_override_strings``‡: ``Optional[Iterable[str]]``
157
+ * ``config_override_strings`` ‡: ``Optional[Iterable[str]]``
155
158
  * Sequential flow controls (if parameter ``sequential_flow_controls`` is ``True``)
156
- * ``frm``§: ``Optional[str]``: Start from a step with this ID. Supported by sequential flows.
157
- * ``to``§: ``Optional[str]``: Stop at a step with this id. Supported by sequential flows.
158
- * ``skip``§: ``Iterable[str]``: Skip these steps. Supported by sequential flows.
159
+ * ``frm`` §: ``Optional[str]``: Start from a step with this ID. Supported by sequential flows.
160
+ * ``to`` §: ``Optional[str]``: Stop at a step with this id. Supported by sequential flows.
161
+ * ``skip`` §: ``Iterable[str]``: Skip these steps. Supported by sequential flows.
159
162
  * Sequential flow reproducible (if parameter ``sequential_flow_reproducible`` is ``True``)
160
- * ``reproducible``§: ``str``: Create a reproducible for a step with is ID, aborting the flow afterwards. Supported by sequential flows.
163
+ * ``reproducible`` §: ``str``: Create a reproducible for a step with is ID, aborting the flow afterwards. Supported by sequential flows.
161
164
  * Flow run options (if parameter ``run_options`` is ``True``):
162
- * ``tag``§: ``Optional[str]``
163
- * ``last_run``§: ``bool``: If ``True``, ``tag`` is guaranteed to be None.
164
- * ``with_initial_state``§: ``Optional[State]``
165
+ * ``tag`` §: ``Optional[str]``
166
+ * ``last_run`` §: ``bool``: If ``True``, ``tag`` is guaranteed to be None.
167
+ * ``with_initial_state`` §: ``Optional[State]``
165
168
  * PDK options
166
- * ``use_volare``: ``bool``
167
- * ``pdk_root``‡: ``Optional[str]``
168
- * ``pdk``‡: ``str``
169
- * ``scl``‡: ``Optional[str]``
169
+ * ``use_volare`` : ``bool``
170
+ * ``pdk_root`` ‡: ``Optional[str]``
171
+ * ``pdk`` ‡: ``str``
172
+ * ``scl`` ‡: ``Optional[str]``
170
173
  * ``config_files``: ``Iterable[str]``: Paths to configuration files (if
171
174
  parameter ``accept_config_files`` is ``True``)
172
175
 
@@ -443,16 +446,29 @@ def cloup_flow_opts(
443
446
  err(f"Could not resolve the PDK '{pdk}'.")
444
447
  exit(1)
445
448
 
446
- version = ciel.fetch(
447
- ciel_home,
448
- pdk_family,
449
- opdks_rev,
450
- data_source=StaticWebDataSource(
451
- "https://fossi-foundation.github.io/ciel-releases"
452
- ),
453
- include_libraries=include_libraries,
454
- )
455
- pdk_root = version.get_dir(ciel_home)
449
+ if pdk_family == "ihp-sg13g2":
450
+ err(
451
+ "The IHP Open PDK is only supported in the development version of LibreLane, specifically 3.0.0.dev28 or higher."
452
+ )
453
+ info(
454
+ "If you're using Nix, switch to the 'dev' branch. If you're using the Python package, run \"python3 -m pip install 'librelane>=3.0.0.dev28'\"."
455
+ )
456
+ exit(1)
457
+
458
+ try:
459
+ version = ciel.fetch(
460
+ ciel_home,
461
+ pdk_family,
462
+ opdks_rev,
463
+ data_source=StaticWebDataSource(
464
+ "https://fossi-foundation.github.io/ciel-releases"
465
+ ),
466
+ include_libraries=include_libraries,
467
+ )
468
+ pdk_root = version.get_dir(ciel_home)
469
+ except ValueError as e:
470
+ err(f"Failed to download PDK: {e}")
471
+ exit(1)
456
472
 
457
473
  return f(*args, pdk_root=pdk_root, pdk=pdk, scl=scl, **kwargs)
458
474