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/__main__.py CHANGED
@@ -22,7 +22,6 @@ import shutil
22
22
  import marshal
23
23
  import tempfile
24
24
  import traceback
25
- import subprocess
26
25
  from textwrap import dedent
27
26
  from functools import partial
28
27
  from typing import Any, Dict, Sequence, Tuple, Type, Optional, List
@@ -278,20 +277,12 @@ def run_included_example(
278
277
  if os.path.isdir(final_path):
279
278
  print(f"A directory named {value} already exists.", file=sys.stderr)
280
279
  ctx.exit(1)
281
- # 1. Copy the files
282
- shutil.copytree(
283
- example_path,
284
- final_path,
285
- symlinks=False,
286
- )
287
-
288
- # 2. Make files writable
289
- if os.name == "posix":
290
- subprocess.check_call(["chmod", "-R", "755", final_path])
291
280
 
281
+ # 1. Copy the files
282
+ common.recreate_tree(example_path, final_path)
292
283
  config_file = glob.glob(os.path.join(final_path, "config.*"))[0]
293
284
 
294
- # 3. Run
285
+ # 2. Run
295
286
  run(
296
287
  ctx,
297
288
  config_files=[config_file],
@@ -321,28 +312,38 @@ def cli_in_container(
321
312
  if not value:
322
313
  return
323
314
 
324
- docker_mounts = list(ctx.params.get("docker_mounts") or ())
325
- docker_tty: bool = ctx.params.get("docker_tty", True)
315
+ mounts = list(ctx.params.get("docker_mounts") or ())
316
+ tty: bool = ctx.params.get("docker_tty", True)
326
317
  pdk_root = ctx.params.get("pdk_root")
327
- argv = sys.argv[sys.argv.index("--dockerized") + 1 :]
318
+
319
+ try:
320
+ containerized_index = sys.argv.index("--dockerized")
321
+ except ValueError:
322
+ containerized_index = sys.argv.index("--containerized")
323
+
324
+ argv = sys.argv[containerized_index + 1 :]
328
325
 
329
326
  final_argv = ["zsh"]
330
327
  if len(argv) != 0:
331
- final_argv = ["librelane"] + argv
328
+ final_argv = ["python3", "-m", "librelane"] + argv
332
329
 
333
- docker_image = os.getenv(
330
+ container_image = os.getenv(
334
331
  "LIBRELANE_IMAGE_OVERRIDE", f"ghcr.io/librelane/librelane:{__version__}"
335
332
  )
336
333
 
337
334
  try:
338
335
  run_in_container(
339
- docker_image,
336
+ container_image,
340
337
  final_argv,
341
338
  pdk_root=pdk_root,
342
- other_mounts=docker_mounts,
343
- tty=docker_tty,
339
+ other_mounts=mounts,
340
+ tty=tty,
344
341
  )
342
+ except ValueError as e:
343
+ err(e)
344
+ ctx.exit(1)
345
345
  except Exception as e:
346
+ traceback.print_exc(file=sys.stderr)
346
347
  err(e)
347
348
  ctx.exit(1)
348
349
 
@@ -377,25 +378,28 @@ o = partial(option, show_default=True)
377
378
  "Containerization options",
378
379
  o(
379
380
  "--docker-mount",
381
+ "--container-mount",
380
382
  "-m",
381
383
  "docker_mounts",
382
384
  multiple=True,
383
- is_eager=True, # docker options should be processed before anything else
385
+ is_eager=True, # container options should be processed before anything else
384
386
  default=(),
385
- help="Used to mount more directories in dockerized mode. If a valid directory is specified, it will be mounted in the same path in the container. Otherwise, the value of the option will be passed to the Docker-compatible container engine verbatim. Must be passed before --dockerized, has no effect if --dockerized is not set.",
387
+ help="Used to mount more directories in dockerized mode. If a valid directory is specified, it will be mounted in the same path in the container. Otherwise, the value of the option will be passed to the container engine verbatim. Must be passed before --containerized/--dockerized, has no effect if not set.",
386
388
  ),
387
389
  o(
388
390
  "--docker-tty/--docker-no-tty",
389
- is_eager=True, # docker options should be processed before anything else
391
+ "--container-tty/--container-no-tty",
392
+ is_eager=True, # container options should be processed before anything else
390
393
  default=True,
391
- help="Controls the allocation of a virtual terminal by passing -t to the Docker-compatible container engine invocation. Must be passed before --dockerized, has no effect if --dockerized is not set.",
394
+ help="Controls the allocation of a virtual terminal by passing -t to the Docker-compatible container engine invocation. Must be passed before --containerized/--dockerized, has no effect if not set.",
392
395
  ),
393
396
  o(
394
397
  "--dockerized",
398
+ "--containerized",
395
399
  default=False,
396
400
  is_flag=True,
397
401
  is_eager=True, # docker options should be processed before anything else
398
- help="Run the remaining flags using a Docker container. Some caveats apply. Must precede all options except --docker-mount, --docker-tty/--docker-no-tty.",
402
+ help="Run the remaining flags using a containerized version of LibreLane. Some caveats apply. Must precede all options except --{docker,container}-mount, --{docker,container}-[no-]tty.",
399
403
  callback=cli_in_container,
400
404
  ),
401
405
  )
@@ -41,8 +41,10 @@ from .misc import (
41
41
  format_size,
42
42
  format_elapsed_time,
43
43
  Filter,
44
+ recreate_tree,
44
45
  get_latest_file,
45
46
  process_list_file,
47
+ count_occurences,
46
48
  _get_process_limit,
47
49
  )
48
50
  from .types import (
librelane/common/drc.py CHANGED
@@ -208,6 +208,7 @@ class DRC:
208
208
  from lxml import etree as ET
209
209
 
210
210
  with ET.xmlfile(out, encoding="utf8", buffered=False) as xf:
211
+ xf.write_declaration()
211
212
  with xf.element("report-database"):
212
213
  # 1. Cells
213
214
  with xf.element("cells"):
librelane/common/misc.py CHANGED
@@ -11,16 +11,19 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ import io
14
15
  import os
15
16
  import re
16
17
  import glob
17
18
  import gzip
19
+ import shutil
18
20
  import typing
19
21
  import fnmatch
20
22
  import pathlib
21
23
  import unicodedata
22
24
  from math import inf
23
25
  from typing import (
26
+ IO,
24
27
  Any,
25
28
  Generator,
26
29
  Iterable,
@@ -314,6 +317,37 @@ class Filter(object):
314
317
  yield input
315
318
 
316
319
 
320
+ def recreate_tree(
321
+ source: AnyPath,
322
+ target: AnyPath,
323
+ ):
324
+ """
325
+ This function attempts to recreate a file tree from a source path in another
326
+ target path.
327
+
328
+ Permissions are not copied over. Symlinks and hardlinks are followed.
329
+
330
+ Directories are not recreated unless they contain files as (grand)children.
331
+
332
+ If the source and target are the same, the function returns early and does
333
+ nothing.
334
+
335
+ :param source: The source file tree to replicate
336
+ :param target: The target path to recreate the file tree within
337
+ """
338
+ source = os.path.abspath(source)
339
+ target = os.path.abspath(target)
340
+ if os.path.exists(target) and os.path.samefile(source, target):
341
+ return
342
+ for dirname, _, files in os.walk(source):
343
+ for file in files:
344
+ resolved = os.path.join(dirname, file)
345
+ resolved_target = os.path.join(target, os.path.relpath(resolved, source))
346
+ os.makedirs(os.path.dirname(resolved_target), exist_ok=True)
347
+ with open(resolved, "rb") as fi, open(resolved_target, "wb") as fo:
348
+ shutil.copyfileobj(fi, fo)
349
+
350
+
317
351
  def get_latest_file(in_path: Union[str, os.PathLike], filename: str) -> Optional[Path]:
318
352
  """
319
353
  :param in_path: A directory to search in
@@ -378,9 +412,9 @@ def _get_process_limit() -> int:
378
412
  return int(os.getenv("_OPENLANE_MAX_CORES", os.cpu_count() or 1))
379
413
 
380
414
 
381
- def gzopen(filename, mode="rt"):
415
+ def gzopen(filename: AnyPath, mode="rt") -> IO[Any]:
382
416
  """
383
- This method (tries to?) emulate the gzopen from the Linux Standard Base,
417
+ This function (tries to?) emulate the gzopen from the Linux Standard Base,
384
418
  specifically this part:
385
419
 
386
420
  If path refers to an uncompressed file, and mode refers to a read mode,
@@ -388,6 +422,11 @@ def gzopen(filename, mode="rt"):
388
422
  for reading directly from the file without any decompression.
389
423
 
390
424
  gzip.open does not have this behavior.
425
+
426
+ :param filename: The full path to the uncompressed or gzipped file.
427
+ :param mode: "r", "rb", "w", "wb", "x", "xb", "a" or "ab" for
428
+ binary mode, or "rt", "wt", "xt" or "at" for text mode.
429
+ :returns: An I/O wrapper that may very slightly based on the mode.
391
430
  """
392
431
  try:
393
432
  g = gzip.open(filename, mode=mode)
@@ -400,3 +439,18 @@ def gzopen(filename, mode="rt"):
400
439
  except gzip.BadGzipFile:
401
440
  g.close()
402
441
  return open(filename, mode=mode)
442
+
443
+
444
+ def count_occurences(fp: io.TextIOWrapper, pattern: str = "") -> int:
445
+ """
446
+ Counts the occurences of a certain string in a stream, line-by-line, without
447
+ necessarily loading the entire file into memory.
448
+
449
+ Equivalent to: ``grep -c 'pattern' <file>`` (but without regex support).
450
+
451
+ :param fp: the text stream
452
+ :param pattern: the substring to search for. if set to "", it will simply
453
+ count the lines in the file.
454
+ :returns: the number of matching lines
455
+ """
456
+ return sum(pattern in line for line in fp)
@@ -14,7 +14,6 @@
14
14
  import os
15
15
  import sys
16
16
  import json
17
- import functools
18
17
  from decimal import Decimal
19
18
 
20
19
  import click
@@ -111,9 +110,7 @@ def create_config(
111
110
  print("At least one source RTL file is required.", file=sys.stderr)
112
111
  exit(1)
113
112
  source_rtl_key = "VERILOG_FILES"
114
- if not functools.reduce(
115
- lambda acc, x: acc and (x.endswith(".sv") or x.endswith(".v")), source_rtl, True
116
- ):
113
+ if not all(file.endswith(".sv") or file.endswith(".v") for file in source_rtl):
117
114
  print(
118
115
  "Only Verilog/SystemVerilog files are supported by create-config.",
119
116
  file=sys.stderr,
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
  ):
@@ -70,9 +69,9 @@ def gui_args(osinfo: OSInfo) -> List[str]:
70
69
  return args
71
70
 
72
71
 
73
- def image_exists(image: str) -> bool:
72
+ def image_exists(ce_path: str, image: str) -> bool:
74
73
  images = (
75
- subprocess.check_output([CONTAINER_ENGINE, "images", image])
74
+ subprocess.check_output([ce_path, "images", image])
76
75
  .decode("utf8")
77
76
  .rstrip()
78
77
  .split("\n")[1:]
@@ -116,14 +115,14 @@ def remote_manifest_exists(image: str) -> bool:
116
115
  return True
117
116
 
118
117
 
119
- def ensure_image(image: str) -> bool:
120
- if image_exists(image):
118
+ def ensure_image(ce_path: str, image: str) -> bool:
119
+ if image_exists(ce_path, image):
121
120
  return True
122
121
 
123
122
  try:
124
- subprocess.check_call([CONTAINER_ENGINE, "pull", image])
123
+ subprocess.check_call([ce_path, "pull", image])
125
124
  except subprocess.CalledProcessError:
126
- err(f"Failed to pull image {image} from the container registries.")
125
+ err(f"Failed to pull image '{image}' from the container registries.")
127
126
  return False
128
127
 
129
128
  return True
@@ -150,6 +149,22 @@ def sanitize_path(path: Union[str, os.PathLike]) -> Tuple[str, str]:
150
149
  return (abspath, mountable_path)
151
150
 
152
151
 
152
+ def container_version_error(input: str, against: str) -> Optional[str]:
153
+ if input == "UNKNOWN":
154
+ return (
155
+ "Could not determine version for %s. You may encounter unexpected issues."
156
+ )
157
+ if semver.compare(input, against) < 0:
158
+ return f"Your %s version ({input}) is out of date. You may encounter unexpected issues."
159
+ return None
160
+
161
+
162
+ def ubuntu_version_at_least(current: str, minimum: str) -> bool:
163
+ if current == "UNKNOWN":
164
+ return False
165
+ return tuple(map(int, current.split("."))) >= tuple(map(int, minimum.split(".")))
166
+
167
+
153
168
  def run_in_container(
154
169
  image: str,
155
170
  args: Sequence[str],
@@ -169,20 +184,31 @@ def run_in_container(
169
184
  f"Unsupported host operating system '{osinfo.kernel}'. You may encounter unexpected issues."
170
185
  )
171
186
 
172
- if osinfo.container_info is None:
187
+ if not isinstance(osinfo.container_info, ContainerInfo):
173
188
  raise FileNotFoundError("No compatible container engine found.")
174
189
 
175
- if osinfo.container_info.engine.lower() == "docker":
176
- if semver.compare(osinfo.container_info.version, "25.0.5") < 0:
190
+ ce_path = osinfo.container_info.path
191
+ assert ce_path is not None
192
+
193
+ engine_name = osinfo.container_info.engine.lower()
194
+ if engine_name == "docker":
195
+ if error := container_version_error(osinfo.container_info.version, "25.0.5"):
196
+ warn(error % engine_name)
197
+ elif engine_name == "podman":
198
+ if osinfo.distro.lower() == "ubuntu" and not ubuntu_version_at_least(
199
+ osinfo.distro_version, "24.04"
200
+ ):
177
201
  warn(
178
- f"Your Docker engine version ({osinfo.container_info.version}) is out of date. You may encounter unexpected issues."
202
+ "Versions of Podman for Ubuntu before Ubuntu 24.04 are generally pretty buggy. We recommend using Docker instead if possible."
179
203
  )
204
+ elif error := container_version_error(osinfo.container_info.version, "4.1.0"):
205
+ warn(error % engine_name)
180
206
  else:
181
207
  warn(
182
- f"Unsupported container engine '{osinfo.container_info.engine}'. You may encounter unexpected issues."
208
+ f"Unsupported container engine referenced by '{osinfo.container_info.path}'. You may encounter unexpected issues."
183
209
  )
184
210
 
185
- if not ensure_image(image):
211
+ if not ensure_image(ce_path, image):
186
212
  raise ValueError(f"Failed to use image '{image}'.")
187
213
 
188
214
  terminal_args = ["-i"]
@@ -222,15 +248,6 @@ def run_in_container(
222
248
  mount_args += ["-v", f"{from_cwd}:{to_cwd}"]
223
249
  mount_args += ["-w", to_cwd]
224
250
 
225
- tempdir = tempfile.mkdtemp("librelane_docker")
226
-
227
- mount_args += [
228
- "-v",
229
- f"{tempdir}:/tmp",
230
- "-e",
231
- "TMPDIR=/tmp",
232
- ]
233
-
234
251
  if other_mounts is not None:
235
252
  for mount in other_mounts:
236
253
  if os.path.isdir(mount):
@@ -242,9 +259,13 @@ def run_in_container(
242
259
 
243
260
  container_id = str(uuid.uuid4())
244
261
 
262
+ if os.getenv("_MOUNT_HOST_LIBRELANE") == "1":
263
+ host_librelane_pythonpath = os.path.dirname(__file_dir__)
264
+ mount_args += ["-v", f"{host_librelane_pythonpath}:/host_librelane"]
265
+
245
266
  cmd = (
246
267
  [
247
- CONTAINER_ENGINE,
268
+ ce_path,
248
269
  "run",
249
270
  "--rm",
250
271
  "--name",
@@ -261,4 +282,4 @@ def run_in_container(
261
282
  info("Running containerized command:")
262
283
  print(shlex.join(cmd))
263
284
 
264
- os.execlp(CONTAINER_ENGINE, *cmd)
285
+ os.execlp(ce_path, *cmd)