diff --git a/.github/actions/setup-python-deps/action.yml b/.github/actions/setup-python-deps/action.yml index c92905ac..fe1b9081 100644 --- a/.github/actions/setup-python-deps/action.yml +++ b/.github/actions/setup-python-deps/action.yml @@ -1,13 +1,18 @@ name: Setup Python Dependencies description: Setup Python 3.11, uv, and install project dependencies. +inputs: + build-all: + description: Build native dependencies from source instead of downloading prebuilt native wheels. + required: false + default: "false" + runs: using: composite steps: - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" + - name: Setup Python 3.11 + shell: bash + run: echo "/opt/python/cp311-cp311/bin" >> "$GITHUB_PATH" - name: Setup uv uses: astral-sh/setup-uv@v3 @@ -15,7 +20,34 @@ runs: version: latest enable-cache: "true" + - name: Trust GitHub workspace + shell: bash + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Install native build dependencies + shell: bash + run: | + dnf install -y epel-release dnf-plugins-core + dnf config-manager --set-enabled crb || true + dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo + dnf install -y \ + cmake ninja-build pkgconfig patchelf mold lld \ + 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 gh + + - name: Setup Rust + if: ${{ inputs.build-all == 'true' }} + uses: dtolnay/rust-toolchain@stable + + - name: Setup sccache + if: ${{ inputs.build-all == 'true' }} + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install native tool wheels + if: ${{ inputs.build-all != 'true' }} shell: bash env: GH_TOKEN: ${{ github.token }} @@ -25,24 +57,55 @@ runs: mkdir -p dist/native-wheels uv venv --python 3.11 - dreamplace_version="$(sed -n 's/^version = "\(.*\)"$/\1/p' chipcompiler/thirdparty/ecc-dreamplace/pyproject.toml)" - ecc_tools_version="$(sed -n 's/^version = "\(.*\)"$/\1/p' chipcompiler/thirdparty/ecc-tools/pyproject.toml)" + download_wheel_artifact() { + local repo="$1" + local submodule_path="$2" + local artifact_name="$3" + local commit + local run_id + + commit="$(git rev-parse "HEAD:${submodule_path}")" + run_id="$( + gh api "repos/${repo}/actions/workflows/ci.yml/runs?head_sha=${commit}&per_page=20" \ + --jq '[.workflow_runs[] | select(.conclusion == "success") | .id][0] // empty' + )" + + if [ -z "$run_id" ]; then + echo "::error::No successful CI artifact found for ${repo} at submodule commit ${commit}" + exit 1 + fi + + echo "Downloading ${artifact_name} from ${repo} CI run ${run_id} (${commit})" + gh run download "$run_id" \ + --repo "$repo" \ + --name "$artifact_name" \ + --dir dist/native-wheels + } - gh release download "v$dreamplace_version" \ - --repo "openecos-projects/ecc-dreamplace" \ - --pattern '*.whl' \ - --dir dist/native-wheels + download_wheel_artifact \ + "openecos-projects/ecc-dreamplace" \ + "chipcompiler/thirdparty/ecc-dreamplace" \ + "ecc-dreamplace-wheel" - gh release download "v$ecc_tools_version" \ - --repo "openecos-projects/ecc-tools" \ - --pattern '*.whl' \ - --dir dist/native-wheels + download_wheel_artifact \ + "openecos-projects/ecc-tools" \ + "chipcompiler/thirdparty/ecc-tools" \ + "ecc-tools-wheel" uv pip install --python .venv/bin/python --no-deps dist/native-wheels/*.whl - name: Install Python dependencies shell: bash run: | - uv sync --frozen --all-groups --python 3.11 --inexact \ - --no-install-package ecc-dreamplace \ - --no-install-package ecc-tools-bin + if [ "${{ inputs.build-all }}" = "true" ]; then + export SCCACHE_GHA_ENABLED="true" + export CMAKE_ARGS="-DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache" + uv sync --frozen --all-groups --python 3.11 \ + --no-build-isolation-package ecc-dreamplace \ + --no-build-isolation-package ecc-tools-bin \ + --verbose + else + uv sync --frozen --all-groups --python 3.11 --inexact \ + --no-install-package ecc-dreamplace \ + --no-install-package ecc-tools-bin + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e391b29..828ee7d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,12 @@ name: CI on: workflow_dispatch: + inputs: + build-all: + description: Build native dependencies from source instead of downloading prebuilt native wheels. + required: false + type: boolean + default: false push: branches: [main] pull_request: @@ -27,6 +33,7 @@ concurrency: cancel-in-progress: true permissions: + actions: read contents: read env: @@ -47,6 +54,7 @@ jobs: name: Test needs: check-version runs-on: ubuntu-latest + container: quay.io/pypa/manylinux_2_34_x86_64 steps: - name: Checkout uses: actions/checkout@v4 @@ -55,6 +63,8 @@ jobs: - name: Setup Python dependencies uses: ./.github/actions/setup-python-deps + with: + build-all: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs['build-all'] == 'true' && 'true' || 'false' }} - name: Setup PDK run: | @@ -97,6 +107,7 @@ jobs: name: Build PyInstaller Bundle needs: check-version runs-on: ubuntu-latest + container: quay.io/pypa/manylinux_2_34_x86_64 steps: - name: Checkout uses: actions/checkout@v4 @@ -105,6 +116,8 @@ jobs: - name: Setup Python dependencies uses: ./.github/actions/setup-python-deps + with: + build-all: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs['build-all'] == 'true' && 'true' || 'false' }} - name: Smoke test native toolchain wheels run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 42781571..b977a5f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,7 @@ on: default: 'v0.1.0-alpha.3' permissions: + actions: read contents: write jobs: @@ -32,6 +33,7 @@ jobs: name: Build CLI Bundle needs: check-version runs-on: ubuntu-22.04 + container: quay.io/pypa/manylinux_2_34_x86_64 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/test/cli/commands/test_log.py b/test/cli/commands/test_log.py index eda44862..e835a126 100644 --- a/test/cli/commands/test_log.py +++ b/test/cli/commands/test_log.py @@ -4,6 +4,18 @@ from chipcompiler.cli import main as cli_main +def _make_path_unreadable(monkeypatch, unreadable_path): + real_open = open + unreadable_path = os.fspath(unreadable_path) + + def open_unless_target(path, *args, **kwargs): + if os.fspath(path) == unreadable_path: + raise PermissionError("cannot read") + return real_open(path, *args, **kwargs) + + monkeypatch.setattr("builtins.open", open_unless_target) + + class TestLog: def test_log_step_errors(self, tmp_path, capsys, create_cli_project): project_dir = create_cli_project() @@ -536,7 +548,9 @@ def test_artifacts_log_disclosure_no_errors( class TestLogUnreadableFile: """AC-9: Unreadable log files return non-zero with OS error.""" - def test_unreadable_log_returns_nonzero(self, tmp_path, capsys, create_cli_project): + def test_unreadable_log_returns_nonzero( + self, tmp_path, monkeypatch, capsys, create_cli_project + ): project_dir = create_cli_project() run_dir = os.path.join(project_dir, "runs", "default") step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") @@ -544,17 +558,14 @@ def test_unreadable_log_returns_nonzero(self, tmp_path, capsys, create_cli_proje log_path = os.path.join(step_dir, "synthesis.log") with open(log_path, "w") as f: f.write("content\n") - os.chmod(log_path, 0o000) + _make_path_unreadable(monkeypatch, log_path) - try: - rc = cli_main.run(["log", "synthesis", "--project", project_dir]) - assert rc == 1 - out = capsys.readouterr().out - assert "unreadable" in out - finally: - os.chmod(log_path, 0o644) + rc = cli_main.run(["log", "synthesis", "--project", project_dir]) + assert rc == 1 + out = capsys.readouterr().out + assert "unreadable" in out - def test_unreadable_log_jsonl(self, tmp_path, capsys, create_cli_project): + def test_unreadable_log_jsonl(self, tmp_path, monkeypatch, capsys, create_cli_project): project_dir = create_cli_project() run_dir = os.path.join(project_dir, "runs", "default") step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") @@ -562,17 +573,14 @@ def test_unreadable_log_jsonl(self, tmp_path, capsys, create_cli_project): log_path = os.path.join(step_dir, "synthesis.log") with open(log_path, "w") as f: f.write("content\n") - os.chmod(log_path, 0o000) + _make_path_unreadable(monkeypatch, log_path) - try: - rc = cli_main.run(["log", "synthesis", "--jsonl", "--project", project_dir]) - assert rc == 1 - record = json.loads(capsys.readouterr().out.strip()) - assert record["log_status"] == "unreadable" - assert "source" in record - assert "error" in record - finally: - os.chmod(log_path, 0o644) + rc = cli_main.run(["log", "synthesis", "--jsonl", "--project", project_dir]) + assert rc == 1 + record = json.loads(capsys.readouterr().out.strip()) + assert record["log_status"] == "unreadable" + assert "source" in record + assert "error" in record class TestLogMultiSource: @@ -958,7 +966,9 @@ def test_step_jsonl_unchanged(self, tmp_path, capsys, create_cli_project): class TestLogListingUnreadable: """Unreadable logs in listing mode must omit tail, keep path+inspect, no traceback.""" - def test_unreadable_step_log_in_listing(self, tmp_path, capsys, create_cli_project): + def test_unreadable_step_log_in_listing( + self, tmp_path, monkeypatch, capsys, create_cli_project + ): project_dir = create_cli_project() run_dir = os.path.join(project_dir, "runs", "default") step_dir = os.path.join(run_dir, "Synthesis_yosys", "log") @@ -966,15 +976,12 @@ def test_unreadable_step_log_in_listing(self, tmp_path, capsys, create_cli_proje log_path = os.path.join(step_dir, "synthesis.log") with open(log_path, "w") as f: f.write("content\n") - os.chmod(log_path, 0o000) - - try: - rc = cli_main.run(["log", "--project", project_dir]) - assert rc == 0 - out = capsys.readouterr().out - assert "tail:" not in out - assert "Synthesis_yosys" in out - assert "inspect:" in out - assert "Traceback" not in out - finally: - os.chmod(log_path, 0o644) + _make_path_unreadable(monkeypatch, log_path) + + rc = cli_main.run(["log", "--project", project_dir]) + assert rc == 0 + out = capsys.readouterr().out + assert "tail:" not in out + assert "Synthesis_yosys" in out + assert "inspect:" in out + assert "Traceback" not in out diff --git a/test/cli/rendering/test_log_view.py b/test/cli/rendering/test_log_view.py index aa64f087..18e8c49b 100644 --- a/test/cli/rendering/test_log_view.py +++ b/test/cli/rendering/test_log_view.py @@ -1,5 +1,3 @@ -import os - from chipcompiler.cli.inspection.log_view import ( LineKind, annotate_log_lines, @@ -852,17 +850,18 @@ def test_bel_and_backspace_stripped(self, tmp_path): result = tail_lines_for_log(str(log_file)) assert result == ["abc", "done"] - def test_unreadable_file_returns_empty(self, tmp_path): + def test_unreadable_file_returns_empty(self, monkeypatch, tmp_path): from chipcompiler.cli.inspection.log_view import tail_lines_for_log log_file = tmp_path / "test.log" log_file.write_text("content\n") - os.chmod(str(log_file), 0o000) - try: - result = tail_lines_for_log(str(log_file)) - assert result == [] - finally: - os.chmod(str(log_file), 0o644) + + def raise_permission_error(*args, **kwargs): + raise PermissionError("cannot read") + + monkeypatch.setattr("builtins.open", raise_permission_error) + result = tail_lines_for_log(str(log_file)) + assert result == [] class TestListingTailRendering: