diff --git a/.github/actions/build-wheel/action.yml b/.github/actions/build-wheel/action.yml new file mode 100644 index 000000000..9977b3124 --- /dev/null +++ b/.github/actions/build-wheel/action.yml @@ -0,0 +1,54 @@ +name: Build ECC-Tools Wheel +description: CMake build + auditwheel repair + smoke test. Requires manylinux_2_34_x86_64 container. + +runs: + using: composite + steps: + - name: Install system dependencies + shell: bash + run: | + dnf install -y epel-release + dnf config-manager --set-enabled crb || true + dnf install -y \ + cmake ninja-build pkgconfig patchelf \ + boost-devel cairo-devel gflags-devel glog-devel \ + flex flex-devel bison eigen3-devel gtest-devel \ + tbb-devel hwloc-devel libcurl-devel libunwind-devel \ + metis-devel gmp-devel tcl-devel \ + unzip zip + + - name: Setup Python 3.11 + shell: bash + run: echo "/opt/python/cp311-cp311/bin" >> "$GITHUB_PATH" + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Setup sccache + uses: mozilla-actions/sccache-action@v0.0.9 + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + version: latest + enable-cache: true + + - name: Install Python dev dependencies + shell: bash + run: | + uv sync --all-groups --python 3.11 + echo "${{ github.workspace }}/.venv/bin" >> "$GITHUB_PATH" + + - name: Restore CMake build cache + uses: actions/cache@v4 + with: + path: build + key: cmake-${{ runner.os }}-${{ hashFiles('CMakeLists.txt', 'src/**/CMakeLists.txt') }} + restore-keys: | + cmake-${{ runner.os }}- + + - name: Build repaired wheel + shell: bash + env: + SCCACHE_GHA_ENABLED: "true" + run: .github/scripts/build-wheel.sh diff --git a/.github/scripts/build-wheel.sh b/.github/scripts/build-wheel.sh new file mode 100755 index 000000000..689c91151 --- /dev/null +++ b/.github/scripts/build-wheel.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build ecc-tools wheel: CMake build -> collect .so -> uv build -> auditwheel repair -> smoke test +# Intended to run inside manylinux_2_34_x86_64 container or equivalent. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$REPO_ROOT" + +die() { echo "ERROR: $1" >&2; exit 1; } + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +# Validate environment +[[ "${OSTYPE:-}" == linux* ]] || die "wheel build is only supported on Linux" + +PYTHON3="${PYTHON3:-$(command -v python3 || true)}" +[[ -x "${PYTHON3:-}" ]] || die "python3 not found" + +for cmd in cmake ninja auditwheel uv sha256sum; do + require_cmd "$cmd" +done + +# CMake configure +echo "[build-wheel] CMake configure" +cmake_launcher_args=() +if command -v sccache >/dev/null 2>&1; then + cmake_launcher_args+=( + -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + -DCMAKE_C_COMPILER_LAUNCHER=sccache + ) + echo "[build-wheel] sccache enabled" +fi + +cmake -B build -G Ninja \ + -DBUILD_ECOS=ON \ + -DBUILD_PYTHON=ON \ + -DBUILD_STATIC_LIB=OFF \ + -DCOMPATIBILITY_MODE=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DPython3_EXECUTABLE="$PYTHON3" \ + -DPython3_ROOT_DIR="$("$PYTHON3" -c "import sys; print(sys.prefix)")" \ + -DPython3_FIND_STRATEGY=LOCATION \ + "${cmake_launcher_args[@]}" + +# CMake build +echo "[build-wheel] CMake build ($(nproc) jobs)" +cmake --build build --target ecc_py -j"$(nproc)" + +# Collect artifacts +ECC_PY_ABI="$("$PYTHON3" -c "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")" +ecc_py_so="$(find build bin -name "ecc_py${ECC_PY_ABI}" -print -quit 2>/dev/null)" +[[ -n "$ecc_py_so" ]] || die "ecc_py${ECC_PY_ABI} not found in build/ or bin/" +echo "[build-wheel] Found: $ecc_py_so" +cp -f "$ecc_py_so" "ecc_tools_bin/" + +# Build raw wheel +echo "[build-wheel] Building raw wheel" +raw_out="dist/wheel/raw" +mkdir -p "$raw_out" +uv build --wheel --out-dir "$raw_out" + +raw_whl="$(find "$raw_out" -name 'ecc_tools-*.whl' -print -quit)" +[[ -n "$raw_whl" ]] || die "raw wheel not found in $raw_out" +echo "[build-wheel] Raw wheel: $raw_whl" + +# auditwheel repair +echo "[build-wheel] auditwheel show/repair" +repair_out="dist/wheel/repaired" +report_out="dist/wheel/reports" +mkdir -p "$repair_out" "$report_out" + +show_report="$report_out/show.txt" +{ + echo "=== $(basename "$raw_whl") ===" + auditwheel show "$raw_whl" + echo +} > "$show_report" + +auditwheel repair "$raw_whl" -w "$repair_out" + +repaired_whl="$(find "$repair_out" -name 'ecc_tools-*.whl' -print -quit)" +[[ -n "$repaired_whl" ]] || die "no repaired ecc_tools wheel found in $repair_out" + +# Smoke test +echo "[build-wheel] Running smoke test" +smoke_dir="$(mktemp -d)" +trap 'rm -rf "$smoke_dir"' EXIT + +"$PYTHON3" -m pip install --target "$smoke_dir/site" "$repair_out"/ecc_tools-*.whl +PYTHONPATH="$smoke_dir/site" "$PYTHON3" -c " +from ecc_tools_bin import ecc_py + +required = ['flow_init', 'flow_exit', 'db_init', 'def_init', 'lef_init', + 'def_save', 'run_placer', 'run_cts', 'run_rt', 'run_drc', + 'run_filler', 'init_floorplan', 'report_db', 'feature_summary'] +missing = [f for f in required if not callable(getattr(ecc_py, f, None))] +assert not missing, f'missing or non-callable bindings: {missing}' + +print(f'ecc_py smoke test passed: {len(required)} bindings verified') +" + +# Checksums +sha256sum "$repair_out"/*.whl > dist/wheel/SHA256SUMS + +echo "[build-wheel] done" +echo "[build-wheel] raw wheels: $raw_out" +echo "[build-wheel] repaired wheels: $repair_out" +echo "[build-wheel] report: $show_report" +echo "[build-wheel] checksums: dist/wheel/SHA256SUMS" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..8d96f2eaf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-wheel: + name: Build Wheel + runs-on: ubuntu-latest + container: quay.io/pypa/manylinux_2_34_x86_64 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build wheel + uses: ./.github/actions/build-wheel + + - name: Upload repaired wheel + uses: actions/upload-artifact@v4 + with: + name: ecc-tools-wheel + path: dist/wheel/repaired/ecc_tools-*.whl + retention-days: 7 + if-no-files-found: error + + - name: Upload checksums + uses: actions/upload-artifact@v4 + with: + name: checksums + path: dist/wheel/SHA256SUMS + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..2eab8015a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,100 @@ +name: Release + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + tag_name: + description: 'Tag to release (e.g., v0.1.0-alpha)' + required: true + default: 'v0.1.0-alpha' + +permissions: + contents: write + +jobs: + build: + name: Build Wheel + runs-on: ubuntu-latest + container: quay.io/pypa/manylinux_2_34_x86_64 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag_name || github.ref }} + submodules: recursive + fetch-depth: 0 + + - name: Build wheel + uses: ./.github/actions/build-wheel + + - name: Upload wheel artifact + uses: actions/upload-artifact@v4 + with: + name: ecc-tools-wheel + path: | + dist/wheel/repaired/ecc_tools-*.whl + dist/wheel/SHA256SUMS + + release: + name: Create Release + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag_name || github.ref }} + fetch-depth: 0 + + - name: Download wheel artifact + uses: actions/download-artifact@v4 + with: + name: ecc-tools-wheel + path: dist/wheel + + - name: Determine tag version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + FULL_TAG="${{ github.event.inputs.tag_name }}" + TAG_VERSION="${FULL_TAG#v}" + else + FULL_TAG="${GITHUB_REF_NAME}" + TAG_VERSION="${GITHUB_REF_NAME#v}" + fi + echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT" + echo "full_tag=$FULL_TAG" >> "$GITHUB_OUTPUT" + + - name: Generate release notes + id: notes + run: | + PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + if [[ -n "$PREV_TAG" ]]; then + RANGE="${PREV_TAG}..HEAD" + else + RANGE="HEAD" + fi + { + echo "## Changes" + echo "" + git log --oneline --no-merges "$RANGE" | sed 's/^/- /' + echo "" + echo "## Checksums" + echo "" + echo '```' + cat dist/wheel/SHA256SUMS + echo '```' + } > release-notes.md + cat release-notes.md + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ steps.version.outputs.full_tag }}" \ + --title "ecc-tools ${{ steps.version.outputs.full_tag }}" \ + --notes-file release-notes.md \ + dist/wheel/repaired/ecc_tools-*.whl \ + dist/wheel/SHA256SUMS diff --git a/.gitignore b/.gitignore index 27fb474a9..09f3e91f2 100755 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,7 @@ rpt*/ target/ *-stamp/ tmp/ -scripts +/scripts *.patch CMakePresets.json .venv/ diff --git a/ecc_tools_bin/__init__.py b/ecc_tools_bin/__init__.py new file mode 100644 index 000000000..502076f96 --- /dev/null +++ b/ecc_tools_bin/__init__.py @@ -0,0 +1,2 @@ +# Intentionally minimal — consumers import ecc_py directly: +# from ecc_tools_bin import ecc_py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..f320872a1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +requires = ["uv_build>=0.8.5,<0.12"] +build-backend = "uv_build" + +[project] +name = "ecc-tools" +version = "0.1.0-alpha" +requires-python = ">=3.11" +dependencies = [] + +[tool.uv] +build-backend.module-name = ["ecc_tools_bin"] +build-backend.module-root = "" +build-backend.source-exclude = [ + "src/**", + "build/**", + "scripts/**", + ".github/**", + ".gitee/**", +] +build-backend.wheel-exclude = [ + "src/**", + "build/**", + "scripts/**", + ".github/**", + ".gitee/**", +] + +[dependency-groups] +dev = ["auditwheel"]