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