diff --git a/.coverity/README.md b/.coverity/README.md new file mode 100644 index 000000000..b5e84ff8a --- /dev/null +++ b/.coverity/README.md @@ -0,0 +1,9 @@ +# Coverity + +`model.c` describes HIDAPI's nullable, owned device handles; cleanup, I/O and +threading remain source-derived. Upload it under **Analysis Settings → Modeling +File** to reanalyze the last build. It is analysis input, never runtime code. + +`setup/` installs the capture tools. `merge.py` validates and combines native +Windows, macOS and Linux captures; `test_merge.py` checks its validation gates. +The [Checks workflow](../.github/workflows/checks.yml) runs capture and submission. diff --git a/.coverity/merge.py b/.coverity/merge.py new file mode 100644 index 000000000..78d71f1b7 --- /dev/null +++ b/.coverity/merge.py @@ -0,0 +1,78 @@ +"""Merge complete native captures and reject missing or damaged coverage.""" + +import collections +import json +import pathlib +import re +import shutil +import subprocess +import sys + + +def cov(*args): + result = subprocess.run( + ["timeout", "--kill-after=3s", "300s", "cov-manage-emit", *map(str, args)], + text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ) + if result.stderr: + print(result.stderr, file=sys.stderr, end="") + result.check_returncode() + return result.stdout + + +def version(text): + release = re.search(r"\bversion (\S+)", text) + internal = re.search(r"^Internal version numbers: (.+)", text, re.MULTILINE) + if not release or not internal: + raise ValueError("Unrecognized Coverity version output") + return release.group(1) + " " + internal.group(1).strip() + + +def units(idir): + listing = cov("--dir", idir, "list") + print(listing, end="") + paths = re.findall(r"^\s*\d+ -> (.+)$", listing, re.MULTILINE) + if not paths or "errors)" in listing: + raise RuntimeError(f"Empty or incomplete capture: {idir}") + return collections.Counter(path.replace("\\", "/") for path in paths) + + +def merge(captures, target): + expected = collections.Counter() + counts = {} + tool_version = version(cov("--ident")) + required_sources = { + "windows": ("/windows/hid.c",), + "macos": ("/mac/hid.c",), + "linux": ("/linux/hid.c", "/libusb/hid.c"), + } + for platform, required in required_sources.items(): + capture = captures / f"coverity-{platform}" + if version((capture / "coverity-version.txt").read_text(encoding="utf-8-sig")) != tool_version: + raise RuntimeError(f"Capture tool version mismatch: {platform}") + idir = capture / "cov-int" + cov("--dir", idir, "reset-host-name") + cov("--dir", idir, "check-compatible") + cov("--dir", idir, "check-integrity") + paths = units(idir) + for suffix in required: + if not any(path.endswith(suffix) for path in paths): + raise RuntimeError(f"Missing backend: {suffix}") + counts[platform] = paths.total() + expected.update(paths) + + # The target must be an existing capture; do not edit emit/ or its locks. + shutil.copytree(captures / "coverity-linux" / "cov-int", target) + for platform in ("windows", "macos"): + cov("--dir", target, "add", captures / f"coverity-{platform}" / "cov-int") + cov("--dir", target, "check-compatible") + cov("--dir", target, "check-integrity") + if units(target) != expected: + raise RuntimeError("Merged translation units differ from the native captures") + report = {"tool_version": tool_version, "captures": counts, "merged": expected.total()} + pathlib.Path("coverity-merge.json").write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + merge(pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])) diff --git a/.coverity/model.c b/.coverity/model.c new file mode 100644 index 000000000..06bd63c14 --- /dev/null +++ b/.coverity/model.c @@ -0,0 +1,55 @@ +/* + * HIDAPI Coverity Scan model. See LICENSE.txt for licensing terms. + * + * Analysis input only: upload through Analysis Settings -> Modeling File. + * Never compile or link this file into HIDAPI. + * + * Model the public handle factories, whose implementation varies by backend. + * Both return an owned handle or NULL. Keep hid_close() and all I/O, error, + * enumeration and synchronization functions derived from the captured source: + * their effects include backend-specific resources and borrowed storage. + * + * Uninitialized choices intentionally express nondeterminism in Coverity's + * modeling language. They are not executable C. + * + * Contracts: hidapi/hidapi.h (hid_open and hid_open_path). + * Primitives: https://scan.coverity.com/models + */ + +/* Scan compiles this file without system headers. The serial is never read, + * so its platform-specific character width does not affect this C model. */ +typedef int wchar_t; + +typedef struct hid_device_ hid_device; + +void *__coverity_alloc_nosize__(void); +void __coverity_writeall__(void *ptr); + +hid_device *hid_open(unsigned short vendor_id, unsigned short product_id, + const wchar_t *serial_number) +{ + int succeeds; + hid_device *dev; + (void)vendor_id; + (void)product_id; + (void)serial_number; + + if (!succeeds) + return 0; + dev = (hid_device *)__coverity_alloc_nosize__(); + __coverity_writeall__(dev); + return dev; +} + +hid_device *hid_open_path(const char *path) +{ + int succeeds; + hid_device *dev; + (void)path; + + if (!succeeds) + return 0; + dev = (hid_device *)__coverity_alloc_nosize__(); + __coverity_writeall__(dev); + return dev; +} diff --git a/.coverity/setup/action.yml b/.coverity/setup/action.yml new file mode 100644 index 000000000..b323f5dd4 --- /dev/null +++ b/.coverity/setup/action.yml @@ -0,0 +1,96 @@ +name: Set up Coverity Scan capture tools +description: Download or restore an unconfigured native Scan tool installation. +inputs: + platform: + description: Scan download platform (win64, Darwin, or linux64) + required: true + token: + description: Coverity Scan project token + required: true +runs: + using: composite + steps: + - name: Look up package hash + id: package + shell: bash + env: + COVERITY_PLATFORM: ${{ inputs.platform }} + COVERITY_SCAN_TOKEN: ${{ inputs.token }} + run: | + case "$COVERITY_PLATFORM" in win64|Darwin|linux64) ;; *) exit 1 ;; esac + hash=$(curl --fail --silent --show-error --connect-timeout 30 --max-time 120 \ + --data-urlencode "token=$COVERITY_SCAN_TOKEN" --data project=hidapi --data md5=1 \ + "https://scan.coverity.com/download/cxx/$COVERITY_PLATFORM") + [[ "$hash" =~ ^[[:xdigit:]]{32}$ ]] || { echo 'Invalid package hash'; exit 1; } + echo "hash=$hash" >> "$GITHUB_OUTPUT" + - uses: actions/cache/restore@v4 + id: cache + with: + path: cov-root + key: coverity-native-${{ inputs.platform }}-${{ steps.package.outputs.hash }} + - name: Download tools + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + env: + COVERITY_PLATFORM: ${{ inputs.platform }} + COVERITY_SCAN_TOKEN: ${{ inputs.token }} + COVERITY_PACKAGE_HASH: ${{ steps.package.outputs.hash }} + run: | + curl --fail --silent --show-error --location --connect-timeout 30 --max-time 540 \ + --data-urlencode "token=$COVERITY_SCAN_TOKEN" --data project=hidapi \ + --output coverity-package "https://scan.coverity.com/download/cxx/$COVERITY_PLATFORM" + if [[ "$COVERITY_PLATFORM" == Darwin ]]; then + actual_hash=$(md5 -q coverity-package) + else + actual_hash=$(md5sum coverity-package | cut -d ' ' -f 1) + fi + [[ "$actual_hash" == "$COVERITY_PACKAGE_HASH" ]] || { echo 'Package checksum mismatch'; exit 1; } + - name: Extract Windows tools + if: steps.cache.outputs.cache-hit != 'true' && inputs.platform == 'win64' + shell: pwsh + run: | + Move-Item -LiteralPath coverity-package -Destination coverity-package.zip + Expand-Archive -LiteralPath coverity-package.zip -DestinationPath cov-root + - name: Extract Linux tools + if: steps.cache.outputs.cache-hit != 'true' && inputs.platform == 'linux64' + shell: bash + run: | + mkdir cov-root + timeout --kill-after=3s 300s tar -xzf coverity-package --strip-components=1 -C cov-root + - name: Extract macOS tools + if: steps.cache.outputs.cache-hit != 'true' && inputs.platform == 'Darwin' + shell: bash + run: | + mv coverity-package coverity-package.dmg + mkdir coverity + hdiutil attach coverity-package.dmg -mountroot coverity + package_dirs=(coverity/*) + package_dir="${package_dirs[0]}" + installer="$package_dir/$(basename "$package_dir").sh" + mkdir cov-root + installer_name=$(basename "$installer") + cp "$installer" "cov-root/$installer_name" + chmod +x "cov-root/$installer_name" + (cd cov-root && "./$installer_name") + # Some packages remove their installer after extraction. + rm -f "cov-root/$installer_name" + hdiutil detach "$package_dir" + - name: Add tools to PATH + shell: bash + env: + COVERITY_PLATFORM: ${{ inputs.platform }} + run: | + if [[ "$COVERITY_PLATFORM" == win64 ]]; then + tool_dirs=("$PWD"/cov-root/*/bin) + cygpath -w "${tool_dirs[0]}" >> "$GITHUB_PATH" + else + echo "$PWD/cov-root/bin" >> "$GITHUB_PATH" + fi + - name: Verify tool installation + shell: bash + run: cov-manage-emit --ident + - uses: actions/cache/save@v4 + if: steps.cache.outputs.cache-hit != 'true' + with: + path: cov-root + key: ${{ steps.cache.outputs.cache-primary-key }} diff --git a/.coverity/test_merge.py b/.coverity/test_merge.py new file mode 100644 index 000000000..e3835d014 --- /dev/null +++ b/.coverity/test_merge.py @@ -0,0 +1,115 @@ +"""Validate submission gates without running or uploading an analysis.""" + +import contextlib +import io +import pathlib +import tempfile +import unittest +from unittest.mock import patch + +import merge + + +VERSION = "version 2024.12.1 on test\nInternal version numbers: test-build\n" +SOURCES = { + "windows": ["C:/src/windows/hid.c", "C:/src/hidtest/test.c"], + "macos": ["/mac/src/mac/hid.c"], + "linux": ["/linux/src/linux/hid.c", "/linux/src/libusb/hid.c", + "/linux/src/hidtest/test.c", "/linux/src/hidtest/test.c"], +} + + +class MergeTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = pathlib.Path(self.temp.name) + self.captures = self.root / "captures" + self.target = self.root / "combined" + self.sources = {key: value.copy() for key, value in SOURCES.items()} + self.calls = [] + self.drop_duplicate = False + for platform in SOURCES: + capture = self.captures / f"coverity-{platform}" + (capture / "cov-int").mkdir(parents=True) + (capture / "coverity-version.txt").write_text(VERSION) + + def fake_cov(self, *args): + self.calls.append(args) + if args == ("--ident",): + return VERSION + idir, command = args[1:3] + if command == "list": + if idir == self.target: + paths = [path for values in self.sources.values() for path in values] + if self.drop_duplicate: + paths = list(dict.fromkeys(paths)) + else: + paths = self.sources[idir.parent.name.removeprefix("coverity-")] + return "".join(f"Translation unit:\n{i} -> {path}\n" for i, path in enumerate(paths, 1)) + return "" + + def run_merge(self): + with patch.object(merge, "cov", self.fake_cov), \ + patch.object(pathlib.Path, "write_text") as report, \ + contextlib.redirect_stdout(io.StringIO()): + merge.merge(self.captures, self.target) + return report.call_args.args[0] + + def test_preserves_repeated_sources(self): + report = self.run_merge() + self.assertIn('"merged": 7', report) + self.assertEqual(sum(call[2] == "add" for call in self.calls if len(call) > 2), 2) + + def test_rejects_version_mismatch(self): + (self.captures / "coverity-macos/coverity-version.txt").write_text( + VERSION.replace("test-build", "other-build")) + with self.assertRaisesRegex(RuntimeError, "version mismatch"): + self.run_merge() + + def test_rejects_empty_capture(self): + self.sources["windows"] = [] + with self.assertRaisesRegex(RuntimeError, "Empty or incomplete"): + self.run_merge() + + def test_rejects_missing_backend(self): + self.sources["linux"].remove("/linux/src/libusb/hid.c") + with self.assertRaisesRegex(RuntimeError, "Missing backend"): + self.run_merge() + + def test_rejects_recoverable_errors(self): + self.sources["linux"][1] += " (recoverable errors)" + with self.assertRaisesRegex(RuntimeError, "Empty or incomplete"): + self.run_merge() + + def test_rejects_lost_duplicate(self): + self.drop_duplicate = True + with self.assertRaisesRegex(RuntimeError, "differ"): + self.run_merge() + + def test_rejects_invalid_version(self): + with self.assertRaisesRegex(ValueError, "version output"): + merge.version("unrecognized output") + + def test_accepts_tool_prefixed_version(self): + self.assertEqual(merge.version("Coverity tool " + VERSION), merge.version(VERSION)) + + def test_accepts_windows_text(self): + (self.captures / "coverity-windows/coverity-version.txt").write_bytes( + ("\ufeff" + VERSION.replace("\n", "\r\n")).encode("utf-8")) + self.sources["windows"] = [path.replace("/", "\\") for path in SOURCES["windows"]] + self.assertIn('"merged": 7', self.run_merge()) + + def test_rejects_unrecoverable_errors(self): + self.sources["linux"][1] += " (unrecoverable errors)" + with self.assertRaisesRegex(RuntimeError, "Empty or incomplete"): + self.run_merge() + + def test_rejects_missing_metadata(self): + (self.captures / "coverity-macos/coverity-version.txt").unlink() + with self.assertRaises(FileNotFoundError): + self.run_merge() + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 217853f5a..6f426c845 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,198 +1,176 @@ name: Checks run-name: Code checks for '${{ github.ref_name }}' -# General comment: -# Coverity doesn't support merging or including reports from multible machine/platforms (at least not officially). -# But otherwise there is no good way to keep the issues from all platforms at Coverity Scans at once. -# This script uses undocumented (but appears to be working) hack: -# The build logs from one machine/platform gets moved to a next once, -# and "fixed" so that cov-build can append logs from the next platform. -# The "fix" is based on the fact, that Coverity perfectly allows appending logs from multiple builds -# that are done *on the same host* machine. - on: - # On-demand run workflow_dispatch: - # Weekly run schedule: - cron: '30 5 * * 0' +permissions: + contents: read + +# Coverity Scan has one snapshot history for this project. Serialize submissions +# across branches, while allowing the three native captures to run in parallel. +concurrency: + group: coverity-scan + cancel-in-progress: false + jobs: coverity-windows: runs-on: windows-latest - + timeout-minutes: 30 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: path: src - name: Setup MSVC uses: TheMrMilchmann/setup-msvc-dev@v2.0.0 with: arch: x64 - - name: Configure - run: | - cmake -B build -S src -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_HIDTEST=ON - - name: Lookup Coverity Build Tool hash - id: coverity-cache-lookup - run: | - $coverity_hash=Invoke-RestMethod -Uri https://scan.coverity.com/download/cxx/win64 -Method Post -Body @{token='${{ secrets.COVERITY_SCAN_TOKEN }}';project='hidapi';md5=1} - echo "coverity_hash=$coverity_hash" >> $Env:GITHUB_OUTPUT - - name: Get cached Coverity Build Tool - id: cov-build-cache - uses: actions/cache@v3 + - uses: ./src/.coverity/setup + timeout-minutes: 15 with: - path: cov-root - key: cov-root-cxx-win64-${{ steps.coverity-cache-lookup.outputs.coverity_hash }} - - name: Get and configure Coverity - if: steps.cov-build-cache.outputs.cache-hit != 'true' - run: | - Invoke-WebRequest -Uri https://scan.coverity.com/download/cxx/win64 -OutFile coverity.zip -Method Post -Body @{token='${{ secrets.COVERITY_SCAN_TOKEN }}';project='hidapi'} - Remove-Item 'cov-root' -Recurse -Force -ErrorAction SilentlyContinue - Expand-Archive coverity.zip -DestinationPath cov-root - - $cov_root=Get-ChildItem -Path 'cov-root' - $Env:PATH += ";$($cov_root.FullName)\bin" - cov-configure -msvc - - name: Make Coverity available in PATH - run: | - $cov_root=Get-ChildItem -Path 'cov-root' - echo "$($cov_root.FullName)\bin" >> $Env:GITHUB_PATH - - name: Build with Coverity + platform: win64 + token: ${{ secrets.COVERITY_SCAN_TOKEN }} + - name: Configure + timeout-minutes: 5 + run: cmake -B build -S src -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_HIDTEST=ON + - name: Capture Windows + timeout-minutes: 10 working-directory: build run: | + cov-configure --msvc + if ($LASTEXITCODE -ne 0) { throw 'cov-configure failed' } cov-build --dir cov-int nmake - Rename-Item ".\cov-int\emit\$(hostname)" hostname - - name: Backup Coverity logs - uses: actions/upload-artifact@v4 + if ($LASTEXITCODE -ne 0) { throw 'cov-build failed' } + cov-manage-emit --ident > coverity-version.txt + if ($LASTEXITCODE -ne 0) { throw 'cov-manage-emit failed' } + tar -czf ../coverity-windows.tar.gz cov-int coverity-version.txt + if ($LASTEXITCODE -ne 0) { throw 'tar failed' } + - uses: actions/upload-artifact@v4 with: - name: coverity-logs-windows - path: build/cov-int + name: coverity-windows + path: coverity-windows.tar.gz + if-no-files-found: error retention-days: 7 - coverity-macos: runs-on: macos-15-intel - needs: [coverity-windows] - + timeout-minutes: 30 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: path: src + - uses: ./src/.coverity/setup + timeout-minutes: 15 + with: + platform: Darwin + token: ${{ secrets.COVERITY_SCAN_TOKEN }} - name: Install dependencies + timeout-minutes: 10 run: brew install ninja - name: Configure - run: | - cmake -B build -S src -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_HIDTEST=ON -DCMAKE_C_COMPILER=clang - - uses: actions/download-artifact@v4 - with: - name: coverity-logs-windows - path: build/cov-int - - name: Fixup cov-int - run: | - rm -f build/cov-int/emit/hostname/emit-db.lock build/cov-int/emit/hostname/emit-db.write-lock - mv build/cov-int/emit/hostname build/cov-int/emit/$(hostname) - - name: Lookup Coverity Build Tool hash - id: coverity-cache-lookup - shell: bash - run: | - hash=$(curl https://scan.coverity.com/download/cxx/Darwin --data "token=${{ secrets.COVERITY_SCAN_TOKEN }}&project=hidapi&md5=1") - echo "coverity_hash=${hash}" >> $GITHUB_OUTPUT - - name: Get cached Coverity Build Tool - id: cov-build-cache - uses: actions/cache@v3 - with: - path: cov-root - key: cov-root-cxx-Darwin-${{ steps.coverity-cache-lookup.outputs.coverity_hash }} - - name: Get and configure Coverity - if: steps.cov-build-cache.outputs.cache-hit != 'true' - run: | - curl https://scan.coverity.com/download/cxx/Darwin --output coverity.dmg --data "token=${{ secrets.COVERITY_SCAN_TOKEN }}&project=hidapi" - hdiutil attach coverity.dmg -mountroot coverity - export COV_DIR_NAME=$(ls -1 --color=never coverity) - rm -rf cov-root - mkdir cov-root - cp ./coverity/${COV_DIR_NAME}/${COV_DIR_NAME}.sh cov-root/ - cd cov-root/ - ./${COV_DIR_NAME}.sh - ./bin/cov-configure --clang - - name: Make Coverity available in PATH - run: echo "$(pwd)/cov-root/bin" >> $GITHUB_PATH - - name: Build with Coverity + timeout-minutes: 5 + run: cmake -B build -S src -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_HIDTEST=ON -DCMAKE_C_COMPILER=clang + - name: Capture macOS + timeout-minutes: 10 working-directory: build run: | - cov-build --dir cov-int --append-log ninja - mv cov-int/emit/$(hostname) cov-int/emit/hostname - - name: Backup Coverity logs - uses: actions/upload-artifact@v4 + cov-configure --clang + cov-build --dir cov-int ninja + cov-manage-emit --ident > coverity-version.txt + tar -czf ../coverity-macos.tar.gz cov-int coverity-version.txt + - uses: actions/upload-artifact@v4 with: - name: coverity-logs-windows-macos - path: build/cov-int + name: coverity-macos + path: coverity-macos.tar.gz + if-no-files-found: error retention-days: 7 - - coverity-ubuntu: - runs-on: ubuntu-latest - needs: [coverity-macos] - + coverity-linux: + # Keep the compiler within the range understood by the Scan capture tools. + runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: path: src + - uses: ./src/.coverity/setup + timeout-minutes: 15 + with: + platform: linux64 + token: ${{ secrets.COVERITY_SCAN_TOKEN }} - name: Install dependencies + timeout-minutes: 10 run: | sudo apt update - sudo apt install libudev-dev libusb-1.0-0-dev ninja-build + sudo apt install -y libudev-dev libusb-1.0-0-dev ninja-build gcc-13 - name: Configure + timeout-minutes: 5 + run: cmake -B build -S src -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_HIDTEST=ON -DCMAKE_C_COMPILER=gcc-13 -DCMAKE_C_STANDARD=11 + - name: Capture Linux backends + timeout-minutes: 10 + working-directory: build run: | - cmake -B build -S src -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_HIDTEST=ON -DCMAKE_C_COMPILER=gcc - - uses: actions/download-artifact@v4 + cov-configure --template --compiler gcc-13 --comptype gcc + cov-build --dir cov-int ninja + cov-manage-emit --ident > coverity-version.txt + tar -czf ../coverity-linux.tar.gz cov-int coverity-version.txt + - uses: actions/upload-artifact@v4 with: - name: coverity-logs-windows-macos - path: build/cov-int - - name: Fixup cov-int - run: | - rm -f build/cov-int/emit/hostname/emit-db.lock build/cov-int/emit/hostname/emit-db.write-lock - mv build/cov-int/emit/hostname build/cov-int/emit/$(hostname) - - name: Lookup Coverity Build Tool hash - id: coverity-cache-lookup - shell: bash - run: | - hash=$(curl https://scan.coverity.com/download/cxx/linux64 --data "token=${{ secrets.COVERITY_SCAN_TOKEN }}&project=hidapi&md5=1") - echo "coverity_hash=${hash}" >> $GITHUB_OUTPUT - - name: Get cached Coverity Build Tool - id: cov-build-cache - uses: actions/cache@v3 + name: coverity-linux + path: coverity-linux.tar.gz + if-no-files-found: error + retention-days: 7 + + coverity-submit: + needs: [coverity-windows, coverity-macos, coverity-linux] + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 with: - path: cov-root - key: cov-root-cxx-linux64-${{ steps.coverity-cache-lookup.outputs.coverity_hash }} - - name: Get and configure Coverity - if: steps.cov-build-cache.outputs.cache-hit != 'true' - run: | - curl https://scan.coverity.com/download/cxx/linux64 --output coverity.tar.gz --data "token=${{ secrets.COVERITY_SCAN_TOKEN }}&project=hidapi" - rm -rf cov-root - mkdir cov-root - tar -xzf coverity.tar.gz --strip 1 -C cov-root - ./cov-root/bin/cov-configure --gcc - - name: Make Coverity available in PATH - run: echo "$(pwd)/cov-root/bin" >> $GITHUB_PATH - - name: Build with Coverity - working-directory: build - run: | - cov-build --dir cov-int --append-log ninja - - name: Submit results to Coverity Scan - working-directory: build - run: | + path: src + - uses: ./src/.coverity/setup + timeout-minutes: 15 + with: + platform: linux64 + token: ${{ secrets.COVERITY_SCAN_TOKEN }} + - uses: actions/download-artifact@v4 + with: + pattern: coverity-* + path: captures + - name: Merge and validate captures + timeout-minutes: 10 + run: | + python3 -B src/.coverity/test_merge.py + for platform in windows macos linux; do + tar -xzf "captures/coverity-$platform/coverity-$platform.tar.gz" -C "captures/coverity-$platform" + done + python3 src/.coverity/merge.py captures cov-int tar -czf cov-int.tar.gz cov-int - curl --form token=${{ secrets.COVERITY_SCAN_TOKEN }} \ - --form email=${{ secrets.COVERITY_SCAN_EMAIL }} \ - --form file=@cov-int.tar.gz \ - --form version="$GITHUB_SHA" \ - --form description="Automatic HIDAPI build" \ - https://scan.coverity.com/builds?project=hidapi - mv cov-int/emit/$(hostname) cov-int/emit/hostname - - name: Backup Coverity logs + - name: Archive merged capture uses: actions/upload-artifact@v4 with: - name: coverity-logs-windows-macos-linux - path: build/cov-int + name: coverity-combined + path: | + cov-int.tar.gz + coverity-merge.json + if-no-files-found: error retention-days: 7 + - name: Submit results to Coverity Scan + timeout-minutes: 10 + env: + COVERITY_SCAN_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }} + COVERITY_SCAN_EMAIL: ${{ secrets.COVERITY_SCAN_EMAIL }} + run: | + curl --fail-with-body --silent --show-error --connect-timeout 30 --max-time 540 \ + --output coverity-submit-response.txt \ + --form-string "token=$COVERITY_SCAN_TOKEN" \ + --form-string "email=$COVERITY_SCAN_EMAIL" \ + --form file=@cov-int.tar.gz \ + --form-string "version=$GITHUB_SHA" \ + --form-string "description=Independent Windows, macOS and Linux captures" \ + https://scan.coverity.com/builds?project=hidapi + cat coverity-submit-response.txt + grep -Eq '^Build successfully submitted\.[[:space:]]*$' coverity-submit-response.txt