siliconcompiler 0.34.2__py3-none-any.whl → 0.34.3__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.
- siliconcompiler/__init__.py +12 -5
- siliconcompiler/__main__.py +1 -7
- siliconcompiler/_metadata.py +1 -1
- siliconcompiler/apps/_common.py +104 -23
- siliconcompiler/apps/sc.py +4 -8
- siliconcompiler/apps/sc_dashboard.py +6 -4
- siliconcompiler/apps/sc_install.py +10 -6
- siliconcompiler/apps/sc_issue.py +7 -5
- siliconcompiler/apps/sc_remote.py +1 -1
- siliconcompiler/apps/sc_server.py +9 -14
- siliconcompiler/apps/sc_show.py +6 -5
- siliconcompiler/apps/smake.py +130 -94
- siliconcompiler/apps/utils/replay.py +4 -7
- siliconcompiler/apps/utils/summarize.py +3 -5
- siliconcompiler/asic.py +420 -0
- siliconcompiler/checklist.py +25 -2
- siliconcompiler/cmdlineschema.py +534 -0
- siliconcompiler/constraints/asic_component.py +2 -2
- siliconcompiler/constraints/asic_pins.py +2 -2
- siliconcompiler/constraints/asic_timing.py +3 -3
- siliconcompiler/core.py +7 -32
- siliconcompiler/data/templates/tcl/manifest.tcl.j2 +8 -0
- siliconcompiler/dependencyschema.py +89 -31
- siliconcompiler/design.py +176 -207
- siliconcompiler/filesetschema.py +250 -0
- siliconcompiler/flowgraph.py +274 -95
- siliconcompiler/fpga.py +124 -1
- siliconcompiler/library.py +218 -20
- siliconcompiler/metric.py +233 -20
- siliconcompiler/package/__init__.py +271 -50
- siliconcompiler/package/git.py +92 -16
- siliconcompiler/package/github.py +108 -12
- siliconcompiler/package/https.py +79 -16
- siliconcompiler/packageschema.py +88 -7
- siliconcompiler/pathschema.py +31 -2
- siliconcompiler/pdk.py +566 -1
- siliconcompiler/project.py +1095 -94
- siliconcompiler/record.py +38 -1
- siliconcompiler/remote/__init__.py +5 -2
- siliconcompiler/remote/client.py +11 -6
- siliconcompiler/remote/schema.py +5 -23
- siliconcompiler/remote/server.py +41 -54
- siliconcompiler/report/__init__.py +3 -3
- siliconcompiler/report/dashboard/__init__.py +48 -14
- siliconcompiler/report/dashboard/cli/__init__.py +99 -21
- siliconcompiler/report/dashboard/cli/board.py +364 -179
- siliconcompiler/report/dashboard/web/__init__.py +90 -12
- siliconcompiler/report/dashboard/web/components/__init__.py +219 -240
- siliconcompiler/report/dashboard/web/components/flowgraph.py +49 -26
- siliconcompiler/report/dashboard/web/components/graph.py +139 -100
- siliconcompiler/report/dashboard/web/layouts/__init__.py +29 -1
- siliconcompiler/report/dashboard/web/layouts/_common.py +38 -2
- siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph.py +39 -26
- siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_node_tab.py +50 -50
- siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_sac_tabs.py +49 -46
- siliconcompiler/report/dashboard/web/state.py +141 -14
- siliconcompiler/report/dashboard/web/utils/__init__.py +79 -16
- siliconcompiler/report/dashboard/web/utils/file_utils.py +74 -11
- siliconcompiler/report/dashboard/web/viewer.py +25 -1
- siliconcompiler/report/report.py +5 -2
- siliconcompiler/report/summary_image.py +29 -11
- siliconcompiler/scheduler/__init__.py +9 -1
- siliconcompiler/scheduler/docker.py +79 -1
- siliconcompiler/scheduler/run_node.py +35 -19
- siliconcompiler/scheduler/scheduler.py +208 -24
- siliconcompiler/scheduler/schedulernode.py +372 -46
- siliconcompiler/scheduler/send_messages.py +77 -29
- siliconcompiler/scheduler/slurm.py +76 -12
- siliconcompiler/scheduler/taskscheduler.py +140 -20
- siliconcompiler/schema/__init__.py +0 -2
- siliconcompiler/schema/baseschema.py +194 -38
- siliconcompiler/schema/journal.py +7 -4
- siliconcompiler/schema/namedschema.py +16 -10
- siliconcompiler/schema/parameter.py +55 -9
- siliconcompiler/schema/parametervalue.py +60 -0
- siliconcompiler/schema/safeschema.py +25 -2
- siliconcompiler/schema/schema_cfg.py +5 -5
- siliconcompiler/schema/utils.py +2 -2
- siliconcompiler/schema_obj.py +20 -3
- siliconcompiler/tool.py +979 -302
- siliconcompiler/tools/bambu/__init__.py +41 -0
- siliconcompiler/tools/builtin/concatenate.py +2 -2
- siliconcompiler/tools/builtin/minimum.py +2 -1
- siliconcompiler/tools/builtin/mux.py +2 -1
- siliconcompiler/tools/builtin/nop.py +2 -1
- siliconcompiler/tools/builtin/verify.py +2 -1
- siliconcompiler/tools/klayout/__init__.py +95 -0
- siliconcompiler/tools/openroad/__init__.py +289 -0
- siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +3 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +7 -2
- siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +8 -4
- siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +9 -5
- siliconcompiler/tools/openroad/scripts/common/write_images.tcl +5 -1
- siliconcompiler/tools/slang/__init__.py +1 -1
- siliconcompiler/tools/slang/elaborate.py +2 -1
- siliconcompiler/tools/vivado/scripts/sc_run.tcl +1 -1
- siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +8 -1
- siliconcompiler/tools/vivado/syn_fpga.py +6 -0
- siliconcompiler/tools/vivado/vivado.py +35 -2
- siliconcompiler/tools/vpr/__init__.py +150 -0
- siliconcompiler/tools/yosys/__init__.py +369 -1
- siliconcompiler/tools/yosys/scripts/procs.tcl +0 -1
- siliconcompiler/toolscripts/_tools.json +5 -10
- siliconcompiler/utils/__init__.py +66 -0
- siliconcompiler/utils/flowgraph.py +2 -2
- siliconcompiler/utils/issue.py +2 -1
- siliconcompiler/utils/logging.py +14 -0
- siliconcompiler/utils/multiprocessing.py +256 -0
- siliconcompiler/utils/showtools.py +10 -0
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/METADATA +5 -5
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/RECORD +115 -118
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/entry_points.txt +3 -0
- siliconcompiler/schema/cmdlineschema.py +0 -250
- siliconcompiler/toolscripts/rhel8/install-slang.sh +0 -40
- siliconcompiler/toolscripts/rhel9/install-slang.sh +0 -40
- siliconcompiler/toolscripts/ubuntu20/install-slang.sh +0 -47
- siliconcompiler/toolscripts/ubuntu22/install-slang.sh +0 -37
- siliconcompiler/toolscripts/ubuntu24/install-slang.sh +0 -37
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/WHEEL +0 -0
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/licenses/LICENSE +0 -0
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the core path resolution system for SiliconCompiler packages.
|
|
3
|
+
|
|
4
|
+
It provides a flexible mechanism to locate data sources, whether they are on
|
|
5
|
+
the local filesystem, remote servers, or part of a Python package. The system
|
|
6
|
+
is designed to be extensible, allowing new resolver types to be added as plugins.
|
|
7
|
+
It also includes robust caching and locking mechanisms for handling remote data
|
|
8
|
+
efficiently and safely in multi-process and multi-threaded environments.
|
|
9
|
+
"""
|
|
1
10
|
import contextlib
|
|
2
11
|
import functools
|
|
3
12
|
import hashlib
|
|
@@ -22,6 +31,7 @@ from siliconcompiler.utils import get_plugins
|
|
|
22
31
|
|
|
23
32
|
|
|
24
33
|
def path(chip, package):
|
|
34
|
+
"""DEPRECATED: Use chip.get_resolver(package).get_path() instead."""
|
|
25
35
|
import warnings
|
|
26
36
|
warnings.warn("The 'path' method has been deprecated",
|
|
27
37
|
DeprecationWarning)
|
|
@@ -34,10 +44,7 @@ def register_python_data_source(chip,
|
|
|
34
44
|
alternative_path,
|
|
35
45
|
alternative_ref=None,
|
|
36
46
|
python_module_path_append=None):
|
|
37
|
-
|
|
38
|
-
Helper function to register a python module as data source with an alternative in case
|
|
39
|
-
the module is not installed in an editable state
|
|
40
|
-
'''
|
|
47
|
+
"""DEPRECATED: Use PythonPathResolver.register_source() instead."""
|
|
41
48
|
import warnings
|
|
42
49
|
warnings.warn("The 'register_python_data_source' method was renamed "
|
|
43
50
|
"PythonPathResolver.register_source",
|
|
@@ -50,6 +57,20 @@ def register_python_data_source(chip,
|
|
|
50
57
|
|
|
51
58
|
|
|
52
59
|
class Resolver:
|
|
60
|
+
"""
|
|
61
|
+
Abstract base class for all data source resolvers.
|
|
62
|
+
|
|
63
|
+
This class defines the common interface for locating and accessing data
|
|
64
|
+
from various sources. It includes a caching mechanism to avoid redundant
|
|
65
|
+
resolutions within a single run.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
name (str): The name of the data package being resolved.
|
|
69
|
+
root (object): The root object (typically a Chip) providing context,
|
|
70
|
+
such as environment variables and the working directory.
|
|
71
|
+
source (str): The URI or path specifying the data source.
|
|
72
|
+
reference (str): A version, commit hash, or tag for remote sources.
|
|
73
|
+
"""
|
|
53
74
|
_RESOLVERS_LOCK = threading.Lock()
|
|
54
75
|
_RESOLVERS = {}
|
|
55
76
|
|
|
@@ -57,6 +78,9 @@ class Resolver:
|
|
|
57
78
|
__CACHE = {}
|
|
58
79
|
|
|
59
80
|
def __init__(self, name, root, source, reference=None):
|
|
81
|
+
"""
|
|
82
|
+
Initializes the Resolver.
|
|
83
|
+
"""
|
|
60
84
|
self.__name = name
|
|
61
85
|
self.__root = root
|
|
62
86
|
self.__source = source
|
|
@@ -71,6 +95,13 @@ class Resolver:
|
|
|
71
95
|
|
|
72
96
|
@staticmethod
|
|
73
97
|
def populate_resolvers():
|
|
98
|
+
"""
|
|
99
|
+
Scans for and registers all available resolver plugins.
|
|
100
|
+
|
|
101
|
+
This method populates the internal `_RESOLVERS` dictionary with both
|
|
102
|
+
built-in resolvers (file, key, python) and any resolvers provided
|
|
103
|
+
by external plugins.
|
|
104
|
+
"""
|
|
74
105
|
with Resolver._RESOLVERS_LOCK:
|
|
75
106
|
Resolver._RESOLVERS.clear()
|
|
76
107
|
|
|
@@ -86,6 +117,18 @@ class Resolver:
|
|
|
86
117
|
|
|
87
118
|
@staticmethod
|
|
88
119
|
def find_resolver(source):
|
|
120
|
+
"""
|
|
121
|
+
Finds the appropriate resolver class for a given source URI.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
source (str): The source URI (e.g., 'file:///path/to/file', 'git://...').
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Resolver: The resolver class capable of handling the source.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: If no suitable resolver is found for the URI scheme.
|
|
131
|
+
"""
|
|
89
132
|
if os.path.isabs(source):
|
|
90
133
|
return FileResolver
|
|
91
134
|
|
|
@@ -97,67 +140,89 @@ class Resolver:
|
|
|
97
140
|
if url.scheme in Resolver._RESOLVERS:
|
|
98
141
|
return Resolver._RESOLVERS[url.scheme]
|
|
99
142
|
|
|
100
|
-
raise ValueError(f"{source} is not supported")
|
|
143
|
+
raise ValueError(f"Source URI '{source}' is not supported")
|
|
101
144
|
|
|
102
145
|
@property
|
|
103
146
|
def name(self) -> str:
|
|
147
|
+
"""The name of the data package being resolved."""
|
|
104
148
|
return self.__name
|
|
105
149
|
|
|
106
150
|
@property
|
|
107
151
|
def root(self):
|
|
152
|
+
"""The root object (e.g., Chip) providing context."""
|
|
108
153
|
return self.__root
|
|
109
154
|
|
|
110
155
|
@property
|
|
111
156
|
def logger(self) -> logging.Logger:
|
|
157
|
+
"""The logger instance for this resolver."""
|
|
112
158
|
return self.__logger
|
|
113
159
|
|
|
114
160
|
@property
|
|
115
161
|
def source(self) -> str:
|
|
162
|
+
"""The URI or path specifying the data source."""
|
|
116
163
|
return self.__source
|
|
117
164
|
|
|
118
165
|
@property
|
|
119
166
|
def reference(self) -> str:
|
|
167
|
+
"""A version, commit hash, or tag for the source."""
|
|
120
168
|
return self.__reference
|
|
121
169
|
|
|
122
170
|
@property
|
|
123
171
|
def urlparse(self) -> url_parse.ParseResult:
|
|
172
|
+
"""The parsed URL of the source after environment variable expansion."""
|
|
124
173
|
return url_parse.urlparse(self.__resolve_env(self.source))
|
|
125
174
|
|
|
126
175
|
@property
|
|
127
176
|
def urlscheme(self) -> str:
|
|
177
|
+
"""The scheme of the source URL (e.g., 'file', 'git')."""
|
|
128
178
|
return self.urlparse.scheme
|
|
129
179
|
|
|
130
180
|
@property
|
|
131
181
|
def urlpath(self) -> str:
|
|
182
|
+
"""The path component of the source URL."""
|
|
132
183
|
return self.urlparse.netloc
|
|
133
184
|
|
|
134
185
|
@property
|
|
135
186
|
def changed(self) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Indicates if the resolved data has changed (e.g., was newly fetched).
|
|
189
|
+
|
|
190
|
+
This flag is reset to False after being read.
|
|
191
|
+
"""
|
|
136
192
|
change = self.__changed
|
|
137
193
|
self.__changed = False
|
|
138
194
|
return change
|
|
139
195
|
|
|
140
196
|
@property
|
|
141
197
|
def cache_id(self) -> str:
|
|
198
|
+
"""A unique ID for this resolver instance, used for caching."""
|
|
142
199
|
if self.__cacheid is None:
|
|
143
|
-
|
|
144
|
-
|
|
200
|
+
hash_obj = hashlib.sha1()
|
|
201
|
+
hash_obj.update(self.__source.encode())
|
|
145
202
|
if self.__reference:
|
|
146
|
-
|
|
203
|
+
hash_obj.update(self.__reference.encode())
|
|
147
204
|
else:
|
|
148
|
-
|
|
205
|
+
hash_obj.update("".encode())
|
|
149
206
|
|
|
150
|
-
self.__cacheid =
|
|
207
|
+
self.__cacheid = hash_obj.hexdigest()
|
|
151
208
|
return self.__cacheid
|
|
152
209
|
|
|
153
210
|
def set_changed(self):
|
|
211
|
+
"""Marks the resolved data as having been changed."""
|
|
154
212
|
self.__changed = True
|
|
155
213
|
|
|
156
214
|
def resolve(self):
|
|
215
|
+
"""
|
|
216
|
+
Abstract method to perform the actual data resolution.
|
|
217
|
+
|
|
218
|
+
Subclasses must implement this method to locate or fetch the data
|
|
219
|
+
and return its local path.
|
|
220
|
+
"""
|
|
157
221
|
raise NotImplementedError("child class must implement this")
|
|
158
222
|
|
|
159
223
|
@staticmethod
|
|
160
224
|
def __get_root_id(root):
|
|
225
|
+
"""Generates or retrieves a unique ID for a root object."""
|
|
161
226
|
STORAGE = "__Resolver_cache_id"
|
|
162
227
|
if not getattr(root, STORAGE, None):
|
|
163
228
|
setattr(root, STORAGE, uuid.uuid4().hex)
|
|
@@ -165,6 +230,17 @@ class Resolver:
|
|
|
165
230
|
|
|
166
231
|
@staticmethod
|
|
167
232
|
def get_cache(root, name: str = None):
|
|
233
|
+
"""
|
|
234
|
+
Gets a cached path for a given root object and resolver name.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
root: The root object (e.g., Chip).
|
|
238
|
+
name (str, optional): The name of the resolver cache to retrieve.
|
|
239
|
+
If None, returns a copy of the entire cache for the root.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
str or dict or None: The cached path, a copy of the cache, or None.
|
|
243
|
+
"""
|
|
168
244
|
with Resolver.__CACHE_LOCK:
|
|
169
245
|
root_id = Resolver.__get_root_id(root)
|
|
170
246
|
if root_id not in Resolver.__CACHE:
|
|
@@ -177,6 +253,14 @@ class Resolver:
|
|
|
177
253
|
|
|
178
254
|
@staticmethod
|
|
179
255
|
def set_cache(root, name: str, path: str):
|
|
256
|
+
"""
|
|
257
|
+
Sets a cached path for a given root object and resolver name.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
root: The root object (e.g., Chip).
|
|
261
|
+
name (str): The name of the resolver cache to set.
|
|
262
|
+
path (str): The path to cache.
|
|
263
|
+
"""
|
|
180
264
|
with Resolver.__CACHE_LOCK:
|
|
181
265
|
root_id = Resolver.__get_root_id(root)
|
|
182
266
|
if root_id not in Resolver.__CACHE:
|
|
@@ -185,19 +269,37 @@ class Resolver:
|
|
|
185
269
|
|
|
186
270
|
@staticmethod
|
|
187
271
|
def reset_cache(root):
|
|
272
|
+
"""
|
|
273
|
+
Resets the entire cache for a given root object.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
root: The root object whose cache will be cleared.
|
|
277
|
+
"""
|
|
188
278
|
with Resolver.__CACHE_LOCK:
|
|
189
279
|
root_id = Resolver.__get_root_id(root)
|
|
190
280
|
if root_id in Resolver.__CACHE:
|
|
191
281
|
del Resolver.__CACHE[root_id]
|
|
192
282
|
|
|
193
283
|
def get_path(self):
|
|
284
|
+
"""
|
|
285
|
+
Resolves the data source and returns its local path.
|
|
286
|
+
|
|
287
|
+
This method first checks the in-memory cache. If not found, it calls
|
|
288
|
+
the `resolve()` method and caches the result.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
str: The absolute path to the resolved data on the local filesystem.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
FileNotFoundError: If the resolved path does not exist.
|
|
295
|
+
"""
|
|
194
296
|
cache_path = Resolver.get_cache(self.__root, self.cache_id)
|
|
195
297
|
if cache_path:
|
|
196
298
|
return cache_path
|
|
197
299
|
|
|
198
300
|
path = self.resolve()
|
|
199
301
|
if not os.path.exists(path):
|
|
200
|
-
raise FileNotFoundError(f"Unable to locate {self.name} at {path}")
|
|
302
|
+
raise FileNotFoundError(f"Unable to locate '{self.name}' at {path}")
|
|
201
303
|
|
|
202
304
|
if self.changed:
|
|
203
305
|
self.logger.info(f'Saved {self.name} data to {path}')
|
|
@@ -208,6 +310,7 @@ class Resolver:
|
|
|
208
310
|
return path
|
|
209
311
|
|
|
210
312
|
def __resolve_env(self, path):
|
|
313
|
+
"""Expands environment variables and user home directory in a path."""
|
|
211
314
|
env_save = os.environ.copy()
|
|
212
315
|
|
|
213
316
|
if self.root:
|
|
@@ -225,12 +328,20 @@ class Resolver:
|
|
|
225
328
|
|
|
226
329
|
|
|
227
330
|
class RemoteResolver(Resolver):
|
|
331
|
+
"""
|
|
332
|
+
An abstract base class for resolvers that fetch data from remote sources.
|
|
333
|
+
|
|
334
|
+
This class extends `Resolver` with functionality for managing a persistent
|
|
335
|
+
on-disk cache in `~/.sc/cache` or a user-defined location. It implements
|
|
336
|
+
both thread-safe and process-safe locking to prevent race conditions when
|
|
337
|
+
multiple SC instances try to download the same resource simultaneously.
|
|
338
|
+
"""
|
|
228
339
|
_CACHE_LOCKS = {}
|
|
229
340
|
_CACHE_LOCK = threading.Lock()
|
|
230
341
|
|
|
231
342
|
def __init__(self, name, root, source, reference=None):
|
|
232
343
|
if reference is None:
|
|
233
|
-
raise ValueError(f'
|
|
344
|
+
raise ValueError(f'A reference (e.g., version, commit) is required for {name}')
|
|
234
345
|
|
|
235
346
|
super().__init__(name, root, source, reference)
|
|
236
347
|
|
|
@@ -239,13 +350,27 @@ class RemoteResolver(Resolver):
|
|
|
239
350
|
|
|
240
351
|
@property
|
|
241
352
|
def timeout(self):
|
|
353
|
+
"""The maximum time in seconds to wait for a lock."""
|
|
242
354
|
return self.__max_lock_wait
|
|
243
355
|
|
|
244
356
|
def set_timeout(self, value):
|
|
357
|
+
"""Sets the maximum time in seconds to wait for a lock."""
|
|
245
358
|
self.__max_lock_wait = value
|
|
246
359
|
|
|
247
360
|
@staticmethod
|
|
248
361
|
def determine_cache_dir(root) -> Path:
|
|
362
|
+
"""
|
|
363
|
+
Determines the directory for the on-disk cache.
|
|
364
|
+
|
|
365
|
+
The location is determined by ['option', 'cachedir'] if set, otherwise
|
|
366
|
+
it defaults to `~/.sc/cache`.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
root: The root Chip object.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Path: The path to the cache directory.
|
|
373
|
+
"""
|
|
249
374
|
default_path = os.path.join(Path.home(), '.sc', 'cache')
|
|
250
375
|
if not root:
|
|
251
376
|
return Path(default_path)
|
|
@@ -265,14 +390,17 @@ class RemoteResolver(Resolver):
|
|
|
265
390
|
|
|
266
391
|
@property
|
|
267
392
|
def cache_dir(self) -> Path:
|
|
393
|
+
"""The directory for the on-disk cache."""
|
|
268
394
|
return RemoteResolver.determine_cache_dir(self.root)
|
|
269
395
|
|
|
270
396
|
@property
|
|
271
397
|
def cache_name(self) -> str:
|
|
272
|
-
|
|
398
|
+
"""A unique name for the cached data directory."""
|
|
399
|
+
return f"{self.name}-{self.reference[0:16]}-{self.cache_id[0:16]}"
|
|
273
400
|
|
|
274
401
|
@property
|
|
275
402
|
def cache_path(self) -> Path:
|
|
403
|
+
"""The full path to the cached data directory."""
|
|
276
404
|
cache_dir = self.cache_dir
|
|
277
405
|
if not os.path.exists(cache_dir):
|
|
278
406
|
os.makedirs(cache_dir, exist_ok=True)
|
|
@@ -281,6 +409,7 @@ class RemoteResolver(Resolver):
|
|
|
281
409
|
|
|
282
410
|
@property
|
|
283
411
|
def lock_file(self) -> Path:
|
|
412
|
+
"""The path to the file used for inter-process locking."""
|
|
284
413
|
cache_dir = self.cache_dir
|
|
285
414
|
if not os.path.exists(cache_dir):
|
|
286
415
|
os.makedirs(cache_dir, exist_ok=True)
|
|
@@ -289,6 +418,9 @@ class RemoteResolver(Resolver):
|
|
|
289
418
|
|
|
290
419
|
@property
|
|
291
420
|
def sc_lock_file(self) -> Path:
|
|
421
|
+
"""
|
|
422
|
+
The path to a secondary lock file used as a fallback mechanism.
|
|
423
|
+
"""
|
|
292
424
|
cache_dir = self.cache_dir
|
|
293
425
|
if not os.path.exists(cache_dir):
|
|
294
426
|
os.makedirs(cache_dir, exist_ok=True)
|
|
@@ -296,6 +428,7 @@ class RemoteResolver(Resolver):
|
|
|
296
428
|
return self.cache_dir / f"{self.cache_name}.sc_lock"
|
|
297
429
|
|
|
298
430
|
def thread_lock(self):
|
|
431
|
+
"""Gets a threading.Lock specific to this resolver instance."""
|
|
299
432
|
with RemoteResolver._CACHE_LOCK:
|
|
300
433
|
if self.name not in RemoteResolver._CACHE_LOCKS:
|
|
301
434
|
RemoteResolver._CACHE_LOCKS[self.name] = threading.Lock()
|
|
@@ -303,6 +436,7 @@ class RemoteResolver(Resolver):
|
|
|
303
436
|
|
|
304
437
|
@contextlib.contextmanager
|
|
305
438
|
def __thread_lock(self):
|
|
439
|
+
"""A context manager for acquiring the thread lock with a timeout."""
|
|
306
440
|
lock = self.thread_lock()
|
|
307
441
|
lock_acquired = False
|
|
308
442
|
try:
|
|
@@ -326,6 +460,7 @@ class RemoteResolver(Resolver):
|
|
|
326
460
|
|
|
327
461
|
@contextlib.contextmanager
|
|
328
462
|
def __file_lock(self):
|
|
463
|
+
"""A context manager for acquiring the inter-process file lock."""
|
|
329
464
|
data_path_lock = InterProcessLock(self.lock_file)
|
|
330
465
|
lock_acquired = False
|
|
331
466
|
sc_data_path_lock = None
|
|
@@ -355,30 +490,54 @@ class RemoteResolver(Resolver):
|
|
|
355
490
|
|
|
356
491
|
if not lock_acquired:
|
|
357
492
|
raise RuntimeError(f'Failed to access {self.cache_path}. '
|
|
358
|
-
f'{self.lock_file} is still locked
|
|
359
|
-
'please delete
|
|
493
|
+
f'{self.lock_file} is still locked. If this is a mistake, '
|
|
494
|
+
'please delete the lock file.')
|
|
360
495
|
|
|
361
496
|
@contextlib.contextmanager
|
|
362
497
|
def lock(self):
|
|
498
|
+
"""
|
|
499
|
+
A context manager that acquires both the thread and file locks.
|
|
500
|
+
|
|
501
|
+
This ensures that only one thread in one process can access the cache
|
|
502
|
+
for a specific resource at a time.
|
|
503
|
+
"""
|
|
363
504
|
with self.__thread_lock():
|
|
364
505
|
with self.__file_lock():
|
|
365
506
|
yield
|
|
366
507
|
|
|
367
508
|
def resolve_remote(self):
|
|
509
|
+
"""Abstract method to fetch the remote data."""
|
|
368
510
|
raise NotImplementedError("child class must implement this")
|
|
369
511
|
|
|
370
512
|
def check_cache(self):
|
|
513
|
+
"""
|
|
514
|
+
Abstract method to check if the on-disk cache is valid.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
bool: True if the cache is valid, False otherwise.
|
|
518
|
+
"""
|
|
371
519
|
raise NotImplementedError("child class must implement this")
|
|
372
520
|
|
|
373
521
|
def resolve(self) -> Path:
|
|
522
|
+
"""
|
|
523
|
+
Resolves the remote data, using the on-disk cache if possible.
|
|
524
|
+
|
|
525
|
+
This method acquires locks, checks the cache validity, and calls
|
|
526
|
+
`resolve_remote()` to fetch the data if the cache is missing or invalid.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
Path: The path to the locally cached data.
|
|
530
|
+
"""
|
|
374
531
|
cache_dir = self.cache_dir
|
|
375
532
|
if not os.path.exists(cache_dir):
|
|
376
533
|
try:
|
|
377
534
|
os.makedirs(cache_dir, exist_ok=True)
|
|
378
535
|
except OSError:
|
|
536
|
+
# Can't create directory, return path and let it fail later
|
|
379
537
|
return self.cache_path
|
|
380
538
|
|
|
381
539
|
if not os.access(self.cache_dir, os.W_OK):
|
|
540
|
+
# Can't write to directory, assume cache is valid if it exists
|
|
382
541
|
return self.cache_path
|
|
383
542
|
|
|
384
543
|
with self.lock():
|
|
@@ -390,8 +549,14 @@ class RemoteResolver(Resolver):
|
|
|
390
549
|
return self.cache_path
|
|
391
550
|
|
|
392
551
|
|
|
393
|
-
###############
|
|
394
552
|
class FileResolver(Resolver):
|
|
553
|
+
"""
|
|
554
|
+
A resolver for local file system paths.
|
|
555
|
+
|
|
556
|
+
It handles both absolute paths and paths relative to the chip's CWD.
|
|
557
|
+
It normalizes the source string to a `file://` URI.
|
|
558
|
+
"""
|
|
559
|
+
|
|
395
560
|
def __init__(self, name, root, source, reference=None):
|
|
396
561
|
if source.startswith("file://"):
|
|
397
562
|
source = source[7:]
|
|
@@ -402,27 +567,43 @@ class FileResolver(Resolver):
|
|
|
402
567
|
|
|
403
568
|
@property
|
|
404
569
|
def urlpath(self):
|
|
570
|
+
"""The absolute file path, stripped of the 'file://' prefix."""
|
|
405
571
|
# Rebuild URL and remove scheme prefix
|
|
406
572
|
return self.urlparse.geturl()[7:]
|
|
407
573
|
|
|
408
574
|
def resolve(self):
|
|
575
|
+
"""Returns the absolute path to the file."""
|
|
409
576
|
return os.path.abspath(self.urlpath)
|
|
410
577
|
|
|
411
578
|
|
|
412
579
|
class PythonPathResolver(Resolver):
|
|
580
|
+
"""
|
|
581
|
+
A resolver for locating installed Python packages.
|
|
582
|
+
|
|
583
|
+
This resolver uses Python's import machinery to find the installation
|
|
584
|
+
directory of a given Python module. It also includes helper methods to
|
|
585
|
+
determine if a package is installed in "editable" mode.
|
|
586
|
+
"""
|
|
587
|
+
|
|
413
588
|
def __init__(self, name, root, source, reference=None):
|
|
414
589
|
super().__init__(name, root, source, None)
|
|
415
590
|
|
|
416
591
|
@staticmethod
|
|
417
592
|
@functools.lru_cache(maxsize=1)
|
|
418
593
|
def get_python_module_mapping():
|
|
594
|
+
"""
|
|
595
|
+
Creates a mapping from importable module names to their distribution names.
|
|
596
|
+
|
|
597
|
+
This is used to find the distribution package that provides a given module.
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
dict: A dictionary mapping module names to a list of distribution names.
|
|
601
|
+
"""
|
|
419
602
|
mapping = {}
|
|
420
603
|
|
|
421
604
|
for dist in distributions():
|
|
422
|
-
dist_name = None
|
|
423
|
-
if
|
|
424
|
-
dist_name = dist.name
|
|
425
|
-
else:
|
|
605
|
+
dist_name = getattr(dist, 'name', None)
|
|
606
|
+
if not dist_name:
|
|
426
607
|
metadata = dist.read_text('METADATA')
|
|
427
608
|
if metadata:
|
|
428
609
|
find_name = re.compile(r'Name: (.*)')
|
|
@@ -444,15 +625,30 @@ class PythonPathResolver(Resolver):
|
|
|
444
625
|
|
|
445
626
|
@staticmethod
|
|
446
627
|
def is_python_module_editable(module_name):
|
|
628
|
+
"""
|
|
629
|
+
Checks if a Python module is installed in "editable" mode.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
module_name (str): The name of the Python module to check.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
bool: True if the module is installed in editable mode, False otherwise.
|
|
636
|
+
"""
|
|
447
637
|
dist_map = PythonPathResolver.get_python_module_mapping()
|
|
448
|
-
|
|
638
|
+
if module_name not in dist_map:
|
|
639
|
+
return False
|
|
640
|
+
dist_name = dist_map[module_name][0]
|
|
449
641
|
|
|
450
642
|
is_editable = False
|
|
451
|
-
|
|
643
|
+
dist_obj = distribution(dist_name)
|
|
644
|
+
if not dist_obj or not dist_obj.files:
|
|
645
|
+
return False
|
|
646
|
+
|
|
647
|
+
for f in dist_obj.files:
|
|
452
648
|
if f.name == 'direct_url.json':
|
|
453
649
|
info = None
|
|
454
|
-
with open(f.locate(), 'r') as
|
|
455
|
-
info = json.load(
|
|
650
|
+
with open(f.locate(), 'r') as fp:
|
|
651
|
+
info = json.load(fp)
|
|
456
652
|
|
|
457
653
|
if "dir_info" in info:
|
|
458
654
|
is_editable = info["dir_info"].get("editable", False)
|
|
@@ -466,26 +662,33 @@ class PythonPathResolver(Resolver):
|
|
|
466
662
|
alternative_path,
|
|
467
663
|
alternative_ref=None,
|
|
468
664
|
python_module_path_append=None):
|
|
469
|
-
|
|
470
|
-
Helper
|
|
471
|
-
|
|
472
|
-
'
|
|
473
|
-
|
|
665
|
+
"""
|
|
666
|
+
Helper to conditionally register a Python module or a fallback path.
|
|
667
|
+
|
|
668
|
+
If the specified `python_module` is installed in editable mode, it's
|
|
669
|
+
registered as the source. Otherwise, the `alternative_path` and
|
|
670
|
+
`alternative_ref` are used.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
root (Chip): The chip object to register the source with.
|
|
674
|
+
package_name (str): The name of the package to register.
|
|
675
|
+
python_module (str): The Python module to check for.
|
|
676
|
+
alternative_path (str): The fallback source path.
|
|
677
|
+
alternative_ref (str, optional): The fallback reference. Defaults to None.
|
|
678
|
+
python_module_path_append (str, optional): A subdirectory to append
|
|
679
|
+
to the resolved Python module path. Defaults to None.
|
|
680
|
+
"""
|
|
474
681
|
if PythonPathResolver.is_python_module_editable(python_module):
|
|
682
|
+
path = f"python://{python_module}"
|
|
475
683
|
if python_module_path_append:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
path = os.path.abspath(os.path.join(path, python_module_path_append))
|
|
479
|
-
else:
|
|
480
|
-
path = f"python://{python_module}"
|
|
684
|
+
py_path = PythonPathResolver(python_module, root, path).resolve()
|
|
685
|
+
path = os.path.abspath(os.path.join(py_path, python_module_path_append))
|
|
481
686
|
ref = None
|
|
482
687
|
else:
|
|
483
688
|
path = alternative_path
|
|
484
689
|
ref = alternative_ref
|
|
485
690
|
|
|
486
|
-
root.register_source(name=package_name,
|
|
487
|
-
path=path,
|
|
488
|
-
ref=ref)
|
|
691
|
+
root.register_source(name=package_name, path=path, ref=ref)
|
|
489
692
|
|
|
490
693
|
@staticmethod
|
|
491
694
|
def set_dataroot(root,
|
|
@@ -494,40 +697,58 @@ class PythonPathResolver(Resolver):
|
|
|
494
697
|
alternative_path,
|
|
495
698
|
alternative_ref=None,
|
|
496
699
|
python_module_path_append=None):
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
700
|
+
"""
|
|
701
|
+
DEPRECATED: Use register_source.
|
|
702
|
+
Helper to conditionally set a dataroot to a Python module or a fallback path.
|
|
703
|
+
"""
|
|
501
704
|
# check if installed in an editable state
|
|
502
705
|
if PythonPathResolver.is_python_module_editable(python_module):
|
|
706
|
+
path = f"python://{python_module}"
|
|
503
707
|
if python_module_path_append:
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
path = os.path.abspath(os.path.join(path, python_module_path_append))
|
|
507
|
-
else:
|
|
508
|
-
path = f"python://{python_module}"
|
|
708
|
+
py_path = PythonPathResolver(python_module, root, path).resolve()
|
|
709
|
+
path = os.path.abspath(os.path.join(py_path, python_module_path_append))
|
|
509
710
|
ref = None
|
|
510
711
|
else:
|
|
511
712
|
path = alternative_path
|
|
512
713
|
ref = alternative_ref
|
|
513
714
|
|
|
514
|
-
root.set_dataroot(
|
|
515
|
-
path=path,
|
|
516
|
-
tag=ref)
|
|
715
|
+
root.set_dataroot(package_name, path=path, tag=ref)
|
|
517
716
|
|
|
518
717
|
def resolve(self):
|
|
718
|
+
"""
|
|
719
|
+
Resolves the path to the specified Python module.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
str: The absolute path to the module's directory.
|
|
723
|
+
"""
|
|
519
724
|
module = importlib.import_module(self.urlpath)
|
|
520
725
|
python_path = os.path.dirname(module.__file__)
|
|
521
726
|
return os.path.abspath(python_path)
|
|
522
727
|
|
|
523
728
|
|
|
524
729
|
class KeyPathResolver(Resolver):
|
|
730
|
+
"""
|
|
731
|
+
A resolver for finding file paths stored within the Chip schema itself.
|
|
732
|
+
|
|
733
|
+
This resolver takes a keypath (e.g., 'tool,openroad,exe') and uses the
|
|
734
|
+
`find_files` method of the root Chip object to locate the corresponding file.
|
|
735
|
+
"""
|
|
736
|
+
|
|
525
737
|
def __init__(self, name, root, source, reference=None):
|
|
526
738
|
super().__init__(name, root, source, None)
|
|
527
739
|
|
|
528
740
|
def resolve(self):
|
|
741
|
+
"""
|
|
742
|
+
Resolves the path by looking up the keypath in the Chip schema.
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
str: The file path found in the schema.
|
|
746
|
+
|
|
747
|
+
Raises:
|
|
748
|
+
RuntimeError: If the resolver does not have a root Chip object defined.
|
|
749
|
+
"""
|
|
529
750
|
if not self.root:
|
|
530
|
-
raise RuntimeError(f"
|
|
751
|
+
raise RuntimeError(f"A root schema has not been defined for '{self.name}'")
|
|
531
752
|
|
|
532
753
|
key = self.urlpath.split(",")
|
|
533
754
|
if self.root.get(*key, field='pernode').is_never():
|