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
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
import re
|
|
15
|
+
import textwrap
|
|
16
|
+
from enum import IntEnum
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import (
|
|
19
|
+
List,
|
|
20
|
+
Mapping,
|
|
21
|
+
Tuple,
|
|
22
|
+
Dict,
|
|
23
|
+
Any,
|
|
24
|
+
Iterable,
|
|
25
|
+
Optional,
|
|
26
|
+
Union,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .metric import Metric, MetricAggregator, MetricComparisonResult
|
|
30
|
+
from ..misc import Filter
|
|
31
|
+
|
|
32
|
+
modifier_rx = re.compile(r"([\w\-]+)\:([\w\-]+)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TableVerbosity(IntEnum):
|
|
36
|
+
"""
|
|
37
|
+
The verbosity of the table: whether to include everything, just changes, only
|
|
38
|
+
bad changes or only critical change. Or just nothing.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
NONE = 0
|
|
42
|
+
CRITICAL = 1
|
|
43
|
+
WORSE = 2
|
|
44
|
+
CHANGED = 3
|
|
45
|
+
ALL = 4
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_metric_modifiers(metric_name: str) -> Tuple[str, Mapping[str, str]]:
|
|
49
|
+
"""
|
|
50
|
+
Parses a metric name into a base and modifiers as specified in
|
|
51
|
+
the METRICS2.1 naming convention.
|
|
52
|
+
|
|
53
|
+
:param metric_name: The name of the metric as generated by a utility.
|
|
54
|
+
:returns: A tuple of the base part as a string, then the modifiers as
|
|
55
|
+
a key-value mapping.
|
|
56
|
+
"""
|
|
57
|
+
mn_mut = metric_name.split("__")
|
|
58
|
+
modifiers = {}
|
|
59
|
+
while ":" in mn_mut[-1]:
|
|
60
|
+
key, value = mn_mut.pop().split(":", maxsplit=1)
|
|
61
|
+
modifiers[key] = value
|
|
62
|
+
return "__".join(mn_mut), {k: modifiers[k] for k in reversed(modifiers)}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def aggregate_metrics(
|
|
66
|
+
input: Mapping[str, Any],
|
|
67
|
+
aggregator_by_metric: Optional[
|
|
68
|
+
Mapping[str, Union[MetricAggregator, Metric]]
|
|
69
|
+
] = None,
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Takes a set of metrics generated according to the METRICS2.1 naming
|
|
73
|
+
convention.
|
|
74
|
+
|
|
75
|
+
:param input: A mapping of strings to values of metrics.
|
|
76
|
+
:param aggregator_by_metric: A mapping of metric names to either:
|
|
77
|
+
- A tuple of the initial accumulator and reducer to aggregate the values from all modifier metrics
|
|
78
|
+
- A :class:`Metric` class
|
|
79
|
+
:returns: A tuple of the base part as a string, then the modifiers as
|
|
80
|
+
a key-value mapping.
|
|
81
|
+
"""
|
|
82
|
+
if aggregator_by_metric is None:
|
|
83
|
+
aggregator_by_metric = Metric.by_name
|
|
84
|
+
|
|
85
|
+
aggregated: Dict[str, Any] = {}
|
|
86
|
+
for name, value in input.items():
|
|
87
|
+
metric_name, modifiers = parse_metric_modifiers(name)
|
|
88
|
+
if len(modifiers) < 1:
|
|
89
|
+
# No modifiers = final aggregate, don't double-represent in sums
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
modifier_names = list(modifiers.keys())
|
|
93
|
+
dont_aggregate: Iterable[str] = []
|
|
94
|
+
entry = aggregator_by_metric.get(metric_name)
|
|
95
|
+
if isinstance(entry, Metric):
|
|
96
|
+
dont_aggregate = entry.dont_aggregate or []
|
|
97
|
+
entry = entry.aggregator
|
|
98
|
+
|
|
99
|
+
if entry is None:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
if len(set(modifier_names).intersection(set(dont_aggregate))):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
metric_name_so_far = metric_name
|
|
106
|
+
for modifier in modifier_names:
|
|
107
|
+
start, aggregation_fn = entry
|
|
108
|
+
current = aggregated.get(metric_name_so_far) or start
|
|
109
|
+
aggregated[metric_name_so_far] = aggregation_fn([current, value])
|
|
110
|
+
metric_name_so_far += f"__{modifier}:{modifiers[modifier]}"
|
|
111
|
+
|
|
112
|
+
final_values = dict(input)
|
|
113
|
+
final_values.update(aggregated)
|
|
114
|
+
return final_values
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _key_from_metrics(fields: Iterable[str], metric: str) -> List[str]:
|
|
118
|
+
base, modifiers = parse_metric_modifiers(metric)
|
|
119
|
+
result = []
|
|
120
|
+
for field in fields:
|
|
121
|
+
if field == "":
|
|
122
|
+
result.append(base)
|
|
123
|
+
else:
|
|
124
|
+
result.append(modifiers.get(field, ""))
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MetricDiff(object):
|
|
129
|
+
"""
|
|
130
|
+
Aggregates a number of ``MetricComparisonResult`` and allows a number of
|
|
131
|
+
functions to be performed on them.
|
|
132
|
+
|
|
133
|
+
:param differences: The metric comparison results.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class MetricStatistics:
|
|
138
|
+
"""
|
|
139
|
+
A glorified namespace encapsulating a number of statistics of
|
|
140
|
+
:class:`MetricDiff`.
|
|
141
|
+
|
|
142
|
+
Should be generated using :meth:`MetricDiff.stats`.
|
|
143
|
+
|
|
144
|
+
:param better: The number of datapoints that represent a positive change.
|
|
145
|
+
:param worse: The number of datapoints that represent a negative change.
|
|
146
|
+
:param critical: The number of changes for critical metrics.
|
|
147
|
+
:param unchanged: Values that are unchanged.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
better: int = 0
|
|
151
|
+
worse: int = 0
|
|
152
|
+
critical: int = 0
|
|
153
|
+
unchanged: int = 0
|
|
154
|
+
|
|
155
|
+
differences: List[MetricComparisonResult]
|
|
156
|
+
|
|
157
|
+
def __init__(self, differences: Iterable[MetricComparisonResult]) -> None:
|
|
158
|
+
self.differences = list(differences)
|
|
159
|
+
|
|
160
|
+
def render_md(
|
|
161
|
+
self,
|
|
162
|
+
sort_by: Optional[Iterable[str]] = None,
|
|
163
|
+
table_verbosity: TableVerbosity = TableVerbosity.ALL,
|
|
164
|
+
) -> str:
|
|
165
|
+
"""
|
|
166
|
+
:param sort_by: A list of tuples corresponding to modifiers to sort
|
|
167
|
+
metrics ascendingly by.
|
|
168
|
+
:param table_verbosity: The verbosity of the table: whether to include everything, just changes, only bad changes or only critical changes. Or just nothing.
|
|
169
|
+
:returns: A table of the differences in Markdown format.
|
|
170
|
+
"""
|
|
171
|
+
if table_verbosity == TableVerbosity.NONE:
|
|
172
|
+
return ""
|
|
173
|
+
|
|
174
|
+
differences = self.differences
|
|
175
|
+
if fields := sort_by:
|
|
176
|
+
differences = sorted(
|
|
177
|
+
differences,
|
|
178
|
+
key=lambda x: _key_from_metrics(fields, x.metric_name), # type: ignore # (mypy bug)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
table = ""
|
|
182
|
+
|
|
183
|
+
changed = []
|
|
184
|
+
worse = []
|
|
185
|
+
critical = []
|
|
186
|
+
remaining = []
|
|
187
|
+
|
|
188
|
+
for row in differences:
|
|
189
|
+
if row.critical is True:
|
|
190
|
+
critical.append(row)
|
|
191
|
+
elif row.better is False:
|
|
192
|
+
worse.append(row)
|
|
193
|
+
elif row.is_changed():
|
|
194
|
+
changed.append(row)
|
|
195
|
+
else:
|
|
196
|
+
remaining.append(row)
|
|
197
|
+
|
|
198
|
+
listed_differences: List[MetricComparisonResult] = []
|
|
199
|
+
if table_verbosity >= TableVerbosity.CRITICAL:
|
|
200
|
+
listed_differences += critical
|
|
201
|
+
if table_verbosity >= TableVerbosity.WORSE:
|
|
202
|
+
listed_differences += worse
|
|
203
|
+
if table_verbosity >= TableVerbosity.CHANGED:
|
|
204
|
+
listed_differences += changed
|
|
205
|
+
if table_verbosity >= TableVerbosity.ALL:
|
|
206
|
+
listed_differences += remaining
|
|
207
|
+
|
|
208
|
+
if len(listed_differences) > 0:
|
|
209
|
+
table = textwrap.dedent(
|
|
210
|
+
f"""
|
|
211
|
+
| {'Metric':<70} | {'Before':<10} | {'After':<10} | {'Delta':<20} |
|
|
212
|
+
| {'-':<70} | {'-':<10} | {'-':<10} | {'-':<20} |
|
|
213
|
+
"""
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
for row in listed_differences:
|
|
217
|
+
before, after, delta = row.format_values()
|
|
218
|
+
emoji = ""
|
|
219
|
+
if row.better is not None:
|
|
220
|
+
if row.better:
|
|
221
|
+
emoji = " ⭕"
|
|
222
|
+
else:
|
|
223
|
+
emoji = " ❗"
|
|
224
|
+
if row.critical and row.is_changed():
|
|
225
|
+
emoji = " ‼️"
|
|
226
|
+
table += f"| {row.metric_name:<70} | {before:<10} | {after:<10} | {f'{delta}{emoji}':<20} |\n"
|
|
227
|
+
|
|
228
|
+
return table
|
|
229
|
+
|
|
230
|
+
def stats(self) -> MetricStatistics:
|
|
231
|
+
"""
|
|
232
|
+
:returns: A :class:`MetricStatistics` object based on this aggregate.
|
|
233
|
+
"""
|
|
234
|
+
stats = MetricDiff.MetricStatistics()
|
|
235
|
+
for row in self.differences:
|
|
236
|
+
if not row.is_changed():
|
|
237
|
+
stats.unchanged += 1
|
|
238
|
+
elif row.better is not None:
|
|
239
|
+
if row.better:
|
|
240
|
+
stats.better += 1
|
|
241
|
+
else:
|
|
242
|
+
stats.worse += 1
|
|
243
|
+
if row.critical:
|
|
244
|
+
stats.critical += 1
|
|
245
|
+
return stats
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def from_metrics(
|
|
249
|
+
Self,
|
|
250
|
+
gold: dict,
|
|
251
|
+
new: dict,
|
|
252
|
+
significant_figures: int,
|
|
253
|
+
filter: Filter = Filter(["*"]),
|
|
254
|
+
) -> "MetricDiff":
|
|
255
|
+
"""
|
|
256
|
+
Creates a :class:`MetricDiff` object from two sets of metrics.
|
|
257
|
+
|
|
258
|
+
:param gold: The "gold-standard" metrics to compare against
|
|
259
|
+
:param new: The metrics being evaluated
|
|
260
|
+
:param filter: A :class:`Filter` for the names of the metrics to include
|
|
261
|
+
or exclude certain metrics.
|
|
262
|
+
:returns: The aggregate of the differences between gold and good
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def generator(g, n):
|
|
266
|
+
for metric in filter.filter(sorted(n.keys())):
|
|
267
|
+
if metric not in g:
|
|
268
|
+
continue
|
|
269
|
+
base_metric, modifiers = parse_metric_modifiers(metric)
|
|
270
|
+
lhs_value, rhs_value = g[metric], n[metric]
|
|
271
|
+
if type(lhs_value) != type(rhs_value):
|
|
272
|
+
lhs_value = type(rhs_value)(lhs_value)
|
|
273
|
+
|
|
274
|
+
if metric_object := Metric.by_name.get(base_metric):
|
|
275
|
+
yield metric_object.compare(
|
|
276
|
+
lhs_value, rhs_value, significant_figures, modifiers=modifiers
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return MetricDiff(generator(gold, new))
|
librelane/common/misc.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
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
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import glob
|
|
17
|
+
import gzip
|
|
18
|
+
import typing
|
|
19
|
+
import fnmatch
|
|
20
|
+
import pathlib
|
|
21
|
+
import unicodedata
|
|
22
|
+
from math import inf
|
|
23
|
+
from typing import (
|
|
24
|
+
Any,
|
|
25
|
+
Generator,
|
|
26
|
+
Iterable,
|
|
27
|
+
List,
|
|
28
|
+
TypeVar,
|
|
29
|
+
Optional,
|
|
30
|
+
SupportsFloat,
|
|
31
|
+
Union,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
import httpx
|
|
35
|
+
|
|
36
|
+
from .types import AnyPath, Path
|
|
37
|
+
from ..__version__ import __version__
|
|
38
|
+
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def idem(obj: T, *args, **kwargs) -> T:
|
|
43
|
+
"""
|
|
44
|
+
:returns: the parameter ``obj`` unchanged. Useful for some lambdas.
|
|
45
|
+
"""
|
|
46
|
+
return obj
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_librelane_root() -> str:
|
|
50
|
+
"""
|
|
51
|
+
Returns the root LibreLane folder, i.e., the folder containing the
|
|
52
|
+
``__init__.py``.
|
|
53
|
+
"""
|
|
54
|
+
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_script_dir() -> str:
|
|
58
|
+
"""
|
|
59
|
+
Gets the LibreLane tool `scripts` directory.
|
|
60
|
+
|
|
61
|
+
:meta private:
|
|
62
|
+
"""
|
|
63
|
+
return os.path.join(
|
|
64
|
+
get_librelane_root(),
|
|
65
|
+
"scripts",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_opdks_rev() -> str:
|
|
70
|
+
"""
|
|
71
|
+
Gets the Open_PDKs revision confirmed compatible with this version of LibreLane.
|
|
72
|
+
"""
|
|
73
|
+
return (
|
|
74
|
+
open(os.path.join(get_librelane_root(), "open_pdks_rev"), encoding="utf8")
|
|
75
|
+
.read()
|
|
76
|
+
.strip()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# The following code snippet has been adapted under the following license:
|
|
81
|
+
#
|
|
82
|
+
# Copyright (c) Django Software Foundation and individual contributors.
|
|
83
|
+
# All rights reserved.
|
|
84
|
+
|
|
85
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
86
|
+
# are permitted provided that the following conditions are met:
|
|
87
|
+
|
|
88
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
89
|
+
# this list of conditions and the following disclaimer.
|
|
90
|
+
|
|
91
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
|
92
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
93
|
+
# documentation and/or other materials provided with the distribution.
|
|
94
|
+
|
|
95
|
+
# 3. Neither the name of Django nor the names of its contributors may be used
|
|
96
|
+
# to endorse or promote products derived from this software without
|
|
97
|
+
# specific prior written permission.
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
101
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
102
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
103
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
104
|
+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
105
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
106
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
107
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
108
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
109
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
110
|
+
def slugify(value: str, lower: bool = False) -> str:
|
|
111
|
+
"""
|
|
112
|
+
:param value: Input string
|
|
113
|
+
:returns: The input string converted to lower case, with all characters
|
|
114
|
+
except alphanumerics, underscores and hyphens removed, and spaces and\
|
|
115
|
+
dots converted into hyphens.
|
|
116
|
+
|
|
117
|
+
Leading and trailing whitespace is stripped.
|
|
118
|
+
"""
|
|
119
|
+
if lower:
|
|
120
|
+
value = value.lower()
|
|
121
|
+
value = (
|
|
122
|
+
unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
|
|
123
|
+
)
|
|
124
|
+
value = re.sub(r"[^\w\s\-\.]", "", value).strip().lower()
|
|
125
|
+
return re.sub(r"[\s\.]+", "-", value)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def protected(method):
|
|
129
|
+
"""A decorator to indicate protected methods.
|
|
130
|
+
|
|
131
|
+
It dynamically adds a statement to the effect in the docstring as well
|
|
132
|
+
as setting an attribute, ``protected``, to ``True``, but has no other effects.
|
|
133
|
+
|
|
134
|
+
:param f: Method to mark as protected
|
|
135
|
+
"""
|
|
136
|
+
if method.__doc__ is None:
|
|
137
|
+
method.__doc__ = ""
|
|
138
|
+
method.__doc__ = "**protected**\n" + method.__doc__
|
|
139
|
+
|
|
140
|
+
setattr(method, "protected", True)
|
|
141
|
+
return method
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
final = typing.final
|
|
145
|
+
final.__doc__ = """A decorator to indicate final methods and final classes.
|
|
146
|
+
|
|
147
|
+
Use this decorator to indicate to type checkers that the decorated
|
|
148
|
+
method cannot be overridden, and decorated class cannot be subclassed.
|
|
149
|
+
For example:
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
.. code-block:: python
|
|
153
|
+
|
|
154
|
+
class Base:
|
|
155
|
+
@final
|
|
156
|
+
def done(self) -> None:
|
|
157
|
+
...
|
|
158
|
+
class Sub(Base):
|
|
159
|
+
def done(self) -> None: # Error reported by type checker
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
@final
|
|
163
|
+
class Leaf:
|
|
164
|
+
...
|
|
165
|
+
class Other(Leaf): # Error reported by type checker
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
There is no runtime checking of these properties.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def mkdirp(path: typing.Union[str, os.PathLike]):
|
|
173
|
+
"""
|
|
174
|
+
Attempts to create a directory and all of its parents.
|
|
175
|
+
|
|
176
|
+
Does not fail if the directory already exists, however, it does fail
|
|
177
|
+
if it is unable to create any of the components and/or if the path
|
|
178
|
+
already exists as a file.
|
|
179
|
+
|
|
180
|
+
:param path: A filesystem path for the directory
|
|
181
|
+
"""
|
|
182
|
+
return pathlib.Path(path).mkdir(parents=True, exist_ok=True)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class zip_first(object):
|
|
186
|
+
"""
|
|
187
|
+
Works like ``zip_longest`` if |a| > |b| and ``zip`` if |a| <= |b|.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, a: Iterable, b: Iterable, fillvalue: Any) -> None:
|
|
191
|
+
self.a = a
|
|
192
|
+
self.b = b
|
|
193
|
+
self.fillvalue = fillvalue
|
|
194
|
+
|
|
195
|
+
def __iter__(self):
|
|
196
|
+
self.iter_a = iter(self.a)
|
|
197
|
+
self.iter_b = iter(self.b)
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def __next__(self):
|
|
201
|
+
a = next(self.iter_a)
|
|
202
|
+
b = self.fillvalue
|
|
203
|
+
try:
|
|
204
|
+
b = next(self.iter_b)
|
|
205
|
+
except StopIteration:
|
|
206
|
+
pass
|
|
207
|
+
return (a, b)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def format_size(byte_count: int) -> str:
|
|
211
|
+
units = [
|
|
212
|
+
"B",
|
|
213
|
+
"KiB",
|
|
214
|
+
"MiB",
|
|
215
|
+
"GiB",
|
|
216
|
+
"TiB",
|
|
217
|
+
"PiB",
|
|
218
|
+
"EiB",
|
|
219
|
+
# TODO: update LibreLane when zebibytes are a thing
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
tracker = 0
|
|
223
|
+
so_far = byte_count
|
|
224
|
+
while (so_far // 1024) > 0 and tracker < (len(units) - 1):
|
|
225
|
+
tracker += 1
|
|
226
|
+
so_far //= 1024
|
|
227
|
+
|
|
228
|
+
return f"{so_far}{units[tracker]}"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def format_elapsed_time(elapsed_seconds: SupportsFloat) -> str:
|
|
232
|
+
"""
|
|
233
|
+
:param elapsed_seconds: Total time elapsed in seconds
|
|
234
|
+
:returns: A string in the format ``{hours}:{minutes}:{seconds}:{milliseconds}``
|
|
235
|
+
"""
|
|
236
|
+
elapsed_seconds = float(elapsed_seconds)
|
|
237
|
+
|
|
238
|
+
hours = int(elapsed_seconds // 3600)
|
|
239
|
+
leftover = elapsed_seconds % 3600
|
|
240
|
+
|
|
241
|
+
minutes = int(leftover // 60)
|
|
242
|
+
leftover = leftover % 60
|
|
243
|
+
|
|
244
|
+
seconds = int(leftover // 1)
|
|
245
|
+
milliseconds = int((leftover % 1) * 1000)
|
|
246
|
+
|
|
247
|
+
return f"{hours:02}:{minutes:02}:{seconds:02}.{milliseconds:03}"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class Filter(object):
|
|
251
|
+
"""
|
|
252
|
+
Encapsulates commonly used wildcard-based filtering functions into an object.
|
|
253
|
+
|
|
254
|
+
:param filters: A list of a wildcards supporting the
|
|
255
|
+
`fnmatch spec <https://docs.python.org/3.10/library/fnmatch.html>`_.
|
|
256
|
+
|
|
257
|
+
The wildcards will be split into an "allow" and "deny" list based on whether
|
|
258
|
+
the filter is prefixed with a ``!``.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def __init__(self, filters: Iterable[str]):
|
|
262
|
+
self.allow = []
|
|
263
|
+
self.deny = []
|
|
264
|
+
for filter in filters:
|
|
265
|
+
if filter.startswith("!"):
|
|
266
|
+
self.deny.append(filter[1:])
|
|
267
|
+
else:
|
|
268
|
+
self.allow.append(filter)
|
|
269
|
+
|
|
270
|
+
def get_matching_wildcards(self, input: str) -> Generator[str, Any, None]:
|
|
271
|
+
"""
|
|
272
|
+
:param input: An input to match wildcards against.
|
|
273
|
+
:returns: An iterable object for *all* wildcards in the allow list
|
|
274
|
+
accepting ``input``, and *all* wildcards in the deny list rejecting
|
|
275
|
+
``input``.
|
|
276
|
+
"""
|
|
277
|
+
for wildcard in self.allow:
|
|
278
|
+
if fnmatch.fnmatch(input, wildcard):
|
|
279
|
+
yield wildcard
|
|
280
|
+
for wildcard in self.deny:
|
|
281
|
+
if not fnmatch.fnmatch(input, wildcard):
|
|
282
|
+
yield wildcard
|
|
283
|
+
|
|
284
|
+
def match(self, input: str) -> bool:
|
|
285
|
+
"""
|
|
286
|
+
:param input: An input string to either accept or reject
|
|
287
|
+
:returns: A boolean indicating whether the input:
|
|
288
|
+
* Has matched at least one wildcard in the allow list
|
|
289
|
+
* Has matched exactly 0 inputs in the deny list
|
|
290
|
+
"""
|
|
291
|
+
allowed = False
|
|
292
|
+
for wildcard in self.allow:
|
|
293
|
+
if fnmatch.fnmatch(input, wildcard):
|
|
294
|
+
allowed = True
|
|
295
|
+
break
|
|
296
|
+
for wildcard in self.deny:
|
|
297
|
+
if fnmatch.fnmatch(input, wildcard):
|
|
298
|
+
allowed = False
|
|
299
|
+
break
|
|
300
|
+
return allowed
|
|
301
|
+
|
|
302
|
+
def filter(
|
|
303
|
+
self,
|
|
304
|
+
inputs: Iterable[str],
|
|
305
|
+
) -> Generator[str, Any, None]:
|
|
306
|
+
"""
|
|
307
|
+
:param inputs: A series of inputs to filter according to the wildcards.
|
|
308
|
+
:returns: An iterable object of any values in ``inputs`` that:
|
|
309
|
+
* Have matched at least one wildcard in the allow list
|
|
310
|
+
* Have matched exactly 0 inputs in the deny list
|
|
311
|
+
"""
|
|
312
|
+
for input in inputs:
|
|
313
|
+
if self.match(input):
|
|
314
|
+
yield input
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def get_latest_file(in_path: Union[str, os.PathLike], filename: str) -> Optional[Path]:
|
|
318
|
+
"""
|
|
319
|
+
:param in_path: A directory to search in
|
|
320
|
+
:param filename: The final filename
|
|
321
|
+
:returns: The latest file matching the parameters, by modification time
|
|
322
|
+
"""
|
|
323
|
+
glob_results = glob.glob(os.path.join(in_path, "**", filename), recursive=True)
|
|
324
|
+
latest_time = -inf
|
|
325
|
+
latest_json = None
|
|
326
|
+
for result in glob_results:
|
|
327
|
+
time = os.path.getmtime(result)
|
|
328
|
+
if time > latest_time:
|
|
329
|
+
latest_time = time
|
|
330
|
+
latest_json = Path(result)
|
|
331
|
+
|
|
332
|
+
return latest_json
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_httpx_session(token: Optional[str] = None) -> httpx.Client:
|
|
336
|
+
"""
|
|
337
|
+
Creates an ``httpx`` session client that follows redirects and has the
|
|
338
|
+
User-Agent header set to ``librelane/{__version__}``.
|
|
339
|
+
|
|
340
|
+
:param token: If this parameter is non-None and not empty, another header,
|
|
341
|
+
Authorization: Bearer {token}, is included.
|
|
342
|
+
:returns: The created client
|
|
343
|
+
"""
|
|
344
|
+
session = httpx.Client(follow_redirects=True)
|
|
345
|
+
headers_raw = {"User-Agent": f"librelane/{__version__}"}
|
|
346
|
+
if token is not None and token.strip() != "":
|
|
347
|
+
headers_raw["Authorization"] = f"Bearer {token}"
|
|
348
|
+
session.headers = httpx.Headers(headers_raw)
|
|
349
|
+
return session
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def process_list_file(from_file: AnyPath) -> List[str]:
|
|
353
|
+
"""
|
|
354
|
+
Convenience function to process text files in a ``.gitignore``-style format,
|
|
355
|
+
i.e., those where the lines may be:
|
|
356
|
+
|
|
357
|
+
* A list element
|
|
358
|
+
* A comment prefixed with ``#``
|
|
359
|
+
* Blank
|
|
360
|
+
|
|
361
|
+
:param from_file: The input text file.
|
|
362
|
+
:returns: A list of the strings listed in the file, ignoring lines
|
|
363
|
+
prefixed with a ``#`` and empty lines.
|
|
364
|
+
"""
|
|
365
|
+
excluded_cells = []
|
|
366
|
+
list_str = open(str(from_file), encoding="utf8").read()
|
|
367
|
+
for line in list_str.splitlines():
|
|
368
|
+
line = line.strip()
|
|
369
|
+
if line == "":
|
|
370
|
+
continue
|
|
371
|
+
if line[0] == "#":
|
|
372
|
+
continue
|
|
373
|
+
excluded_cells.append(line)
|
|
374
|
+
return excluded_cells
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _get_process_limit() -> int:
|
|
378
|
+
return int(os.getenv("_OPENLANE_MAX_CORES", os.cpu_count() or 1))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def gzopen(filename, mode="rt"):
|
|
382
|
+
"""
|
|
383
|
+
This method (tries to?) emulate the gzopen from the Linux Standard Base,
|
|
384
|
+
specifically this part:
|
|
385
|
+
|
|
386
|
+
If path refers to an uncompressed file, and mode refers to a read mode,
|
|
387
|
+
gzopen() shall attempt to open the file and return a gzFile object suitable
|
|
388
|
+
for reading directly from the file without any decompression.
|
|
389
|
+
|
|
390
|
+
gzip.open does not have this behavior.
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
g = gzip.open(filename, mode=mode)
|
|
394
|
+
# Incredibly, it won't actually try to figure out if it's a gzipped
|
|
395
|
+
# file until you try to read from it.
|
|
396
|
+
if "r" in mode:
|
|
397
|
+
g.read(1)
|
|
398
|
+
g.seek(0)
|
|
399
|
+
return g
|
|
400
|
+
except gzip.BadGzipFile:
|
|
401
|
+
g.close()
|
|
402
|
+
return open(filename, mode=mode)
|