librelane 2.4.0__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.

Files changed (170) hide show
  1. librelane/__init__.py +38 -0
  2. librelane/__main__.py +479 -0
  3. librelane/__version__.py +43 -0
  4. librelane/common/__init__.py +63 -0
  5. librelane/common/cli.py +75 -0
  6. librelane/common/drc.py +246 -0
  7. librelane/common/generic_dict.py +319 -0
  8. librelane/common/metrics/__init__.py +35 -0
  9. librelane/common/metrics/__main__.py +413 -0
  10. librelane/common/metrics/library.py +354 -0
  11. librelane/common/metrics/metric.py +186 -0
  12. librelane/common/metrics/util.py +279 -0
  13. librelane/common/misc.py +456 -0
  14. librelane/common/ring_buffer.py +63 -0
  15. librelane/common/tcl.py +80 -0
  16. librelane/common/toolbox.py +549 -0
  17. librelane/common/tpe.py +41 -0
  18. librelane/common/types.py +116 -0
  19. librelane/config/__init__.py +32 -0
  20. librelane/config/__main__.py +155 -0
  21. librelane/config/config.py +1025 -0
  22. librelane/config/flow.py +490 -0
  23. librelane/config/pdk_compat.py +255 -0
  24. librelane/config/preprocessor.py +464 -0
  25. librelane/config/removals.py +45 -0
  26. librelane/config/variable.py +743 -0
  27. librelane/container.py +285 -0
  28. librelane/env_info.py +320 -0
  29. librelane/examples/spm/config.yaml +33 -0
  30. librelane/examples/spm/pin_order.cfg +14 -0
  31. librelane/examples/spm/src/impl.sdc +73 -0
  32. librelane/examples/spm/src/signoff.sdc +68 -0
  33. librelane/examples/spm/src/spm.v +73 -0
  34. librelane/examples/spm/verify/spm_tb.v +106 -0
  35. librelane/examples/spm-user_project_wrapper/SPM_example.v +286 -0
  36. librelane/examples/spm-user_project_wrapper/base_sdc_file.sdc +145 -0
  37. librelane/examples/spm-user_project_wrapper/config-tut.json +12 -0
  38. librelane/examples/spm-user_project_wrapper/config.json +13 -0
  39. librelane/examples/spm-user_project_wrapper/defines.v +66 -0
  40. librelane/examples/spm-user_project_wrapper/template.def +7656 -0
  41. librelane/examples/spm-user_project_wrapper/user_project_wrapper.v +123 -0
  42. librelane/flows/__init__.py +24 -0
  43. librelane/flows/builtins.py +18 -0
  44. librelane/flows/classic.py +327 -0
  45. librelane/flows/cli.py +463 -0
  46. librelane/flows/flow.py +1049 -0
  47. librelane/flows/misc.py +71 -0
  48. librelane/flows/optimizing.py +179 -0
  49. librelane/flows/sequential.py +367 -0
  50. librelane/flows/synth_explore.py +173 -0
  51. librelane/help/__main__.py +39 -0
  52. librelane/logging/__init__.py +40 -0
  53. librelane/logging/logger.py +323 -0
  54. librelane/open_pdks_rev +1 -0
  55. librelane/plugins.py +21 -0
  56. librelane/py.typed +0 -0
  57. librelane/scripts/base.sdc +80 -0
  58. librelane/scripts/klayout/Readme.md +2 -0
  59. librelane/scripts/klayout/open_design.py +63 -0
  60. librelane/scripts/klayout/render.py +121 -0
  61. librelane/scripts/klayout/stream_out.py +176 -0
  62. librelane/scripts/klayout/xml_drc_report_to_json.py +45 -0
  63. librelane/scripts/klayout/xor.drc +120 -0
  64. librelane/scripts/magic/Readme.md +1 -0
  65. librelane/scripts/magic/common/read.tcl +114 -0
  66. librelane/scripts/magic/def/antenna_check.tcl +35 -0
  67. librelane/scripts/magic/def/mag.tcl +19 -0
  68. librelane/scripts/magic/def/mag_gds.tcl +79 -0
  69. librelane/scripts/magic/drc.tcl +78 -0
  70. librelane/scripts/magic/extract_spice.tcl +98 -0
  71. librelane/scripts/magic/gds/drc_batch.tcl +74 -0
  72. librelane/scripts/magic/gds/erase_box.tcl +32 -0
  73. librelane/scripts/magic/gds/extras_mag.tcl +45 -0
  74. librelane/scripts/magic/gds/mag_with_pointers.tcl +31 -0
  75. librelane/scripts/magic/get_bbox.tcl +11 -0
  76. librelane/scripts/magic/lef/extras_maglef.tcl +61 -0
  77. librelane/scripts/magic/lef/maglef.tcl +26 -0
  78. librelane/scripts/magic/lef.tcl +57 -0
  79. librelane/scripts/magic/open.tcl +28 -0
  80. librelane/scripts/magic/wrapper.tcl +21 -0
  81. librelane/scripts/netgen/setup.tcl +28 -0
  82. librelane/scripts/odbpy/apply_def_template.py +49 -0
  83. librelane/scripts/odbpy/cell_frequency.py +107 -0
  84. librelane/scripts/odbpy/check_antenna_properties.py +116 -0
  85. librelane/scripts/odbpy/contextualize.py +109 -0
  86. librelane/scripts/odbpy/defutil.py +573 -0
  87. librelane/scripts/odbpy/diodes.py +373 -0
  88. librelane/scripts/odbpy/disconnected_pins.py +305 -0
  89. librelane/scripts/odbpy/eco_buffer.py +181 -0
  90. librelane/scripts/odbpy/eco_diode.py +139 -0
  91. librelane/scripts/odbpy/filter_unannotated.py +100 -0
  92. librelane/scripts/odbpy/io_place.py +482 -0
  93. librelane/scripts/odbpy/ioplace_parser/__init__.py +23 -0
  94. librelane/scripts/odbpy/ioplace_parser/parse.py +147 -0
  95. librelane/scripts/odbpy/label_macro_pins.py +277 -0
  96. librelane/scripts/odbpy/lefutil.py +97 -0
  97. librelane/scripts/odbpy/placers.py +162 -0
  98. librelane/scripts/odbpy/power_utils.py +397 -0
  99. librelane/scripts/odbpy/random_place.py +57 -0
  100. librelane/scripts/odbpy/reader.py +250 -0
  101. librelane/scripts/odbpy/remove_buffers.py +173 -0
  102. librelane/scripts/odbpy/snap_to_grid.py +57 -0
  103. librelane/scripts/odbpy/wire_lengths.py +93 -0
  104. librelane/scripts/openroad/antenna_check.tcl +20 -0
  105. librelane/scripts/openroad/antenna_repair.tcl +31 -0
  106. librelane/scripts/openroad/basic_mp.tcl +24 -0
  107. librelane/scripts/openroad/buffer_list.tcl +10 -0
  108. librelane/scripts/openroad/common/dpl.tcl +24 -0
  109. librelane/scripts/openroad/common/dpl_cell_pad.tcl +26 -0
  110. librelane/scripts/openroad/common/grt.tcl +32 -0
  111. librelane/scripts/openroad/common/io.tcl +540 -0
  112. librelane/scripts/openroad/common/pdn_cfg.tcl +135 -0
  113. librelane/scripts/openroad/common/resizer.tcl +103 -0
  114. librelane/scripts/openroad/common/set_global_connections.tcl +78 -0
  115. librelane/scripts/openroad/common/set_layer_adjustments.tcl +31 -0
  116. librelane/scripts/openroad/common/set_power_nets.tcl +30 -0
  117. librelane/scripts/openroad/common/set_rc.tcl +75 -0
  118. librelane/scripts/openroad/common/set_routing_layers.tcl +30 -0
  119. librelane/scripts/openroad/cts.tcl +80 -0
  120. librelane/scripts/openroad/cut_rows.tcl +24 -0
  121. librelane/scripts/openroad/dpl.tcl +24 -0
  122. librelane/scripts/openroad/drt.tcl +37 -0
  123. librelane/scripts/openroad/fill.tcl +30 -0
  124. librelane/scripts/openroad/floorplan.tcl +145 -0
  125. librelane/scripts/openroad/gpl.tcl +88 -0
  126. librelane/scripts/openroad/grt.tcl +30 -0
  127. librelane/scripts/openroad/gui.tcl +37 -0
  128. librelane/scripts/openroad/insert_buffer.tcl +127 -0
  129. librelane/scripts/openroad/ioplacer.tcl +67 -0
  130. librelane/scripts/openroad/irdrop.tcl +51 -0
  131. librelane/scripts/openroad/pdn.tcl +52 -0
  132. librelane/scripts/openroad/rcx.tcl +32 -0
  133. librelane/scripts/openroad/repair_design.tcl +70 -0
  134. librelane/scripts/openroad/repair_design_postgrt.tcl +48 -0
  135. librelane/scripts/openroad/rsz_timing_postcts.tcl +68 -0
  136. librelane/scripts/openroad/rsz_timing_postgrt.tcl +70 -0
  137. librelane/scripts/openroad/sta/check_macro_instances.tcl +53 -0
  138. librelane/scripts/openroad/sta/corner.tcl +393 -0
  139. librelane/scripts/openroad/tapcell.tcl +25 -0
  140. librelane/scripts/openroad/write_views.tcl +27 -0
  141. librelane/scripts/pyosys/construct_abc_script.py +177 -0
  142. librelane/scripts/pyosys/json_header.py +84 -0
  143. librelane/scripts/pyosys/synthesize.py +493 -0
  144. librelane/scripts/pyosys/ys_common.py +153 -0
  145. librelane/scripts/tclsh/hello.tcl +1 -0
  146. librelane/state/__init__.py +24 -0
  147. librelane/state/__main__.py +61 -0
  148. librelane/state/design_format.py +195 -0
  149. librelane/state/state.py +359 -0
  150. librelane/steps/__init__.py +61 -0
  151. librelane/steps/__main__.py +510 -0
  152. librelane/steps/checker.py +637 -0
  153. librelane/steps/common_variables.py +340 -0
  154. librelane/steps/cvc_rv.py +169 -0
  155. librelane/steps/klayout.py +509 -0
  156. librelane/steps/magic.py +576 -0
  157. librelane/steps/misc.py +160 -0
  158. librelane/steps/netgen.py +253 -0
  159. librelane/steps/odb.py +1088 -0
  160. librelane/steps/openroad.py +2460 -0
  161. librelane/steps/openroad_alerts.py +102 -0
  162. librelane/steps/pyosys.py +640 -0
  163. librelane/steps/step.py +1571 -0
  164. librelane/steps/tclstep.py +288 -0
  165. librelane/steps/verilator.py +222 -0
  166. librelane/steps/yosys.py +371 -0
  167. librelane-2.4.0.dist-info/METADATA +169 -0
  168. librelane-2.4.0.dist-info/RECORD +170 -0
  169. librelane-2.4.0.dist-info/WHEEL +4 -0
  170. librelane-2.4.0.dist-info/entry_points.txt +9 -0
@@ -0,0 +1,1571 @@
1
+ # Copyright 2023 Efabless Corporation
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 __future__ import annotations
15
+
16
+ import os
17
+ import sys
18
+ import json
19
+ import time
20
+ import psutil
21
+ import shutil
22
+ import textwrap
23
+ import datetime
24
+ import subprocess
25
+ from signal import Signals
26
+ from decimal import Decimal
27
+ from io import TextIOWrapper
28
+ from threading import Thread
29
+ from inspect import isabstract
30
+ from itertools import zip_longest
31
+ from abc import abstractmethod, ABC
32
+ from concurrent.futures import Future
33
+ from typing import (
34
+ Any,
35
+ List,
36
+ Callable,
37
+ Optional,
38
+ Set,
39
+ Union,
40
+ Tuple,
41
+ Sequence,
42
+ Dict,
43
+ ClassVar,
44
+ Type,
45
+ Generic,
46
+ TypeVar,
47
+ )
48
+
49
+ from rich.markup import escape
50
+
51
+ from ..config import (
52
+ Config,
53
+ Variable,
54
+ universal_flow_config_variables,
55
+ )
56
+ from ..state import (
57
+ DesignFormat,
58
+ DesignFormatObject,
59
+ State,
60
+ InvalidState,
61
+ StateElement,
62
+ )
63
+ from ..common import (
64
+ GenericDict,
65
+ GenericImmutableDict,
66
+ GenericDictEncoder,
67
+ Toolbox,
68
+ Path,
69
+ RingBuffer,
70
+ mkdirp,
71
+ slugify,
72
+ final,
73
+ protected,
74
+ copy_recursive,
75
+ format_size,
76
+ format_elapsed_time,
77
+ )
78
+ from .. import logging
79
+ from ..logging import (
80
+ rule,
81
+ verbose,
82
+ info,
83
+ warn,
84
+ err,
85
+ debug,
86
+ )
87
+ from ..__version__ import __version__
88
+
89
+
90
+ VT = TypeVar("VT")
91
+
92
+
93
+ class OutputProcessor(ABC, Generic[VT]):
94
+ """
95
+ An abstract base class that processes terminal output from
96
+ :meth:`librelane.steps.Step.run_subprocess`
97
+ and append a resultant key/value pair to its returned dictionary.
98
+
99
+ :param step: The step object instantiating this output processor
100
+ :param report_dir: The report directory for this instantiation of
101
+ ``run_subprocess``.
102
+ :param silent: Whether the ``run_subprocess`` was called with ``silent`` or
103
+ not.
104
+ :cvar key: The fixed key to be added to the return value of
105
+ ``run_subprocess``. Must be implemented by subclasses.
106
+ """
107
+
108
+ key: ClassVar[str] = NotImplemented
109
+
110
+ def __init__(self, step: Step, report_dir: str, silent: bool) -> None:
111
+ self.step = step
112
+ self.report_dir: str = report_dir
113
+ self.silent: bool = silent
114
+
115
+ @abstractmethod
116
+ def process_line(self, line: str) -> bool:
117
+ """
118
+ Fires when a line is received by
119
+ :meth:`librelane.steps.Step.run_subprocess`. Subclasses may do any
120
+ arbitrary processing here.
121
+
122
+ :param line: The line emitted by the subprocess
123
+ :returns: ``True`` if the line is "consumed", i.e. other output
124
+ processors are skipped. ``False`` if the line is to be passed on
125
+ to later output processors.
126
+ """
127
+ pass
128
+
129
+ @abstractmethod
130
+ def result(self) -> VT:
131
+ """
132
+ :returns: The result of all previous ``process_line`` calls.
133
+ """
134
+ pass
135
+
136
+
137
+ class DefaultOutputProcessor(OutputProcessor[Dict[str, Any]]):
138
+ """
139
+ An output processor that makes a number of special functions accessible to
140
+ subprocesses by simply printing keywords in the terminal, such as:
141
+
142
+ * ``%OL_CREATE_REPORT <file>``\\: Starts redirecting all output from
143
+ standard output to a report file inside the step directory, with the
144
+ name <file>.
145
+ * ``%OL_END_REPORT``: Stops redirection behavior.
146
+ * ``%OL_METRIC <name> <value>``\\: Adds a string metric with the name <name>
147
+ and the value <value> to this function's returned object.
148
+ * ``%OL_METRIC_F <name> <value>``\\: Adds a floating-point metric with the
149
+ name <name> and the value <value> to this function's returned object.
150
+ * ``%OL_METRIC_I <name> <value>``\\: Adds an integer metric with the name
151
+ <name> and the value <value> to this function's returned object.
152
+
153
+ Otherwise, the line is simply printed to the logger.
154
+ """
155
+
156
+ key = "generated_metrics"
157
+
158
+ def __init__(self, *args, **kwargs):
159
+ super().__init__(*args, **kwargs)
160
+ self.generated_metrics: Dict[str, Any] = {}
161
+ self.current_rpt: Optional[TextIOWrapper] = None
162
+
163
+ def process_line(self, line: str) -> bool:
164
+ """
165
+ Always returns ``True``, so ``DefaultOutputProcessor`` should always be
166
+ at the end of your list.
167
+ """
168
+ if self.step.step_dir is not None and line.startswith(REPORT_START_LOCUS):
169
+ if self.current_rpt is not None:
170
+ self.current_rpt.close()
171
+ report_name = line[len(REPORT_START_LOCUS) + 1 :].strip()
172
+ report_path = os.path.join(self.report_dir, report_name)
173
+ self.current_rpt = open(report_path, "w")
174
+ elif line.startswith(REPORT_END_LOCUS):
175
+ if self.current_rpt is not None:
176
+ self.current_rpt.close()
177
+ self.current_rpt = None
178
+ elif line.startswith(METRIC_LOCUS):
179
+ command, name, value = line.split(" ", maxsplit=3)
180
+ metric_type: Union[Type[str], Type[int], Type[Decimal]] = str
181
+ if command.endswith("_I"):
182
+ metric_type = int
183
+ elif command.endswith("_F"):
184
+ metric_type = Decimal
185
+ self.generated_metrics[name] = metric_type(value)
186
+ elif self.current_rpt is not None:
187
+ # No echo- the timing reports especially can be very large
188
+ # and terminal emulators will slow the flow down.
189
+ self.current_rpt.write(line)
190
+ elif not self.silent:
191
+ logging.subprocess(line.strip())
192
+ return True
193
+
194
+ def result(self) -> Dict[str, Any]:
195
+ """
196
+ A dictionary of all generated metrics.
197
+ """
198
+ return self.generated_metrics
199
+
200
+
201
+ class StepError(RuntimeError):
202
+ """
203
+ A ``RuntimeError`` that occurs when a Step fails to finish execution
204
+ properly.
205
+ """
206
+
207
+ def __init__(self, *args, underlying_error: Optional[Exception] = None, **kwargs):
208
+ self.underlying_error = underlying_error
209
+ super().__init__(*args, **kwargs)
210
+
211
+
212
+ class DeferredStepError(StepError):
213
+ """
214
+ A variant of :class:`StepError` where parent Flows are encouraged to continue
215
+ execution of subsequent steps regardless and then finally flag the Error
216
+ at the very end.
217
+ """
218
+
219
+ pass
220
+
221
+
222
+ class StepException(StepError):
223
+ """
224
+ A variant of :class:`StepError` for unexpected failures or failures due
225
+ to misconfiguration, such as:
226
+
227
+ * Invalid inputs
228
+ * Mis-use of class interfaces of the :class:`Step`
229
+ * Other unexpected failures
230
+ """
231
+
232
+ pass
233
+
234
+
235
+ class StepSignalled(StepException):
236
+ pass
237
+
238
+
239
+ class StepNotFound(NameError):
240
+ def __init__(self, *args: object, id: Optional[str] = None) -> None:
241
+ super().__init__(*args)
242
+ self.id = id
243
+
244
+
245
+ REPORT_START_LOCUS = "%OL_CREATE_REPORT"
246
+ REPORT_END_LOCUS = "%OL_END_REPORT"
247
+ METRIC_LOCUS = "%OL_METRIC"
248
+
249
+ GlobalToolbox = Toolbox(os.path.join(os.getcwd(), "librelane_run", "tmp"))
250
+ ViewsUpdate = Dict[DesignFormat, StateElement]
251
+ MetricsUpdate = Dict[str, Any]
252
+
253
+
254
+ class ProcessStatsThread(Thread):
255
+ def __init__(self, process: psutil.Popen, interval: float = 0.1):
256
+ Thread.__init__(
257
+ self,
258
+ )
259
+ self.process = process
260
+ self.result = None
261
+ self.interval = interval
262
+ self.time = {
263
+ "cpu_time_user": 0.0,
264
+ "cpu_time_system": 0.0,
265
+ "runtime": 0.0,
266
+ }
267
+ if sys.platform == "linux":
268
+ self.time["cpu_time_iowait"] = 0.0
269
+
270
+ self.peak_resources = {
271
+ "cpu_percent": 0.0,
272
+ "memory_rss": 0.0,
273
+ "memory_vms": 0.0,
274
+ "threads": 0.0,
275
+ }
276
+ self.avg_resources = {
277
+ "cpu_percent": 0.0,
278
+ "memory_rss": 0.0,
279
+ "memory_vms": 0.0,
280
+ "threads": 0.0,
281
+ }
282
+
283
+ def run(self):
284
+ try:
285
+ count = 1
286
+ status = self.process.status()
287
+ now = datetime.datetime.now()
288
+ while status not in [psutil.STATUS_ZOMBIE, psutil.STATUS_DEAD]:
289
+ with self.process.oneshot():
290
+ cpu = self.process.cpu_percent()
291
+ memory = self.process.memory_info()
292
+ cpu_time = self.process.cpu_times()
293
+ threads = self.process.num_threads()
294
+
295
+ runtime = datetime.datetime.now() - now
296
+ self.time["runtime"] = runtime.total_seconds()
297
+ self.time["cpu_time_user"] = cpu_time.user
298
+ self.time["cpu_time_system"] = cpu_time.system
299
+ if sys.platform == "linux":
300
+ self.time["cpu_time_iowait"] = cpu_time.iowait # type: ignore
301
+
302
+ current: Dict[str, float] = {}
303
+ current["cpu_percent"] = cpu
304
+ current["memory_rss"] = memory.rss
305
+ current["memory_vms"] = memory.vms
306
+ current["threads"] = threads
307
+
308
+ for key in self.peak_resources.keys():
309
+ self.peak_resources[key] = max(
310
+ current[key], self.peak_resources[key]
311
+ )
312
+
313
+ # moving average
314
+ self.avg_resources[key] = (
315
+ (count * self.avg_resources[key]) + current[key]
316
+ ) / (count + 1)
317
+
318
+ count += 1
319
+ time.sleep(self.interval)
320
+ status = self.process.status()
321
+ except psutil.Error as e:
322
+ message = e.msg
323
+ for normal in ["process no longer exists", "but it's a zombie"]:
324
+ if normal in message:
325
+ return
326
+ warn(f"Process resource tracker encountered an error: {e}")
327
+
328
+ def stats_as_dict(self):
329
+ return {
330
+ "time": {k: format_elapsed_time(self.time[k]) for k in self.time},
331
+ "peak_resources": {
332
+ k: (
333
+ self.peak_resources[k]
334
+ if "memory" not in k
335
+ else format_size(int(self.peak_resources[k]))
336
+ )
337
+ for k in self.peak_resources
338
+ },
339
+ "avg_resources": {
340
+ k: (
341
+ self.avg_resources[k]
342
+ if "memory" not in k
343
+ else format_size(int(self.avg_resources[k]))
344
+ )
345
+ for k in self.avg_resources
346
+ },
347
+ }
348
+
349
+
350
+ class Step(ABC):
351
+ """
352
+ An abstract base class for Step objects.
353
+
354
+ Steps encapsulate a subroutine that acts upon certain classes of formats
355
+ in an input state and returns a new output state with updated design format
356
+ paths and/or metrics.
357
+
358
+ Warning: The initializer for Step is not thread-safe. Please use it on the main
359
+ thread and then, if you're using a Flow object, use ``start_step_async``, or
360
+ if you're not, you may use ``start`` in another thread. That part's fine.
361
+
362
+ :param config: A configuration object.
363
+
364
+ If running in interactive mode, you can set this to ``None``, but it is
365
+ otherwise required.
366
+
367
+ :param state_in: The state object this step will use as an input.
368
+
369
+ The state may also be a ``Future[State]``, in which case,
370
+ the ``start()`` call will block until that Future is realized.
371
+ This allows you to chain a number of asynchronous steps.
372
+
373
+ See https://en.wikipedia.org/wiki/Futures_and_promises for a primer.
374
+
375
+ If running in interactive mode, you can set this to ``None``, where it
376
+ will use the last generated state, but it is otherwise required.
377
+
378
+ :param step_dir: A "scratch directory" for the step. Required.
379
+
380
+ You may omit this argument as ``None`` if "flow" is specified.
381
+
382
+ :param id: A string ID for the Step. The convention is f"{a}.{b}", where the
383
+ first is common between all Steps using the same tools.
384
+
385
+ The ID should be in ``UpperCamelCase``.
386
+
387
+ While this is technically a class variable, instances allowed to change it
388
+ per-instance to disambiguate when the same step is used multiple times
389
+ in a flow.
390
+
391
+ :class:`Step` subclasses without the ``id`` class property declared
392
+ are considered abstract and cannot be initialized or used in a :class:`Flow`.
393
+
394
+ :param name: A short name for the Step, used in progress bars and
395
+ the like.
396
+
397
+ While this is technically an instance variable, it is expected for every
398
+ subclass to override this variable and instances are only to change it
399
+ to disambiguate when the same step is used multiple times in a flow.
400
+
401
+ :param long_name: A longer descriptive for the Step, used to delimit
402
+ logs.
403
+
404
+ While this is technically an instance variable, it is expected for every
405
+ subclass to override this variable and instances are only to change it
406
+ to disambiguate when the same step is used multiple times in a flow.
407
+
408
+ :param flow: Deprecated: the parent flow. Ignored if passed.
409
+
410
+ :cvar inputs: A list of :class:`librelane.state.DesignFormat` objects that
411
+ are required for this step. These will be validated by the :meth:`start`
412
+ method.
413
+
414
+ :class:`Step` subclasses without the ``inputs`` class property declared
415
+ are considered abstract and cannot be initialized or used in a :class:`Flow`.
416
+
417
+ :cvar outputs: A list of :class:`librelane.state.DesignFormat` objects that
418
+ may be emitted by this step. A step is not allowed to modify design
419
+ formats not declared in ``outputs``.
420
+
421
+ :class:`Step` subclasses without the ``outputs`` class property declared
422
+ are considered abstract and cannot be initialized or used in a :class:`Flow`.
423
+
424
+ :cvar config_vars: A list of configuration :class:`librelane.config.Variable` objects
425
+ to be used to alter the behavior of this Step.
426
+
427
+ :cvar output_processors: A default set of
428
+ :class:`librelane.steps.OutputProcessor` classes for use with
429
+ :meth:`run_subprocess`.
430
+
431
+ :ivar state_out:
432
+ The last output state from running this step object, if it exists.
433
+
434
+ If :meth:`start` is called again, the reference is destroyed.
435
+
436
+ :ivar start_time:
437
+ The last starting time from running this step object, if it exists.
438
+
439
+ If :meth:`start` is called again, the reference is destroyed.
440
+
441
+ :ivar end_time:
442
+ The last ending time from running this step object, if it exists.
443
+
444
+ If :meth:`start` is called again, the reference is destroyed.
445
+
446
+ :ivar config_path:
447
+ Path to the last step-specific `config.json` generated while running
448
+ this step object, if it exists.
449
+
450
+ If :meth:`start` is called again, the path will be replaced.
451
+
452
+ :ivar toolbox:
453
+ The last :class:`Toolbox` used while running this step object, if it
454
+ exists.
455
+
456
+ If :meth:`start` is called again, the reference is destroyed.
457
+ """
458
+
459
+ # Class Variables
460
+ id: str = NotImplemented
461
+ inputs: ClassVar[List[DesignFormat]] = NotImplemented
462
+ outputs: ClassVar[List[DesignFormat]] = NotImplemented
463
+ output_processors: ClassVar[List[Type[OutputProcessor]]] = [DefaultOutputProcessor]
464
+ config_vars: ClassVar[List[Variable]] = []
465
+
466
+ # Instance Variables
467
+ name: str
468
+ long_name: str
469
+ state_in: Future[State]
470
+
471
+ ## Stateful
472
+ toolbox: Toolbox = GlobalToolbox
473
+ state_out: Optional[State] = None
474
+ start_time: Optional[float] = None
475
+ end_time: Optional[float] = None
476
+ config_path: Optional[str] = None
477
+
478
+ # These are mutable class variables. However, they will only be used
479
+ # when steps are run outside of a Flow, pretty much.
480
+ counter: ClassVar[int] = 1
481
+ # End Mutable Global Variables
482
+
483
+ def __init__(
484
+ self,
485
+ config: Optional[Config] = None,
486
+ state_in: Union[Optional[State], Future[State]] = None,
487
+ *,
488
+ id: Optional[str] = None,
489
+ name: Optional[str] = None,
490
+ long_name: Optional[str] = None,
491
+ flow: Optional[Any] = None,
492
+ _config_quiet: bool = False,
493
+ _no_revalidate_conf: bool = False,
494
+ **kwargs,
495
+ ):
496
+ self.__class__.assert_concrete()
497
+
498
+ if flow is not None:
499
+ self.warn(
500
+ f"Passing 'flow' to a Step class's initializer is deprecated. Please update the flow '{type(flow).__name__}'."
501
+ )
502
+
503
+ if id is not None:
504
+ self.id = id
505
+
506
+ if config is None:
507
+ if current_interactive := Config.current_interactive:
508
+ config = current_interactive
509
+ else:
510
+ raise TypeError("Missing required argument 'config'")
511
+
512
+ if state_in is None:
513
+ if Config.current_interactive is not None:
514
+ raise TypeError(
515
+ "Using an implicit input state in interactive mode is no longer supported- pass the last state in as follows: `state_in=last_step.state_out`"
516
+ )
517
+ else:
518
+ raise TypeError("Missing required argument 'state_in'")
519
+
520
+ if name is not None:
521
+ self.name = name
522
+ elif not hasattr(self, "name"):
523
+ self.name = self.__class__.__name__
524
+
525
+ if long_name is not None:
526
+ self.long_name = long_name
527
+ elif not hasattr(self, "long_name"):
528
+ self.long_name = self.name
529
+
530
+ if _no_revalidate_conf:
531
+ self.config = config.copy_filtered(
532
+ self.get_all_config_variables(),
533
+ include_flow_variables=False, # get_all_config_variables() gets them anyway
534
+ )
535
+ else:
536
+ self.config = config.with_increment(
537
+ self.get_all_config_variables(),
538
+ kwargs,
539
+ _config_quiet,
540
+ )
541
+
542
+ state_in_future: Future[State] = Future()
543
+ if isinstance(state_in, State):
544
+ state_in_future.set_result(state_in)
545
+ else:
546
+ state_in_future = state_in
547
+ self.state_in = state_in_future
548
+
549
+ def __init_subclass__(cls):
550
+ if hasattr(cls, "flow_control_variable"):
551
+ warn(
552
+ f"Step '{cls.__name__}' uses deprecated property 'flow_control_variable'. Flow control should now be done using the Flow class's 'gating_config_vars' property."
553
+ )
554
+ if cls.id != NotImplemented:
555
+ if f".{cls.__name__}" not in cls.id:
556
+ debug(f"Step '{cls.__name__}' has a non-matching ID: '{cls.id}'")
557
+
558
+ def warn(self, msg: object, /, **kwargs):
559
+ """
560
+ Logs to the LibreLane logger with the log level WARNING, appending the
561
+ step's ID as extra data.
562
+
563
+ :param msg: The message to log
564
+ """
565
+ if kwargs.get("stacklevel") is None:
566
+ kwargs["stacklevel"] = 3
567
+ extra = kwargs.pop("extra", {})
568
+ extra["step"] = self.id
569
+ warn(msg, extra=extra, **kwargs)
570
+
571
+ def err(self, msg: object, /, **kwargs):
572
+ """
573
+ Logs to the LibreLane logger with the log level ERROR, appending the
574
+ step's ID as extra data.
575
+
576
+ :param msg: The message to log
577
+ """
578
+ if kwargs.get("stacklevel") is None:
579
+ kwargs["stacklevel"] = 3
580
+ extra = kwargs.pop("extra", {})
581
+ extra["step"] = self.id
582
+ err(msg, extra=extra, **kwargs)
583
+
584
+ @classmethod
585
+ def get_implementation_id(Self) -> str:
586
+ if hasattr(Self, "_implementation_id"):
587
+ return getattr(Self, "_implementation_id")
588
+ return Self.id
589
+
590
+ @classmethod
591
+ def assert_concrete(Self, action: str = "initialized"):
592
+ """
593
+ Checks if the Step class in question is concrete, with abstract methods
594
+ AND ``NotImplemented`` classes implemented and declared respectively.
595
+
596
+ Should be called before any ``Step`` subclass is used.
597
+
598
+ If the class is not concrete, a ``NotImplementedError`` is raised.
599
+
600
+ :param action: The action to be attempted, to be included in the
601
+ ``NotImplementedError`` message.
602
+ """
603
+ if isabstract(Self):
604
+ raise NotImplementedError(
605
+ f"Abstract step {Self.__qualname__} has one or more methods not implemented ({' '.join(Self.__abstractmethods__)}) and cannot be {action}"
606
+ )
607
+
608
+ for attr in ["id", "inputs", "outputs"]:
609
+ if not hasattr(Self, attr) or getattr(Self, attr) == NotImplemented:
610
+ raise NotImplementedError(
611
+ f"Abstract step {Self.__qualname__} does not implement the .{attr} property and cannot be {action}"
612
+ )
613
+
614
+ @classmethod
615
+ def __get_desc(Self) -> str: # pragma: no cover
616
+ if hasattr(Self, "long_name"):
617
+ return Self.long_name
618
+ elif hasattr(Self, "name"):
619
+ return Self.name
620
+ return Self.__name__
621
+
622
+ @classmethod
623
+ def get_help_md(
624
+ Self,
625
+ *,
626
+ docstring_override: str = "",
627
+ use_dropdown: bool = False,
628
+ myst_anchors: bool = False,
629
+ ): # pragma: no cover
630
+ """
631
+ Renders Markdown help for this step to a string.
632
+ """
633
+ doc_string = docstring_override
634
+ if Self.__doc__:
635
+ doc_string = textwrap.dedent(Self.__doc__)
636
+
637
+ result = (
638
+ textwrap.dedent(
639
+ f"""
640
+ ```{{eval-rst}}
641
+ %s
642
+ ```
643
+
644
+ {':::{dropdown} Importing' if use_dropdown else '#### Importing'}
645
+ ```python
646
+ from {Self.__module__} import {Self.__name__}
647
+
648
+ # or
649
+
650
+ from librelane.steps import Step
651
+
652
+ {Self.__name__} = Step.factory.get("{Self.id}")
653
+ ```
654
+ {':::' if use_dropdown else ''}
655
+ """
656
+ )
657
+ % doc_string
658
+ )
659
+ if len(Self.inputs) + len(Self.outputs):
660
+ result += textwrap.dedent(
661
+ """
662
+ #### Inputs and Outputs
663
+
664
+ | Inputs | Outputs |
665
+ | - | - |
666
+ """
667
+ )
668
+ for input, output in zip_longest(Self.inputs, Self.outputs):
669
+ input_str = ""
670
+ if input is not None:
671
+ optional = "?" if input.value.optional else ""
672
+ input_str = (
673
+ f"{input.value.name}{optional} (.{input.value.extension})"
674
+ )
675
+
676
+ output_str = ""
677
+ if output is not None:
678
+ if not isinstance(output, DesignFormat):
679
+ raise StepException(
680
+ f"Output '{output}' is not a valid DesignFormat enum object."
681
+ )
682
+ output_str = f"{output.value.name} (.{output.value.extension})"
683
+ result += f"| {input_str} | {output_str} |\n"
684
+
685
+ if len(Self.config_vars):
686
+ all_vars_anchor = f"({Self.id.lower()}-configuration-variables)="
687
+ result += textwrap.dedent(
688
+ f"""
689
+ {all_vars_anchor * myst_anchors}
690
+ #### Configuration Variables
691
+
692
+ | Variable Name | Type | Description | Default | Units |
693
+ | - | - | - | - | - |
694
+ """
695
+ )
696
+ for var in Self.config_vars:
697
+ units = var.units or ""
698
+ pdk_superscript = "<sup>PDK</sup>" if var.pdk else ""
699
+ var_anchor = f"{{#{var._get_docs_identifier(Self.id)}}}"
700
+ result += f"| `{var.name}`{var_anchor * myst_anchors} {pdk_superscript} | {var.type_repr_md(for_document=True)} | {var.desc_repr_md()} | `{var.default}` | {units} |\n"
701
+ result += "\n"
702
+
703
+ step_anchor = f"(step-{slugify(Self.id.lower())})="
704
+ result = (
705
+ textwrap.dedent(
706
+ f"""
707
+ {step_anchor * myst_anchors}
708
+ ### {Self.__get_desc()}
709
+ """
710
+ )
711
+ + result
712
+ )
713
+
714
+ return result
715
+
716
+ @classmethod
717
+ def display_help(Self): # pragma: no cover
718
+ """
719
+ Displays Markdown help for this Step.
720
+
721
+ If in an IPython environment, it's rendered using ``IPython.display``.
722
+ Otherwise, it's rendered using ``rich.markdown``.
723
+ """
724
+ try:
725
+ get_ipython() # type: ignore
726
+
727
+ import IPython.display
728
+
729
+ IPython.display.display(IPython.display.Markdown(Self.get_help_md()))
730
+ except NameError:
731
+ from ..logging import console
732
+ from rich.markdown import Markdown
733
+
734
+ console.log(Markdown(Self.get_help_md()))
735
+
736
+ def _repr_markdown_(self) -> str: # pragma: no cover
737
+ """
738
+ Only one _ because this is used by IPython.
739
+ """
740
+ if self.state_out is None:
741
+ return """
742
+ ### Step not yet executed.
743
+ """
744
+ state_in = self.state_in.result()
745
+
746
+ assert (
747
+ self.start_time is not None
748
+ ), "Start time not set even though self.state_out exists"
749
+ assert (
750
+ self.end_time is not None
751
+ ), "End time not set even though self.state_out exists"
752
+ result = f"#### Time Elapsed: {'%.2f' % (self.end_time - self.start_time)}s\n"
753
+
754
+ views_updated = []
755
+ for id, value in dict(self.state_out).items():
756
+ if value is None:
757
+ continue
758
+
759
+ if state_in.get(id) != value:
760
+ df = DesignFormat.by_id(id)
761
+ assert df is not None
762
+ views_updated.append(df.value.name)
763
+
764
+ if len(views_updated):
765
+ result += "#### Views updated:\n"
766
+ for view in views_updated:
767
+ result += f"* {view}\n"
768
+
769
+ if preview := self.layout_preview():
770
+ result += "#### Preview:\n"
771
+ result += preview
772
+
773
+ return result
774
+
775
+ def layout_preview(self) -> Optional[str]: # pragma: no cover
776
+ """
777
+ :returns: An HTML tag that could act as a preview for a specific stage
778
+ or ``None`` if a preview is unavailable for this step.
779
+ """
780
+ return None
781
+
782
+ def display_result(self): # pragma: no cover
783
+ """
784
+ IPython-only. Displays the results of a given step.
785
+ """
786
+ import IPython.display
787
+
788
+ IPython.display.display(IPython.display.Markdown(self._repr_markdown_()))
789
+
790
+ @classmethod
791
+ def _load_config_from_file(
792
+ Self, config_path: Union[str, os.PathLike], pdk_root: str = "."
793
+ ) -> Config:
794
+ config, _ = Config.load(
795
+ config_in=json.loads(open(config_path).read(), parse_float=Decimal),
796
+ flow_config_vars=Self.get_all_config_variables(),
797
+ design_dir=".",
798
+ pdk_root=pdk_root,
799
+ _load_pdk_configs=False,
800
+ )
801
+ return config
802
+
803
+ @classmethod
804
+ def load(
805
+ Self,
806
+ config: Union[str, os.PathLike, Config],
807
+ state_in: Union[str, State],
808
+ pdk_root: Optional[str] = None,
809
+ ) -> Step:
810
+ """
811
+ Creates a step object, but instead of using a Flow or a global state,
812
+ the config_path and input state are deserialized from JSON files.
813
+
814
+ Useful for re-running steps that have already run.
815
+
816
+ :param config:
817
+ (Path to) a **Step-filtered** configuration
818
+
819
+ The step will not tolerate variables unrelated to this specific step.
820
+ :param state: (Path to) a valid input state
821
+ :param pdk_root: The PDK root, which is needed for some utilities.
822
+
823
+ If your utility doesn't require it, just keep the default value
824
+ as-is.
825
+ :returns: The created step object
826
+ """
827
+ if Self.id == NotImplemented: # If abstract
828
+ id, Target = Step.factory.from_step_config(config)
829
+ if id is None:
830
+ raise StepNotFound(
831
+ "Attempted to initialize abstract Step, and no step ID was found in the configuration."
832
+ )
833
+ if Target is None:
834
+ raise StepNotFound(
835
+ "Attempted to initialize abstract Step, and Step designated in configuration file not found.",
836
+ id=id,
837
+ )
838
+ return Target.load(config, state_in, pdk_root)
839
+
840
+ pdk_root = pdk_root or "."
841
+ if not isinstance(config, Config):
842
+ config = Self._load_config_from_file(config, pdk_root)
843
+ if not isinstance(state_in, State):
844
+ state_in = State.loads(open(state_in).read())
845
+ return Self(
846
+ config=config,
847
+ state_in=state_in,
848
+ _no_revalidate_conf=True,
849
+ )
850
+
851
+ @classmethod
852
+ def load_finished(
853
+ Self,
854
+ step_dir: str,
855
+ pdk_root: Optional[str] = None,
856
+ search_steps: Optional[List[Type[Step]]] = None,
857
+ ) -> "Step":
858
+ config_path = os.path.join(step_dir, "config.json")
859
+ state_in_path = os.path.join(step_dir, "state_in.json")
860
+ state_out_path = os.path.join(step_dir, "state_out.json")
861
+ for file in config_path, state_in_path, state_out_path:
862
+ if not os.path.isfile(file):
863
+ raise FileNotFoundError(file)
864
+
865
+ try:
866
+ step_object = Self.load(config_path, state_in_path, pdk_root)
867
+ except StepNotFound as e:
868
+ if e.id is not None:
869
+ search_steps = search_steps or []
870
+ Matched: Optional[Type[Step]] = None
871
+ for step in search_steps:
872
+ if step.get_implementation_id() == e.id:
873
+ Matched = step
874
+ break
875
+ if Matched is None:
876
+ raise e from None
877
+ step_object = Matched.load(config_path, state_in_path, pdk_root)
878
+ else:
879
+ raise e from None
880
+ step_object.step_dir = step_dir
881
+ step_object.state_out = State.loads(open(state_out_path).read())
882
+ return step_object
883
+
884
+ @classmethod
885
+ def get_all_config_variables(Self) -> List[Variable]:
886
+ variables_by_name: Dict[str, Variable] = {
887
+ variable.name: variable for variable in universal_flow_config_variables
888
+ }
889
+ for variable in Self.config_vars:
890
+ if existing_variable := variables_by_name.get(variable.name):
891
+ if variable != existing_variable:
892
+ raise StepException(
893
+ f"Misconstructed step: Unrelated variable exists with the same name as one in the common Flow variables: {variable.name}"
894
+ )
895
+ else:
896
+ variables_by_name[variable.name] = variable
897
+
898
+ return list(variables_by_name.values())
899
+
900
+ def create_reproducible(
901
+ self,
902
+ target_dir: str,
903
+ include_pdk: bool = True,
904
+ flatten: bool = False,
905
+ ):
906
+ """
907
+ Creates a folder that, given a specific version of LibreLane being
908
+ installed, makes a portable reproducible of that step's execution.
909
+
910
+ ..note
911
+
912
+ Reproducibles are limited on Magic and Netgen, as their RC files
913
+ form an indirect dependency on many `.mag` files or similar that
914
+ cannot be enumerated by LibreLane.
915
+
916
+ Reproducibles are automatically generated for failed steps, but
917
+ this may be called manually on any step, too.
918
+
919
+ :param target_dir: The directory in which to create the reproducible
920
+ :param include_pdk: Include PDK files. If set to false, Path pointing
921
+ to PDK files will be prefixed with ``pdk_dir::`` instead of being
922
+ copied.
923
+ :param flatten: Creates a reproducible with a flat (single-directory)
924
+ file structure, except for the PDK which will maintain its internal
925
+ folder structure (as it is sensitive to it.)
926
+ """
927
+ # 0. Create Directories
928
+ try:
929
+ shutil.rmtree(target_dir, ignore_errors=False)
930
+ except FileNotFoundError:
931
+ pass
932
+ mkdirp(target_dir)
933
+
934
+ files_path = target_dir if flatten else os.path.join(target_dir, "files")
935
+ pdk_root_flat_dirname = "pdk"
936
+ pdk_flat_dirname = os.path.join(pdk_root_flat_dirname, self.config["PDK"], "")
937
+ pdk_flat_path = os.path.join(target_dir, pdk_flat_dirname)
938
+ if flatten and include_pdk:
939
+ mkdirp(pdk_flat_path)
940
+
941
+ pdk_path = os.path.join(self.config["PDK_ROOT"], self.config["PDK"], "")
942
+
943
+ def visitor(x: Any) -> Any:
944
+ nonlocal files_path, include_pdk, pdk_path, pdk_flat_dirname
945
+ if not isinstance(x, Path):
946
+ return x
947
+
948
+ if not include_pdk and x.startswith(pdk_path):
949
+ return x.replace(pdk_path, "pdk_dir::")
950
+
951
+ target_relpath = os.path.join(".", "files", x[1:])
952
+ target_abspath = os.path.join(files_path, x[1:])
953
+
954
+ if flatten:
955
+ if include_pdk and x.startswith(pdk_path):
956
+ target_relpath = os.path.join(
957
+ ".", x.replace(pdk_path, pdk_flat_dirname)
958
+ )
959
+ target_abspath = os.path.join(target_dir, target_relpath)
960
+ else:
961
+ counter = 0
962
+ filename = os.path.basename(x)
963
+
964
+ def filename_with_counter():
965
+ nonlocal counter, filename
966
+ if counter == 0:
967
+ return filename
968
+ else:
969
+ return f"{counter}-{filename}"
970
+
971
+ target_relpath = ""
972
+ target_abspath = "/"
973
+ while os.path.exists(target_abspath):
974
+ current = filename_with_counter()
975
+ target_relpath = os.path.join(".", current)
976
+ target_abspath = os.path.join(files_path, current)
977
+ counter += 1
978
+
979
+ mkdirp(os.path.dirname(target_abspath))
980
+
981
+ if os.path.isdir(x):
982
+ if not flatten:
983
+ mkdirp(target_abspath)
984
+ else:
985
+ shutil.copy(x, target_abspath)
986
+ if hasattr(os, "chmod"):
987
+ os.chmod(target_abspath, 0o755)
988
+
989
+ return Path(target_relpath)
990
+
991
+ # 1. Config
992
+ dumpable_config: dict = copy_recursive(self.config, translator=visitor)
993
+ dumpable_config["meta"] = {
994
+ "librelane_version": __version__,
995
+ "step": self.__class__.get_implementation_id(),
996
+ }
997
+
998
+ del dumpable_config["DESIGN_DIR"]
999
+
1000
+ if include_pdk:
1001
+ pdk_dirname = dumpable_config["PDK_ROOT"]
1002
+ if flatten:
1003
+ pdk_dirname = pdk_root_flat_dirname
1004
+
1005
+ # So it's always the first one:
1006
+ dumpable_config = {"PDK_ROOT": pdk_dirname, **dumpable_config}
1007
+
1008
+ else:
1009
+ # If not including the PDK, pdk_root is going to have to be
1010
+ # passed to the config when running the reproducible.
1011
+ del dumpable_config["PDK_ROOT"]
1012
+
1013
+ dumpable_config = {
1014
+ k: dumpable_config[k] for k in sorted(dumpable_config)
1015
+ } # sort dict
1016
+
1017
+ config_path = os.path.join(target_dir, "config.json")
1018
+ with open(config_path, "w") as f:
1019
+ f.write(json.dumps(dumpable_config, cls=GenericDictEncoder))
1020
+
1021
+ # 2. State
1022
+ state_in: GenericDict[str, Any] = self.state_in.result().copy_mut()
1023
+ for format in DesignFormat:
1024
+ assert isinstance(format.value, DesignFormatObject) # type checker shut up
1025
+ if format not in self.__class__.inputs and not (
1026
+ format == DesignFormat.DEF
1027
+ and DesignFormat.ODB
1028
+ in self.__class__.inputs # hack to write tests a bit more easily
1029
+ ):
1030
+ state_in[format.value.id] = None
1031
+ state_in["metrics"] = self.state_in.result().metrics.copy_mut()
1032
+ dumpable_state = copy_recursive(state_in, translator=visitor)
1033
+ state_path = os.path.join(target_dir, "state_in.json")
1034
+ with open(state_path, "w") as f:
1035
+ f.write(json.dumps(dumpable_state, cls=GenericDictEncoder))
1036
+
1037
+ # 3. Runner (LibreLane)
1038
+ script_path = os.path.join(target_dir, "run_ol.sh")
1039
+ with open(script_path, "w") as f:
1040
+ f.write(
1041
+ textwrap.dedent(
1042
+ """
1043
+ #!/bin/sh
1044
+ set -e
1045
+ python3 -m librelane --version
1046
+ if [ "$?" != "0" ]; then
1047
+ echo "Failed to run 'python3 -m librelane --version'."
1048
+ exit -1
1049
+ fi
1050
+
1051
+ ARGS="$@"
1052
+ if [ "$1" != "eject" ] && [ "$1" != "run" ]; then
1053
+ ARGS="run $@"
1054
+ fi
1055
+ python3 -m librelane.steps $ARGS\\
1056
+ --config ./config.json\\
1057
+ --state-in ./state_in.json
1058
+ """
1059
+ ).strip()
1060
+ )
1061
+ if hasattr(os, "chmod"):
1062
+ os.chmod(script_path, 0o755)
1063
+ hyperlinks = (
1064
+ os.getenv(
1065
+ "_i_want_librelane_to_hyperlink_things_for_some_reason",
1066
+ None,
1067
+ )
1068
+ == "1"
1069
+ )
1070
+ link_start = ""
1071
+ link_end = ""
1072
+ if hyperlinks:
1073
+ link_start = f"[link=file://{os.path.abspath(target_dir)}]"
1074
+ link_end = "[/link]"
1075
+
1076
+ info(
1077
+ f"Reproducible created at: {link_start}'{os.path.relpath(target_dir)}'{link_end}"
1078
+ )
1079
+
1080
+ @final
1081
+ def start(
1082
+ self,
1083
+ toolbox: Optional[Toolbox] = None,
1084
+ step_dir: Optional[str] = None,
1085
+ _no_rule: bool = False,
1086
+ **kwargs,
1087
+ ) -> State:
1088
+ """
1089
+ Begins execution on a step.
1090
+
1091
+ This method is final and should not be subclassed.
1092
+
1093
+ :param toolbox: The flow's :class:`Toolbox` object, required.
1094
+
1095
+ If running in interactive mode, you may omit this argument as ``None``\\,
1096
+ where a global toolbox will be used instead.
1097
+
1098
+ If running inside a flow, you may also omit this argument as ``None``\\,
1099
+ where the flow's toolbox will used to be instead.
1100
+
1101
+ :param \\*\\*kwargs: Passed on to subprocess execution: useful if you want to
1102
+ redirect stdin, stdout, etc.
1103
+
1104
+ :returns: An altered State object.
1105
+ """
1106
+
1107
+ if step_dir is None:
1108
+ if Config.current_interactive is not None:
1109
+ self.step_dir = os.path.join(
1110
+ os.getcwd(),
1111
+ "librelane_run",
1112
+ f"{Step.counter}-{slugify(self.id)}",
1113
+ )
1114
+ Step.counter += 1
1115
+ else:
1116
+ raise TypeError("Missing required argument 'step_dir'")
1117
+ else:
1118
+ self.step_dir = step_dir
1119
+
1120
+ if toolbox is None:
1121
+ if Config.current_interactive is not None:
1122
+ pass
1123
+ else:
1124
+ self.toolbox = Toolbox(self.step_dir)
1125
+ else:
1126
+ self.toolbox = toolbox
1127
+
1128
+ state_in_result = self.state_in.result()
1129
+
1130
+ if not logging.options.get_condensed_mode():
1131
+ rule(f"{self.long_name}")
1132
+
1133
+ hyperlinks = (
1134
+ os.getenv(
1135
+ "_i_want_librelane_to_hyperlink_things_for_some_reason",
1136
+ None,
1137
+ )
1138
+ == "1"
1139
+ )
1140
+ link_start = ""
1141
+ link_end = ""
1142
+ if hyperlinks:
1143
+ link_start = f"[link=file://{os.path.abspath(self.step_dir)}]"
1144
+ link_end = "[/link]"
1145
+
1146
+ verbose(
1147
+ f"Running '{self.id}' at {link_start}'{os.path.relpath(self.step_dir)}'{link_end}…"
1148
+ )
1149
+
1150
+ mkdirp(self.step_dir)
1151
+ with open(os.path.join(self.step_dir, "state_in.json"), "w") as f:
1152
+ f.write(state_in_result.dumps())
1153
+
1154
+ self.config_path = os.path.join(self.step_dir, "config.json")
1155
+ with open(self.config_path, "w") as f:
1156
+ config_mut = self.config.to_raw_dict()
1157
+ config_mut["meta"] = {
1158
+ "librelane_version": __version__,
1159
+ "step": self.__class__.get_implementation_id(),
1160
+ }
1161
+ f.write(json.dumps(config_mut, cls=GenericDictEncoder, indent=4))
1162
+
1163
+ debug(f"Step directory ▶ '{self.step_dir}'")
1164
+ self.start_time = time.time()
1165
+
1166
+ for input in self.inputs:
1167
+ value = state_in_result[input]
1168
+ if value is None and not input.value.optional:
1169
+ raise StepException(
1170
+ f"{type(self).__name__}: missing required input '{input.name}'"
1171
+ ) from None
1172
+
1173
+ try:
1174
+ views_updates, metrics_updates = self.run(state_in_result, **kwargs)
1175
+ except subprocess.CalledProcessError as e:
1176
+ if e.returncode is not None and e.returncode < 0:
1177
+ raise StepSignalled(
1178
+ f"{self.name}: Interrupted ({Signals(-e.returncode).name})"
1179
+ ) from None
1180
+ else:
1181
+ raise StepError(
1182
+ f"{self.name}: subprocess {e.args} failed", underlying_error=e
1183
+ ) from None
1184
+
1185
+ metrics = GenericImmutableDict(
1186
+ state_in_result.metrics, overrides=metrics_updates
1187
+ )
1188
+
1189
+ self.state_out = state_in_result.__class__(
1190
+ state_in_result, overrides=views_updates, metrics=metrics
1191
+ )
1192
+
1193
+ try:
1194
+ self.state_out.validate()
1195
+ except InvalidState as e:
1196
+ raise StepException(
1197
+ f"Step {self.name} generated invalid state: {e}"
1198
+ ) from None
1199
+
1200
+ with open(os.path.join(self.step_dir, "state_out.json"), "w") as f:
1201
+ f.write(self.state_out.dumps())
1202
+
1203
+ self.end_time = time.time()
1204
+ with open(os.path.join(self.step_dir, "runtime.txt"), "w") as f:
1205
+ f.write(format_elapsed_time(self.end_time - self.start_time))
1206
+
1207
+ return self.state_out
1208
+
1209
+ @protected
1210
+ @abstractmethod
1211
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1212
+ """
1213
+ The "core" of a step.
1214
+
1215
+ This step is considered per-object private, i.e., if a Step's run is
1216
+ called anywhere outside of the same object's :meth:`start`\\, its behavior
1217
+ is undefined.
1218
+
1219
+ :param state_in: The input state.
1220
+
1221
+ Note that ``self.state_in`` is stored as a future and would need to be
1222
+ resolved before use first otherwise.
1223
+
1224
+ For reference, ``start()`` is responsible for resolving it
1225
+ for ``.run()``\\.
1226
+
1227
+ :param \\*\\*kwargs: Passed on to subprocess execution: useful if you want to
1228
+ redirect stdin, stdout, etc.
1229
+ """
1230
+ pass
1231
+
1232
+ @protected
1233
+ def get_log_path(self) -> str:
1234
+ """
1235
+ :returns: the default value for :meth:`run_subprocess`'s "log_to"
1236
+ parameter.
1237
+
1238
+ Override it to change the default log path.
1239
+ """
1240
+ return os.path.join(self.step_dir, f"{slugify(self.id)}.log")
1241
+
1242
+ @protected
1243
+ def run_subprocess(
1244
+ self,
1245
+ cmd: Sequence[Union[str, os.PathLike]],
1246
+ log_to: Optional[Union[str, os.PathLike]] = None,
1247
+ silent: bool = False,
1248
+ report_dir: Optional[Union[str, os.PathLike]] = None,
1249
+ env: Optional[Dict[str, Any]] = None,
1250
+ *,
1251
+ check: bool = True,
1252
+ output_processing: Optional[Sequence[Type[OutputProcessor]]] = None,
1253
+ _popen_callable: Callable[..., psutil.Popen] = psutil.Popen,
1254
+ **kwargs,
1255
+ ) -> Dict[str, Any]:
1256
+ """
1257
+ A helper function for :class:`Step` objects to run subprocesses.
1258
+
1259
+ The output from the subprocess is processed line-by-line by instances
1260
+ of output processor classes.
1261
+
1262
+ :param cmd: A list of variables, representing a program and its arguments,
1263
+ similar to how you would use it in a shell.
1264
+ :param log_to: An optional override for the log path from
1265
+ :meth:`get_log_path`\\. Useful for if you run multiple subprocesses
1266
+ within one step.
1267
+ :param silent: If specified, the subprocess does not print anything to
1268
+ the terminal. Useful when running multiple processes simultaneously.
1269
+ :param report_dir: An optional override for where reports by output
1270
+ processors
1271
+
1272
+ :param check: Whether to raise ``subprocess.CalledProcessError`` in
1273
+ the event of a non-zero exit code. Set to ``False`` if you'd like
1274
+ to do further processing on the output(s).
1275
+ :param output_processing: An override for the class's list of
1276
+ :class:`librelane.steps.OutputProcessor` classes.
1277
+ :param \\*\\*kwargs: Passed on to subprocess execution: useful if you want to
1278
+ redirect stdin, stdout, etc.
1279
+ :returns: A dictionary of output processor results.
1280
+
1281
+ These key/value pairs are included in all cases:
1282
+ * ``returncode``: Exit code for the subprocess
1283
+ * ``log_path``: The resolved log path for the subprocess
1284
+
1285
+ The other key value pairs depend on the ``key`` class variables
1286
+ and :meth:`librelane.steps.OutputProcessor.result` methods of the
1287
+ output processors.
1288
+ :raises subprocess.CalledProcessError: If the process has a non-zero
1289
+ exit, and ``check`` is True, this exception will be raised.
1290
+ """
1291
+ if report_dir is None:
1292
+ report_dir = self.step_dir
1293
+ report_dir = str(report_dir)
1294
+ mkdirp(report_dir)
1295
+
1296
+ log_path = log_to or self.get_log_path()
1297
+ log_file = open(log_path, "w")
1298
+ cmd_str = [str(arg) for arg in cmd]
1299
+
1300
+ with open(os.path.join(self.step_dir, "COMMANDS"), "a+") as f:
1301
+ f.write(" ".join(cmd_str))
1302
+ f.write("\n")
1303
+
1304
+ kwargs = kwargs.copy()
1305
+ if "stdin" not in kwargs:
1306
+ kwargs["stdin"] = open(os.devnull, "r")
1307
+ if "stdout" not in kwargs:
1308
+ kwargs["stdout"] = subprocess.PIPE
1309
+ if "stderr" not in kwargs:
1310
+ kwargs["stderr"] = subprocess.STDOUT
1311
+
1312
+ env = env or os.environ.copy()
1313
+ for key, value in env.items():
1314
+ if not (
1315
+ isinstance(value, str)
1316
+ or isinstance(value, bytes)
1317
+ or isinstance(value, os.PathLike)
1318
+ ):
1319
+ raise StepException(
1320
+ f"Environment variable for key '{key}' is of invalid type {type(value)}: {value}"
1321
+ )
1322
+
1323
+ if output_processing is None:
1324
+ output_processing = self.output_processors
1325
+ output_processors = []
1326
+ for cls in output_processing:
1327
+ output_processors.append(cls(self, report_dir, silent))
1328
+
1329
+ hyperlinks = (
1330
+ os.getenv(
1331
+ "_i_want_librelane_to_hyperlink_things_for_some_reason",
1332
+ None,
1333
+ )
1334
+ == "1"
1335
+ )
1336
+ link_start = ""
1337
+ link_end = ""
1338
+ if hyperlinks:
1339
+ link_start = f"[link=file://{os.path.abspath(log_path)}]"
1340
+ link_end = "[/link]"
1341
+
1342
+ verbose(
1343
+ f"Logging subprocess to [repr.filename]{link_start}'{os.path.relpath(log_path)}'{link_end}[/repr.filename]…"
1344
+ )
1345
+ process = _popen_callable(
1346
+ cmd_str,
1347
+ encoding="utf8",
1348
+ env=env,
1349
+ **kwargs,
1350
+ )
1351
+
1352
+ process_stats_thread = ProcessStatsThread(process)
1353
+ process_stats_thread.start()
1354
+
1355
+ line_buffer = RingBuffer(str, 10)
1356
+ if process_stdout := process.stdout:
1357
+ try:
1358
+ for line in process_stdout:
1359
+ log_file.write(line)
1360
+ line_buffer.push(line)
1361
+ for processor in output_processors:
1362
+ if processor.process_line(line):
1363
+ break
1364
+ except UnicodeDecodeError as e:
1365
+ raise StepException(f"Subprocess emitted non-UTF-8 output: {e}")
1366
+ process_stats_thread.join()
1367
+
1368
+ json_stats = f"{os.path.splitext(log_path)[0]}.process_stats.json"
1369
+
1370
+ with open(json_stats, "w") as f:
1371
+ json.dump(
1372
+ process_stats_thread.stats_as_dict(),
1373
+ f,
1374
+ indent=4,
1375
+ )
1376
+
1377
+ result: Dict[str, Any] = {}
1378
+ returncode = process.wait()
1379
+ log_file.close()
1380
+ result["returncode"] = returncode
1381
+ result["log_path"] = log_path
1382
+
1383
+ for processor in output_processors:
1384
+ result[processor.key] = processor.result()
1385
+
1386
+ if check and returncode != 0:
1387
+ if returncode > 0:
1388
+ self.err("Subprocess had a non-zero exit.")
1389
+ concatenated = ""
1390
+ for line in line_buffer:
1391
+ concatenated += line
1392
+ if concatenated.strip() != "":
1393
+ self.err(
1394
+ f"Last {len(line_buffer)} line(s):\n" + escape(concatenated)
1395
+ )
1396
+ self.err(
1397
+ f"Full log file: {link_start}'{os.path.relpath(log_path)}'{link_end}"
1398
+ )
1399
+ raise subprocess.CalledProcessError(returncode, process.args)
1400
+
1401
+ return result
1402
+
1403
+ @protected
1404
+ def extract_env(self, kwargs) -> Tuple[dict, Dict[str, str]]:
1405
+ """
1406
+ An assisting function: Given a ``kwargs`` object, it does the following:
1407
+
1408
+ * If the kwargs object has an "env" variable, it separates it into
1409
+ its own variable.
1410
+ * If the kwargs object has no "env" variable, a new "env" dictionary
1411
+ is created based on the current environment.
1412
+
1413
+ :param kwargs: A Python keyword arguments object.
1414
+ :returns (kwargs, env): A kwargs without an ``env`` object, and an isolated ``env`` object.
1415
+ """
1416
+ env = kwargs.get("env")
1417
+ if env is None:
1418
+ env = os.environ.copy()
1419
+ else:
1420
+ kwargs = kwargs.copy()
1421
+ del kwargs["env"]
1422
+ return (kwargs, env)
1423
+
1424
+ @classmethod
1425
+ def with_id(Self, id: str) -> Type["Step"]:
1426
+ """
1427
+ Syntactic sugar for creating a subclass of a step with a different ID.
1428
+
1429
+ Useful in flows, where you want different IDs for different instance of the
1430
+ same step.
1431
+ """
1432
+ return type(
1433
+ Self.__name__,
1434
+ (Self,),
1435
+ {"id": id, "_implementation_id": Self.get_implementation_id()},
1436
+ )
1437
+
1438
+ class StepFactory(object):
1439
+ """
1440
+ A factory singleton for Steps, allowing steps types to be registered and then
1441
+ retrieved by name.
1442
+
1443
+ See https://en.wikipedia.org/wiki/Factory_(object-oriented_programming) for
1444
+ a primer.
1445
+ """
1446
+
1447
+ __registry: ClassVar[Dict[str, Type[Step]]] = {}
1448
+
1449
+ @classmethod
1450
+ def from_step_config(
1451
+ Self, step_config_path: Union[Config, str, os.PathLike]
1452
+ ) -> Tuple[Optional[str], Optional[Type[Step]]]:
1453
+ if isinstance(step_config_path, Config):
1454
+ step_id = Config.meta.step
1455
+ else:
1456
+ config_dict = json.load(open(step_config_path, encoding="utf8"))
1457
+ meta = config_dict.get("meta") or {}
1458
+ step_id = meta.get("step")
1459
+ if step_id is None:
1460
+ return (None, None)
1461
+ step_id = str(step_id)
1462
+ return (step_id, Self.get(step_id))
1463
+
1464
+ @classmethod
1465
+ def register(Self) -> Callable[[Type[Step]], Type[Step]]:
1466
+ """
1467
+ Adds a step type to the registry using its :attr:`Step.id` attribute.
1468
+ """
1469
+
1470
+ def decorator(cls: Type[Step]) -> Type[Step]:
1471
+ if cls.id == NotImplemented:
1472
+ raise RuntimeError(
1473
+ f"Abstract step {cls} without property .id cannot be registered."
1474
+ )
1475
+ Self.__registry[cls.id.lower()] = cls
1476
+ return cls
1477
+
1478
+ return decorator
1479
+
1480
+ @classmethod
1481
+ def get(Self, name: str) -> Optional[Type[Step]]:
1482
+ """
1483
+ Retrieves a Step type from the registry using a lookup string.
1484
+
1485
+ :param name: The registered name of the Step. Case-insensitive.
1486
+ """
1487
+ return Self.__registry.get(name.lower())
1488
+
1489
+ @classmethod
1490
+ def list(Self) -> List[str]:
1491
+ """
1492
+ :returns: A list of IDs of all registered names.
1493
+ """
1494
+ return [cls.id for cls in Self.__registry.values()]
1495
+
1496
+ factory = StepFactory
1497
+
1498
+
1499
+ class CompositeStep(Step):
1500
+ """
1501
+ A step composed of other steps, run sequentially. The steps are intended
1502
+ to run as a unit within a flow and cannot be run separately.
1503
+
1504
+ Composite steps are currently considered an internal object that is not
1505
+ ready to be part of the API. The API may change at any time for any reason.
1506
+
1507
+ ``inputs`` and ``config_vars`` are automatically generated based on the
1508
+ constituent steps.
1509
+
1510
+ ``outputs`` may be set explicitly. If not set, it is automatically generated
1511
+ based on the constituent steps.
1512
+ """
1513
+
1514
+ Steps: List[Type[Step]] = []
1515
+
1516
+ def __init_subclass__(Self):
1517
+ super().__init_subclass__()
1518
+ available_inputs = set()
1519
+
1520
+ input_set: Set[DesignFormat] = set()
1521
+ output_set: Set[DesignFormat] = set()
1522
+ config_var_dict: Dict[str, Variable] = {}
1523
+ for step in Self.Steps:
1524
+ for input in step.inputs:
1525
+ if input not in available_inputs:
1526
+ input_set.add(input)
1527
+ available_inputs.add(input)
1528
+ for output in step.outputs:
1529
+ available_inputs.add(output)
1530
+ output_set.add(output)
1531
+ for cvar in step.config_vars:
1532
+ if existing := config_var_dict.get(cvar.name):
1533
+ if existing != cvar:
1534
+ raise TypeError(
1535
+ f"Internal error: composite step has mismatching config_vars: {cvar.name} contradicts an earlier declaration"
1536
+ )
1537
+ else:
1538
+ config_var_dict[cvar.name] = cvar
1539
+ Self.inputs = list(input_set)
1540
+ if Self.outputs == NotImplemented: # Allow for setting explicit outputs
1541
+ Self.outputs = list(output_set)
1542
+ Self.config_vars = list(config_var_dict.values())
1543
+
1544
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1545
+ state = state_in
1546
+ step_count = len(self.Steps)
1547
+ ordinal_length = len(str(step_count - 1))
1548
+ for i, Step in enumerate(self.Steps):
1549
+ step = Step(self.config, state)
1550
+ step_dir = os.path.join(
1551
+ self.step_dir, f"{str(i + 1).zfill(ordinal_length)}-{slugify(step.id)}"
1552
+ )
1553
+ state = step.start(
1554
+ toolbox=self.toolbox,
1555
+ step_dir=step_dir,
1556
+ _no_rule=True,
1557
+ )
1558
+
1559
+ views_updates: dict = {}
1560
+ metrics_updates: dict = {}
1561
+ for key in state:
1562
+ if (
1563
+ state_in.get(key) != state.get(key)
1564
+ and DesignFormat.by_id(key) in self.outputs
1565
+ ):
1566
+ views_updates[key] = state[key]
1567
+ for key in state.metrics:
1568
+ if state_in.metrics.get(key) != state.metrics.get(key):
1569
+ metrics_updates[key] = state.metrics[key]
1570
+
1571
+ return views_updates, metrics_updates