ciel 0.21.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.
ciel/common.py ADDED
@@ -0,0 +1,256 @@
1
+ # Copyright 2022-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 shutil
17
+ import pathlib
18
+ from datetime import datetime
19
+ from dataclasses import dataclass
20
+ from typing import Iterable, Optional, List, Dict, Tuple
21
+
22
+ from . import github
23
+ from .families import Family
24
+
25
+ # -- Assorted Helper Functions
26
+ ISO8601_FMT = "%Y-%m-%dT%H:%M:%SZ"
27
+
28
+
29
+ def date_to_iso8601(date: datetime) -> str:
30
+ return date.strftime(ISO8601_FMT)
31
+
32
+
33
+ def date_from_iso8601(string: str) -> datetime:
34
+ return datetime.strptime(string, ISO8601_FMT)
35
+
36
+
37
+ def mkdirp(path):
38
+ return pathlib.Path(path).mkdir(parents=True, exist_ok=True)
39
+
40
+
41
+ # -- API Variables
42
+
43
+ # -- PDK Root Management
44
+ VOLARE_DEFAULT_HOME = os.path.join(os.path.expanduser("~"), ".cielo")
45
+ VOLARE_RESOLVED_HOME = os.getenv("PDK_ROOT") or VOLARE_DEFAULT_HOME
46
+
47
+
48
+ def _get_current_version(pdk_root: str, pdk: str) -> Optional[str]:
49
+ current_file = os.path.join(get_cielo_dir(pdk_root, pdk), "current")
50
+ current_file_dir = os.path.dirname(current_file)
51
+ mkdirp(current_file_dir)
52
+ version = None
53
+ try:
54
+ version = open(current_file).read().strip()
55
+ except FileNotFoundError:
56
+ pass
57
+
58
+ return version
59
+
60
+
61
+ def get_cielo_home(pdk_root: Optional[str] = None) -> str:
62
+ return pdk_root or VOLARE_RESOLVED_HOME
63
+
64
+
65
+ def get_cielo_dir(pdk_root: str, pdk: str) -> str:
66
+ return os.path.join(pdk_root, "cielo", pdk)
67
+
68
+
69
+ def get_versions_dir(pdk_root: str, pdk: str) -> str:
70
+ return os.path.join(get_cielo_dir(pdk_root, pdk), "versions")
71
+
72
+
73
+ @dataclass
74
+ class Version(object):
75
+ name: str
76
+ pdk: str
77
+ commit_date: Optional[datetime] = None
78
+ upload_date: Optional[datetime] = None
79
+ prerelease: bool = False
80
+
81
+ def __lt__(self, rhs: "Version"):
82
+ return (self.commit_date or datetime.min) < (rhs.commit_date or datetime.min)
83
+
84
+ def __str__(self) -> str:
85
+ return self.name
86
+
87
+ def is_installed(self, pdk_root: str) -> bool:
88
+ version_dir = self.get_dir(pdk_root)
89
+ return os.path.isdir(version_dir)
90
+
91
+ def is_current(self, pdk_root: str) -> bool:
92
+ return self.name == _get_current_version(pdk_root, self.pdk)
93
+
94
+ def get_dir(self, pdk_root: str) -> str:
95
+ return os.path.join(get_versions_dir(pdk_root, self.pdk), self.name)
96
+
97
+ def unset_current(self, pdk_root: str):
98
+ if not self.is_installed(pdk_root):
99
+ return
100
+ if not self.is_current(pdk_root):
101
+ return
102
+
103
+ for variant in Family.by_name[self.pdk].variants:
104
+ try:
105
+ os.unlink(os.path.join(pdk_root, variant))
106
+ except FileNotFoundError:
107
+ pass
108
+
109
+ current_file = os.path.join(get_cielo_dir(pdk_root, self.pdk), "current")
110
+ os.unlink(current_file)
111
+
112
+ def uninstall(self, pdk_root: str):
113
+ if not self.is_installed(pdk_root):
114
+ raise ValueError(
115
+ f"Version {self.name} of the {self.pdk} PDK is not installed."
116
+ )
117
+
118
+ self.unset_current(pdk_root)
119
+
120
+ version_dir = self.get_dir(pdk_root)
121
+
122
+ shutil.rmtree(version_dir)
123
+
124
+ @classmethod
125
+ def get_current(Self, pdk_root: str, pdk: str) -> Optional["Version"]:
126
+ current_version = _get_current_version(pdk_root, pdk)
127
+ if current_version is None:
128
+ return None
129
+
130
+ return Version(current_version, pdk)
131
+
132
+ @classmethod
133
+ def get_all_installed(Self, pdk_root: str, pdk: str) -> List["Version"]:
134
+ versions_dir = get_versions_dir(pdk_root, pdk)
135
+ mkdirp(versions_dir)
136
+ return [
137
+ Version(
138
+ name=version,
139
+ pdk=pdk,
140
+ )
141
+ for version in os.listdir(versions_dir)
142
+ if os.path.isdir(os.path.join(versions_dir, version))
143
+ ]
144
+
145
+ @classmethod
146
+ def _from_github(
147
+ Self,
148
+ session: Optional[github.GitHubSession] = None,
149
+ ) -> Dict[str, List["Version"]]:
150
+ releases = github.get_releases(session)
151
+
152
+ rvs_by_pdk: Dict[str, List["Version"]] = {}
153
+
154
+ commit_rx = re.compile(r"released on ([\d\-\:TZ]+)")
155
+
156
+ for release in releases:
157
+ if release["draft"]:
158
+ continue
159
+ family, hash = release["tag_name"].rsplit("-", maxsplit=1)
160
+
161
+ upload_date = date_from_iso8601(release["published_at"])
162
+ commit_date = None
163
+
164
+ commit_date_match = commit_rx.search(release["body"])
165
+ if commit_date_match is not None:
166
+ commit_date = date_from_iso8601(commit_date_match[1])
167
+
168
+ remote_version = Self(
169
+ name=hash,
170
+ pdk=family,
171
+ commit_date=commit_date,
172
+ upload_date=upload_date,
173
+ prerelease=release["prerelease"],
174
+ )
175
+
176
+ if rvs_by_pdk.get(family) is None:
177
+ rvs_by_pdk[family] = rvs_by_pdk.get(family) or []
178
+
179
+ rvs_by_pdk[family].append(remote_version)
180
+
181
+ for family in rvs_by_pdk.keys():
182
+ rvs_by_pdk[family].sort(reverse=True)
183
+
184
+ return rvs_by_pdk
185
+
186
+ def get_release_links(
187
+ self,
188
+ scl_filter: Iterable[str],
189
+ include_common: bool,
190
+ session: Optional[github.GitHubSession] = None,
191
+ ) -> List[Tuple[str, str]]:
192
+ release = github.get_release_links(f"{self.pdk}-{self.name}", session)
193
+
194
+ assets = release["assets"]
195
+ zst_files = []
196
+ for asset in assets:
197
+ if asset["name"].endswith(".tar.zst"):
198
+ asset_scl = asset["name"][:-8]
199
+ if (
200
+ asset_scl == "common" and include_common
201
+ ) or asset_scl in scl_filter:
202
+ zst_files.append((asset["name"], asset["browser_download_url"]))
203
+
204
+ if len(zst_files) == 0:
205
+ raise ValueError(
206
+ f"No files found for standard cell libraries: {scl_filter}."
207
+ )
208
+
209
+ return zst_files
210
+
211
+
212
+ def resolve_version(
213
+ version: Optional[str],
214
+ tool_metadata_file_path: Optional[str],
215
+ ) -> str:
216
+ """
217
+ Takes an optional version and tool_metadata_file_path.
218
+
219
+ If version is set, it is returned.
220
+
221
+ If not, tool_metadata_file_path is checked if it exists.
222
+
223
+ If not specified, ./tool_metadata.yml and ./dependencies/tool_metadata.yml
224
+ are both checked if they exist.
225
+
226
+ If none are specified, execution is halted.
227
+
228
+ Otherwise, the resulting metadata file is parsed for an open_pdks version,
229
+ which is then returned.
230
+ """
231
+ if version is not None:
232
+ return version
233
+
234
+ import yaml
235
+
236
+ if tool_metadata_file_path is None:
237
+ tool_metadata_file_path = os.path.join(".", "tool_metadata.yml")
238
+ if not os.path.isfile(tool_metadata_file_path):
239
+ tool_metadata_file_path = os.path.join(
240
+ ".", "dependencies", "tool_metadata.yml"
241
+ )
242
+ if not os.path.isfile(tool_metadata_file_path):
243
+ raise FileNotFoundError(
244
+ "Any of ./tool_metadata.yml or ./dependencies/tool_metadata.yml not found. You'll need to specify the file path or the commits explicitly."
245
+ )
246
+
247
+ tool_metadata = yaml.safe_load(open(tool_metadata_file_path).read())
248
+
249
+ open_pdks_list = [tool for tool in tool_metadata if tool["name"] == "open_pdks"]
250
+
251
+ if len(open_pdks_list) < 1:
252
+ raise ValueError("No entry for open_pdks found in tool_metadata.yml")
253
+
254
+ version = open_pdks_list[0]["commit"]
255
+
256
+ return version
ciel/families.py ADDED
@@ -0,0 +1,119 @@
1
+ # Copyright 2022-2023 Efabless Corporation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from dataclasses import dataclass
15
+ from typing import Iterable, List, Dict, Optional, Set, ClassVar
16
+
17
+ from .github import RepoInfo, opdks_repo, ihp_repo
18
+
19
+
20
+ @dataclass
21
+ class Family(object):
22
+ by_name: ClassVar[Dict[str, "Family"]] = {}
23
+
24
+ name: str
25
+ variants: List[str]
26
+ all_libraries: List[str]
27
+ repo: RepoInfo
28
+ # lol no implicitly unwrapped optionals
29
+ default_variant: str = None # type: ignore
30
+ default_includes: List[str] = None # type: ignore
31
+
32
+ def __post_init__(self):
33
+ if self.default_variant is None:
34
+ self.default_variant = self.variants[0]
35
+ if self.default_includes is None:
36
+ self.default_includes = self.all_libraries.copy()
37
+
38
+ def resolve_libraries(
39
+ self,
40
+ input: Optional[Iterable[str]],
41
+ ) -> Set[str]:
42
+ if input is None:
43
+ input = ("default",)
44
+ final_set: Set[str] = set()
45
+ for element in input:
46
+ if element.lower() == "all":
47
+ final_set = set(self.all_libraries)
48
+ return final_set
49
+ elif element.lower() == "default":
50
+ final_set = final_set.union(set(self.default_includes))
51
+ elif element in self.all_libraries:
52
+ final_set.add(element)
53
+ else:
54
+ raise ValueError(f"Unknown library {element} for PDK {self.name}")
55
+ return final_set
56
+
57
+
58
+ Family.by_name = {}
59
+ Family.by_name["sky130"] = Family(
60
+ name="sky130",
61
+ variants=["sky130A", "sky130B"],
62
+ default_variant="sky130A",
63
+ all_libraries=[
64
+ "sky130_fd_io",
65
+ "sky130_fd_pr",
66
+ "sky130_ml_xx_hd",
67
+ "sky130_fd_sc_hd",
68
+ "sky130_fd_sc_hdll",
69
+ "sky130_fd_sc_lp",
70
+ "sky130_fd_sc_hvl",
71
+ "sky130_fd_sc_ls",
72
+ "sky130_fd_sc_ms",
73
+ "sky130_fd_sc_hs",
74
+ "sky130_sram_macros",
75
+ "sky130_fd_pr_reram",
76
+ ],
77
+ default_includes=[
78
+ "sky130_fd_io",
79
+ "sky130_fd_pr",
80
+ "sky130_fd_sc_hd",
81
+ "sky130_fd_sc_hvl",
82
+ "sky130_ml_xx_hd",
83
+ "sky130_sram_macros",
84
+ ],
85
+ repo=opdks_repo,
86
+ )
87
+ Family.by_name["gf180mcu"] = Family(
88
+ name="gf180mcu",
89
+ variants=["gf180mcuA", "gf180mcuB", "gf180mcuC", "gf180mcuD"],
90
+ default_variant="gf180mcuD",
91
+ all_libraries=[
92
+ "gf180mcu_fd_io",
93
+ "gf180mcu_fd_pr",
94
+ "gf180mcu_fd_sc_mcu7t5v0",
95
+ "gf180mcu_fd_sc_mcu9t5v0",
96
+ "gf180mcu_fd_ip_sram",
97
+ "gf180mcu_osu_sc_gp12t3v3",
98
+ "gf180mcu_osu_sc_gp9t3v3",
99
+ ],
100
+ default_includes=[
101
+ "gf180mcu_fd_io",
102
+ "gf180mcu_fd_pr",
103
+ "gf180mcu_fd_sc_mcu7t5v0",
104
+ "gf180mcu_fd_sc_mcu9t5v0",
105
+ "gf180mcu_fd_ip_sram",
106
+ ],
107
+ repo=opdks_repo,
108
+ )
109
+ Family.by_name["ihp_sg13g2"] = Family(
110
+ name="ihp_sg13g2",
111
+ variants=["ihp-sg13g2"],
112
+ all_libraries=[
113
+ "sg13g2_io",
114
+ "sg13g2_pr",
115
+ "sg13g2_sram",
116
+ "sg13g2_stdcell",
117
+ ],
118
+ repo=ihp_repo,
119
+ )
ciel/github.py ADDED
@@ -0,0 +1,184 @@
1
+ # Copyright 2022-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 subprocess
16
+ import sys
17
+ from datetime import datetime
18
+ from dataclasses import dataclass
19
+ from typing import Any, ClassVar, List, Mapping, Optional
20
+
21
+ import httpx
22
+ import ssl
23
+ from .__version__ import __version__
24
+
25
+
26
+ @dataclass
27
+ class RepoInfo:
28
+ owner: str
29
+ name: str
30
+
31
+ @property
32
+ def id(self):
33
+ return f"{self.owner}/{self.name}"
34
+
35
+ @property
36
+ def link(self):
37
+ return f"https://github.com/{self.id}"
38
+
39
+ @property
40
+ def api(self):
41
+ return f"https://api.github.com/repos/{self.id}"
42
+
43
+
44
+ volare_repo = RepoInfo(
45
+ os.getenv("VOLARE_REPO_OWNER", "efabless"),
46
+ os.getenv("VOLARE_REPO_NAME", "volare"),
47
+ )
48
+
49
+ opdks_repo = RepoInfo(
50
+ os.getenv("OPDKS_REPO_OWNER", "RTimothyEdwards"),
51
+ os.getenv("OPDKS_REPO_NAME", "open_pdks"),
52
+ )
53
+
54
+ ihp_repo = RepoInfo(
55
+ os.getenv("IHP_REPO_OWNER", "IHP-GmbH"),
56
+ os.getenv("IHP_REPO_NAME", "IHP-Open-PDK"),
57
+ )
58
+
59
+
60
+ class GitHubSession(httpx.Client):
61
+ class Token(object):
62
+ override: ClassVar[Optional[str]] = None
63
+
64
+ @classmethod
65
+ def get_gh_token(Self) -> Optional[str]:
66
+ token = None
67
+
68
+ # 0. Lowest priority: ghcli
69
+ try:
70
+ token = subprocess.check_output(
71
+ [
72
+ "gh",
73
+ "auth",
74
+ "token",
75
+ ],
76
+ encoding="utf8",
77
+ ).strip()
78
+ except FileNotFoundError:
79
+ pass
80
+ except subprocess.CalledProcessError:
81
+ pass
82
+
83
+ # 1. Higher priority: environment GITHUB_TOKEN
84
+ env_token = os.getenv("GITHUB_TOKEN")
85
+ if env_token is not None and env_token.strip() != "":
86
+ token = env_token
87
+
88
+ # 2. Highest priority: the -t flag
89
+ if Self.override is not None:
90
+ token = Self.override
91
+
92
+ return token
93
+
94
+ def __init__(
95
+ self,
96
+ *,
97
+ follow_redirects: bool = True,
98
+ github_token: Optional[str] = None,
99
+ ssl_context=None,
100
+ **kwargs,
101
+ ):
102
+ if ssl_context is None:
103
+ try:
104
+ import truststore
105
+
106
+ ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
107
+ except ImportError:
108
+ pass
109
+
110
+ try:
111
+ super().__init__(
112
+ follow_redirects=follow_redirects,
113
+ verify=ssl_context,
114
+ **kwargs,
115
+ )
116
+ except ValueError as e:
117
+ if "Unknown scheme for proxy URL" in e.args[0] and "socks://" in e.args[0]:
118
+ print(
119
+ f"Invalid SOCKS proxy: Cielo only supports http://, https:// and socks5:// schemes: {e.args[0]}",
120
+ file=sys.stderr,
121
+ )
122
+ exit(-1)
123
+ else:
124
+ raise e from None
125
+ github_token = github_token or GitHubSession.Token.get_gh_token()
126
+ self.github_token = github_token
127
+
128
+ raw_headers = {
129
+ "User-Agent": type(self).get_user_agent(),
130
+ }
131
+ if github_token is not None:
132
+ raw_headers["Authorization"] = f"Bearer {github_token}"
133
+ self.headers = httpx.Headers(raw_headers)
134
+
135
+ def api(
136
+ self,
137
+ repo: RepoInfo,
138
+ endpoint: str,
139
+ method: str,
140
+ *args,
141
+ **kwargs,
142
+ ) -> Any:
143
+ url = repo.api + endpoint
144
+ req = self.request(method, url, *args, **kwargs)
145
+ req.raise_for_status()
146
+ return req.json()
147
+
148
+ @classmethod
149
+ def get_user_agent(Self) -> str:
150
+ return f"cielo/{__version__}"
151
+
152
+
153
+ def get_commit_date(
154
+ commit: str,
155
+ repo: RepoInfo,
156
+ session: Optional[GitHubSession] = None,
157
+ ) -> Optional[datetime]:
158
+ if session is None:
159
+ session = GitHubSession()
160
+
161
+ try:
162
+ response = session.api(repo, f"/commits/{commit}", "get")
163
+ except httpx.HTTPError:
164
+ return None
165
+
166
+ date = response["commit"]["author"]["date"]
167
+ commit_date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
168
+ return commit_date
169
+
170
+
171
+ def get_releases(session: Optional[GitHubSession] = None) -> List[Mapping[str, Any]]:
172
+ if session is None:
173
+ session = GitHubSession()
174
+
175
+ return session.api(volare_repo, "/releases", "get", params={"per_page": 100})
176
+
177
+
178
+ def get_release_links(
179
+ release: str, session: Optional[GitHubSession] = None
180
+ ) -> Mapping[str, Any]:
181
+ if session is None:
182
+ session = GitHubSession()
183
+
184
+ return session.api(volare_repo, f"/releases/tags/{release}", "get")