diff --git a/.github/workflows/build-resdata.yml b/.github/workflows/build-resdata.yml new file mode 100644 index 000000000000..67300e35eebd --- /dev/null +++ b/.github/workflows/build-resdata.yml @@ -0,0 +1,196 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on: https://github.com/equinor/resdata/blob/6.3.3/.github/workflows/testing.yml +name: Build resdata wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/resdata.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-resdata.yml' + - 'docs/packages/resdata.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-resdata.yml' + - 'docs/packages/resdata.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: resdata + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build resdata ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 300 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + # Upstream ships no free-threaded wheel. + python: ["cp312", "cp313", "cp314"] + + env: + RESDATA_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout resdata ${{ env.RESDATA_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: equinor/resdata + ref: ${{ env.RESDATA_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Apply patches + run: git apply python-wheels/patches/resdata/${{ env.RESDATA_VERSION }}/*.patch + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # Conan's ninja recipe links its binary -static-libstdc++; the riscv64 image + # ships only the shared libstdc++.so, not the .a (gotcha 77). + CIBW_BEFORE_ALL_LINUX: dnf -y install libstdc++-static + # upstream's build[uv] needs a host uv the runner has none of (gotcha 13) + CIBW_BUILD_FRONTEND: build + # PyPI's newest cmake<4 is sdist-only on riscv64, ours is a wheel (gotcha 30); CMake 4 + # turns setup.py's relative CMAKE_INSTALL_*DIR into build-tree paths skbuild rejects. + # The "Apply patches" step's `git apply` dirties the setuptools_scm checkout, so + # git describe reports a post-6.3.3 dev version instead of the clean tag (gotcha 31). + # setup.py calls setuptools_scm.get_version() directly without a dist_name, so only + # the bare SETUPTOOLS_SCM_PRETEND_VERSION is checked - the _FOR_RESDATA suffix needs + # a known dist_name and is silently ignored here. + CIBW_ENVIRONMENT: >- + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + PIP_ONLY_BINARY=cmake + SETUPTOOLS_SCM_PRETEND_VERSION=${{ env.RESDATA_VERSION }} + # Conan links these into libresdata statically and upstream ships no licence + # text for them, so stage each package's own licences at the project root. + CIBW_BEFORE_BUILD: | + set -eu + rm -rf {package}/_skbuild + pip install conan "cmake<4" + conan profile detect --force + conan install {package} --output-folder=/tmp/conan --build=missing --deployer=full_deploy --deployer-folder=/tmp/conan + for dep in /tmp/conan/full_deploy/host/*; do + find "$dep" -path '*/licenses/*' -type f | sort | xargs -r cat > "{package}/LICENSE.${dep##*/}" + test -s "{package}/LICENSE.${dep##*/}" + done + # The registry trails PyPI for these, and pydantic pins pydantic-core exactly. + CIBW_TEST_ENVIRONMENT: PIP_ONLY_BINARY=numpy,scipy,pandas,pydantic-core + # A hang in native code never returns to Python, so only the thread method can end it. + CIBW_TEST_COMMAND: >- + python -c "import importlib.metadata as m; + got = sorted(p.name for p in m.files('resdata') if '.dist-info/licenses/' in str(p)); + assert got == ['COPYING', 'LICENSE', 'LICENSE.backward-cpp', 'LICENSE.bzip2', 'LICENSE.elfutils', 'LICENSE.fmt', 'LICENSE.pybind11', 'LICENSE.xz_utils', 'LICENSE.zlib', 'LICENSE.zstd'], got" && + pytest --timeout=900 --timeout-method=thread {package}/tests + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: resdata-${{ env.RESDATA_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + gpl_sources: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + name: Collect GPL sources (elfutils) for resdata ${{ matrix.version }} + runs-on: ubuntu-latest + env: + RESDATA_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout resdata ${{ env.RESDATA_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: equinor/resdata + ref: ${{ env.RESDATA_VERSION }} + persist-credentials: false + + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + python-version: '3.12' + activate-environment: true + enable-cache: false + + # libresdata statically links libdw/libelf (GPL-2.0+ or LGPL-3.0+) from the + # elfutils that Conan builds from conan.lock's recipe revision and its patches. + - name: Archive the elfutils source and Conan recipe + run: | + set -euo pipefail + uv pip install conan + conan profile detect + ref=$(python -c 'import json; print(next(r.split("%")[0] for r in json.load(open("conan.lock"))["requires"] if r.startswith("elfutils/")))') + conan download "$ref" -r conancenter --only-recipe + recipe=$(conan cache path "$ref") + read -r url sha256 < <(python - "$recipe/conandata.yml" "${ref#*/}" <<'PY' + import sys, yaml + source = yaml.safe_load(open(sys.argv[1]))["sources"][sys.argv[2].split("#")[0]] + print(source["url"], source["sha256"]) + PY + ) + mkdir -p gpl-sources/recipe + curl -fsSL --retry 5 -o "gpl-sources/${url##*/}" "$url" + echo "$sha256 gpl-sources/${url##*/}" | sha256sum -c - + cp -r "$recipe"/. "$(conan cache path --folder export_source "$ref")"/. gpl-sources/recipe/ + tar -cf gpl-sources.tar gpl-sources + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: resdata-${{ env.RESDATA_VERSION }}-gpl-sources + path: gpl-sources.tar + if-no-files-found: error + + publish: + name: Publish resdata ${{ matrix.version }} + needs: [setup, build_wheels, gpl_sources] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: resdata-${{ matrix.version }}-*-manylinux_riscv64 + gpl-sources-artifact: resdata-${{ matrix.version }}-gpl-sources + gpl-sources-description: elfutils diff --git a/docs/packages/resdata.yaml b/docs/packages/resdata.yaml new file mode 100644 index 000000000000..ca89e845b347 --- /dev/null +++ b/docs/packages/resdata.yaml @@ -0,0 +1,5 @@ +package-name: resdata +source-code: https://github.com/equinor/resdata +license: GPL-3.0-only +versions: +- version: 6.3.3 diff --git a/patches/resdata/6.3.3/0001-Fix-infinite-loops-at-EOF-where-char-is-unsigned.patch b/patches/resdata/6.3.3/0001-Fix-infinite-loops-at-EOF-where-char-is-unsigned.patch new file mode 100644 index 000000000000..d460f9f19b53 --- /dev/null +++ b/patches/resdata/6.3.3/0001-Fix-infinite-loops-at-EOF-where-char-is-unsigned.patch @@ -0,0 +1,72 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat, 17 Oct 2026 00:00:00 +0000 +Subject: [PATCH] Fix infinite loops at EOF where char is unsigned + +Upstream-Status: Backport [https://github.com/equinor/resdata/commit/ce341a3666855c57839466d9daa876252b54c8c0] + +fgetc() returns EOF (-1) as an int. Three loops store it in a plain +char and then test c == EOF; where char is unsigned (riscv64, aarch64) +c becomes 255, the test is always false (GCC: "comparison is always +false due to limited range of data type"), and the loop spins forever +once fgetc() keeps returning EOF: + +- rd_kw_qskip(): any formatted file without a quote character, e.g. + tests/rd_tests/test_rd_file.py::test_broken_file, which hangs the + test suite at that test on riscv64; +- util_fskip_lines(): fixed upstream by the commit above; +- fscanf_grdecl_data(): a trailing "--" comment without a newline. + +Upstream fixed the first as part of the rd::KW rewrite (d9eacc14e, +skip_space_until_quote()) and removed the third file in c05264026; +all three fixes are in 7.0.0b3 and in none of the 6.3.x releases. + +Signed-off-by: Ludovic Henry +--- + lib/resdata/rd_kw.cpp | 2 +- + lib/resdata/rd_kw_grdecl.cpp | 2 +- + lib/util/util.cpp | 2 +- + 3 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/lib/resdata/rd_kw.cpp b/lib/resdata/rd_kw.cpp +index 17e08f4e7..f3c2475d1 100644 +--- a/lib/resdata/rd_kw.cpp ++++ b/lib/resdata/rd_kw.cpp +@@ -1043,7 +1043,7 @@ static bool rd_kw_qskip(FILE *stream) { + const char newline = '\n'; + const char tab = '\t'; + bool OK = true; +- char c; ++ int c; + bool cont = true; + while (cont) { + c = fgetc(stream); +diff --git a/lib/resdata/rd_kw_grdecl.cpp b/lib/resdata/rd_kw_grdecl.cpp +index 55785de1f..34262a88b 100644 +--- a/lib/resdata/rd_kw_grdecl.cpp ++++ b/lib/resdata/rd_kw_grdecl.cpp +@@ -232,7 +232,7 @@ fscanf_grdecl_data(const char *header, bool strict, int &kw_size, + if (fscanf(stream, "%32s", buffer.get()) == 1) { + if (strcmp(buffer.get(), RD_COMMENT_STRING) == 0) { + // We have read a comment marker - just read up to the end of line. +- char c; ++ int c; + while (true) { + c = fgetc(stream); + if (c == newline) +diff --git a/lib/util/util.cpp b/lib/util/util.cpp +index 4014b594b..ad2485779 100644 +--- a/lib/util/util.cpp ++++ b/lib/util/util.cpp +@@ -807,7 +807,7 @@ void util_fskip_lines(FILE *stream, int lines) { + int line_nr = 0; + do { + bool at_eof = false; +- char c; ++ int c; + do { + c = fgetc(stream); + if (c == EOF) +-- +2.47.3 + diff --git a/patches/resdata/6.3.3/0002-Raise-test_performance_with_many_keys-timeout-for-sl.patch b/patches/resdata/6.3.3/0002-Raise-test_performance_with_many_keys-timeout-for-sl.patch new file mode 100644 index 000000000000..4a2ab693903f --- /dev/null +++ b/patches/resdata/6.3.3/0002-Raise-test_performance_with_many_keys-timeout-for-sl.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat, 17 Oct 2026 00:00:00 +0000 +Subject: [PATCH] Raise test_performance_with_many_keys timeout for slower + hosts + +Upstream-Status: Inappropriate [the 10s ceiling is sized for upstream's x86 CI; the riscv64 runners finish the same bounded work in about 12s] + +The test builds a 5000-key summary case and lets pytest-benchmark run +summary.x over it. A call that takes longer than max_time (1s) makes +pytest-benchmark do exactly 2 calibration calls plus min_rounds (5), +so the work is a fixed 7 calls. On x86_64 one call takes about 0.1s +and the whole test about 1.2s, well inside its 10s marker. On the +riscv64 runners one call takes about 1.4s: the timeout fired after +the sixth call had printed its table, during the seventh and last. + +The profile is linear in the number of keys (one SummaryKeyType +lookup per key); nothing loops. Raise the ceiling to 60s so it still +catches a real regression (a quadratic key lookup blows far past it) +without failing on a slower host. + +Signed-off-by: Ludovic Henry +--- + tests/test_view_summary.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/test_view_summary.py b/tests/test_view_summary.py +index 91ae42c..dca1053 100644 +--- a/tests/test_view_summary.py ++++ b/tests/test_view_summary.py +@@ -855,7 +855,7 @@ def test_that_restart_and_base_times_are_concated(capsys): + + + @pytest.mark.usefixtures("use_tmpdir") +-@pytest.mark.timeout(10) ++@pytest.mark.timeout(60) + def test_performance_with_many_keys(monkeypatch, benchmark): + subdir = Path("subdir") + subdir.mkdir()