diff --git a/.build-config.yml b/.build-config.yml index 95c100d..54eead3 100644 --- a/.build-config.yml +++ b/.build-config.yml @@ -4,4 +4,4 @@ tools: node: 20.12.2 python: 3.11 dependencies: - xcomm: 18f3e60e9eb9d38127e3ecdccfacf91cf016b19c + xcomm: c39db6652cd08d46df05d08f0bb5a033b83e4d2f diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2948a8d..750cbe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,6 +155,9 @@ jobs: - name: Build Python wheels if: matrix.full_test + env: + # Match the planned xcomm 1.x release while it is still in development. + SETUPTOOLS_SCM_PRETEND_VERSION_FOR_XSPCOMM: 1.0.0.dev0 run: | python3 -m venv /tmp/picker-wheel-builder /tmp/picker-wheel-builder/bin/python -m pip install 'build>=1,<2' diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index c1bd151..7ce2885 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -201,7 +201,7 @@ jobs: test "$(uname -m)" = "$ARCH" test -x AppDir/usr/bin/picker test -d AppDir/usr/share/picker/template - test -e AppDir/usr/lib/libxspcomm.so.1 + test -e AppDir/usr/lib/libxspcomm.so.3 output="$(AppDir/usr/bin/picker --version)" [[ "$output" == *"$VERSION"* ]] mkdir -p release-assets @@ -256,3 +256,17 @@ jobs: name: picker-release-bundle path: release-assets/* if-no-files-found: error + + dispatch-release: + needs: bundle + runs-on: ubuntu-24.04 + permissions: + actions: write + steps: + - name: Start release for this Tagged CI run + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh workflow run release.yml --repo "$GITHUB_REPOSITORY" --ref master \ + --raw-field "run_id=$GITHUB_RUN_ID" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 780b4d9..45dbcb9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,10 @@ name: Release Picker on: - workflow_run: - workflows: [Tagged CI] - types: [completed] workflow_dispatch: inputs: run_id: - description: "Successful Tagged CI run ID whose exact artifacts should be published" + description: "Tagged CI run ID to wait for and publish its exact artifacts" required: true type: string @@ -17,36 +14,24 @@ permissions: jobs: publish: - if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-24.04 permissions: contents: write actions: read pull-requests: read env: - SOURCE_RUN_ID: ${{ github.event.workflow_run.id || inputs.run_id }} + SOURCE_RUN_ID: ${{ inputs.run_id }} GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Validate source run + - name: Wait for successful Tagged CI completion + timeout-minutes: 65 run: | set -euo pipefail - [[ "$SOURCE_RUN_ID" =~ ^[0-9]+$ ]] - gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}" > source-run.json - python - <<'PY' - import json - import os - - with open("source-run.json") as source: - run = json.load(source) - assert run["repository"]["full_name"] == os.environ["GITHUB_REPOSITORY"] - assert run["path"].split("@", 1)[0].endswith("/.github/workflows/release-build.yml") or run["path"].split("@", 1)[0] == ".github/workflows/release-build.yml" - assert run["event"] in {"repository_dispatch", "workflow_dispatch"} - assert run["status"] == "completed" and run["conclusion"] == "success" - assert run["id"] == int(os.environ["SOURCE_RUN_ID"]) - PY + python3 scripts/release/wait_for_source_run.py \ + "$GITHUB_REPOSITORY" "$SOURCE_RUN_ID" --timeout 3600 - name: Download exact CI release bundle uses: actions/download-artifact@v4 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 3461359..8080e09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,11 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) #dependencies if ("$ENV{NO_BUILD_XSPCOMM}" STREQUAL "") add_subdirectory(dependence/xcomm) +# Cover pinned xcomm revisions as well as their SWIG consumers. Standalone +# xcomm wheels are built separately by the wheel target in the Makefile. +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(xspcomm PUBLIC -fno-strict-aliasing) +endif() endif() # Verilog/SystemVerilog frontend diff --git a/Makefile b/Makefile index e729993..2a6fb65 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ clean: rm -rf temp build dist output app_image_build AppDir picker_out* picker_e203_ifu_ift2icb wheel: - cd dependence/xcomm && $(PYTHON) -m build --wheel + cd dependence/xcomm && CFLAGS="$(CFLAGS) -fno-strict-aliasing" $(PYTHON) -m build --wheel NO_BUILD_XSPCOMM=1 $(PYTHON) -m build --wheel mkdir -p dist && cp dependence/xcomm/dist/*.whl ./dist diff --git a/doc/maintainer-guidance.md b/doc/maintainer-guidance.md index 74213d0..0fc3af7 100644 --- a/doc/maintainer-guidance.md +++ b/doc/maintainer-guidance.md @@ -1,140 +1,81 @@ -# Maintaining Picker releases +# Maintaining Picker -Picker and xspcomm have independent version tags. Release xspcomm first; -Picker's wheel and native packages use a compatible xspcomm release. This page -records the maintainer procedure. +## Versions and dependencies -## Versions and releases +Picker and xcomm have independent `vX.Y.Z` package tags. Picker derives its +package, CLI and CMake release versions from its own tag using setuptools-scm. +Release a compatible xcomm version before releasing Picker. -Version tags are independent in the two Git repositories. Use `vX.Y.Z` tags, -for example `v2.0.0` for Picker and `v0.1.0` for xcomm. The wheel version is -derived from the tag by setuptools-scm. Commits after a tag yield development -versions such as `2.0.1.dev3`; ordinary CI uploads these wheels as artifacts -and does not create a stable release. Older date/SHA AppImage tags do not -participate in Python package version calculation; CI no longer creates them. +| Setting | Source | Current requirement | +| --- | --- | --- | +| Development xcomm source | `.build-config.yml` | Exact tested commit; `XCOMM_SOURCE_REF` can override it | +| Python dependency | `pyproject.toml` | `xspcomm>=1.0.0.dev0,<2` | +| Development xcomm wheel | Development CI | `1.0.0.dev0` preview until xcomm 1.0 is released | +| Native ABI | `xspcomm.abi_version()` | 3; packages contain `libxspcomm.so.3` | +| Coverage descriptors | `XTriggerEngine.CoverageVersion()` | 5 | -Every PR targeting `master` must have exactly one of `release:patch`, -`release:minor`, or `release:major`. Other labels are allowed. The **Version -policy / release-label** check verifies this. In GitHub Settings → Rules → -Rulesets (or the `master` branch protection rule), add this check to the -required status checks; the workflow alone does not prevent merging. Create -the three labels in each repository. Also require the relevant CI build checks -and require an up-to-date PR before merging; otherwise tagging after merge does -not imply the merged source has passed CI. Do not use merge queue until the -required checks also support `merge_group`. +Package version, native ABI and descriptor protocol are separate identifiers. +When updating xcomm, align the source pin, Python range and preview version, +then rebuild native clients and generated DUT extensions against matching +headers and runtime. ABI checks cannot be replaced by a package version range. -After a labeled PR merges, the `master` push starts **Version policy**. It -finds the merged PR, computes the next version, pushes its tag and dispatches -**Tagged CI** with the tag and commit SHA. A direct push without an associated -merged PR does not create a tag. Every merged PR, including a -documentation-only PR, creates a version tag. The tag push itself does not -start CI when it was made using `GITHUB_TOKEN`; the explicit dispatch does. -Both version calculation and Tagged CI apply the same master-tag rule: the tag -must point to a commit on master's first-parent line that GitHub records as the -result of a merged PR targeting master. A higher-numbered tag on a feature -branch is ignored when calculating the next version, and manually requesting a -build for a tag outside this rule fails before any assets are built. Tag names -remain repository-wide, so a conflicting name elsewhere still blocks creation -of the next tag rather than being overwritten. Git itself does not record the -branch on which a tag was created. To prevent users from creating, moving or -deleting `v*` tags outside this workflow, protect that tag pattern with a -GitHub tag ruleset; the workflow checks the target commit, not the tagger's -historical branch. -The version-tag job serializes its runs, but GitHub does not guarantee that -rapid consecutive merges enter that queue in commit order. Until version -allocation is made order-aware, avoid merging another PR while a previous -Version policy run is still tagging; a later commit tagged first can cause the -older run to fail its ancestry check. +## Local build and validation -**Tagged CI** checks out that exact tag, tests the native builds and builds a -CPython 3.12 Linux x86-64 wheel, Linux x86-64 and aarch64 AppImages, and Linux -x86-64 and aarch64 native archives. It uploads one checked release bundle. -**Release Picker** runs only after Tagged CI succeeds, downloads that run's -bundle, checks its hashes and tag SHA, and publishes those exact files without -rebuilding. All five packages, `SHA256SUMS` and `RELEASE-MANIFEST.json` go into -one GitHub Release. The manifest records the Picker tag, commit, CI run ID and -xcomm version used. The archives contain the installed -`usr/bin/picker`, `usr/share/picker` templates and native libraries; they are -not bare executables. AppImage is the self-contained desktop distribution; -the archive is an installed tree for a compatible Linux system. The wheel is -for pip and depends on a separately published xspcomm wheel. Tagged CI selects -one compatible xspcomm version for all three package types. - -**Release Picker** never changes the tag. If the tagged build fails, rerun -**Tagged CI** manually with the existing tag and a compatible xcomm version. -If publication fails, rerun **Release Picker** with the successful Tagged CI -run ID; it publishes the same artifact. **Version policy** has a manual bump -input for recovering from a missed merge event. GitHub release wheels are not -automatically uploaded to PyPI; publishing there needs separate credentials -and platform-wheel policy. Development CI still builds AppImages and wheels, -but it no longer creates a date/SHA AppImage Release. The old `master`-push -Docker image publish is not part of this pipeline; Docker PR changes still -receive a build check. - -Development CI and Tagged CI use the same scripts under `scripts/build/` for -native toolchain setup, CMake build and install, AppImage packaging, and wheel -installation checks. A master PR runs full Development CI and the version-label -check. A non-master branch push also runs Development CI unless that branch -already has an open master PR; in that case the PR run owns the build. A merge -to master runs Version policy rather than a second development build. Tagged -CI uses the final tag and a published xcomm version; its checked artifacts are -the only inputs to Release Picker. PR and branch builds pin xcomm to the commit -in `.build-config.yml` (an explicit `XCOMM_SOURCE_REF` commit can override it) -and never become release assets. Tagged builds use the selected published xcomm -tag instead. The build refuses to switch an xcomm checkout with local changes. - -The `xspcomm>=0.1.0.dev0,<0.2` requirement in `pyproject.toml` is a deliberate -compatibility range, not an automatically generated version. Change it only -after testing the corresponding xcomm ABI and baseline APIs. Release metadata, -the installed Picker command and CMake receive their version from the same tag. - -## Building and installing locally - -Install a compiler, CMake, SWIG >= 4.2, Python development headers and the -Picker source dependencies. Run `make init` explicitly before building Picker -if those dependencies are missing. The wheel target itself does not fetch or -switch source repositories and does not remove existing packages. +Use a compiler, CMake, SWIG >= 4.2, Python development headers and a JDK +for Java tests. Run `make init` when source dependencies are missing. Build +from a clean checkout with a fresh wheel output directory; `make wheel` does +not align an existing xcomm checkout to the dependency pin or install packages +into the interpreter. ```sh -python3 -m venv /tmp/picker-package-build -/tmp/picker-package-build/bin/python -m pip install build -make wheel PYTHON=/tmp/picker-package-build/bin/python - -python3 -m venv /tmp/picker-package-test -/tmp/picker-package-test/bin/python -m pip install --no-index --find-links=dist picker -/tmp/picker-package-test/bin/python -m pip check -/tmp/picker-package-test/bin/picker --version +python3 -m venv /tmp/picker-build +/tmp/picker-build/bin/python -m pip install build +make unit_tests BUILD_XSPCOMM_SWIG=python,java +SETUPTOOLS_SCM_PRETEND_VERSION_FOR_XSPCOMM=1.0.0.dev0 \ + make wheel PYTHON=/tmp/picker-build/bin/python +bash scripts/build/verify-wheel.sh +make -C test all ``` -Use a fresh checkout or empty `dist` directory for release builds; pip selects -the highest matching version when several old wheels are present. `make -wheel_install` intentionally only prints an installation command and never -uninstalls packages from the current interpreter. - -Picker contains a compiled executable and xspcomm contains a SWIG/CPython -extension plus a C++ shared library. A matching wheel avoids compilation on -the user's machine. Picker's current sdist omits its vendored slang/fmt/yaml-cpp -sources, so **do not publish it as a pip fallback**. If no Picker wheel matches, -build from a full source checkout after `make init`; this does require local -compilation and the build toolchain. Use `--only-binary=:all:` when an -installation must fail instead of attempting a source build. -The wheel release currently targets CPython 3.12 on Linux x86-64. Native -archives and AppImages target Linux x86-64 and aarch64; expand these matrices -before claiming other platforms are supported. - -## Native compatibility - -xcomm's package version and C++ ABI version are different. The latter is -`XSPCOMM_ABI_VERSION` in xcomm's `CMakeLists.txt`; it controls the -`libxspcomm.so.N` SONAME and is exposed as `xspcomm.abi_version()`. Bump it -when a native ABI change is incompatible, then rebuild xcomm, Picker and any -generated DUT extensions. A package patch release alone must not change the -SONAME. A Python wheel is also tied to its CPython ABI and platform tag; -the SWIG module is not declared `abi3`. - -The wheel CI installs xcomm and Picker from built wheels in a clean virtual -environment, checks metadata against native versions, exercises signal and clock APIs, -and runs the installed Picker command. These checks do **not** yet prove that -an independently packaged DUT extension uses the same `libxspcomm` instance: -generated Python DUTs can still copy xcomm. Keep DUT wheel/linker relocation -tests as a separate release gate before publishing a general-purpose DUT wheel. +The preview override affects only xcomm. Picker keeps its Git-derived version. +CI's native build script refuses to switch an xcomm checkout with local changes. +Keep `-O3`, the native `-fno-strict-aliasing` flags and signal-value regressions +enabled. Wheel verification installs both packages into a fresh environment and +checks dependency consistency, native APIs, clock ownership and CLI exit codes. + +## Release procedure + +1. Publish the compatible xcomm release. Tagged CI selects a published stable + release satisfying `pyproject.toml`; it never uses a development preview. +2. Give the Picker master PR exactly one label: `release:patch`, `release:minor` + or `release:major`. Require the version-label and build checks in branch + protection, and test the PR against current master. Merge queues need + `merge_group` checks before they can be used. +3. After merge, **Version policy** creates the next tag and explicitly dispatches + **Tagged CI**. Direct master pushes without an associated merged PR do not + create a tag. Wait for tagging to finish before merging the next release PR. +4. Tagged CI builds and validates the exact tag: CPython 3.12 Linux x86-64 wheel, + plus x86-64/aarch64 AppImages and native archives, using one xcomm release. +5. After bundle upload, Tagged CI dispatches **Release Picker** with its run ID. + The publisher waits up to 60 minutes for successful completion, verifies the + bundle hashes and tag SHA, and publishes those exact artifacts without + rebuilding. GitHub Releases are not automatically uploaded to PyPI. + +Only tags on master's first-parent line that identify merged PR results are +accepted by the tagging/build policy. Configure a `v*` tag ruleset to restrict +manual creation or replacement. Do not publish the current Picker sdist as a +pip fallback: it omits vendored build dependencies; use a full source checkout +when a matching wheel is unavailable. + +## Recovery + +- Build failed: rerun **Tagged CI** with the existing tag and a compatible + published xcomm version. Do not move the tag. +- Publication failed: rerun **Release Picker** with the successful Tagged CI + run ID; it downloads the same checked bundle. +- Merge did not create a tag: inspect **Version policy**, then use its manual + bump input if recovery is needed. + +Generated DUTs may copy xcomm libraries. The package checks do not establish +that independently packaged DUT extensions load the same runtime instance; +verify ABI compatibility and relocation separately before publishing DUT wheels. diff --git a/example/MultiClock/example.py b/example/MultiClock/example.py index 5234f27..d5aae87 100644 --- a/example/MultiClock/example.py +++ b/example/MultiClock/example.py @@ -17,12 +17,12 @@ def test_multi_clock(): clk1, clk2, clk3 = XClock(lambda x: 0), XClock(lambda x: 0), XClock(lambda x: 0) clk4, clk5, clk6 = XClock(lambda x: 0), XClock(lambda x: 0), XClock(lambda x: 0) - clk1.Add(dut.xport.SelectPins(["reg1"])).Add(dut.clk1.xdata) - clk2.Add(dut.xport.SelectPins(["reg2"])).Add(dut.clk2.xdata) - clk3.Add(dut.xport.SelectPins(["reg3"])).Add(dut.clk3.xdata) - clk4.Add(dut.xport.SelectPins(["reg4"])).Add(dut.clk4.xdata) - clk5.Add(dut.xport.SelectPins(["reg5"])).Add(dut.clk5.xdata) - clk6.Add(dut.xport.SelectPins(["reg6"])).Add(dut.clk6.xdata) + clk1.Add(dut.xport.SelectPins(["reg1"])).Add(dut.clk1) + clk2.Add(dut.xport.SelectPins(["reg2"])).Add(dut.clk2) + clk3.Add(dut.xport.SelectPins(["reg3"])).Add(dut.clk3) + clk4.Add(dut.xport.SelectPins(["reg4"])).Add(dut.clk4) + clk5.Add(dut.xport.SelectPins(["reg5"])).Add(dut.clk5) + clk6.Add(dut.xport.SelectPins(["reg6"])).Add(dut.clk6) main_clock.FreqDivWith(1, clk1) main_clock.FreqDivWith(2, clk2) diff --git a/pyproject.toml b/pyproject.toml index 81e6afc..bf49267 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "scikit_build_core.build" name = "picker" dynamic = ["version"] dependencies = [ - "xspcomm>=0.1.0.dev0,<0.2", + "xspcomm>=1.0.0.dev0,<2", ] [[tool.dynamic-metadata]] diff --git a/scripts/build/verify-wheel.sh b/scripts/build/verify-wheel.sh index 21d8528..af87ad7 100644 --- a/scripts/build/verify-wheel.sh +++ b/scripts/build/verify-wheel.sh @@ -39,11 +39,40 @@ if os.environ.get("XCOMM_VERSION_EXPECTED"): assert xspcomm.version() == xcomm_version signal = xspcomm.XData(8, xspcomm.XData.InOut) signal.Set(0xA5) -assert signal.U() == 0xA5 +assert (signal.U() & 0xFF) == 0xA5 clock = xspcomm.XClock(lambda _: 0) clock.Step(2) assert clock.clk == 2 -assert xspcomm.abi_version() == 1 +assert xspcomm.abi_version() == 3 +assert xspcomm.XTriggerEngine.CoverageVersion() == 5 +assert xspcomm.XCoveragePoint().kind == xspcomm.XCoveragePointKind_Value +assert xspcomm.XCoverageBin().point_id == 0 +engine = xspcomm.XTriggerEngine(clock) +assert not clock.IsControlled() +engine.AcquireClock() +try: + try: + clock.Step(1) + except RuntimeError: + pass + else: + raise AssertionError("manual stepping bypassed native engine ownership") + assert clock.GetHalfTick() == 4 + assert engine.RunUntil(2).advanced_ticks == 2 + wide = xspcomm.XData(128, xspcomm.XData.InOut) + wide.Set((1 << 100) | 0xA5) + lhs = engine.ExprNewBitsSignal(wide, False) + rhs = engine.ExprNewBitsConst(128, False, (7).to_bytes(16, "little")) + total = engine.ExprNewBits(xspcomm.ExprOp_ADD, lhs, rhs, 128, False) + engine.ExprNewConst(0) # Collection must remove this unused node. + remap = engine.CollectExpressions(xspcomm.XIntVector([total])) + total = remap[total] + assert total >= 0 and engine.ExpressionNodeCount() == 3 + assert int.from_bytes(engine.ExprEvalBits(total), "little") == (1 << 100) | 0xAC +finally: + engine.ReleaseClock() +assert not clock.IsControlled() +clock.Step(1) picker = os.path.join(os.path.dirname(sys.executable), "picker") output = subprocess.check_output([picker, "--version"], text=True) diff --git a/scripts/release/wait_for_source_run.py b/scripts/release/wait_for_source_run.py new file mode 100644 index 0000000..7e5ebdd --- /dev/null +++ b/scripts/release/wait_for_source_run.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Wait for a validated Tagged CI run to complete successfully before publishing.""" + +import argparse +import json +import subprocess +import sys +import time + + +def fetch_run(repository: str, run_id: int) -> dict: + response = subprocess.check_output( + ("gh", "api", f"repos/{repository}/actions/runs/{run_id}"), + text=True, + timeout=30, + ) + return json.loads(response) + + +def wait_for_success( + repository: str, run_id: int, timeout: int = 3600, interval: int = 10 +) -> dict: + if run_id <= 0 or timeout <= 0 or interval <= 0: + raise ValueError("Run ID, timeout and polling interval must be positive") + deadline = time.monotonic() + timeout + while True: + run = fetch_run(repository, run_id) + path = run["path"].split("@", 1)[0] + if ( + run["repository"]["full_name"] != repository + or run["id"] != run_id + or not ( + path == ".github/workflows/release-build.yml" + or path.endswith("/.github/workflows/release-build.yml") + ) + or run["event"] not in {"repository_dispatch", "workflow_dispatch"} + ): + raise ValueError(f"Run {run_id} is not a dispatched Tagged CI run in {repository}") + if run["status"] == "completed": + if run["conclusion"] != "success": + raise RuntimeError( + f"Tagged CI run {run_id} completed with {run['conclusion']}; refusing to publish" + ) + print(f"Verified successful Tagged CI run: {run_id}", flush=True) + return run + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError(f"Timed out waiting for Tagged CI run {run_id} after {timeout}s") + print(f"Tagged CI run {run_id} is {run['status']}; waiting for completion", flush=True) + time.sleep(min(interval, remaining)) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("repository") + parser.add_argument("run_id", type=int) + parser.add_argument("--timeout", type=int, default=3600) + args = parser.parse_args() + try: + wait_for_success(args.repository, args.run_id, args.timeout) + except (ValueError, RuntimeError, TimeoutError, subprocess.SubprocessError) as error: + print(error, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/codegen/python.cpp b/src/codegen/python.cpp index e8b0094..a3986de 100644 --- a/src/codegen/python.cpp +++ b/src/codegen/python.cpp @@ -5,13 +5,13 @@ namespace picker { namespace codegen { namespace py { static const std::string xdata_init_template = - " self.{{pin_uniq_name}} = xsp.XPin(xsp.XData({{logic_pin_length}}, xsp.XData.{{logic_pin_type}}), self.event)\n"; + " self.{{pin_uniq_name}} = xsp.XData({{logic_pin_length}}, xsp.XData.{{logic_pin_type}})\n"; static const std::string xdata_binddpi_template = " self.{{pin_uniq_name}}.BindDPIPtr(self.dut.GetDPIHandle(\"{{pin_func_name}}\", 0), self.dut.GetDPIHandle(\"{{pin_func_name}}\", 1))\n"; static const std::string xdata_bindptr_template = " self.{{pin_uniq_name}}.BindNativeData(self.dut.NativeSignalAddr(\"{{logic_pin}}\"))\n"; static const std::string xport_add_template = - " self.xport.Add(\"{{pin_func_name}}\", self.{{pin_uniq_name}}.xdata)\n"; + " self.xport.Add(\"{{pin_func_name}}\", self.{{pin_uniq_name}})\n"; static const std::string xport_cascaded_template = " self.{{port_name}} = self.xport.NewSubPort(\"{{prefix_key}}_\")\n"; diff --git a/template/lib/CMakeLists.txt b/template/lib/CMakeLists.txt index ced3f19..5f0d84a 100644 --- a/template/lib/CMakeLists.txt +++ b/template/lib/CMakeLists.txt @@ -13,7 +13,7 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # enable coroutines set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED) -set(CFLAGS "$ENV{CFLAGS} -ftls-model=global-dynamic -O3 -march=native") +set(CFLAGS "$ENV{CFLAGS} -ftls-model=global-dynamic -O3 -fno-strict-aliasing -march=native") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines ${CFLAGS}") set(SIMULATOR_FLAGS "$ENV{SIMULATOR_FLAGS}") if(NOT "${SIMULATOR_FLAGS}" STREQUAL "") diff --git a/template/python/dut.py b/template/python/dut.py index fec4e10..35779fa 100644 --- a/template/python/dut.py +++ b/template/python/dut.py @@ -163,9 +163,9 @@ def GetInternalSignal(self, name: str, index=-1, is_array=False, use_vpi=False): if signal is None: return None if not isinstance(signal, xsp.XData): - self.internal_signals[name] = [xsp.XPin(s, self.event) for s in signal] + self.internal_signals[name] = list(signal) else: - self.internal_signals[name] = xsp.XPin(signal, self.event) + self.internal_signals[name] = signal return self.internal_signals[name] def GetInternalSignalList(self, prefix="", deep=99, use_vpi=False): @@ -192,7 +192,7 @@ def AtClone(self): ################################ def __getitem__(self, key): - return xsp.XPin(self.port[key], self.event) + return self.xport[key] # Async APIs wrapped from XClock async def AStep(self,i: int): @@ -206,8 +206,8 @@ def RunStep(self,i: int): def __setattr__(self, name, value): assert not isinstance(getattr(self, name, None), - (xsp.XPin, xsp.XData)), \ - f"XPin and XData of DUT are read-only, do you mean to set the value of the signal? please use `{name}.value = ` instead." + xsp.XData), \ + f"XData attributes of DUT are read-only; set the signal with `{name}.value = ...` instead." return super().__setattr__(name, value) diff --git a/template/uvm/Makefile b/template/uvm/Makefile index cde9f57..0030413 100755 --- a/template/uvm/Makefile +++ b/template/uvm/Makefile @@ -1,3 +1,5 @@ +XSP_COMM_TLM ?= $(XSP_COMM_INCLUDE)/../share/xspcomm/tlm + PYTHON_INCLUDE:=$(firstword $(shell python3-config --includes)) XSP_COMM_INCLUDE:=$(shell picker --show_xcom_lib_location_cpp | awk '/Include:/ {print $$2}') XSP_PYTHON:=$(shell picker --show_xcom_lib_location_python | sed -n '$$p') @@ -79,9 +81,9 @@ comp: @mkdir -p $(BUILD_DIR) swig -D'MODULE_NAME="tlm_pbsb"' -python -c++ -DUSE_VCS -I${XSP_COMM_INCLUDE} \ -outdir $(PKG_DIR) -o $(BUILD_DIR)/tlmps.cpp \ - ${XSP_COMM_INCLUDE}/xspcomm/python_tlm_pbsb.i + ${XSP_COMM_TLM}/python.i $(SYSCAN) -Mdir=$(BUILD_DIR) \ - ${XSP_COMM_INCLUDE}/xspcomm/tlm_pbsb.cpp $(BUILD_DIR)/tlmps.cpp + ${XSP_COMM_TLM}/pubsub.cpp $(BUILD_DIR)/tlmps.cpp @if [ -f example-uvm.sv ]; then \ UVM_FILE=example-uvm.sv; \ elif [ -f example.sv ]; then \ diff --git a/template/uvm/dut.py b/template/uvm/dut.py index b821a3d..9cacc87 100644 --- a/template/uvm/dut.py +++ b/template/uvm/dut.py @@ -88,31 +88,6 @@ def fix_path(arg): # ==================== DUT Implementation ==================== -class _PinWrapper: - """Wrapper to provide dut.pin.value access instead of dut.pin.xdata.value""" - def __init__(self, xpin: xsp.XPin): - self._xpin = xpin - - @property - def value(self): - return self._xpin.xdata.value - - @value.setter - def value(self, val): - self._xpin.xdata.value = val - - @property - def xpin(self): - return self._xpin - - @property - def xdata(self): - return self._xpin.xdata - - @property - def event(self): - return self._xpin.event - class DUT{{package_name}}: """ DUT abstraction for {{package_name}} with pin-level interface. @@ -134,29 +109,28 @@ class DUT{{package_name}}: def __init__(self, **kwargs): """Initialize DUT with Agent and xspcomm infrastructure.""" - self._event = xsp.Event() self._xports: Dict[str, xsp.XPort] = {} self._user_callback: Optional[Callable[['DUT{{package_name}}'], None]] = None - self._xpins = {} + self._xdata = {} self._callback_pending = False # Monitor feedback extra cycles (configurable) self._monitor_extra_cycles: int = kwargs.get('monitor_extra_cycles', 1) - # Initialize XPins and XPorts per transaction + # Initialize direct XData leaves and XPorts per transaction {% for trans in transactions -%} # Transaction: {{trans.name}} self._xports['{{trans.name}}'] = xsp.XPort() {% for data in trans.variables -%} - self._xpins['{{data.name}}'] = xsp.XPin(xsp.XData({{data.bit_count}}), self._event) - self._xpins['{{data.name}}'].xdata.AsImmWrite() - self._xports['{{trans.name}}'].Add("{{data.name}}", self._xpins['{{data.name}}'].xdata) + self._xdata['{{data.name}}'] = xsp.XData({{data.bit_count}}) + self._xdata['{{data.name}}'].AsImmWrite() + self._xports['{{trans.name}}'].Add("{{data.name}}", self._xdata['{{data.name}}']) {% endfor -%} {% endfor -%} - # Create pin accessors + # Expose the same XData leaves as DUT attributes. {% for data in variables -%} - self.{{data.name}} = _PinWrapper(self._xpins['{{data.name}}']) + self.{{data.name}} = self._xdata['{{data.name}}'] {% endfor -%} # Monitor callback @@ -167,7 +141,7 @@ def _monitor_callback(trans_type: str, trans_obj): {% for trans in transactions %} if trans_type == '{{trans.name}}': {% for data in trans.variables -%} - self._xpins['{{data.name}}'].xdata.value = trans_obj.{{data.name}}.value + self._xdata['{{data.name}}'].value = trans_obj.{{data.name}}.value {% endfor -%} {% endfor %} self._callback_pending = True @@ -204,7 +178,7 @@ def Step(self, cycles: int = 1): {% for trans in transactions -%} tr = {{trans.name}}() {% for data in trans.variables -%} - tr.{{data.name}}.value = self._xpins['{{data.name}}'].xdata.value + tr.{{data.name}}.value = self._xdata['{{data.name}}'].value {% endfor -%} self.agent.drive(tr) {% endfor -%} @@ -240,7 +214,7 @@ def SetUpdateCallback(self, callback: Optional[Callable[['DUT{{package_name}}'], def SetZero(self): """Set all pins to zero.""" {% for data in variables -%} - self._xpins['{{data.name}}'].xdata.value = 0 + self._xdata['{{data.name}}'].value = 0 {% endfor %} def InitClock(self, clock_pin: Optional[str] = None, domain: str = "default", frequency: Optional[float] = None): @@ -258,8 +232,8 @@ def InitClock(self, clock_pin: Optional[str] = None, domain: str = "default", fr dut.InitClock("clk") # Validate clock pin exists dut.InitClock("clk", domain="sys_clk", frequency=100e6) """ - if clock_pin is not None and clock_pin not in self._xpins: - raise ValueError(f"Clock pin '{clock_pin}' not found. Available pins: {list(self._xpins.keys())}") + if clock_pin is not None and clock_pin not in self._xdata: + raise ValueError(f"Clock pin '{clock_pin}' not found. Available pins: {list(self._xdata.keys())}") # Delegate to Agent self.agent.InitClock(domain=domain, frequency=frequency) @@ -323,7 +297,7 @@ def GetAgent(self): def __repr__(self): fields = [] {% for data in variables -%} - fields.append(f"{{data.name}}={self._xpins['{{data.name}}'].xdata.value}") + fields.append(f"{{data.name}}={self._xdata['{{data.name}}'].value}") {% endfor -%} if self.agent._clock_enabled: fields.append(f"cycle={self.agent.GetCycleCount()}") diff --git a/test/release/test_wait_for_source_run.py b/test/release/test_wait_for_source_run.py new file mode 100644 index 0000000..c2a0ca0 --- /dev/null +++ b/test/release/test_wait_for_source_run.py @@ -0,0 +1,89 @@ +import importlib.util +from pathlib import Path +import unittest +from unittest.mock import patch + + +SCRIPT = Path(__file__).resolve().parents[2] / "scripts/release/wait_for_source_run.py" +spec = importlib.util.spec_from_file_location("wait_for_source_run", SCRIPT) +source_run = importlib.util.module_from_spec(spec) +spec.loader.exec_module(source_run) + + +def run(status="completed", conclusion="success", **overrides): + return { + "id": 123, + "repository": {"full_name": "owner/repo"}, + "path": ".github/workflows/release-build.yml", + "event": "repository_dispatch", + "status": status, + "conclusion": conclusion, + **overrides, + } + + +class SourceRunTests(unittest.TestCase): + def test_waits_through_pending_states_until_success(self): + snapshots = [run("queued", None), run("in_progress", None), run()] + with patch.object(source_run, "fetch_run", side_effect=snapshots), patch.object( + source_run.time, "sleep" + ) as sleep, patch.object(source_run.time, "monotonic", return_value=0): + self.assertEqual(source_run.wait_for_success("owner/repo", 123), snapshots[-1]) + self.assertEqual(sleep.call_count, 2) + + def test_completed_success_does_not_wait(self): + with patch.object(source_run, "fetch_run", return_value=run()), patch.object( + source_run.time, "sleep" + ) as sleep: + source_run.wait_for_success("owner/repo", 123) + sleep.assert_not_called() + + def test_failed_or_cancelled_source_never_passes(self): + for conclusion in ("failure", "cancelled", "timed_out", "skipped", "neutral", None): + with self.subTest(conclusion=conclusion), patch.object( + source_run, "fetch_run", return_value=run(conclusion=conclusion) + ), patch.object(source_run.time, "sleep") as sleep: + with self.assertRaisesRegex(RuntimeError, "refusing to publish"): + source_run.wait_for_success("owner/repo", 123) + sleep.assert_not_called() + + def test_wrong_source_is_rejected_before_waiting(self): + for overrides in ( + {"id": 456}, + {"repository": {"full_name": "other/repo"}}, + {"path": ".github/workflows/ci.yml"}, + {"event": "pull_request"}, + ): + with self.subTest(overrides=overrides), patch.object( + source_run, "fetch_run", return_value=run("in_progress", None, **overrides) + ), patch.object(source_run.time, "sleep") as sleep: + with self.assertRaisesRegex(ValueError, "not a dispatched Tagged CI"): + source_run.wait_for_success("owner/repo", 123) + sleep.assert_not_called() + + def test_manual_source_and_qualified_workflow_path_are_accepted(self): + snapshot = run( + event="workflow_dispatch", + path="owner/repo/.github/workflows/release-build.yml@refs/heads/master", + ) + with patch.object(source_run, "fetch_run", return_value=snapshot): + self.assertEqual(source_run.wait_for_success("owner/repo", 123), snapshot) + + def test_wait_is_bounded_and_last_sleep_is_clipped_to_deadline(self): + with patch.object(source_run, "fetch_run", return_value=run("in_progress", None)), patch.object( + source_run.time, "monotonic", side_effect=[0, 3595, 3600] + ), patch.object(source_run.time, "sleep") as sleep: + with self.assertRaisesRegex(TimeoutError, "after 3600s"): + source_run.wait_for_success("owner/repo", 123) + sleep.assert_called_once_with(5) + + def test_invalid_inputs_do_not_query_github(self): + for values in ({"run_id": 0}, {"run_id": 123, "timeout": 0}, {"run_id": 123, "interval": 0}): + with self.subTest(values=values), patch.object(source_run, "fetch_run") as fetch: + with self.assertRaises(ValueError): + source_run.wait_for_success("owner/repo", **values) + fetch.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/test/scripts/test_export_adder_python.sh b/test/scripts/test_export_adder_python.sh index e54a32f..9e144bd 100644 --- a/test/scripts/test_export_adder_python.sh +++ b/test/scripts/test_export_adder_python.sh @@ -25,4 +25,81 @@ cp "${ROOT_DIR}/example/Adder/example.py" "${ROOT_DIR}/picker_out/Adder/python/" blue "[export-python] Building generated project" make -C "${ROOT_DIR}/picker_out/Adder" EXAMPLE=ON -j"$(nproc)" +blue "[export-python] Verifying generated signals against xcomm ABI 3" +( + cd "${ROOT_DIR}/picker_out" + python3 - <<'PY' +import Adder as generated + +assert generated.xsp.abi_version() == 3 +assert generated.xsp.XTriggerEngine.CoverageVersion() == 5 +dut = generated.DUTAdder() +try: + for name in ("a", "b", "cin", "sum", "cout"): + signal = getattr(dut, name) + assert isinstance(signal, generated.xsp.XData), name + assert dut[name].CSelf() == signal.CSelf(), name + try: + dut.a = 0 + except AssertionError: + pass + else: + raise AssertionError("assigning a signal must use its value property") + + mask = (1 << 128) - 1 + for a, b, cin in ((0, 0, 0), (mask, 1, 0), (1 << 127, 1 << 127, 1)): + dut["a"].value, dut.b.value, dut.cin.value = a, b, cin + dut.Step(1) + expected = a + b + cin + assert dut.sum.value == expected & mask + assert dut.cout.value == expected >> 128 + clock = dut.GetXClock() + engine = generated.xsp.XTriggerEngine(clock) + competing = generated.xsp.XTriggerEngine(clock) + assert not clock.IsControlled() + tick = clock.GetHalfTick() + engine.AcquireClock() + try: + for operation in (lambda: dut.Step(1), lambda: clock.RawStep(1), + clock.StepHalf, competing.AcquireClock): + try: + operation() + except RuntimeError: + pass + else: + raise AssertionError("clock ownership was bypassed") + assert clock.GetHalfTick() == tick + + cycles = engine.ArmClockCycles(2) + result = engine.RunUntil(8) + assert result.advanced_ticks == 4 + assert len(result.hits) == 1 and result.hits[0].tick == tick + 4 + assert engine.Disarm(cycles) + + point = generated.xsp.XCoveragePoint() + point.signal = dut.cin + bin_spec = generated.xsp.XCoverageBin() + bin_spec.point_id = 0 + bin_spec.program.root = engine.ExprNewConst(1) + points = generated.xsp.XCoveragePointVector([point]) + bins = generated.xsp.XCoverageBinVector([bin_spec]) + sample = engine.ArmSample(generated.xsp.XPhase_RisingStable) + engine.AttachCoverage(sample, points, bins) + result = engine.RunUntil(4) + snapshot = engine.CoverageSnapshot(sample) + assert result.advanced_ticks == 4 and not result.hits + assert snapshot.counters[0] == snapshot.counters[-1] == 2 + assert engine.Disarm(sample) + finally: + engine.Clear() + engine.ReleaseClock() + assert not clock.IsControlled() + dut.Step(1) + assert dut.sum.value == expected & mask + assert dut.cout.value == expected >> 128 +finally: + dut.Finish() +PY +) + green "[export-python] OK"