From 0f3e0655f5379ea6ff73906f9358b35495ef637a Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 18 Jun 2026 01:54:59 -0500 Subject: [PATCH 01/33] Prototype: SPM distribution via prebuilt xcframework (Apple) First-pass scaffold to support Swift Package Manager on Apple platforms (the successor to CocoaPods, whose trunk goes read-only Dec 2 2026; there is no official in-repo podspec today). SPM cannot practically compile the C++ tree (CMake/Bond/sqlite/zlib/platform conditionals), so the compiled C++ core + Obj-C wrappers ship as a prebuilt MATTelemetry.xcframework (.binaryTarget) and the thin Swift layer (wrappers/swift/Sources/OneDSSwift) is compiled from source on top of the Obj-C module the xcframework vends. - Package.swift (root): binaryTarget (xcframework) + OneDSSwift source target; documents the path: -> url:+checksum: switch for releases. - tools/apple/build-xcframework.sh: per-slice static libmat.a via build-ios.sh (iOS device + simulator), lipo, then xcodebuild -create-xcframework; emits a zip + SPM checksum. - tools/apple/module.modulemap + MATTelemetry-umbrella.h: vend the `ObjCModule` Clang module the existing Swift sources already import. - tools/apple/README.md: approach, build/consume steps, release wiring, and the macOS-validation TODOs (macOS/Catalyst slices, conditional modules, header flattening, signing). NOT yet validated on macOS -- needs a mac with Xcode to run the build and adjust the static-lib path / header layout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Package.swift | 65 ++++++++++++++++++++++ tools/apple/MATTelemetry-umbrella.h | 29 ++++++++++ tools/apple/README.md | 70 ++++++++++++++++++++++++ tools/apple/build-xcframework.sh | 84 +++++++++++++++++++++++++++++ tools/apple/module.modulemap | 9 ++++ 5 files changed, 257 insertions(+) create mode 100644 Package.swift create mode 100644 tools/apple/MATTelemetry-umbrella.h create mode 100644 tools/apple/README.md create mode 100755 tools/apple/build-xcframework.sh create mode 100644 tools/apple/module.modulemap diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..ad4364e70 --- /dev/null +++ b/Package.swift @@ -0,0 +1,65 @@ +// swift-tools-version: 5.9 +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// Swift Package Manager manifest for the 1DS C++ SDK (Microsoft Applications +// Telemetry) on Apple platforms. +// +// PROTOTYPE — distribution model: +// * The compiled C++ core + Obj-C wrappers ship as a prebuilt binary +// xcframework (built by tools/apple/build-xcframework.sh). This avoids +// compiling the CMake/Bond/sqlite/zlib C++ tree through SPM, which is not +// practical. +// * The thin Swift wrapper (wrappers/swift/Sources/OneDSSwift) is compiled +// from source on top of the Obj-C module vended by the xcframework. +// +// Local development: +// 1. Run `tools/apple/build-xcframework.sh release` on macOS with Xcode. +// It produces ./build/apple/MATTelemetry.xcframework. +// 2. `swift build` (or add this package as a local dependency). +// +// Release distribution (so consumers can add the repo by URL in Xcode): +// 1. Build the xcframework, zip it, and attach it to the GitHub Release. +// 2. Run `swift package compute-checksum MATTelemetry.xcframework.zip`. +// 3. Replace the `.binaryTarget(... path:)` below with the `url:`+`checksum:` +// form shown in the comment. The vcpkg-release-bump workflow pattern can be +// extended to automate steps 1-3 on each release tag. + +import PackageDescription + +let package = Package( + name: "OneDSSwift", + platforms: [ + .iOS(.v12), + .macOS(.v10_15), + ], + products: [ + .library(name: "OneDSSwift", targets: ["OneDSSwift"]), + ], + targets: [ + // Prebuilt C++ core + Obj-C wrappers. The xcframework's bundled + // module map vends the Clang module `ObjCModule` (see + // tools/apple/module.modulemap), which the Swift layer imports. + // + // For a tagged release, swap the local path for the hosted artifact: + // + // .binaryTarget( + // name: "MATTelemetry", + // url: "https://github.com/microsoft/cpp_client_telemetry/releases/download/v3.10.161.1/MATTelemetry.xcframework.zip", + // checksum: ""), + .binaryTarget( + name: "MATTelemetry", + path: "build/apple/MATTelemetry.xcframework"), + + // Thin Swift API layer (source). Depends on the Obj-C module from the + // xcframework. NOTE: the conditional source exclusions in + // wrappers/swift/Package.swift (PrivacyGuard / Sanitizer / DataViewer + // when those private modules aren't built) should be carried over here + // and kept in sync with the headers baked into the xcframework. + .target( + name: "OneDSSwift", + dependencies: ["MATTelemetry"], + path: "wrappers/swift/Sources/OneDSSwift"), + ] +) diff --git a/tools/apple/MATTelemetry-umbrella.h b/tools/apple/MATTelemetry-umbrella.h new file mode 100644 index 000000000..562509962 --- /dev/null +++ b/tools/apple/MATTelemetry-umbrella.h @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// Umbrella header for the Obj-C surface vended by MATTelemetry.xcframework. +// +// build-xcframework.sh flattens the ODW*.h headers into the framework's +// Headers/ directory, so these are imported by bare name (not by the +// ../../obj-c/ relative path the in-repo Swift bridging header uses). +// +// Keep this list in sync with: +// * the ODW*.h headers copied by tools/apple/build-xcframework.sh, and +// * the Swift sources compiled in wrappers/swift/Sources/OneDSSwift. +// +// TODO(validate on macOS): confirm the ODW headers reference each other by bare +// name (or adjust the copy step) so the flattened layout compiles cleanly. + +#import + +#import "ODWDiagnosticDataViewer.h" +#import "ODWEventProperties.h" +#import "ODWLogConfiguration.h" +#import "ODWLogger.h" +#import "ODWLogManager.h" +#import "ODWPrivacyGuard.h" +#import "ODWPrivacyGuardInitConfig.h" +#import "ODWSanitizer.h" +#import "ODWSanitizerInitConfig.h" +#import "ODWSemanticContext.h" diff --git a/tools/apple/README.md b/tools/apple/README.md new file mode 100644 index 000000000..5c5848e8f --- /dev/null +++ b/tools/apple/README.md @@ -0,0 +1,70 @@ +# Swift Package Manager (xcframework) — prototype + +**Status: prototype / not yet validated on macOS.** This is a first-pass scaffold +for distributing the 1DS C++ SDK to Apple app developers via **Swift Package +Manager (SPM)**, the successor to CocoaPods (the CocoaPods trunk goes read-only +on 2 Dec 2026, and there is no official in-repo podspec today). + +## Approach + +SPM cannot practically compile this SDK's C++ tree from source (CMake build, +Bond codegen, vendored sqlite3/zlib, heavy platform conditionals). So: + +| Layer | How it ships | +| --- | --- | +| C++ core + Obj-C wrappers (`ODW*`) | **Prebuilt binary** — `MATTelemetry.xcframework` (`.binaryTarget`) | +| Swift API (`OneDSSwift`) | **Source** — `wrappers/swift/Sources/OneDSSwift`, depends on the Obj-C module from the xcframework | + +The Obj-C wrappers already compile into `libmat.a` on Apple +(`lib/CMakeLists.txt:217`), and the Swift sources already `import ObjCModule`, +so the xcframework just needs to vend a Clang module named `ObjCModule` +(`tools/apple/module.modulemap` + `MATTelemetry-umbrella.h`). + +## Files + +| File | Purpose | +| --- | --- | +| `Package.swift` (repo root) | Distributable SPM manifest: `binaryTarget` (xcframework) + `OneDSSwift` source target | +| `tools/apple/build-xcframework.sh` | Builds a static `libmat.a` per Apple slice via `build-ios.sh`, lipo's the simulator archs, and assembles the xcframework with `xcodebuild -create-xcframework` | +| `tools/apple/module.modulemap` | Defines the `ObjCModule` Clang module the Swift layer imports | +| `tools/apple/MATTelemetry-umbrella.h` | Umbrella over the `ODW*.h` headers baked into the xcframework | + +## Build (on macOS) + +```bash +tools/apple/build-xcframework.sh release +# -> build/apple/MATTelemetry.xcframework +# -> build/apple/MATTelemetry.xcframework.zip (+ prints the SPM checksum) +swift build # resolves Package.swift against the local xcframework +``` + +## Consume + +- **Local:** point a sample app at this package directory (path dependency). +- **Released:** in Xcode, *File → Add Packages…* and enter the repo URL once the + release flow below is in place. + +## Release wiring (to make it URL-consumable) + +1. Build the xcframework and zip it (the script does both). +2. Attach `MATTelemetry.xcframework.zip` to the GitHub Release for the tag. +3. Switch the `.binaryTarget` in `Package.swift` from `path:` to + `url:`+`checksum:` (the `compute-checksum` value the script prints). + +This mirrors the `vcpkg-release-bump` workflow: a release-triggered job can build +the xcframework, upload it to the Release, and bump the `binaryTarget` URL + +checksum automatically. + +## Known gaps / TODO (validate on macOS) + +- **macOS / Catalyst / visionOS slices** — only iOS device + simulator are wired + up in this first pass; add the macOS slice (see section 3 of the script). +- **Conditional modules** — carry over the `moduleExists()` source exclusions + from `wrappers/swift/Package.swift` (PrivacyGuard / Sanitizer / DataViewer) and + keep the umbrella header in sync with the headers actually built. +- **Header flattening** — confirm the `ODW*.h` headers reference each other by + bare name in the flattened `Headers/` layout (adjust the copy step if not). +- **Static-lib path** — verify `out/lib/libmat.a` is the actual artifact name and + that `-DBUILD_SHARED_LIBS=OFF` yields a static archive for every slice. +- **Code signing** — release xcframeworks are typically signed; add a signing + step before zipping for distribution. diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh new file mode 100755 index 000000000..728a90dae --- /dev/null +++ b/tools/apple/build-xcframework.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# PROTOTYPE: build MATTelemetry.xcframework (1DS C++ core + Obj-C wrappers) for +# Apple platforms, for Swift Package Manager distribution. +# +# Run on macOS with Xcode + CMake installed. Usage: +# tools/apple/build-xcframework.sh [release|debug] +# +# Produces: +# build/apple/MATTelemetry.xcframework +# build/apple/MATTelemetry.xcframework.zip (+ prints the SPM checksum) +# +# Slices built here: iOS device (arm64) and iOS simulator (arm64 + x86_64 fat). +# A macOS slice (and visionOS, Catalyst) can be folded in the same way -- see +# the note in section 3. +# +# NOTE: this is a first-pass scaffold. It has NOT been executed on macOS yet; +# validate on a mac and adjust the static-lib path / header flattening as needed. + +set -euo pipefail + +CONFIG="${1:-release}" +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +OUT="$ROOT/build/apple" +LIB="libmat.a" # mat target; the Obj-C wrappers compile into it (lib/CMakeLists.txt:217) + +# Force a STATIC libmat that includes the Obj-C wrappers, regardless of the +# repo's default library type. +export CMAKE_OPTS="-DBUILD_SHARED_LIBS=OFF -DBUILD_OBJC_WRAPPER=YES ${CMAKE_OPTS:-}" + +rm -rf "$OUT" +mkdir -p "$OUT" + +# --- 1. Public Obj-C headers + module map (vended by the xcframework) -------- +# Flatten the ODW*.h headers + umbrella + modulemap into one Headers dir. The +# module is named `ObjCModule` to match what wrappers/swift sources import. +HDRS="$OUT/Headers" +mkdir -p "$HDRS" +cp "$ROOT"/wrappers/obj-c/ODW*.h "$HDRS/" +cp "$ROOT"/tools/apple/MATTelemetry-umbrella.h "$HDRS/" +cp "$ROOT"/tools/apple/module.modulemap "$HDRS/" + +# --- 2. Build one static lib per (arch, platform) ---------------------------- +build_slice() { # arch platform out-subdir + local arch="$1" plat="$2" sub="$3" + echo "=== building $arch / $plat ($CONFIG) ===" + ( cd "$ROOT" && ./build-ios.sh clean "$CONFIG" "$arch" "$plat" ) + mkdir -p "$OUT/$sub" + cp "$ROOT/out/lib/$LIB" "$OUT/$sub/$LIB" +} + +build_slice arm64 iphoneos ios-arm64 +build_slice arm64 iphonesimulator ios-arm64-sim +build_slice x86_64 iphonesimulator ios-x86_64-sim + +# Fat simulator archive (arm64 + x86_64) -- a single xcframework slice cannot +# mix device and simulator, but it can contain multiple archs for one platform. +mkdir -p "$OUT/ios-simulator" +lipo -create "$OUT/ios-arm64-sim/$LIB" "$OUT/ios-x86_64-sim/$LIB" \ + -output "$OUT/ios-simulator/$LIB" + +# --- 3. (Optional) macOS slice ------------------------------------------------ +# Add a native macOS build (e.g. cmake -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" +# -DBUILD_APPLE_HTTP=YES) here and append another `-library .../libmat.a +# -headers "$HDRS"` pair to the xcodebuild call below. Omitted from this first +# pass to keep the prototype focused on iOS. + +# --- 4. Assemble the xcframework --------------------------------------------- +rm -rf "$OUT/MATTelemetry.xcframework" +xcodebuild -create-xcframework \ + -library "$OUT/ios-arm64/$LIB" -headers "$HDRS" \ + -library "$OUT/ios-simulator/$LIB" -headers "$HDRS" \ + -output "$OUT/MATTelemetry.xcframework" +echo "Created $OUT/MATTelemetry.xcframework" + +# --- 5. Zip + checksum for release distribution ------------------------------ +( cd "$OUT" && rm -f MATTelemetry.xcframework.zip \ + && zip -qry MATTelemetry.xcframework.zip MATTelemetry.xcframework ) +echo "Zipped: $OUT/MATTelemetry.xcframework.zip" +echo -n "SPM checksum (for Package.swift binaryTarget url: form): " +swift package compute-checksum "$OUT/MATTelemetry.xcframework.zip" diff --git a/tools/apple/module.modulemap b/tools/apple/module.modulemap new file mode 100644 index 000000000..0a2c0167a --- /dev/null +++ b/tools/apple/module.modulemap @@ -0,0 +1,9 @@ +// Clang module vended by MATTelemetry.xcframework. Imported by the OneDSSwift +// Swift layer as `import ObjCModule` -- the module name matches what the +// existing wrappers/swift/Sources/OneDSSwift sources already import, so no Swift +// source changes are needed. + +module ObjCModule { + umbrella header "MATTelemetry-umbrella.h" + export * +} From 5e3687e2b016b8b1cbfdeb530748fb5d1a39854d Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 18 Jun 2026 02:36:21 -0500 Subject: [PATCH 02/33] Prototype: SPM release workflow + parallel 3-component SemVer tag Adds .github/workflows/spm-release.yml: on a published 4-component release (vX.Y.Z.W), a macOS-runner job builds MATTelemetry.xcframework, uploads it to the Release, computes the SPM checksum, rewrites the Package.swift binaryTarget from path: to url:+checksum:, and pushes a 3-component SemVer tag (X.Y.Z) that Swift Package Manager can resolve (the SDK's own 4-component tags are not valid SemVer, so SPM ignores them). Also documents the parallel-tag consumption (`from: "3.10.161"`) and the release flow in tools/apple/README.md. Mirrors the vcpkg-release-bump pattern. NOT yet validated -- needs the prototype merged (so Package.swift exists at the release tag) and a macOS runner; the build script itself still needs a first run on a mac. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/spm-release.yml | 141 ++++++++++++++++++++++++++++++ tools/apple/README.md | 37 +++++--- 2 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/spm-release.yml diff --git a/.github/workflows/spm-release.yml b/.github/workflows/spm-release.yml new file mode 100644 index 000000000..cd8c1c670 --- /dev/null +++ b/.github/workflows/spm-release.yml @@ -0,0 +1,141 @@ +name: SPM release (xcframework) + +# On a published SDK release, build MATTelemetry.xcframework, upload it to the +# GitHub Release, and publish a 3-component SemVer tag for Swift Package Manager +# whose Package.swift binaryTarget points at the uploaded artifact + checksum. +# +# Why a separate tag: the SDK's own release tags are 4-component (vX.Y.Z.W), +# which is NOT valid SemVer, so Swift Package Manager ignores them. This derives +# a 3-component tag (X.Y.Z) from the same release that SPM can resolve. +# +# Prerequisites: +# * The root Package.swift (the SPM manifest) must exist at the release tag +# (i.e. this prototype merged to main before the release is cut). +# * Uses the default GITHUB_TOKEN (needs contents: write). No extra secrets. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "4-component release tag to publish for SPM (e.g. v3.10.161.1)" + required: true + type: string + +permissions: + contents: write + +concurrency: + group: spm-release-${{ github.event.release.tag_name || inputs.tag }} + cancel-in-progress: false + +jobs: + spm: + name: Publish SPM xcframework + tag + # Skip drafts/pre-releases; always allow manual dispatch. + if: >- + ${{ github.event_name == 'workflow_dispatch' || + (github.event.release.draft == false && github.event.release.prerelease == false) }} + runs-on: macos-14 # provides Xcode (xcodebuild, swift) + env: + ARTIFACT: MATTelemetry.xcframework.zip + steps: + - name: Resolve tag and derive SPM version + id: ver + env: + # Pass untrusted tag values through the environment rather than + # interpolating ${{ ... }} into the script body. + RELEASE_TAG: ${{ github.event.release.tag_name }} + INPUT_TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + TAG="${RELEASE_TAG:-$INPUT_TAG}" + if [ -z "$TAG" ]; then echo "::error::No release tag could be resolved."; exit 1; fi + # Only act on 4-component version tags vX.Y.Z.W. + if ! printf '%s' "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "::error::Tag '$TAG' is not a 4-component version tag (expected vX.Y.Z.W)." + exit 1 + fi + echo "::notice::Tag '$TAG' is not a 4-component version tag; nothing to publish." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + VERSION="${TAG#v}" # X.Y.Z.W + SPM_VERSION="${VERSION%.*}" # X.Y.Z (drop the trailing build component) + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "spm_version=$SPM_VERSION" >> "$GITHUB_OUTPUT" + echo "Release $TAG -> SPM tag $SPM_VERSION" + + - name: Checkout the release tag + if: ${{ steps.ver.outputs.skip != 'true' }} + uses: actions/checkout@v4 + with: + ref: ${{ steps.ver.outputs.tag }} + fetch-depth: 0 + # The private lib/modules submodule is intentionally NOT fetched; the + # xcframework ships the core SDK + Obj-C wrappers, matching the vcpkg + # port (the optional modules are excluded there too). + submodules: false + + - name: Build MATTelemetry.xcframework + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + chmod +x tools/apple/build-xcframework.sh + tools/apple/build-xcframework.sh release + test -f "build/apple/$ARTIFACT" + + - name: Compute SPM checksum + id: sum + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + echo "checksum=$(swift package compute-checksum "build/apple/$ARTIFACT")" >> "$GITHUB_OUTPUT" + + - name: Upload xcframework to the release + if: ${{ steps.ver.outputs.skip != 'true' }} + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "${{ steps.ver.outputs.tag }}" "build/apple/$ARTIFACT" --clobber + + - name: Point Package.swift at the released artifact + if: ${{ steps.ver.outputs.skip != 'true' }} + env: + ASSET_URL: https://github.com/${{ github.repository }}/releases/download/${{ steps.ver.outputs.tag }}/MATTelemetry.xcframework.zip + CHECKSUM: ${{ steps.sum.outputs.checksum }} + run: | + set -euo pipefail + python3 - "$ASSET_URL" "$CHECKSUM" <<'PY' + import re, sys + url, checksum = sys.argv[1], sys.argv[2] + path = "Package.swift" + src = open(path).read() + repl = ( + '.binaryTarget(\n' + ' name: "MATTelemetry",\n' + f' url: "{url}",\n' + f' checksum: "{checksum}")' + ) + out = re.sub( + r'\.binaryTarget\(\s*name:\s*"MATTelemetry",\s*path:\s*"[^"]*"\s*\)', + repl, src, count=1) + assert out != src, "binaryTarget(path:) block not found in Package.swift" + open(path, "w").write(out) + PY + + - name: Commit manifest and push the 3-component SPM tag + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Package.swift + git commit -m "[spm] ${{ steps.ver.outputs.spm_version }}: pin xcframework url + checksum" + # The SPM tag points at this commit (release source + resolved + # binaryTarget). It is published as a tag only, not merged to a branch. + git tag -a "${{ steps.ver.outputs.spm_version }}" \ + -m "Swift Package Manager release ${{ steps.ver.outputs.spm_version }} (from ${{ steps.ver.outputs.tag }})" + git push origin "refs/tags/${{ steps.ver.outputs.spm_version }}" + echo "Published SPM tag ${{ steps.ver.outputs.spm_version }}" diff --git a/tools/apple/README.md b/tools/apple/README.md index 5c5848e8f..90cb53452 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -41,19 +41,30 @@ swift build # resolves Package.swift against the local xcframework ## Consume - **Local:** point a sample app at this package directory (path dependency). -- **Released:** in Xcode, *File → Add Packages…* and enter the repo URL once the - release flow below is in place. - -## Release wiring (to make it URL-consumable) - -1. Build the xcframework and zip it (the script does both). -2. Attach `MATTelemetry.xcframework.zip` to the GitHub Release for the tag. -3. Switch the `.binaryTarget` in `Package.swift` from `path:` to - `url:`+`checksum:` (the `compute-checksum` value the script prints). - -This mirrors the `vcpkg-release-bump` workflow: a release-triggered job can build -the xcframework, upload it to the Release, and bump the `binaryTarget` URL + -checksum automatically. +- **Released:** in Xcode *File -> Add Package Dependencies...*, enter the repo + URL and pick a version. SPM only accepts **3-component SemVer**, and the SDK's + own `vX.Y.Z.W` tags are not valid SemVer, so consumers pin the **parallel + 3-component tag** the release workflow publishes: + + ```swift + .package(url: "https://github.com/microsoft/cpp_client_telemetry.git", from: "3.10.161") + ``` + +## Release wiring + +`.github/workflows/spm-release.yml` automates distribution on each published +release (a 4-component `vX.Y.Z.W` tag). On a macOS runner it: + +1. Builds `MATTelemetry.xcframework` and zips it. +2. Uploads the zip to the GitHub Release. +3. Computes the SPM checksum and rewrites the `Package.swift` `binaryTarget` + from `path:` to `url:`+`checksum:`. +4. Commits that manifest and pushes a **3-component SemVer tag** (`X.Y.Z`, + derived by dropping the trailing build component) that SPM can resolve. + +This mirrors the `vcpkg-release-bump` workflow. It requires the root +`Package.swift` to already exist at the release tag (i.e. this prototype merged +before the release is cut). ## Known gaps / TODO (validate on macOS) From ba7ba4cb3c0a1ac3b6d84b466030cfffd9c3a79a Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 18 Jun 2026 13:38:08 -0500 Subject: [PATCH 03/33] Fix local SPM xcframework consumption Copy objc_begin/end support headers into the flattened xcframework Headers directory and mirror the existing Swift wrapper optional-module source exclusions in the root package manifest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Package.swift | 39 +++++++++++++++++++++++++++++++- tools/apple/build-xcframework.sh | 2 ++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index ad4364e70..2e3a51ba7 100644 --- a/Package.swift +++ b/Package.swift @@ -27,6 +27,41 @@ // extended to automate steps 1-3 on each release tag. import PackageDescription +import Foundation + +let packageDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent() + +func moduleExists(_ relativePath: String) -> Bool { + FileManager.default.fileExists(atPath: packageDirectory.appendingPathComponent(relativePath).standardizedFileURL.path) +} + +let hasDiagnosticDataViewer = moduleExists("lib/modules/dataviewer") +let hasPrivacyGuard = moduleExists("lib/modules/privacyguard") +let hasSanitizer = moduleExists("lib/modules/sanitizer") + +var excludedSources: [String] = [] +var swiftSettings: [SwiftSetting] = [] + +if !hasDiagnosticDataViewer { + excludedSources.append("DiagnosticDataViewer.swift") +} + +if hasPrivacyGuard { + swiftSettings.append(.define("MATSDK_PRIVACYGUARD_AVAILABLE")) +} else { + excludedSources.append(contentsOf: [ + "CommonDataContext.swift", + "PrivacyGuard.swift", + "PrivacyGuardInitConfig.swift", + ]) +} + +if !hasSanitizer { + excludedSources.append(contentsOf: [ + "Sanitizer.swift", + "SanitizerInitConfig.swift", + ]) +} let package = Package( name: "OneDSSwift", @@ -60,6 +95,8 @@ let package = Package( .target( name: "OneDSSwift", dependencies: ["MATTelemetry"], - path: "wrappers/swift/Sources/OneDSSwift"), + path: "wrappers/swift/Sources/OneDSSwift", + exclude: excludedSources, + swiftSettings: swiftSettings), ] ) diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 728a90dae..342f9b29a 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -40,6 +40,8 @@ mkdir -p "$OUT" HDRS="$OUT/Headers" mkdir -p "$HDRS" cp "$ROOT"/wrappers/obj-c/ODW*.h "$HDRS/" +cp "$ROOT"/wrappers/obj-c/objc_begin.h "$HDRS/" +cp "$ROOT"/wrappers/obj-c/objc_end.h "$HDRS/" cp "$ROOT"/tools/apple/MATTelemetry-umbrella.h "$HDRS/" cp "$ROOT"/tools/apple/module.modulemap "$HDRS/" From 308e03110a3da3fc92d4af3da292e093c03701aa Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 18 Jun 2026 13:51:49 -0500 Subject: [PATCH 04/33] Copy only public ObjC headers into xcframework Avoid vending private wrapper headers from the flattened MATTelemetry.xcframework Headers directory, which otherwise triggers incomplete umbrella warnings when ObjCModule is imported. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/apple/build-xcframework.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 342f9b29a..5f4e39634 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -39,7 +39,7 @@ mkdir -p "$OUT" # module is named `ObjCModule` to match what wrappers/swift sources import. HDRS="$OUT/Headers" mkdir -p "$HDRS" -cp "$ROOT"/wrappers/obj-c/ODW*.h "$HDRS/" +find "$ROOT/wrappers/obj-c" -maxdepth 1 -name 'ODW*.h' ! -name '*_private.h' -exec cp {} "$HDRS/" \; cp "$ROOT"/wrappers/obj-c/objc_begin.h "$HDRS/" cp "$ROOT"/wrappers/obj-c/objc_end.h "$HDRS/" cp "$ROOT"/tools/apple/MATTelemetry-umbrella.h "$HDRS/" From dd972345632c70f9e1b919e8c99b45c51f38f850 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 18 Jun 2026 17:47:06 -0500 Subject: [PATCH 05/33] Align SPM package with xcframework contents Generate the ObjC umbrella and availability manifest from the modules built into the xcframework, read that manifest from Package.swift, and guard Swift type aliases for optional modules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/spm-release.yml | 2 +- Package.swift | 25 ++++++++-- tools/apple/MATTelemetry-umbrella.h | 12 ++--- tools/apple/MATTelemetryAvailability.json | 5 ++ tools/apple/build-xcframework.sh | 46 ++++++++++++++++++- .../swift/Sources/OneDSSwift/ObjCTypes.swift | 2 + 6 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 tools/apple/MATTelemetryAvailability.json diff --git a/.github/workflows/spm-release.yml b/.github/workflows/spm-release.yml index cd8c1c670..28eb1f414 100644 --- a/.github/workflows/spm-release.yml +++ b/.github/workflows/spm-release.yml @@ -131,7 +131,7 @@ jobs: set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Package.swift + git add Package.swift tools/apple/MATTelemetryAvailability.json git commit -m "[spm] ${{ steps.ver.outputs.spm_version }}: pin xcframework url + checksum" # The SPM tag points at this commit (release source + resolved # binaryTarget). It is published as a tag only, not merged to a branch. diff --git a/Package.swift b/Package.swift index 2e3a51ba7..773733ba2 100644 --- a/Package.swift +++ b/Package.swift @@ -31,13 +31,28 @@ import Foundation let packageDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent() -func moduleExists(_ relativePath: String) -> Bool { - FileManager.default.fileExists(atPath: packageDirectory.appendingPathComponent(relativePath).standardizedFileURL.path) +func readAvailability() -> [String: Bool] { + let candidates = [ + "build/apple/MATTelemetryAvailability.json", + "tools/apple/MATTelemetryAvailability.json", + ] + + for relativePath in candidates { + let url = packageDirectory.appendingPathComponent(relativePath).standardizedFileURL + guard let data = try? Data(contentsOf: url), + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Bool] else { + continue + } + return object + } + + return [:] } -let hasDiagnosticDataViewer = moduleExists("lib/modules/dataviewer") -let hasPrivacyGuard = moduleExists("lib/modules/privacyguard") -let hasSanitizer = moduleExists("lib/modules/sanitizer") +let availability = readAvailability() +let hasDiagnosticDataViewer = availability["diagnosticDataViewer"] ?? false +let hasPrivacyGuard = availability["privacyGuard"] ?? false +let hasSanitizer = availability["sanitizer"] ?? false var excludedSources: [String] = [] var swiftSettings: [SwiftSetting] = [] diff --git a/tools/apple/MATTelemetry-umbrella.h b/tools/apple/MATTelemetry-umbrella.h index 562509962..734ceb07f 100644 --- a/tools/apple/MATTelemetry-umbrella.h +++ b/tools/apple/MATTelemetry-umbrella.h @@ -8,22 +8,16 @@ // Headers/ directory, so these are imported by bare name (not by the // ../../obj-c/ relative path the in-repo Swift bridging header uses). // -// Keep this list in sync with: -// * the ODW*.h headers copied by tools/apple/build-xcframework.sh, and -// * the Swift sources compiled in wrappers/swift/Sources/OneDSSwift. -// -// TODO(validate on macOS): confirm the ODW headers reference each other by bare -// name (or adjust the copy step) so the flattened layout compiles cleanly. +// build-xcframework.sh copies this template and appends imports for optional +// module headers only when those modules were actually built into the binary. #import -#import "ODWDiagnosticDataViewer.h" +#import "ODWCommonDataContext.h" #import "ODWEventProperties.h" #import "ODWLogConfiguration.h" #import "ODWLogger.h" #import "ODWLogManager.h" -#import "ODWPrivacyGuard.h" #import "ODWPrivacyGuardInitConfig.h" -#import "ODWSanitizer.h" #import "ODWSanitizerInitConfig.h" #import "ODWSemanticContext.h" diff --git a/tools/apple/MATTelemetryAvailability.json b/tools/apple/MATTelemetryAvailability.json new file mode 100644 index 000000000..34cb38831 --- /dev/null +++ b/tools/apple/MATTelemetryAvailability.json @@ -0,0 +1,5 @@ +{ + "diagnosticDataViewer": false, + "privacyGuard": false, + "sanitizer": false +} diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 5f4e39634..29e23edf0 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -39,12 +39,54 @@ mkdir -p "$OUT" # module is named `ObjCModule` to match what wrappers/swift sources import. HDRS="$OUT/Headers" mkdir -p "$HDRS" -find "$ROOT/wrappers/obj-c" -maxdepth 1 -name 'ODW*.h' ! -name '*_private.h' -exec cp {} "$HDRS/" \; + +has_dataviewer=false +has_privacyguard=false +has_sanitizer=false +[[ -d "$ROOT/lib/modules/dataviewer" ]] && has_dataviewer=true +[[ -d "$ROOT/lib/modules/privacyguard" ]] && has_privacyguard=true +[[ -d "$ROOT/lib/modules/sanitizer" ]] && has_sanitizer=true + +cat > "$OUT/MATTelemetryAvailability.json" <> "$HDRS/MATTelemetry-umbrella.h" + # --- 2. Build one static lib per (arch, platform) ---------------------------- build_slice() { # arch platform out-subdir local arch="$1" plat="$2" sub="$3" diff --git a/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift b/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift index 1042a42e1..c0c1ad4e0 100644 --- a/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift +++ b/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift @@ -27,4 +27,6 @@ public typealias TransmissionProfile = ODWTransmissionProfile public typealias FlushStatus = ODWStatus // ODWPrivacyGuard.h +#if MATSDK_PRIVACYGUARD_AVAILABLE public typealias DataConcernType = ODWDataConcernType +#endif From accb600cb6f44c3d1f447e114127dda414bd55bc Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 18 Jun 2026 18:00:27 -0500 Subject: [PATCH 06/33] Generate SPM availability from xcframework build Drive Swift source exclusions and ObjC umbrella optional imports from the modules actually built into MATTelemetry.xcframework, remove unsupported macOS package advertising, and add Apple system linker settings for the static binary target. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Package.swift | 14 ++++++++++++-- tools/apple/MATTelemetry-umbrella.h | 5 +++-- tools/apple/build-xcframework.sh | 12 +++++++----- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index 773733ba2..2ff67d8ee 100644 --- a/Package.swift +++ b/Package.swift @@ -82,7 +82,6 @@ let package = Package( name: "OneDSSwift", platforms: [ .iOS(.v12), - .macOS(.v10_15), ], products: [ .library(name: "OneDSSwift", targets: ["OneDSSwift"]), @@ -112,6 +111,17 @@ let package = Package( dependencies: ["MATTelemetry"], path: "wrappers/swift/Sources/OneDSSwift", exclude: excludedSources, - swiftSettings: swiftSettings), + swiftSettings: swiftSettings, + linkerSettings: [ + .linkedLibrary("sqlite3"), + .linkedLibrary("z"), + .linkedFramework("CFNetwork"), + .linkedFramework("CoreFoundation"), + .linkedFramework("Foundation"), + .linkedFramework("IOKit"), + .linkedFramework("Network"), + .linkedFramework("SystemConfiguration"), + .linkedFramework("UIKit"), + ]), ] ) diff --git a/tools/apple/MATTelemetry-umbrella.h b/tools/apple/MATTelemetry-umbrella.h index 734ceb07f..8d4aaf952 100644 --- a/tools/apple/MATTelemetry-umbrella.h +++ b/tools/apple/MATTelemetry-umbrella.h @@ -8,8 +8,9 @@ // Headers/ directory, so these are imported by bare name (not by the // ../../obj-c/ relative path the in-repo Swift bridging header uses). // -// build-xcframework.sh copies this template and appends imports for optional -// module headers only when those modules were actually built into the binary. +// This template intentionally lists only always-available Obj-C headers. +// build-xcframework.sh appends imports for optional module headers only when +// those modules were actually built into the binary. #import diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 29e23edf0..36bc5238f 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -88,17 +88,19 @@ cp "$ROOT"/tools/apple/MATTelemetry-umbrella.h "$HDRS/" } >> "$HDRS/MATTelemetry-umbrella.h" # --- 2. Build one static lib per (arch, platform) ---------------------------- -build_slice() { # arch platform out-subdir +build_slice() { # clean-arg arch platform out-subdir + local clean_arg="$1" + shift local arch="$1" plat="$2" sub="$3" echo "=== building $arch / $plat ($CONFIG) ===" - ( cd "$ROOT" && ./build-ios.sh clean "$CONFIG" "$arch" "$plat" ) + ( cd "$ROOT" && ./build-ios.sh $clean_arg "$CONFIG" "$arch" "$plat" ) mkdir -p "$OUT/$sub" cp "$ROOT/out/lib/$LIB" "$OUT/$sub/$LIB" } -build_slice arm64 iphoneos ios-arm64 -build_slice arm64 iphonesimulator ios-arm64-sim -build_slice x86_64 iphonesimulator ios-x86_64-sim +build_slice clean arm64 iphoneos ios-arm64 +build_slice "" arm64 iphonesimulator ios-arm64-sim +build_slice "" x86_64 iphonesimulator ios-x86_64-sim # Fat simulator archive (arm64 + x86_64) -- a single xcframework slice cannot # mix device and simulator, but it can contain multiple archs for one platform. From 4e03dff20e8da5ae54e5bdfca7ca5ea3591d631f Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 18 Jun 2026 18:23:17 -0500 Subject: [PATCH 07/33] Address SPM prototype review refinements Remove macOS package advertising until a macOS slice exists, add iOS linker settings for the static xcframework, and avoid repeating build tool setup for each slice. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Package.swift | 14 +++++++------- tools/apple/build-xcframework.sh | 4 +++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Package.swift b/Package.swift index 2ff67d8ee..ace48f629 100644 --- a/Package.swift +++ b/Package.swift @@ -113,15 +113,15 @@ let package = Package( exclude: excludedSources, swiftSettings: swiftSettings, linkerSettings: [ + .linkedLibrary("c++"), .linkedLibrary("sqlite3"), .linkedLibrary("z"), - .linkedFramework("CFNetwork"), - .linkedFramework("CoreFoundation"), - .linkedFramework("Foundation"), - .linkedFramework("IOKit"), - .linkedFramework("Network"), - .linkedFramework("SystemConfiguration"), - .linkedFramework("UIKit"), + .linkedFramework("CFNetwork", .when(platforms: [.iOS])), + .linkedFramework("CoreFoundation", .when(platforms: [.iOS])), + .linkedFramework("Foundation", .when(platforms: [.iOS])), + .linkedFramework("Network", .when(platforms: [.iOS])), + .linkedFramework("SystemConfiguration", .when(platforms: [.iOS])), + .linkedFramework("UIKit", .when(platforms: [.iOS])), ]), ] ) diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 36bc5238f..9f531ce71 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -54,7 +54,9 @@ cat > "$OUT/MATTelemetryAvailability.json" < Date: Thu, 18 Jun 2026 18:45:27 -0500 Subject: [PATCH 08/33] Align Apple SPM docs and module availability Mark the prototype as validated, document remaining gaps, and make optional module availability respect explicit BUILD_PRIVACYGUARD/BUILD_SANITIZER settings passed through CMAKE_OPTS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/apple/README.md | 29 +++++++++++++++++------------ tools/apple/build-xcframework.sh | 30 ++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/tools/apple/README.md b/tools/apple/README.md index 90cb53452..162043957 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -1,9 +1,9 @@ # Swift Package Manager (xcframework) — prototype -**Status: prototype / not yet validated on macOS.** This is a first-pass scaffold -for distributing the 1DS C++ SDK to Apple app developers via **Swift Package -Manager (SPM)**, the successor to CocoaPods (the CocoaPods trunk goes read-only -on 2 Dec 2026, and there is no official in-repo podspec today). +**Status: validated prototype.** This is a first-pass scaffold for distributing +the 1DS C++ SDK to Apple app developers via **Swift Package Manager (SPM)**, the +successor to CocoaPods (the CocoaPods trunk goes read-only on 2 Dec 2026, and +there is no official in-repo podspec today). ## Approach @@ -28,6 +28,7 @@ so the xcframework just needs to vend a Clang module named `ObjCModule` | `tools/apple/build-xcframework.sh` | Builds a static `libmat.a` per Apple slice via `build-ios.sh`, lipo's the simulator archs, and assembles the xcframework with `xcodebuild -create-xcframework` | | `tools/apple/module.modulemap` | Defines the `ObjCModule` Clang module the Swift layer imports | | `tools/apple/MATTelemetry-umbrella.h` | Umbrella over the `ODW*.h` headers baked into the xcframework | +| `tools/apple/MATTelemetryAvailability.json` | Build-time optional-module manifest consumed by `Package.swift` so Swift sources match the xcframework contents | ## Build (on macOS) @@ -66,16 +67,20 @@ This mirrors the `vcpkg-release-bump` workflow. It requires the root `Package.swift` to already exist at the release tag (i.e. this prototype merged before the release is cut). -## Known gaps / TODO (validate on macOS) +## Validation performed + +- `tools/apple/build-xcframework.sh release` builds the iOS device and simulator + slices and prints the SPM checksum. +- `xcodebuild -scheme OneDSSwift -destination 'generic/platform=iOS Simulator' build` + validates local SwiftPM consumption. +- A small Obj-C module/static-link smoke test was built and run on an iOS + Simulator. + +## Known gaps / TODO - **macOS / Catalyst / visionOS slices** — only iOS device + simulator are wired up in this first pass; add the macOS slice (see section 3 of the script). -- **Conditional modules** — carry over the `moduleExists()` source exclusions - from `wrappers/swift/Package.swift` (PrivacyGuard / Sanitizer / DataViewer) and - keep the umbrella header in sync with the headers actually built. -- **Header flattening** — confirm the `ODW*.h` headers reference each other by - bare name in the flattened `Headers/` layout (adjust the copy step if not). -- **Static-lib path** — verify `out/lib/libmat.a` is the actual artifact name and - that `-DBUILD_SHARED_LIBS=OFF` yields a static archive for every slice. - **Code signing** — release xcframeworks are typically signed; add a signing step before zipping for distribution. +- **Release workflow validation** — exercise `.github/workflows/spm-release.yml` + end-to-end on an actual published release. diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 9f531ce71..37dfae1c4 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -17,8 +17,8 @@ # A macOS slice (and visionOS, Catalyst) can be folded in the same way -- see # the note in section 3. # -# NOTE: this is a first-pass scaffold. It has NOT been executed on macOS yet; -# validate on a mac and adjust the static-lib path / header flattening as needed. +# NOTE: this is a first-pass scaffold. It has been validated on macOS for iOS +# device + simulator slices; macOS/Catalyst/visionOS slices are still TODO. set -euo pipefail @@ -43,9 +43,31 @@ mkdir -p "$HDRS" has_dataviewer=false has_privacyguard=false has_sanitizer=false + +cmake_option_enabled() { # option-name default-value + local option="$1" + local value="$2" + local token + for token in $CMAKE_OPTS; do + case "$token" in + -D${option}=*) value="${token#*=}" ;; + -D${option}) value=ON ;; + esac + done + value="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')" + case "$value" in + 0|false|no|off) return 1 ;; + *) return 0 ;; + esac +} + [[ -d "$ROOT/lib/modules/dataviewer" ]] && has_dataviewer=true -[[ -d "$ROOT/lib/modules/privacyguard" ]] && has_privacyguard=true -[[ -d "$ROOT/lib/modules/sanitizer" ]] && has_sanitizer=true +if [[ -d "$ROOT/lib/modules/privacyguard" ]] && cmake_option_enabled BUILD_PRIVACYGUARD ON; then + has_privacyguard=true +fi +if [[ -d "$ROOT/lib/modules/sanitizer" ]] && cmake_option_enabled BUILD_SANITIZER ON; then + has_sanitizer=true +fi cat > "$OUT/MATTelemetryAvailability.json" < Date: Thu, 18 Jun 2026 21:21:17 -0500 Subject: [PATCH 09/33] Add macOS slice to SPM xcframework Build a universal macOS libmat archive alongside the iOS device and simulator slices, advertise macOS in the root Swift package, and document the expanded validation story. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Package.swift | 15 ++++++----- tools/apple/README.md | 10 +++++--- tools/apple/build-xcframework.sh | 43 +++++++++++++++++++++++++------- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/Package.swift b/Package.swift index ace48f629..fe7385189 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,8 @@ // Local development: // 1. Run `tools/apple/build-xcframework.sh release` on macOS with Xcode. // It produces ./build/apple/MATTelemetry.xcframework. -// 2. `swift build` (or add this package as a local dependency). +// 2. `swift build` validates macOS consumption; for iOS, add this package as a +// local dependency or build the package with an iOS Simulator destination. // // Release distribution (so consumers can add the repo by URL in Xcode): // 1. Build the xcframework, zip it, and attach it to the GitHub Release. @@ -82,6 +83,7 @@ let package = Package( name: "OneDSSwift", platforms: [ .iOS(.v12), + .macOS(.v10_15), ], products: [ .library(name: "OneDSSwift", targets: ["OneDSSwift"]), @@ -116,11 +118,12 @@ let package = Package( .linkedLibrary("c++"), .linkedLibrary("sqlite3"), .linkedLibrary("z"), - .linkedFramework("CFNetwork", .when(platforms: [.iOS])), - .linkedFramework("CoreFoundation", .when(platforms: [.iOS])), - .linkedFramework("Foundation", .when(platforms: [.iOS])), - .linkedFramework("Network", .when(platforms: [.iOS])), - .linkedFramework("SystemConfiguration", .when(platforms: [.iOS])), + .linkedFramework("CFNetwork", .when(platforms: [.iOS, .macOS])), + .linkedFramework("CoreFoundation", .when(platforms: [.iOS, .macOS])), + .linkedFramework("Foundation", .when(platforms: [.iOS, .macOS])), + .linkedFramework("Network", .when(platforms: [.iOS, .macOS])), + .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macOS])), + .linkedFramework("IOKit", .when(platforms: [.macOS])), .linkedFramework("UIKit", .when(platforms: [.iOS])), ]), ] diff --git a/tools/apple/README.md b/tools/apple/README.md index 162043957..c2d98771d 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -25,7 +25,7 @@ so the xcframework just needs to vend a Clang module named `ObjCModule` | File | Purpose | | --- | --- | | `Package.swift` (repo root) | Distributable SPM manifest: `binaryTarget` (xcframework) + `OneDSSwift` source target | -| `tools/apple/build-xcframework.sh` | Builds a static `libmat.a` per Apple slice via `build-ios.sh`, lipo's the simulator archs, and assembles the xcframework with `xcodebuild -create-xcframework` | +| `tools/apple/build-xcframework.sh` | Builds a static `libmat.a` per Apple slice, lipo's the simulator/macOS archs where needed, and assembles the xcframework with `xcodebuild -create-xcframework` | | `tools/apple/module.modulemap` | Defines the `ObjCModule` Clang module the Swift layer imports | | `tools/apple/MATTelemetry-umbrella.h` | Umbrella over the `ODW*.h` headers baked into the xcframework | | `tools/apple/MATTelemetryAvailability.json` | Build-time optional-module manifest consumed by `Package.swift` so Swift sources match the xcframework contents | @@ -70,7 +70,8 @@ before the release is cut). ## Validation performed - `tools/apple/build-xcframework.sh release` builds the iOS device and simulator - slices and prints the SPM checksum. + slices, plus a universal macOS slice, and prints the SPM checksum. +- `swift build` validates local macOS SwiftPM consumption. - `xcodebuild -scheme OneDSSwift -destination 'generic/platform=iOS Simulator' build` validates local SwiftPM consumption. - A small Obj-C module/static-link smoke test was built and run on an iOS @@ -78,8 +79,9 @@ before the release is cut). ## Known gaps / TODO -- **macOS / Catalyst / visionOS slices** — only iOS device + simulator are wired - up in this first pass; add the macOS slice (see section 3 of the script). +- **Catalyst / visionOS slices** — iOS device, iOS simulator, and macOS are wired + up in this first pass; Catalyst and visionOS still need separate slice wiring + and validation. - **Code signing** — release xcframeworks are typically signed; add a signing step before zipping for distribution. - **Release workflow validation** — exercise `.github/workflows/spm-release.yml` diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 37dfae1c4..85d272d3d 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -13,12 +13,11 @@ # build/apple/MATTelemetry.xcframework # build/apple/MATTelemetry.xcframework.zip (+ prints the SPM checksum) # -# Slices built here: iOS device (arm64) and iOS simulator (arm64 + x86_64 fat). -# A macOS slice (and visionOS, Catalyst) can be folded in the same way -- see -# the note in section 3. +# Slices built here: iOS device (arm64), iOS simulator (arm64 + x86_64 fat), +# and macOS (arm64 + x86_64 universal). # # NOTE: this is a first-pass scaffold. It has been validated on macOS for iOS -# device + simulator slices; macOS/Catalyst/visionOS slices are still TODO. +# device, simulator, and macOS slices; Catalyst/visionOS slices are still TODO. set -euo pipefail @@ -27,6 +26,15 @@ ROOT="$(cd "$(dirname "$0")/../.." && pwd)" OUT="$ROOT/build/apple" LIB="libmat.a" # mat target; the Obj-C wrappers compile into it (lib/CMakeLists.txt:217) +case "$CONFIG" in + release) CMAKE_BUILD_TYPE="Release" ;; + debug) CMAKE_BUILD_TYPE="Debug" ;; + *) + echo "Usage: $0 [release|debug]" >&2 + exit 1 + ;; +esac + # Force a STATIC libmat that includes the Obj-C wrappers, regardless of the # repo's default library type. export CMAKE_OPTS="-DBUILD_SHARED_LIBS=OFF -DBUILD_OBJC_WRAPPER=YES ${CMAKE_OPTS:-}" @@ -132,17 +140,34 @@ mkdir -p "$OUT/ios-simulator" lipo -create "$OUT/ios-arm64-sim/$LIB" "$OUT/ios-x86_64-sim/$LIB" \ -output "$OUT/ios-simulator/$LIB" -# --- 3. (Optional) macOS slice ------------------------------------------------ -# Add a native macOS build (e.g. cmake -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -# -DBUILD_APPLE_HTTP=YES) here and append another `-library .../libmat.a -# -headers "$HDRS"` pair to the xcodebuild call below. Omitted from this first -# pass to keep the prototype focused on iOS. +# Native universal macOS archive. Build only the `mat` target in an isolated +# CMake build directory so switching away from the iOS toolchain does not +# disturb the already-copied iOS archives. +echo "=== building arm64+x86_64 / macosx ($CONFIG) ===" +MACOS_BUILD="$OUT/macos-build" +MACOS_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-10.15}" +cmake -S "$ROOT" -B "$MACOS_BUILD" \ + -DMAC_ARCH=universal \ + -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOS_DEPLOYMENT_TARGET" \ + -DCMAKE_BUILD_TYPE="$CMAKE_BUILD_TYPE" \ + -DCMAKE_PACKAGE_TYPE=tgz \ + -DBUILD_TEST_TOOL=OFF \ + -DBUILD_UNIT_TESTS=OFF \ + -DBUILD_FUNC_TESTS=OFF \ + -DBUILD_SWIFT_WRAPPER=OFF \ + -DBUILD_PACKAGE=OFF \ + $CMAKE_OPTS +cmake --build "$MACOS_BUILD" --target mat +mkdir -p "$OUT/macos-universal" +cp "$MACOS_BUILD/lib/$LIB" "$OUT/macos-universal/$LIB" # --- 4. Assemble the xcframework --------------------------------------------- rm -rf "$OUT/MATTelemetry.xcframework" xcodebuild -create-xcframework \ -library "$OUT/ios-arm64/$LIB" -headers "$HDRS" \ -library "$OUT/ios-simulator/$LIB" -headers "$HDRS" \ + -library "$OUT/macos-universal/$LIB" -headers "$HDRS" \ -output "$OUT/MATTelemetry.xcframework" echo "Created $OUT/MATTelemetry.xcframework" From d242830e0c64d4e6177e303914a22629ce4bf763 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Thu, 18 Jun 2026 22:43:49 -0500 Subject: [PATCH 10/33] Add Mac Catalyst slice to SPM xcframework Teach the Apple build path to produce macabi archives, include a fat Catalyst variant in MATTelemetry.xcframework, advertise Mac Catalyst in the Swift package, and document the expanded validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CMakeLists.txt | 13 +++++++++++-- Package.swift | 18 ++++++++++-------- build-ios.sh | 16 +++++++++++++--- tools/apple/README.md | 19 ++++++++++--------- tools/apple/build-xcframework.sh | 14 ++++++++++++-- 5 files changed, 56 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a0ba0e82..6d117cb42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,13 +57,17 @@ if(APPLE) if (${IOS_PLAT} STREQUAL "iphonesimulator") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mios-simulator-version-min=${IOS_DEPLOYMENT_TARGET}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mios-simulator-version-min=${IOS_DEPLOYMENT_TARGET}") - else() + elseif(NOT ${IOS_PLAT} STREQUAL "maccatalyst") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -miphoneos-version-min=${IOS_DEPLOYMENT_TARGET}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -miphoneos-version-min=${IOS_DEPLOYMENT_TARGET}") endif() endif() - if((${IOS_PLAT} STREQUAL "iphoneos") OR (${IOS_PLAT} STREQUAL "iphonesimulator") OR (${IOS_PLAT} STREQUAL "xros") OR (${IOS_PLAT} STREQUAL "xrsimulator")) + if(${IOS_PLAT} STREQUAL "maccatalyst") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -target ${IOS_ARCH}-apple-ios${IOS_DEPLOYMENT_TARGET}-macabi") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -target ${IOS_ARCH}-apple-ios${IOS_DEPLOYMENT_TARGET}-macabi") + set(IOS_PLATFORM "macosx") + elseif((${IOS_PLAT} STREQUAL "iphoneos") OR (${IOS_PLAT} STREQUAL "iphonesimulator") OR (${IOS_PLAT} STREQUAL "xros") OR (${IOS_PLAT} STREQUAL "xrsimulator")) set(IOS_PLATFORM "${IOS_PLAT}") else() message(FATAL_ERROR "Unrecognized iOS platform '${IOS_PLAT}'") @@ -89,6 +93,11 @@ if(APPLE) OUTPUT_VARIABLE CMAKE_OSX_SYSROOT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + if(${IOS_PLAT} STREQUAL "maccatalyst") + set(IOS_SUPPORT_FRAMEWORKS "${CMAKE_OSX_SYSROOT}/System/iOSSupport/System/Library/Frameworks") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -iframework ${IOS_SUPPORT_FRAMEWORKS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -iframework ${IOS_SUPPORT_FRAMEWORKS}") + endif() message(STATUS "CMAKE_OSX_SYSROOT ${CMAKE_OSX_SYSROOT}") message(STATUS "ARCHITECTURE: ${CMAKE_SYSTEM_PROCESSOR}") message(STATUS "PLATFORM: ${IOS_PLATFORM}") diff --git a/Package.swift b/Package.swift index fe7385189..6144610d0 100644 --- a/Package.swift +++ b/Package.swift @@ -17,8 +17,9 @@ // Local development: // 1. Run `tools/apple/build-xcframework.sh release` on macOS with Xcode. // It produces ./build/apple/MATTelemetry.xcframework. -// 2. `swift build` validates macOS consumption; for iOS, add this package as a -// local dependency or build the package with an iOS Simulator destination. +// 2. `swift build` validates macOS consumption; for iOS / Mac Catalyst, add +// this package as a local dependency or build the package with the desired +// Xcode destination. // // Release distribution (so consumers can add the repo by URL in Xcode): // 1. Build the xcframework, zip it, and attach it to the GitHub Release. @@ -83,6 +84,7 @@ let package = Package( name: "OneDSSwift", platforms: [ .iOS(.v12), + .macCatalyst(.v14), .macOS(.v10_15), ], products: [ @@ -118,13 +120,13 @@ let package = Package( .linkedLibrary("c++"), .linkedLibrary("sqlite3"), .linkedLibrary("z"), - .linkedFramework("CFNetwork", .when(platforms: [.iOS, .macOS])), - .linkedFramework("CoreFoundation", .when(platforms: [.iOS, .macOS])), - .linkedFramework("Foundation", .when(platforms: [.iOS, .macOS])), - .linkedFramework("Network", .when(platforms: [.iOS, .macOS])), - .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macOS])), + .linkedFramework("CFNetwork", .when(platforms: [.iOS, .macCatalyst, .macOS])), + .linkedFramework("CoreFoundation", .when(platforms: [.iOS, .macCatalyst, .macOS])), + .linkedFramework("Foundation", .when(platforms: [.iOS, .macCatalyst, .macOS])), + .linkedFramework("Network", .when(platforms: [.iOS, .macCatalyst, .macOS])), + .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macCatalyst, .macOS])), .linkedFramework("IOKit", .when(platforms: [.macOS])), - .linkedFramework("UIKit", .when(platforms: [.iOS])), + .linkedFramework("UIKit", .when(platforms: [.iOS, .macCatalyst])), ]), ] ) diff --git a/build-ios.sh b/build-ios.sh index d316fe2fa..731572865 100755 --- a/build-ios.sh +++ b/build-ios.sh @@ -4,7 +4,7 @@ # build-ios.sh [clean] [release|debug] ${ARCH} ${PLATFORM} # where # ARCH = arm64|arm64e|x86_64 -# PLATFORM = iphoneos|iphonesimulator|xros|xrsimulator +# PLATFORM = iphoneos|iphonesimulator|maccatalyst|xros|xrsimulator if [ "$1" == "clean" ]; then echo "build-ios.sh: cleaning previous build artifacts" @@ -37,7 +37,7 @@ elif [ "$1" == "x86_64" ]; then shift fi -# the last param is expected to specify the platform name: iphoneos|iphonesimulator|xros|xrsimulator +# the last param is expected to specify the platform name: iphoneos|iphonesimulator|maccatalyst|xros|xrsimulator # so if it is non-empty and it is not "device", we take it as a valid platform name # otherwise we fall back to old iOS logic which only supported iphoneos|iphonesimulator IOS_PLAT="iphonesimulator" @@ -54,13 +54,23 @@ DEPLOYMENT_TARGET="" if [ "$IOS_PLAT" == "iphoneos" ] || [ "$IOS_PLAT" == "iphonesimulator" ]; then SYS_NAME="iOS" + IOS_SYSROOT="$IOS_PLAT" DEPLOYMENT_TARGET="$IOS_DEPLOYMENT_TARGET" if [ -z "$DEPLOYMENT_TARGET" ]; then DEPLOYMENT_TARGET="12.0" FORCE_RESET_DEPLOYMENT_TARGET=YES fi +elif [ "$IOS_PLAT" == "maccatalyst" ]; then + SYS_NAME="iOS" + IOS_SYSROOT="macosx" + DEPLOYMENT_TARGET="$MACCATALYST_DEPLOYMENT_TARGET" + if [ -z "$DEPLOYMENT_TARGET" ]; then + DEPLOYMENT_TARGET="14.0" + FORCE_RESET_DEPLOYMENT_TARGET=YES + fi elif [ "$IOS_PLAT" == "xros" ] || [ "$IOS_PLAT" == "xrsimulator" ]; then SYS_NAME="visionOS" + IOS_SYSROOT="$IOS_PLAT" DEPLOYMENT_TARGET="$XROS_DEPLOYMENT_TARGET" if [ -z "$DEPLOYMENT_TARGET" ]; then DEPLOYMENT_TARGET="1.0" @@ -92,7 +102,7 @@ cd out CMAKE_PACKAGE_TYPE=tgz -cmake_cmd="cmake -DCMAKE_OSX_SYSROOT=$IOS_PLAT -DCMAKE_SYSTEM_NAME=$SYS_NAME -DCMAKE_IOS_ARCH_ABI=$IOS_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$DEPLOYMENT_TARGET -DBUILD_IOS=YES -DIOS_ARCH=$IOS_ARCH -DIOS_PLAT=$IOS_PLAT -DIOS_DEPLOYMENT_TARGET=$DEPLOYMENT_TARGET -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_PACKAGE_TYPE=$CMAKE_PACKAGE_TYPE -DFORCE_RESET_DEPLOYMENT_TARGET=$FORCE_RESET_DEPLOYMENT_TARGET $CMAKE_OPTS .." +cmake_cmd="cmake -DCMAKE_OSX_SYSROOT=$IOS_SYSROOT -DCMAKE_SYSTEM_NAME=$SYS_NAME -DCMAKE_IOS_ARCH_ABI=$IOS_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$DEPLOYMENT_TARGET -DBUILD_IOS=YES -DIOS_ARCH=$IOS_ARCH -DIOS_PLAT=$IOS_PLAT -DIOS_DEPLOYMENT_TARGET=$DEPLOYMENT_TARGET -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_PACKAGE_TYPE=$CMAKE_PACKAGE_TYPE -DFORCE_RESET_DEPLOYMENT_TARGET=$FORCE_RESET_DEPLOYMENT_TARGET $CMAKE_OPTS .." echo "${cmake_cmd}" eval $cmake_cmd diff --git a/tools/apple/README.md b/tools/apple/README.md index c2d98771d..ca13f40a3 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -25,7 +25,7 @@ so the xcframework just needs to vend a Clang module named `ObjCModule` | File | Purpose | | --- | --- | | `Package.swift` (repo root) | Distributable SPM manifest: `binaryTarget` (xcframework) + `OneDSSwift` source target | -| `tools/apple/build-xcframework.sh` | Builds a static `libmat.a` per Apple slice, lipo's the simulator/macOS archs where needed, and assembles the xcframework with `xcodebuild -create-xcframework` | +| `tools/apple/build-xcframework.sh` | Builds a static `libmat.a` per Apple slice, lipo's the simulator/Catalyst/macOS archs where needed, and assembles the xcframework with `xcodebuild -create-xcframework` | | `tools/apple/module.modulemap` | Defines the `ObjCModule` Clang module the Swift layer imports | | `tools/apple/MATTelemetry-umbrella.h` | Umbrella over the `ODW*.h` headers baked into the xcframework | | `tools/apple/MATTelemetryAvailability.json` | Build-time optional-module manifest consumed by `Package.swift` so Swift sources match the xcframework contents | @@ -69,19 +69,20 @@ before the release is cut). ## Validation performed -- `tools/apple/build-xcframework.sh release` builds the iOS device and simulator - slices, plus a universal macOS slice, and prints the SPM checksum. +- `tools/apple/build-xcframework.sh release` builds the iOS device, iOS + simulator, Mac Catalyst, and macOS slices, and prints the SPM checksum. - `swift build` validates local macOS SwiftPM consumption. - `xcodebuild -scheme OneDSSwift -destination 'generic/platform=iOS Simulator' build` - validates local SwiftPM consumption. -- A small Obj-C module/static-link smoke test was built and run on an iOS - Simulator. + validates iOS Simulator SwiftPM consumption. +- `xcodebuild -scheme OneDSSwift -destination 'platform=macOS,variant=Mac Catalyst' build` + validates Mac Catalyst SwiftPM consumption. +- Small Obj-C module/static-link smoke tests validate binary module linkability. ## Known gaps / TODO -- **Catalyst / visionOS slices** — iOS device, iOS simulator, and macOS are wired - up in this first pass; Catalyst and visionOS still need separate slice wiring - and validation. +- **visionOS slices** — iOS device, iOS simulator, Mac Catalyst, and macOS are + wired up in this first pass; visionOS still needs separate slice wiring and + validation. - **Code signing** — release xcframeworks are typically signed; add a signing step before zipping for distribution. - **Release workflow validation** — exercise `.github/workflows/spm-release.yml` diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 85d272d3d..961e8a0e3 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -14,10 +14,11 @@ # build/apple/MATTelemetry.xcframework.zip (+ prints the SPM checksum) # # Slices built here: iOS device (arm64), iOS simulator (arm64 + x86_64 fat), -# and macOS (arm64 + x86_64 universal). +# Mac Catalyst (arm64 + x86_64 fat), and macOS (arm64 + x86_64 universal). # # NOTE: this is a first-pass scaffold. It has been validated on macOS for iOS -# device, simulator, and macOS slices; Catalyst/visionOS slices are still TODO. +# device, simulator, Mac Catalyst, and macOS slices; visionOS slices are still +# TODO. set -euo pipefail @@ -133,6 +134,8 @@ build_slice() { # clean-arg arch platform out-subdir build_slice clean arm64 iphoneos ios-arm64 build_slice "" arm64 iphonesimulator ios-arm64-sim build_slice "" x86_64 iphonesimulator ios-x86_64-sim +build_slice "" arm64 maccatalyst maccatalyst-arm64 +build_slice "" x86_64 maccatalyst maccatalyst-x86_64 # Fat simulator archive (arm64 + x86_64) -- a single xcframework slice cannot # mix device and simulator, but it can contain multiple archs for one platform. @@ -140,6 +143,12 @@ mkdir -p "$OUT/ios-simulator" lipo -create "$OUT/ios-arm64-sim/$LIB" "$OUT/ios-x86_64-sim/$LIB" \ -output "$OUT/ios-simulator/$LIB" +# Fat Catalyst archive (arm64 + x86_64), emitted as a separate platform variant +# from both iOS simulator and native macOS. +mkdir -p "$OUT/maccatalyst" +lipo -create "$OUT/maccatalyst-arm64/$LIB" "$OUT/maccatalyst-x86_64/$LIB" \ + -output "$OUT/maccatalyst/$LIB" + # Native universal macOS archive. Build only the `mat` target in an isolated # CMake build directory so switching away from the iOS toolchain does not # disturb the already-copied iOS archives. @@ -167,6 +176,7 @@ rm -rf "$OUT/MATTelemetry.xcframework" xcodebuild -create-xcframework \ -library "$OUT/ios-arm64/$LIB" -headers "$HDRS" \ -library "$OUT/ios-simulator/$LIB" -headers "$HDRS" \ + -library "$OUT/maccatalyst/$LIB" -headers "$HDRS" \ -library "$OUT/macos-universal/$LIB" -headers "$HDRS" \ -output "$OUT/MATTelemetry.xcframework" echo "Created $OUT/MATTelemetry.xcframework" From 1d89f6d463a9b80067cc706e94b64f4e0ef6a28e Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 01:28:10 -0500 Subject: [PATCH 11/33] Add visionOS slices to SPM xcframework Build visionOS device and simulator archives, advertise visionOS in the Swift package, and document the expanded validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CMakeLists.txt | 12 ++++++++++-- Package.swift | 19 ++++++++++--------- tools/apple/README.md | 8 ++++---- tools/apple/build-xcframework.sh | 12 +++++++++--- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d117cb42..b67461ba2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,7 +57,7 @@ if(APPLE) if (${IOS_PLAT} STREQUAL "iphonesimulator") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mios-simulator-version-min=${IOS_DEPLOYMENT_TARGET}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mios-simulator-version-min=${IOS_DEPLOYMENT_TARGET}") - elseif(NOT ${IOS_PLAT} STREQUAL "maccatalyst") + elseif(${IOS_PLAT} STREQUAL "iphoneos") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -miphoneos-version-min=${IOS_DEPLOYMENT_TARGET}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -miphoneos-version-min=${IOS_DEPLOYMENT_TARGET}") endif() @@ -67,7 +67,15 @@ if(APPLE) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -target ${IOS_ARCH}-apple-ios${IOS_DEPLOYMENT_TARGET}-macabi") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -target ${IOS_ARCH}-apple-ios${IOS_DEPLOYMENT_TARGET}-macabi") set(IOS_PLATFORM "macosx") - elseif((${IOS_PLAT} STREQUAL "iphoneos") OR (${IOS_PLAT} STREQUAL "iphonesimulator") OR (${IOS_PLAT} STREQUAL "xros") OR (${IOS_PLAT} STREQUAL "xrsimulator")) + elseif(${IOS_PLAT} STREQUAL "xros") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -target ${IOS_ARCH}-apple-xros${IOS_DEPLOYMENT_TARGET}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -target ${IOS_ARCH}-apple-xros${IOS_DEPLOYMENT_TARGET}") + set(IOS_PLATFORM "${IOS_PLAT}") + elseif(${IOS_PLAT} STREQUAL "xrsimulator") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -target ${IOS_ARCH}-apple-xros${IOS_DEPLOYMENT_TARGET}-simulator") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -target ${IOS_ARCH}-apple-xros${IOS_DEPLOYMENT_TARGET}-simulator") + set(IOS_PLATFORM "${IOS_PLAT}") + elseif((${IOS_PLAT} STREQUAL "iphoneos") OR (${IOS_PLAT} STREQUAL "iphonesimulator")) set(IOS_PLATFORM "${IOS_PLAT}") else() message(FATAL_ERROR "Unrecognized iOS platform '${IOS_PLAT}'") diff --git a/Package.swift b/Package.swift index 6144610d0..e7b4e56bd 100644 --- a/Package.swift +++ b/Package.swift @@ -17,9 +17,9 @@ // Local development: // 1. Run `tools/apple/build-xcframework.sh release` on macOS with Xcode. // It produces ./build/apple/MATTelemetry.xcframework. -// 2. `swift build` validates macOS consumption; for iOS / Mac Catalyst, add -// this package as a local dependency or build the package with the desired -// Xcode destination. +// 2. `swift build` validates macOS consumption; for iOS / Mac Catalyst / +// visionOS, add this package as a local dependency or build the package +// with the desired Xcode destination. // // Release distribution (so consumers can add the repo by URL in Xcode): // 1. Build the xcframework, zip it, and attach it to the GitHub Release. @@ -86,6 +86,7 @@ let package = Package( .iOS(.v12), .macCatalyst(.v14), .macOS(.v10_15), + .visionOS(.v1), ], products: [ .library(name: "OneDSSwift", targets: ["OneDSSwift"]), @@ -120,13 +121,13 @@ let package = Package( .linkedLibrary("c++"), .linkedLibrary("sqlite3"), .linkedLibrary("z"), - .linkedFramework("CFNetwork", .when(platforms: [.iOS, .macCatalyst, .macOS])), - .linkedFramework("CoreFoundation", .when(platforms: [.iOS, .macCatalyst, .macOS])), - .linkedFramework("Foundation", .when(platforms: [.iOS, .macCatalyst, .macOS])), - .linkedFramework("Network", .when(platforms: [.iOS, .macCatalyst, .macOS])), - .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macCatalyst, .macOS])), + .linkedFramework("CFNetwork", .when(platforms: [.iOS, .macCatalyst, .macOS, .visionOS])), + .linkedFramework("CoreFoundation", .when(platforms: [.iOS, .macCatalyst, .macOS, .visionOS])), + .linkedFramework("Foundation", .when(platforms: [.iOS, .macCatalyst, .macOS, .visionOS])), + .linkedFramework("Network", .when(platforms: [.iOS, .macCatalyst, .macOS, .visionOS])), + .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macCatalyst, .macOS, .visionOS])), .linkedFramework("IOKit", .when(platforms: [.macOS])), - .linkedFramework("UIKit", .when(platforms: [.iOS, .macCatalyst])), + .linkedFramework("UIKit", .when(platforms: [.iOS, .macCatalyst, .visionOS])), ]), ] ) diff --git a/tools/apple/README.md b/tools/apple/README.md index ca13f40a3..2fed02012 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -70,19 +70,19 @@ before the release is cut). ## Validation performed - `tools/apple/build-xcframework.sh release` builds the iOS device, iOS - simulator, Mac Catalyst, and macOS slices, and prints the SPM checksum. + simulator, Mac Catalyst, visionOS device, visionOS simulator, and macOS + slices, and prints the SPM checksum. - `swift build` validates local macOS SwiftPM consumption. - `xcodebuild -scheme OneDSSwift -destination 'generic/platform=iOS Simulator' build` validates iOS Simulator SwiftPM consumption. - `xcodebuild -scheme OneDSSwift -destination 'platform=macOS,variant=Mac Catalyst' build` validates Mac Catalyst SwiftPM consumption. +- `xcodebuild -scheme OneDSSwift -destination 'generic/platform=visionOS Simulator' build` + validates visionOS Simulator SwiftPM consumption. - Small Obj-C module/static-link smoke tests validate binary module linkability. ## Known gaps / TODO -- **visionOS slices** — iOS device, iOS simulator, Mac Catalyst, and macOS are - wired up in this first pass; visionOS still needs separate slice wiring and - validation. - **Code signing** — release xcframeworks are typically signed; add a signing step before zipping for distribution. - **Release workflow validation** — exercise `.github/workflows/spm-release.yml` diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 961e8a0e3..023fe7d3f 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -14,11 +14,11 @@ # build/apple/MATTelemetry.xcframework.zip (+ prints the SPM checksum) # # Slices built here: iOS device (arm64), iOS simulator (arm64 + x86_64 fat), -# Mac Catalyst (arm64 + x86_64 fat), and macOS (arm64 + x86_64 universal). +# Mac Catalyst (arm64 + x86_64 fat), visionOS device/simulator (arm64), and +# macOS (arm64 + x86_64 universal). # # NOTE: this is a first-pass scaffold. It has been validated on macOS for iOS -# device, simulator, Mac Catalyst, and macOS slices; visionOS slices are still -# TODO. +# device, simulator, Mac Catalyst, visionOS, and macOS slices. set -euo pipefail @@ -137,6 +137,10 @@ build_slice "" x86_64 iphonesimulator ios-x86_64-sim build_slice "" arm64 maccatalyst maccatalyst-arm64 build_slice "" x86_64 maccatalyst maccatalyst-x86_64 +# visionOS uses a different CMake system name, so start it from a fresh cache. +build_slice clean arm64 xros visionos-arm64 +build_slice "" arm64 xrsimulator visionos-arm64-sim + # Fat simulator archive (arm64 + x86_64) -- a single xcframework slice cannot # mix device and simulator, but it can contain multiple archs for one platform. mkdir -p "$OUT/ios-simulator" @@ -177,6 +181,8 @@ xcodebuild -create-xcframework \ -library "$OUT/ios-arm64/$LIB" -headers "$HDRS" \ -library "$OUT/ios-simulator/$LIB" -headers "$HDRS" \ -library "$OUT/maccatalyst/$LIB" -headers "$HDRS" \ + -library "$OUT/visionos-arm64/$LIB" -headers "$HDRS" \ + -library "$OUT/visionos-arm64-sim/$LIB" -headers "$HDRS" \ -library "$OUT/macos-universal/$LIB" -headers "$HDRS" \ -output "$OUT/MATTelemetry.xcframework" echo "Created $OUT/MATTelemetry.xcframework" From 024684724006f8be8d835e104bbd54ca00c3afe7 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 01:36:56 -0500 Subject: [PATCH 12/33] Address SPM release review comments Clarify release automation comments, make the checksum workflow step unambiguous, and ensure forced xcframework CMake flags cannot be overridden by caller-provided CMAKE_OPTS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/spm-release.yml | 3 ++- Package.swift | 9 ++++----- tools/apple/build-xcframework.sh | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/spm-release.yml b/.github/workflows/spm-release.yml index 28eb1f414..6c129b4bd 100644 --- a/.github/workflows/spm-release.yml +++ b/.github/workflows/spm-release.yml @@ -92,7 +92,8 @@ jobs: if: ${{ steps.ver.outputs.skip != 'true' }} run: | set -euo pipefail - echo "checksum=$(swift package compute-checksum "build/apple/$ARTIFACT")" >> "$GITHUB_OUTPUT" + checksum="$(swift package compute-checksum "build/apple/$ARTIFACT")" + echo "checksum=$checksum" >> "$GITHUB_OUTPUT" - name: Upload xcframework to the release if: ${{ steps.ver.outputs.skip != 'true' }} diff --git a/Package.swift b/Package.swift index e7b4e56bd..1a1806ccb 100644 --- a/Package.swift +++ b/Package.swift @@ -22,11 +22,10 @@ // with the desired Xcode destination. // // Release distribution (so consumers can add the repo by URL in Xcode): -// 1. Build the xcframework, zip it, and attach it to the GitHub Release. -// 2. Run `swift package compute-checksum MATTelemetry.xcframework.zip`. -// 3. Replace the `.binaryTarget(... path:)` below with the `url:`+`checksum:` -// form shown in the comment. The vcpkg-release-bump workflow pattern can be -// extended to automate steps 1-3 on each release tag. +// .github/workflows/spm-release.yml builds and uploads the xcframework, +// computes the checksum, rewrites the local `.binaryTarget(... path:)` below +// to `url:`+`checksum:`, and pushes the 3-component SemVer tag that SPM can +// resolve. import PackageDescription import Foundation diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 023fe7d3f..406ed7513 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -38,7 +38,7 @@ esac # Force a STATIC libmat that includes the Obj-C wrappers, regardless of the # repo's default library type. -export CMAKE_OPTS="-DBUILD_SHARED_LIBS=OFF -DBUILD_OBJC_WRAPPER=YES ${CMAKE_OPTS:-}" +export CMAKE_OPTS="${CMAKE_OPTS:-} -DBUILD_SHARED_LIBS=OFF -DBUILD_OBJC_WRAPPER=YES" rm -rf "$OUT" mkdir -p "$OUT" From 9634894be8bc3e2b216b683e86a3decd5db5d4e4 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 01:46:12 -0500 Subject: [PATCH 13/33] Address follow-up SPM review comments Update the package comment for optional Swift sources, fail early for unsupported Apple platforms, and skip SPM tag publishing when the release tag already exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/spm-release.yml | 4 ++++ Package.swift | 6 ++---- build-ios.sh | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/spm-release.yml b/.github/workflows/spm-release.yml index 6c129b4bd..bd485b04f 100644 --- a/.github/workflows/spm-release.yml +++ b/.github/workflows/spm-release.yml @@ -130,6 +130,10 @@ jobs: if: ${{ steps.ver.outputs.skip != 'true' }} run: | set -euo pipefail + if git ls-remote --exit-code --tags origin "refs/tags/${{ steps.ver.outputs.spm_version }}" >/dev/null; then + echo "::notice::SPM tag ${{ steps.ver.outputs.spm_version }} already exists; skipping tag publish." + exit 0 + fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add Package.swift tools/apple/MATTelemetryAvailability.json diff --git a/Package.swift b/Package.swift index 1a1806ccb..c7dc5346d 100644 --- a/Package.swift +++ b/Package.swift @@ -106,10 +106,8 @@ let package = Package( path: "build/apple/MATTelemetry.xcframework"), // Thin Swift API layer (source). Depends on the Obj-C module from the - // xcframework. NOTE: the conditional source exclusions in - // wrappers/swift/Package.swift (PrivacyGuard / Sanitizer / DataViewer - // when those private modules aren't built) should be carried over here - // and kept in sync with the headers baked into the xcframework. + // xcframework. The conditional source exclusions above must stay in sync + // with the headers baked into the xcframework. .target( name: "OneDSSwift", dependencies: ["MATTelemetry"], diff --git a/build-ios.sh b/build-ios.sh index 731572865..8fb50094e 100755 --- a/build-ios.sh +++ b/build-ios.sh @@ -76,6 +76,9 @@ elif [ "$IOS_PLAT" == "xros" ] || [ "$IOS_PLAT" == "xrsimulator" ]; then DEPLOYMENT_TARGET="1.0" FORCE_RESET_DEPLOYMENT_TARGET=YES fi +else + echo "ERROR: unsupported Apple platform '$IOS_PLAT'. Expected iphoneos, iphonesimulator, maccatalyst, xros, or xrsimulator." 1>&2 + exit 1 fi echo "deployment target = $DEPLOYMENT_TARGET" From 5923e5cfed97e943fdeeacca6caf970041ca7bad Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 01:53:42 -0500 Subject: [PATCH 14/33] Validate SPM Apple platforms in release workflow Run SwiftPM and Xcode package builds for macOS, iOS Simulator, Mac Catalyst, and visionOS Simulator after producing the xcframework artifact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/spm-release.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/spm-release.yml b/.github/workflows/spm-release.yml index bd485b04f..ac06c2649 100644 --- a/.github/workflows/spm-release.yml +++ b/.github/workflows/spm-release.yml @@ -37,7 +37,7 @@ jobs: if: >- ${{ github.event_name == 'workflow_dispatch' || (github.event.release.draft == false && github.event.release.prerelease == false) }} - runs-on: macos-14 # provides Xcode (xcodebuild, swift) + runs-on: macos-15 # provides Xcode with Apple platform SDKs (xcodebuild, swift) env: ARTIFACT: MATTelemetry.xcframework.zip steps: @@ -87,6 +87,15 @@ jobs: tools/apple/build-xcframework.sh release test -f "build/apple/$ARTIFACT" + - name: Validate SwiftPM package consumption + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + swift build + xcodebuild -scheme OneDSSwift -destination 'generic/platform=iOS Simulator' build + xcodebuild -scheme OneDSSwift -destination 'platform=macOS,variant=Mac Catalyst' build + xcodebuild -scheme OneDSSwift -destination 'generic/platform=visionOS Simulator' build + - name: Compute SPM checksum id: sum if: ${{ steps.ver.outputs.skip != 'true' }} From ccceaa5fed519193ba4d56376c80803ee19d9e49 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 02:09:05 -0500 Subject: [PATCH 15/33] Skip package creation for xcframework slices Allow build-ios.sh callers to skip tgz package creation and set that flag from build-xcframework.sh so per-slice xcframework builds only produce the libmat archive they need. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build-ios.sh | 6 +++++- tools/apple/build-xcframework.sh | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build-ios.sh b/build-ios.sh index 8fb50094e..c3da652e7 100755 --- a/build-ios.sh +++ b/build-ios.sh @@ -111,4 +111,8 @@ eval $cmake_cmd make -make package +if [ "${MATTELEMETRY_SKIP_PACKAGE:-}" == "1" ]; then + echo "MATTELEMETRY_SKIP_PACKAGE=1: skipping package creation" +else + make package +fi diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 406ed7513..036c29c53 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -126,7 +126,7 @@ build_slice() { # clean-arg arch platform out-subdir shift local arch="$1" plat="$2" sub="$3" echo "=== building $arch / $plat ($CONFIG) ===" - ( cd "$ROOT" && ./build-ios.sh $clean_arg "$CONFIG" "$arch" "$plat" ) + ( cd "$ROOT" && MATTELEMETRY_SKIP_PACKAGE=1 ./build-ios.sh $clean_arg "$CONFIG" "$arch" "$plat" ) mkdir -p "$OUT/$sub" cp "$ROOT/out/lib/$LIB" "$OUT/$sub/$LIB" } From 5c6f57414336b6b28c7bf4f69aedacb41b2d310a Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 02:22:13 -0500 Subject: [PATCH 16/33] Clean up Apple SPM README Refresh the prototype documentation to match the current multi-platform xcframework, release workflow, validation coverage, and remaining gaps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/apple/README.md | 143 +++++++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 63 deletions(-) diff --git a/tools/apple/README.md b/tools/apple/README.md index 2fed02012..9c791e48b 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -1,89 +1,106 @@ -# Swift Package Manager (xcframework) — prototype +# Swift Package Manager xcframework prototype -**Status: validated prototype.** This is a first-pass scaffold for distributing -the 1DS C++ SDK to Apple app developers via **Swift Package Manager (SPM)**, the -successor to CocoaPods (the CocoaPods trunk goes read-only on 2 Dec 2026, and -there is no official in-repo podspec today). +**Status: validated prototype.** This packages the 1DS C++ SDK for Apple +developers through Swift Package Manager (SPM), using a prebuilt xcframework for +the C++ core plus Obj-C wrappers and compiling the Swift API from source. -## Approach +## Package shape -SPM cannot practically compile this SDK's C++ tree from source (CMake build, -Bond codegen, vendored sqlite3/zlib, heavy platform conditionals). So: +SPM is not a good fit for compiling this SDK's full C++ tree directly because +the SDK depends on CMake, Bond codegen, vendored sqlite3/zlib, and platform +conditionals. Instead, the package is split into: -| Layer | How it ships | +| Layer | Packaging | | --- | --- | -| C++ core + Obj-C wrappers (`ODW*`) | **Prebuilt binary** — `MATTelemetry.xcframework` (`.binaryTarget`) | -| Swift API (`OneDSSwift`) | **Source** — `wrappers/swift/Sources/OneDSSwift`, depends on the Obj-C module from the xcframework | +| C++ core + Obj-C wrappers (`ODW*`) | `MATTelemetry.xcframework` binary target | +| Swift API (`OneDSSwift`) | Source target in `wrappers/swift/Sources/OneDSSwift` | -The Obj-C wrappers already compile into `libmat.a` on Apple -(`lib/CMakeLists.txt:217`), and the Swift sources already `import ObjCModule`, -so the xcframework just needs to vend a Clang module named `ObjCModule` -(`tools/apple/module.modulemap` + `MATTelemetry-umbrella.h`). +The xcframework vendors a Clang module named `ObjCModule` through +`tools/apple/module.modulemap` and `MATTelemetry-umbrella.h`, matching the +existing Swift sources' `import ObjCModule`. -## Files +## Supported slices + +`tools/apple/build-xcframework.sh release` builds: + +| Platform | Slice | +| --- | --- | +| iOS device | `ios-arm64` | +| iOS Simulator | `ios-arm64_x86_64-simulator` | +| Mac Catalyst | `ios-arm64_x86_64-maccatalyst` | +| macOS | `macos-arm64_x86_64` | +| visionOS device | `xros-arm64` | +| visionOS Simulator | `xros-arm64-simulator` | + +## Important files | File | Purpose | | --- | --- | -| `Package.swift` (repo root) | Distributable SPM manifest: `binaryTarget` (xcframework) + `OneDSSwift` source target | -| `tools/apple/build-xcframework.sh` | Builds a static `libmat.a` per Apple slice, lipo's the simulator/Catalyst/macOS archs where needed, and assembles the xcframework with `xcodebuild -create-xcframework` | -| `tools/apple/module.modulemap` | Defines the `ObjCModule` Clang module the Swift layer imports | -| `tools/apple/MATTelemetry-umbrella.h` | Umbrella over the `ODW*.h` headers baked into the xcframework | -| `tools/apple/MATTelemetryAvailability.json` | Build-time optional-module manifest consumed by `Package.swift` so Swift sources match the xcframework contents | +| `Package.swift` | Root SPM manifest: binary target + Swift source target | +| `tools/apple/build-xcframework.sh` | Builds static `libmat.a` slices and assembles `MATTelemetry.xcframework` | +| `tools/apple/module.modulemap` | Defines the `ObjCModule` Clang module | +| `tools/apple/MATTelemetry-umbrella.h` | Base umbrella for always-available Obj-C wrapper headers | +| `tools/apple/MATTelemetryAvailability.json` | Optional-module manifest consumed by `Package.swift` | +| `.github/workflows/spm-release.yml` | Release automation for the hosted xcframework and SPM tag | + +## Local build -## Build (on macOS) +Run on macOS with Xcode and CMake: ```bash tools/apple/build-xcframework.sh release # -> build/apple/MATTelemetry.xcframework -# -> build/apple/MATTelemetry.xcframework.zip (+ prints the SPM checksum) -swift build # resolves Package.swift against the local xcframework +# -> build/apple/MATTelemetry.xcframework.zip +# -> prints the SwiftPM checksum ``` -## Consume +For local development, `Package.swift` points at +`build/apple/MATTelemetry.xcframework`. `swift build` validates macOS +consumption; use Xcode destinations for iOS Simulator, Mac Catalyst, and +visionOS Simulator. -- **Local:** point a sample app at this package directory (path dependency). -- **Released:** in Xcode *File -> Add Package Dependencies...*, enter the repo - URL and pick a version. SPM only accepts **3-component SemVer**, and the SDK's - own `vX.Y.Z.W` tags are not valid SemVer, so consumers pin the **parallel - 3-component tag** the release workflow publishes: +## Consumption - ```swift - .package(url: "https://github.com/microsoft/cpp_client_telemetry.git", from: "3.10.161") - ``` +- **Local:** add this repository as a local package dependency after building + `build/apple/MATTelemetry.xcframework`. +- **Released:** add the repository URL in Xcode and pin the parallel + 3-component SemVer tag published by the release workflow: -## Release wiring +```swift +.package(url: "https://github.com/microsoft/cpp_client_telemetry.git", from: "3.10.161") +``` + +The SDK's native release tags are 4-component (`vX.Y.Z.W`), which SPM does not +accept as SemVer. The release workflow publishes the corresponding `X.Y.Z` tag. + +## Release workflow -`.github/workflows/spm-release.yml` automates distribution on each published -release (a 4-component `vX.Y.Z.W` tag). On a macOS runner it: +`.github/workflows/spm-release.yml` runs for published SDK releases and manual +dispatch. It: -1. Builds `MATTelemetry.xcframework` and zips it. -2. Uploads the zip to the GitHub Release. -3. Computes the SPM checksum and rewrites the `Package.swift` `binaryTarget` - from `path:` to `url:`+`checksum:`. -4. Commits that manifest and pushes a **3-component SemVer tag** (`X.Y.Z`, - derived by dropping the trailing build component) that SPM can resolve. +1. Builds and zips `MATTelemetry.xcframework`. +2. Validates package consumption for macOS, iOS Simulator, Mac Catalyst, and + visionOS Simulator. +3. Uploads the zip to the GitHub Release. +4. Rewrites `Package.swift` from local `path:` to hosted `url:` + `checksum:`. +5. Commits the resolved manifest and pushes the 3-component SPM tag. -This mirrors the `vcpkg-release-bump` workflow. It requires the root -`Package.swift` to already exist at the release tag (i.e. this prototype merged -before the release is cut). +The private `lib/modules` submodule is intentionally not fetched by the release +workflow, so optional module headers and Swift sources are gated by +`MATTelemetryAvailability.json`. ## Validation performed -- `tools/apple/build-xcframework.sh release` builds the iOS device, iOS - simulator, Mac Catalyst, visionOS device, visionOS simulator, and macOS - slices, and prints the SPM checksum. -- `swift build` validates local macOS SwiftPM consumption. -- `xcodebuild -scheme OneDSSwift -destination 'generic/platform=iOS Simulator' build` - validates iOS Simulator SwiftPM consumption. -- `xcodebuild -scheme OneDSSwift -destination 'platform=macOS,variant=Mac Catalyst' build` - validates Mac Catalyst SwiftPM consumption. -- `xcodebuild -scheme OneDSSwift -destination 'generic/platform=visionOS Simulator' build` - validates visionOS Simulator SwiftPM consumption. -- Small Obj-C module/static-link smoke tests validate binary module linkability. - -## Known gaps / TODO - -- **Code signing** — release xcframeworks are typically signed; add a signing - step before zipping for distribution. -- **Release workflow validation** — exercise `.github/workflows/spm-release.yml` - end-to-end on an actual published release. +- Full xcframework build for iOS, iOS Simulator, Mac Catalyst, macOS, visionOS, + and visionOS Simulator. +- SwiftPM/Xcode builds for macOS, iOS Simulator, Mac Catalyst, visionOS + Simulator, and visionOS device. +- External TelemetryTest package consumer builds, including visionOS. +- Obj-C module/static-link smoke tests for representative binary slices. +- Apple Vision Pro simulator runtime installation and boot. + +## Known gaps + +- Release xcframework signing/notarization. +- End-to-end execution of `.github/workflows/spm-release.yml` on a real + published release. From 79f55faaff49f829ced5becefdda8c1a89037eae Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 02:24:19 -0500 Subject: [PATCH 17/33] Remove status label from Apple SPM README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/apple/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/apple/README.md b/tools/apple/README.md index 9c791e48b..a5a7eeddf 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -1,8 +1,8 @@ # Swift Package Manager xcframework prototype -**Status: validated prototype.** This packages the 1DS C++ SDK for Apple -developers through Swift Package Manager (SPM), using a prebuilt xcframework for -the C++ core plus Obj-C wrappers and compiling the Swift API from source. +This packages the 1DS C++ SDK for Apple developers through Swift Package Manager +(SPM), using a prebuilt xcframework for the C++ core plus Obj-C wrappers and +compiling the Swift API from source. ## Package shape From 27049fe003c5603ca9dcf2b9f64530cace354215 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 02:25:33 -0500 Subject: [PATCH 18/33] Assert SPM platforms in release workflow Validate the Package.swift platform list with swift package dump-package before running the platform-specific package builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/spm-release.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/spm-release.yml b/.github/workflows/spm-release.yml index ac06c2649..2fbd94ba1 100644 --- a/.github/workflows/spm-release.yml +++ b/.github/workflows/spm-release.yml @@ -91,6 +91,23 @@ jobs: if: ${{ steps.ver.outputs.skip != 'true' }} run: | set -euo pipefail + swift package dump-package > package.json + python3 - <<'PY' + import json + expected = { + "ios": "12.0", + "maccatalyst": "14.0", + "macos": "10.15", + "visionos": "1.0", + } + with open("package.json", encoding="utf-8") as f: + platforms = { + item["platformName"]: item["version"] + for item in json.load(f)["platforms"] + } + if platforms != expected: + raise SystemExit(f"Unexpected Package.swift platforms: {platforms}") + PY swift build xcodebuild -scheme OneDSSwift -destination 'generic/platform=iOS Simulator' build xcodebuild -scheme OneDSSwift -destination 'platform=macOS,variant=Mac Catalyst' build From 55c4ce5dc7f5e010551839ac90a6dd49788fce08 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 02:49:37 -0500 Subject: [PATCH 19/33] Clean up xcframework build script comments Remove a drifting line-number reference and keep section headings sequential. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/apple/build-xcframework.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 036c29c53..30dd95c1c 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -25,7 +25,7 @@ set -euo pipefail CONFIG="${1:-release}" ROOT="$(cd "$(dirname "$0")/../.." && pwd)" OUT="$ROOT/build/apple" -LIB="libmat.a" # mat target; the Obj-C wrappers compile into it (lib/CMakeLists.txt:217) +LIB="libmat.a" # mat target; the Obj-C wrappers compile into it. case "$CONFIG" in release) CMAKE_BUILD_TYPE="Release" ;; @@ -175,7 +175,7 @@ cmake --build "$MACOS_BUILD" --target mat mkdir -p "$OUT/macos-universal" cp "$MACOS_BUILD/lib/$LIB" "$OUT/macos-universal/$LIB" -# --- 4. Assemble the xcframework --------------------------------------------- +# --- 3. Assemble the xcframework --------------------------------------------- rm -rf "$OUT/MATTelemetry.xcframework" xcodebuild -create-xcframework \ -library "$OUT/ios-arm64/$LIB" -headers "$HDRS" \ @@ -187,7 +187,7 @@ xcodebuild -create-xcframework \ -output "$OUT/MATTelemetry.xcframework" echo "Created $OUT/MATTelemetry.xcframework" -# --- 5. Zip + checksum for release distribution ------------------------------ +# --- 4. Zip + checksum for release distribution ------------------------------ ( cd "$OUT" && rm -f MATTelemetry.xcframework.zip \ && zip -qry MATTelemetry.xcframework.zip MATTelemetry.xcframework ) echo "Zipped: $OUT/MATTelemetry.xcframework.zip" From 9a18330a8fa5ca536070edb9f603fac9a0fdaa53 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 03:21:34 -0500 Subject: [PATCH 20/33] Use fresh build cache for each xcframework slice Build each Apple slice from a clean CMake out directory, while preserving the build-tools marker, and restrict xcframework slice builds to the libmat archive target inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/apple/build-xcframework.sh | 42 +++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 30dd95c1c..00af27b1d 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -36,9 +36,17 @@ case "$CONFIG" in ;; esac -# Force a STATIC libmat that includes the Obj-C wrappers, regardless of the -# repo's default library type. -export CMAKE_OPTS="${CMAKE_OPTS:-} -DBUILD_SHARED_LIBS=OFF -DBUILD_OBJC_WRAPPER=YES" +# Build only the static libmat archive with Obj-C wrappers; slice builds do not +# need the repo's test, Swift wrapper, or package targets. +CMAKE_OPTS="${CMAKE_OPTS:-}" +CMAKE_OPTS="$CMAKE_OPTS -DBUILD_SHARED_LIBS=OFF" +CMAKE_OPTS="$CMAKE_OPTS -DBUILD_OBJC_WRAPPER=YES" +CMAKE_OPTS="$CMAKE_OPTS -DBUILD_TEST_TOOL=OFF" +CMAKE_OPTS="$CMAKE_OPTS -DBUILD_UNIT_TESTS=OFF" +CMAKE_OPTS="$CMAKE_OPTS -DBUILD_FUNC_TESTS=OFF" +CMAKE_OPTS="$CMAKE_OPTS -DBUILD_SWIFT_WRAPPER=OFF" +CMAKE_OPTS="$CMAKE_OPTS -DBUILD_PACKAGE=OFF" +export CMAKE_OPTS rm -rf "$OUT" mkdir -p "$OUT" @@ -121,25 +129,26 @@ cp "$ROOT"/tools/apple/MATTelemetry-umbrella.h "$HDRS/" } >> "$HDRS/MATTelemetry-umbrella.h" # --- 2. Build one static lib per (arch, platform) ---------------------------- -build_slice() { # clean-arg arch platform out-subdir - local clean_arg="$1" - shift +build_slice() { # arch platform out-subdir local arch="$1" plat="$2" sub="$3" echo "=== building $arch / $plat ($CONFIG) ===" - ( cd "$ROOT" && MATTELEMETRY_SKIP_PACKAGE=1 ./build-ios.sh $clean_arg "$CONFIG" "$arch" "$plat" ) + ( + cd "$ROOT" + rm -f CMakeCache.txt *.cmake + rm -rf out + MATTELEMETRY_SKIP_PACKAGE=1 ./build-ios.sh "$CONFIG" "$arch" "$plat" + ) mkdir -p "$OUT/$sub" cp "$ROOT/out/lib/$LIB" "$OUT/$sub/$LIB" } -build_slice clean arm64 iphoneos ios-arm64 -build_slice "" arm64 iphonesimulator ios-arm64-sim -build_slice "" x86_64 iphonesimulator ios-x86_64-sim -build_slice "" arm64 maccatalyst maccatalyst-arm64 -build_slice "" x86_64 maccatalyst maccatalyst-x86_64 - -# visionOS uses a different CMake system name, so start it from a fresh cache. -build_slice clean arm64 xros visionos-arm64 -build_slice "" arm64 xrsimulator visionos-arm64-sim +build_slice arm64 iphoneos ios-arm64 +build_slice arm64 iphonesimulator ios-arm64-sim +build_slice x86_64 iphonesimulator ios-x86_64-sim +build_slice arm64 maccatalyst maccatalyst-arm64 +build_slice x86_64 maccatalyst maccatalyst-x86_64 +build_slice arm64 xros visionos-arm64 +build_slice arm64 xrsimulator visionos-arm64-sim # Fat simulator archive (arm64 + x86_64) -- a single xcframework slice cannot # mix device and simulator, but it can contain multiple archs for one platform. @@ -169,7 +178,6 @@ cmake -S "$ROOT" -B "$MACOS_BUILD" \ -DBUILD_UNIT_TESTS=OFF \ -DBUILD_FUNC_TESTS=OFF \ -DBUILD_SWIFT_WRAPPER=OFF \ - -DBUILD_PACKAGE=OFF \ $CMAKE_OPTS cmake --build "$MACOS_BUILD" --target mat mkdir -p "$OUT/macos-universal" From 279ffd39a15e28b1645c83395842e99a1e403e8d Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 03:47:18 -0500 Subject: [PATCH 21/33] Use portable shell comparison in iOS build Use POSIX '=' for the MATTELEMETRY_SKIP_PACKAGE check in build-ios.sh. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build-ios.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-ios.sh b/build-ios.sh index c3da652e7..d96b93bc4 100755 --- a/build-ios.sh +++ b/build-ios.sh @@ -111,7 +111,7 @@ eval $cmake_cmd make -if [ "${MATTELEMETRY_SKIP_PACKAGE:-}" == "1" ]; then +if [ "${MATTELEMETRY_SKIP_PACKAGE:-}" = "1" ]; then echo "MATTELEMETRY_SKIP_PACKAGE=1: skipping package creation" else make package From 7bc7415c175addd5953992e7ad05421af01087dc Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 11:26:25 -0500 Subject: [PATCH 22/33] Quote ${IOS_PLAT}/${IOS_ARCH} in Apple if() conditions Addresses Copilot review on #1486 (CMakeLists.txt:70, :108): the new Apple-slice platform conditionals used unquoted ${IOS_PLAT} (and the adjacent ${IOS_ARCH}) inside if()/elseif(). If the variable is ever empty/undefined, `if(${IOS_PLAT} STREQUAL "...")` expands to `if( STREQUAL "...")`, which is a hard CMake parse error that aborts configuration. Quoting the expansion ("${IOS_PLAT}") keeps the compare well-formed (empty -> false) and matches the existing `if("${MAC_ARCH}" STREQUAL ...)` idiom already used in this file. Quoted all 7 ${IOS_PLAT} comparisons (lines 57/60/66/70/74/78/104) and the 3 ${IOS_ARCH} comparisons (84/88/92). ${CMAKE_SYSTEM_NAME} (line 313) is left as-is: it is always defined by CMake, so it cannot trigger the empty-expansion parse error. Verified with a minimal CMake repro: the quoted form configures cleanly with the variable undefined and still matches when set, whereas the old unquoted form errors out at configure time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CMakeLists.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b67461ba2..28d43fb7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,42 +54,42 @@ if(APPLE) if(FORCE_RESET_OSX_DEPLOYMENT_TARGET) set(CMAKE_OSX_DEPLOYMENT_TARGET "" CACHE STRING "Force unset of the deployment target for iOS" FORCE) - if (${IOS_PLAT} STREQUAL "iphonesimulator") + if ("${IOS_PLAT}" STREQUAL "iphonesimulator") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mios-simulator-version-min=${IOS_DEPLOYMENT_TARGET}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mios-simulator-version-min=${IOS_DEPLOYMENT_TARGET}") - elseif(${IOS_PLAT} STREQUAL "iphoneos") + elseif("${IOS_PLAT}" STREQUAL "iphoneos") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -miphoneos-version-min=${IOS_DEPLOYMENT_TARGET}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -miphoneos-version-min=${IOS_DEPLOYMENT_TARGET}") endif() endif() - if(${IOS_PLAT} STREQUAL "maccatalyst") + if("${IOS_PLAT}" STREQUAL "maccatalyst") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -target ${IOS_ARCH}-apple-ios${IOS_DEPLOYMENT_TARGET}-macabi") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -target ${IOS_ARCH}-apple-ios${IOS_DEPLOYMENT_TARGET}-macabi") set(IOS_PLATFORM "macosx") - elseif(${IOS_PLAT} STREQUAL "xros") + elseif("${IOS_PLAT}" STREQUAL "xros") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -target ${IOS_ARCH}-apple-xros${IOS_DEPLOYMENT_TARGET}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -target ${IOS_ARCH}-apple-xros${IOS_DEPLOYMENT_TARGET}") set(IOS_PLATFORM "${IOS_PLAT}") - elseif(${IOS_PLAT} STREQUAL "xrsimulator") + elseif("${IOS_PLAT}" STREQUAL "xrsimulator") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -target ${IOS_ARCH}-apple-xros${IOS_DEPLOYMENT_TARGET}-simulator") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -target ${IOS_ARCH}-apple-xros${IOS_DEPLOYMENT_TARGET}-simulator") set(IOS_PLATFORM "${IOS_PLAT}") - elseif((${IOS_PLAT} STREQUAL "iphoneos") OR (${IOS_PLAT} STREQUAL "iphonesimulator")) + elseif(("${IOS_PLAT}" STREQUAL "iphoneos") OR ("${IOS_PLAT}" STREQUAL "iphonesimulator")) set(IOS_PLATFORM "${IOS_PLAT}") else() message(FATAL_ERROR "Unrecognized iOS platform '${IOS_PLAT}'") endif() - if(${IOS_ARCH} STREQUAL "x86_64") + if("${IOS_ARCH}" STREQUAL "x86_64") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -arch x86_64") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -arch x86_64") set(CMAKE_SYSTEM_PROCESSOR x86_64) - elseif(${IOS_ARCH} STREQUAL "arm64") + elseif("${IOS_ARCH}" STREQUAL "arm64") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -arch arm64") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -arch arm64") set(CMAKE_SYSTEM_PROCESSOR arm64) - elseif(${IOS_ARCH} STREQUAL "arm64e") + elseif("${IOS_ARCH}" STREQUAL "arm64e") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -arch arm64e") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -arch arm64e") set(CMAKE_SYSTEM_PROCESSOR arm64e) @@ -101,7 +101,7 @@ if(APPLE) OUTPUT_VARIABLE CMAKE_OSX_SYSROOT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) - if(${IOS_PLAT} STREQUAL "maccatalyst") + if("${IOS_PLAT}" STREQUAL "maccatalyst") set(IOS_SUPPORT_FRAMEWORKS "${CMAKE_OSX_SYSROOT}/System/iOSSupport/System/Library/Frameworks") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -iframework ${IOS_SUPPORT_FRAMEWORKS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -iframework ${IOS_SUPPORT_FRAMEWORKS}") From 9d522660d0051b0b6454965be749961c0b5721be Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 12:10:01 -0500 Subject: [PATCH 23/33] Rename public Clang module ObjCModule -> MATTelemetryObjC The Obj-C wrapper module vended by MATTelemetry.xcframework (and the local wrappers/swift package) was named `ObjCModule` -- a generic name that consumers `import`. Once #1486 makes this module public via SPM, a generic name risks colliding with another binary/SPM package that also vends an `ObjCModule`, and pollutes the consumer's module namespace. Rename it to the namespaced `MATTelemetryObjC` while the name is still internal (the wrappers/swift package was never distributed), so it is collision-safe before first release. Renamed consistently across both build paths so the same Swift sources compile against both modulemaps: - tools/apple/module.modulemap (xcframework) and wrappers/swift/Modules/module.modulemap (local): `module MATTelemetryObjC`. - All 13 `import ObjCModule` -> `import MATTelemetryObjC` in wrappers/swift/Sources/OneDSSwift/*.swift. - Bridging header file renamed ObjCModule-Bridging-Header.h -> MATTelemetryObjC-Bridging-Header.h and its modulemap reference updated. - Comments/docs (Package.swift, build-xcframework.sh, tools/apple/README.md, examples/swift/README.md) updated. Pure rename; no behavioral change. Needs a macOS `swift build` (local wrappers path) + xcframework SPM build to confirm both paths still resolve the module -- I cannot run those on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Package.swift | 2 +- examples/swift/README.md | 2 +- tools/apple/README.md | 6 +++--- tools/apple/build-xcframework.sh | 2 +- tools/apple/module.modulemap | 8 ++++---- ...idging-Header.h => MATTelemetryObjC-Bridging-Header.h} | 0 wrappers/swift/Modules/module.modulemap | 4 ++-- wrappers/swift/Sources/OneDSSwift/CommonDataContext.swift | 2 +- .../swift/Sources/OneDSSwift/DiagnosticDataViewer.swift | 2 +- wrappers/swift/Sources/OneDSSwift/EventProperties.swift | 2 +- wrappers/swift/Sources/OneDSSwift/LogConfiguration.swift | 2 +- wrappers/swift/Sources/OneDSSwift/LogManager.swift | 2 +- wrappers/swift/Sources/OneDSSwift/Logger.swift | 2 +- wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift | 4 ++-- wrappers/swift/Sources/OneDSSwift/PrivacyGuard.swift | 2 +- .../swift/Sources/OneDSSwift/PrivacyGuardInitConfig.swift | 2 +- wrappers/swift/Sources/OneDSSwift/Sanitizer.swift | 2 +- .../swift/Sources/OneDSSwift/SanitizerInitConfig.swift | 2 +- wrappers/swift/Sources/OneDSSwift/SemanticContext.swift | 2 +- 19 files changed, 25 insertions(+), 25 deletions(-) rename wrappers/swift/Headers/{ObjCModule-Bridging-Header.h => MATTelemetryObjC-Bridging-Header.h} (100%) diff --git a/Package.swift b/Package.swift index c7dc5346d..bf8313af8 100644 --- a/Package.swift +++ b/Package.swift @@ -92,7 +92,7 @@ let package = Package( ], targets: [ // Prebuilt C++ core + Obj-C wrappers. The xcframework's bundled - // module map vends the Clang module `ObjCModule` (see + // module map vends the Clang module `MATTelemetryObjC` (see // tools/apple/module.modulemap), which the Swift layer imports. // // For a tagged release, swap the local path for the hosted artifact: diff --git a/examples/swift/README.md b/examples/swift/README.md index 2c7db6c3a..3f66816da 100644 --- a/examples/swift/README.md +++ b/examples/swift/README.md @@ -30,7 +30,7 @@ Details: - OneDSSwift: Package containing swift wrappers - Modules Included - - ObjCModule: Module exposing ObjC headers via module.modulemap file. + - MATTelemetryObjC: Module exposing ObjC headers via module.modulemap file. - Libraries and Frameworks to link to Target - [Same as mentioned in the SampleXcodeApp section](#to-be-linked) \ No newline at end of file diff --git a/tools/apple/README.md b/tools/apple/README.md index a5a7eeddf..c08dd4ac3 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -15,9 +15,9 @@ conditionals. Instead, the package is split into: | C++ core + Obj-C wrappers (`ODW*`) | `MATTelemetry.xcframework` binary target | | Swift API (`OneDSSwift`) | Source target in `wrappers/swift/Sources/OneDSSwift` | -The xcframework vendors a Clang module named `ObjCModule` through +The xcframework vendors a Clang module named `MATTelemetryObjC` through `tools/apple/module.modulemap` and `MATTelemetry-umbrella.h`, matching the -existing Swift sources' `import ObjCModule`. +existing Swift sources' `import MATTelemetryObjC`. ## Supported slices @@ -38,7 +38,7 @@ existing Swift sources' `import ObjCModule`. | --- | --- | | `Package.swift` | Root SPM manifest: binary target + Swift source target | | `tools/apple/build-xcframework.sh` | Builds static `libmat.a` slices and assembles `MATTelemetry.xcframework` | -| `tools/apple/module.modulemap` | Defines the `ObjCModule` Clang module | +| `tools/apple/module.modulemap` | Defines the `MATTelemetryObjC` Clang module | | `tools/apple/MATTelemetry-umbrella.h` | Base umbrella for always-available Obj-C wrapper headers | | `tools/apple/MATTelemetryAvailability.json` | Optional-module manifest consumed by `Package.swift` | | `.github/workflows/spm-release.yml` | Release automation for the hosted xcframework and SPM tag | diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 00af27b1d..21bb9add1 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -53,7 +53,7 @@ mkdir -p "$OUT" # --- 1. Public Obj-C headers + module map (vended by the xcframework) -------- # Flatten the ODW*.h headers + umbrella + modulemap into one Headers dir. The -# module is named `ObjCModule` to match what wrappers/swift sources import. +# module is named `MATTelemetryObjC` to match what wrappers/swift sources import. HDRS="$OUT/Headers" mkdir -p "$HDRS" diff --git a/tools/apple/module.modulemap b/tools/apple/module.modulemap index 0a2c0167a..ec2e84f22 100644 --- a/tools/apple/module.modulemap +++ b/tools/apple/module.modulemap @@ -1,9 +1,9 @@ // Clang module vended by MATTelemetry.xcframework. Imported by the OneDSSwift -// Swift layer as `import ObjCModule` -- the module name matches what the -// existing wrappers/swift/Sources/OneDSSwift sources already import, so no Swift -// source changes are needed. +// Swift layer as `import MATTelemetryObjC`. The module name is kept identical +// to the one in wrappers/swift/Modules/module.modulemap so the same Swift +// sources compile against both the local modulemap and this xcframework. -module ObjCModule { +module MATTelemetryObjC { umbrella header "MATTelemetry-umbrella.h" export * } diff --git a/wrappers/swift/Headers/ObjCModule-Bridging-Header.h b/wrappers/swift/Headers/MATTelemetryObjC-Bridging-Header.h similarity index 100% rename from wrappers/swift/Headers/ObjCModule-Bridging-Header.h rename to wrappers/swift/Headers/MATTelemetryObjC-Bridging-Header.h diff --git a/wrappers/swift/Modules/module.modulemap b/wrappers/swift/Modules/module.modulemap index 2e1856bae..f4fbec71d 100644 --- a/wrappers/swift/Modules/module.modulemap +++ b/wrappers/swift/Modules/module.modulemap @@ -1,6 +1,6 @@ /// Module exporting headers declared in ObjC. Imported by Swift package to have access to the ObjC types. -module ObjCModule { - header "../Headers/ObjCModule-Bridging-Header.h" +module MATTelemetryObjC { + header "../Headers/MATTelemetryObjC-Bridging-Header.h" export * } diff --git a/wrappers/swift/Sources/OneDSSwift/CommonDataContext.swift b/wrappers/swift/Sources/OneDSSwift/CommonDataContext.swift index 88786ac95..74c5bf0d0 100644 --- a/wrappers/swift/Sources/OneDSSwift/CommonDataContext.swift +++ b/wrappers/swift/Sources/OneDSSwift/CommonDataContext.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC /// Wrapper over ODWCommonDataContext class. public final class CommonDataContext { diff --git a/wrappers/swift/Sources/OneDSSwift/DiagnosticDataViewer.swift b/wrappers/swift/Sources/OneDSSwift/DiagnosticDataViewer.swift index 46d3011e9..dc136b076 100644 --- a/wrappers/swift/Sources/OneDSSwift/DiagnosticDataViewer.swift +++ b/wrappers/swift/Sources/OneDSSwift/DiagnosticDataViewer.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC /// Wrapper class over `ODWDiagnosticDataViewer` representing Diagnostic Data Viewer Hook. public final class DiagnosticDataViewer { diff --git a/wrappers/swift/Sources/OneDSSwift/EventProperties.swift b/wrappers/swift/Sources/OneDSSwift/EventProperties.swift index eb346a84e..bee46e573 100644 --- a/wrappers/swift/Sources/OneDSSwift/EventProperties.swift +++ b/wrappers/swift/Sources/OneDSSwift/EventProperties.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC /** Represents Event's properties. diff --git a/wrappers/swift/Sources/OneDSSwift/LogConfiguration.swift b/wrappers/swift/Sources/OneDSSwift/LogConfiguration.swift index 0f6bd18bb..1a5812770 100644 --- a/wrappers/swift/Sources/OneDSSwift/LogConfiguration.swift +++ b/wrappers/swift/Sources/OneDSSwift/LogConfiguration.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC /// Class wrapping `ODWLogConfiguration` ObjC class object, representing configuration related to events. public final class LogConfiguration { diff --git a/wrappers/swift/Sources/OneDSSwift/LogManager.swift b/wrappers/swift/Sources/OneDSSwift/LogManager.swift index 11a0af415..5a659795f 100644 --- a/wrappers/swift/Sources/OneDSSwift/LogManager.swift +++ b/wrappers/swift/Sources/OneDSSwift/LogManager.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC /// Wrapper over ODWLogManager which manages the telemetry logging system. public final class LogManager { diff --git a/wrappers/swift/Sources/OneDSSwift/Logger.swift b/wrappers/swift/Sources/OneDSSwift/Logger.swift index 04a9c1497..b901cfa4b 100644 --- a/wrappers/swift/Sources/OneDSSwift/Logger.swift +++ b/wrappers/swift/Sources/OneDSSwift/Logger.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC /// Wrapper class around ObjC Logger class `ODWLogger` used to events. public final class Logger { diff --git a/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift b/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift index c0c1ad4e0..0f0483eee 100644 --- a/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift +++ b/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift @@ -5,12 +5,12 @@ /// Contains alias for the types declared in the ObjC header files to make them available /// as part of the swift package module. -/// To avoid clients not have to import ObjCModule explicitly. +/// To avoid clients not have to import MATTelemetryObjC explicitly. /// Important: Due to objc->swift conventions, Type name is removed, so ODWPiiKindGenericData would be accessed as .genericData in swift. /// Check corresponding header file for the doc of each type. -import ObjCModule +import MATTelemetryObjC // ODWEventProperties.h public typealias EventPriority = ODWEventPriority diff --git a/wrappers/swift/Sources/OneDSSwift/PrivacyGuard.swift b/wrappers/swift/Sources/OneDSSwift/PrivacyGuard.swift index 19ad4787a..b589b82bc 100644 --- a/wrappers/swift/Sources/OneDSSwift/PrivacyGuard.swift +++ b/wrappers/swift/Sources/OneDSSwift/PrivacyGuard.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC /// Wrapper to `ODWPrivacyGuard` representing Privacy Guard Hook. public final class PrivacyGuard { diff --git a/wrappers/swift/Sources/OneDSSwift/PrivacyGuardInitConfig.swift b/wrappers/swift/Sources/OneDSSwift/PrivacyGuardInitConfig.swift index 7f660d9f6..2459c7f08 100644 --- a/wrappers/swift/Sources/OneDSSwift/PrivacyGuardInitConfig.swift +++ b/wrappers/swift/Sources/OneDSSwift/PrivacyGuardInitConfig.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC public final class PrivacyGuardInitConfig { let odwPrivacyGuardInitConfig: ODWPrivacyGuardInitConfig diff --git a/wrappers/swift/Sources/OneDSSwift/Sanitizer.swift b/wrappers/swift/Sources/OneDSSwift/Sanitizer.swift index 4b5035f3c..004166f16 100644 --- a/wrappers/swift/Sources/OneDSSwift/Sanitizer.swift +++ b/wrappers/swift/Sources/OneDSSwift/Sanitizer.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC /// Wrapper to `ODWSanitizer` representing the Sanitizer. public final class Sanitizer { diff --git a/wrappers/swift/Sources/OneDSSwift/SanitizerInitConfig.swift b/wrappers/swift/Sources/OneDSSwift/SanitizerInitConfig.swift index 8eb70fd37..81f23d75b 100644 --- a/wrappers/swift/Sources/OneDSSwift/SanitizerInitConfig.swift +++ b/wrappers/swift/Sources/OneDSSwift/SanitizerInitConfig.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC public final class SanitizerInitConfig { let odwSanitizerInitConfig: ODWSanitizerInitConfig diff --git a/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift b/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift index 184d19bc4..211983097 100644 --- a/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift +++ b/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // -import ObjCModule +import MATTelemetryObjC /// Wrapper over `ODWSemanticContext` class that manages the inclusion of semantic context values on logged events. public class SemanticContext { From 2b5bdd50a555617931ff092273e39e698f172045 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 14:09:26 -0500 Subject: [PATCH 24/33] docs(apple): note xcframework expects system sqlite3/zlib The SPM xcframework links the platform's libsqlite3/libz (Package.swift .linkedLibrary) rather than bundling them. Document why: bundling a private static sqlite3 would collide with any consumer that also uses SQLite (Core Data/GRDB/FMDB) -> duplicate symbols / two-instance state; system linking yields one shared copy. Notes the contrast with vcpkg (uses vcpkg packages) and Android (bundles, since the NDK has no system copy). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/apple/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/apple/README.md b/tools/apple/README.md index c08dd4ac3..1c97c31b1 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -19,6 +19,21 @@ The xcframework vendors a Clang module named `MATTelemetryObjC` through `tools/apple/module.modulemap` and `MATTelemetry-umbrella.h`, matching the existing Swift sources' `import MATTelemetryObjC`. +## Runtime dependencies (sqlite3 / zlib) + +The xcframework does **not** bundle sqlite3 or zlib. `Package.swift` links the +**system** `libsqlite3` and `libz` that Apple ships on every iOS / macOS / Mac +Catalyst / visionOS target (`.linkedLibrary("sqlite3")` / `.linkedLibrary("z")`), +so consumers do not need to add them. + +This is deliberate. Embedding a private static copy of sqlite3 into the +xcframework would give any app that also uses SQLite (Core Data, GRDB, FMDB, +etc.) two copies of the library in one process — risking duplicate-symbol link +errors or divergent SQLite state. Linking the OS-provided libraries guarantees a +single shared instance. For comparison, the vcpkg build consumes vcpkg's own +sqlite3/zlib packages, and only the Android build bundles them (the NDK ships no +system copy). + ## Supported slices `tools/apple/build-xcframework.sh release` builds: From f2c5fc946ced4b51fa06b67cdcde42f8ff8fca53 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 14:29:31 -0500 Subject: [PATCH 25/33] Use PIIKind alias in SemanticContext.setUserID signature Every other PII-tagged public API in OneDSSwift (EventProperties, Logger, LogManager) already uses the `PIIKind` typealias; only SemanticContext.setUserID exposed the raw Obj-C `ODWPiiKind` in its signature/default. Switch it to `PIIKind` (same underlying type via the ObjCTypes.swift typealias) so consumers of the Swift API never need to reference the MATTelemetryObjC module name. Pure source-level alias swap; no behavioral change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wrappers/swift/Sources/OneDSSwift/SemanticContext.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift b/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift index 211983097..a95d53992 100644 --- a/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift +++ b/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift @@ -50,9 +50,9 @@ public class SemanticContext { - Parameters: - userID: A `String` that contains the unique user identifier. - withPiiKind: A PIIKind of the userID. Set it to PiiKind_None t odenote it as non-PII. - - Note: Default value is `ODWPiiKind.identity`. + - Note: Default value is `PIIKind.identity`. */ - public func setUserID(_ userID: String, withPiiKind piiKind: ODWPiiKind = ODWPiiKind.identity) { + public func setUserID(_ userID: String, withPiiKind piiKind: PIIKind = PIIKind.identity) { odwSemanticContext.setUserId(userID) } From 06caea2aaafd11d5e9965743c7708b1e480458ec Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 15:17:39 -0500 Subject: [PATCH 26/33] Fix SemanticContext.setUserID dropping the caller's piiKind setUserID accepted a piiKind argument but called the no-piiKind Obj-C overload `setUserId(_:)`, silently discarding the caller's PII classification for the user id. ODWSemanticContext exposes a `setUserId:piiKind:` overload (wrappers/obj-c/ODWSemanticContext.h:47-48) for exactly this. Route the argument through so the requested PII tag is actually applied. Privacy-relevant behavioral fix. Needs a macOS `swift build` to confirm the bridged selector (`setUserId(_:piiKind:)`); cannot run Swift on the Windows session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- wrappers/swift/Sources/OneDSSwift/SemanticContext.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift b/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift index a95d53992..03d731102 100644 --- a/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift +++ b/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift @@ -53,7 +53,7 @@ public class SemanticContext { - Note: Default value is `PIIKind.identity`. */ public func setUserID(_ userID: String, withPiiKind piiKind: PIIKind = PIIKind.identity) { - odwSemanticContext.setUserId(userID) + odwSemanticContext.setUserId(userID, piiKind: piiKind) } /** From a57c2a1008a64dbdf8f98060e29c48ddcde68998 Mon Sep 17 00:00:00 2001 From: Bhagirath Mehta Date: Fri, 19 Jun 2026 15:42:52 -0500 Subject: [PATCH 27/33] Address Swift package review refinements Keep CommonDataContext available without PrivacyGuard, tighten Swift docs, and scope xcframework slice cleanup to the output directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Package.swift | 1 - tools/apple/README.md | 6 +++--- tools/apple/build-xcframework.sh | 1 - wrappers/swift/Package.swift | 1 - wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift | 2 +- wrappers/swift/Sources/OneDSSwift/SemanticContext.swift | 2 +- 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Package.swift b/Package.swift index bf8313af8..18af3547c 100644 --- a/Package.swift +++ b/Package.swift @@ -66,7 +66,6 @@ if hasPrivacyGuard { swiftSettings.append(.define("MATSDK_PRIVACYGUARD_AVAILABLE")) } else { excludedSources.append(contentsOf: [ - "CommonDataContext.swift", "PrivacyGuard.swift", "PrivacyGuardInitConfig.swift", ]) diff --git a/tools/apple/README.md b/tools/apple/README.md index 1c97c31b1..79d706fd1 100644 --- a/tools/apple/README.md +++ b/tools/apple/README.md @@ -1,8 +1,8 @@ # Swift Package Manager xcframework prototype -This packages the 1DS C++ SDK for Apple developers through Swift Package Manager -(SPM), using a prebuilt xcframework for the C++ core plus Obj-C wrappers and -compiling the Swift API from source. +This package distributes the 1DS C++ SDK to Apple developers through Swift +Package Manager (SPM), using a prebuilt xcframework for the C++ core plus Obj-C +wrappers and compiling the Swift API from source. ## Package shape diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh index 21bb9add1..7fed4ccf9 100755 --- a/tools/apple/build-xcframework.sh +++ b/tools/apple/build-xcframework.sh @@ -134,7 +134,6 @@ build_slice() { # arch platform out-subdir echo "=== building $arch / $plat ($CONFIG) ===" ( cd "$ROOT" - rm -f CMakeCache.txt *.cmake rm -rf out MATTELEMETRY_SKIP_PACKAGE=1 ./build-ios.sh "$CONFIG" "$arch" "$plat" ) diff --git a/wrappers/swift/Package.swift b/wrappers/swift/Package.swift index 879943354..5547c5c4d 100644 --- a/wrappers/swift/Package.swift +++ b/wrappers/swift/Package.swift @@ -25,7 +25,6 @@ if hasPrivacyGuard { swiftSettings.append(.define("MATSDK_PRIVACYGUARD_AVAILABLE")) } else { excludedSources.append(contentsOf: [ - "CommonDataContext.swift", "PrivacyGuard.swift", "PrivacyGuardInitConfig.swift", ]) diff --git a/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift b/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift index 0f0483eee..e0e446b00 100644 --- a/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift +++ b/wrappers/swift/Sources/OneDSSwift/ObjCTypes.swift @@ -5,7 +5,7 @@ /// Contains alias for the types declared in the ObjC header files to make them available /// as part of the swift package module. -/// To avoid clients not have to import MATTelemetryObjC explicitly. +/// This lets clients use the Swift package module without importing MATTelemetryObjC explicitly. /// Important: Due to objc->swift conventions, Type name is removed, so ODWPiiKindGenericData would be accessed as .genericData in swift. /// Check corresponding header file for the doc of each type. diff --git a/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift b/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift index 03d731102..3bf485fdf 100644 --- a/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift +++ b/wrappers/swift/Sources/OneDSSwift/SemanticContext.swift @@ -49,7 +49,7 @@ public class SemanticContext { - Parameters: - userID: A `String` that contains the unique user identifier. - - withPiiKind: A PIIKind of the userID. Set it to PiiKind_None t odenote it as non-PII. + - withPiiKind: A `PIIKind` for the userID. Set it to `PIIKind.none` to denote it as non-PII. - Note: Default value is `PIIKind.identity`. */ public func setUserID(_ userID: String, withPiiKind piiKind: PIIKind = PIIKind.identity) { From 5aa9d43c870094f41efffe404b675840cbff2411 Mon Sep 17 00:00:00 2001 From: mogiligarimidi23 Date: Tue, 23 Jun 2026 00:22:16 +0530 Subject: [PATCH 28/33] Surface per-batch record count (param2) in HTTP response debug events (#1487) --- lib/http/HttpResponseDecoder.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/http/HttpResponseDecoder.cpp b/lib/http/HttpResponseDecoder.cpp index 11e9d4096..6014cec19 100644 --- a/lib/http/HttpResponseDecoder.cpp +++ b/lib/http/HttpResponseDecoder.cpp @@ -91,6 +91,7 @@ namespace MAT_NS_BEGIN { DebugEvent evt; evt.type = DebugEventType::EVT_HTTP_OK; evt.param1 = response.GetStatusCode(); + evt.param2 = ctx->recordIdsAndTenantIds.size(); evt.data = static_cast(request.GetBody().data()); evt.size = request.GetBody().size(); DispatchEvent(evt); @@ -112,6 +113,7 @@ namespace MAT_NS_BEGIN { // This is to be addressed with ETW trace API that can send // a detailed error context to ETW provider. evt.param1 = response.GetStatusCode(); + evt.param2 = ctx->recordIdsAndTenantIds.size(); evt.data = static_cast(request.GetBody().data()); evt.size = request.GetBody().size(); DispatchEvent(evt); @@ -127,6 +129,7 @@ namespace MAT_NS_BEGIN { DebugEvent evt; evt.type = DebugEventType::EVT_HTTP_FAILURE; evt.param1 = 0; // response.GetStatusCode(); + evt.param2 = ctx->recordIdsAndTenantIds.size(); DispatchEvent(evt); } ctx->httpResponse = nullptr; @@ -144,6 +147,7 @@ namespace MAT_NS_BEGIN { DebugEvent evt; evt.type = DebugEventType::EVT_HTTP_FAILURE; evt.param1 = response.GetStatusCode(); + evt.param2 = ctx->recordIdsAndTenantIds.size(); DispatchEvent(evt); } temporaryServerFailure(ctx); @@ -157,6 +161,7 @@ namespace MAT_NS_BEGIN { DebugEvent evt; evt.type = DebugEventType::EVT_HTTP_FAILURE; evt.param1 = response.GetStatusCode(); + evt.param2 = ctx->recordIdsAndTenantIds.size(); DispatchEvent(evt); } temporaryNetworkFailure(ctx); @@ -253,4 +258,3 @@ namespace MAT_NS_BEGIN { } } MAT_NS_END - From 62cef16907d2cf3070b0f72cc052dc2cea463ba7 Mon Sep 17 00:00:00 2001 From: bmehta001 Date: Mon, 22 Jun 2026 16:23:08 -0500 Subject: [PATCH 29/33] Fix GUID_t::operator< to be a valid strict-weak-ordering (#1490) GUID_t::operator< used a non-lexicographic chained-|| comparison with a `Data3 == other.Data3` typo (should be `<`): return Data1 < other.Data1 || Data2 < other.Data2 || Data3 == other.Data3 || (memcmp(Data4,...) < 0); Two defects: (1) the Data3 line uses == instead of <, so GUIDs differing only in Data3 compare equivalent; (2) the chained-|| form is not lexicographic and violates antisymmetry (e.g. {1,5,..} and {2,3,..} can report both a / std::map (the default std::less calls it; the comment says it is "needed for maps") is undefined behavior -- container corruption, infinite loops, or crashes. GUID_t is a public header type (EventProperty.hpp), so SDK consumers hit this with default ordered containers. Replace with proper lexicographic comparison (Data1, then Data2, then Data3, then memcmp(Data4)). Test: GuidTests.OperatorLess_IsStrictWeakOrdering (added) fails on the old operator (c --- lib/system/EventProperty.cpp | 11 +++++++---- tests/unittests/GuidTests.cpp | 23 ++++++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/system/EventProperty.cpp b/lib/system/EventProperty.cpp index 3bf9c3f3c..6d0582440 100644 --- a/lib/system/EventProperty.cpp +++ b/lib/system/EventProperty.cpp @@ -307,10 +307,13 @@ namespace MAT_NS_BEGIN { // How to sort 2 objects (needed for maps) bool GUID_t::operator<(GUID_t const& other) const { - return Data1 < other.Data1 || - Data2 < other.Data2 || - Data3 == other.Data3 || - (memcmp(Data4, other.Data4, sizeof(Data4)) < 0); + if (Data1 != other.Data1) + return Data1 < other.Data1; + if (Data2 != other.Data2) + return Data2 < other.Data2; + if (Data3 != other.Data3) + return Data3 < other.Data3; + return memcmp(Data4, other.Data4, sizeof(Data4)) < 0; } void EventProperty::copydata(EventProperty const* source) diff --git a/tests/unittests/GuidTests.cpp b/tests/unittests/GuidTests.cpp index a0fc9fdb5..4a45d880c 100644 --- a/tests/unittests/GuidTests.cpp +++ b/tests/unittests/GuidTests.cpp @@ -7,6 +7,8 @@ #include "utils/Utils.hpp" #include "EventProperties.hpp" +#include + using namespace testing; using namespace MAT; @@ -84,4 +86,23 @@ TEST(GuidTests, MoveAssignment_ValidInput_MovesCorrectly) GUID_t second{"BEE391C8-72B0-464F-93C3-1B27879AD103"}; second = std::move(first); ASSERT_EQ("9D016D64-372E-4DCE-9FA3-0D0772217C54", second.to_string()); -} \ No newline at end of file +} +TEST(GuidTests, OperatorLess_IsStrictWeakOrdering) +{ + // a and b differ in Data1/Data2 such that a non-lexicographic chained-|| operator + // reported BOTH a < b and b < a (antisymmetry violation). + GUID_t a{ "00000001-0005-0000-0000-000000000000" }; + GUID_t b{ "00000002-0003-0000-0000-000000000000" }; + EXPECT_TRUE(a < b); + EXPECT_FALSE(b < a); + + // c and d differ ONLY in Data3; a '==' in that position made them compare equivalent. + GUID_t c{ "00000001-0001-0001-0000-000000000000" }; + GUID_t d{ "00000001-0001-0002-0000-000000000000" }; + EXPECT_TRUE(c < d); + EXPECT_FALSE(d < c); + + // A std::set keyed on operator< must keep four distinct GUIDs distinct. + std::set s{ a, b, c, d }; + EXPECT_EQ(static_cast(4), s.size()); +} From beba0e8e440e97f20f50e5fb51b2c9cc76cf19dc Mon Sep 17 00:00:00 2001 From: bmehta001 Date: Mon, 22 Jun 2026 16:45:10 -0500 Subject: [PATCH 30/33] Fix inverted oneds_memcpy_s result in CompactBinaryProtocolReader::ReadBlob (#1492) Under USE_ONEDS_BOUNDCHECK_METHODS, ReadBlob assigned the errno_t return of MAT::BoundCheckFunctions::oneds_memcpy_s (0 == success) directly to a bool: bool result = MAT::BoundCheckFunctions::oneds_memcpy_s(...); so `result` is false on success and true on failure -- inverted. ReadBlob (and thus ReadFloat/ReadDouble, which call it) returns "failure" for every successful read, breaking decode of any payload containing a blob/float/double field whenever the SDK is built with bound-check methods. The non-boundcheck branch one line below already does it correctly: `bool result = (memcpy_s(...) == 0);`. Compare the result against 0, matching the memcpy_s branch. Validated (TDD) with a standalone reader round-trip compiled with -DHAVE_ONEDS_BOUNDCHECK_METHODS: ReadDouble of a written double returns false before the fix and true (correct value) after. The bug is gated behind the non-default USE_ONEDS_BOUNDCHECK_METHODS option; the default build is unaffected (the changed line is #ifdef'd out) and the existing bondlite CompactBinaryProtocolTests assert ReadBlob == true under that option. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/bond/CompactBinaryProtocolReader.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bond/CompactBinaryProtocolReader.hpp b/lib/bond/CompactBinaryProtocolReader.hpp index 612970421..708aba421 100644 --- a/lib/bond/CompactBinaryProtocolReader.hpp +++ b/lib/bond/CompactBinaryProtocolReader.hpp @@ -69,7 +69,7 @@ class CompactBinaryProtocolReader { return false; } #ifdef HAVE_ONEDS_BOUNDCHECK_METHODS - bool result = MAT::BoundCheckFunctions::oneds_memcpy_s(static_cast(data), size, &(m_input[m_ofs]), size); + bool result = (MAT::BoundCheckFunctions::oneds_memcpy_s(static_cast(data), size, &(m_input[m_ofs]), size) == 0); #else bool result = (memcpy_s(static_cast(data), size, &(m_input[m_ofs]), size) == 0); #endif From c093ac17d0820024dbf27c30b6e5fba9d560125f Mon Sep 17 00:00:00 2001 From: Max Golovanov Date: Tue, 23 Jun 2026 12:37:03 -0700 Subject: [PATCH 31/33] New flag to scrub IP addresses (#1161) * Update EventPropertiesDecorator.hpp Scrub IP addresses by default. * Gate IP scrubbing behind CFG_BOOL_ENABLE_IP_SCRUBBING (default on) The initial change set RECORD_FLAGS_EVENTTAG_SCRUB_IP unconditionally for every event, forcing IP scrubbing on all SDK consumers in direct-upload mode -- a breaking change for apps that need client IP (e.g. geo-location). Make scrubbing the default but opt-out: the decorator sets the SCRUB_IP record flag unless CFG_BOOL_ENABLE_IP_SCRUBBING is explicitly set to false. record.flags is forwarded on the cross-platform/direct-upload path, so this redacts client IP at the collector without relying on ext.metadata privacy tags. - ILogConfiguration.hpp: add CFG_BOOL_ENABLE_IP_SCRUBBING config key - EventPropertiesDecorator.hpp: gate SCRUB_IP flag behind the config - EventPropertiesDecoratorTests.cpp: add default-on, opt-out, explicit-enable tests with a per-instance ConfigurableLogManager helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cover SDK stats events and add wrapper-parity config keys for IP scrubbing - Statistics.cpp: SDK statistics/metastats events bypass EventPropertiesDecorator, so apply the same collector-side client-IP scrub (gated by CFG_BOOL_ENABLE_IP_SCRUBBING, on by default) to those records too. Closes the gap identified in PR review. - LogConfigurationKey.java + ODWLogConfiguration.{h,mm}: expose CFG_BOOL_ENABLE_IP_SCRUBBING ('enableIpScrubbing') to the Android (Java) and Apple (Obj-C) wrappers for API parity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarify CFG_BOOL_ENABLE_IP_SCRUBBING docstring (Copilot round 1) The doc implied the setting only applies in direct-upload mode, but the scrub flag is set for all events and modes. Clarify that the flag is honored by the OneCollector direct-upload path while UTC mode applies its own client-privacy handling. No behavior change -- the flag is intentionally mode-agnostic and is ignored by the UTC pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address Copilot round 2 on #1161: self-contained header + default-config docs - EventPropertiesDecorator.hpp: include ILogManager.hpp so the header is self-contained for the ILogManager (m_owner) and ILogConfiguration types / CFG_BOOL_ENABLE_IP_SCRUBBING used inline, instead of relying on the includer. - Clarify CFG_BOOL_ENABLE_IP_SCRUBBING docs (C++ / Java / Obj-C): scrubbing is applied unless explicitly set to false (on by default) and the key is not present in the default configuration, so GetDefaultConfiguration() does not surface it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address Copilot round 3 on #1161: extract on-wire record flags to a shared header Move the RECORD_FLAGS_EVENTTAG_* on-wire bits out of EventPropertiesDecorator.hpp into a dedicated decorators/RecordFlagConstants.hpp, exposed as static constexpr std::int64_t in the MAT namespace (no longer #define macros). This lets the stats pipeline reference RECORD_FLAGS_EVENTTAG_SCRUB_IP via the small shared header instead of pulling in the full decorator header, and avoids macro pollution. - New: lib/decorators/RecordFlagConstants.hpp - EventPropertiesDecorator.hpp: include the shared header; drop the macros - Statistics.cpp: include the shared header instead of EventPropertiesDecorator.hpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden OfflineStorage_Room against null JNI objects (no-crash on null m_room/element) Defensive follow-up to the #1227 family of Android Room crashes ("java_object == null in call to GetObjectClass" under GetAndReserveRecords). #1417 fixed the stale-local-ref root cause but left two null paths unguarded, which can still hard-abort the host process: * m_room is null when the Room DB failed to open or was torn down (the destructor already guards with `if (s_vm && m_room)`, but ~10 other methods dereferenced it unconditionally). Add `if (!m_room) return ;` guards to DeleteRecords(x2), GetAndReserveRecords, ReleaseRecords, StoreRecords, DeleteSetting, StoreSetting, GetSizeInternal, GetRecordCount, ResizeDbInternal, and GetRecords. GetSetting already had this guard. * a null element in the getAndReserve/releaseRecords result array (observed to be androidx.room-version sensitive) was passed to GetObjectClass. Guard both loops: GetAndReserveRecords pops the frame and stops (the existing index < limit path releases the rest for retry); ReleaseRecords skips it. Turns a process-killing JNI abort into graceful degradation. No functional change on the healthy path. Compiles on Android only (JNI/Room) -- validated by CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: note the androidx.room version the SDK is built against (#1227 follow-up) cpp-start-android.md covered Room setup but said nothing about the Room version. The GetAndReserveRecords native crash (#1227, Room-version-sensitive) and the null-guard hardening in this PR make this worth documenting: the maesdk AAR brings androidx.room transitively (pinned in maesdk/build.gradle, currently 2.8.4); since Gradle resolves one Room version app-wide, consumers should not force a version below what the SDK is built against and should prefer aligning on the bundled (or a compatible newer) version. Points at build.gradle as the source of truth so the doc doesn't drift on future bumps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Room: guard null JNIEnv in 5 JNI methods; skip releaseUnconsumed on null element Address the latest Copilot review (6 comments) on lib/offline/OfflineStorage_Room.cpp. ConnectedEnv null-env guards (5): DeleteByToken, ReleaseRecords, DeleteSetting, StoreSetting and GetRecords created ConnectedEnv env(s_vm) and dereferenced env->... behind only an if(!m_room) guard. ConnectedEnv::operator! can report a null JNIEnv (null s_vm / thread-attach failure), and sibling methods already guard with if(!env); added the matching early return (void/false/records) so a null env no longer crashes. releaseUnconsumed on null element: the null-array-element path broke out and fell through to releaseUnconsumed(selected, index). The Java StorageRecordDao.releaseUnconsumed (pre-existing since 2020, commit c81d46aa) reads selected[0..unconsumed-1] ignoring the offset, so on the null path it could index the null element and throw, or release the wrong rows. A sawNullElement flag now skips releaseUnconsumed on that path; the reserved records expire and are retried (no data loss), and the normal end-early path is unchanged. Validated: NDK aarch64-linux-android23 -fsyntax-only clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Lalit Kumar Bhasin Co-authored-by: bmehta001 Co-authored-by: Bhagirath Mehta Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/cpp-start-android.md | 2 + .../events/LogConfigurationKey.java | 3 + lib/decorators/EventPropertiesDecorator.hpp | 19 ++-- lib/decorators/RecordFlagConstants.hpp | 30 ++++++ lib/include/public/ILogConfiguration.hpp | 10 ++ lib/offline/OfflineStorage_Room.cpp | 98 ++++++++++++++++++- lib/stats/Statistics.cpp | 8 ++ .../EventPropertiesDecoratorTests.cpp | 51 ++++++++++ wrappers/obj-c/ODWLogConfiguration.h | 5 + wrappers/obj-c/ODWLogConfiguration.mm | 5 + 10 files changed, 217 insertions(+), 14 deletions(-) create mode 100644 lib/decorators/RecordFlagConstants.hpp diff --git a/docs/cpp-start-android.md b/docs/cpp-start-android.md index 0f979bcda..2281a5e31 100644 --- a/docs/cpp-start-android.md +++ b/docs/cpp-start-android.md @@ -14,6 +14,8 @@ The Gradle wrapper in ```android_build``` builds two modules, ```app``` and ```m On Android, there are two database implementations to choose from. By default (the main branch on Github), the SDK will use the Android-supported androidx.Room database package. This reduces APK size because we don't need to compile and link in a copy of SQLite in native code (SQLite is hundreds of kB per ABI of APK file size). Room does have a slight CPU performance disadvantage since database transactions cross the JNI boundary when native code uses it. If you wish to change from Room to the native SQLite implementation, you should change the two module ```build.gradle``` files (app and maesdk). In those files, you will see an argument to CMake to select Room: ```"-DUSE_ROOM=1"```. Change this to ```"-DUSE_ROOM=0``` to select the native SQLite. +When using the Room implementation, the ```maesdk``` AAR brings ```androidx.room``` as a transitive dependency, pinned in ```lib/android_build/maesdk/build.gradle``` (currently ```2.8.4```). The SDK's native (JNI) code is compiled and tested against this version and the Room-generated schema. Because Gradle resolves a single ```androidx.room``` version for the entire app, if your app (or one of its dependencies) selects a different version, the SDK's native code runs against it. **Do not force ```androidx.room``` below the version the SDK is built against**, and prefer aligning your app on the bundled version (or a compatible newer one). A significantly different Room version can change the shape of query results that cross the JNI boundary and has historically caused native crashes in record retrieval (issue #1227); the SDK now guards against null results defensively, but version alignment avoids subtle behavior differences. + The Room database implementation adds one additional initialization requirement, since it needs a pointer to the JVM and an object reference to the application context. See below (4.5) for the required call to either ```connectContext``` (in Java) or ```ConnectJVM``` (in C++) to set this up. If you are building on Windows, this helper script [build-android.cmd](../build-android.cmd) is provided to illustrate how to deploy the necessary SDK and NDK dependencies. Once you installed the necessary dependencies, you may use Android Studio IDE for local builds. See [ide.cmd](../lib/android_build/ide.cmd) that shows how to build the project from IDE. The `app` project (`maesdktest`) allows to build and run all SDK tests on either emulator or real Android device. While the tests are running, you can monitor the test results in logcat output. diff --git a/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/LogConfigurationKey.java b/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/LogConfigurationKey.java index 329f17680..0ce2881ef 100644 --- a/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/LogConfigurationKey.java +++ b/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/LogConfigurationKey.java @@ -29,6 +29,9 @@ public enum LogConfigurationKey { /** Enable network detector. */ CFG_BOOL_ENABLE_NET_DETECT("enableNetworkDetector", Boolean.class), + /** Scrub (obfuscate) the client IP address at the collector. Applied unless explicitly set to false (on by default; not present in the default configuration). */ + CFG_BOOL_ENABLE_IP_SCRUBBING("enableIpScrubbing", Boolean.class), + CFG_BOOL_TPM_CLOCK_SKEW_ENABLED("clockSkewEnabled", Boolean.class), /** Parameter that allows to check if the SDK is running on UTC mode */ diff --git a/lib/decorators/EventPropertiesDecorator.hpp b/lib/decorators/EventPropertiesDecorator.hpp index 5bb3e927a..800dc1635 100644 --- a/lib/decorators/EventPropertiesDecorator.hpp +++ b/lib/decorators/EventPropertiesDecorator.hpp @@ -6,6 +6,8 @@ #define EVENTPROPERTIESDECORATOR_HPP #include "IDecorator.hpp" +#include "ILogManager.hpp" +#include "RecordFlagConstants.hpp" #include "EventProperties.hpp" #include "CorrelationVector.hpp" #include "utils/Utils.hpp" @@ -16,15 +18,6 @@ namespace MAT_NS_BEGIN { -// Bit remapping has to happen on bits passed via API surface. -// Ref CS2.1+ : https://osgwiki.com/wiki/CommonSchema/flags -// #define MICROSOFT_EVENTTAG_MARK_PII 0x08000000 -#define RECORD_FLAGS_EVENTTAG_MARK_PII 0x00080000 -// #define MICROSOFT_EVENTTAG_HASH_PII 0x04000000 -#define RECORD_FLAGS_EVENTTAG_HASH_PII 0x00100000 -// #define MICROSOFT_EVENTTAG_DROP_PII 0x02000000 -#define RECORD_FLAGS_EVENTTAG_DROP_PII 0x00200000 - class EventPropertiesDecorator : public IDecorator { protected: @@ -125,6 +118,14 @@ namespace MAT_NS_BEGIN { int64_t tags = eventProperties.GetPolicyBitFlags(); int64_t flags = 0; + // Scrub/obfuscate the client IP address at the collector by default. + // Hosts that require the client IP (e.g. for geo-location enrichment) + // can opt out by setting CFG_BOOL_ENABLE_IP_SCRUBBING = false. + ILogConfiguration& config = m_owner.GetLogConfiguration(); + if (!config.HasConfig(CFG_BOOL_ENABLE_IP_SCRUBBING) || config[CFG_BOOL_ENABLE_IP_SCRUBBING]) + { + flags |= RECORD_FLAGS_EVENTTAG_SCRUB_IP; + } // We must remap from one bitfield set to another, no way to bit-shift :( // At the moment 1DS SDK in direct upload mode supports DROP and MARK tags only: flags |= (tags & MICROSOFT_EVENTTAG_MARK_PII) ? RECORD_FLAGS_EVENTTAG_MARK_PII : 0; diff --git a/lib/decorators/RecordFlagConstants.hpp b/lib/decorators/RecordFlagConstants.hpp new file mode 100644 index 000000000..8c0fe56d3 --- /dev/null +++ b/lib/decorators/RecordFlagConstants.hpp @@ -0,0 +1,30 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +#ifndef RECORDFLAGCONSTANTS_HPP +#define RECORDFLAGCONSTANTS_HPP + +#include "ctmacros.hpp" + +#include + +namespace MAT_NS_BEGIN { + + // On-wire CS protocol record.flags bits. These are distinct from the + // API-surface MICROSOFT_EVENTTAG_* policy flags and are remapped onto + // record.flags by EventPropertiesDecorator. Kept in a dedicated header so + // other components (e.g. the stats pipeline) can reference a single bit + // definition without depending on the decorator's inline implementation. + // Ref CS2.1+: https://osgwiki.com/wiki/CommonSchema/flags + // (API-surface MICROSOFT_EVENTTAG_MARK_PII 0x08000000) + static constexpr std::int64_t RECORD_FLAGS_EVENTTAG_MARK_PII = 0x00080000; + // (API-surface MICROSOFT_EVENTTAG_HASH_PII 0x04000000) + static constexpr std::int64_t RECORD_FLAGS_EVENTTAG_HASH_PII = 0x00100000; + // (API-surface MICROSOFT_EVENTTAG_DROP_PII 0x02000000) + static constexpr std::int64_t RECORD_FLAGS_EVENTTAG_DROP_PII = 0x00200000; + static constexpr std::int64_t RECORD_FLAGS_EVENTTAG_SCRUB_IP = 0x00400000; + +} MAT_NS_END + +#endif // RECORDFLAGCONSTANTS_HPP diff --git a/lib/include/public/ILogConfiguration.hpp b/lib/include/public/ILogConfiguration.hpp index 1cb8103b8..af1bc44c2 100644 --- a/lib/include/public/ILogConfiguration.hpp +++ b/lib/include/public/ILogConfiguration.hpp @@ -104,6 +104,16 @@ namespace MAT_NS_BEGIN /// static constexpr const char* const CFG_BOOL_ENABLE_NET_DETECT = "enableNetworkDetector"; + /// + /// Request collector-side scrubbing (obfuscation) of the client IP address. + /// Applied unless explicitly set to false (on by default; the key is not + /// present in the default configuration). Opt out when the client IP is + /// needed, e.g. for geo-location enrichment. Honored by the OneCollector + /// direct-upload path; in UTC mode client privacy is governed by the OS UTC + /// pipeline instead. + /// + static constexpr const char* const CFG_BOOL_ENABLE_IP_SCRUBBING = "enableIpScrubbing"; + /// /// Parameter that allows to check if the SDK is running on UTC mode /// diff --git a/lib/offline/OfflineStorage_Room.cpp b/lib/offline/OfflineStorage_Room.cpp index 72a04d0ed..d052e7a9d 100644 --- a/lib/offline/OfflineStorage_Room.cpp +++ b/lib/offline/OfflineStorage_Room.cpp @@ -240,6 +240,14 @@ namespace MAT_NS_BEGIN MATSDK_THROW(std::logic_error("whereFilter not implemented")); } + if (!env) + { + return; + } + if (!m_room) + { + return; + } auto room_class = env->GetObjectClass(m_room); auto deleteByToken = env->GetMethodID(room_class, "deleteByToken", @@ -274,6 +282,10 @@ namespace MAT_NS_BEGIN { return; } + if (!m_room) + { + return; + } auto room_class = env->GetObjectClass(m_room); auto method = env->GetMethodID(room_class, "deleteById", "([J)J"); ThrowLogic(env, "Unable to get deleteById method"); @@ -377,6 +389,10 @@ namespace MAT_NS_BEGIN { return false; } + if (!m_room) + { + return false; + } auto room_class = env->GetObjectClass(m_room); auto reserve = env->GetMethodID(room_class, "getAndReserve", "(IJJJ)[Lcom/microsoft/applications/events/StorageRecord;"); @@ -424,11 +440,26 @@ namespace MAT_NS_BEGIN int persist_lb = static_cast(EventPersistence_Normal); int persist_ub = static_cast(EventPersistence_DoNotStoreOnDisk); + // Set if a null array element is hit below, so the early-release + // path skips releaseUnconsumed (which would index into the null). + bool sawNullElement = false; for (index = 0; index < limit; ++index) { env.pushLocalFrame(32); auto record = env->GetObjectArrayElement(selected, index); ThrowLogic(env, "getAndReserve element"); + if (!record) + { + // Null array element (observed with some androidx.room + // versions): pop this frame and stop rather than + // dereferencing null in GetObjectClass. We cannot safely + // release the tail here (it contains this null and Java + // releaseUnconsumed indexes from 0), so leave the + // remaining reservations to expire and be retried. + sawNullElement = true; + env.popLocalFrame(); + break; + } if (!record_class) { // Promote to a global ref so it survives popLocalFrame on @@ -518,11 +549,14 @@ namespace MAT_NS_BEGIN if (index < limit) { // we did not consume all these events - auto release = env->GetMethodID(room_class, "releaseUnconsumed", - "([Lcom/microsoft/applications/events/StorageRecord;I)V"); - ThrowLogic(env, "releaseUnconsumed"); - env->CallVoidMethod(m_room, release, selected, static_cast(index)); - ThrowRuntime(env, "call ru"); + if (!sawNullElement) + { + auto release = env->GetMethodID(room_class, "releaseUnconsumed", + "([Lcom/microsoft/applications/events/StorageRecord;I)V"); + ThrowLogic(env, "releaseUnconsumed"); + env->CallVoidMethod(m_room, release, selected, static_cast(index)); + ThrowRuntime(env, "call ru"); + } break; // break out of the request > collected loop--end early by request } } @@ -633,6 +667,14 @@ namespace MAT_NS_BEGIN try { ConnectedEnv env(s_vm); + if (!env) + { + return; + } + if (!m_room) + { + return; + } auto room_class = env->GetObjectClass(m_room); ThrowLogic(env, "GetObjectClass for m_room"); auto release = env->GetMethodID(room_class, @@ -700,6 +742,12 @@ namespace MAT_NS_BEGIN env.pushLocalFrame(8); auto byTenant = env->GetObjectArrayElement(results, index); ThrowRuntime(env, "Exception fetching element from results"); + if (!byTenant) + { + // Skip a null array element rather than dereference null. + env.popLocalFrame(); + continue; + } if (!bt_class) { // Promote to a global ref so it survives popLocalFrame. @@ -794,6 +842,10 @@ namespace MAT_NS_BEGIN static constexpr char newRecordSignature[] = "(JIIJIJ[B)Lcom/microsoft/applications/events/StorageRecord;"; + if (!m_room) + { + return 0; + } auto room_class = env->GetObjectClass(m_room); size_t count = std::min(records.size(), INT32_MAX); @@ -924,6 +976,14 @@ namespace MAT_NS_BEGIN try { ConnectedEnv env(s_vm); + if (!env) + { + return false; + } + if (!m_room) + { + return false; + } auto room_class = env->GetObjectClass(m_room); auto delete_method = env->GetMethodID(room_class, "deleteSetting", "(Ljava/lang/String;)V"); @@ -964,6 +1024,14 @@ namespace MAT_NS_BEGIN try { ConnectedEnv env(s_vm); + if (!env) + { + return false; + } + if (!m_room) + { + return false; + } auto room_class = env->GetObjectClass(m_room); jmethodID store_setting = env->GetMethodID( room_class, @@ -1081,6 +1149,10 @@ namespace MAT_NS_BEGIN size_t OfflineStorage_Room::GetSizeInternal(ConnectedEnv& env) const { + if (!m_room) + { + return 0; + } auto room_class = env->GetObjectClass(m_room); auto method = env->GetMethodID(room_class, "totalSize", "()J"); if (!method) @@ -1107,6 +1179,10 @@ namespace MAT_NS_BEGIN { return 0; } + if (!m_room) + { + return 0; + } auto room_class = env->GetObjectClass(m_room); auto count_id = env->GetMethodID(room_class, "getRecordCount", "(I)J"); ThrowLogic(env, "getRecordCount"); @@ -1162,6 +1238,10 @@ namespace MAT_NS_BEGIN { return false; } + if (!m_room) + { + return false; + } auto room_class = env->GetObjectClass(m_room); auto trim_id = env->GetMethodID(room_class, "trim", "(J)J"); ThrowLogic(env, "trim"); @@ -1192,6 +1272,14 @@ namespace MAT_NS_BEGIN { ConnectedEnv env(s_vm); + if (!env) + { + return records; + } + if (!m_room) + { + return records; + } auto room_class = env->GetObjectClass(m_room); auto method = env->GetMethodID(room_class, "getRecords", "(ZIJ)[Lcom/microsoft/applications/events/StorageRecord;"); diff --git a/lib/stats/Statistics.cpp b/lib/stats/Statistics.cpp index 773dd4d13..a1377ac37 100644 --- a/lib/stats/Statistics.cpp +++ b/lib/stats/Statistics.cpp @@ -9,6 +9,7 @@ #include "ILogManager.hpp" #include "mat/config.h" #include "utils/Utils.hpp" +#include "decorators/RecordFlagConstants.hpp" #include namespace MAT_NS_BEGIN { @@ -83,6 +84,13 @@ namespace MAT_NS_BEGIN { result &= m_baseDecorator.decorate(record); // Allow stats to capture Part A common properties, but not the custom result &= m_semanticContextDecorator.decorate(record, true); + // Stats events bypass EventPropertiesDecorator, so apply the same + // collector-side client-IP scrub here (on by default; opt out via + // CFG_BOOL_ENABLE_IP_SCRUBBING = false). + if (!m_config.HasConfig(CFG_BOOL_ENABLE_IP_SCRUBBING) || m_config[CFG_BOOL_ENABLE_IP_SCRUBBING]) + { + record.flags |= RECORD_FLAGS_EVENTTAG_SCRUB_IP; + } if (result) { IncomingEventContext evt(PAL::generateUuidString(), tenantToken, EventLatency_Normal, EventPersistence_Normal, &record); diff --git a/tests/unittests/EventPropertiesDecoratorTests.cpp b/tests/unittests/EventPropertiesDecoratorTests.cpp index f38444fd0..348354604 100644 --- a/tests/unittests/EventPropertiesDecoratorTests.cpp +++ b/tests/unittests/EventPropertiesDecoratorTests.cpp @@ -28,6 +28,19 @@ class TestEventPropertiesDecorator : public EventPropertiesDecorator } }; +// NullLogManager hands out a single shared static ILogConfiguration, which would +// leak configuration across tests. This subclass owns a per-instance configuration +// so the IP-scrubbing opt-out can be exercised in isolation. +class ConfigurableLogManager : public NullLogManager +{ +public: + ILogConfiguration localConfig; + ILogConfiguration& GetLogConfiguration() override + { + return localConfig; + } +}; + static std::unique_ptr PopulateRecordForDropPii() { auto record = std::unique_ptr(new Record{}); @@ -545,3 +558,41 @@ TEST(EventPropertiesDecoratorTests, DropPiiPartA_StripsValues) EXPECT_THAT(record->extSdk[0].installId, Eq("")); EXPECT_THAT(record->cV, Eq("")); } + +TEST(EventPropertiesDecoratorTests, Decorate_ScrubIp_EnabledByDefault) +{ + ConfigurableLogManager logManager; // CFG_BOOL_ENABLE_IP_SCRUBBING not set + EventPropertiesDecorator decorator(logManager); + Record record; + EventProperties props {"TestEvent"}; + EventLatency latency = EventLatency::EventLatency_Normal; + + EXPECT_TRUE(decorator.decorate(record, latency, props)); + EXPECT_TRUE(record.flags & RECORD_FLAGS_EVENTTAG_SCRUB_IP); +} + +TEST(EventPropertiesDecoratorTests, Decorate_ScrubIp_OptOutViaConfig) +{ + ConfigurableLogManager logManager; + logManager.localConfig[CFG_BOOL_ENABLE_IP_SCRUBBING] = false; + EventPropertiesDecorator decorator(logManager); + Record record; + EventProperties props {"TestEvent"}; + EventLatency latency = EventLatency::EventLatency_Normal; + + EXPECT_TRUE(decorator.decorate(record, latency, props)); + EXPECT_FALSE(record.flags & RECORD_FLAGS_EVENTTAG_SCRUB_IP); +} + +TEST(EventPropertiesDecoratorTests, Decorate_ScrubIp_ExplicitlyEnabled) +{ + ConfigurableLogManager logManager; + logManager.localConfig[CFG_BOOL_ENABLE_IP_SCRUBBING] = true; + EventPropertiesDecorator decorator(logManager); + Record record; + EventProperties props {"TestEvent"}; + EventLatency latency = EventLatency::EventLatency_Normal; + + EXPECT_TRUE(decorator.decorate(record, latency, props)); + EXPECT_TRUE(record.flags & RECORD_FLAGS_EVENTTAG_SCRUB_IP); +} diff --git a/wrappers/obj-c/ODWLogConfiguration.h b/wrappers/obj-c/ODWLogConfiguration.h index 3cbdaaa61..6e3f77946 100644 --- a/wrappers/obj-c/ODWLogConfiguration.h +++ b/wrappers/obj-c/ODWLogConfiguration.h @@ -44,6 +44,11 @@ extern NSString * _Nonnull const ODWCFG_BOOL_ENABLE_WAL_JOURNAL; */ extern NSString * _Nonnull const ODWCFG_BOOL_ENABLE_NET_DETECT; +/*! + Scrub (obfuscate) the client IP address at the collector. Applied unless explicitly set to false (on by default; not present in the default configuration). +*/ +extern NSString * _Nonnull const ODWCFG_BOOL_ENABLE_IP_SCRUBBING; + /*! The event collection URI. */ diff --git a/wrappers/obj-c/ODWLogConfiguration.mm b/wrappers/obj-c/ODWLogConfiguration.mm index d69ddf70c..611b92940 100644 --- a/wrappers/obj-c/ODWLogConfiguration.mm +++ b/wrappers/obj-c/ODWLogConfiguration.mm @@ -50,6 +50,11 @@ */ NSString *const ODWCFG_BOOL_ENABLE_NET_DETECT = @"enableNetworkDetector"; +/*! + Scrub (obfuscate) the client IP address at the collector. Applied unless explicitly set to false (on by default; not present in the default configuration). +*/ +NSString *const ODWCFG_BOOL_ENABLE_IP_SCRUBBING = @"enableIpScrubbing"; + /*! The event collection URI. */ From 6c37993c3bc4debb8e6101d6e99765a27631bf71 Mon Sep 17 00:00:00 2001 From: bmehta001 Date: Tue, 23 Jun 2026 17:46:40 -0500 Subject: [PATCH 32/33] vcpkg port: release-bump workflow, overlay port bump, and consumer binary-footprint reductions (#1475) * Add vcpkg-release-bump workflow to automate port version bumps On a published version release, open a PR to microsoft/vcpkg bumping the cpp-client-telemetry port (REF -> tag, recomputed SHA512, version, then x-add-version). Runs only on published, non-prerelease version tags (vX.Y.Z.W) or manual dispatch, and opens no PR when the port already matches the release. Requires repo variable VCPKG_FORK_REPO and secret VCPKG_BUMP_TOKEN. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bump in-repo overlay port to v3.10.161.1 tag Repoint tools/ports/cpp-client-telemetry REF from the pre-release commit to the published v3.10.161.1 tag (SHA512 updated) for exact parity with the official microsoft/vcpkg port. Version was already 3.10.161.1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(vcpkg): add manifest-mode overlay fallback for pre-registry installs building-with-vcpkg.md only told manifest-mode users to add `cpp-client-telemetry` to vcpkg.json, which fails with an unknown-port error until the port is accepted into the official vcpkg registry. Document the `vcpkg-configuration.json` `overlay-ports` fallback that points manifest mode at the in-repo overlay port, giving parity with the classic-mode `--overlay-ports` instructions already in the doc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * vcpkg-bump: address Copilot force-with-lease comment + drop docs note .github/workflows/vcpkg-release-bump.yml (Copilot): `git push --force-with-lease` could fail on reruns because a fresh clone has no remote-tracking ref for an already-existing bump branch, so the workflow couldn't refresh an open bump PR (contradicting the "force-pushed branch refreshes it" intent). Fetch the branch into refs/remotes/origin/${BR} (|| true on the first run, when it doesn't exist yet) before the force-with-lease push so the lease has a ref to compare against. Verified at .github/workflows/vcpkg-release-bump.yml:146-152. docs/building-with-vcpkg.md: remove the manifest-mode overlay-ports note added in 136e0100, per maintainer request. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update vcpkg docs: port is now live in the official registry The cpp-client-telemetry port was merged into the upstream vcpkg registry (microsoft/vcpkg#52316, version 3.10.161.1), so docs/building-with-vcpkg.md no longer needs the conditional "once the port is accepted" phrasing. - Intro: state the port is published in the official registry and consumable directly; drop the stale "build recipe / CONTROL file" wording (vcpkg uses vcpkg.json, and the port is registry-resolved now). - "Installing from the vcpkg registry": present tense, link to the upstream ports/cpp-client-telemetry directory. - "Installing from the overlay port": reframe as development-only (test local port changes or a newer SDK revision before they reach the registry). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align overlay port with the merged upstream vcpkg port The cpp-client-telemetry port merged into microsoft/vcpkg (ports/cpp-client-telemetry) ships only portfile.cmake + vcpkg.json. Bring the in-repo overlay back in sync so testing the overlay validates exactly what is published. - Drop the custom 'usage' file and its install step in portfile.cmake. The two lines it printed (find_package(MSTelemetry CONFIG REQUIRED) + target_link_libraries ... MSTelemetry::mat) duplicate vcpkg's auto-generated heuristic usage, and the upstream port carries no usage file. - Reorder vcpkg.json dependencies to vcpkg format-manifest canonical (alphabetical) order; same dependency set, no resolution change. After this, the overlay portfile.cmake and vcpkg.json are byte-identical to the upstream port blobs (cfdab236 / a74cc08b). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix vcpkg-bump PR existence check: jq null skipped PR creation Copilot review (round): the open-PR existence guard used --jq '.[0].number'. On the first run, when no PR exists yet, gh pr list returns [] and .[0].number evaluates to null, which gh prints as the literal string "null". [ -n "null" ] is true, so the workflow wrongly logged "An open PR already exists" and skipped 'gh pr create' -- the release-bump PR would never be opened on a clean run. Fix: --jq '.[0].number // empty' yields empty output when no PR exists (guard false -> PR created) and the PR number when one does (guard true -> skipped). Verified jq semantics (jq 1.x): '[] | .[0].number' -> null (prints "null"); '[] | .[0].number // empty' -> no output; '[{number:42}] | .[0].number // empty' -> 42. Confirmed at .github/workflows/vcpkg-release-bump.yml:158. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden vcpkg-bump workflow: graceful no-op on non-version tags, no token in URL Address Copilot round comments on the release-bump workflow. vcpkg-release-bump.yml:77 - non-version tag handling was contradictory: the message said "skipping" but the step ran exit 1, failing the workflow. A release published with a non-4-part tag (the SDK has historical 3-part tags like v3.3.8) would mark the automatic run red for what should be a no-op. Now: manual workflow_dispatch with a bad tag still fails loudly (user error), but the automatic release trigger emits a notice, sets a 'skip' output, and exits 0. All downstream steps are gated on steps.ver.outputs.skip != 'true'. vcpkg-release-bump.yml:100 - the PAT was embedded in the clone URL, which persists it in .git/config and risks leaking if git echoes the remote. Switch to 'gh auth setup-git' (writes a credential helper to the global gitconfig) plus a tokenless https clone; the later push step reuses that helper via its GH_TOKEN env. No token appears in any URL or on disk. Validated: workflow YAML parses (PyYAML) and every embedded run block passes 'bash -n'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * vcpkg-bump workflow: pass release tag via env to prevent shell injection Code review flagged a GitHub Actions script-injection vector: the "Resolve tag and version" step interpolated the untrusted release tag directly into the run: shell, before any validation. A tag containing shell metacharacters (e.g. v1.0.0.0";id;") would execute at assignment time, before the version regex runs. Injected code could write to GITHUB_ENV/GITHUB_PATH, which persist into the later Clone and push/PR steps that carry the VCPKG_BUMP_TOKEN PAT, enabling token exfiltration. Fix: pass the tag values through env: (RELEASE_TAG/INPUT_TAG) and reference them as quoted shell variables (TAG="${RELEASE_TAG:-$INPUT_TAG}"). Env values are not parsed as shell, so metacharacters can no longer inject. All downstream steps already use the regex-validated steps.ver.outputs.* values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Enable function-level linking in the CMake build so consumers can dead-strip The compiler-flags block in CMakeLists.txt is wrapped entirely in `if(NOT MATSDK_USE_VCPKG_DEPS)`, and its MSVC branch only sets warning flags -- never /Gy. So the CMake/vcpkg build (the one packaged for downstream consumers) compiles every TU without function-level COMDATs, and referencing one symbol pulls the whole .obj into the consumer image. Add a block, applied in BOTH vendored and vcpkg modes, that splits functions/data into COMDATs/sections (MSVC /Gy /Gw; GCC/Clang -ffunction-sections -fdata-sections; AppleClang -ffunction-sections). This lets a consumer's linker dead-strip unreferenced SDK code (/OPT:REF + /OPT:ICF, --gc-sections, -dead_strip) and matches the MSBuild Release projects, which already enable FunctionLevelLinking + OptimizeReferences + EnableCOMDATFolding. No source or ABI change. Bundled into the vcpkg PR since it directly improves the footprint of the vcpkg-packaged library. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reword function-level-linking comment to justify the vcpkg-mode exception Copilot review: the new comment read as conflicting with the earlier "let the toolchain manage compiler flags" note. Clarify that these section/COMDAT flags are a deliberate exception -- they are not optimization/dependency choices the toolchain owns; the toolchain doesn't set them, and without them the vcpkg-packaged library links whole .obj files. No code change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Consolidate section-splitting flags into the global block (drop REL_FLAGS dupes) Copilot review: the non-vcpkg REL_FLAGS already injected -ffunction-sections (and -fdata-sections for GNU), duplicating the new global block on Release builds. Remove them from REL_FLAGS so the global add_compile_options block is the single source of truth for section splitting across both dependency modes (it now also covers MSVC /Gy /Gw and AppleClang, which CI confirms build clean). No behavior change; eliminates redundant flags and future drift. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * vcpkg: request sqlite3 without default features (drop json1) + docs The SDK uses SQLite only for offline event storage (plain tables/indexes, no JSON/FTS/RTREE/vtab), so the port now declares sqlite3 with default-features:false instead of pulling json1. This is the necessary floor for footprint-conscious consumers: vcpkg unions features across the dependency graph and ignores default-features:false on transitive deps, so without this the SDK's own edge forces json1 on and no consumer can opt out. Measured: a consumer that also sets {"name":"sqlite3","default-features":false} in its root manifest links ~52 KB smaller (SQLITE_OMIT_JSON) on x64-windows-static Release; the vcpkg integration test stays 10/10. - tools/ports/cpp-client-telemetry/vcpkg.json: sqlite3 default-features:false; port-version 1 (port-only change over the published 3.10.161.1#0) - docs/building-with-vcpkg.md: document the required root-manifest opt-out Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(vcpkg): add consumer linker dead-strip guidance; clarify json1 opt-out Address Copilot round comments on docs/building-with-vcpkg.md and document the footprint lever consumers actually need (validated end-to-end on a real downstream DLL, which shrank substantially once /OPT:REF,ICF were on). - Add "Enable linker dead-stripping" section: the SDK's /Gy /Gw only enable stripping; the consumer's link must set /OPT:REF + /OPT:ICF (with the /DEBUG gotcha that silently disables them) + /INCREMENTAL:NO, or --gc-sections / -dead_strip on GNU/Clang/Apple. Note static-link vs DLL-reexport caveat. - json1 section: clarify this is the in-repo OVERLAY port (registry port to follow upstream), addressing the "registry still pulls defaults" comment. - Reword the resolution explanation around vcpkg's union model instead of "ignores transitive default-features:false". Verified by dry-run: SDK edge opt-out alone keeps sqlite3[core,json1]; adding the same at the root yields sqlite3 (no json1); any edge requesting defaults restores json1 -- so the consumer must opt out at the root too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * vcpkg(overlay): drop port-version The in-repo overlay is only used for local testing (overlays ignore the version database), and the vcpkg-release-bump workflow deletes port-version on every bump, so it served no purpose here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(vcpkg): precise /DEBUG wording for /OPT:REF,ICF Address Copilot comment on the linker-stripping guidance. Per the MSVC /OPT docs, /DEBUG changes the /OPT default from REF/ICF to NOREF/NOICF (it does disable them by default, contrary to the comment's premise). Reword to the exact behavior ("flips their default to off, /OPT:NOREF,NOICF") and note that /OPT:REF is also incompatible with incremental linking (hence /INCREMENTAL:NO). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * cmake: clarify AppleClang dead-strip comment (ld64 atomizes per symbol) Address Copilot comment: reword the AppleClang branch to state the actual mechanism -- clang emits .subsections_via_symbols on Mach-O, so ld64's -dead_strip removes unreferenced code at per-symbol (function) granularity without -ffunction-sections (which we add only for cross-toolchain consistency). -fdata-sections stays omitted due to the historical bitcode conflict. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * build: hide non-public symbols on non-Windows (-fvisibility=hidden) Add -fvisibility=hidden -fvisibility-inlines-hidden for non-MSVC builds and make MATSDK_LIBABI = __attribute__((visibility("default"))) on GCC/Clang, so only the MATSDK_LIBABI-decorated public API (the 56 public classes + the EVTSDK_LIBABI C API in mat.h) is exported; SDK internals and the bundled sqlite3/zlib are hidden. Non-Windows analog of the __declspec(dllexport)-gated export on Windows and of /Gy + the consumer's /OPT:REF: a much smaller dynamic symbol table -> faster dynamic linking/loading, smaller shared binaries, and more inlining/dead-code elimination. No behavior change for static consumers (hidden symbols remain usable within the same link); for shared-lib builds it restricts exports to the public API. Validated (NDK aarch64): compiling EventProperties.cpp with the flag yields the decorated public methods (EventProperties::SetType/GetType) as GLOBAL DEFAULT while internals (EventPropertiesStorage) are WEAK HIDDEN -- 64 exported vs 300 hidden in that one TU. Full cross-platform validation (build a shared lib and link a separate consumer on Linux/macOS/iOS/Android to confirm no public symbol is missing) should run in CI before merge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * address Copilot round on #1475: clang-cl /Gw gating + workflow_dispatch input - CMakeLists.txt: if(MSVC) is also true for the ClangCL toolset, which supports /Gy but not /Gw. Apply /Gy for all MSVC-like compilers and gate /Gw to real cl.exe (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") so ClangCL builds don't choke on /Gw. - vcpkg-release-bump.yml: read the workflow_dispatch tag via github.event.inputs.tag (concurrency key + resolve step) so manual dispatches resolve the tag unambiguously. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Windows shared-lib: add a dllimport path and tie export decoration to linkage The SDK has no .def file, so __declspec(dllexport)/(dllimport) via MATSDK_LIBABI is the sole Windows export mechanism, but it only had export and static states -- no dllimport path for a consumer of a shared mat.dll. And the cmake build hard-defined MATSDK_SHARED_LIB=1 for every Win32 build regardless of BUILD_SHARED_LIBS, so a static build still decorated the public API with dllexport (which also re-exports SDK symbols from any consumer DLL that absorbs the static lib). - ctmacros.hpp: add a MATSDK_IMPORT_LIB branch -> __declspec(dllimport). - lib/CMakeLists.txt: drop the hard-coded MATSDK_SHARED_LIB=1; instead set it on the mat target by linkage. Shared: PRIVATE MATSDK_SHARED_LIB (export from the SDK) + INTERFACE MATSDK_IMPORT_LIB (carried by the installed MSTelemetry::mat target, so find_package() consumers get dllimport automatically). Static: MATSDK_STATIC_LIB so nothing is decorated. This makes the C++ public API safe to consume from a single shared mat.dll (and fixes the missing-dllimport gap). The MSBuild/.vcxproj projects are unaffected (they define MATSDK_SHARED_LIB themselves). Add docs/sharing-a-single-sdk-runtime.md: how to ship one shared mat runtime that multiple modules in a process import (stable C ABI recommended; C++ shared-DLL path with its ABI-matching requirements), per-port vcpkg linkage, single LogManager lifetime ownership, one-copy-on-the-loader-path, and validation. Validated: MSVC preprocessor expands MATSDK_LIBABI to dllexport / dllimport / empty for the shared / import / static cases; cmake configures and the static Linux build is unaffected (change is Windows-guarded). Windows shared export/import is exercised by CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Scope -fvisibility-inlines-hidden to C++ to avoid -Werror C failures -fvisibility-inlines-hidden is a C++-only option. Applying it to all languages via add_compile_options meant the legacy Android path's bundled C sources (sqlite3_bundled, zlib_bundled) received it too; under Clang this emits an unused-argument warning that becomes an error with the project's -Werror. Scope it to CXX via a COMPILE_LANGUAGE generator expression while keeping -fvisibility=hidden for both C and C++. Files changed: - CMakeLists.txt: -fvisibility-inlines-hidden gated to \$ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Re-export ObjC wrapper classes in shared Apple builds The global -fvisibility=hidden (root CMakeLists.txt) hides Objective-C class symbols (_OBJC_CLASS_\*) as well as C++ internals. With the default BUILD_OBJC_WRAPPER=YES, a shared libmat.dylib on Apple would therefore export none of the public ODW* wrapper classes, and consumers linking against them would fail with undefined _OBJC_CLASS_\... symbols. Collect the ObjC wrapper translation units into OBJC_WRAPPER_SRCS and, for shared Apple builds, compile just those units with -fvisibility=default so the public Objective-C API is re-exported while the C++ core stays hidden. Files changed: - lib/CMakeLists.txt: OBJC_WRAPPER_SRCS variable + per-source -fvisibility=default for shared Apple builds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use portable OR generator expression in footprint docs snippet The multi-config form \$ only matches on CMake >= 3.19; on older CMake it compares the literal string and never matches, so a consumer copy-pasting the snippet would silently fail to enable /OPT:REF, /OPT:ICF, and /INCREMENTAL:NO. Switch to \$,\$>, which is valid across all supported CMake versions. Files changed: - docs/building-with-vcpkg.md: OR-based CONFIG generator expression in the dead-strip snippet Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Gate visibility on NOT WIN32; clarify C-API dllimport doc wording Two review fixes: - CMakeLists.txt: the hidden-visibility block is described as non-Windows but was gated on NOT MSVC, which also matches MinGW/Clang-GNU Windows and would apply ELF-style -fvisibility=hidden to a PE/COFF target. Gate it on NOT WIN32 so all Windows toolchains rely on __declspec(dllexport) as intended. - docs/sharing-a-single-sdk-runtime.md: the C-API bullet said it 'links without __declspec(dllimport)', which contradicted the new MATSDK_IMPORT_LIB interface define. Reworded to: dllimport is not required (a C function resolves via the import-lib thunk) but is applied automatically to shared consumers as a harmless optimization. Files changed: - CMakeLists.txt: NOT MSVC -> NOT WIN32 for the visibility block - docs/sharing-a-single-sdk-runtime.md: C-API dllimport wording Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Scope footprint guidance to static-link scenarios The 'Reducing binary footprint' section assumed the SDK is always linked statically, but vcpkg's default triplets (e.g. x64-windows) build dynamic libraries and the port also supports BUILD_SHARED_LIBS=ON. Clarify that the consumer-side dead-stripping guidance applies to static linkage, and note that a dynamic mat ships its own runtime whose export table is already trimmed by the SDK's -fvisibility=hidden and /Gy /Gw. Files changed: - docs/building-with-vcpkg.md: scope footprint section to static-link case Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Gate non-Windows default-visibility on shared builds (mirror Windows) On non-Windows, MATSDK_LIBABI unconditionally expanded to __attribute__((visibility("default"))), so even in a static build the public API kept default visibility despite -fvisibility=hidden. A consumer that statically absorbs libmat.a into its own .so/.dylib would then unintentionally re-export the SDK's public API (larger dynamic symbol table, leaked SDK surface) -- the non-Windows analog of the Windows re-export that MATSDK_STATIC_LIB already prevents. Gate the visibility attribute on MATSDK_SHARED_LIB so it mirrors the __declspec(dllexport) gating: shared builds export the API; static builds omit the attribute, letting the public symbols inherit -fvisibility=hidden. Define MATSDK_SHARED_LIB PRIVATE for all shared builds in lib/CMakeLists.txt (keeping the Windows-only INTERFACE MATSDK_IMPORT_LIB and MATSDK_STATIC_LIB). Verified on Linux (readelf): static build -> evt_api_call_default is GLOBAL HIDDEN (still resolvable by static linking -- a consumer links+runs against libmat.a -- but not re-exported); shared build -> GLOBAL DEFAULT (exported from libmat.so). Files changed: - lib/include/public/ctmacros.hpp: non-Windows MATSDK_LIBABI gated on MATSDK_SHARED_LIB - lib/CMakeLists.txt: define MATSDK_SHARED_LIB for all shared builds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/vcpkg-release-bump.yml | 193 ++++++++++++++++++ CMakeLists.txt | 60 +++++- docs/building-with-vcpkg.md | 74 ++++++- docs/sharing-a-single-sdk-runtime.md | 164 +++++++++++++++ lib/CMakeLists.txt | 45 +++- lib/include/public/ctmacros.hpp | 22 +- .../ports/cpp-client-telemetry/portfile.cmake | 7 +- tools/ports/cpp-client-telemetry/usage | 4 - tools/ports/cpp-client-telemetry/vcpkg.json | 11 +- 9 files changed, 550 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/vcpkg-release-bump.yml create mode 100644 docs/sharing-a-single-sdk-runtime.md delete mode 100644 tools/ports/cpp-client-telemetry/usage diff --git a/.github/workflows/vcpkg-release-bump.yml b/.github/workflows/vcpkg-release-bump.yml new file mode 100644 index 000000000..d09706242 --- /dev/null +++ b/.github/workflows/vcpkg-release-bump.yml @@ -0,0 +1,193 @@ +name: Vcpkg release bump + +# Opens a version-bump pull request against microsoft/vcpkg for the +# `cpp-client-telemetry` port whenever a new SDK release is published. +# +# It runs ONLY when a new version is cut: +# * automatically on a published, non-draft, non-prerelease GitHub Release +# whose tag looks like a version (vMAJOR.MINOR.PATCH.BUILD), or +# * manually via workflow_dispatch for a specific tag (recovery / re-run). +# It never runs on ordinary pushes, and it opens no PR if the port already +# matches the release (no version change). +# +# One-time setup required in this repository: +# * Variable VCPKG_FORK_REPO -> the vcpkg fork to push branches to, +# e.g. "your-org/vcpkg". +# * Secret VCPKG_BUMP_TOKEN -> a PAT (classic: repo+workflow, or +# fine-grained: Contents+Pull requests RW on +# the fork) able to push to VCPKG_FORK_REPO and +# open pull requests on microsoft/vcpkg. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Release tag to bump the vcpkg port to (e.g. v3.10.161.1)" + required: true + type: string + +permissions: + contents: read + +concurrency: + group: vcpkg-release-bump-${{ github.event.release.tag_name || github.event.inputs.tag }} + cancel-in-progress: false + +jobs: + bump: + name: Bump cpp-client-telemetry port + # Skip drafts and pre-releases; always allow manual dispatch. + if: >- + ${{ github.event_name == 'workflow_dispatch' || + (github.event.release.draft == false && github.event.release.prerelease == false) }} + runs-on: ubuntu-latest + env: + UPSTREAM_REPO: ${{ github.repository }} # microsoft/cpp_client_telemetry + VCPKG_UPSTREAM: microsoft/vcpkg + VCPKG_FORK_REPO: ${{ vars.VCPKG_FORK_REPO }} + PORT: cpp-client-telemetry + steps: + - name: Validate configuration + env: + VCPKG_BUMP_TOKEN: ${{ secrets.VCPKG_BUMP_TOKEN }} + run: | + set -euo pipefail + if [ -z "${VCPKG_FORK_REPO}" ]; then + echo "::error::Repository variable VCPKG_FORK_REPO is not set (e.g. 'your-org/vcpkg')." + exit 1 + fi + if [ -z "${VCPKG_BUMP_TOKEN}" ]; then + echo "::error::Secret VCPKG_BUMP_TOKEN is not set. Provide a token that can push to ${VCPKG_FORK_REPO} and open PRs on ${VCPKG_UPSTREAM}." + exit 1 + fi + + - name: Resolve tag and version + id: ver + env: + # Pass untrusted tag values through the environment instead of + # interpolating ${{ ... }} directly into the script body, so a tag + # containing shell metacharacters cannot inject commands into this + # step (which shares a runner with later PAT-bearing steps). + RELEASE_TAG: ${{ github.event.release.tag_name }} + INPUT_TAG: ${{ github.event.inputs.tag }} + run: | + set -euo pipefail + TAG="${RELEASE_TAG:-$INPUT_TAG}" + if [ -z "${TAG}" ]; then echo "::error::No release tag could be resolved."; exit 1; fi + # Only act on version tags: vMAJOR.MINOR.PATCH.BUILD. A non-matching + # tag from the automatic release trigger is a clean no-op (the SDK also + # has historical 3-part tags such as v3.3.8); a non-matching tag from a + # manual workflow_dispatch is user error and fails loudly. + if ! printf '%s' "${TAG}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "::error::Tag '${TAG}' is not a version tag (expected vX.Y.Z.W)." + exit 1 + fi + echo "::notice::Tag '${TAG}' is not a version tag (expected vX.Y.Z.W); nothing to bump." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + VERSION="${TAG#v}" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "branch=port/${PORT}-${VERSION}" >> "$GITHUB_OUTPUT" + echo "Bumping ${PORT} -> tag=${TAG} version=${VERSION}" + + - name: Compute source archive SHA512 + id: sha + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + URL="https://github.com/${UPSTREAM_REPO}/archive/${{ steps.ver.outputs.tag }}.tar.gz" + echo "Downloading ${URL}" + curl -fsSL --retry 3 "${URL}" -o source.tar.gz + SHA512="$(sha512sum source.tar.gz | cut -d' ' -f1)" + echo "sha512=${SHA512}" >> "$GITHUB_OUTPUT" + echo "SHA512=${SHA512}" + + - name: Clone vcpkg fork and branch off upstream master + if: ${{ steps.ver.outputs.skip != 'true' }} + env: + GH_TOKEN: ${{ secrets.VCPKG_BUMP_TOKEN }} + run: | + set -euo pipefail + # Authenticate git via gh's credential helper instead of embedding the + # token in the clone URL (which would persist it in .git/config and + # risk leaking it if git echoes the remote). The helper is written to + # the global gitconfig and reused by the later push step. + gh auth setup-git + git clone --depth 1 "https://github.com/${VCPKG_FORK_REPO}.git" vcpkg + cd vcpkg + git remote add upstream "https://github.com/${VCPKG_UPSTREAM}.git" + git fetch --depth 1 upstream master + git checkout -B "${{ steps.ver.outputs.branch }}" upstream/master + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Bootstrap vcpkg + if: ${{ steps.ver.outputs.skip != 'true' }} + run: cd vcpkg && ./bootstrap-vcpkg.sh -disableMetrics + + - name: Update port REF, SHA512 and version + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + cd vcpkg + PORTFILE="ports/${PORT}/portfile.cmake" + MANIFEST="ports/${PORT}/vcpkg.json" + if [ ! -f "${PORTFILE}" ] || [ ! -f "${MANIFEST}" ]; then + echo "::error::${PORT} port not found in ${VCPKG_UPSTREAM}. The port must already be in the registry before it can be bumped." + exit 1 + fi + sed -i -E "s|^([[:space:]]*REF[[:space:]]+).*$|\1${{ steps.ver.outputs.tag }}|" "${PORTFILE}" + sed -i -E "s|^([[:space:]]*SHA512[[:space:]]+).*$|\1${{ steps.sha.outputs.sha512 }}|" "${PORTFILE}" + jq --arg v "${{ steps.ver.outputs.version }}" '.version = $v | del(."port-version")' "${MANIFEST}" > "${MANIFEST}.tmp" + mv "${MANIFEST}.tmp" "${MANIFEST}" + ./vcpkg format-manifest "${MANIFEST}" + + - name: Detect change + id: diff + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + cd vcpkg + if git diff --quiet -- "ports/${PORT}"; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No change: ${PORT} is already at ${{ steps.ver.outputs.version }} with this REF/SHA512. Nothing to do." + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Commit, update version DB, push and open PR + if: ${{ steps.ver.outputs.skip != 'true' && steps.diff.outputs.changed == 'true' }} + env: + GH_TOKEN: ${{ secrets.VCPKG_BUMP_TOKEN }} + run: | + set -euo pipefail + cd vcpkg + # gh auth setup-git ran in the clone step; reuse that credential helper + # so 'git push' authenticates without a token in the remote URL. + BR="${{ steps.ver.outputs.branch }}" + git add "ports/${PORT}" + git commit -m "[${PORT}] Update to ${{ steps.ver.outputs.version }}" + ./vcpkg x-add-version "${PORT}" --overwrite-version + git add versions + git commit -m "[${PORT}] Update version database" + # Ensure a remote-tracking ref exists so --force-with-lease has a lease + # to compare against on reruns: the bump branch may already exist on the + # fork but be absent from this fresh clone. Ignore failure on the first + # run, when the branch does not exist remotely yet. + git fetch origin "+refs/heads/${BR}:refs/remotes/origin/${BR}" || true + git push --force-with-lease origin "${BR}" + if [ -n "$(gh pr list --repo "${VCPKG_UPSTREAM}" --head "$(printf '%s' "${VCPKG_FORK_REPO}" | cut -d/ -f1):${BR}" --state open --json number --jq '.[0].number // empty' 2>/dev/null)" ]; then + echo "An open PR already exists for ${BR}; the force-pushed branch refreshes it." + else + gh pr create \ + --repo "${VCPKG_UPSTREAM}" \ + --base master \ + --head "$(printf '%s' "${VCPKG_FORK_REPO}" | cut -d/ -f1):${BR}" \ + --title "[${PORT}] Update to ${{ steps.ver.outputs.version }}" \ + --body "Automated port bump to [\`${UPSTREAM_REPO}@${{ steps.ver.outputs.tag }}\`](https://github.com/${UPSTREAM_REPO}/releases/tag/${{ steps.ver.outputs.tag }}). Generated by the \`vcpkg-release-bump\` workflow." + fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a0ba0e82..360bf7436 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,16 +149,17 @@ else() endif() if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - # Using GCC with -s and -Wl linker flags - set(REL_FLAGS "-s -Wl,--gc-sections -Os ${WARN_FLAGS} -ffunction-sections -fdata-sections -fmerge-all-constants") + # Using GCC with -s and -Wl linker flags. -ffunction-sections/-fdata-sections + # are set once for all dep modes by the global block further below. + set(REL_FLAGS "-s -Wl,--gc-sections -Os ${WARN_FLAGS} -fmerge-all-constants") elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") set(REL_FLAGS "${WARN_FLAGS}") elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") - # AppleClang does not support -ffunction-sections and -fdata-sections with the -fembed-bitcode and -fembed-bitcode-marker set(REL_FLAGS "-Os ${WARN_FLAGS} -fmerge-all-constants") else() - # Using clang - strip unsupported GCC options - set(REL_FLAGS "-Os ${WARN_FLAGS} -ffunction-sections -fmerge-all-constants") + # Using clang - strip unsupported GCC options (-ffunction-sections is set by + # the global block further below). + set(REL_FLAGS "-Os ${WARN_FLAGS} -fmerge-all-constants") endif() ## Uncomment this to reduce the volume of note warnings on RPi4 w/gcc-8 Ref. https://gcc.gnu.org/ml/gcc/2017-05/msg00073.html @@ -206,6 +207,55 @@ endif() endif() # NOT MATSDK_USE_VCPKG_DEPS (compiler flags) +# --- Dead-strip enablement (applies in BOTH vendored and vcpkg modes) --------- +# Deliberate exception to the "let the toolchain manage compiler flags" note +# above (the NOT MATSDK_USE_VCPKG_DEPS block): these flags are NOT optimization +# or dependency choices the vcpkg toolchain owns -- they only split functions and +# data into separate COMDATs/sections so a *consumer's* linker can drop +# unreferenced SDK code (MSVC /OPT:REF + /OPT:ICF, GNU/Clang --gc-sections, Apple +# ld -dead_strip). The toolchain does not set them, and the vcpkg-packaged +# library (and every MSVC build, which never gets /Gy from the block above) would +# otherwise link whole .obj files instead of individual functions. Applying them +# here in both modes closes that gap and matches the MSBuild Release projects, +# which already enable FunctionLevelLinking + OptimizeReferences + COMDATFolding. +if(MSVC) + # /Gy (function-level linking) is supported by both cl.exe and clang-cl. + add_compile_options(/Gy) + # /Gw (whole-program global data) is cl.exe-only; the ClangCL toolset (for + # which MSVC is also true) does not support it. + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + add_compile_options(/Gw) + endif() +elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") + # On Mach-O, clang emits .subsections_via_symbols, so ld64's -dead_strip + # already removes unreferenced code at per-symbol (function) granularity + # without -ffunction-sections; we add it only for cross-toolchain + # consistency. -fdata-sections is omitted because it historically conflicted + # with bitcode on AppleClang. + add_compile_options(-ffunction-sections) +else() + # GCC / Clang (Linux, Android, MinGW) + add_compile_options(-ffunction-sections -fdata-sections) +endif() + +# Hidden symbol visibility (non-Windows): export only the MATSDK_LIBABI-decorated +# public API (classes + the C API), hiding SDK internals and the bundled +# sqlite3/zlib. This shrinks the dynamic symbol table (faster dynamic +# linking/loading, smaller binaries) and enables more inlining + dead-code +# elimination -- the non-Windows analog of what /Gy plus the consumer's /OPT:REF +# achieve on MSVC. All Windows toolchains (MSVC, MinGW, ClangCL) restrict exports +# via __declspec(dllexport) on MATSDK_LIBABI (lib/include/public/ctmacros.hpp), +# so this is gated on NOT WIN32 (not NOT MSVC, which would also catch MinGW/ +# Clang-GNU Windows builds and apply ELF-style visibility that does not belong on +# a PE/COFF target). +if(NOT WIN32) + # -fvisibility=hidden applies to C and C++; -fvisibility-inlines-hidden is a + # C++-only option, so scope it to CXX. (Applying it to C sources -- e.g. the + # bundled sqlite3/zlib on the legacy Android path -- makes Clang emit an + # "unused argument" warning that becomes an error under the project's -Werror.) + add_compile_options(-fvisibility=hidden $<$:-fvisibility-inlines-hidden>) +endif() + include(tools/Utils.cmake) include(GNUInstallDirs) include(CMakePackageConfigHelpers) diff --git a/docs/building-with-vcpkg.md b/docs/building-with-vcpkg.md index b736ba3c5..7e5cff7d0 100644 --- a/docs/building-with-vcpkg.md +++ b/docs/building-with-vcpkg.md @@ -1,6 +1,6 @@ # Building 1DS C++ SDK with vcpkg -[vcpkg](https://vcpkg.io/) is a Microsoft cross-platform open source C++ package manager. Onboarding instructions for Windows, Linux and Mac OS X [available here](https://docs.microsoft.com/en-us/cpp/build/vcpkg). This document assumes that the customer build system is already configured to use vcpkg ([getting started guide](https://learn.microsoft.com/en-us/vcpkg/get_started/overview)). 1DS C++ SDK maintainers provide a build recipe, `cpp-client-telemetry` port or CONTROL file for vcpkg. The mainline vcpkg repo is refreshed to point to latest stable open source release of 1DS C++ SDK. +[vcpkg](https://vcpkg.io/) is a Microsoft cross-platform open source C++ package manager. Onboarding instructions for Windows, Linux and Mac OS X [available here](https://docs.microsoft.com/en-us/cpp/build/vcpkg). This document assumes that the customer build system is already configured to use vcpkg ([getting started guide](https://learn.microsoft.com/en-us/vcpkg/get_started/overview)). The `cpp-client-telemetry` port is published in the official vcpkg registry, so it can be consumed directly with no overlay or extra configuration. Maintainers refresh the registry to point to the latest stable open source release of the 1DS C++ SDK on each release. The port provides the core SDK — the `MSTelemetry::mat` target and its public C++ headers. The optional Microsoft-proprietary modules (Privacy Guard, @@ -16,7 +16,8 @@ git clone --recurse-submodules https://github.com/microsoft/cpp_client_telemetry ### Installing from the vcpkg registry -Once a new port has been accepted into the official vcpkg registry, install with: +The `cpp-client-telemetry` port is available in the [official vcpkg registry](https://github.com/microsoft/vcpkg/tree/master/ports/cpp-client-telemetry), +so you can install it directly — no overlay or extra configuration required: ```console vcpkg install cpp-client-telemetry @@ -26,8 +27,9 @@ That's it! The package should be compiled for the current OS. ### Installing from the overlay port (development / pre-release) -Before the port is published, or to test local changes, use the overlay port -shipped in this repository: +The overlay port shipped in this repository is for **development only** — use it +to test local changes to the port, or a newer SDK revision, before they are +published to the registry: ```console git clone https://github.com/microsoft/cpp_client_telemetry @@ -190,6 +192,70 @@ will automatically use the optimized zlib-ng build. > zlib. When using `ZLIB_COMPAT=ON`, ensure all dependencies resolve to > zlib-ng rather than mixing stock zlib and zlib-ng. +## Reducing binary footprint + +This section applies when the SDK is linked **statically** into your binary +(the default for the `*-static` vcpkg triplets) — most footprint control then +lives on *your* side of the link. If you instead consume a **dynamic** `mat` +(e.g. the default `x64-windows` triplet, or `BUILD_SHARED_LIBS=ON`), the runtime +ships as its own `mat.dll` / `libmat.so` / `libmat.dylib`; the SDK's own +`-fvisibility=hidden` and `/Gy /Gw` already trim its exported symbol table, and +the consumer-side linker options below are specific to the static-link case. + +### Enable linker dead-stripping (largest lever) + +The SDK is compiled with function-level linking (`/Gy /Gw` on MSVC, +`-ffunction-sections -fdata-sections` on GCC/Clang) so that **your** linker can +discard SDK code you never reference. Make sure your final link enables it: + +- **MSVC:** `/OPT:REF` (drop unreferenced functions/data) and `/OPT:ICF` (fold + identical COMDATs). These are on by default for Release, **but `/DEBUG` flips + their default to off** (`/OPT:NOREF,NOICF`, per the MSVC `/OPT` docs) — so if + you ship PDBs, re-enable them explicitly. `/OPT:REF` is also incompatible with + incremental linking, so set `/INCREMENTAL:NO`: + + ```cmake + target_link_options(your_target PRIVATE + $<$,$>:/OPT:REF> + $<$,$>:/OPT:ICF> + $<$,$>:/INCREMENTAL:NO>) + ``` + +- **GCC / Clang:** link with `-Wl,--gc-sections`. +- **Apple (clang):** link with `-Wl,-dead_strip`. + +This is by far the largest lever — on a static `x64-windows-static` Release link +it can roughly halve the binary. The SDK's `/Gy /Gw` flags only *enable* this; +the stripping happens at your link. Keep the SDK a static dependency linked +*into* your binary: if you re-export its API across your own DLL boundary, the +export table pins its symbols and defeats `/OPT:REF`. + +### Drop unused SQLite features (json1) + +The SDK uses SQLite only for offline event storage — plain tables and indexes, +with no JSON, FTS, R*Tree, or virtual-table features. This in-repo overlay port +already requests `sqlite3` with `default-features: false` on its dependency edge +(the published registry port will follow once this change is upstreamed). + +vcpkg unions feature requests across the whole dependency graph, and a +transitive opt-out alone is **not** enough: you must **also** request `sqlite3` +with `default-features: false` in your own top-level manifest to actually omit +`json1` (which compiles SQLite with `SQLITE_OMIT_JSON`, ~50 KB smaller on a +static `x64-windows-static` Release build): + +```json +{ + "dependencies": [ + "cpp-client-telemetry", + { "name": "sqlite3", "default-features": false } + ] +} +``` + +If any package in your build (or your own code) needs SQLite's JSON functions, +request `sqlite3[json1]` instead and the extension is restored for the whole +graph. + ## How It Works: MATSDK_USE_VCPKG_DEPS When the SDK detects it is being built via vcpkg (by checking for diff --git a/docs/sharing-a-single-sdk-runtime.md b/docs/sharing-a-single-sdk-runtime.md new file mode 100644 index 000000000..10e69746d --- /dev/null +++ b/docs/sharing-a-single-sdk-runtime.md @@ -0,0 +1,164 @@ +# Sharing one SDK runtime across several modules in a process + +When more than one module in a single process links this SDK — for example an +application that loads several plug-ins or libraries, each of which uses 1DS — +the easy default (every module statically embeds the SDK) has two costs: + +1. **Size.** The SDK (plus its bundled SQLite/zlib) is duplicated once per module. +2. **Duplicated global state.** Each static copy has its *own* default + `LogManager`, HTTP transport, offline SQLite cache, and upload threads. They do + not share a pipeline, and multiple writers to the same offline-cache path will + corrupt it. + +This document describes how to ship **one** shared SDK runtime (`mat.dll` / +`libmat.so` / `libmat.dylib`) that every module imports, so there is a single +copy on disk and a single set of process-global state. + +There are two ways to consume the shared runtime. **The C API is strongly +recommended** because it removes the fragile C++/CRT ABI coupling between modules. + +--- + +## Option 1 (recommended): consume the stable C API + +The SDK ships a flat **C ABI** in [`mat.h`](../lib/include/public/mat.h). Every +`evt_*` entry point (`evt_open`, `evt_log`, `evt_flush`, `evt_upload`, +`evt_pause`, `evt_resume`, `evt_close`, `evt_configure`, …) is a `static inline` +wrapper that marshals its arguments into a POD struct and calls through a single +exported `__cdecl` symbol, `evt_api_call_default`. + +Consequences that make this the robust choice: + +* **Only one symbol crosses the module boundary, and no C++/STL type does.** The + request is a plain C struct, so there is *no* requirement that the modules and + the shared runtime agree on the C++ standard library ABI (`/MD`, + `_ITERATOR_DEBUG_LEVEL`, MSVC toolset/STL version, libstdc++ vs libc++, + `_GLIBCXX_USE_CXX11_ABI`, …). A 1DS version bump does not force every module to + rebuild in lockstep against an identical toolchain. +* **It does not *require* `__declspec(dllimport)` to link.** A plain C function + resolves through the shared library's import lib even without `dllimport`, so + the C API works across the boundary regardless. Consumers that link the shared + `MSTelemetry::mat` target do get `dllimport` applied automatically (via the + `MATSDK_IMPORT_LIB` interface define this PR adds); for a C function that is a + harmless calling-convention optimization, not a requirement. + +Each module includes `mat.h`, links the one shared runtime, and uses its own +tenant/source. You still pin the **same SDK version** in every module (so the +request/struct layout matches), but you avoid the C++ ABI lockstep entirely. + +## Option 2: consume the C++ API from a shared library + +All modules `find_package(MSTelemetry CONFIG REQUIRED)` and link +`MSTelemetry::mat` (resolving to the import lib); none statically embed the SDK. + +The C++ public API passes C++ standard-library types (`std::string`, `std::map`, +…) across the module boundary, so **every module and the shared runtime must +share one C++ ABI**. If they do not, you get heap corruption / undefined +behavior. Pin all of the following identically: + +| Axis | Requirement | +|------|-------------| +| **CRT linkage (Windows)** | Dynamic CRT (`/MD`, `/MDd` for Debug) everywhere — never `/MT`, and never mix Debug/Release CRT across the boundary. (vcpkg: `VCPKG_CRT_LINKAGE dynamic`.) | +| **STL / iterator debug** | One compiler + STL, one build config. `_ITERATOR_DEBUG_LEVEL` must match (Release `0` vs Debug `2`) — a Release consumer + Debug runtime is a silent layout mismatch. | +| **Toolset** | One MSVC toolset across all binaries (the v14x toolsets share an STL ABI, but don't mix major versions); or one libstdc++/libc++ with the same `_GLIBCXX_USE_CXX11_ABI`. | +| **Language / model** | Same `/std:c++NN`, same `/EHsc` exception model, same architecture, no overridden struct packing. | +| **SDK build options** | Same SDK feature/version selection in every module's manifest — different features mean different headers, hence a different ABI even at the same version. | + +Because the C++ ABI must match exactly across separately built and separately +versioned modules, this option is materially more brittle than the C API. Prefer +Option 1 unless you specifically need the C++ surface and control all modules' +toolchains. + +--- + +## Building the shared runtime with vcpkg (per-port linkage) + +A common requirement is "share *this* SDK, but keep everything else statically +linked (no DLL forest)". Override the library linkage **per port** in your +triplet so only this SDK goes dynamic: + +```cmake +set(VCPKG_CRT_LINKAGE dynamic) +if(PORT STREQUAL "cpp-client-telemetry") + set(VCPKG_LIBRARY_LINKAGE dynamic) # mat.dll / libmat.so / libmat.dylib + import lib +else() + set(VCPKG_LIBRARY_LINKAGE static) +endif() +``` + +The port honors `VCPKG_LIBRARY_LINKAGE` / `BUILD_SHARED_LIBS` and emits the +shared `mat` plus its import lib and the `MSTelemetry` CMake config package. + +### Pin one version across all modules + +All modules must compile against identical SDK headers. Pin the same +`cpp-client-telemetry` version and `builtin-baseline` (or a shared version +override) in every module's manifest, and ideally build all artifacts in the same +CI job/container with the same toolchain image. Most ABI drift comes from +separate modules quietly building on different agents. + +--- + +## How the SDK decorates its public symbols + +The SDK has no `.def` file; on Windows, exporting/importing is driven entirely by +`MATSDK_LIBABI` in [`ctmacros.hpp`](../lib/include/public/ctmacros.hpp), which the +build ties to the actual linkage: + +* **Shared build:** the SDK is compiled with `MATSDK_SHARED_LIB` + (`__declspec(dllexport)`), and the installed `MSTelemetry::mat` target carries + an `INTERFACE` definition of `MATSDK_IMPORT_LIB`, so consumers that + `find_package` + link automatically get `__declspec(dllimport)` — no + consumer-side configuration required. +* **Static build:** nothing is decorated, so the SDK's public symbols are not + re-exported by a consumer DLL that absorbs the static lib. + +On non-Windows platforms the SDK is built with `-fvisibility=hidden` and the +public API is marked `__attribute__((visibility("default")))`, so only the public +API (including the C API) is exported from the shared object. + +--- + +## Coordinate the single runtime's lifetime + +One shared runtime means **one** set of process-global state. Decide ownership: + +* **Recommended — single owner.** The top-level module initializes and tears down + the SDK (`LogManager::Initialize` / `FlushAndTeardown`, or `evt_open` / + `evt_close`). Other modules obtain loggers (their own tenant/source) but never + initialize or tear down. This avoids teardown-ordering crashes. +* **Alternative — named instances.** `LogManagerProvider::CreateLogManager(id)` + gives each module its own instance/tenant/config sharing the one transport; then + you need a last-one-out teardown refcount and **distinct offline-cache paths** + (one shared path with multiple writers corrupts it). +* Teardown must happen exactly once, **last**, after every module has stopped + logging. + +--- + +## Ship exactly one copy on the loader path + +Place a single runtime where every module finds it: + +* **Windows:** the same directory as the consumers (or side-by-side assembly). +* **Linux:** `RPATH=$ORIGIN` so every module resolves the one copy. +* **macOS:** a stable install name, `@rpath/libmat.dylib`. + +Make exactly one package own and ship the SDK runtime; the others declare a +dependency rather than bundling their own. If several packages each ship their +own copy, which one loads is path-order luck — and if their versions differ, you +are back to an ABI mismatch even with "one" DLL. + +--- + +## Validate + +* **Dependency present, definitions absent.** `dumpbin /dependents` (Windows), + `ldd` (Linux), `otool -L` (macOS) on each consumer should show a dependency on + the one `mat` module; `dumpbin /exports` (or `nm -D`) on a consumer should show + it imports — not defines — the SDK symbols. +* **One copy at runtime.** Process Explorer / `/proc//maps` / `vmmap` should + map the `mat` module exactly once; there should be one offline-cache file. +* **(C++ option) CRT/STL smoke test.** Have a consumer pass a `std::string` event + property into the SDK and read it back. A `/MD` vs `/MT` or `_ITERATOR_DEBUG_LEVEL` + mismatch typically crashes immediately (especially in Debug). diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 584c678ec..0d4161811 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -216,7 +216,7 @@ if(PAL_IMPLEMENTATION STREQUAL "CPP11") endif() if(APPLE AND BUILD_OBJC_WRAPPER) message(STATUS "Include ObjC Wrappers") - list(APPEND SRCS + set(OBJC_WRAPPER_SRCS ../wrappers/obj-c/ODWLogger.mm ../wrappers/obj-c/ODWLogManager.mm ../wrappers/obj-c/ODWEventProperties.mm @@ -227,22 +227,23 @@ if(PAL_IMPLEMENTATION STREQUAL "CPP11") ../wrappers/obj-c/ODWSanitizerInitConfig.mm ) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/dataviewer/") - list(APPEND SRCS + list(APPEND OBJC_WRAPPER_SRCS ../wrappers/obj-c/ODWDiagnosticDataViewer.mm ) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/privacyguard/" AND BUILD_PRIVACYGUARD) set(MATSDK_OBJC_PRIVACYGUARD_AVAILABLE ON) - list(APPEND SRCS + list(APPEND OBJC_WRAPPER_SRCS ../wrappers/obj-c/ODWPrivacyGuard.mm ) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/sanitizer/" AND BUILD_SANITIZER) set(MATSDK_OBJC_SANITIZER_AVAILABLE ON) - list(APPEND SRCS + list(APPEND OBJC_WRAPPER_SRCS ../wrappers/obj-c/ODWSanitizer.mm ) endif() + list(APPEND SRCS ${OBJC_WRAPPER_SRCS}) endif() if(APPLE AND BUILD_SWIFT_WRAPPER) @@ -271,7 +272,7 @@ elseif(PAL_IMPLEMENTATION STREQUAL "WIN32") if(NOT MATSDK_USE_VCPKG_DEPS) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../zlib ${CMAKE_CURRENT_SOURCE_DIR}/../sqlite) endif() -add_definitions(-D_UNICODE -DUNICODE -DWIN32 -DMATSDK_PLATFORM_WINDOWS=1 -DMATSDK_SHARED_LIB=1 -D_UTC_SDK -DUSE_BOND -D_WINDOWS -D_USRDLL -DWINVER=_WIN32_WINNT_WIN7) +add_definitions(-D_UNICODE -DUNICODE -DWIN32 -DMATSDK_PLATFORM_WINDOWS=1 -D_UTC_SDK -DUSE_BOND -D_WINDOWS -D_USRDLL -DWINVER=_WIN32_WINNT_WIN7) remove_definitions(-D_MBCS) list(APPEND SRCS http/HttpClient_WinInet.cpp @@ -323,6 +324,29 @@ else() add_library(mat STATIC ${SRCS}) endif() +# Public-API export decoration (MATSDK_LIBABI in lib/include/public/ctmacros.hpp). +# The SDK has no .def file, so __declspec(dllexport)/(dllimport) on Windows and +# __attribute__((visibility("default"))) elsewhere are the sole export mechanisms, +# and the decoration must follow the actual linkage: +# * shared: the SDK's own translation units export the public API +# (MATSDK_SHARED_LIB, PRIVATE). On Windows, consumers must additionally import +# it -- the INTERFACE MATSDK_IMPORT_LIB is carried by the installed +# MSTelemetry::mat target, so find_package() + link gives consumers dllimport +# automatically with no consumer-side configuration. Non-Windows consumers need +# nothing: they call symbols exported by libmat.so/.dylib. +# * static: decorate nothing, so the SDK's public symbols are NOT re-exported by +# a consumer DLL/.so that statically absorbs this library. Windows needs an +# explicit (empty) MATSDK_STATIC_LIB; elsewhere the empty MATSDK_LIBABI default +# plus -fvisibility=hidden (root CMakeLists.txt) already hides them. +if(BUILD_SHARED_LIBS) + target_compile_definitions(mat PRIVATE MATSDK_SHARED_LIB=1) + if(WIN32) + target_compile_definitions(mat INTERFACE MATSDK_IMPORT_LIB=1) + endif() +elseif(WIN32) + target_compile_definitions(mat PUBLIC MATSDK_STATIC_LIB=1) +endif() + # Target-based include paths for vcpkg / install workflow. # PUBLIC propagates to consumers; PRIVATE is SDK-internal only. # BUILD_INTERFACE is used during the SDK build; INSTALL_INTERFACE is used @@ -339,6 +363,17 @@ target_include_directories(mat ) if(APPLE AND BUILD_OBJC_WRAPPER) + if(BUILD_SHARED_LIBS AND OBJC_WRAPPER_SRCS) + # The root CMakeLists.txt applies -fvisibility=hidden globally to shrink the + # exported symbol table of the core C++ SDK. For Objective-C that also hides + # the wrapper class symbols (_OBJC_CLASS_$_ODW*), which are public API on + # Apple: a shared libmat.dylib would export no ODW* classes and consumers + # would fail to link (undefined _OBJC_CLASS_$_...). Re-export just the wrapper + # translation units with default visibility; the C++ core stays hidden. + set_source_files_properties(${OBJC_WRAPPER_SRCS} + PROPERTIES COMPILE_FLAGS "-fvisibility=default") + endif() + if(MATSDK_OBJC_PRIVACYGUARD_AVAILABLE) target_compile_definitions(mat PRIVATE MATSDK_OBJC_PRIVACYGUARD_AVAILABLE=1) else() diff --git a/lib/include/public/ctmacros.hpp b/lib/include/public/ctmacros.hpp index 42547e41d..cabd36f5f 100644 --- a/lib/include/public/ctmacros.hpp +++ b/lib/include/public/ctmacros.hpp @@ -28,9 +28,14 @@ #define MATSDK_LIBABI_CDECL __cdecl # if defined(MATSDK_SHARED_LIB) # define MATSDK_LIBABI __declspec(dllexport) +# elif defined(MATSDK_IMPORT_LIB) +// Consumer importing the public API from a shared mat.dll. The installed +// MSTelemetry::mat CMake target propagates this automatically when the SDK was +// built shared (see lib/CMakeLists.txt). +# define MATSDK_LIBABI __declspec(dllimport) # elif defined(MATSDK_STATIC_LIB) # define MATSDK_LIBABI -# else // Header file included by client +# else // Header file included by client; linkage unspecified # ifndef MATSDK_LIBABI # define MATSDK_LIBABI # endif @@ -47,8 +52,19 @@ #define MATSDK_LIBABI_CDECL #endif -#ifndef MATSDK_LIBABI -#define MATSDK_LIBABI +#ifndef MATSDK_LIBABI +// Mark the public API as default-visibility ONLY in shared builds, so it stays +// exported when the SDK is compiled with -fvisibility=hidden (see CMakeLists.txt). +// This mirrors the __declspec(dllexport) gating above: in a static build the +// attribute is omitted, so the public symbols inherit -fvisibility=hidden and are +// NOT re-exported when a consumer .so/.dylib statically absorbs libmat. (When a +// consumer includes this header, MATSDK_SHARED_LIB is not defined either, which +// is fine: the symbols are exported by the shared libmat they link against.) +# if (defined(__GNUC__) || defined(__clang__)) && defined(MATSDK_SHARED_LIB) +# define MATSDK_LIBABI __attribute__((visibility("default"))) +# else +# define MATSDK_LIBABI +# endif #endif // TODO: [MG] - ideally we'd like to use __attribute__((unused)) with gcc/clang diff --git a/tools/ports/cpp-client-telemetry/portfile.cmake b/tools/ports/cpp-client-telemetry/portfile.cmake index b0ce77107..cfdab2368 100644 --- a/tools/ports/cpp-client-telemetry/portfile.cmake +++ b/tools/ports/cpp-client-telemetry/portfile.cmake @@ -1,8 +1,8 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO microsoft/cpp_client_telemetry - REF 4485b82005abf1d24336ace99b11df88dd578eb0 - SHA512 1f3ee1c26f1ae9e7323262c9b4c8796efba2c6addcde432d6c6c77b8c1c2f254cb8ff334b1dd0a72dc8ecfbfbae04ab374ec5ac7e5d286d6042953d53e50fd5b + REF v3.10.161.1 + SHA512 4664b34ddce601d6a95669df4a59d11a6cc67de1f23de132192f791a275edc6a10b8498d340e6cf7d120d9e7a22c494d7517b24fc0954bf9e236e84a8800589a HEAD_REF main ) @@ -46,8 +46,5 @@ vcpkg_cmake_config_fixup(PACKAGE_NAME MSTelemetry CONFIG_PATH lib/cmake/MSTeleme file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") -# Install usage instructions -file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") - # Install license vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") diff --git a/tools/ports/cpp-client-telemetry/usage b/tools/ports/cpp-client-telemetry/usage deleted file mode 100644 index 736d289f6..000000000 --- a/tools/ports/cpp-client-telemetry/usage +++ /dev/null @@ -1,4 +0,0 @@ -cpp-client-telemetry provides CMake targets: - - find_package(MSTelemetry CONFIG REQUIRED) - target_link_libraries(main PRIVATE MSTelemetry::mat) diff --git a/tools/ports/cpp-client-telemetry/vcpkg.json b/tools/ports/cpp-client-telemetry/vcpkg.json index d721df65a..2ee6d9bc3 100644 --- a/tools/ports/cpp-client-telemetry/vcpkg.json +++ b/tools/ports/cpp-client-telemetry/vcpkg.json @@ -6,9 +6,6 @@ "license": "Apache-2.0", "supports": "((windows & !mingw) | linux | osx | ios | android) & !uwp", "dependencies": [ - "nlohmann-json", - "sqlite3", - "zlib", { "name": "curl", "default-features": false, @@ -17,6 +14,11 @@ ], "platform": "linux | android" }, + "nlohmann-json", + { + "name": "sqlite3", + "default-features": false + }, { "name": "vcpkg-cmake", "host": true @@ -24,6 +26,7 @@ { "name": "vcpkg-cmake-config", "host": true - } + }, + "zlib" ] } From edf33f80035575b82f1fafd5f9bd0dc0d2064e94 Mon Sep 17 00:00:00 2001 From: bmehta001 Date: Tue, 23 Jun 2026 18:06:43 -0500 Subject: [PATCH 33/33] Prepare for new release - 3.10.173.1 (#1489) * Prepare for new release - 3.10.170.1 Regenerated lib/include/public/Version.hpp via tools/gen-version (date-derived version: 3.<(year-2020)+4>..1 = 3.10.170.1). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update version to 3.10.173.1 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/include/public/Version.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/include/public/Version.hpp b/lib/include/public/Version.hpp index bb114f5a5..cf7ec9b99 100644 --- a/lib/include/public/Version.hpp +++ b/lib/include/public/Version.hpp @@ -6,8 +6,8 @@ #define MAT_VERSION_HPP // WARNING: DO NOT MODIFY THIS FILE! // This file has been automatically generated, manual changes will be lost. -#define BUILD_VERSION_STR "3.10.161.1" -#define BUILD_VERSION 3,10,161,1 +#define BUILD_VERSION_STR "3.10.173.1" +#define BUILD_VERSION 3,10,173,1 #ifndef RESOURCE_COMPILER_INVOKED #include "ctmacros.hpp" @@ -18,7 +18,7 @@ namespace MAT_NS_BEGIN { uint64_t const Version = ((uint64_t)3 << 48) | ((uint64_t)10 << 32) | - ((uint64_t)161 << 16) | + ((uint64_t)173 << 16) | ((uint64_t)1); } MAT_NS_END