diff --git a/.github/workflows/spm-release.yml b/.github/workflows/spm-release.yml new file mode 100644 index 000000000..2fbd94ba1 --- /dev/null +++ b/.github/workflows/spm-release.yml @@ -0,0 +1,172 @@ +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-15 # provides Xcode with Apple platform SDKs (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: Validate SwiftPM package consumption + 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 + xcodebuild -scheme OneDSSwift -destination 'generic/platform=visionOS Simulator' build + + - name: Compute SPM checksum + id: sum + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + 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' }} + 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 + 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 + 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/CMakeLists.txt b/CMakeLists.txt index 7a0ba0e82..28d43fb7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,30 +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}") - else() + 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 "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 "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}'") 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) @@ -89,6 +101,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 new file mode 100644 index 000000000..18af3547c --- /dev/null +++ b/Package.swift @@ -0,0 +1,129 @@ +// 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` 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): +// .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 + +let packageDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent() + +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 availability = readAvailability() +let hasDiagnosticDataViewer = availability["diagnosticDataViewer"] ?? false +let hasPrivacyGuard = availability["privacyGuard"] ?? false +let hasSanitizer = availability["sanitizer"] ?? false + +var excludedSources: [String] = [] +var swiftSettings: [SwiftSetting] = [] + +if !hasDiagnosticDataViewer { + excludedSources.append("DiagnosticDataViewer.swift") +} + +if hasPrivacyGuard { + swiftSettings.append(.define("MATSDK_PRIVACYGUARD_AVAILABLE")) +} else { + excludedSources.append(contentsOf: [ + "PrivacyGuard.swift", + "PrivacyGuardInitConfig.swift", + ]) +} + +if !hasSanitizer { + excludedSources.append(contentsOf: [ + "Sanitizer.swift", + "SanitizerInitConfig.swift", + ]) +} + +let package = Package( + name: "OneDSSwift", + platforms: [ + .iOS(.v12), + .macCatalyst(.v14), + .macOS(.v10_15), + .visionOS(.v1), + ], + products: [ + .library(name: "OneDSSwift", targets: ["OneDSSwift"]), + ], + targets: [ + // Prebuilt C++ core + Obj-C wrappers. The xcframework's bundled + // 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: + // + // .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. The conditional source exclusions above must stay in sync + // with the headers baked into the xcframework. + .target( + name: "OneDSSwift", + dependencies: ["MATTelemetry"], + path: "wrappers/swift/Sources/OneDSSwift", + exclude: excludedSources, + swiftSettings: swiftSettings, + linkerSettings: [ + .linkedLibrary("c++"), + .linkedLibrary("sqlite3"), + .linkedLibrary("z"), + .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, .visionOS])), + ]), + ] +) diff --git a/build-ios.sh b/build-ios.sh index d316fe2fa..d96b93bc4 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,18 +54,31 @@ 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" 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" @@ -92,10 +105,14 @@ 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 make -make package +if [ "${MATTELEMETRY_SKIP_PACKAGE:-}" = "1" ]; then + echo "MATTELEMETRY_SKIP_PACKAGE=1: skipping package creation" +else + make package +fi 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/MATTelemetry-umbrella.h b/tools/apple/MATTelemetry-umbrella.h new file mode 100644 index 000000000..8d4aaf952 --- /dev/null +++ b/tools/apple/MATTelemetry-umbrella.h @@ -0,0 +1,24 @@ +// +// 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). +// +// 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 + +#import "ODWCommonDataContext.h" +#import "ODWEventProperties.h" +#import "ODWLogConfiguration.h" +#import "ODWLogger.h" +#import "ODWLogManager.h" +#import "ODWPrivacyGuardInitConfig.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/README.md b/tools/apple/README.md new file mode 100644 index 000000000..79d706fd1 --- /dev/null +++ b/tools/apple/README.md @@ -0,0 +1,121 @@ +# Swift Package Manager xcframework prototype + +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 + +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 | Packaging | +| --- | --- | +| 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 `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: + +| 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` | 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 `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 | + +## Local build + +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 SwiftPM checksum +``` + +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. + +## Consumption + +- **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: + +```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` runs for published SDK releases and manual +dispatch. It: + +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. + +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 + +- 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. diff --git a/tools/apple/build-xcframework.sh b/tools/apple/build-xcframework.sh new file mode 100755 index 000000000..7fed4ccf9 --- /dev/null +++ b/tools/apple/build-xcframework.sh @@ -0,0 +1,202 @@ +#!/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), iOS simulator (arm64 + x86_64 fat), +# 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, visionOS, and macOS slices. + +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. + +case "$CONFIG" in + release) CMAKE_BUILD_TYPE="Release" ;; + debug) CMAKE_BUILD_TYPE="Debug" ;; + *) + echo "Usage: $0 [release|debug]" >&2 + exit 1 + ;; +esac + +# 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" + +# --- 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 `MATTelemetryObjC` to match what wrappers/swift sources import. +HDRS="$OUT/Headers" +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 +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" <> "$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" + echo "=== building $arch / $plat ($CONFIG) ===" + ( + cd "$ROOT" + 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 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. +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. +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 \ + $CMAKE_OPTS +cmake --build "$MACOS_BUILD" --target mat +mkdir -p "$OUT/macos-universal" +cp "$MACOS_BUILD/lib/$LIB" "$OUT/macos-universal/$LIB" + +# --- 3. 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/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" + +# --- 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" +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..ec2e84f22 --- /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 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 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/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/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 1042a42e1..e0e446b00 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. +/// 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. -import ObjCModule +import MATTelemetryObjC // ODWEventProperties.h public typealias EventPriority = ODWEventPriority @@ -27,4 +27,6 @@ public typealias TransmissionProfile = ODWTransmissionProfile public typealias FlushStatus = ODWStatus // ODWPrivacyGuard.h +#if MATSDK_PRIVACYGUARD_AVAILABLE public typealias DataConcernType = ODWDataConcernType +#endif 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..3bf485fdf 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 { @@ -49,11 +49,11 @@ 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`. + - 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: ODWPiiKind = ODWPiiKind.identity) { - odwSemanticContext.setUserId(userID) + public func setUserID(_ userID: String, withPiiKind piiKind: PIIKind = PIIKind.identity) { + odwSemanticContext.setUserId(userID, piiKind: piiKind) } /**