Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/actions/prepare_nix_environment/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ runs:
purge-last-accessed: 0
purge-primary-key: never

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"

- name: Generate librelane_plugin_fabulous side-package
shell: bash
run: uv sync --locked

- name: Build nix environment
shell: bash
run: nix develop --command true
209 changes: 209 additions & 0 deletions .github/workflows/librelane-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
name: LibreLane Plugin Nightly

on:
schedule:
# 03:00 UTC, offset from gds-flow-ci.yml (02:00) and dependency test (00:00)
- cron: "0 3 * * *"
workflow_dispatch:
pull_request:

permissions:
contents: read
issues: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
PDK: ihp-sg13g2
TILE_NAME: LUT4x8_ha
FABRIC_NAME: synthetic_lut4x8_ha_10x10
TILE_BASE_CONFIG: tests/assets/librelane_plugin/tiles/classic/common/config.yaml
TILE_CONFIG: tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/config.yaml
FABRIC_CONFIG: tests/assets/librelane_plugin/fabrics/synthetic_lut4x8_ha_10x10/config.yaml

jobs:
plugin-smoke:
name: Plugin import smoke test
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"

- name: Install FABulous (triggers build_hooks side-packages)
run: uv sync --locked

- name: Confirm LibreLane auto-discovers the plugin and flows register
run: |
uv run python - <<'PY'
import librelane_plugin_fabulous
from librelane.flows import Flow
expected = {
"FABulousTileVerilogMacroFlow",
"FABulousTileVHDLMacroFlowClassic",
"FABulousFabricMacroFlow",
"FABulousFabricMacroFullFlow",
"FABulousTile",
"FABulousFabric",
}
registered = set(Flow.factory.list())
missing = expected - registered
assert not missing, f"Missing FABulous flows: {missing}"
print("plugin file:", librelane_plugin_fabulous.__file__)
print("flows OK:", sorted(expected))
PY

tile-hardening:
name: Harden LUT4x8_ha via FABulousTile
runs-on: ubuntu-latest
needs: plugin-smoke
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/prepare_nix_environment

- name: Run librelane on FABulousTile
run: |
nix develop --command bash -c "
set -euo pipefail
librelane \
--pdk ${PDK} \
--run-tag ci \
--save-views-to tile_final_views \
${TILE_BASE_CONFIG} \
${TILE_CONFIG} \
2>&1 | tee tile_run.log
"

- name: Verify tile final views exist
run: |
set -e
for sub in metrics.json gds lef nl pnl vh; do
if [ ! -e "tile_final_views/${sub}" ]; then
echo "FAIL: tile_final_views/${sub} missing"
ls -la tile_final_views/ || true
exit 1
fi
done
echo "Tile final views OK"
ls tile_final_views/

- name: Package tile final views
if: always()
run: |
if [ -d tile_final_views ]; then
tar --zstd -cf tile-final-views.tar.zst tile_final_views
fi
if [ -f tile_run.log ]; then
tar --zstd -cf tile-run-log.tar.zst tile_run.log
fi

- name: Upload tile final views
if: always()
uses: actions/upload-artifact@v4
with:
name: tile-final-views-${{ env.PDK }}
path: tile-final-views.tar.zst
if-no-files-found: error
retention-days: 7

- name: Upload tile run log
if: always()
uses: actions/upload-artifact@v4
with:
name: tile-run-log-${{ env.PDK }}
path: tile-run-log.tar.zst
if-no-files-found: warn
retention-days: 7

fabric-stitching:
name: Stitch synthetic_lut4x8_ha_10x10 via FABulousFabric
runs-on: ubuntu-latest
needs: tile-hardening
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/prepare_nix_environment

- name: Download hardened tile final views
uses: actions/download-artifact@v4
with:
name: tile-final-views-${{ env.PDK }}

- name: Unpack tile final views
run: |
tar --zstd -xf tile-final-views.tar.zst
ls tile_final_views/

- name: Generate FABULOUS_TILE_MACROS overlay
run: |
TILE_MACRO_DIR="$(realpath tile_final_views)"
cat > tile_macros_overlay.yaml <<EOF
FABULOUS_TILE_MACROS:
${TILE_NAME}: ${TILE_MACRO_DIR}
EOF
cat tile_macros_overlay.yaml

- name: Run librelane on FABulousFabric
run: |
FABRIC_DIR="$(dirname ${FABRIC_CONFIG})"
nix develop --command bash -c "
set -euo pipefail
librelane \
--pdk ${PDK} \
--run-tag ci \
--design-dir ${FABRIC_DIR} \
--save-views-to fabric_final_views \
${FABRIC_CONFIG} \
tile_macros_overlay.yaml \
2>&1 | tee fabric_run.log
"

- name: Verify fabric final GDS exists
run: |
set -e
if ! ls fabric_final_views/gds/*.gds >/dev/null 2>&1; then
echo "FAIL: no fabric GDS produced"
ls -la fabric_final_views/ || true
exit 1
fi
echo "Fabric GDS OK"
ls fabric_final_views/gds/

- name: Package fabric final views
if: always()
run: |
if [ -d fabric_final_views ]; then
tar --zstd -cf fabric-final-views.tar.zst fabric_final_views
fi
if [ -f fabric_run.log ]; then
tar --zstd -cf fabric-run-log.tar.zst fabric_run.log
fi

- name: Upload fabric final views
if: always()
uses: actions/upload-artifact@v4
with:
name: fabric-final-views-${{ env.PDK }}
path: fabric-final-views.tar.zst
if-no-files-found: error
retention-days: 7

- name: Upload fabric run log
if: always()
uses: actions/upload-artifact@v4
with:
name: fabric-run-log-${{ env.PDK }}
path: fabric-run-log.tar.zst
if-no-files-found: warn
retention-days: 7
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ docker-image
.codex
.coverage*
fabulous_nix
librelane_plugin_fabulous
build

# Documentation build artifacts (auto-generated in CI)
Expand Down
112 changes: 104 additions & 8 deletions build_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,93 @@

from setuptools.command.build_py import build_py

_UPSTREAM_PKG = "fabulous.fabric_generator.gds_generator"
_LIBRELANE_PLUGIN_SUBPKGS: tuple[str, ...] = ("steps", "flows")


def _render_librelane_plugin_init() -> str:
"""Render the librelane_plugin_fabulous/__init__.py content."""
subpkgs = ", ".join(repr(s) for s in _LIBRELANE_PLUGIN_SUBPKGS)
return f'''\
"""LibreLane plugin: re-export of the FABulous GDS steps and flows.

Ships as a side-package inside the FABulous-FPGA wheel. LibreLane
auto-discovers packages whose name matches ``librelane_plugin_*`` — importing
this package walks every submodule under
``{_UPSTREAM_PKG}.{{steps,flows}}`` so their
``@Step.factory.register()`` / ``@Flow.factory.register()`` decorators fire.

``FABulousTile`` and ``FABulousFabric`` are additionally re-exported at the
package root as a drop-in replacement for ``mole99/librelane_plugin_fabulous``.

Generated at build time by ``build_hooks.BuildPyWithFabulousNix`` — do not
edit by hand. The source of truth for every Step and Flow lives in
``{_UPSTREAM_PKG}``.
"""

import importlib as _importlib
import pkgutil as _pkgutil
import sys as _sys

_DEFERRED_FLOWS = False


def _tile_optimisation_is_initialising() -> bool:
module = _sys.modules.get(
"{_UPSTREAM_PKG}.steps.tile_optimisation"
)
return module is not None and not hasattr(module, "TileOptimisation")


def _register_submodules(include_flows: bool = True) -> None:
global _DEFERRED_FLOWS
for _sub in {subpkgs!s}:
if _sub == "flows" and not include_flows:
_DEFERRED_FLOWS = True
continue
_pkg = _importlib.import_module(f"{_UPSTREAM_PKG}.{{_sub}}")
for _info in _pkgutil.iter_modules(_pkg.__path__):
_importlib.import_module(f"{_UPSTREAM_PKG}.{{_sub}}.{{_info.name}}")
if include_flows:
_DEFERRED_FLOWS = False


def _finish_deferred_registration() -> None:
if _DEFERRED_FLOWS:
_register_submodules(include_flows=True)


_register_submodules(include_flows=not _tile_optimisation_is_initialising())

__all__ = ["FABulousTile", "FABulousFabric"]

_REEXPORTS = {{
"FABulousTile": ("{_UPSTREAM_PKG}.flows.plugin_tile_flow", "FABulousTile"),
"FABulousFabric": ("{_UPSTREAM_PKG}.flows.plugin_fabric_flow", "FABulousFabric"),
}}


def __getattr__(name: str) -> object:
"""Lazy re-export.

Resolved on first access rather than at package-import time to avoid a
circular import when LibreLane's plugin discovery fires while this package's
own flow modules are still being initialised.
"""
target = _REEXPORTS.get(name)
if target is None:
raise AttributeError(f"module {{__name__!r}} has no attribute {{name!r}}")
_finish_deferred_registration()
module_name, attr = target
return getattr(_importlib.import_module(module_name), attr)
'''


class BuildPyWithFabulousNix(build_py):
"""Generate a build-only `fabulous_nix` package with required nix assets."""
"""Generate FABulous side-packages."""

_PACKAGE_NAME = "fabulous_nix"
_NIX_PACKAGE_NAME = "fabulous_nix"
_LIBRELANE_PLUGIN_PACKAGE_NAME = "librelane_plugin_fabulous"

_ASSET_MAP: tuple[tuple[str, str], ...] = (
("flake.nix", "flake.nix"),
Expand All @@ -26,12 +108,14 @@ class BuildPyWithFabulousNix(build_py):
)

def run(self) -> None:
"""Run package build and generate side packages."""
super().run()
self._build_fabulous_nix_package()
self._build_librelane_plugin_fabulous_package()

def _build_fabulous_nix_package(self) -> None:
project_root = Path(__file__).resolve().parent
target_root = self._target_root(project_root)
target_root = self._target_root(project_root, self._NIX_PACKAGE_NAME)
rmtree(target_root, ignore_errors=True)
target_root.mkdir(parents=True, exist_ok=True)

Expand All @@ -47,12 +131,24 @@ def _build_fabulous_nix_package(self) -> None:
target_path.parent.mkdir(parents=True, exist_ok=True)
copy2(source_path, target_path)

def _target_root(self, project_root: Path) -> Path:
def _build_librelane_plugin_fabulous_package(self) -> None:
"""Generate librelane_plugin_fabulous/ as a thin re-export side-package."""
project_root = Path(__file__).resolve().parent
target_root = self._target_root(
project_root, self._LIBRELANE_PLUGIN_PACKAGE_NAME
)
rmtree(target_root, ignore_errors=True)
target_root.mkdir(parents=True, exist_ok=True)
(target_root / "__init__.py").write_text(
_render_librelane_plugin_init(), encoding="utf-8"
)

def _target_root(self, project_root: Path, package_name: str) -> Path:
if getattr(self, "editable_mode", False):
packages = list(self.distribution.packages or [])
if self._PACKAGE_NAME not in packages:
packages.append(self._PACKAGE_NAME)
if package_name not in packages:
packages.append(package_name)
self.distribution.packages = packages
return project_root / self._PACKAGE_NAME
return project_root / package_name

return Path(self.build_lib) / self._PACKAGE_NAME
return Path(self.build_lib) / package_name
Loading
Loading