diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 32394a6..0000000 --- a/.flake8 +++ /dev/null @@ -1,12 +0,0 @@ -[flake8] -ignore = E203,E231,E266,E302,E501,W503 -exclude = - .git, - __pycache__, - build/, - pdk/ - venv/, - .venv/, - !ciel/build -per-file-ignores = - */__init__.py:F401 diff --git a/.github/scripts/generate_tag.py b/.github/scripts/generate_tag.py deleted file mode 100644 index c3d8afc..0000000 --- a/.github/scripts/generate_tag.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright 2020 Efabless Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os -import sys - -from gh import gh - -sys.path.insert(0, os.getcwd()) - -import ciel # noqa: E402 - -print("Getting tags…") - -latest_tag = None -latest_tag_commit = None -tags = [pair[1] for pair in gh.ciel.tags] - -tag_exists = ciel.__version__ in tags - -if tag_exists: - print("Tag already exists. Leaving NEW_TAG unaltered.") -else: - new_tag = ciel.__version__ - - print("Found new tag %s." % new_tag) - gh.export_env("NEW_TAG", new_tag) diff --git a/.github/scripts/gh.py b/.github/scripts/gh.py deleted file mode 100644 index f8c1416..0000000 --- a/.github/scripts/gh.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2020 Efabless Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os -import subprocess -from types import SimpleNamespace - - -def export_env_default(key, value): - with open(os.getenv("GITHUB_ENV"), "a") as f: - f.write("%s=%s\n" % (key, value)) - - -export_env = export_env_default - - -class Repo(object): - def __init__(self, name, url, branch_rx=None, extraction_rx=None): - print("[Repo Object] Initializing repo %s with URL %s…" % (name, url)) - self.name = name - self.url = url - self.commit = None - self.branch_rx = branch_rx - self.extraction_rx = extraction_rx - - self._latest_commit = None - self._branches = None - self._tags = None - - @property - def latest_commit(self): - if self._latest_commit is None: - print("[Repo Object] Fetching latest commit for %s…" % self.name) - p = subprocess.check_output(["git", "ls-remote", self.url]).decode("utf8") - for line in p.split("\n"): - if "HEAD" in line: - self._latest_commit = line[:40] - return self._latest_commit - - @property - def branches(self): - if self._branches is None: - print("[Repo Object] Fetching branches for %s…" % self.name) - p = subprocess.check_output( - ["git", "ls-remote", "--heads", self.url] - ).decode("utf8") - branches = [] - for line in p.split("\n"): - if line.strip() == "": - continue - - match = line.split() - - hash = match[0] - name = match[1] - - branches.append((hash, name)) - self._branches = branches - return self._branches - - @property - def tags(self): - if self._tags is None: - print("[Repo Object] Fetching tags for %s…" % self.name) - p = subprocess.check_output( - ["git", "ls-remote", "--tags", "--sort=creatordate", self.url] - ).decode("utf8") - - tags = [] - for line in p.split("\n"): - if line.strip() == "": - continue - - match = line.split() - - hash = match[0] - name = match[1].split("/")[2] - - tags.append((hash, name)) - self._tags = tags - return self._tags - - def out_of_date(self): - return self.commit != self.latest_commit - - -if os.getenv("GITHUB_ACTIONS") != "true": - dn = os.path.dirname - git_directory = dn(dn(dn(os.path.realpath(__file__)))) - - def git_command(*args): - return subprocess.check_output(["git"] + list(args), cwd=git_directory).decode( - "utf-8" - )[:-1] - - repo_url = git_command("remote", "get-url", "origin") - branch = git_command("branch", "--show-current") - - os.environ["REPO_URL"] = repo_url - os.environ["GITHUB_WORKSPACE"] = git_directory - os.environ["GITHUB_EVENT_NAME"] = "workspace_dispatch" - os.environ["GITHUB_RUN_ID"] = "mock_gha_run" - - def export_env_alt(key, value): - os.environ[key] = value - print("Setting ENV[%s] to %s..." % (key, value)) - - export_env = export_env_alt - -origin = os.getenv("REPO_URL") -repo = Repo("ciel", origin) - -# public -gh = SimpleNamespace( - **{ - "run_id": os.getenv("GITHUB_RUN_ID"), - "origin": origin, - "root": os.getenv("GITHUB_WORKSPACE"), - "pdk": os.getenv("PDK_ROOT"), - "tool": os.getenv("TOOL"), - "event": SimpleNamespace(**{"name": os.getenv("GITHUB_EVENT_NAME")}), - "export_env": export_env, - "Repo": Repo, - "ciel": repo, - } -) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d79f32e..9b4274c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,16 +4,30 @@ name: CI on: push: branches: - - "*" + - "main" pull_request: +concurrency: + # Behavior: + # - Group all pull requests: latest push cancels previous ones + # - Group all branches: + # - If main/version- branches: run serially + # - Else, latest push cancels previous ones + group: > + ${{ + (github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number)) || + (github.event_name == 'push' && format('branch-{0}', github.ref_name)) || + '' + }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: lint: name: Lint Python Code runs-on: ubuntu-24.04 steps: - name: Check out Git repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install Linters run: make venv - name: Lint @@ -24,7 +38,7 @@ jobs: needs: lint steps: - name: Check Git repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set Up Python uses: actions/setup-python@v4 with: @@ -66,46 +80,51 @@ jobs: runs-on: ubuntu-24.04 if: github.event_name == 'push' && github.ref_name == 'main' outputs: - new_tag: ${{ steps.new_tag.outputs.new_tag }} + version: ${{ steps.get_version.outputs.version }} steps: - name: Check out Git repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - - name: Export Repo URL - run: echo "REPO_URL=https://github.com/${{ github.repository }}" >> $GITHUB_ENV - - name: Set Up Python - uses: actions/setup-python@v4 - with: - python-version: "3.8" - - name: Set default for env.NEW_TAG - run: echo "NEW_TAG=NO_NEW_TAG" >> $GITHUB_ENV - - name: Check for new version + - id: get_version + name: Extract new version run: | - make venv - cd ${GITHUB_WORKSPACE}/ && ./venv/bin/python3 .github/scripts/generate_tag.py - - id: new_tag - name: Set new tag as job output - run: | - echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT + VERSION="$(perl -ne 'print $1 if /^version\s*=\s*\"(.+?)\"/' pyproject.toml)" + if ! gh release view $VERSION 2>&1 > /dev/null; then + echo "Uploading new version $VERSION if CI passes." + echo "version=$VERSION" >> $GITHUB_OUTPUT + fi publish: name: Publish runs-on: ubuntu-24.04 needs: [lint, build, test, check_new_version] - if: needs.check_new_version.outputs.new_tag != 'NO_NEW_TAG' + if: needs.check_new_version.outputs.version != '' environment: pypi permissions: # IMPORTANT: this permission is mandatory for Trusted Publishing id-token: write + contents: write steps: - uses: actions/download-artifact@v8 with: name: wheel path: ./dist - - name: Tag Commit - uses: tvdias/github-tagger@v0.0.1 - with: - tag: "${{ needs.check_new_version.outputs.new_tag }}" - repo-token: "${{ secrets.BOT_TOKEN }}" + # 2nd to last because it is immutable but likely to fail, so if it fails + # I don't want PyPI publishing + - name: Create release + run: | + prerelease_arg=() + version="${{ needs.check_new_version.outputs.version }}" + if [[ "$version" = *b* ]] || [[ "$version" = *a* ]] || [[ "$version" = *.dev* ]] || [[ "$version" = *rc* ]]; then + prerelease_arg+=( "--prerelease" ) + fi + + gh release create $version \ + --generate-notes \ + --target "${{ github.ref_name }}" \ + "${prerelease_arg[@]}" \ + ./dist/* + env: + GH_TOKEN: ${{ github.token }} - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/Authors.md b/Authors.md new file mode 100644 index 0000000..bb21b96 --- /dev/null +++ b/Authors.md @@ -0,0 +1,14 @@ +# Authors + +All categories arranged alphabetically. + +> This list may be non-exhaustive and primarily reflects copyright holders for +> significant portions of the code. See +> https://github.com/librelane/librelane/graphs/contributors for a full list of +> human authors. + +* Efabless Corporation \ (until February 2023) + * Mohamed Gaber \ + * Kareem Farid \ +* Leo Moser \ +* Mohamed Gaber \ diff --git a/Makefile b/Makefile index 7ff983e..086a207 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,8 @@ dist: venv/manifest.txt .PHONY: lint lint: venv/manifest.txt - ./venv/bin/black --check . - ./venv/bin/flake8 . + ./venv/bin/ruff format --check . + ./venv/bin/ruff check ./venv/bin/mypy --check-untyped-defs . venv: venv/manifest.txt diff --git a/OSAcknowledgements b/OSAcknowledgements deleted file mode 100644 index 160b963..0000000 --- a/OSAcknowledgements +++ /dev/null @@ -1,9 +0,0 @@ -sky130-builds - -©2021-2022 The American University in Cairo & The Cloud V Project - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Readme.md b/Readme.md index 85a57f0..c167429 100644 --- a/Readme.md +++ b/Readme.md @@ -1,16 +1,14 @@

🌌 Ciel

License: Apache 2.0 + Python ≥3.8 CI Status Invite to FOSSi Chat - Code Style: Black

Ciel is a version manager (and builder) for builds of open-source process design kits (PDKs).

# Requirements -* Python 3.8+ with PIP -* macOS or GNU/Linux ## macOS Get [Homebrew](https://brew.sh) then: @@ -46,11 +44,11 @@ ciel --version # About the builds In its current inception, ciel supports builds of **sky130** and **gf180mcu** PDKs using [Open-PDKs](https://github.com/rtimothyedwards/open_pdks), including the following libraries: -|sky130|gf180mcu|ihp-sg13g2| +|sky130|gf180mcu|ihp-sg13| |-|-|-| |sky130_fd_io|gf180mcu_fd_io|sg13g2_io| |sky130_fd_pr|gf180mcu_fd_pr|sg13g2_pr| -|sky130_fd_pr_reram|gf180mcu_fd_pr|sg13g2_pr| +|sky130_fd_pr_reram|-|-| |sky130_fd_sc_hd|gf180mcu_fd_sc_mcu7t5v0|sg13g2_stdcell| |sky130_ml_xx_hd|gf180mcu_fd_sc_mcu9t5v0|-| |sky130_fd_sc_hvl|gf180mcu_osu_sc_gp9t3v3|-| diff --git a/ciel/__init__.py b/ciel/__init__.py index 22c4904..1534ad4 100644 --- a/ciel/__init__.py +++ b/ciel/__init__.py @@ -1,3 +1,7 @@ +# Copyright 2026 Ciel Contributors +# +# Adapted from the Volare Project +# # Copyright 2022-2023 Efabless Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,16 +18,33 @@ from .manage import ( VersionNotFound, enable, - get, fetch, ) from .common import ( get_ciel_home, Version, ) -from .families import Family +from .families import ( + Family, + resolve_pdk_family, + resolve_pdk_variant, +) from .github import ( GitHubSession, ) from .build import build from .__version__ import __version__ + +__all__ = [ + "Family", + "GitHubSession", + "Version", + "VersionNotFound", + "__version__", + "build", + "enable", + "fetch", + "get_ciel_home", + "resolve_pdk_family", + "resolve_pdk_variant", +] diff --git a/ciel/__main__.py b/ciel/__main__.py old mode 100755 new mode 100644 index a351741..7af7f38 --- a/ciel/__main__.py +++ b/ciel/__main__.py @@ -1,4 +1,4 @@ -# Copyright 2025 The American University in Cairo +# Copyright 2025 Ciel Contributors # # Adapted from the Volare project # @@ -29,7 +29,7 @@ get_ciel_home, ) from .click_common import ( - opt_pdk_root, + opt_pdk, arg_version, ) from .manage import ( @@ -45,39 +45,31 @@ ) from .github import opt_github_token from .source import opt_data_source -from .families import Family +from .families import Family, resolve_pdk_family @click.command("output") -@opt_pdk_root -def output_cmd(pdk_root, pdk_family): - """Outputs the currently enabled PDK version. - - If not outputting to a tty, the output is either the version string - unembellished, or, if no current version is enabled, an empty output with an - exit code of 1. - """ +@opt_pdk +def output_cmd(pdk_root, pdk_selector): + """Outputs the currently enabled PDK version.""" + pdk_family = resolve_pdk_family(pdk_selector) version = Version.get_current(pdk_root, pdk_family) - if sys.stdout.isatty(): - if version is None: - print( - f"No version of the PDK {pdk_family} is currently enabled at {pdk_root}." - ) - print("Invoke ciel --help for assistance installing and enabling versions.") - exit(1) - else: - print(f"Installed: {pdk_family} v{version.name}") - print("Invoke ciel --help for assistance installing and enabling versions.") - else: - if version is None: - exit(1) - else: - print(version.name, end="") + if version is None: + print( + f"No version of the PDK {pdk_family} is currently enabled at {pdk_root}.", + file=sys.stderr, + ) + print( + "Invoke ciel --help for assistance installing and enabling versions.", + file=sys.stderr, + ) + sys.exit(1) + print(version.name, end="") @click.command("prune") -@opt_pdk_root +@opt_pdk @click.option( "--yes", is_flag=True, @@ -85,9 +77,10 @@ def output_cmd(pdk_root, pdk_family): expose_value=False, prompt="Are you sure? This will delete all non-enabled versions of the PDK from your computer.", ) -def prune_cmd(pdk_root, pdk_family): +def prune_cmd(pdk_root, pdk_selector): """Removes all PDKs other than, if it exists, the one currently set as 'enabled' in the PDK root.""" + pdk_family = resolve_pdk_family(pdk_selector) pdk_versions = Version.get_all_installed(pdk_root, pdk_family) for version in pdk_versions: if version.is_current(pdk_root): @@ -100,7 +93,7 @@ def prune_cmd(pdk_root, pdk_family): @click.command("optimize") -@opt_pdk_root +@opt_pdk @arg_version def optimize_cmd(pdk_root, pdk_family, version): """ @@ -120,8 +113,8 @@ def optimize_cmd(pdk_root, pdk_family, version): @click.command("optimize-all") -@opt_pdk_root -def optimize_all_cmd(pdk_root, pdk_family): +@opt_pdk +def optimize_all_cmd(pdk_root, pdk_selector): """ [Experimental] This command attempts to save space by converting identical files across variants for all versions of a specific PDK family to symbolic @@ -134,6 +127,7 @@ def optimize_all_cmd(pdk_root, pdk_family): """ recovered = 0 + pdk_family = resolve_pdk_family(pdk_selector) for version in Version.get_all_installed(pdk_root, pdk_family): recovered += optimize(pdk_root, version) @@ -142,7 +136,7 @@ def optimize_all_cmd(pdk_root, pdk_family): @click.command("rm") -@opt_pdk_root +@opt_pdk @click.option( "--yes", is_flag=True, @@ -151,25 +145,32 @@ def optimize_all_cmd(pdk_root, pdk_family): prompt="Are you sure? This will delete this version of the PDK from your computer.", ) @arg_version -def rm_cmd(pdk_root, pdk_family, version): +def rm_cmd(pdk_root, pdk_selector, version): """Removes the PDK version specified.""" + pdk_family = resolve_pdk_family(pdk_selector) version_object = Version(version, pdk_family) try: version_object.uninstall(pdk_root) print(f"Deleted {version}.") except Exception as e: print(f"Failed to delete: {e}", file=sys.stderr) - exit(1) + sys.exit(1) @click.command("ls") @opt_data_source @opt_github_token -@opt_pdk_root -def list_cmd(data_source, pdk_root, pdk_family): - """Lists PDK versions that are locally installed. JSON if not outputting to a tty.""" +@opt_pdk +def list_cmd(data_source, pdk_root, pdk_selector): + """ + Lists PDK versions that are locally installed. + + If not outputting to a tty, each version will be output on its own line + in plain text. + """ + pdk_family = resolve_pdk_family(pdk_selector) pdk_versions = Version.get_all_installed(pdk_root, pdk_family) if sys.stdout.isatty(): @@ -182,16 +183,23 @@ def list_cmd(data_source, pdk_root, pdk_family): installed_list=pdk_versions, ) else: - print(json.dumps([version.name for version in pdk_versions]), end="") + for version in pdk_versions: + print(version.name) @click.command("ls-remote") @opt_github_token @opt_data_source -@opt_pdk_root -def list_remote_cmd(data_source, pdk_root, pdk_family): - """Lists PDK versions that are remotely available. JSON if not outputting to a tty.""" +@opt_pdk +def list_remote_cmd(data_source, pdk_root, pdk_selector): + """ + Lists PDK versions that are remotely available. + If not outputting to a tty, each version will be output on its own line + in plain text. + """ + + pdk_family = resolve_pdk_family(pdk_selector) try: pdk_versions = data_source.get_available_versions(pdk_family) @@ -202,40 +210,32 @@ def list_remote_cmd(data_source, pdk_root, pdk_family): for version in pdk_versions: print(version.name) except ValueError as e: - if sys.stdout.isatty(): - console = Console() - console.print(f"[red]{e}") - else: - print(f"{e}", file=sys.stderr) + console = Console(stderr=True) + console.print(f"[red]{e}") sys.exit(-1) except httpx.HTTPStatusError as e: - if sys.stdout.isatty(): - console = Console() - console.print(f"[red]Encountered an error when polling version list: {e}") - else: - print(f"Failed to get version list: {e}", file=sys.stderr) + console = Console(stderr=True) + console.print(f"[red]Encountered an error when polling version list: {e}") sys.exit(-1) except httpx.NetworkError as e: - if sys.stdout.isatty(): - console = Console() - console.print( - "[red]You don't appear to be connected to the Internet. ls-remote cannot be used." - ) - else: - print(f"Failed to connect to remote server: {e}", file=sys.stderr) + console = Console(stderr=True) + console.print( + f"[red]You don't appear to be connected to the Internet. ls-remote cannot be used.: {e}" + ) sys.exit(-1) @click.command("path") -@opt_pdk_root +@opt_pdk @arg_version -def path_cmd(pdk_root, pdk_family, version): +def path_cmd(pdk_root, pdk_selector, version): """ Prints the path of the ciel PDK root. If a version is provided over the commandline, it prints the path to this version instead. """ + pdk_family = resolve_pdk_family(pdk_selector) if version is not None: version = Version(version, pdk_family) print(version.get_dir(pdk_root), end="") @@ -246,7 +246,7 @@ def path_cmd(pdk_root, pdk_family, version): @click.command("enable") @opt_data_source @opt_github_token -@opt_pdk_root +@opt_pdk @click.option( "-l", "--include-libraries", @@ -258,7 +258,7 @@ def path_cmd(pdk_root, pdk_family, version): def enable_cmd( data_source, pdk_root, - pdk_family, + pdk_selector, version, include_libraries, ): @@ -274,7 +274,7 @@ def enable_cmd( try: enable( pdk_root, - pdk_family, + pdk_selector, version, include_libraries=include_libraries, output=console, @@ -282,13 +282,13 @@ def enable_cmd( ) except Exception as e: console.print(f"[red]{e}") - exit(-1) + sys.exit(-1) @click.command("fetch") @opt_data_source @opt_github_token -@opt_pdk_root +@opt_pdk @click.option( "-l", "--include-libraries", @@ -300,7 +300,7 @@ def enable_cmd( def fetch_cmd( data_source, pdk_root, - pdk_family, + pdk_selector, version, include_libraries, ): @@ -316,10 +316,10 @@ def fetch_cmd( try: version = fetch( + pdk_root, + pdk_selector, + version, data_source=data_source, - pdk_root=pdk_root, - pdk=pdk_family, - version=version, include_libraries=include_libraries, output=console, ) @@ -327,7 +327,7 @@ def fetch_cmd( except Exception as e: console.print(f"[red]{e}") - exit(-1) + sys.exit(-1) @click.command("ls-pdks") @@ -399,7 +399,7 @@ def cli(): file=sys.stderr, ) print("This is a fatal error. Ciel will now quit.", file=sys.stderr) - exit(-1) + sys.exit(-1) if __name__ == "__main__": diff --git a/ciel/__version__.py b/ciel/__version__.py index cf5f027..587a56f 100644 --- a/ciel/__version__.py +++ b/ciel/__version__.py @@ -26,7 +26,9 @@ def __get_version(): repo_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) pyproject_path = os.path.join(repo_directory, "pyproject.toml") try: - match = rx.search(open(pyproject_path, encoding="utf8").read()) + with open(pyproject_path, encoding="utf8") as f: + pyproject_toml_str = f.read() + match = rx.search(pyproject_toml_str) assert match is not None, "pyproject.toml found, but without a version" return match[1] except FileNotFoundError: diff --git a/ciel/build/__init__.py b/ciel/build/__init__.py index 18918c2..86c7327 100644 --- a/ciel/build/__init__.py +++ b/ciel/build/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 The American University in Cairo +# Copyright 2025 Ciel Contributors # # Adapted from the Volare project # @@ -16,13 +16,14 @@ # See the License for the specific language governing permissions and # limitations under the License. import os +import sys import uuid import pathlib import tarfile import tempfile import importlib import subprocess -from typing import Optional, List, Dict +from typing import Optional, List, Dict, Tuple, Union import click import zstandard as zstd @@ -42,15 +43,15 @@ from ..click_common import ( opt_push, opt_build, - opt_pdk_root, + opt_pdk, arg_version, ) -from ..families import Family +from ..families import Family, resolve_pdk_selector def build( pdk_root: str, - pdk_family: str, + pdk: Union[str, Tuple[str, str]], version: str, jobs: int = 1, sram: bool = True, # Deprecated @@ -58,17 +59,23 @@ def build( include_libraries: Optional[List[str]] = None, use_repo_at: Optional[List[str]] = None, ): + if isinstance(pdk, tuple): + pdk_family_name, pdk_variant_name = pdk + else: + pdk_family_name, pdk_variant_name = resolve_pdk_selector(pdk) + use_repos = {} if use_repo_at is not None: for repo in use_repo_at: name, path = repo.split("=") use_repos[name] = os.path.abspath(path) - if pdk_family not in Family.by_name: - raise Exception(f"Unsupported PDK family '{pdk_family}'.") + if pdk_family_name not in Family.by_name: + raise ValueError(f"Unsupported PDK family '{pdk_family_name}'.") kwargs = { "pdk_root": pdk_root, + "pdk_variant": pdk_variant_name, "version": version, "jobs": jobs, "clear_build_artifacts": clear_build_artifacts, @@ -76,21 +83,21 @@ def build( "using_repos": use_repos, } - build_module = importlib.import_module(f".{pdk_family}", package=__name__) + build_module = importlib.import_module(f".{pdk_family_name}", package=__name__) build_function = build_module.build build_function(**kwargs) @click.command("build") @opt_github_token -@opt_pdk_root +@opt_pdk @opt_build @arg_version def build_cmd( include_libraries, jobs, pdk_root, - pdk_family, + pdk_selector, clear_build_artifacts, version, use_repo_at, @@ -109,7 +116,7 @@ def build_cmd( build( pdk_root=pdk_root, - pdk_family=pdk_family, + pdk=pdk_selector, version=version, jobs=jobs, clear_build_artifacts=clear_build_artifacts, @@ -120,7 +127,7 @@ def build_cmd( def push( pdk_root, - pdk_family, + pdk, version, *, owner, @@ -128,7 +135,14 @@ def push( pre=False, push_libraries=None, ): - family = Family.by_name[pdk_family] + # variant doesn't matter, we're pushing whatever we can unless an explicit + # list is provided + if isinstance(pdk, tuple): + pdk_family_name, _ = pdk + else: + pdk_family_name, _ = resolve_pdk_selector(pdk) + + pdk_family = Family.by_name[pdk_family_name] session = GitHubSession() if session.github_token is None: @@ -137,10 +151,10 @@ def push( console = Console() if push_libraries is None or len(push_libraries) == 0: - push_libraries = family.all_libraries + push_libraries = pdk_family.all_libraries library_list = set(push_libraries) - version_object = Version(version, pdk_family) + version_object = Version(version, pdk_family_name) version_directory = version_object.get_dir(pdk_root) if not os.path.isdir(version_directory): raise FileNotFoundError(f"Version {version} not found.") @@ -171,25 +185,26 @@ def push( for name, files in collections.items(): tarball_path = os.path.join(tarball_directory, f"{name}.tar.zst") task = progress.add_task(f"Compressing {name}…", total=len(files)) - with zstd.open(tarball_path, mode="wb") as stream: - with tarfile.TarFile(fileobj=stream, mode="w") as tf: - for i, file in enumerate(files): - progress.update(task, completed=i + 1) - path_in_tarball = os.path.relpath(file, version_directory) - tf.add(file, arcname=path_in_tarball) + with zstd.open(tarball_path, mode="wb") as stream, tarfile.TarFile( + fileobj=stream, mode="w" + ) as tf: + for i, file in enumerate(files): + progress.update(task, completed=i + 1) + path_in_tarball = os.path.relpath(file, version_directory) + tf.add(file, arcname=path_in_tarball) console.log(f"\nCompressed to {tarball_path}.") progress.remove_task(task) final_tarballs.append(tarball_path) - tag = f"{pdk_family}-{version}" + tag = f"{pdk_family_name}-{version}" # If someone wants to rewrite this to not use ghr, please, by all means. console.log("Starting upload…") - body = f"{pdk_family} variants built using ciel" - date = get_commit_date(version, family.repo, session) + body = f"{pdk_family_name} variants built using ciel" + date = get_commit_date(version, pdk_family.repo, session) if date is not None: - body = f"{pdk_family} variants (released on {date_to_iso8601(date)})" + body = f"{pdk_family_name} variants (released on {date_to_iso8601(date)})" for tarball_path in final_tarballs: subprocess.check_call( @@ -218,7 +233,7 @@ def push( @click.command("push", hidden=True) @opt_github_token -@opt_pdk_root +@opt_pdk @opt_push @click.argument("version") def push_cmd( @@ -226,7 +241,7 @@ def push_cmd( repository, pre, pdk_root, - pdk_family, + pdk_selector, version, push_libraries, ): @@ -241,7 +256,7 @@ def push_cmd( try: push( pdk_root, - pdk_family, + pdk_selector, version, owner=owner, repository=repository, @@ -250,4 +265,4 @@ def push_cmd( ) except Exception as e: console.print(f"[red]Failed to push version: {e}") - exit(-1) + sys.exit(-1) diff --git a/ciel/build/common.py b/ciel/build/common.py index f0bbb56..d4ecdd7 100644 --- a/ciel/build/common.py +++ b/ciel/build/common.py @@ -13,6 +13,7 @@ # limitations under the License. import os import re +import sys import shutil import subprocess @@ -66,8 +67,9 @@ def open_pdks_patch_gnu_sed(at_path: str): shutil.move(at_path, backup_path) with open(backup_path, "r") as file_in, open(at_path, "w") as file_out: - for line in file_in: - file_out.write(line.replace("${SED} -i ", "${SED} -i.bak ")) + file_out.writelines( + line.replace("${SED} -i ", "${SED} -i.bak ") for line in file_in + ) def patch_open_pdks(at_path: str): @@ -98,7 +100,7 @@ def is_ancestor(commit: str): print( f"Commit {head} cannot be built using Ciel: the minimum version of open_pdks buildable with Ciel is 1.0.381." ) - exit(-1) + sys.exit(-1) gf180mcu_sources_ok = is_ancestor("c1e2118846fd216b2c065a216950e75d2d67ccb8") if not gf180mcu_sources_ok: diff --git a/ciel/build/gf180mcu.py b/ciel/build/gf180mcu.py index 62022d7..9cd17f2 100644 --- a/ciel/build/gf180mcu.py +++ b/ciel/build/gf180mcu.py @@ -1,3 +1,7 @@ +# Copyright 2026 Ciel Contributors +# +# Adapted from Volare +# # Copyright 2022-2023 Efabless Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,6 +17,7 @@ # limitations under the License. import os import io +import sys import json import shlex import shutil @@ -46,18 +51,19 @@ def get_open_pdks( open_pdks_repo = None if repo_path is None: - with Progress() as progress: - with ThreadPoolExecutor(max_workers=jobs) as executor: - gmc = GitMultiClone(build_directory, progress) - open_pdks_future = executor.submit( - GitMultiClone.clone, - gmc, - opdks_repo.link, - version, - default_branch="main", - ) - open_pdks_repo = open_pdks_future.result() - repo_path = open_pdks_repo.path + with Progress() as progress, ThreadPoolExecutor( + max_workers=jobs + ) as executor: + gmc = GitMultiClone(build_directory, progress) + open_pdks_future = executor.submit( + GitMultiClone.clone, + gmc, + opdks_repo.link, + version, + default_branch="main", + ) + open_pdks_repo = open_pdks_future.result() + repo_path = open_pdks_repo.path console.log(f"Done fetching {open_pdks_repo.name}.") else: @@ -66,7 +72,8 @@ def get_open_pdks( patch_open_pdks(repo_path) try: - json_raw = open(f"{repo_path}/gf180mcu/gf180mcu.json").read() + with open(f"{repo_path}/gf180mcu/gf180mcu.json", encoding="utf8") as f: + json_raw = f.read() cpp = pcpp.Preprocessor() cpp.line_directive = None cpp.parse(json_raw) @@ -91,7 +98,7 @@ def get_open_pdks( except subprocess.CalledProcessError as e: print(e) print(e.stderr) - exit(-1) + sys.exit(-1) LIB_FLAG_MAP = { @@ -120,29 +127,29 @@ def build_variants( console = Console() def run_sh(script, log_to): - output_file = open(log_to, "w") - try: - subprocess.check_call( - ["sh", "-c", script], - cwd=open_pdks_path, - stdout=output_file, - stderr=output_file, - stdin=open(os.devnull), - ) - except subprocess.CalledProcessError as e: - console.log( - f"An error occurred while building the PDK. Check {log_to} for more information." - ) - raise e - - library_flags = set([LIB_FLAG_MAP[library] for library in include_libraries]) - library_flags_disable = set( - [ - LIB_FLAG_MAP[library].replace("enable", "disable") - for library in LIB_FLAG_MAP - if library not in include_libraries - ] - ) + with open(log_to, "w", encoding="utf8") as output_file, open( + os.devnull, encoding="utf8" + ) as devnull: + try: + subprocess.check_call( + ["sh", "-c", script], + cwd=open_pdks_path, + stdout=output_file, + stderr=output_file, + stdin=devnull, + ) + except subprocess.CalledProcessError as e: + console.log( + f"An error occurred while building the PDK. Check {log_to} for more information." + ) + raise e from None + + library_flags = {LIB_FLAG_MAP[library] for library in include_libraries} + library_flags_disable = { + LIB_FLAG_MAP[library].replace("enable", "disable") + for library in LIB_FLAG_MAP + if library not in include_libraries + } magic_dirname = os.path.dirname(magic_bin) configuration_flags = ["--enable-gf180mcu-pdk", "--with-reference"] + list( @@ -188,7 +195,7 @@ def run_sh(script, log_to): except subprocess.CalledProcessError as e: print(e) print(e.stderr) - exit(-1) + sys.exit(-1) def install_gf180mcu(build_directory, pdk_root, version): @@ -225,6 +232,7 @@ def install_gf180mcu(build_directory, pdk_root, version): def build( pdk_root: str, + pdk_variant: str, version: str, jobs: int = 1, clear_build_artifacts: bool = True, @@ -232,7 +240,7 @@ def build( using_repos: Optional[Dict[str, str]] = None, ): family = Family.by_name["gf180mcu"] - library_set = family.resolve_libraries(include_libraries) + library_set = family.resolve_libraries(include_libraries, pdk_variant) if using_repos is None: using_repos = {} @@ -252,7 +260,7 @@ def build( magic_bin = shutil.which("magic") if magic_bin is None: print("Magic is either not installed or not in PATH.") - exit(-1) + sys.exit(-1) build_variants( magic_bin, diff --git a/ciel/build/git_multi_clone.py b/ciel/build/git_multi_clone.py index 48e91ed..927a5f3 100644 --- a/ciel/build/git_multi_clone.py +++ b/ciel/build/git_multi_clone.py @@ -22,7 +22,7 @@ from ..common import mkdirp -class Repository(object): +class Repository: @classmethod def from_path(Self, path): name = os.path.basename(path) @@ -30,9 +30,11 @@ def from_path(Self, path): ["git", "remote", "get-url", "origin"], stderr=subprocess.PIPE ).strip() - remote_branch_info = open( - os.path.join(path, ".git", "refs", "remotes", "origin", "HEAD") - ).read() + with open( + os.path.join(path, ".git", "refs", "remotes", "origin", "HEAD"), + encoding="utf8", + ) as f: + remote_branch_info = f.read() remote_branch = os.path.basename(remote_branch_info) return Self(name, url, path, remote_branch) @@ -78,9 +80,8 @@ def clone(self, callback=None): break if char_read in ["\n", "\r"]: match = ro_rx.search(buffer) - if match is not None: - if callback is not None: - callback(int(match[1])) + if match is not None and callback is not None: + callback(int(match[1])) buffer = "" else: buffer += char_read @@ -122,9 +123,8 @@ def pull(self, callback=None): break if char_read in ["\n", "\r"]: match = ro_rx.search(buffer) - if match is not None: - if callback is not None: - callback(int(match[1])) + if match is not None and callback is not None: + callback(int(match[1])) buffer = "" else: buffer += char_read @@ -145,7 +145,7 @@ def checkout_commit(self, commit: str): cwd=self.path, stderr=subprocess.PIPE, ) - except Exception: + except Exception: # noqa: S110 pass subprocess.check_output( ["git", "checkout", "-f", "-b", "current", commit], @@ -177,9 +177,8 @@ def init_submodule(self, submodule: Optional[str] = None, callback=None): break if char_read in ["\n", "\r"]: match = ro_rx.search(buffer) - if match is not None: - if callback is not None: - callback(int(match[1])) + if match is not None and callback is not None: + callback(int(match[1])) buffer = "" else: buffer += char_read @@ -187,7 +186,7 @@ def init_submodule(self, submodule: Optional[str] = None, callback=None): process.wait() -class GitMultiClone(object): +class GitMultiClone: progress: Progress def __init__(self, folder, progress): diff --git a/ciel/build/ihp-sg13g2.py b/ciel/build/ihp-sg13.py similarity index 64% rename from ciel/build/ihp-sg13g2.py rename to ciel/build/ihp-sg13.py index 12d8e1d..44d0670 100644 --- a/ciel/build/ihp-sg13g2.py +++ b/ciel/build/ihp-sg13.py @@ -1,3 +1,7 @@ +# Copyright 2025 Ciel Contributors +# +# Adapted from the Volare project +# # Copyright 2022-2023 Efabless Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,8 +16,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import os +import sys import shutil import subprocess +from pathlib import Path from datetime import datetime from typing import Optional, List, Tuple, Dict from concurrent.futures import ThreadPoolExecutor @@ -38,21 +44,22 @@ def get_ihp( console = Console() if repo_path is None: - with Progress() as progress: - with ThreadPoolExecutor(max_workers=jobs) as executor: - gmc = GitMultiClone(build_directory, progress) - ihp_future = executor.submit( - GitMultiClone.clone, - gmc, - ihp_repo.link, - version, - ) - repo = ihp_future.result() - current_task = progress.add_task("Updating submodules…", total=100) - repo.init_submodule( - callback=lambda x: progress.update(current_task, completed=x) - ) - repo_path = repo.path + with Progress() as progress, ThreadPoolExecutor( + max_workers=jobs + ) as executor: + gmc = GitMultiClone(build_directory, progress) + ihp_future = executor.submit( + GitMultiClone.clone, + gmc, + ihp_repo.link, + version, + ) + repo = ihp_future.result() + current_task = progress.add_task("Updating submodules…", total=100) + repo.init_submodule( + callback=lambda x: progress.update(current_task, completed=x) + ) + repo_path = repo.path console.log(f"Done fetching {ihp_repo.name}.") else: console.log(f"Using IHP-Open-PDK at {repo_path} unaltered.") @@ -62,30 +69,42 @@ def get_ihp( except subprocess.CalledProcessError as e: print(e) print(e.stderr) - exit(-1) + sys.exit(-1) def build_ihp(build_directory, ihp_path): # """Build""" + def filter(dir_s, files): + dir = Path(dir_s) + if dir.name == ".git": + return files + rejects = [".git", ".DS_Store"] + for file in files: + # ignore bad symlinks + if not (Path(dir) / file).resolve().exists(): + rejects.append(file) + return rejects + + ihp_sg13_family = Family.by_name["ihp-sg13"] try: - shutil.rmtree(os.path.join(build_directory, "ihp-sg13g2")) + for variant in ihp_sg13_family.variants: + shutil.rmtree(os.path.join(build_directory, variant)) except FileNotFoundError: pass - shutil.copytree( - os.path.join(ihp_path, "ihp-sg13g2"), - os.path.join(build_directory, "ihp-sg13g2"), - ignore=lambda dir, files: ( - files if ".git" in os.path.split(dir) else [".git", ".DS_Store"] - ), - ) + for variant in ihp_sg13_family.variants: + shutil.copytree( + os.path.join(ihp_path, variant), + os.path.join(build_directory, variant), + ignore=filter, + ) def install_ihp(build_directory, pdk_root, version): console = Console() with console.status("Adding build to list of installed versions…"): - ihp_sg13g2_family = Family.by_name["ihp-sg13g2"] + ihp_sg13_family = Family.by_name["ihp-sg13"] - version_directory = Version(version, "ihp-sg13g2").get_dir(pdk_root) + version_directory = Version(version, "ihp-sg13").get_dir(pdk_root) if ( os.path.exists(version_directory) and len(os.listdir(version_directory)) != 0 @@ -94,9 +113,7 @@ def install_ihp(build_directory, pdk_root, version): it = 0 while os.path.exists(backup_path) and len(os.listdir(backup_path)) != 0: it += 1 - backup_path = Version(f"{version}.bk{it}", "ihp-sg13g2").get_dir( - pdk_root - ) + backup_path = Version(f"{version}.bk{it}", "ihp-sg13").get_dir(pdk_root) console.log( f"Build already found at {version_directory}, moving to {backup_path}…" ) @@ -105,7 +122,7 @@ def install_ihp(build_directory, pdk_root, version): console.log("Copying…") mkdirp(version_directory) - for variant in ihp_sg13g2_family.variants: + for variant in ihp_sg13_family.variants: variant_build_path = os.path.join(build_directory, variant) variant_install_path = os.path.join(version_directory, variant) if os.path.isdir(variant_build_path): @@ -116,6 +133,7 @@ def install_ihp(build_directory, pdk_root, version): def build( pdk_root: str, + pdk_variant: str, version: str, jobs: int = 1, clear_build_artifacts: bool = True, @@ -131,10 +149,8 @@ def build( if using_repos is None: using_repos = {} - build_directory = os.path.join( - get_ciel_dir(pdk_root, "ihp-sg13g2"), "build", version - ) - timestamp = datetime.now().strftime("build_ihp-sg13g2-%Y-%m-%d-%H-%M-%S") + build_directory = os.path.join(get_ciel_dir(pdk_root, "ihp-sg13"), "build", version) + timestamp = datetime.now().strftime("build_ihp-sg13-%Y-%m-%d-%H-%M-%S") log_dir = os.path.join(build_directory, "logs", timestamp) mkdirp(log_dir) diff --git a/ciel/build/sky130.py b/ciel/build/sky130.py index 6f25432..c2bd5a1 100644 --- a/ciel/build/sky130.py +++ b/ciel/build/sky130.py @@ -1,3 +1,7 @@ +# Copyright 2026 Ciel Contributors +# +# Adapted from Volare +# # Copyright 2022-2023 Efabless Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,6 +17,7 @@ # limitations under the License. import os import io +import sys import json import venv import shlex @@ -47,18 +52,19 @@ def get_open_pdks( open_pdks_repo = None if repo_path is None: - with Progress() as progress: - with ThreadPoolExecutor(max_workers=jobs) as executor: - gmc = GitMultiClone(build_directory, progress) - open_pdks_future = executor.submit( - GitMultiClone.clone, - gmc, - opdks_repo.link, - version, - default_branch="main", - ) - open_pdks_repo = open_pdks_future.result() - repo_path = open_pdks_repo.path + with Progress() as progress, ThreadPoolExecutor( + max_workers=jobs + ) as executor: + gmc = GitMultiClone(build_directory, progress) + open_pdks_future = executor.submit( + GitMultiClone.clone, + gmc, + opdks_repo.link, + version, + default_branch="main", + ) + open_pdks_repo = open_pdks_future.result() + repo_path = open_pdks_repo.path console.log(f"Done fetching {open_pdks_repo.name}.") else: @@ -67,7 +73,8 @@ def get_open_pdks( patch_open_pdks(repo_path) try: - json_raw = open(f"{repo_path}/sky130/sky130.json").read() + with open(f"{repo_path}/sky130/sky130.json", encoding="utf8") as f: + json_raw = f.read() cpp = pcpp.Preprocessor() cpp.line_directive = None cpp.parse(json_raw) @@ -92,7 +99,7 @@ def get_open_pdks( except subprocess.CalledProcessError as e: print(e) print(e.stderr) - exit(-1) + sys.exit(-1) def build_sky130_timing(build_directory, sky130_path, log_dir, jobs=1): @@ -127,7 +134,7 @@ def build_sky130_timing(build_directory, sky130_path, log_dir, jobs=1): set -e source {venv_path}/bin/activate python3 -m pip install wheel - python3 -m pip install {os.path.join(sky130_path, 'scripts', 'python-skywater-pdk')} + python3 -m pip install {os.path.join(sky130_path, "scripts", "python-skywater-pdk")} """, ], stdout=out, @@ -174,7 +181,7 @@ def do_submodule(submodule: str): except subprocess.CalledProcessError as e: print(e) print(e.stderr) - exit(-1) + sys.exit(-1) LIB_FLAG_MAP = { @@ -206,33 +213,33 @@ def build_variants( console = Console() def run_sh(script, log_to): - output_file = open(log_to, "w") - output_file.write(script + "\n") - output_file.write("---\n") - output_file.flush() - try: - subprocess.check_call( - ["sh", "-c", script], - cwd=open_pdks_path, - stdout=output_file, - stderr=output_file, - stdin=open(os.devnull), - ) - except subprocess.CalledProcessError as e: - console.log( - f"An error occurred while building the PDK. Check {log_to} for more information." - ) - raise e + with open(log_to, "w", encoding="utf8") as output_file, open( + os.devnull, encoding="utf8" + ) as devnull: + output_file.write(script + "\n") + output_file.write("---\n") + output_file.flush() + try: + subprocess.check_call( + ["sh", "-c", script], + cwd=open_pdks_path, + stdout=output_file, + stderr=output_file, + stdin=devnull, + ) + except subprocess.CalledProcessError as e: + console.log( + f"An error occurred while building the PDK. Check {log_to} for more information." + ) + raise e from None magic_dirname = os.path.dirname(magic_bin) - library_flags = set([LIB_FLAG_MAP[library] for library in include_libraries]) - library_flags_disable = set( - [ - LIB_FLAG_MAP[library].replace("enable", "disable") - for library in LIB_FLAG_MAP - if library not in include_libraries - ] - ) + library_flags = {LIB_FLAG_MAP[library] for library in include_libraries} + library_flags_disable = { + LIB_FLAG_MAP[library].replace("enable", "disable") + for library in LIB_FLAG_MAP + if library not in include_libraries + } configuration_flags = ["--enable-sky130-pdk", "--with-reference"] + list( library_flags.union(library_flags_disable) @@ -278,7 +285,7 @@ def run_sh(script, log_to): except subprocess.CalledProcessError as e: print(e) print(e.stderr) - exit(-1) + sys.exit(-1) def install_sky130(build_directory, pdk_root, version): @@ -315,6 +322,7 @@ def install_sky130(build_directory, pdk_root, version): def build( pdk_root: str, + pdk_variant: str, version: str, jobs: int = 1, clear_build_artifacts: bool = True, @@ -322,7 +330,7 @@ def build( using_repos: Optional[Dict[str, str]] = None, ): family = Family.by_name["sky130"] - library_set = family.resolve_libraries(include_libraries) + library_set = family.resolve_libraries(include_libraries, pdk_variant) if using_repos is None: using_repos = {} @@ -342,7 +350,7 @@ def build( magic_bin = shutil.which("magic") if magic_bin is None: print("Magic is either not installed or not in PATH.") - exit(-1) + sys.exit(-1) build_variants( magic_bin, @@ -351,7 +359,7 @@ def build( library_set, log_dir, jobs, - ), + ) install_sky130(build_directory, pdk_root, version) if clear_build_artifacts: diff --git a/ciel/click_common.py b/ciel/click_common.py index ea5a20b..5c35a70 100644 --- a/ciel/click_common.py +++ b/ciel/click_common.py @@ -1,4 +1,4 @@ -# Copyright 2025 The American University in Cairo +# Copyright 2025 Ciel Contributors # # Adapted from the Volare project # @@ -22,10 +22,9 @@ from .common import ( CIEL_RESOLVED_HOME, - resolve_pdk_family, resolve_version, ) -from .families import Family +from .families import Family, resolve_pdk_family opt = partial(click.option, show_default=True) @@ -93,21 +92,22 @@ def process_value(self, ctx: click.Context, value): value = self.callback(ctx, self, value) try: - resolved = resolve_pdk_family(value) + resolve_pdk_family(value) except ValueError as e: raise click.BadParameter(str(e), ctx=ctx, param=self) - return resolved + return value # pass selector as is, we just needed to validate it -def opt_pdk_root(function: Callable): +def opt_pdk(function: Callable): function = opt( "--pdk-family", "--pdk", + "pdk_selector", cls=PDKOption, required=True, envvar=["PDK_FAMILY", "PDK"], - help="A valid PDK family or variant (the latter of which is resolved to a family). If the environment PDK_FAMILY or PDK are set, they are used as secondary sources for this value.", + help="A valid PDK family or variant. If the environment PDK_FAMILY or PDK are set, they are used as secondary sources for this value.", )(function) function = opt( "--pdk-root", diff --git a/ciel/common.py b/ciel/common.py index 8cd6703..ee73659 100644 --- a/ciel/common.py +++ b/ciel/common.py @@ -1,4 +1,4 @@ -# Copyright 2025 The American University in Cairo +# Copyright 2025 Ciel Contributors # # Modified from the Volare project # @@ -17,8 +17,7 @@ # limitations under the License. import os import shutil -import pathlib -import warnings +from pathlib import Path from datetime import datetime from dataclasses import dataclass from typing import Optional, List @@ -38,7 +37,7 @@ def date_from_iso8601(string: str) -> datetime: def mkdirp(path): - return pathlib.Path(path).mkdir(parents=True, exist_ok=True) + return Path(path).mkdir(parents=True, exist_ok=True) # -- API Variables @@ -54,94 +53,28 @@ def _get_current_version(pdk_root: str, pdk: str) -> Optional[str]: mkdirp(current_file_dir) version = None try: - version = open(current_file).read().strip() + with open(current_file, encoding="utf8") as f: + version = f.read().strip() except FileNotFoundError: pass return version -def get_ciel_home(pdk_root: Optional[str] = None) -> str: - return pdk_root or CIEL_RESOLVED_HOME +def get_ciel_home(pdk_root: Optional[str] = None) -> Path: + return Path(pdk_root or CIEL_RESOLVED_HOME) -def get_ciel_dir(pdk_root: str, pdk: str) -> str: - return os.path.join(pdk_root, "ciel", pdk) +def get_ciel_dir(pdk_root: str, pdk: str) -> Path: + return Path(pdk_root) / "ciel" / pdk -def get_versions_dir(pdk_root: str, pdk: str) -> str: - return os.path.join(get_ciel_dir(pdk_root, pdk), "versions") - - -def resolve_pdk_family(selector: Optional[str]): - """ - :returns: - If selector is a valid PDK family, the same string. - - If selector is a valid PDK variant, the family the variant belongs to. - - If selector is None, the PDK_FAMILY and PDK environment variables are - used as fallbacks. If all are None, the function will simply return None. - - Starting Ciel 3.0.0, supplying None will no longer work and the selector - will be a string. - - If the selector is invalid, a ValueError will be raised. "ihp_sg13g2" - will resolve to "ihp-sg13g2" however for some semblance of backwards - compatibility with previous versions of Ciel/Volare. - """ - if selector is None: - warnings.warn( - "Passing None to resolve_pdk_family is deprecated and will be removed in Ciel 3.0.0. Please resolve any environment variables manually.", - DeprecationWarning, - stacklevel=2, - ) - if environment_specified_pdk := os.getenv("PDK_FAMILY") or os.getenv("PDK"): - selector = environment_specified_pdk - if selector is None: - return None - - if selector == "ihp_sg13g2": - selector = "ihp-sg13g2" - - if selector in Family.by_name: - return selector - - for pdk_family in Family.by_name.values(): - if selector in pdk_family.variants: - return pdk_family.name - - raise ValueError(f"'{selector}' is not a valid PDK family or variant.") - - -def resolve_pdk_variant(selector: Optional[str]): - """ - :returns: - If selector is a valid PDK variant, the same string. - - If selector is a valid PDK family, the default variant of said PDK. - - If selector is None, the PDK environment variables is used as a - fallback. If all are None, the function will simply return None. - - If the selector is invalid, a ValueError will be raised. - """ - selector = selector or os.getenv("PDK") - if selector is None: - return None - - if family := Family.by_name.get(selector): - return family.default_variant - - for pdk_family in Family.by_name.values(): - if selector in pdk_family.variants: - return selector - - raise ValueError(f"'{selector}' is not a valid PDK family or variant.") +def get_versions_dir(pdk_root: str, pdk: str) -> Path: + return get_ciel_dir(pdk_root, pdk) / "versions" @dataclass -class Version(object): +class Version: name: str pdk: str commit_date: Optional[datetime] = None @@ -161,8 +94,8 @@ def is_installed(self, pdk_root: str) -> bool: def is_current(self, pdk_root: str) -> bool: return self.name == _get_current_version(pdk_root, self.pdk) - def get_dir(self, pdk_root: str) -> str: - return os.path.join(get_versions_dir(pdk_root, self.pdk), self.name) + def get_dir(self, pdk_root: str) -> Path: + return get_versions_dir(pdk_root, self.pdk) / self.name def unset_current(self, pdk_root: str): if not self.is_installed(pdk_root): @@ -202,14 +135,14 @@ def get_current(Self, pdk_root: str, pdk: str) -> Optional["Version"]: @classmethod def get_all_installed(Self, pdk_root: str, pdk: str) -> List["Version"]: versions_dir = get_versions_dir(pdk_root, pdk) - mkdirp(versions_dir) + versions_dir.mkdir(parents=True, exist_ok=True) return [ Version( name=version, pdk=pdk, ) for version in os.listdir(versions_dir) - if os.path.isdir(os.path.join(versions_dir, version)) + if (versions_dir / version).is_dir() ] @@ -248,7 +181,8 @@ def resolve_version( "Any of ./tool_metadata.yml or ./dependencies/tool_metadata.yml not found. You'll need to specify the file path or the commits explicitly." ) - tool_metadata = yaml.safe_load(open(tool_metadata_file_path).read()) + with open(tool_metadata_file_path, encoding="utf8") as f: + tool_metadata = yaml.safe_load(f) open_pdks_list = [tool for tool in tool_metadata if tool["name"] == "open_pdks"] diff --git a/ciel/families.py b/ciel/families.py index e9b825e..a0f4cfb 100644 --- a/ciel/families.py +++ b/ciel/families.py @@ -1,3 +1,7 @@ +# Copyright 2026 Ciel Contributors +# +# Adapted from Volare +# # Copyright 2022-2023 Efabless Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,15 +15,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import fnmatch from dataclasses import dataclass -from typing import Iterable, List, Dict, Optional, Set, ClassVar +from typing import Iterable, List, Dict, Optional, Set, ClassVar, Tuple from .github import RepoInfo, opdks_repo, ihp_repo @dataclass -class Family(object): +class Family: by_name: ClassVar[Dict[str, "Family"]] = {} + by_variant: ClassVar[Dict[str, "Family"]] = {} name: str variants: List[str] @@ -27,17 +33,22 @@ class Family(object): repo: RepoInfo # lol no implicitly unwrapped optionals default_variant: str = None # type: ignore - default_includes: List[str] = None # type: ignore + default_includes: Dict[str, List[str]] = None # type: ignore def __post_init__(self): if self.default_variant is None: self.default_variant = self.variants[0] if self.default_includes is None: - self.default_includes = self.all_libraries.copy() + self.default_includes = {"*": self.all_libraries.copy()} + + Family.by_name[self.name] = self + for variant in self.variants: + Family.by_variant[variant] = self def resolve_libraries( self, input: Optional[Iterable[str]], + variant: str, ) -> Set[str]: if input is None: input = ("default",) @@ -47,7 +58,9 @@ def resolve_libraries( final_set = set(self.all_libraries) return final_set elif element.lower() == "default": - final_set = final_set.union(set(self.default_includes)) + for pattern, includes in self.default_includes.items(): + if fnmatch.fnmatch(variant, pattern): + final_set = final_set.union(includes) elif element in self.all_libraries: final_set.add(element) else: @@ -55,8 +68,7 @@ def resolve_libraries( return final_set -Family.by_name = {} -Family.by_name["sky130"] = Family( +Family( name="sky130", variants=["sky130A", "sky130B"], default_variant="sky130A", @@ -74,17 +86,21 @@ def resolve_libraries( "sky130_sram_macros", "sky130_fd_pr_reram", ], - default_includes=[ - "sky130_fd_io", - "sky130_fd_pr", - "sky130_fd_sc_hd", - "sky130_fd_sc_hvl", - "sky130_ml_xx_hd", - "sky130_sram_macros", - ], + default_includes={ + "*": [ + "sky130_fd_io", + "sky130_fd_pr", + "sky130_fd_sc_hd", + "sky130_fd_sc_hvl", + "sky130_ml_xx_hd", + "sky130_sram_macros", + ], + "sky130B": ["sky130_fd_pr_reram"], + }, repo=opdks_repo, ) -Family.by_name["gf180mcu"] = Family( + +Family( name="gf180mcu", variants=["gf180mcuA", "gf180mcuB", "gf180mcuC", "gf180mcuD"], default_variant="gf180mcuD", @@ -104,23 +120,93 @@ def resolve_libraries( "gf180mcu_ocd_alpha_large", "gf180mcu_ocd_alpha_misc", ], - default_includes=[ - "gf180mcu_fd_io", - "gf180mcu_fd_pr", - "gf180mcu_fd_sc_mcu7t5v0", - "gf180mcu_fd_sc_mcu9t5v0", - "gf180mcu_fd_ip_sram", - ], + default_includes={ + "*": [ + "gf180mcu_fd_io", + "gf180mcu_fd_pr", + "gf180mcu_fd_sc_mcu7t5v0", + "gf180mcu_fd_sc_mcu9t5v0", + "gf180mcu_fd_ip_sram", + ] + }, repo=opdks_repo, ) -Family.by_name["ihp-sg13g2"] = Family( - name="ihp-sg13g2", - variants=["ihp-sg13g2"], + +Family( + name="ihp-sg13", + variants=["ihp-sg13g2", "ihp-sg13cmos5l"], all_libraries=[ "sg13g2_io", "sg13g2_pr", "sg13g2_sram", "sg13g2_stdcell", + "sg13cmos5l_io", + "sg13cmos5l_sram", + "sg13cmos5l_stdcell", ], + default_includes={ + "ihp-sg13g2": [ + "sg13g2_io", + "sg13g2_pr", + "sg13g2_sram", + "sg13g2_stdcell", + ], + "ihp-sg13cmos5l": [ + "sg13cmos5l_io", + "sg13cmos5l_sram", + "sg13cmos5l_stdcell", + ], + }, repo=ihp_repo, ) + + +def resolve_pdk_family(selector: str): + """ + :returns: + If selector is a valid PDK family, the same string. + + If selector is a valid PDK variant, the family the variant belongs to. + + If the selector is invalid, a ValueError will be raised. "ihp_sg13g2" + will resolve to "ihp-sg13g2" however for some semblance of backwards + compatibility with previous versions of Ciel/Volare. + """ + if selector == "ihp_sg13g2": + selector = "ihp-sg13" + + if selector in Family.by_name: + return selector + + for pdk_family in Family.by_name.values(): + if selector in pdk_family.variants: + return pdk_family.name + + raise ValueError(f"'{selector}' is not a valid PDK family or variant.") + + +def resolve_pdk_variant(selector: Optional[str]): + """ + :returns: + If selector is a valid PDK variant, the same string. + + If selector is a valid PDK family, the default variant of said PDK. + + If selector is None, the function will simply return None. + + If the selector is invalid, a ValueError will be raised. + """ + if selector is None: + return None + + if selector in Family.by_variant: + return str(selector) + + if family := Family.by_name.get(selector): + return family.default_variant + + raise ValueError(f"'{selector}' is not a valid PDK family or variant.") + + +def resolve_pdk_selector(selector: str) -> Tuple[str, str]: + return (resolve_pdk_family(selector), resolve_pdk_variant(selector)) diff --git a/ciel/github.py b/ciel/github.py index 0ce8ada..4cc5a73 100644 --- a/ciel/github.py +++ b/ciel/github.py @@ -1,4 +1,4 @@ -# Copyright 2025 The American University in Cairo +# Copyright 2025 Ciel Contributors # # Modified from the Volare project # @@ -62,7 +62,7 @@ def api(self): class GitHubSession(httpx.Client): - class Token(object): + class Token: override: ClassVar[Optional[str]] = None @classmethod @@ -75,7 +75,7 @@ def get_gh_token(Self) -> Optional[str]: ["gh", "auth", "token"], encoding="utf8", ).strip() - except Exception: + except Exception: # noqa: S110 pass # 1. Higher priority: environment GITHUB_TOKEN @@ -117,7 +117,7 @@ def __init__( f"Invalid SOCKS proxy: Ciel only supports http://, https:// and socks5:// schemes: {e.args[0]}", file=sys.stderr, ) - exit(-1) + sys.exit(-1) else: raise e from None github_token = github_token or GitHubSession.Token.get_gh_token() @@ -136,15 +136,18 @@ def api( endpoint: str, method: str, *args, + raw_request=False, **kwargs, ) -> Any: url = repo.api + endpoint - req = self.request(method, url, *args, **kwargs) - req.raise_for_status() + res = self.request(method, url, *args, **kwargs) + if raw_request: + return res + res.raise_for_status() try: - return req.json() + return res.json() except ValueError as e: - raise ValueError(f"Request {req.url} returned invalid JSON: {e}") from None + raise ValueError(f"Request {res.url} returned invalid JSON: {e}") from None @classmethod def get_user_agent(Self) -> str: @@ -165,6 +168,7 @@ def get_commit_date( return None date = response["commit"]["author"]["date"] + print(date) commit_date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") return commit_date diff --git a/ciel/manage.py b/ciel/manage.py index aa3f124..9f54a90 100644 --- a/ciel/manage.py +++ b/ciel/manage.py @@ -1,4 +1,4 @@ -# Copyright 2025 The American University in Cairo +# Copyright 2025 Ciel Contributors # # Modified from the Volare project # @@ -21,8 +21,8 @@ import hashlib import tarfile import tempfile -import warnings -from typing import Dict, Iterable, List, Optional, Union +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional, Union, Tuple import rich import httpx @@ -37,8 +37,8 @@ get_versions_dir, get_ciel_dir, ) -from .build import build, push -from .families import Family +from .build import build +from .families import Family, resolve_pdk_selector from .source import DataSource @@ -101,9 +101,9 @@ def print_remote_list( tree = rich.tree.Tree(f"Pre-built {pdk} PDK versions") for remote_version in pdk_list: name = remote_version.name - assert ( - remote_version.commit_date is not None - ), f"Remote version {name} has no commit date" + assert remote_version.commit_date is not None, ( + f"Remote version {name} has no commit date" + ) day = remote_version.commit_date.strftime("%Y.%m.%d") desc = f"[green]{name} ({day})" if remote_version.prerelease: @@ -119,30 +119,36 @@ def print_remote_list( def fetch( pdk_root: str, - pdk: str, + pdk: Union[Tuple[str, str], str], version: str, *, data_source: DataSource, build_if_not_found=False, - also_push=False, - build_kwargs: dict = {}, - push_kwargs: dict = {}, + build_kwargs: Optional[Dict[str, Any]] = None, include_libraries: Optional[Iterable[str]] = None, - output: Union[Console, io.TextIOWrapper] = Console(), + output: Union[Console, io.TextIOWrapper, None] = None, ) -> Version: + if output is None: + output = Console() + + if isinstance(pdk, tuple): + pdk_family_name, pdk_variant_name = pdk + else: + pdk_family_name, pdk_variant_name = resolve_pdk_selector(pdk) + console = output if not isinstance(console, Console): console = Console(file=console) - version_object = Version(version, pdk) + version_object = Version(version, pdk_family_name) version_directory = version_object.get_dir(pdk_root) - pdk_family = Family.by_name.get(pdk) + pdk_family = Family.by_name.get(pdk_family_name) if pdk_family is None: - raise ValueError(f"Unsupported PDK family '{pdk}'.") + raise ValueError(f"Unsupported PDK family '{pdk_family_name}'.") - library_set = pdk_family.resolve_libraries(include_libraries) + library_set = pdk_family.resolve_libraries(include_libraries, pdk_variant_name) variants = pdk_family.variants @@ -163,7 +169,7 @@ def fetch( if not found: missing_libraries.add(library) - affected_paths = [] + affected_paths: List[Path] = [] if len(missing_libraries) != 0 or common_missing: if common_missing: console.print( @@ -174,21 +180,24 @@ def fetch( console.print(f"Libraries {missing_libraries} not found, downloading them…") for variant in variants: affected_paths.append( - os.path.join(version_directory, variant, "libs.ref", library) + version_directory / variant / "libs.ref" / library ) - tarball_paths = [] + tarball_paths: List[Path] = [] try: client, assets = data_source.get_downloads_for_version(version_object) assets_filtered = [] for asset in assets: - if asset.content == "common" and common_missing: - assets_filtered.append(asset) - elif asset.content in missing_libraries: + if ( + asset.content == "common" + and common_missing + or asset.content in missing_libraries + ): assets_filtered.append(asset) - tarball_directory = tempfile.TemporaryDirectory(suffix=".ciel") + tarball_directory_obj = tempfile.TemporaryDirectory(suffix=".ciel") + tarball_directory = Path(tarball_directory_obj.name) for asset in assets_filtered: - tarball_path = os.path.join(tarball_directory.name, asset.filename) + tarball_path = tarball_directory / asset.filename tarball_paths.append(tarball_path) with client.stream("get", asset.url) as r, rich.progress.Progress( console=console @@ -213,12 +222,11 @@ def fetch( for file in tf: if file.isdir(): continue - final_path = os.path.join(version_directory, file.name) - final_dir = os.path.dirname(final_path) - mkdirp(final_dir) + final_path = version_directory / file.name + final_path.parent.mkdir(parents=True, exist_ok=True) io = tf.extractfile(file) if io is None: - raise IOError( + raise OSError( f"Failed to unpack file in {asset.filename}'s tarball: {file.name}." ) with open(final_path, "wb") as f: @@ -232,21 +240,10 @@ def fetch( ) build( pdk_root=pdk_root, - pdk_family=pdk, + pdk=pdk, version=version, - **build_kwargs, + **(build_kwargs or {}), ) - if also_push: - if push_kwargs["push_libraries"] is None: - push_kwargs["push_libraries"] = Family.by_name[ - pdk - ].default_includes.copy() - push( - pdk_root=pdk_root, - pdk_family=pdk, - version=version, - **push_kwargs, - ) else: if e.response is not None: raise RuntimeError( @@ -279,33 +276,37 @@ def fetch( with open(variant_sources_file, "w") as f: print(f"{pdk_family.repo.name} {version}", file=f) - return Version(version, pdk) + return Version(version, pdk_family_name) def enable( pdk_root: str, - pdk: str, + pdk: Union[str, Tuple[str, str]], version: str, *, data_source: DataSource, build_if_not_found: bool = False, - also_push: bool = False, - build_kwargs: dict = {}, - push_kwargs: dict = {}, + build_kwargs: Optional[Dict[str, Any]] = None, include_libraries: Optional[List[str]] = None, - output: Union[Console, io.TextIOWrapper] = Console(), + output: Union[None, Console, io.TextIOWrapper] = None, ) -> Version: + if output is None: + output = Console() + if isinstance(pdk, tuple): + pdk_family_name, _ = pdk + else: + pdk_family_name, _ = resolve_pdk_selector(pdk) console = output if not isinstance(console, Console): console = Console(file=console) - version_object = Version(version, pdk) + version_object = Version(version, pdk_family_name) version_directory = version_object.get_dir(pdk_root) - pdk_family = Family.by_name.get(pdk) + pdk_family = Family.by_name.get(pdk_family_name) if pdk_family is None: - raise ValueError(f"Unsupported PDK family '{pdk}'.") + raise ValueError(f"Unsupported PDK family '{pdk_family_name}'.") variants = pdk_family.variants version_paths = [os.path.join(version_directory, variant) for variant in variants] @@ -317,14 +318,12 @@ def enable( version, data_source=data_source, build_if_not_found=build_if_not_found, - also_push=also_push, build_kwargs=build_kwargs, - push_kwargs=push_kwargs, include_libraries=include_libraries, output=output, ) - current_file = os.path.join(get_ciel_dir(pdk_root, pdk), "current") + current_file = os.path.join(get_ciel_dir(pdk_root, pdk_family_name), "current") current_file_dir = os.path.dirname(current_file) mkdirp(current_file_dir) @@ -346,15 +345,10 @@ def enable( with open(current_file, "w") as f: f.write(version) - console.print(f"Version {version} enabled for the {pdk} PDK.") + console.print(f"Version {version} enabled for the {pdk_family_name} PDK.") return version_object -def get(*args, **kwargs): - warnings.warn("get() has been deprecated: use fetch()") - return fetch(*args, **kwargs) - - def optimize(pdk_root, version_object: Version): if os.name != "posix": return 0 diff --git a/ciel/source.py b/ciel/source.py index f208aab..8f9c74a 100644 --- a/ciel/source.py +++ b/ciel/source.py @@ -1,4 +1,4 @@ -# Copyright 2025 The American University in Cairo +# Copyright 2025 Ciel Contributors # # Modified from the Volare project # @@ -25,6 +25,7 @@ from .github import GitHubSession, RepoInfo from .common import Version, date_from_iso8601 +from .families import Family @dataclass @@ -34,7 +35,7 @@ class Asset: url: str -class DataSource(object): +class DataSource: factory: ClassVar[Dict[str, Type["DataSource"]]] = {} default: ClassVar["DataSource"] @@ -56,6 +57,7 @@ def __init__(self, repo_id: str): self.repo = RepoInfo.from_id(repo_id) def get_available_versions(self, pdk: str) -> List[Version]: + pdk_family = Family.by_name[pdk] page = 1 last = self.session.api( self.repo, @@ -80,9 +82,12 @@ def get_available_versions(self, pdk: str) -> List[Version]: if release["draft"]: continue - family, hash = release["tag_name"].rsplit("-", maxsplit=1) + release_family_name, hash = release["tag_name"].rsplit("-", maxsplit=1) - if pdk != family: + if ( + release_family_name != pdk_family.name + and release_family_name not in pdk_family.variants + ): continue upload_date = date_from_iso8601(release["published_at"]) @@ -94,7 +99,7 @@ def get_available_versions(self, pdk: str) -> List[Version]: remote_version = Version( name=hash, - pdk=family, + pdk=pdk_family.name, commit_date=commit_date, upload_date=upload_date, prerelease=release["prerelease"], @@ -111,11 +116,31 @@ def get_available_versions(self, pdk: str) -> List[Version]: def get_downloads_for_version( self, version: Version ) -> Tuple[httpx.Client, List[Asset]]: - release = self.session.api( + family_res: httpx.Response = self.session.api( self.repo, f"/releases/tags/{version.pdk}-{version.name}", "get", + raw_request=True, ) + release = None + if family_res.status_code // 100 == 2: + release = family_res.json() + elif family_res.status_code == 404 and version.pdk in Family.by_name: + # try variants because ihp was renamed (grumble) + variants = Family.by_name[version.pdk].variants + for variant in variants: + variant_res: httpx.Response = self.session.api( + self.repo, + f"/releases/tags/{variant}-{version.name}", + "get", + raw_request=True, + ) + if variant_res.status_code != 404: + release = variant_res.json() + break + if release is None: + family_res.raise_for_status() + assert release is not None # raise_for_status is a noreturn assets = release["assets"] zst_files = [] diff --git a/poetry.lock b/poetry.lock index 9bf2bce..aae3799 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand. [[package]] name = "anyio" @@ -23,53 +23,6 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.26.1)"] -[[package]] -name = "black" -version = "24.8.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, - {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, - {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, - {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, - {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, - {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, - {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, - {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, - {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, - {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, - {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, - {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, - {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, - {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, - {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, - {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, - {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, - {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, - {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, - {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, - {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, - {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" version = "2024.12.14" @@ -169,7 +122,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -184,7 +137,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] +groups = ["main"] markers = "platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, @@ -207,50 +160,33 @@ files = [ [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "flake8" -version = "5.0.4" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.6.1" -groups = ["dev"] -files = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" - [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [package.dependencies] certifi = "*" -h11 = ">=0.13,<0.15" +h11 = ">=0.16" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] @@ -323,18 +259,6 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -407,30 +331,6 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "pcpp" version = "1.30" @@ -443,35 +343,6 @@ files = [ {file = "pcpp-1.30.tar.gz", hash = "sha256:5af9fbce55f136d7931ae915fae03c34030a3b36c496e72d9636cedc8e2543a1"}, ] -[[package]] -name = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - -[[package]] -name = "pycodestyle" -version = "2.9.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, -] - [[package]] name = "pycparser" version = "2.22" @@ -485,18 +356,6 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -[[package]] -name = "pyflakes" -version = "2.5.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, -] - [[package]] name = "pygments" version = "2.19.1" @@ -594,6 +453,34 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "ruff" +version = "0.16.6" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8"}, + {file = "ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32"}, + {file = "ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757"}, + {file = "ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e"}, + {file = "ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4"}, + {file = "ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88"}, + {file = "ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953"}, + {file = "ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718"}, + {file = "ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25"}, + {file = "ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39"}, + {file = "ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5"}, + {file = "ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -828,4 +715,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = ">=3.8" -content-hash = "874548b418c4bf0200140f0798ee7f5dade38424b8b2eca2087de3d9c7eb26b9" +content-hash = "2620ad17b7fb74272772568def0034d4a519373b761f7700c2004227fb6d518d" diff --git a/pyproject.toml b/pyproject.toml index 8724c2b..2f7ad1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,10 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + [tool.poetry] name = "ciel" -version = "2.6.1" +version = "3.0.0" description = "An PDK builder/version manager for PDKs in the open_pdks format" authors = ["Mohamed Gaber ", "Efabless Corporation"] readme = "Readme.md" @@ -26,8 +30,7 @@ zstandard = ">=0.19.0,<1" [tool.poetry.group.dev.dependencies] wheel = "*" -black = ">=24.4.0,<25" -flake8 = ">=4" +ruff = ">=0.16.6,<0.17.0" mypy = ">=1.9.0,<1.10.0" types-PyYAML = "*" types-setuptools = "*" @@ -37,10 +40,19 @@ types-attrs = "*" [tool.poetry.scripts] ciel = "ciel.__main__:cli" -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.ruff] +target-version = "py38" -[tool.black] -include = 'ciel/[\w\-]+.py|ciel/build/[\w\-]+.py' -exclude = '.git|venv|.venv|.mypy_cache|(? None: ... - def parse(self, input, source: Optional[str] = None, ignore: dict = {}): ... + def parse(self, input, source: str | None = None, ignore: dict = {}): ... def write(self, io: StringIO): ...