Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/actions/select-xcode/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ runs:
run: |
set -uo pipefail

# The packages advertise net10.0-ios26.0, and .NET for iOS only builds that target
# framework against an Xcode carrying the matching iOS SDK:
# A copy of DatadogNet.iOS's select-xcode action - keep the two in sync. It selects by
# the *iOS* SDK line deliberately even here: Mac Catalyst builds against the iOS SDK, so
# the packages' net10.0-maccatalyst26.0 has exactly the same Xcode requirement as the iOS
# repository's net10.0-ios26.0, and .NET only builds either against an Xcode carrying the
# matching SDK:
#
# error : This version of .NET for iOS (26.0.11017) requires Xcode 26.0.
# The current version of Xcode is 26.5.
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Check README versions
run: ./build/CheckReadmeVersions.sh

- name: Select Xcode
uses: ./.github/actions/select-xcode

Expand Down Expand Up @@ -103,6 +106,17 @@ jobs:
if: steps.xcframeworks.outputs.cache-hit != 'true'
run: ./build/BuildXcFrameworks.sh "${{ steps.native.outputs.version }}"

# 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.
- name: Upload dSYMs
uses: actions/upload-artifact@v4
with:
name: dsyms
path: libs/dsyms
if-no-files-found: error
retention-days: 7

- name: Pack all packages
run: ./build/BuildNugets.sh "${{ inputs.version }}" "${{ steps.native.outputs.version }}"

Expand Down Expand Up @@ -164,3 +178,51 @@ jobs:
--configuration Debug \
-p:RuntimeIdentifier=maccatalyst-arm64 \
-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.
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 }}."
17 changes: 16 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ jobs:
name: nuget-packages
path: artifacts

- name: Download dSYMs
uses: actions/download-artifact@v4
with:
name: dsyms
path: dsyms

# 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
Expand Down Expand Up @@ -140,6 +146,10 @@ jobs:
echo
echo "Bound against [dd-sdk-ios \`${native}\`](https://github.com/DataDog/dd-sdk-ios/releases/tag/${native}), built from source for Mac Catalyst, targeting \`net8.0-maccatalyst18.0\`, \`net9.0-maccatalyst18.0\` and \`net10.0-maccatalyst26.0\`."
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
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"
echo "> the native binaries stay put."
Expand Down Expand Up @@ -178,13 +188,18 @@ 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 }}
run: |
# The packages themselves are not attached: nuget.org is the distribution channel, and
# the notes link to every published package instead.
# the notes link to every published package instead. The dSYMs are attached, because
# this release is the only place symbolication data for these binaries exists.
gh release create "${GITHUB_REF_NAME}" \
"dsyms-${VERSION}.zip" \
--title "${VERSION}" \
--notes-file release-notes.md \
${{ needs.version.outputs.prerelease == 'true' && '--prerelease' || '' }}
14 changes: 13 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
script needs Xcode rather than just curl.
-->
<DatadogNativeVersion>3.14.0</DatadogNativeVersion>
<DatadogBindingRevision>1</DatadogBindingRevision>
<DatadogBindingRevision>2</DatadogBindingRevision>
<VersionPrefix>$(DatadogNativeVersion).$(DatadogBindingRevision)</VersionPrefix>

<!--
Expand Down Expand Up @@ -53,4 +53,16 @@
<ContinuousIntegrationBuild Condition=" '$(GITHUB_ACTIONS)' == 'true' ">true</ContinuousIntegrationBuild>
</PropertyGroup>

<!--
Surface the curated release notes on nuget.org. docs/release-notes/<version>.md already feeds
the GitHub release; packing it here is what makes it reach the .nupkg, so nuget.org stops
showing empty release notes. A version with no curated file - a PR prerelease of unreleased
work, typically - falls back to the releases page rather than shipping nothing.
-->
<PropertyGroup>
<DatadogReleaseNotesFile>$(MSBuildThisFileDirectory)docs/release-notes/$(VersionPrefix).md</DatadogReleaseNotesFile>
<PackageReleaseNotes Condition=" Exists('$(DatadogReleaseNotesFile)') ">$([System.IO.File]::ReadAllText('$(DatadogReleaseNotesFile)'))</PackageReleaseNotes>
<PackageReleaseNotes Condition=" '$(PackageReleaseNotes)' == '' ">https://github.com/sbokatuk/DatadogNet.Mac/releases/tag/v$(VersionPrefix)</PackageReleaseNotes>
</PropertyGroup>

</Project>
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ Every package targets `net8.0-maccatalyst18.0`, `net9.0-maccatalyst18.0` and
`net10.0-maccatalyst26.0`, with `SupportedOSPlatformVersion` 15.0 (macOS 12) - the floor both the
.NET 9 maccatalyst workload and Datadog's own Catalyst support statement impose.

> **net8 sunset.** The net8 head is already past its platform support window — the net8 mobile
> workloads left support with MAUI 8 on 14 May 2025 — and ships for the apps that still target
> it. So the decision does not persist by inertia: **the net8 head is dropped in the first
> release after .NET 8 itself leaves support on 10 November 2026**, in step with DatadogNet.iOS.

## Installing

```sh
Expand All @@ -69,8 +74,8 @@ bindings:

```xml
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">
<PackageReference Include="DatadogNet.Core.Mac" Version="3.14.0.1" />
<PackageReference Include="DatadogNet.RUM.Mac" Version="3.14.0.1" />
<PackageReference Include="DatadogNet.Core.Mac" Version="3.14.0.2" />
<PackageReference Include="DatadogNet.RUM.Mac" Version="3.14.0.2" />
</ItemGroup>
```

Expand Down Expand Up @@ -117,7 +122,11 @@ make that possible; the script documents both.
The binding definitions (`ApiDefinitions.cs`, `StructsAndEnums.cs`, `Additions/`) are **verbatim
copies from DatadogNet.iOS**, refreshed by
[build/SyncBindingsFromiOS.sh](build/SyncBindingsFromiOS.sh). Do not edit them here: fix them in
the iOS repository and re-sync, so the two platforms cannot drift.
the iOS repository and re-sync, so the two platforms cannot drift. That is enforced, not asked
politely: the sync records the iOS commit it copied from in `build/ios-bindings-source.txt`, and
CI's `binding-drift` job re-runs the sync against exactly that commit and fails on any
difference. (`shims/` is deliberately outside the sync — see the script's header for what must
happen if the iOS Flags shim ever ships.)

### Layout

Expand Down
3 changes: 2 additions & 1 deletion build/BuildNugets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ set -e
# Packages are written to ../artifacts.
#
# Each .NET SDK's maccatalyst workload supports only two target frameworks - the .NET 9 band
# builds net8/net9, the .NET 10 band builds net9/net10 - so this runs two passes and merges them,
# builds net8/net9, and the .NET 10 band contributes net10 (Datadog.Binding.props points its pass
# at net10.0-maccatalyst26.0 alone) - so this runs two passes and merges them,
# exactly as DatadogNet.iOS does. The repository's global.json pins the .NET 9 SDK, so the second
# pass is invoked from a scratch directory carrying its own global.json, since the SDK is resolved
# from the working directory.
Expand Down
80 changes: 76 additions & 4 deletions build/BuildXcFrameworks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ fi
DD_REPO="$WORK/dd-sdk-ios"
OTEL_REPO="$WORK/opentelemetry-swift-packages"
ARCHIVES="$WORK/archives"
DSYMS="$WORK/dsyms"
# Under libs/, not under $WORK: $WORK is a mktemp directory removed on exit, and dSYMs that only
# ever exist there cannot be attached to a release - which is the one thing they are for. Next to
# the frameworks they survive the build, ride the same CI cache, and are never packed (the binding
# projects reference libs/<Framework>.xcframework by name; nothing globs libs/).
DSYMS="$LIBS/dsyms"

echo "==> building dd-sdk-ios $DATADOG_VERSION + OpenTelemetryApi $OTEL_VERSION for Mac Catalyst"
echo " work directory: $WORK"
Expand All @@ -80,6 +84,39 @@ if [ ! -d "$OTEL_REPO" ]; then
git clone --depth 1 --branch "$OTEL_VERSION" https://github.com/DataDog/opentelemetry-swift-packages.git "$OTEL_REPO"
fi

# ---------------------------------------------------------------------------------------------
# Guard the second pin. DatadogOtelVersion is maintained by hand in Directory.Build.props and
# must match the OpenTelemetryApi version the checked-out dd-sdk-ios tag pins in its
# Cartfile.resolved - bumping the native version and forgetting the OTEL line would otherwise
# silently build the wrong OpenTelemetryApi. Checked here, against the actual checkout, so the
# mismatch fails the build instead of shipping.
# ---------------------------------------------------------------------------------------------

CARTFILE="$DD_REPO/Cartfile.resolved"
if [ "$DATADOG_SKIP_OTEL_CHECK" = "1" ]; then
echo "==> skipping the DatadogOtelVersion check (DATADOG_SKIP_OTEL_CHECK=1)"
else
resolved=""
if [ -f "$CARTFILE" ]; then
resolved=$(grep -i 'opentelemetry' "$CARTFILE" | grep -oE '"[0-9][A-Za-z0-9._-]*"' | tail -1 | tr -d '"')
fi
if [ -z "$resolved" ]; then
echo "error: could not read the OpenTelemetryApi version from $CARTFILE." >&2
echo " dd-sdk-ios $DATADOG_VERSION no longer pins it there. Find where the new tag pins" >&2
echo " it, update DatadogOtelVersion in Directory.Build.props, and update this check." >&2
echo " DATADOG_SKIP_OTEL_CHECK=1 skips it if the pin has genuinely moved." >&2
exit 1
fi
if [ "$resolved" != "$OTEL_VERSION" ]; then
echo "error: DatadogOtelVersion is $OTEL_VERSION, but dd-sdk-ios $DATADOG_VERSION pins OpenTelemetryApi $resolved." >&2
echo " Update DatadogOtelVersion in Directory.Build.props to $resolved. Building with a" >&2
echo " mismatched pin links the Datadog frameworks against one OpenTelemetryApi and" >&2
echo " ships another." >&2
exit 1
fi
echo "==> DatadogOtelVersion $OTEL_VERSION matches dd-sdk-ios $DATADOG_VERSION's Cartfile.resolved"
fi

# ---------------------------------------------------------------------------------------------
# Patch the Datadog Xcode project for Catalyst. Two things stand between the source - which
# compiles for Catalyst - and an archive that actually builds:
Expand Down Expand Up @@ -107,21 +144,56 @@ import sys
path = sys.argv[1]
text = open(path).read()

# Every patch below is a text replacement against an Xcode-generated file, and a replacement
# whose pattern has drifted out from under it is a silent no-op that only surfaces later, as a
# cryptic xcodebuild failure. So each patch asserts it either changed something or found the
# already-patched state (a reused DATADOG_BUILD_DIR checkout is patched twice without harm), and
# anything else fails loudly here, naming the patch that missed.
failures = []

supports_no = text.count("SUPPORTS_MACCATALYST = NO;")
supports_yes = text.count("SUPPORTS_MACCATALYST = YES;")
text = text.replace("SUPPORTS_MACCATALYST = NO;", "SUPPORTS_MACCATALYST = YES;")
if supports_no == 0 and supports_yes == 0:
failures.append(
"SUPPORTS_MACCATALYST: found neither '= NO;' to patch nor an existing '= YES;'. "
"Upstream has moved or reformatted the setting (an .xcconfig, perhaps); without it the "
"archive step rejects the Catalyst destination outright.")

# The singular form appears on link-phase entries and cannot carry two values.
singular = text.count("platformFilter = ios;")
singular_patched = text.count("platformFilters = (ios, maccatalyst, );")
text = text.replace("platformFilter = ios;", "platformFilters = (ios, maccatalyst, );")

# List form, both the inline and the one-value-per-line layout. Idempotent, so a reused
# DATADOG_BUILD_DIR checkout is patched twice without harm.
# List form, both the inline and the one-value-per-line layout.
lists_patched = 0
def add_maccatalyst(match):
global lists_patched
body = match.group(1)
if "maccatalyst" in body or "ios" not in body:
return match.group(0)
lists_patched += 1
return match.group(0).replace("ios,", "ios, maccatalyst,", 1)

text = re.sub(r"platformFilters = \(([^)]*)\)", add_maccatalyst, text)

if singular + singular_patched + lists_patched == 0 and "maccatalyst" not in text:
failures.append(
"platformFilter(s): nothing was patched and no filter mentions maccatalyst. Catalyst "
"would silently drop inter-framework dependencies and the archive would die with "
"\"unable to resolve module dependency: 'DatadogInternal'\". If upstream has removed "
"platform filters entirely this check can go; otherwise the patterns need updating.")

if failures:
print("error: the pbxproj patches no longer match upstream's project file:", file=sys.stderr)
for failure in failures:
print(" * " + failure, file=sys.stderr)
print(" Review the patch block in build/BuildXcFrameworks.sh against " + path, file=sys.stderr)
sys.exit(1)

print(" patched SUPPORTS_MACCATALYST on %d target(s), %d singular and %d list platform filter(s)"
% (supports_no, singular, lists_patched))

open(path, "w").write(text)
EOF

Expand Down Expand Up @@ -234,5 +306,5 @@ echo
echo "==> built into $LIBS:"
ls "$LIBS"
echo
echo " dSYMs (for a GitHub release / Datadog symbolication) are in:"
echo " dSYMs (attached to the GitHub release, for Datadog crash symbolication) are in:"
echo " $DSYMS"
50 changes: 50 additions & 0 deletions build/CheckReadmeVersions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/sh
# Fails when README.md pins a package version that is not the one this repository currently
# builds. The install snippets are copy-paste starting points, and a hardcoded version there goes
# stale silently on every release - it would go stale the same way the sibling repositories' snippets did. Running
# this in CI makes the version bump before a release drag the README along with it.
#
# What is checked: every <PackageReference Include="DatadogNet..." Version="..."> pin, and the
# device-check example (run-simulator-tests.sh <version> ...). Prose that explains the version
# *scheme* ("3.14.0.1 wraps dd-sdk-ios 3.14.0") is deliberately not checked -
# it describes the format, not the current release.
set -eu

root="$(cd "$(dirname "$0")/.." && pwd)"
readme="$root/README.md"
props="$root/Directory.Build.props"

prop() {
sed -n "s/.*<$1>\(.*\)<\/$1>.*/\1/p" "$props" | head -1
}

version="$(prop DatadogNativeVersion).$(prop DatadogBindingRevision)"

bad=0

old_ifs=$IFS
IFS='
'
for pin in $(grep -oE 'Include="DatadogNet[^"]*" +Version="[0-9][^"]*"' "$readme"); do
id=$(printf '%s' "$pin" | sed -E 's/Include="([^"]*)".*/\1/')
ver=$(printf '%s' "$pin" | sed -E 's/.*Version="([^"]*)"/\1/')
if [ "$ver" != "$version" ]; then
echo "README pins $id $ver, but the current version is $version" >&2
bad=1
fi
done

for token in $(grep -oE 'run-(simulator|emulator)-tests\.sh +[0-9][0-9.]*' "$readme" | grep -oE '[0-9][0-9.]*$'); do
if [ "$token" != "$version" ]; then
echo "README runs the device checks at $token, but the current version is $version" >&2
bad=1
fi
done
IFS=$old_ifs

if [ "$bad" -ne 0 ]; then
echo "CheckReadmeVersions: README.md is stale - update the versions above (current: $version)" >&2
exit 1
fi

echo "CheckReadmeVersions: README.md agrees with Directory.Build.props ($version)"
Loading
Loading