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 +29 -25
- librelane/common/__init__.py +2 -0
- librelane/common/drc.py +1 -0
- librelane/common/misc.py +56 -2
- librelane/config/__main__.py +1 -4
- librelane/container.py +48 -27
- librelane/env_info.py +129 -115
- librelane/flows/flow.py +66 -30
- librelane/help/__main__.py +39 -0
- librelane/scripts/magic/def/mag_gds.tcl +0 -2
- librelane/scripts/magic/drc.tcl +0 -1
- librelane/scripts/magic/gds/extras_mag.tcl +0 -2
- librelane/scripts/magic/gds/mag_with_pointers.tcl +0 -1
- librelane/scripts/magic/lef/extras_maglef.tcl +0 -2
- librelane/scripts/magic/lef/maglef.tcl +0 -1
- librelane/scripts/magic/wrapper.tcl +2 -0
- librelane/scripts/odbpy/power_utils.py +8 -6
- librelane/scripts/odbpy/reader.py +2 -2
- librelane/state/state.py +11 -3
- librelane/steps/__main__.py +1 -2
- librelane/steps/magic.py +24 -14
- librelane/steps/odb.py +1 -4
- librelane/steps/openroad.py +2 -5
- {librelane-2.4.0.dev11.dist-info → librelane-2.4.1.dist-info}/METADATA +2 -2
- {librelane-2.4.0.dev11.dist-info → librelane-2.4.1.dist-info}/RECORD +27 -26
- {librelane-2.4.0.dev11.dist-info → librelane-2.4.1.dist-info}/entry_points.txt +1 -0
- {librelane-2.4.0.dev11.dist-info → librelane-2.4.1.dist-info}/WHEEL +0 -0
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
|
-
#
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
+
container_image,
|
|
340
337
|
final_argv,
|
|
341
338
|
pdk_root=pdk_root,
|
|
342
|
-
other_mounts=
|
|
343
|
-
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, #
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
)
|
librelane/common/__init__.py
CHANGED
librelane/common/drc.py
CHANGED
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
|
|
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)
|
librelane/config/__main__.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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([
|
|
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([
|
|
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
|
|
187
|
+
if not isinstance(osinfo.container_info, ContainerInfo):
|
|
173
188
|
raise FileNotFoundError("No compatible container engine found.")
|
|
174
189
|
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
285
|
+
os.execlp(ce_path, *cmd)
|