diff --git a/.github/workflows/binding-drift.yml b/.github/workflows/binding-drift.yml new file mode 100644 index 0000000..5000287 --- /dev/null +++ b/.github/workflows/binding-drift.yml @@ -0,0 +1,118 @@ +name: binding drift + +# The whole model of this repository rests on the binding sources being verbatim copies of +# DatadogNet.iOS's - the Catalyst head of the façade compiles against them on that assumption. +# This workflow turns "do not edit the copies here" from prose into a failing check: it checks +# out DatadogNet.iOS at the commit build/ios-bindings-source.txt records (written by the sync +# script), re-runs the sync, and fails on any difference. A sync recorded from an uncommitted +# iOS tree disarms the guard with a warning until a clean sync replaces it. +# +# The bindings are not the only hand-synced copies, so the same job also compares the tooling +# files that are carried from DatadogNet.iOS by hand - each one marked "keep in sync" in its own +# comments. +# +# A separate workflow file rather than a job inside build.yml, so that the weekly schedule can +# run the guard between releases without dragging the 15-minute native build along; build.yml +# still calls it on every pull request and release. + +on: + workflow_call: + +permissions: + contents: read + +jobs: + binding-drift: + name: binding sources match DatadogNet.iOS + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Read the recorded iOS source commit + id: source + run: | + file=build/ios-bindings-source.txt + if [ ! -f "${file}" ]; then + echo "::error::${file} is missing - run build/SyncBindingsFromiOS.sh from a committed DatadogNet.iOS checkout" + exit 1 + fi + ref=$(head -1 "${file}" | cut -d' ' -f1) + if grep -q 'dirty' "${file}"; then + echo "::warning::the last binding sync was taken from an uncommitted DatadogNet.iOS tree, so the drift guard is disarmed. Re-run build/SyncBindingsFromiOS.sh once the iOS changes are committed." + echo "armed=false" >> "$GITHUB_OUTPUT" + else + echo "armed=true" >> "$GITHUB_OUTPUT" + fi + echo "ref=${ref}" >> "$GITHUB_OUTPUT" + + - name: Check out DatadogNet.iOS at the recorded commit + if: steps.source.outputs.armed == 'true' + uses: actions/checkout@v4 + with: + repository: sbokatuk/DatadogNet.iOS + ref: ${{ steps.source.outputs.ref }} + path: .ios-sync + + - name: Re-run the sync and fail on any difference + if: steps.source.outputs.armed == 'true' + run: | + ./build/SyncBindingsFromiOS.sh "${GITHUB_WORKSPACE}/.ios-sync" + if ! git diff --exit-code -- src/; then + echo "::error::binding sources differ from DatadogNet.iOS@${{ steps.source.outputs.ref }}. They are verbatim copies by design - make the change in DatadogNet.iOS, re-run build/SyncBindingsFromiOS.sh, and commit both." + exit 1 + fi + echo "Binding sources are byte-identical to DatadogNet.iOS@${{ steps.source.outputs.ref }}." + + # The bindings arrive by script; these three arrive by hand, and hand-synced copies drift. + # Unlike the bindings they are NOT verbatim: each copy's comments say what is true in its + # own repository, and that divergence is deliberate. So the comparison strips whole-line + # #-comments and blank lines first - and for merge-packages.py the module docstring too, + # since that file's header names the docstring as the one place it may differ. What + # survives the strip is code, and a code difference means the copies have functionally + # drifted: fix it in DatadogNet.iOS and carry it here, or vice versa, but do not let the + # two quietly solve the same problem differently. + # + # Whole-line comments only, on purpose: a '#' can legitimately appear inside code, and + # stripping trailing fragments risks eating real differences. Both sides pass through the + # same strip, so anything cosmetic disappears symmetrically. + - name: Compare the hand-synced tooling against the same commit + if: steps.source.outputs.armed == 'true' + run: | + strip() { + case "$1" in + *.py) + # Drop everything through the docstring's closing '"""' (the shebang goes with + # it - a #-line anyway), then comments and blanks. + awk 'f { print } /^"""$/ { f = 1 }' "$1" | grep -vE '^[[:space:]]*#' | grep -vE '^[[:space:]]*$' + ;; + *) + grep -vE '^[[:space:]]*#' "$1" | grep -vE '^[[:space:]]*$' + ;; + esac + } + + failed=0 + for file in \ + build/merge-packages.py \ + .github/actions/select-xcode/action.yml \ + build/CheckReadmeVersions.sh + do + theirs="${GITHUB_WORKSPACE}/.ios-sync/${file}" + if [ ! -f "${theirs}" ]; then + echo "::error::${file} no longer exists in DatadogNet.iOS@${{ steps.source.outputs.ref }} - the copy here has lost its upstream; decide whether it is now owned here and update this manifest" + failed=1 + continue + fi + if ! diff -u \ + --label "DatadogNet.iOS/${file}" --label "DatadogNet.Mac/${file}" \ + <(strip "${theirs}") <(strip "${file}"); then + echo "::error::${file} has functionally drifted from DatadogNet.iOS@${{ steps.source.outputs.ref }} (compared with comments stripped). The two are hand-synced copies - land the change in both repositories." + failed=1 + fi + done + + if [ "${failed}" -ne 0 ]; then + exit 1 + fi + echo "Hand-synced tooling matches DatadogNet.iOS@${{ steps.source.outputs.ref }} (comments aside)." diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4c71e4..ae331ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,8 +8,8 @@ name: build # Two structural differences from DatadogNet.iOS's pipeline: # # * The xcframeworks are compiled here (BuildXcFrameworks.sh), not downloaded - Datadog -# publishes no Mac Catalyst binaries. The ~15 minute build is cached on the native versions -# and the build script's own hash. +# publishes no Mac Catalyst binaries. The ~15 minute build is cached on the native versions, +# the resolved Xcode and the build script's own hash. # # * There is no simulator smoke-test job. Catalyst apps run directly on macOS, and launching a # GUI app on a headless runner is flakier than the coverage is worth; building the sample @@ -51,6 +51,20 @@ jobs: - name: Select Xcode uses: ./.github/actions/select-xcode + # Asked of the selected Xcode itself, in this workflow rather than inside the select-xcode + # action: the action is a hand-synced copy of DatadogNet.iOS's, guarded by the tooling half + # of the binding-drift job, so it stays verbatim and this repository's extra need - a cache + # key ingredient - lives here. `xcodebuild -version` reflects the xcode-select the action + # just performed, and the answer ("26.0.1-17A400") moves whenever the resolved Xcode does, + # whatever moved it: an action config change, or the runner image swapping one patch + # release for another inside the same pinned line. + - name: Resolve the selected Xcode version + id: xcode + run: | + xcode=$(xcodebuild -version | awk 'NR == 1 { version = $2 } NR == 2 { build = $NF } END { printf "%s-%s", version, build }') + echo "version=${xcode}" >> "$GITHUB_OUTPUT" + echo "Selected Xcode ${xcode}" + - name: Set up .NET uses: actions/setup-dotnet@v4 with: @@ -98,9 +112,12 @@ jobs: path: libs # The native versions are part of the key: without them a build for a different line # would restore the previous line's frameworks and bind the wrong thing. The script hash - # is too, so a change to how the frameworks are built invalidates the cache. Unlike a - # download, the compiled output also varies with Xcode, which select-xcode pins. - key: datadog-catalyst-xcframeworks-${{ steps.native.outputs.version }}-${{ steps.native.outputs.otel }}-${{ hashFiles('build/BuildXcFrameworks.sh') }} + # is too, so a change to how the frameworks are built invalidates the cache. And unlike + # a download, the compiled output also varies with Xcode - so the *resolved* Xcode + # version is in the key as well. select-xcode pinning the SDK line is not enough: the + # cache would survive a select-xcode config change, and the runner image bumping the + # patch release within the pinned line, both of which change the compiler. + key: datadog-catalyst-xcframeworks-${{ steps.native.outputs.version }}-${{ steps.native.outputs.otel }}-xcode-${{ steps.xcode.outputs.version }}-${{ hashFiles('build/BuildXcFrameworks.sh') }} - name: Build Catalyst xcframeworks from source if: steps.xcframeworks.outputs.cache-hit != 'true' @@ -109,6 +126,16 @@ jobs: # The dSYMs are the only symbolication data these binaries will ever have - Datadog does not # publish Catalyst builds, so nobody else holds them. They live in libs/dsyms (cached with # the frameworks, never packed) and the release workflow attaches them to the GitHub release. + # + # BUILD-INFO.txt rides along: it records which Xcode and SDK compiled these exact binaries, + # which is the first thing anyone rebuilding-to-compare needs - and the dSYM artifact is the + # only build output a human ever downloads, so a record that stays behind in libs/ dies with + # the runner. Copied in rather than listed as a second upload path, because a multi-path + # upload would re-root the artifact at libs/ and nest the dSYMs a level deeper than the + # release zip step expects. + - name: Include the build record with the dSYMs + run: cp libs/BUILD-INFO.txt libs/dsyms/ + - name: Upload dSYMs uses: actions/upload-artifact@v4 with: @@ -143,10 +170,14 @@ jobs: retention-days: 7 sample: - name: build sample app + name: build sample app (${{ matrix.configuration }}) timeout-minutes: 30 needs: pack runs-on: macos-15 + strategy: + fail-fast: false + matrix: + configuration: [Debug, Release] steps: - uses: actions/checkout@v4 @@ -171,58 +202,23 @@ jobs: # that checks the API is still usable the way the README documents - including the # convenience layer, which the generated binding knows nothing about. # - # Debug, and unsigned, because this is a compile-and-link check and nothing here ships. + # Both configurations, because they exercise different toolchains: Debug is the everyday + # compile-and-link check, and Release is what consumers actually ship - the ILLink/AOT + # behaviour Release turns on has broken binding consumers that built fine in Debug. The + # Release leg switches codesigning and packaging off: a runner has no signing identity, + # nothing here ships, and the check is that Release *links*, not that it notarises. - name: Build the sample against the packed packages run: | dotnet build samples/DatadogNet.Mac.Example/DatadogNetExample.csproj \ - --configuration Debug \ + --configuration ${{ matrix.configuration }} \ -p:RuntimeIdentifier=maccatalyst-arm64 \ + ${{ matrix.configuration == 'Release' && '-p:EnableCodeSigning=false -p:CreatePackage=false' || '' }} \ -p:DatadogPackageVersion="${{ inputs.version }}" - # The whole model of this repository rests on the binding sources being verbatim copies of - # DatadogNet.iOS's - the Catalyst head of the façade compiles against them on that assumption. - # This job turns "do not edit the copies here" from prose into a failing check: it checks out - # DatadogNet.iOS at the commit build/ios-bindings-source.txt records (written by the sync - # script), re-runs the sync, and fails on any difference. A sync recorded from an uncommitted - # iOS tree disarms the guard with a warning until a clean sync replaces it. + # The binding sources and a handful of tooling files are hand-synced copies of + # DatadogNet.iOS's; this guard fails when they drift. Factored into its own workflow file + # (which documents exactly what it checks and why) so the weekly schedule can also run it + # between releases, when nothing here changes but the iOS repository moves. binding-drift: - name: binding sources match DatadogNet.iOS - timeout-minutes: 10 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Read the recorded iOS source commit - id: source - run: | - file=build/ios-bindings-source.txt - if [ ! -f "${file}" ]; then - echo "::error::${file} is missing - run build/SyncBindingsFromiOS.sh from a committed DatadogNet.iOS checkout" - exit 1 - fi - ref=$(head -1 "${file}" | cut -d' ' -f1) - if grep -q 'dirty' "${file}"; then - echo "::warning::the last binding sync was taken from an uncommitted DatadogNet.iOS tree, so the drift guard is disarmed. Re-run build/SyncBindingsFromiOS.sh once the iOS changes are committed." - echo "armed=false" >> "$GITHUB_OUTPUT" - else - echo "armed=true" >> "$GITHUB_OUTPUT" - fi - echo "ref=${ref}" >> "$GITHUB_OUTPUT" - - - name: Check out DatadogNet.iOS at the recorded commit - if: steps.source.outputs.armed == 'true' - uses: actions/checkout@v4 - with: - repository: sbokatuk/DatadogNet.iOS - ref: ${{ steps.source.outputs.ref }} - path: .ios-sync - - - name: Re-run the sync and fail on any difference - if: steps.source.outputs.armed == 'true' - run: | - ./build/SyncBindingsFromiOS.sh "${GITHUB_WORKSPACE}/.ios-sync" - if ! git diff --exit-code -- src/; then - echo "::error::binding sources differ from DatadogNet.iOS@${{ steps.source.outputs.ref }}. They are verbatim copies by design - make the change in DatadogNet.iOS, re-run build/SyncBindingsFromiOS.sh, and commit both." - exit 1 - fi - echo "Binding sources are byte-identical to DatadogNet.iOS@${{ steps.source.outputs.ref }}." + name: binding drift + uses: ./.github/workflows/binding-drift.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 372cdbd..ffe16f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,6 +46,34 @@ jobs: echo "prerelease=${prerelease}" >> "$GITHUB_OUTPUT" echo "Releasing ${version} (prerelease=${prerelease})" >> "$GITHUB_STEP_SUMMARY" + # The tag deciding everything is also the tag's trap: a mistyped v3.15.0.1 pushed while the + # branch still pins 3.14.0 would build and publish a 3.15.0 release the branch was never + # prepared for - unsynced bindings, README and release notes claiming another version. + # Releasing a line the branch does not pin IS legitimate (that flexibility is the point of + # deriving from the tag), so the mismatch is a hard stop with an explicit, narrow override + # rather than a forbidden state: set the repository variable DATADOG_RELEASE_LINE_OVERRIDE + # (Settings > Secrets and variables > Actions > Variables) to the exact native version + # being released, e.g. 3.15.0, and re-run this workflow. The variable blesses that one + # line only - the next typo still fails - and should be removed once the release is done. + - name: Check the tag against Directory.Build.props + run: | + native="${{ steps.resolve.outputs.native-version }}" + props=$(sed -n 's:.*\(.*\).*:\1:p' Directory.Build.props | head -1) + if [ -z "${props}" ]; then + echo "::error::could not read DatadogNativeVersion from Directory.Build.props" + exit 1 + fi + if [ "${native}" = "${props}" ]; then + echo "Tag ${GITHUB_REF_NAME} matches DatadogNativeVersion ${props}." + exit 0 + fi + if [ "${{ vars.DATADOG_RELEASE_LINE_OVERRIDE }}" = "${native}" ]; then + echo "::warning::releasing dd-sdk-ios ${native} from a branch pinned to ${props}, allowed by DATADOG_RELEASE_LINE_OVERRIDE. Remove the variable once this release is done." + exit 0 + fi + echo "::error::tag ${GITHUB_REF_NAME} would release dd-sdk-ios ${native}, but Directory.Build.props pins ${props} - this is what a mistyped tag looks like. If the different line is deliberate, set the repository variable DATADOG_RELEASE_LINE_OVERRIDE to ${native} and re-run this workflow." + exit 1 + build: name: build needs: version @@ -63,9 +91,11 @@ jobs: permissions: # contents: write creates the GitHub release; id-token: write lets the job request the OIDC # token that nuget.org exchanges for a short-lived API key. Without the latter the token - # request fails silently and the login step gets no key. + # request fails silently and the login step gets no key. attestations: write lets the + # provenance step store the signed attestation on this repository. contents: write id-token: write + attestations: write env: VERSION: ${{ needs.version.outputs.version }} steps: @@ -91,6 +121,33 @@ jobs: name: dsyms path: dsyms + # Zipped here, before anything is published, so the attestation below can cover the zip - + # the release itself is created last, and attaches this exact file. + - name: Package dSYMs for the release + run: | + # The build workflow puts BUILD-INFO.txt in with the dSYMs, so zipping the directory + # wholesale ships the build record too. Asserted rather than assumed: a zip without it + # strands the question the file exists to answer - which Xcode built the binaries these + # dSYMs symbolicate. + if [ ! -f dsyms/BUILD-INFO.txt ]; then + echo "::error::the dsyms artifact carries no BUILD-INFO.txt - the build workflow stopped including the build record" + exit 1 + fi + (cd dsyms && zip -qry "../dsyms-${VERSION}.zip" .) + + # A signed, publicly verifiable statement binding each artifact to this exact workflow run + # - repository, commit, workflow file - checkable with `gh attestation verify`. It is the + # machine-checkable half of "built by this repository's CI from this tag"; the README's + # "Verifying a build" section is the honest whole, including what an attestation does not + # prove. Deliberately before the nuget.org push: if attesting fails, nothing has been + # published anywhere yet, and a re-run starts clean. + - name: Attest build provenance + uses: actions/attest-build-provenance@v4 + with: + subject-path: | + artifacts/*.nupkg + dsyms-${{ env.VERSION }}.zip + # nuget.org is published to first: a GitHub release that links to packages which failed to # upload would be worse than a release created a moment late. # Deliberately immediately before the push: the issued key is valid for one hour, and each @@ -148,7 +205,8 @@ jobs: echo echo "\`dsyms-${VERSION}.zip\` below carries the dSYMs for these exact binaries - Datadog publishes" echo "no Catalyst builds, so this release is the only place they exist. Upload them to Datadog for" - echo "crash symbolication." + echo "crash symbolication. The zip also carries \`BUILD-INFO.txt\`: the Xcode, SDK and timestamp the" + echo "binaries were built with, for anyone rebuilding at this tag to compare." echo echo "> The first three components of \`${VERSION}\` are the dd-sdk-ios version; the fourth is this" echo "> repository's binding revision, which advances when the bindings or packaging change while" @@ -188,9 +246,6 @@ jobs: cat release-notes.md >> "$GITHUB_STEP_SUMMARY" - - name: Package dSYMs for the release - run: (cd dsyms && zip -qry "../dsyms-${VERSION}.zip" .) - - name: Create GitHub release env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/weekly-drift.yml b/.github/workflows/weekly-drift.yml new file mode 100644 index 0000000..151f57f --- /dev/null +++ b/.github/workflows/weekly-drift.yml @@ -0,0 +1,110 @@ +name: weekly drift + +# The drift guards otherwise run only when something *here* changes - pull requests and release +# tags - but what they guard moves on its own: DatadogNet.iOS keeps advancing after +# build/ios-bindings-source.txt pins a commit, and nothing in a quiet Mac repository would ever +# notice. This schedule closes both gaps between releases: +# +# * re-runs the binding/tooling drift guard (the same reusable workflow build.yml calls), so +# an upstream force-push, a botched merge here, or a tooling edit that skipped one side +# surfaces within a week instead of at the next release; +# +# * checks whether DatadogNet.iOS's src/ has moved past the pinned commit, and opens one issue +# (not one per week) listing the changed files when it has - the cue that a re-sync and a +# binding-revision release are probably due. + +on: + schedule: + # Mondays, 06:23 UTC. Off the whole hour deliberately: GitHub sheds exactly-on-the-hour cron + # load first, and a guard that silently never runs is worse than no guard. + - cron: '23 6 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + binding-drift: + name: binding drift + uses: ./.github/workflows/binding-drift.yml + + ios-staleness: + name: iOS src/ ahead of the pin? + timeout-minutes: 10 + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@v4 + + # No disarming on a dirty pin here, unlike the drift guard: even a sync taken from a dirty + # tree names the commit that tree sat on, and "has src/ moved past that commit" is exactly + # the staleness question either way. A pin naming a commit GitHub has never seen fails the + # checkout below, loudly - which is itself worth knowing. + - name: Read the recorded iOS source commit + id: source + run: | + file=build/ios-bindings-source.txt + if [ ! -f "${file}" ]; then + echo "::error::${file} is missing - run build/SyncBindingsFromiOS.sh from a committed DatadogNet.iOS checkout" + exit 1 + fi + echo "ref=$(head -1 "${file}" | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + + - name: Check out DatadogNet.iOS at the recorded commit + uses: actions/checkout@v4 + with: + repository: sbokatuk/DatadogNet.iOS + ref: ${{ steps.source.outputs.ref }} + path: .ios-sync + + - name: Compare the pin against iOS main and open the issue once + env: + GH_TOKEN: ${{ github.token }} + PIN: ${{ steps.source.outputs.ref }} + run: | + # Shallow on both sides on purpose: the diff needs the two trees, not the history + # between them, and the pin may be hundreds of commits behind by the time this fires. + git -C .ios-sync fetch --quiet --depth=1 origin main + tip=$(git -C .ios-sync rev-parse origin/main) + + # Only src/ matters: that is what SyncBindingsFromiOS.sh copies. iOS README or CI + # churn is none of this repository's business. + changed=$(git -C .ios-sync diff --name-only "${PIN}" origin/main -- src/) + if [ -z "${changed}" ]; then + echo "DatadogNet.iOS src/ is unchanged since the pinned ${PIN}." + exit 0 + fi + + echo "DatadogNet.iOS src/ has moved past ${PIN}:" + echo "${changed}" + + # One standing issue, not one per week: matched by exact title among open issues, so + # closing it after a re-sync re-arms the alert and nothing ever piles up. + title='iOS bindings moved past the Mac pin' + open=$(gh issue list --repo "${GITHUB_REPOSITORY}" --state open --limit 100 \ + --json title --jq "[.[] | select(.title == \"${title}\")] | length") + if [ "${open}" -gt 0 ]; then + echo "An open issue already tracks this; not filing another." + exit 0 + fi + + { + echo "DatadogNet.iOS's \`src/\` has changed since the commit this repository's bindings were last synced from, so the two platforms' binding surfaces may no longer match." + echo + echo "- pinned (\`build/ios-bindings-source.txt\`): \`${PIN}\`" + echo "- \`DatadogNet.iOS\` main at the time of this run: \`${tip}\`" + echo + echo "Changed under \`src/\`:" + echo + echo '```' + echo "${changed}" + echo '```' + echo + echo "If the changes are binding-relevant, run \`./build/SyncBindingsFromiOS.sh\` against an up-to-date DatadogNet.iOS checkout, review the diff, and ship a binding-revision release. Close this issue once the pin has moved (or the changes are judged irrelevant); the weekly workflow opens it again only while none is open." + echo + echo "_Opened by [.github/workflows/weekly-drift.yml](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/main/.github/workflows/weekly-drift.yml); it lists the files changed as of the run that filed it and is not updated afterwards._" + } > /tmp/issue-body.md + + gh issue create --repo "${GITHUB_REPOSITORY}" --title "${title}" --body-file /tmp/issue-body.md diff --git a/Directory.Build.props b/Directory.Build.props index 6888024..1c8be77 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,8 +5,8 @@ Package versions are ., e.g. 3.14.0.1 - the same scheme DatadogNet.iOS uses, deliberately, because both repositories bind the same native SDK: Mac Catalyst is a variant of the iOS platform, and dd-sdk-ios is the SDK that runs - there. A given DatadogNet..MacCatalyst version therefore wraps exactly the same - native release as the DatadogNet..iOS package of the same version. + there. A given DatadogNet..Mac version therefore wraps exactly the same native + release as the DatadogNet..iOS package of the same version. The fourth component belongs to this repository and increments whenever the bindings or packaging change while the native version stays put. It is NOT kept in lock-step with the @@ -18,7 +18,7 @@ script needs Xcode rather than just curl. --> 3.14.0 - 2 + 3 $(DatadogNativeVersion).$(DatadogBindingRevision) 2.5.0 diff --git a/README.md b/README.md index 839fcd0..d2c1296 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ only the package ids differ. - [Building locally](#building-locally) - [Upgrading the Datadog SDK](#upgrading-the-datadog-sdk) - [Releasing](#releasing) +- [Verifying a build](#verifying-a-build) - [Licence](#licence) ## Packages @@ -74,8 +75,8 @@ bindings: ```xml - - + + ``` @@ -154,13 +155,16 @@ for the sample. ## Upgrading the Datadog SDK -1. Bump `DatadogNativeVersion` in [Directory.Build.props](Directory.Build.props), reset - `DatadogBindingRevision` to 1. -2. Update `DatadogOtelVersion` to whatever the new tag's `Cartfile.resolved` pins. -3. Wait for (or produce) the matching DatadogNet.iOS release, then run +1. `./build/BumpNativeVersion.sh ` - one command for every pin: + `DatadogNativeVersion` (revision reset to 1), `DatadogOtelVersion` (read from the new tag's + `Cartfile.resolved` on GitHub), the README's package pins, and a scaffolded + `docs/release-notes/.md`. It refuses versions whose tag does not exist yet, and + prints the rest of this list when it is done. +2. Wait for (or produce) the matching DatadogNet.iOS release, then run `./build/SyncBindingsFromiOS.sh` against it and review the diff. -4. `./build/BuildXcFrameworks.sh && ./build/BuildNugets.sh && dotnet test tests/DatadogNet.Mac.PackageTests` -5. Update the README table and `docs/release-notes/`. +3. `./build/BuildXcFrameworks.sh && ./build/BuildNugets.sh && dotnet test tests/DatadogNet.Mac.PackageTests` +4. Update the README package table if the feature set moved, and finish the scaffolded release + notes - they ship verbatim as every package's `PackageReleaseNotes`. ## Releasing @@ -169,6 +173,41 @@ builds the xcframeworks, packs, validates, publishes to nuget.org via trusted pu creates a GitHub release; a curated `docs/release-notes/.md` replaces the generated commit list when present. Pull requests publish `-beta..` prereleases the same way. +## Verifying a build + +"Built from source in CI" is a claim worth being able to check, so every release attests what it +publishes: the `.nupkg` files and the `dsyms-.zip` carry +[build provenance attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations) +— a signed, public statement that these exact bytes came out of this repository's release +workflow, at a named tag and commit, on GitHub's runners. Verify the file you actually +downloaded with the `gh` CLI: + +```sh +gh attestation verify DatadogNet.Core.Mac..nupkg --repo sbokatuk/DatadogNet.Mac +``` + +That proves *where the bytes were built* — not on someone's laptop, not swapped after the run — +and names the commit they were built from. It does not prove the source does what it says; for +that, the repository is small enough to read. + +The stronger check is rebuilding it yourself. Byte-identity is not achievable — as +[build/BuildXcFrameworks.sh](build/BuildXcFrameworks.sh)'s header says, the output varies with +the Xcode that compiled it, and Mach-O embeds fresh UUIDs regardless — but the exported surface +is stable and comparable: + +1. Read `BUILD-INFO.txt` from the release's `dsyms-.zip`: it records the Xcode and SDK + the release binaries were compiled with. +2. Check out the release tag, install that Xcode, run `./build/BuildXcFrameworks.sh`. +3. Compare exported symbols per framework, your build against the one inside the shipped package + (the payload is `lib//.resources.zip` inside the `.nupkg`): + + ```sh + nm -gU .framework/ | awk '{print $3}' | sort + ``` + +Identical symbol lists from the pinned source, at the tag, under the recorded Xcode, is the +strongest reproducibility statement an Xcode toolchain leaves available. + ## Licence The binding code in this repository is [MIT](LICENSE). The native binaries the packages embed are diff --git a/build/BuildXcFrameworks.sh b/build/BuildXcFrameworks.sh index 7778082..26f88cd 100755 --- a/build/BuildXcFrameworks.sh +++ b/build/BuildXcFrameworks.sh @@ -197,6 +197,105 @@ print(" patched SUPPORTS_MACCATALYST on %d target(s), %d singular and %d list open(path, "w").write(text) EOF +# --------------------------------------------------------------------------------------------- +# Post-build guards. The patches above assert they *applied*; these assert they *worked*. A +# stale patch does not always kill the archive - a target that silently loses a dependency or a +# build setting can still produce a binary, just a gutted one: fewer classes, no Objective-C +# surface, nothing for the bindings to bind. That failure would otherwise surface as a +# MissingMethodException in a consuming app, so each xcframework is checked the moment it is +# created: `nm -gU` on both architecture slices must show a canonical exported symbol. +# +# The table pins one or two load-bearing exports per framework - the Objective-C class behind +# the package's main entry point wherever the framework exports Objective-C at all. +# DatadogFlags and DatadogProfiling export no ObjC classes (no Objective-C API upstream yet), +# and OpenTelemetryApi is pure Swift; for those the anchor is the Swift nominal type descriptor +# of the module's entry type instead (mangled names verified with `xcrun swift-demangle`): +# +# _$s12DatadogFlags0B0OMn nominal type descriptor for DatadogFlags.Flags +# _$s16DatadogProfiling0B0OMn nominal type descriptor for DatadogProfiling.Profiling +# _$s16OpenTelemetryApi0aB0VMn nominal type descriptor for OpenTelemetryApi.OpenTelemetry +# --------------------------------------------------------------------------------------------- + +typeset -A CANONICAL_EXPORTS +CANONICAL_EXPORTS=( + DatadogInternal '_OBJC_CLASS_$_DDInternalLogger' + DatadogCore '_OBJC_CLASS_$_DDDatadog _OBJC_CLASS_$_DDConfiguration' + DatadogLogs '_OBJC_CLASS_$_DDLogs _OBJC_CLASS_$_DDLogger' + DatadogTrace '_OBJC_CLASS_$_DDTrace _OBJC_CLASS_$_DDTracer' + DatadogRUM '_OBJC_CLASS_$_DDRUM _OBJC_CLASS_$_DDRUMMonitor' + DatadogCrashReporting '_OBJC_CLASS_$_DDCrashReporter' + DatadogWebViewTracking '_OBJC_CLASS_$_DDWebViewTracking' + DatadogSessionReplay '_OBJC_CLASS_$_DDSessionReplay _OBJC_CLASS_$_DDSessionReplayConfiguration' + DatadogFlags '_$s12DatadogFlags0B0OMn' + DatadogProfiling '_$s16DatadogProfiling0B0OMn' + OpenTelemetryApi '_$s16OpenTelemetryApi0aB0VMn' +) + +assert_canonical_exports() { + local framework="$1" + local symbols="${CANONICAL_EXPORTS[$framework]:-}" + + if [ -z "$symbols" ]; then + echo "error: no canonical exports are recorded for $framework." >&2 + echo " A new framework needs an entry in CANONICAL_EXPORTS: pick a stable exported" >&2 + echo " symbol with 'nm -gU' on the built binary, so a gutted build of it fails here" >&2 + echo " like the others do." >&2 + exit 1 + fi + + # The path both asserts and documents the one slice this build produces - the exact + # directory name the README and the package tests promise. $framework.framework/$framework + # is the bundle-root symlink into Versions/, which nm follows. + local slice="$LIBS/$framework.xcframework/ios-arm64_x86_64-maccatalyst" + local binary="$slice/$framework.framework/$framework" + if [ ! -f "$binary" ]; then + echo "error: $framework.xcframework has no ios-arm64_x86_64-maccatalyst slice ($binary)." >&2 + echo " That name is a shipped promise - the bindings and the package tests expect" >&2 + echo " exactly it - so if -create-xcframework started naming the slice differently," >&2 + echo " the change has to be deliberate, here and there together." >&2 + exit 1 + fi + + # Both slices checked separately: a universal binary with one healthy and one gutted + # architecture would pass a single fat-file scan. The export list is captured once per arch + # and grepped from a herestring - `nm | grep -q` would let grep exit before nm finishes + # writing, and under this script's pipefail that SIGPIPE turns a *found* symbol into a + # failure on the larger binaries. + local arch symbol exports + for arch in arm64 x86_64; do + exports=$(nm -gU -arch "$arch" "$binary" | awk '{print $3}') + for symbol in ${=symbols}; do + if ! grep -qxF "$symbol" <<< "$exports"; then + echo "error: $framework ($arch) no longer exports $symbol." >&2 + echo " The archive succeeded but produced a gutted module - the usual cause" >&2 + echo " is a pbxproj patch above matching on text but no longer doing what it" >&2 + echo " did (a dropped dependency or build setting still archives). If" >&2 + echo " upstream genuinely renamed or removed the symbol, update" >&2 + echo " CANONICAL_EXPORTS - after checking the bindings survive the change." >&2 + exit 1 + fi + done + done + echo " canonical exports present in both slices: $symbols" +} + +# Strict on purpose, where this used to be a silent `2>/dev/null || true`: these dSYMs are the +# only symbolication data the binaries will ever have - Datadog publishes no Catalyst builds, so +# no one else holds them - and a build that quietly drops one produces a release whose crashes +# can never be symbolicated. An archive without a dSYM means the archive settings changed +# (DEBUG_INFORMATION_FORMAT, most likely); that is a build failure, not a shrug. +copy_dsym() { + local dsym="$1" + if [ ! -d "$dsym" ]; then + echo "error: the archive produced no dSYM at $dsym." >&2 + echo " The release attaches these for crash symbolication and they exist nowhere" >&2 + echo " else. Fix whatever stopped the archive emitting dSYMs rather than shipping" >&2 + echo " binaries that can never be symbolicated." >&2 + exit 1 + fi + cp -R "$dsym" "$DSYMS/" +} + # --------------------------------------------------------------------------------------------- # OpenTelemetryApi for Mac Catalyst, following upstream's scripts/build.sh for that repository: # archive the SwiftPM library scheme (the product lands in usr/local/lib), then graft the @@ -238,7 +337,8 @@ echo "==> creating OpenTelemetryApi.xcframework" xcodebuild -create-xcframework \ -framework "$OTEL_FRAMEWORK" \ -output "$LIBS/OpenTelemetryApi.xcframework" -cp -R "$ARCHIVES/OpenTelemetryApi/catalyst.xcarchive/dSYMs/OpenTelemetryApi.framework.dSYM" "$DSYMS/" 2>/dev/null || true +assert_canonical_exports OpenTelemetryApi +copy_dsym "$ARCHIVES/OpenTelemetryApi/catalyst.xcarchive/dSYMs/OpenTelemetryApi.framework.dSYM" # The Datadog project links OpenTelemetryApi as the Carthage binary at this exact path; give it # one that actually has a Catalyst slice. @@ -286,9 +386,28 @@ for scheme in ${=SCHEMES}; do xcodebuild -create-xcframework \ -framework "$fwk" \ -output "$LIBS/$scheme.xcframework" - cp -R "$archive.xcarchive/dSYMs/$scheme.framework.dSYM" "$DSYMS/" 2>/dev/null || true + assert_canonical_exports "$scheme" + copy_dsym "$archive.xcarchive/dSYMs/$scheme.framework.dSYM" done +# --------------------------------------------------------------------------------------------- +# One dSYM per framework, counted before anything gets recorded as done. copy_dsym already fails +# on a missing bundle, so this is the structural complement: it catches the copies and the +# scheme list drifting apart - a framework added to SCHEMES whose dSYM path changed shape, or a +# stray extra bundle from an earlier layout - rather than any single copy going missing. +# --------------------------------------------------------------------------------------------- + +expected_dsyms=$(( $(echo "$SCHEMES" | wc -w) + 1 )) # every scheme, plus OpenTelemetryApi +actual_dsyms=$(ls -d "$DSYMS"/*.dSYM 2>/dev/null | wc -l | tr -d ' ') +if [ "$actual_dsyms" -ne "$expected_dsyms" ]; then + echo "error: expected $expected_dsyms dSYMs in $DSYMS, found $actual_dsyms:" >&2 + ls "$DSYMS" >&2 + echo " The release attaches exactly one dSYM per shipped framework; a mismatch means" >&2 + echo " the scheme list and the dSYM export in this script have drifted apart." >&2 + exit 1 +fi +echo "==> all $actual_dsyms dSYMs exported to $DSYMS" + # --------------------------------------------------------------------------------------------- # Record what was built with what. The binding packages' version already pins the Datadog # version; this file is for the human diffing two builds that claim the same version. diff --git a/build/BumpNativeVersion.sh b/build/BumpNativeVersion.sh new file mode 100755 index 0000000..fb3502e --- /dev/null +++ b/build/BumpNativeVersion.sh @@ -0,0 +1,184 @@ +#!/bin/sh +set -eu + +# Starts a Datadog SDK upgrade by rewriting every place this repository pins the native version, +# then prints the steps a script cannot do. The pins are more numerous than they look - the two +# version properties, the OTEL companion pin, the README's copy-paste install snippets, the +# release-notes file the packages embed - and every one of them has been forgotten by hand at +# least once somewhere across the DatadogNet repositories. One command, one consistent tree. +# +# Usage: +# ./BumpNativeVersion.sh 3.15.0 # the dd-sdk-ios tag being moved to +# +# What it does: +# * Directory.Build.props: DatadogNativeVersion to the new version, DatadogBindingRevision +# back to 1 (first release on a new native line), DatadogOtelVersion to whatever the new +# dd-sdk-ios tag pins in its Cartfile.resolved - fetched from the tag on GitHub, the same +# file BuildXcFrameworks.sh cross-checks against the actual checkout at build time. +# * README.md: every pinned package version the install snippets carry (the same pins +# CheckReadmeVersions.sh guards - and that check is run at the end, so this script leaves +# the tree passing it). +# * docs/release-notes/.1.md: scaffolded if absent. Mind the TODOs in it: the +# file ships verbatim as PackageReleaseNotes in every package and as the GitHub release +# body. +# +# What it deliberately does not do: sync the bindings (the matching DatadogNet.iOS release may +# not exist yet), build, test, or tag. Those remain manual and are printed at the end, in order. + +cd "$(dirname "$0")" + +ROOT="$(cd .. && pwd)" +PROPS="$ROOT/Directory.Build.props" +README="$ROOT/README.md" + +NEW="${1:-}" +if [ -z "$NEW" ]; then + echo "usage: $0 e.g. $0 3.15.0" >&2 + exit 2 +fi +case "$NEW" in + *[!0-9.]* | *.*.*.* | .* | *. ) + echo "error: '$NEW' does not look like a dd-sdk-ios version (expected e.g. 3.15.0" >&2 + echo " - three numeric components; the fourth, the binding revision, is this" >&2 + echo " repository's and resets to 1)" >&2 + exit 2 + ;; + *.*.* ) ;; + * ) + echo "error: '$NEW' does not look like a dd-sdk-ios version (expected e.g. 3.15.0)" >&2 + exit 2 + ;; +esac + +prop() { + sed -n "s/.*<$1>\(.*\)<\/$1>.*/\1/p" "$PROPS" | head -1 +} + +OLD_NATIVE="$(prop DatadogNativeVersion)" +OLD_REVISION="$(prop DatadogBindingRevision)" +OLD_OTEL="$(prop DatadogOtelVersion)" +if [ -z "$OLD_NATIVE" ] || [ -z "$OLD_REVISION" ] || [ -z "$OLD_OTEL" ]; then + echo "error: could not read the version properties from $PROPS" >&2 + exit 1 +fi + +# Same-version "bumps" are refused rather than absorbed: re-running this against the current +# native version would silently reset the binding revision to 1, and if $OLD_NATIVE.1 has +# shipped, that is a version that can never be published again. Revision bumps (binding or +# packaging changes on the same native line) are a one-property edit, not an upgrade. +if [ "$NEW" = "$OLD_NATIVE" ]; then + echo "error: this repository already pins dd-sdk-ios $OLD_NATIVE (currently at binding" >&2 + echo " revision $OLD_REVISION). To release again on the same native line, bump" >&2 + echo " DatadogBindingRevision in Directory.Build.props by hand instead." >&2 + exit 1 +fi + +VERSION="$NEW.1" + +# --------------------------------------------------------------------------------------------- +# The OTEL companion pin, read from the new tag before anything is rewritten - if the tag does +# not exist yet (upstream not released, or a typo), the tree stays untouched. The parse mirrors +# the cross-check in BuildXcFrameworks.sh, which re-verifies the same line against the actual +# source checkout at build time; this is the early copy of that late check. +# --------------------------------------------------------------------------------------------- + +CARTFILE_URL="https://raw.githubusercontent.com/DataDog/dd-sdk-ios/$NEW/Cartfile.resolved" +echo "==> reading the OpenTelemetryApi pin from dd-sdk-ios $NEW's Cartfile.resolved" +if ! cartfile="$(curl -fsSL "$CARTFILE_URL")"; then + echo "error: could not fetch $CARTFILE_URL." >&2 + echo " Does the dd-sdk-ios tag '$NEW' exist yet? Nothing has been changed." >&2 + exit 1 +fi + +OTEL="$(printf '%s\n' "$cartfile" | grep -i 'opentelemetry' | grep -oE '"[0-9][A-Za-z0-9._-]*"' | tail -1 | tr -d '"')" +if [ -z "$OTEL" ]; then + echo "error: dd-sdk-ios $NEW's Cartfile.resolved no longer names an OpenTelemetryApi" >&2 + echo " version. Find where the new tag pins it, set DatadogOtelVersion by hand, and" >&2 + echo " update this script and the matching cross-check in BuildXcFrameworks.sh." >&2 + exit 1 +fi + +# --------------------------------------------------------------------------------------------- +# Rewrite the pins. sed into a temporary file and move as two separate statements, for two +# reasons: the originals are never half-written, and set -e actually catches a failing sed - +# in `sed ... && mv ...` a left-hand failure is exempt from errexit by POSIX rule, and the +# script would carry on with the file unmodified. +# --------------------------------------------------------------------------------------------- + +echo "==> Directory.Build.props: $OLD_NATIVE.$OLD_REVISION -> $VERSION, OpenTelemetryApi $OLD_OTEL -> $OTEL" +sed \ + -e "s|[^<]*|$NEW|" \ + -e "s|[^<]*|1|" \ + -e "s|[^<]*|$OTEL|" \ + "$PROPS" > "$PROPS.tmp" +mv "$PROPS.tmp" "$PROPS" + +# The same two shapes CheckReadmeVersions.sh greps for: package pins in install snippets, and +# any device-check invocation. Prose describing the version *scheme* stays put, exactly as that +# check deliberately ignores it. (@ as the delimiter on the second expression: its pattern needs +# a literal ERE alternation |, which cannot also be the delimiter.) +echo "==> README.md: pinned package versions -> $VERSION" +sed -E \ + -e "s|(Include=\"DatadogNet[^\"]*\" +Version=\")[0-9][^\"]*(\")|\\1$VERSION\\2|g" \ + -e "s@(run-(simulator|emulator)-tests\.sh +)[0-9][0-9.]*@\\1$VERSION@g" \ + "$README" > "$README.tmp" +mv "$README.tmp" "$README" + +# --------------------------------------------------------------------------------------------- +# Release-notes scaffold. Only ever created, never overwritten: a re-run must not clobber +# half-written notes. +# --------------------------------------------------------------------------------------------- + +NOTES="$ROOT/docs/release-notes/$VERSION.md" +if [ -f "$NOTES" ]; then + echo "==> docs/release-notes/$VERSION.md already exists; leaving it alone" +else + echo "==> scaffolding docs/release-notes/$VERSION.md" + mkdir -p "$ROOT/docs/release-notes" + cat > "$NOTES" < + +First release on [dd-sdk-ios $NEW](https://github.com/DataDog/dd-sdk-ios/releases/tag/$NEW), +built from source for Mac Catalyst. Package ids, namespaces and the version scheme are +unchanged; the fourth component is this repository's binding revision, starting again at 1 on +the new native line. + +## What's new upstream + +TODO: the dd-sdk-ios $NEW changes that matter to Catalyst consumers, from +https://github.com/DataDog/dd-sdk-ios/releases/tag/$NEW - and whether any of them are +iOS/iPadOS-only the way Session Replay is. + +## Binding changes + +TODO: what the re-sync from DatadogNet.iOS changed in the managed surface, or state that the +API is unchanged. + +## Upgrading from $OLD_NATIVE.x + +TODO: breaking changes and required app-side edits, or state there is nothing to change. +EOF +fi + +# Leaves the tree agreeing with itself - the same check CI runs first. +./CheckReadmeVersions.sh + +cat <.zip` carry +[build provenance attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations): +a signed, public statement that the exact bytes you downloaded came out of this repository's +release workflow, at this tag and commit, on GitHub's runners — checkable with +`gh attestation verify --repo sbokatuk/DatadogNet.Mac`. The README's new +[Verifying a build](https://github.com/sbokatuk/DatadogNet.Mac#verifying-a-build) section says +what that proves, what it does not, and documents the stronger rebuild-and-compare path — for +which `dsyms-.zip` now also carries `BUILD-INFO.txt`, the record of the exact Xcode and +SDK the binaries were compiled with. Previously that record never left CI. + +## dSYMs are guaranteed, not best-effort + +3.14.0.2 started attaching the dSYMs to every release; this release makes them a hard guarantee. +The build now *fails* if any framework's dSYM is missing, and counts one per shipped framework +before recording the build as done — for binaries Datadog does not publish, these are the only +symbolication data that will ever exist, and "the copy silently failed" is no longer a way to +lose them. + +## The binaries are smoke-checked at build time + +Building dd-sdk-ios for Catalyst requires patching upstream's project file, and a stale patch +does not always fail the build — it can archive a gutted module: fewer classes, no Objective-C +surface, a `MissingMethodException` waiting in your app. Every xcframework is now checked with +`nm` the moment it is created: a canonical exported symbol per framework (the class behind each +package's main entry point — `DDDatadog`, `DDRUM`, `DDLogs`, ... — or the Swift entry type for +the three frameworks with no Objective-C surface), asserted on both the arm64 and x86_64 slices. +The package tests likewise now assert the exact slice name the README promises, +`ios-arm64_x86_64-maccatalyst`, rather than a shape that a single-arch slice could satisfy. + +## The SessionReplay description tells the whole truth + +`DatadogNet.SessionReplay.Mac`'s nuget.org description promised session recording with no +caveat; the README and the sample have always said the honest part — upstream's recorder is +iOS/iPadOS-only and records nothing on Catalyst. The description now says it too: the package +ships so shared code that references it compiles and links unchanged. + +## Repository machinery + +None of these change the packages, but they change how likely the packages are to be right: + +- **The drift guards run between releases.** The binding-drift check — bindings are verbatim + copies of DatadogNet.iOS's — used to run only on pull requests and tags. A weekly scheduled + workflow now re-runs it, extends it to the hand-synced tooling files (compared + comment-stripped, so each copy's own documentation stays legal), and opens a single idempotent + issue when DatadogNet.iOS's `src/` moves past the pinned sync commit. +- **A mistyped release tag no longer publishes.** The tag drives which native line is released; + it is now cross-checked against `Directory.Build.props`, with a documented override for + deliberate different-line releases. +- **The xcframework cache keys on the resolved Xcode** (version and build), not just the pinned + SDK line — a runner-image patch bump can no longer serve binaries an older compiler built. +- **Release-configuration coverage.** The sample now also builds in Release against the packed + packages, the configuration consumers actually ship. +- **`build/BumpNativeVersion.sh`** rewrites every pin an SDK upgrade touches in one command — + including reading the matching OpenTelemetryApi version from the new tag's + `Cartfile.resolved` — and prints the manual steps that remain. + +## net8 sunset + +Unchanged policy, restated so it does not persist by inertia: the `net8.0-maccatalyst18.0` head +is dropped in the first release after .NET 8 leaves support on **10 November 2026**, in step +with DatadogNet.iOS. + +## Upgrading from 3.14.0.2 + +Nothing to change: same native SDK, same API. diff --git a/src/DatadogNet.SessionReplay.Mac/DatadogNet.SessionReplay.Mac.csproj b/src/DatadogNet.SessionReplay.Mac/DatadogNet.SessionReplay.Mac.csproj index a641327..e8083d6 100644 --- a/src/DatadogNet.SessionReplay.Mac/DatadogNet.SessionReplay.Mac.csproj +++ b/src/DatadogNet.SessionReplay.Mac/DatadogNet.SessionReplay.Mac.csproj @@ -3,7 +3,13 @@ DatadogSessionReplay SessionReplay - .NET for Mac Catalyst / .NET MAUI bindings for the native Datadog iOS SDK's DatadogSessionReplay framework: records and replays user sessions, with privacy levels that mask text and user input. Built against dd-sdk-ios $(DatadogNativeVersion). Requires RUM to be enabled. + + .NET for Mac Catalyst / .NET MAUI bindings for the native Datadog iOS SDK's DatadogSessionReplay framework: records and replays user sessions, with privacy levels that mask text and user input. Session Replay is iOS/iPadOS-only upstream and records nothing on Mac Catalyst - this package ships so shared code that references it compiles and links unchanged. Built against dd-sdk-ios $(DatadogNativeVersion). Requires RUM to be enabled. datadog-session-replay;session-replay diff --git a/tests/DatadogNet.Mac.PackageTests/PackageLayoutTests.cs b/tests/DatadogNet.Mac.PackageTests/PackageLayoutTests.cs index 4f0395d..f55d72f 100644 --- a/tests/DatadogNet.Mac.PackageTests/PackageLayoutTests.cs +++ b/tests/DatadogNet.Mac.PackageTests/PackageLayoutTests.cs @@ -120,14 +120,13 @@ public void Native_payload_carries_the_maccatalyst_slice_only(string name) var slices = SlicesOf(payload, spec.Framework); - // Exactly one slice, and it is the Catalyst one. These frameworks are built by - // BuildXcFrameworks.sh with a single Catalyst destination, so a second slice appearing - - // or an ios-arm64 device slice replacing the maccatalyst one - means the build script - // changed what it archives, and a net*-maccatalyst consumer would fail to link. + // Exactly one slice, and it is the Catalyst one by exact name. These frameworks are + // built by BuildXcFrameworks.sh with a single Catalyst destination and both Mac + // architectures, so anything else here - a second slice, a device slice, a single-arch + // slice name - means the build script changed what it archives, and some + // net*-maccatalyst consumer would fail to link. var slice = Assert.Single(slices); - Assert.True( - Packages.IsMacCatalystSlice(slice), - $"{spec.Framework}.xcframework carries '{slice}' instead of a maccatalyst slice."); + Assert.Equal(Packages.MacCatalystSlice, slice); } [Theory] diff --git a/tests/DatadogNet.Mac.PackageTests/Packages.cs b/tests/DatadogNet.Mac.PackageTests/Packages.cs index 635c3cb..55f68a3 100644 --- a/tests/DatadogNet.Mac.PackageTests/Packages.cs +++ b/tests/DatadogNet.Mac.PackageTests/Packages.cs @@ -141,18 +141,18 @@ public static XDocument ReadNuspec(ZipArchive package, string name) } /// - /// Whether a slice directory name is the single Mac Catalyst slice the packages are meant to - /// ship. + /// The single slice directory every package ships, by exact name - the one the README + /// promises. /// /// - /// Catalyst slices are named ios-*-maccatalyst - the ios- prefix is what makes - /// an iOS-package slice check need the inverse of this test, and what makes asserting on the - /// maccatalyst suffix rather than the prefix the meaningful check here. What must - /// never appear is a plain iOS, simulator, tvOS, macOS, watchOS or visionOS slice. + /// Exact rather than a shape check (ios-*-maccatalyst): BuildXcFrameworks.sh archives + /// one Catalyst destination with both Mac architectures, so a differently named slice is not + /// a cosmetic variation but a changed build - ios-arm64-maccatalyst would satisfy any + /// prefix/suffix test and still break every Intel consumer. The name doubles as the guard + /// against a plain iOS, simulator, tvOS, macOS, watchOS or visionOS slice appearing, which + /// the looser check existed for. /// - public static bool IsMacCatalystSlice(string slice) => - slice.StartsWith("ios-", StringComparison.Ordinal) && - slice.Contains("maccatalyst", StringComparison.Ordinal); + public const string MacCatalystSlice = "ios-arm64_x86_64-maccatalyst"; } /// What one package is expected to be.