From b671245a2fa93739f86f3eccd7e5fe607f7d45a6 Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Thu, 27 Aug 2026 14:28:02 +0300 Subject: [PATCH 1/4] feat!: treat ihp-sg13g2 as a variant of the wider ihp-sg13 family - (breaking) rename family to ihp-sg13 and update ihp build scripts accordingly - fix copyrights on various files - update ci --- .github/scripts/generate_tag.py | 39 ------ .github/scripts/gh.py | 137 ---------------------- .github/workflows/ci.yml | 71 +++++++---- Authors.md | 14 +++ OSAcknowledgements | 9 -- Readme.md | 4 +- ciel/__main__.py | 2 +- ciel/build/__init__.py | 2 +- ciel/build/{ihp-sg13g2.py => ihp-sg13.py} | 38 +++--- ciel/click_common.py | 2 +- ciel/common.py | 4 +- ciel/families.py | 4 +- ciel/github.py | 2 +- ciel/manage.py | 2 +- ciel/source.py | 2 +- pyproject.toml | 2 +- 16 files changed, 92 insertions(+), 242 deletions(-) delete mode 100644 .github/scripts/generate_tag.py delete mode 100644 .github/scripts/gh.py create mode 100644 Authors.md delete mode 100644 OSAcknowledgements rename ciel/build/{ihp-sg13g2.py => ihp-sg13.py} (82%) 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/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..6b32f68 100644 --- a/Readme.md +++ b/Readme.md @@ -46,11 +46,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/__main__.py b/ciel/__main__.py index a351741..91f03d5 100755 --- 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 # diff --git a/ciel/build/__init__.py b/ciel/build/__init__.py index 18918c2..57d47dc 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 # diff --git a/ciel/build/ihp-sg13g2.py b/ciel/build/ihp-sg13.py similarity index 82% rename from ciel/build/ihp-sg13g2.py rename to ciel/build/ihp-sg13.py index 12d8e1d..af8e004 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"); @@ -68,24 +72,26 @@ def get_ihp( def build_ihp(build_directory, ihp_path): # """Build""" try: - shutil.rmtree(os.path.join(build_directory, "ihp-sg13g2")) + shutil.rmtree(os.path.join(build_directory, "ihp-sg13")) 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"] - ), - ) + ihp_sg13_family = Family.by_name["ihp-sg13"] + for variant in ihp_sg13_family.variants: + shutil.copytree( + os.path.join(ihp_path, variant), + os.path.join(build_directory, variant), + ignore=lambda dir, files: ( + files if ".git" in os.path.split(dir) else [".git", ".DS_Store"] + ), + ) 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 +100,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 +109,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): @@ -131,10 +135,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/click_common.py b/ciel/click_common.py index ea5a20b..acf7d36 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 # diff --git a/ciel/common.py b/ciel/common.py index 8cd6703..29dd3ef 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 # @@ -102,7 +102,7 @@ def resolve_pdk_family(selector: Optional[str]): return None if selector == "ihp_sg13g2": - selector = "ihp-sg13g2" + selector = "ihp-sg13" if selector in Family.by_name: return selector diff --git a/ciel/families.py b/ciel/families.py index e9b825e..09fc697 100644 --- a/ciel/families.py +++ b/ciel/families.py @@ -113,8 +113,8 @@ def resolve_libraries( ], repo=opdks_repo, ) -Family.by_name["ihp-sg13g2"] = Family( - name="ihp-sg13g2", +Family.by_name["ihp-sg13"] = Family( + name="ihp-sg13", variants=["ihp-sg13g2"], all_libraries=[ "sg13g2_io", diff --git a/ciel/github.py b/ciel/github.py index 0ce8ada..63c8e4a 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 # diff --git a/ciel/manage.py b/ciel/manage.py index aa3f124..82f6fdf 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 # diff --git a/ciel/source.py b/ciel/source.py index f208aab..1f8dc0a 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 # diff --git a/pyproject.toml b/pyproject.toml index 8724c2b..b7207a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [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" From 353f14af28702c0cd83ef992328fe5ce4c6f9fcd Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Thu, 27 Aug 2026 15:24:18 +0300 Subject: [PATCH 2/4] feat!: per-variant default library inclusions + api cleanup All of these are breaking changes btw: - outputs to non-ttys are now plain-text instead of JSON - Family.default_includes is now a dictionary of patterns to lists, where the patterns are to be matched against a variant to determine the default library set that should be built/pulled - Families now auto-register to both by_name and a new by_variant class dictionaries - Family.resolve_libraries now requires a new argument, variant - enable, fetch, push, and build now take a (pdk_family, pdk_variant) tuple instead of just the PDK family - enable and fetch no longer support automatic pushing - get_ciel_home now returns a pathlib.Path, and so does Version.get_dir - resolve_pdk_family's argument is no longer optional, moved to family.py, added to top-level exports - resolve_pdk_variant moved to families.py, added to top-level exports - remove deprecated method `get()` --- ciel/__init__.py | 11 +++- ciel/__main__.py | 130 ++++++++++++++++++++--------------------- ciel/build/__init__.py | 41 +++++++------ ciel/build/gf180mcu.py | 7 ++- ciel/build/ihp-sg13.py | 6 +- ciel/build/sky130.py | 7 ++- ciel/click_common.py | 13 +++-- ciel/common.py | 92 ++++------------------------- ciel/families.py | 118 ++++++++++++++++++++++++++++++------- ciel/manage.py | 75 +++++++++--------------- 10 files changed, 257 insertions(+), 243 deletions(-) diff --git a/ciel/__init__.py b/ciel/__init__.py index 22c4904..4277d4a 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,14 +18,17 @@ 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, ) diff --git a/ciel/__main__.py b/ciel/__main__.py index 91f03d5..58f618a 100755 --- a/ciel/__main__.py +++ b/ciel/__main__.py @@ -29,7 +29,7 @@ get_ciel_home, ) from .click_common import ( - opt_pdk_root, + opt_pdk, arg_version, ) from .manage import ( @@ -49,35 +49,27 @@ @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_tuple): + """Outputs the currently enabled PDK version.""" + pdk_family, _ = pdk_tuple 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_tuple): """Removes all PDKs other than, if it exists, the one currently set as 'enabled' in the PDK root.""" + pdk_family, _ = pdk_tuple 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_tuple): """ [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, _ = pdk_tuple 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,9 +145,10 @@ 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_tuple, version): """Removes the PDK version specified.""" + pdk_family, _ = pdk_tuple version_object = Version(version, pdk_family) try: version_object.uninstall(pdk_root) @@ -166,10 +161,16 @@ def rm_cmd(pdk_root, pdk_family, version): @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_tuple): + """ + 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, _ = pdk_tuple 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_tuple): + """ + 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, _ = pdk_tuple 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_tuple, 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, _ = pdk_tuple 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_tuple, version, include_libraries, ): @@ -274,7 +274,7 @@ def enable_cmd( try: enable( pdk_root, - pdk_family, + pdk_tuple, version, include_libraries=include_libraries, output=console, @@ -288,7 +288,7 @@ def enable_cmd( @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_tuple, version, include_libraries, ): @@ -316,10 +316,10 @@ def fetch_cmd( try: version = fetch( + pdk_root, + pdk_tuple, + version, data_source=data_source, - pdk_root=pdk_root, - pdk=pdk_family, - version=version, include_libraries=include_libraries, output=console, ) diff --git a/ciel/build/__init__.py b/ciel/build/__init__.py index 57d47dc..2f55703 100644 --- a/ciel/build/__init__.py +++ b/ciel/build/__init__.py @@ -22,7 +22,7 @@ import tempfile import importlib import subprocess -from typing import Optional, List, Dict +from typing import Optional, List, Dict, Tuple import click import zstandard as zstd @@ -42,7 +42,7 @@ from ..click_common import ( opt_push, opt_build, - opt_pdk_root, + opt_pdk, arg_version, ) from ..families import Family @@ -50,7 +50,7 @@ def build( pdk_root: str, - pdk_family: str, + pdk_tuple: Tuple[str, str], version: str, jobs: int = 1, sram: bool = True, # Deprecated @@ -58,6 +58,8 @@ def build( include_libraries: Optional[List[str]] = None, use_repo_at: Optional[List[str]] = None, ): + pdk_family, pdk_variant = pdk_tuple + use_repos = {} if use_repo_at is not None: for repo in use_repo_at: @@ -69,6 +71,7 @@ def build( kwargs = { "pdk_root": pdk_root, + "pdk_variant": pdk_variant, "version": version, "jobs": jobs, "clear_build_artifacts": clear_build_artifacts, @@ -83,14 +86,14 @@ def build( @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_tuple, clear_build_artifacts, version, use_repo_at, @@ -109,7 +112,7 @@ def build_cmd( build( pdk_root=pdk_root, - pdk_family=pdk_family, + pdk_tuple=pdk_tuple, version=version, jobs=jobs, clear_build_artifacts=clear_build_artifacts, @@ -120,7 +123,7 @@ def build_cmd( def push( pdk_root, - pdk_family, + pdk_tuple, version, *, owner, @@ -128,7 +131,11 @@ 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 + pdk_family_name, _ = pdk_tuple + + pdk_family = Family.by_name[pdk_family_name] session = GitHubSession() if session.github_token is None: @@ -137,10 +144,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.") @@ -181,15 +188,15 @@ def push( 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 +225,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 +233,7 @@ def push_cmd( repository, pre, pdk_root, - pdk_family, + pdk_tuple, version, push_libraries, ): @@ -241,7 +248,7 @@ def push_cmd( try: push( pdk_root, - pdk_family, + pdk_tuple, version, owner=owner, repository=repository, diff --git a/ciel/build/gf180mcu.py b/ciel/build/gf180mcu.py index 62022d7..7886312 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"); @@ -225,6 +229,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 +237,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 = {} diff --git a/ciel/build/ihp-sg13.py b/ciel/build/ihp-sg13.py index af8e004..0b5c61e 100644 --- a/ciel/build/ihp-sg13.py +++ b/ciel/build/ihp-sg13.py @@ -71,11 +71,12 @@ def get_ihp( def build_ihp(build_directory, ihp_path): # """Build""" + ihp_sg13_family = Family.by_name["ihp-sg13"] try: - shutil.rmtree(os.path.join(build_directory, "ihp-sg13")) + for variant in ihp_sg13_family.variants: + shutil.rmtree(os.path.join(build_directory, variant)) except FileNotFoundError: pass - ihp_sg13_family = Family.by_name["ihp-sg13"] for variant in ihp_sg13_family.variants: shutil.copytree( os.path.join(ihp_path, variant), @@ -120,6 +121,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, diff --git a/ciel/build/sky130.py b/ciel/build/sky130.py index 6f25432..83bdd1d 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"); @@ -315,6 +319,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 +327,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 = {} diff --git a/ciel/click_common.py b/ciel/click_common.py index acf7d36..13de6e7 100644 --- a/ciel/click_common.py +++ b/ciel/click_common.py @@ -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, resolve_pdk_variant opt = partial(click.option, show_default=True) @@ -93,21 +92,23 @@ def process_value(self, ctx: click.Context, value): value = self.callback(ctx, self, value) try: - resolved = resolve_pdk_family(value) + family = resolve_pdk_family(value) + variant = resolve_pdk_variant(value) except ValueError as e: raise click.BadParameter(str(e), ctx=ctx, param=self) - return resolved + return (family, variant) -def opt_pdk_root(function: Callable): +def opt_pdk(function: Callable): function = opt( "--pdk-family", "--pdk", + "pdk_tuple", 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 29dd3ef..74d5735 100644 --- a/ciel/common.py +++ b/ciel/common.py @@ -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 @@ -61,83 +60,16 @@ def _get_current_version(pdk_root: str, pdk: str) -> Optional[str]: 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-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 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 @@ -161,8 +93,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 +134,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() ] diff --git a/ciel/families.py b/ciel/families.py index 09fc697..93653cc 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,6 +15,7 @@ # 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 @@ -20,6 +25,7 @@ @dataclass class Family(object): 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,16 +120,19 @@ 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-sg13"] = Family( + +Family( name="ihp-sg13", variants=["ihp-sg13g2"], all_libraries=[ @@ -122,5 +141,60 @@ def resolve_libraries( "sg13g2_sram", "sg13g2_stdcell", ], + default_includes={ + "ihp-sg13g2": [ + "sg13g2_io", + "sg13g2_pr", + "sg13g2_sram", + "sg13g2_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.") diff --git a/ciel/manage.py b/ciel/manage.py index 82f6fdf..71df50d 100644 --- a/ciel/manage.py +++ b/ciel/manage.py @@ -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 Dict, Iterable, List, Optional, Union, Tuple import rich import httpx @@ -37,7 +37,7 @@ get_versions_dir, get_ciel_dir, ) -from .build import build, push +from .build import build from .families import Family from .source import DataSource @@ -119,30 +119,30 @@ def print_remote_list( def fetch( pdk_root: str, - pdk: str, + pdk_tuple: Tuple[str, str], version: str, *, data_source: DataSource, build_if_not_found=False, - also_push=False, build_kwargs: dict = {}, - push_kwargs: dict = {}, include_libraries: Optional[Iterable[str]] = None, output: Union[Console, io.TextIOWrapper] = Console(), ) -> Version: + pdk_family_name, pdk_variant_name = pdk_tuple + 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 +163,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,10 +174,10 @@ 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 = [] @@ -186,9 +186,10 @@ def fetch( assets_filtered.append(asset) elif 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,9 +214,8 @@ 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( @@ -232,21 +232,10 @@ def fetch( ) build( pdk_root=pdk_root, - pdk_family=pdk, + pdk_tuple=pdk_tuple, version=version, **build_kwargs, ) - 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 +268,32 @@ 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_tuple: Tuple[str, str], version: str, *, data_source: DataSource, build_if_not_found: bool = False, - also_push: bool = False, build_kwargs: dict = {}, - push_kwargs: dict = {}, include_libraries: Optional[List[str]] = None, output: Union[Console, io.TextIOWrapper] = Console(), ) -> Version: + pdk_family_name, _ = pdk_tuple 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] @@ -313,18 +301,16 @@ def enable( fetch( pdk_root, - pdk, + pdk_tuple, 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 +332,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 From faec8f89b912be54797e2eec07d07ef5ba13757f Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Thu, 27 Aug 2026 16:05:54 +0300 Subject: [PATCH 3/4] feat: support ihp-sg13cmos5l adds new variant to ihp-sg13, three new libraries, and a new default include set if installation for that specific variant is requested --- ciel/build/ihp-sg13.py | 16 +++++++++++++--- ciel/families.py | 10 +++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/ciel/build/ihp-sg13.py b/ciel/build/ihp-sg13.py index 0b5c61e..bffacf6 100644 --- a/ciel/build/ihp-sg13.py +++ b/ciel/build/ihp-sg13.py @@ -18,6 +18,7 @@ import os import shutil import subprocess +from pathlib import Path from datetime import datetime from typing import Optional, List, Tuple, Dict from concurrent.futures import ThreadPoolExecutor @@ -71,6 +72,17 @@ def get_ihp( 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: for variant in ihp_sg13_family.variants: @@ -81,9 +93,7 @@ def build_ihp(build_directory, ihp_path): shutil.copytree( os.path.join(ihp_path, variant), os.path.join(build_directory, variant), - ignore=lambda dir, files: ( - files if ".git" in os.path.split(dir) else [".git", ".DS_Store"] - ), + ignore=filter, ) diff --git a/ciel/families.py b/ciel/families.py index 93653cc..15a0a48 100644 --- a/ciel/families.py +++ b/ciel/families.py @@ -134,12 +134,15 @@ def resolve_libraries( Family( name="ihp-sg13", - variants=["ihp-sg13g2"], + 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": [ @@ -148,6 +151,11 @@ def resolve_libraries( "sg13g2_sram", "sg13g2_stdcell", ], + "ihp-sg13cmos5l": [ + "sg13cmos5l_io", + "sg13cmos5l_sram", + "sg13cmos5l_stdcell", + ], }, repo=ihp_repo, ) From 962517836512f8912e410a5f3e195e66e49d004c Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Fri, 28 Aug 2026 01:38:16 +0300 Subject: [PATCH 4/4] feat: allow GitHub data source to consume old versions named after PDK variant --- ciel/common.py | 1 + ciel/source.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ciel/common.py b/ciel/common.py index 74d5735..e0f7698 100644 --- a/ciel/common.py +++ b/ciel/common.py @@ -79,6 +79,7 @@ class Version(object): commit_date: Optional[datetime] = None upload_date: Optional[datetime] = None prerelease: bool = False + data_source_pdk_override: Optional[str] = None def __lt__(self, rhs: "Version"): return (self.commit_date or datetime.min) < (rhs.commit_date or datetime.min) diff --git a/ciel/source.py b/ciel/source.py index 1f8dc0a..c22e0e9 100644 --- a/ciel/source.py +++ b/ciel/source.py @@ -25,6 +25,7 @@ from .github import GitHubSession, RepoInfo from .common import Version, date_from_iso8601 +from .families import Family @dataclass @@ -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,10 +99,11 @@ 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"], + data_source_pdk_override=release_family_name, ) versions.append(remote_version) @@ -111,9 +117,11 @@ def get_available_versions(self, pdk: str) -> List[Version]: def get_downloads_for_version( self, version: Version ) -> Tuple[httpx.Client, List[Asset]]: + release_family_name = version.data_source_pdk_override or version.pdk + release = self.session.api( self.repo, - f"/releases/tags/{version.pdk}-{version.name}", + f"/releases/tags/{release_family_name}-{version.name}", "get", )