librelane 3.0.0.dev29__py3-none-any.whl → 3.0.0.dev32__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.
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
@@ -275,20 +274,12 @@ def run_included_example(
275
274
  if os.path.isdir(final_path):
276
275
  print(f"A directory named {value} already exists.", file=sys.stderr)
277
276
  ctx.exit(1)
278
- # 1. Copy the files
279
- shutil.copytree(
280
- example_path,
281
- final_path,
282
- symlinks=False,
283
- )
284
-
285
- # 2. Make files writable
286
- if os.name == "posix":
287
- subprocess.check_call(["chmod", "-R", "755", final_path])
288
277
 
278
+ # 1. Copy the files
279
+ common.recreate_tree(example_path, final_path)
289
280
  config_file = glob.glob(os.path.join(final_path, "config.*"))[0]
290
281
 
291
- # 3. Run
282
+ # 2. Run
292
283
  run(
293
284
  ctx,
294
285
  config_files=[config_file],
@@ -318,28 +309,38 @@ def cli_in_container(
318
309
  if not value:
319
310
  return
320
311
 
321
- docker_mounts = list(ctx.params.get("docker_mounts") or ())
322
- docker_tty: bool = ctx.params.get("docker_tty", True)
312
+ mounts = list(ctx.params.get("docker_mounts") or ())
313
+ tty: bool = ctx.params.get("docker_tty", True)
323
314
  pdk_root = ctx.params.get("pdk_root")
324
- argv = sys.argv[sys.argv.index("--dockerized") + 1 :]
315
+
316
+ try:
317
+ containerized_index = sys.argv.index("--dockerized")
318
+ except ValueError:
319
+ containerized_index = sys.argv.index("--containerized")
320
+
321
+ argv = sys.argv[containerized_index + 1 :]
325
322
 
326
323
  final_argv = ["zsh"]
327
324
  if len(argv) != 0:
328
- final_argv = ["librelane"] + argv
325
+ final_argv = ["python3", "-m", "librelane"] + argv
329
326
 
330
- docker_image = os.getenv(
327
+ container_image = os.getenv(
331
328
  "LIBRELANE_IMAGE_OVERRIDE", f"ghcr.io/librelane/librelane:{__version__}"
332
329
  )
333
330
 
334
331
  try:
335
332
  run_in_container(
336
- docker_image,
333
+ container_image,
337
334
  final_argv,
338
335
  pdk_root=pdk_root,
339
- other_mounts=docker_mounts,
340
- tty=docker_tty,
336
+ other_mounts=mounts,
337
+ tty=tty,
341
338
  )
339
+ except ValueError as e:
340
+ err(e)
341
+ ctx.exit(1)
342
342
  except Exception as e:
343
+ traceback.print_exc(file=sys.stderr)
343
344
  err(e)
344
345
  ctx.exit(1)
345
346
 
@@ -374,25 +375,28 @@ o = partial(option, show_default=True)
374
375
  "Containerization options",
375
376
  o(
376
377
  "--docker-mount",
378
+ "--container-mount",
377
379
  "-m",
378
380
  "docker_mounts",
379
381
  multiple=True,
380
- is_eager=True, # docker options should be processed before anything else
382
+ is_eager=True, # container options should be processed before anything else
381
383
  default=(),
382
- 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.",
384
+ 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.",
383
385
  ),
384
386
  o(
385
387
  "--docker-tty/--docker-no-tty",
386
- is_eager=True, # docker options should be processed before anything else
388
+ "--container-tty/--container-no-tty",
389
+ is_eager=True, # container options should be processed before anything else
387
390
  default=True,
388
- 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.",
391
+ 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.",
389
392
  ),
390
393
  o(
391
394
  "--dockerized",
395
+ "--containerized",
392
396
  default=False,
393
397
  is_flag=True,
394
398
  is_eager=True, # docker options should be processed before anything else
395
- help="Run the remaining flags using a Docker container. Some caveats apply. Must precede all options except --docker-mount, --docker-tty/--docker-no-tty.",
399
+ 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.",
396
400
  callback=cli_in_container,
397
401
  ),
398
402
  )
@@ -41,6 +41,7 @@ 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,
46
47
  count_occurences,
librelane/common/misc.py CHANGED
@@ -21,6 +21,7 @@ import re
21
21
  import glob
22
22
  import gzip
23
23
  import yaml
24
+ import shutil
24
25
  import typing
25
26
  import pathlib
26
27
  import fnmatch
@@ -328,6 +329,37 @@ class Filter(object):
328
329
  yield input
329
330
 
330
331
 
332
+ def recreate_tree(
333
+ source: AnyPath,
334
+ target: AnyPath,
335
+ ):
336
+ """
337
+ This function attempts to recreate a file tree from a source path in another
338
+ target path.
339
+
340
+ Permissions are not copied over. Symlinks and hardlinks are followed.
341
+
342
+ Directories are not recreated unless they contain files as (grand)children.
343
+
344
+ If the source and target are the same, the function returns early and does
345
+ nothing.
346
+
347
+ :param source: The source file tree to replicate
348
+ :param target: The target path to recreate the file tree within
349
+ """
350
+ source = os.path.abspath(source)
351
+ target = os.path.abspath(target)
352
+ if os.path.exists(target) and os.path.samefile(source, target):
353
+ return
354
+ for dirname, _, files in os.walk(source):
355
+ for file in files:
356
+ resolved = os.path.join(dirname, file)
357
+ resolved_target = os.path.join(target, os.path.relpath(resolved, source))
358
+ os.makedirs(os.path.dirname(resolved_target), exist_ok=True)
359
+ with open(resolved, "rb") as fi, open(resolved_target, "wb") as fo:
360
+ shutil.copyfileobj(fi, fo)
361
+
362
+
331
363
  def get_latest_file(in_path: Union[str, os.PathLike], filename: str) -> Optional[Path]:
332
364
  """
333
365
  :param in_path: A directory to search in
@@ -394,7 +426,7 @@ def _get_process_limit() -> int:
394
426
 
395
427
  def gzopen(filename: AnyPath, mode="rt") -> IO[Any]:
396
428
  """
397
- This method (tries to?) emulate the gzopen from the Linux Standard Base,
429
+ This function (tries to?) emulate the gzopen from the Linux Standard Base,
398
430
  specifically this part:
399
431
 
400
432
  If path refers to an uncompressed file, and mode refers to a read mode,
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)
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
 
@@ -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()
@@ -2741,7 +2741,7 @@ class OpenGUI(OpenSTAStep):
2741
2741
 
2742
2742
  inputs = [
2743
2743
  DesignFormat.ODB,
2744
- # DesignFormat.SPEF.mkOptional(),
2744
+ DesignFormat.SPEF.mkOptional(),
2745
2745
  ]
2746
2746
  outputs = []
2747
2747
 
@@ -161,7 +161,8 @@ class TclStep(Step):
161
161
 
162
162
  for input in self.inputs:
163
163
  key = f"CURRENT_{input.id.upper()}"
164
- env[key] = TclStep.value_to_tcl(state[input])
164
+ if input_path := state.get_by_df(input):
165
+ env[key] = TclStep.value_to_tcl(input_path)
165
166
 
166
167
  for output in self.outputs:
167
168
  if output.multiple:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: librelane
3
- Version: 3.0.0.dev29
3
+ Version: 3.0.0.dev32
4
4
  Summary: An infrastructure for implementing chip design flows
5
5
  Home-page: https://github.com/librelane/librelane
6
6
  License: Apache-2.0
@@ -1,7 +1,7 @@
1
1
  librelane/__init__.py,sha256=EMpoZrRmS_wsweKjhyAg52OXCK7HWQ8o8CVrYaX4ub0,1220
2
- librelane/__main__.py,sha256=Zq2h2ZTQqA0T7qpGFELJ1bzVQnv-951nnhJ0-9WzNaM,14541
2
+ librelane/__main__.py,sha256=-yJXvww0nKyt-jcOl_QLnZskKZDL_2zScEgjSZ-7ANE,14702
3
3
  librelane/__version__.py,sha256=dbE4stCACDmIoxgKksesAkTa-_hi5dW6nPLWw9Pfq3Q,1486
4
- librelane/common/__init__.py,sha256=FYBKTUKpAwexQSgCaH_IfbozmDk0hA1ITpl66J8Qcwo,1550
4
+ librelane/common/__init__.py,sha256=fK52kxuueEpKI1HNNhUOJFQ4yvxJs0f0Vz1a5S-CBGY,1569
5
5
  librelane/common/cli.py,sha256=xi48GBGHRsYrLGwx40ARwpykHx7GnuHbJjHxjOwtZ5Y,2349
6
6
  librelane/common/drc.py,sha256=l1quZbHXGb7yjKCO5IFn-Xxf_zIx4f6kxqpNm3YmpOs,12809
7
7
  librelane/common/generic_dict.py,sha256=ASa5wtVcLuGlsBqGfLxLYXrYksqQB62iHljV04plIqI,10010
@@ -10,7 +10,7 @@ librelane/common/metrics/__main__.py,sha256=1w23V_1-f0WUSfxG36ewNHctgPisQFFG4p-R
10
10
  librelane/common/metrics/library.py,sha256=CG7rubLdjuCQL9-9bzAC-64hf-KlH-iu_Fg0oKuesqs,7373
11
11
  librelane/common/metrics/metric.py,sha256=h3Xd26z5M80IJgVmmrBTjKcdGLb4I0wyjM-H4jdyi_0,6990
12
12
  librelane/common/metrics/util.py,sha256=Bl_9znlot7-Os2VigYLSmMf56aAkGdv3evWz9vfK7K4,9344
13
- librelane/common/misc.py,sha256=vjSfFT2KV5TNTxqeLt5EFqcprRrjrgNvgOJpBl7JpM4,13627
13
+ librelane/common/misc.py,sha256=_xoU3lnA9Odf1PidjCJ4RDiGZQ3nd3YNGyooHu4RHe4,14757
14
14
  librelane/common/ring_buffer.py,sha256=DGFen9y0JOmiL7E27tmzDTVSJKZtV-waF9hl5Rz9uek,1898
15
15
  librelane/common/tcl.py,sha256=AfTxbSLA0VUXVMMwoAQndyQTcEZQoQfMa4FFizZiEgU,4341
16
16
  librelane/common/toolbox.py,sha256=ijR__rVqQ_nJtfm34H-VdSCIeArKns7lVAc1TcTUSsQ,20975
@@ -24,8 +24,8 @@ librelane/config/pdk_compat.py,sha256=ofqYuD-MgTcfvPVXpGJo8H1GKzCvN6sxHsK_OqCVXa
24
24
  librelane/config/preprocessor.py,sha256=ATi29SHz0_OBq1IqUkGxvhHUDKB5z5jO0KqvoQXg8R8,14913
25
25
  librelane/config/removals.py,sha256=vxqTuRTJ0jt2TX4KmFZCZPTwghDFkCVjIhF2iReHwJA,2958
26
26
  librelane/config/variable.py,sha256=YKRlnQu6YvkwnJ5zYfWTcj0fHP0Jcy22ZTb0i4kb3h4,26823
27
- librelane/container.py,sha256=3KHxs3dUSVUZVYsS6fsA7dD3Q4QEQEzRxgXZZh9dzi0,7554
28
- librelane/env_info.py,sha256=vAE9AZ_vDFLt7Srtg4ZywPzE6vgVhCrIvg8PP25-BJ8,10460
27
+ librelane/container.py,sha256=7w_V2Fpb3dbnZ8FqBce1vK31jH30UrxByppfEJRyG9M,8672
28
+ librelane/env_info.py,sha256=xF9iqwwJv5yZz7n7BTrrT_yP3Dp1HjAOUObNE9k_1g4,11074
29
29
  librelane/examples/spm/config.yaml,sha256=H2ERY4xoIeXN7kM3N9yGWiFBbtByyaN2Ni1kFqYPtO4,612
30
30
  librelane/examples/spm/pin_order.cfg,sha256=-8mTGFKnES0vhQATfaE2TXN_mdCZ3SZIN90Src1l6fY,52
31
31
  librelane/examples/spm/src/impl.sdc,sha256=wP18UoVlOJ9q4lmUoa3XpgcpPdyzEqHBNxCgOOU7QH0,2961
@@ -43,11 +43,12 @@ librelane/flows/__init__.py,sha256=ghtmUG-taVpHJ3CKJRYZGn3dU0r93araT1EIGlBEsxg,8
43
43
  librelane/flows/builtins.py,sha256=tR14Qc1ZUey2w-Ar4DWOvxuP7LGPtMecCJq8WgcYJpk,773
44
44
  librelane/flows/classic.py,sha256=JB9gVgP2hHPhMuCJg7hvoj2BvJcvRec7suEXPgHmz14,10971
45
45
  librelane/flows/cli.py,sha256=_aJjlalCsmUeLiqy1D8VFmBJ0dLelx24erqaK2q-AOs,16706
46
- librelane/flows/flow.py,sha256=qly_ENbw8zHSS6ubUY56JrCRjKnfuSoN78suz1k4chw,36997
46
+ librelane/flows/flow.py,sha256=1zRhYQvnRte-VNcsVmAkikD_kZJVbgsqgLR-8CGYaLI,37034
47
47
  librelane/flows/misc.py,sha256=32Om3isexesfKKiJZCajNmINc-xdv7eVx_tgoh9SR6U,2015
48
48
  librelane/flows/optimizing.py,sha256=OwZz6WGmXpliwO8vtmhjKHD-kzDyNv-zoCECZIigXsI,6076
49
49
  librelane/flows/sequential.py,sha256=kBpR9kxfEfdTaNy9Ter2KNQXkW6qojCwoBsFJBwTq6I,15359
50
50
  librelane/flows/synth_explore.py,sha256=8mpeuG6oxeEXVQi4NwS4I415eCu7Ak6DN4oK30h1eCQ,7418
51
+ librelane/help/__main__.py,sha256=gnm0yi-Ih8YoyY2cMiHONV2ZzR-tvHfdEHCb28YQJZ0,1243
51
52
  librelane/logging/__init__.py,sha256=mrTnzjpH6AOu2CiDZYfOMCVByAS2Xeg9HS4FJyXsJOE,1043
52
53
  librelane/logging/logger.py,sha256=kA61TGsR00Fi6kQSxgTC1pHpS_-zqC1PdQnYqnk2TWY,8632
53
54
  librelane/pdk_hashes.yaml,sha256=wHPz6Ze4e1uhZTo7IKS4wcmU6ShcZdBtHcdACP5tYNI,153
@@ -159,14 +160,14 @@ librelane/steps/magic.py,sha256=m4cZH2VomJs0RudtV8avSaZVqRj1NP7Pm2P6qo2z2X0,2091
159
160
  librelane/steps/misc.py,sha256=8ubCvFeFEspXrgnzNWINY5-TXTyalNtlvcX8TSw0qdg,5685
160
161
  librelane/steps/netgen.py,sha256=R9sDWv-9wKMdi2rkuLQdOc4uLlbYhXcKKd6WsZsnLt0,8953
161
162
  librelane/steps/odb.py,sha256=-zsXi0jVdtfBfAJI0OC4x1jI_B2OX5YVn4uAn6NyFdk,38424
162
- librelane/steps/openroad.py,sha256=lX0L1S9QyCICLYusiu4IJdNatZzephPE89kdY7iqIEA,99588
163
+ librelane/steps/openroad.py,sha256=hgpqsVQi7tFRlj75zefQkD3Vdmq21ubZAFWyU12WxUg,99586
163
164
  librelane/steps/openroad_alerts.py,sha256=IJyB4piBDCKXhkJswHGMYCRDwbdQsR0GZlrGGDhmW6Q,3364
164
165
  librelane/steps/pyosys.py,sha256=LY7qqxkhjfoyBBR7vdkm7ylabbxMJDwIoYm7mAUbLVY,23348
165
166
  librelane/steps/step.py,sha256=THIxZkhtkNYt1iRgMduD0ywrOTCaV7cCfUB2EqXN6-k,55751
166
- librelane/steps/tclstep.py,sha256=8-zpYOo562E86nm7f4DiTqUsLKY0AFtEJgrp9CnWWDw,10083
167
+ librelane/steps/tclstep.py,sha256=68AjCmbLhBscbzQDxRcPQVU-6UvZQNOalO7qNwUXCa4,10138
167
168
  librelane/steps/verilator.py,sha256=MWx2TpLqYyea9_jSeLG9c2S5ujvYERQZRFNaMhfHxZE,7916
168
169
  librelane/steps/yosys.py,sha256=lYdZPFvjcmdu_NE6rtB94_dysIK2qwGdGb480W6pg2w,12711
169
- librelane-3.0.0.dev29.dist-info/METADATA,sha256=w5cSM_7t_ajJ2Msbhc8ibMDJdAUshU7fyz2y5PDuQ4Y,6561
170
- librelane-3.0.0.dev29.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
171
- librelane-3.0.0.dev29.dist-info/entry_points.txt,sha256=GTBvXykNMMFsNKiJFgtEw7P1wb_VZIqVM35EFSpyZQE,263
172
- librelane-3.0.0.dev29.dist-info/RECORD,,
170
+ librelane-3.0.0.dev32.dist-info/METADATA,sha256=djo0K7TzkitmcNUdzjBIlZyhud9u389_OeTWGcmdyjk,6561
171
+ librelane-3.0.0.dev32.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
172
+ librelane-3.0.0.dev32.dist-info/entry_points.txt,sha256=0eZs2NOH-w-W_GVRCs-ualst26XplkPpJkOnGWMaFw0,306
173
+ librelane-3.0.0.dev32.dist-info/RECORD,,
@@ -2,6 +2,7 @@
2
2
  librelane=librelane.__main__:cli
3
3
  librelane.config=librelane.config.__main__:cli
4
4
  librelane.env_info=librelane:env_info_cli
5
+ librelane.help=librelane.help.__main__:cli
5
6
  librelane.state=librelane.state.__main__:cli
6
7
  librelane.steps=librelane.steps.__main__:cli
7
8
  openlane=librelane.__main__:cli