siliconcompiler 0.34.1__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 +23 -4
- 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 +7 -6
- 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/__init__.py +17 -0
- siliconcompiler/constraints/asic_component.py +378 -0
- siliconcompiler/constraints/asic_floorplan.py +449 -0
- siliconcompiler/constraints/asic_pins.py +489 -0
- siliconcompiler/constraints/asic_timing.py +517 -0
- siliconcompiler/core.py +10 -35
- siliconcompiler/data/templates/tcl/manifest.tcl.j2 +8 -0
- siliconcompiler/dependencyschema.py +96 -202
- siliconcompiler/design.py +327 -241
- siliconcompiler/filesetschema.py +250 -0
- siliconcompiler/flowgraph.py +298 -106
- siliconcompiler/fpga.py +124 -1
- siliconcompiler/library.py +331 -0
- siliconcompiler/metric.py +327 -92
- siliconcompiler/metrics/__init__.py +7 -0
- siliconcompiler/metrics/asic.py +245 -0
- siliconcompiler/metrics/fpga.py +220 -0
- siliconcompiler/package/__init__.py +391 -67
- siliconcompiler/package/git.py +92 -16
- siliconcompiler/package/github.py +114 -22
- siliconcompiler/package/https.py +79 -16
- siliconcompiler/packageschema.py +341 -16
- siliconcompiler/pathschema.py +255 -0
- siliconcompiler/pdk.py +566 -1
- siliconcompiler/project.py +1460 -0
- 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 +81 -4
- siliconcompiler/scheduler/run_node.py +37 -20
- siliconcompiler/scheduler/scheduler.py +211 -36
- siliconcompiler/scheduler/schedulernode.py +394 -60
- siliconcompiler/scheduler/send_messages.py +77 -29
- siliconcompiler/scheduler/slurm.py +76 -12
- siliconcompiler/scheduler/taskscheduler.py +142 -21
- siliconcompiler/schema/__init__.py +0 -4
- siliconcompiler/schema/baseschema.py +338 -59
- siliconcompiler/schema/editableschema.py +14 -6
- siliconcompiler/schema/journal.py +28 -17
- siliconcompiler/schema/namedschema.py +22 -14
- siliconcompiler/schema/parameter.py +89 -28
- siliconcompiler/schema/parametertype.py +2 -0
- siliconcompiler/schema/parametervalue.py +258 -15
- siliconcompiler/schema/safeschema.py +25 -2
- siliconcompiler/schema/schema_cfg.py +23 -19
- siliconcompiler/schema/utils.py +2 -2
- siliconcompiler/schema_obj.py +24 -5
- siliconcompiler/tool.py +1131 -265
- 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.1.dist-info → siliconcompiler-0.34.3.dist-info}/METADATA +6 -6
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/RECORD +122 -115
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/entry_points.txt +3 -0
- siliconcompiler/schema/cmdlineschema.py +0 -250
- siliconcompiler/schema/packageschema.py +0 -101
- 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.1.dist-info → siliconcompiler-0.34.3.dist-info}/WHEEL +0 -0
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/licenses/LICENSE +0 -0
- {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,24 @@
|
|
|
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
|
|
12
|
+
import hashlib
|
|
3
13
|
import importlib
|
|
4
14
|
import json
|
|
5
15
|
import logging
|
|
6
16
|
import os
|
|
17
|
+
import random
|
|
7
18
|
import re
|
|
8
19
|
import time
|
|
9
20
|
import threading
|
|
21
|
+
import uuid
|
|
10
22
|
|
|
11
23
|
import os.path
|
|
12
24
|
|
|
@@ -19,6 +31,7 @@ from siliconcompiler.utils import get_plugins
|
|
|
19
31
|
|
|
20
32
|
|
|
21
33
|
def path(chip, package):
|
|
34
|
+
"""DEPRECATED: Use chip.get_resolver(package).get_path() instead."""
|
|
22
35
|
import warnings
|
|
23
36
|
warnings.warn("The 'path' method has been deprecated",
|
|
24
37
|
DeprecationWarning)
|
|
@@ -31,10 +44,7 @@ def register_python_data_source(chip,
|
|
|
31
44
|
alternative_path,
|
|
32
45
|
alternative_ref=None,
|
|
33
46
|
python_module_path_append=None):
|
|
34
|
-
|
|
35
|
-
Helper function to register a python module as data source with an alternative in case
|
|
36
|
-
the module is not installed in an editable state
|
|
37
|
-
'''
|
|
47
|
+
"""DEPRECATED: Use PythonPathResolver.register_source() instead."""
|
|
38
48
|
import warnings
|
|
39
49
|
warnings.warn("The 'register_python_data_source' method was renamed "
|
|
40
50
|
"PythonPathResolver.register_source",
|
|
@@ -47,16 +57,36 @@ def register_python_data_source(chip,
|
|
|
47
57
|
|
|
48
58
|
|
|
49
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
|
+
"""
|
|
50
74
|
_RESOLVERS_LOCK = threading.Lock()
|
|
51
75
|
_RESOLVERS = {}
|
|
52
76
|
|
|
77
|
+
__CACHE_LOCK = threading.Lock()
|
|
78
|
+
__CACHE = {}
|
|
79
|
+
|
|
53
80
|
def __init__(self, name, root, source, reference=None):
|
|
81
|
+
"""
|
|
82
|
+
Initializes the Resolver.
|
|
83
|
+
"""
|
|
54
84
|
self.__name = name
|
|
55
85
|
self.__root = root
|
|
56
86
|
self.__source = source
|
|
57
87
|
self.__reference = reference
|
|
58
88
|
self.__changed = False
|
|
59
|
-
self.
|
|
89
|
+
self.__cacheid = None
|
|
60
90
|
|
|
61
91
|
if self.__root and hasattr(self.__root, "logger"):
|
|
62
92
|
self.__logger = self.__root.logger.getChild(f"resolver-{self.name}")
|
|
@@ -65,6 +95,13 @@ class Resolver:
|
|
|
65
95
|
|
|
66
96
|
@staticmethod
|
|
67
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
|
+
"""
|
|
68
105
|
with Resolver._RESOLVERS_LOCK:
|
|
69
106
|
Resolver._RESOLVERS.clear()
|
|
70
107
|
|
|
@@ -80,6 +117,18 @@ class Resolver:
|
|
|
80
117
|
|
|
81
118
|
@staticmethod
|
|
82
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
|
+
"""
|
|
83
132
|
if os.path.isabs(source):
|
|
84
133
|
return FileResolver
|
|
85
134
|
|
|
@@ -91,71 +140,177 @@ class Resolver:
|
|
|
91
140
|
if url.scheme in Resolver._RESOLVERS:
|
|
92
141
|
return Resolver._RESOLVERS[url.scheme]
|
|
93
142
|
|
|
94
|
-
raise ValueError(f"{source} is not supported")
|
|
143
|
+
raise ValueError(f"Source URI '{source}' is not supported")
|
|
95
144
|
|
|
96
145
|
@property
|
|
97
146
|
def name(self) -> str:
|
|
147
|
+
"""The name of the data package being resolved."""
|
|
98
148
|
return self.__name
|
|
99
149
|
|
|
100
150
|
@property
|
|
101
151
|
def root(self):
|
|
152
|
+
"""The root object (e.g., Chip) providing context."""
|
|
102
153
|
return self.__root
|
|
103
154
|
|
|
104
155
|
@property
|
|
105
156
|
def logger(self) -> logging.Logger:
|
|
157
|
+
"""The logger instance for this resolver."""
|
|
106
158
|
return self.__logger
|
|
107
159
|
|
|
108
160
|
@property
|
|
109
161
|
def source(self) -> str:
|
|
162
|
+
"""The URI or path specifying the data source."""
|
|
110
163
|
return self.__source
|
|
111
164
|
|
|
112
165
|
@property
|
|
113
166
|
def reference(self) -> str:
|
|
167
|
+
"""A version, commit hash, or tag for the source."""
|
|
114
168
|
return self.__reference
|
|
115
169
|
|
|
116
170
|
@property
|
|
117
171
|
def urlparse(self) -> url_parse.ParseResult:
|
|
172
|
+
"""The parsed URL of the source after environment variable expansion."""
|
|
118
173
|
return url_parse.urlparse(self.__resolve_env(self.source))
|
|
119
174
|
|
|
120
175
|
@property
|
|
121
176
|
def urlscheme(self) -> str:
|
|
177
|
+
"""The scheme of the source URL (e.g., 'file', 'git')."""
|
|
122
178
|
return self.urlparse.scheme
|
|
123
179
|
|
|
124
180
|
@property
|
|
125
181
|
def urlpath(self) -> str:
|
|
182
|
+
"""The path component of the source URL."""
|
|
126
183
|
return self.urlparse.netloc
|
|
127
184
|
|
|
128
185
|
@property
|
|
129
|
-
def changed(self):
|
|
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
|
+
"""
|
|
130
192
|
change = self.__changed
|
|
131
193
|
self.__changed = False
|
|
132
194
|
return change
|
|
133
195
|
|
|
196
|
+
@property
|
|
197
|
+
def cache_id(self) -> str:
|
|
198
|
+
"""A unique ID for this resolver instance, used for caching."""
|
|
199
|
+
if self.__cacheid is None:
|
|
200
|
+
hash_obj = hashlib.sha1()
|
|
201
|
+
hash_obj.update(self.__source.encode())
|
|
202
|
+
if self.__reference:
|
|
203
|
+
hash_obj.update(self.__reference.encode())
|
|
204
|
+
else:
|
|
205
|
+
hash_obj.update("".encode())
|
|
206
|
+
|
|
207
|
+
self.__cacheid = hash_obj.hexdigest()
|
|
208
|
+
return self.__cacheid
|
|
209
|
+
|
|
134
210
|
def set_changed(self):
|
|
211
|
+
"""Marks the resolved data as having been changed."""
|
|
135
212
|
self.__changed = True
|
|
136
213
|
|
|
137
|
-
def set_cache(self, cache):
|
|
138
|
-
self.__cache = cache
|
|
139
|
-
|
|
140
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
|
+
"""
|
|
141
221
|
raise NotImplementedError("child class must implement this")
|
|
142
222
|
|
|
223
|
+
@staticmethod
|
|
224
|
+
def __get_root_id(root):
|
|
225
|
+
"""Generates or retrieves a unique ID for a root object."""
|
|
226
|
+
STORAGE = "__Resolver_cache_id"
|
|
227
|
+
if not getattr(root, STORAGE, None):
|
|
228
|
+
setattr(root, STORAGE, uuid.uuid4().hex)
|
|
229
|
+
return getattr(root, STORAGE)
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
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
|
+
"""
|
|
244
|
+
with Resolver.__CACHE_LOCK:
|
|
245
|
+
root_id = Resolver.__get_root_id(root)
|
|
246
|
+
if root_id not in Resolver.__CACHE:
|
|
247
|
+
Resolver.__CACHE[root_id] = {}
|
|
248
|
+
|
|
249
|
+
if name:
|
|
250
|
+
return Resolver.__CACHE[root_id].get(name, None)
|
|
251
|
+
|
|
252
|
+
return Resolver.__CACHE[root_id].copy()
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
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
|
+
"""
|
|
264
|
+
with Resolver.__CACHE_LOCK:
|
|
265
|
+
root_id = Resolver.__get_root_id(root)
|
|
266
|
+
if root_id not in Resolver.__CACHE:
|
|
267
|
+
Resolver.__CACHE[root_id] = {}
|
|
268
|
+
Resolver.__CACHE[root_id][name] = path
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
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
|
+
"""
|
|
278
|
+
with Resolver.__CACHE_LOCK:
|
|
279
|
+
root_id = Resolver.__get_root_id(root)
|
|
280
|
+
if root_id in Resolver.__CACHE:
|
|
281
|
+
del Resolver.__CACHE[root_id]
|
|
282
|
+
|
|
143
283
|
def get_path(self):
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
"""
|
|
296
|
+
cache_path = Resolver.get_cache(self.__root, self.cache_id)
|
|
297
|
+
if cache_path:
|
|
298
|
+
return cache_path
|
|
146
299
|
|
|
147
300
|
path = self.resolve()
|
|
148
301
|
if not os.path.exists(path):
|
|
149
|
-
raise FileNotFoundError(f"Unable to locate {self.name} at {path}")
|
|
302
|
+
raise FileNotFoundError(f"Unable to locate '{self.name}' at {path}")
|
|
150
303
|
|
|
151
|
-
if self.changed
|
|
304
|
+
if self.changed:
|
|
152
305
|
self.logger.info(f'Saved {self.name} data to {path}')
|
|
153
306
|
else:
|
|
154
307
|
self.logger.info(f'Found {self.name} data at {path}')
|
|
155
|
-
|
|
156
|
-
|
|
308
|
+
|
|
309
|
+
Resolver.set_cache(self.__root, self.cache_id, path)
|
|
310
|
+
return path
|
|
157
311
|
|
|
158
312
|
def __resolve_env(self, path):
|
|
313
|
+
"""Expands environment variables and user home directory in a path."""
|
|
159
314
|
env_save = os.environ.copy()
|
|
160
315
|
|
|
161
316
|
if self.root:
|
|
@@ -173,12 +328,20 @@ class Resolver:
|
|
|
173
328
|
|
|
174
329
|
|
|
175
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
|
+
"""
|
|
176
339
|
_CACHE_LOCKS = {}
|
|
177
340
|
_CACHE_LOCK = threading.Lock()
|
|
178
341
|
|
|
179
342
|
def __init__(self, name, root, source, reference=None):
|
|
180
343
|
if reference is None:
|
|
181
|
-
raise ValueError(f'
|
|
344
|
+
raise ValueError(f'A reference (e.g., version, commit) is required for {name}')
|
|
182
345
|
|
|
183
346
|
super().__init__(name, root, source, reference)
|
|
184
347
|
|
|
@@ -187,17 +350,32 @@ class RemoteResolver(Resolver):
|
|
|
187
350
|
|
|
188
351
|
@property
|
|
189
352
|
def timeout(self):
|
|
353
|
+
"""The maximum time in seconds to wait for a lock."""
|
|
190
354
|
return self.__max_lock_wait
|
|
191
355
|
|
|
192
356
|
def set_timeout(self, value):
|
|
357
|
+
"""Sets the maximum time in seconds to wait for a lock."""
|
|
193
358
|
self.__max_lock_wait = value
|
|
194
359
|
|
|
195
360
|
@staticmethod
|
|
196
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
|
+
"""
|
|
197
374
|
default_path = os.path.join(Path.home(), '.sc', 'cache')
|
|
198
375
|
if not root:
|
|
199
376
|
return Path(default_path)
|
|
200
377
|
|
|
378
|
+
path = None
|
|
201
379
|
if root.valid('option', 'cachedir'):
|
|
202
380
|
path = root.get('option', 'cachedir')
|
|
203
381
|
if path:
|
|
@@ -212,14 +390,17 @@ class RemoteResolver(Resolver):
|
|
|
212
390
|
|
|
213
391
|
@property
|
|
214
392
|
def cache_dir(self) -> Path:
|
|
393
|
+
"""The directory for the on-disk cache."""
|
|
215
394
|
return RemoteResolver.determine_cache_dir(self.root)
|
|
216
395
|
|
|
217
396
|
@property
|
|
218
397
|
def cache_name(self) -> str:
|
|
219
|
-
|
|
398
|
+
"""A unique name for the cached data directory."""
|
|
399
|
+
return f"{self.name}-{self.reference[0:16]}-{self.cache_id[0:16]}"
|
|
220
400
|
|
|
221
401
|
@property
|
|
222
402
|
def cache_path(self) -> Path:
|
|
403
|
+
"""The full path to the cached data directory."""
|
|
223
404
|
cache_dir = self.cache_dir
|
|
224
405
|
if not os.path.exists(cache_dir):
|
|
225
406
|
os.makedirs(cache_dir, exist_ok=True)
|
|
@@ -228,6 +409,7 @@ class RemoteResolver(Resolver):
|
|
|
228
409
|
|
|
229
410
|
@property
|
|
230
411
|
def lock_file(self) -> Path:
|
|
412
|
+
"""The path to the file used for inter-process locking."""
|
|
231
413
|
cache_dir = self.cache_dir
|
|
232
414
|
if not os.path.exists(cache_dir):
|
|
233
415
|
os.makedirs(cache_dir, exist_ok=True)
|
|
@@ -236,6 +418,9 @@ class RemoteResolver(Resolver):
|
|
|
236
418
|
|
|
237
419
|
@property
|
|
238
420
|
def sc_lock_file(self) -> Path:
|
|
421
|
+
"""
|
|
422
|
+
The path to a secondary lock file used as a fallback mechanism.
|
|
423
|
+
"""
|
|
239
424
|
cache_dir = self.cache_dir
|
|
240
425
|
if not os.path.exists(cache_dir):
|
|
241
426
|
os.makedirs(cache_dir, exist_ok=True)
|
|
@@ -243,38 +428,60 @@ class RemoteResolver(Resolver):
|
|
|
243
428
|
return self.cache_dir / f"{self.cache_name}.sc_lock"
|
|
244
429
|
|
|
245
430
|
def thread_lock(self):
|
|
431
|
+
"""Gets a threading.Lock specific to this resolver instance."""
|
|
246
432
|
with RemoteResolver._CACHE_LOCK:
|
|
247
433
|
if self.name not in RemoteResolver._CACHE_LOCKS:
|
|
248
434
|
RemoteResolver._CACHE_LOCKS[self.name] = threading.Lock()
|
|
249
435
|
return RemoteResolver._CACHE_LOCKS[self.name]
|
|
250
436
|
|
|
251
437
|
@contextlib.contextmanager
|
|
252
|
-
def
|
|
438
|
+
def __thread_lock(self):
|
|
439
|
+
"""A context manager for acquiring the thread lock with a timeout."""
|
|
253
440
|
lock = self.thread_lock()
|
|
254
441
|
lock_acquired = False
|
|
255
442
|
try:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if max_seconds == 0:
|
|
267
|
-
raise RuntimeError(f'Failed to access {self.cache_path}. '
|
|
268
|
-
f'Lock {sc_data_path_lock} still exists.')
|
|
269
|
-
time.sleep(1)
|
|
270
|
-
max_seconds -= 1
|
|
271
|
-
sc_data_path_lock.touch()
|
|
272
|
-
lock_acquired = True
|
|
273
|
-
if lock_acquired:
|
|
274
|
-
yield
|
|
443
|
+
timeout = self.timeout
|
|
444
|
+
while timeout > 0:
|
|
445
|
+
if lock.acquire_lock(timeout=1):
|
|
446
|
+
lock_acquired = True
|
|
447
|
+
break
|
|
448
|
+
sleep_time = random.randint(1, max(1, int(timeout / 10)))
|
|
449
|
+
timeout -= sleep_time + 1
|
|
450
|
+
time.sleep(sleep_time)
|
|
451
|
+
if lock_acquired:
|
|
452
|
+
yield
|
|
275
453
|
finally:
|
|
276
454
|
if lock.locked():
|
|
277
455
|
lock.release()
|
|
456
|
+
|
|
457
|
+
if not lock_acquired:
|
|
458
|
+
raise RuntimeError(f'Failed to access {self.cache_path}. '
|
|
459
|
+
f'Another thread is currently holding the lock.')
|
|
460
|
+
|
|
461
|
+
@contextlib.contextmanager
|
|
462
|
+
def __file_lock(self):
|
|
463
|
+
"""A context manager for acquiring the inter-process file lock."""
|
|
464
|
+
data_path_lock = InterProcessLock(self.lock_file)
|
|
465
|
+
lock_acquired = False
|
|
466
|
+
sc_data_path_lock = None
|
|
467
|
+
try:
|
|
468
|
+
try:
|
|
469
|
+
lock_acquired = data_path_lock.acquire(timeout=self.timeout)
|
|
470
|
+
except (OSError, RuntimeError):
|
|
471
|
+
if not lock_acquired:
|
|
472
|
+
sc_data_path_lock = Path(self.sc_lock_file)
|
|
473
|
+
max_seconds = self.timeout
|
|
474
|
+
while sc_data_path_lock.exists():
|
|
475
|
+
if max_seconds == 0:
|
|
476
|
+
raise RuntimeError(f'Failed to access {self.cache_path}. '
|
|
477
|
+
f'Lock {sc_data_path_lock} still exists.')
|
|
478
|
+
time.sleep(1)
|
|
479
|
+
max_seconds -= 1
|
|
480
|
+
sc_data_path_lock.touch()
|
|
481
|
+
lock_acquired = True
|
|
482
|
+
if lock_acquired:
|
|
483
|
+
yield
|
|
484
|
+
finally:
|
|
278
485
|
if lock_acquired:
|
|
279
486
|
if data_path_lock.acquired:
|
|
280
487
|
data_path_lock.release()
|
|
@@ -283,24 +490,54 @@ class RemoteResolver(Resolver):
|
|
|
283
490
|
|
|
284
491
|
if not lock_acquired:
|
|
285
492
|
raise RuntimeError(f'Failed to access {self.cache_path}. '
|
|
286
|
-
f'{self.lock_file} is still locked
|
|
287
|
-
'please delete
|
|
493
|
+
f'{self.lock_file} is still locked. If this is a mistake, '
|
|
494
|
+
'please delete the lock file.')
|
|
495
|
+
|
|
496
|
+
@contextlib.contextmanager
|
|
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
|
+
"""
|
|
504
|
+
with self.__thread_lock():
|
|
505
|
+
with self.__file_lock():
|
|
506
|
+
yield
|
|
288
507
|
|
|
289
508
|
def resolve_remote(self):
|
|
509
|
+
"""Abstract method to fetch the remote data."""
|
|
290
510
|
raise NotImplementedError("child class must implement this")
|
|
291
511
|
|
|
292
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
|
+
"""
|
|
293
519
|
raise NotImplementedError("child class must implement this")
|
|
294
520
|
|
|
295
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
|
+
"""
|
|
296
531
|
cache_dir = self.cache_dir
|
|
297
532
|
if not os.path.exists(cache_dir):
|
|
298
533
|
try:
|
|
299
534
|
os.makedirs(cache_dir, exist_ok=True)
|
|
300
535
|
except OSError:
|
|
536
|
+
# Can't create directory, return path and let it fail later
|
|
301
537
|
return self.cache_path
|
|
302
538
|
|
|
303
539
|
if not os.access(self.cache_dir, os.W_OK):
|
|
540
|
+
# Can't write to directory, assume cache is valid if it exists
|
|
304
541
|
return self.cache_path
|
|
305
542
|
|
|
306
543
|
with self.lock():
|
|
@@ -312,8 +549,14 @@ class RemoteResolver(Resolver):
|
|
|
312
549
|
return self.cache_path
|
|
313
550
|
|
|
314
551
|
|
|
315
|
-
###############
|
|
316
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
|
+
|
|
317
560
|
def __init__(self, name, root, source, reference=None):
|
|
318
561
|
if source.startswith("file://"):
|
|
319
562
|
source = source[7:]
|
|
@@ -324,30 +567,43 @@ class FileResolver(Resolver):
|
|
|
324
567
|
|
|
325
568
|
@property
|
|
326
569
|
def urlpath(self):
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
else:
|
|
331
|
-
return parse.path
|
|
570
|
+
"""The absolute file path, stripped of the 'file://' prefix."""
|
|
571
|
+
# Rebuild URL and remove scheme prefix
|
|
572
|
+
return self.urlparse.geturl()[7:]
|
|
332
573
|
|
|
333
574
|
def resolve(self):
|
|
575
|
+
"""Returns the absolute path to the file."""
|
|
334
576
|
return os.path.abspath(self.urlpath)
|
|
335
577
|
|
|
336
578
|
|
|
337
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
|
+
|
|
338
588
|
def __init__(self, name, root, source, reference=None):
|
|
339
589
|
super().__init__(name, root, source, None)
|
|
340
590
|
|
|
341
591
|
@staticmethod
|
|
342
592
|
@functools.lru_cache(maxsize=1)
|
|
343
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
|
+
"""
|
|
344
602
|
mapping = {}
|
|
345
603
|
|
|
346
604
|
for dist in distributions():
|
|
347
|
-
dist_name = None
|
|
348
|
-
if
|
|
349
|
-
dist_name = dist.name
|
|
350
|
-
else:
|
|
605
|
+
dist_name = getattr(dist, 'name', None)
|
|
606
|
+
if not dist_name:
|
|
351
607
|
metadata = dist.read_text('METADATA')
|
|
352
608
|
if metadata:
|
|
353
609
|
find_name = re.compile(r'Name: (.*)')
|
|
@@ -369,15 +625,30 @@ class PythonPathResolver(Resolver):
|
|
|
369
625
|
|
|
370
626
|
@staticmethod
|
|
371
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
|
+
"""
|
|
372
637
|
dist_map = PythonPathResolver.get_python_module_mapping()
|
|
373
|
-
|
|
638
|
+
if module_name not in dist_map:
|
|
639
|
+
return False
|
|
640
|
+
dist_name = dist_map[module_name][0]
|
|
374
641
|
|
|
375
642
|
is_editable = False
|
|
376
|
-
|
|
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:
|
|
377
648
|
if f.name == 'direct_url.json':
|
|
378
649
|
info = None
|
|
379
|
-
with open(f.locate(), 'r') as
|
|
380
|
-
info = json.load(
|
|
650
|
+
with open(f.locate(), 'r') as fp:
|
|
651
|
+
info = json.load(fp)
|
|
381
652
|
|
|
382
653
|
if "dir_info" in info:
|
|
383
654
|
is_editable = info["dir_info"].get("editable", False)
|
|
@@ -391,40 +662,93 @@ class PythonPathResolver(Resolver):
|
|
|
391
662
|
alternative_path,
|
|
392
663
|
alternative_ref=None,
|
|
393
664
|
python_module_path_append=None):
|
|
394
|
-
|
|
395
|
-
Helper
|
|
396
|
-
|
|
397
|
-
'
|
|
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
|
+
"""
|
|
681
|
+
if PythonPathResolver.is_python_module_editable(python_module):
|
|
682
|
+
path = f"python://{python_module}"
|
|
683
|
+
if python_module_path_append:
|
|
684
|
+
py_path = PythonPathResolver(python_module, root, path).resolve()
|
|
685
|
+
path = os.path.abspath(os.path.join(py_path, python_module_path_append))
|
|
686
|
+
ref = None
|
|
687
|
+
else:
|
|
688
|
+
path = alternative_path
|
|
689
|
+
ref = alternative_ref
|
|
690
|
+
|
|
691
|
+
root.register_source(name=package_name, path=path, ref=ref)
|
|
692
|
+
|
|
693
|
+
@staticmethod
|
|
694
|
+
def set_dataroot(root,
|
|
695
|
+
package_name,
|
|
696
|
+
python_module,
|
|
697
|
+
alternative_path,
|
|
698
|
+
alternative_ref=None,
|
|
699
|
+
python_module_path_append=None):
|
|
700
|
+
"""
|
|
701
|
+
DEPRECATED: Use register_source.
|
|
702
|
+
Helper to conditionally set a dataroot to a Python module or a fallback path.
|
|
703
|
+
"""
|
|
398
704
|
# check if installed in an editable state
|
|
399
705
|
if PythonPathResolver.is_python_module_editable(python_module):
|
|
706
|
+
path = f"python://{python_module}"
|
|
400
707
|
if python_module_path_append:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
path = os.path.abspath(os.path.join(path, python_module_path_append))
|
|
404
|
-
else:
|
|
405
|
-
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))
|
|
406
710
|
ref = None
|
|
407
711
|
else:
|
|
408
712
|
path = alternative_path
|
|
409
713
|
ref = alternative_ref
|
|
410
714
|
|
|
411
|
-
root.
|
|
412
|
-
path=path,
|
|
413
|
-
ref=ref)
|
|
715
|
+
root.set_dataroot(package_name, path=path, tag=ref)
|
|
414
716
|
|
|
415
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
|
+
"""
|
|
416
724
|
module = importlib.import_module(self.urlpath)
|
|
417
725
|
python_path = os.path.dirname(module.__file__)
|
|
418
726
|
return os.path.abspath(python_path)
|
|
419
727
|
|
|
420
728
|
|
|
421
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
|
+
|
|
422
737
|
def __init__(self, name, root, source, reference=None):
|
|
423
738
|
super().__init__(name, root, source, None)
|
|
424
739
|
|
|
425
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
|
+
"""
|
|
426
750
|
if not self.root:
|
|
427
|
-
raise RuntimeError(f"
|
|
751
|
+
raise RuntimeError(f"A root schema has not been defined for '{self.name}'")
|
|
428
752
|
|
|
429
753
|
key = self.urlpath.split(",")
|
|
430
754
|
if self.root.get(*key, field='pernode').is_never():
|