Skip to content

(APPLE) Build a Universal Framework, and add code signing and notarization #237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9c82ae5
Cleanup CMake redundancies and use FILE_SET for headers -- only solut…
cboulay Jul 8, 2025
8ecf72f
Add preliminary support for Xcode and building universal dylib
cboulay Jul 13, 2025
ad2feeb
Remove vestigial docker references in CI scripts (docker was only use…
cboulay Jul 14, 2025
2c907ff
First attempt at signing and notarizing framework in GitHub Actions. …
cboulay Jul 14, 2025
8ca5b87
Fix some cmake errors that snuck in during rebase.
cboulay Jul 14, 2025
7f81ae1
FILE_SET is incompatible with frameworks. Revert back to target_inclu…
cboulay Jul 15, 2025
ca5a1f7
examples - find_package must search in Frameworks subdirectory if APPLE.
cboulay Jul 15, 2025
ab2c180
Add codesign step to GHA CI script.
cboulay Jul 15, 2025
780b67d
macOS packaging - auto version and change destination
cboulay Jul 15, 2025
0058c83
Add iOS target to GHA
cboulay Jul 26, 2025
f45fe89
Move Apple GHA to its own script
cboulay Jul 26, 2025
c9ca767
Apple GHA - refactor to reuse certification setup across build and de…
cboulay Jul 26, 2025
5eda470
Apple GHA - don't run tests for iOS target
cboulay Jul 26, 2025
aa8d831
Fixup iOS builds
cboulay Jul 27, 2025
f6a22c5
GHA: Fix target directories for framework packager
cboulay Jul 27, 2025
254b9f7
Comment out apple tests until we fix the release.
cboulay Jul 27, 2025
eac4442
Wrong syntax for env vars.
cboulay Jul 27, 2025
6c30434
Try signing and packaging before uploading artifact
cboulay Jul 27, 2025
73d3650
GHA Apple - Restore unit tests
cboulay Jul 27, 2025
779473e
GHA Apple -- add ios simulator build
cboulay Jul 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/actions/install-apple-certs/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: 'Install Apple Certificates'
description: 'Installs Apple signing and notarization certificates and sets up the keychain'
inputs:
MACOS_CERTIFICATE_APP:
required: true
MACOS_CERTIFICATE_INST:
required: true
MACOS_CERTIFICATE_PWD:
required: true
MACOS_CI_KEYCHAIN_PWD:
required: true
runs:
using: "composite"
steps:
- name: Install certificates and provisioning profiles
shell: bash
run: |
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
security create-keychain -p "${{ inputs.MACOS_CI_KEYCHAIN_PWD }}" $KEYCHAIN_PATH
security default-keychain -s $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "${{ inputs.MACOS_CI_KEYCHAIN_PWD }}" $KEYCHAIN_PATH

# Import certificates from secrets ...
CERTIFICATE_PATH_APP=$RUNNER_TEMP/build_certificate_app.p12
CERTIFICATE_PATH_INST=$RUNNER_TEMP/build_certificate_inst.p12
echo -n "${{ inputs.MACOS_CERTIFICATE_APP }}" | base64 --decode -o $CERTIFICATE_PATH_APP
echo -n "${{ inputs.MACOS_CERTIFICATE_INST }}" | base64 --decode -o $CERTIFICATE_PATH_INST
# ... to keychain
security import $CERTIFICATE_PATH_APP -P "${{ inputs.MACOS_CERTIFICATE_PWD }}" -k $KEYCHAIN_PATH -A -t cert -f pkcs12
security import $CERTIFICATE_PATH_INST -P "${{ inputs.MACOS_CERTIFICATE_PWD }}" -k $KEYCHAIN_PATH -A -t cert -f pkcs12

# Set trusted partitions (groups of applications) that can access the keychain items
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ inputs.MACOS_CI_KEYCHAIN_PWD }}" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH

# Get certificate identities into environment variables
CERT_IDENTITY_APP=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}')
echo "APPLE_CODE_SIGN_IDENTITY_APP=$CERT_IDENTITY_APP" >> $GITHUB_ENV
CERT_IDENTITY_INST=$(security find-identity -v -p basic $KEYCHAIN_PATH | grep "Developer ID Installer" | head -1 | awk -F'"' '{print $2}')
echo "APPLE_CODE_SIGN_IDENTITY_INST=$CERT_IDENTITY_INST" >> $GITHUB_ENV
252 changes: 252 additions & 0 deletions .github/workflows/apple.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
name: Apple CI

on:
push:
branches:
- main
- dev
tags: ['*']
paths:
- '**'
- '!docs/**'
- '!.github/**'
- '.github/workflows/apple.yml'
pull_request:
release:
types: ['created']
workflow_dispatch:
inputs:
cmakeextra:
description: 'Extra CMake options'
required: false
default: ''

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

defaults:
run:
shell: bash

jobs:
build:
name: ${{ matrix.config.name }}
runs-on: ${{ matrix.config.os }}
strategy:
fail-fast: false
matrix:
config:
- {name: "macOS-latest", os: "macOS-latest", cmake_extra: "-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DLSL_UNITTESTS=ON -DLSL_BENCHMARKS=ON -DCMAKE_OSX_ARCHITECTURES=\"x86_64;arm64\"" }
- {name: "iOS", os: "macOS-latest", cmake_extra: "-DCMAKE_TOOLCHAIN_FILE=cmake/ios.toolchain.cmake -DPLATFORM=OS64" }
- {name: "iOS Simulator", os: "macOS-latest", cmake_extra: "-DCMAKE_TOOLCHAIN_FILE=cmake/ios.toolchain.cmake -DPLATFORM=SIMULATOR64COMBINED -G Xcode" }

steps:
- uses: actions/checkout@v4

- name: Install certificates and provisioning profiles
uses: ./.github/actions/install-apple-certs
with:
MACOS_CERTIFICATE_APP: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_INST: ${{ secrets.PROD_MACOS_CERTIFICATE_INST }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}

- name: Configure CMake
env:
APPLE_DEVELOPMENT_TEAM: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
run: |
cmake --version
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=${PWD}/install \
-DCPACK_PACKAGE_DIRECTORY=${PWD}/package \
-DLSL_FRAMEWORK=ON \
-Dlslgitrevision=${{ github.sha }} \
-Dlslgitbranch=${{ github.ref }} \
${{ matrix.config.cmake_extra }} \
${{ github.event.inputs.cmakeextra }}
echo ${PWD}

- name: make
run: cmake --build build --config Release -j

- name: make install
run: cmake --build build --config Release --target install

- name: test install using examples
if: matrix.config.name == 'macOS-latest'
run: |
# Test that the in-tree install was successful by building the examples
cmake -S examples -B examples/build \
-DLSL_INSTALL_ROOT=${PWD}/install \
-DCMAKE_INSTALL_PREFIX=examples/build/install \
-DLSL_COMFY_DEFAULTS=ON \
${{ matrix.config.cmake_extra }} \
${{ github.event.inputs.cmakeextra }}
cmake --build examples/build --target install --config Release -j
./examples/build/install/bin/HandleMetaData

- name: Codesign
run: |
if [[ "${{ matrix.config.name }}" == "macOS-latest" ]]; then
codesign -vvv --force --deep --sign "$APPLE_CODE_SIGN_IDENTITY_APP" \
--entitlements lsl.entitlements --options runtime \
install/Frameworks/lsl.framework/Versions/A/lsl
codesign -vvv --verify --deep --strict install/Frameworks/lsl.framework/Versions/A/lsl
elif [[ "${{ matrix.config.name }}" == "iOS" || "${{ matrix.config.name }}" == "iOS Simulator" ]]; then
codesign -vvv --force --deep --sign "$APPLE_CODE_SIGN_IDENTITY_APP" \
install/Frameworks/lsl.framework/lsl
codesign -vvv --verify --deep --strict install/Frameworks/lsl.framework/lsl
fi
codesign -vvv --force --deep --sign "$APPLE_CODE_SIGN_IDENTITY_APP" \
--entitlements lsl.entitlements --options runtime \
install/Frameworks/lsl.framework
codesign -vvv --verify --deep --strict install/Frameworks/lsl.framework

# run internal tests
- name: unit tests
if: matrix.config.name == 'macOS-latest'
run: |
mkdir -p dumps
install/bin/lsl_test_internal --order rand --wait-for-keypress never --durations yes
install/bin/lsl_test_exported --order rand --wait-for-keypress never --durations yes
timeout-minutes: 10

- name: Package and Notarize macOS Installer
if: matrix.config.name == 'macOS-latest'
env:
APPLE_DEVELOPMENT_TEAM: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
APPLE_NOTARIZE_USERNAME: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
run: |
# Get the version number from the framework's Info.plist
LSL_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" install/Frameworks/lsl.framework/Versions/A/Resources/Info.plist)
echo "LSL_VERSION=$LSL_VERSION" >> $GITHUB_ENV
echo "Debug: LSL_VERSION=$LSL_VERSION"

mkdir -p package
productbuild --sign "$APPLE_CODE_SIGN_IDENTITY_INST" \
--component install/Frameworks/lsl.framework \
/Library/Frameworks package/liblsl-${LSL_VERSION}-Darwin-universal.pkg
# Notarize the package
xcrun notarytool submit package/liblsl-${LSL_VERSION}-Darwin-universal.pkg \
--apple-id "$APPLE_NOTARIZE_USERNAME" \
--password "$APPLE_NOTARIZE_PASSWORD" \
--team-id "$APPLE_DEVELOPMENT_TEAM" \
--wait
# Staple the notarization ticket to the package
xcrun stapler staple package/liblsl-${LSL_VERSION}-Darwin-universal.pkg

- name: upload dump
if: failure()
uses: actions/upload-artifact@v4
with:
name: dumps-${{ matrix.config.name }}
path: dumps

- name: Zip LSL Framework
run: |
cd install/Frameworks
zip -ry lsl.framework.zip lsl.framework
cd ../..

- name: Upload macOS Package and Framework
if: matrix.config.name == 'macOS-latest'
uses: actions/upload-artifact@v4
with:
name: build-macOS-latest
path: |
package/*.pkg
install/Frameworks/lsl.framework.zip
# Note: the artifact will preserve the folder structure up to the common root, in this case all.

- name: Upload iOS Framework
if: matrix.config.name == 'iOS'
uses: actions/upload-artifact@v4
with:
name: build-iOS
path: install/Frameworks/lsl.framework.zip
# Note: the artifact drops the folder structure and only keeps the zip.

- name: Upload iOS Simulator Framework
if: matrix.config.name == 'iOS Simulator'
uses: actions/upload-artifact@v4
with:
name: build-iOS-Simulator
path: install/Frameworks/lsl.framework.zip

xcframework_and_deploy:
name: XCFramework and Deploy
needs: build
runs-on: macOS-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: build-macOS-latest
path: build-macOS-latest
- uses: actions/download-artifact@v4
with:
name: build-iOS
path: build-iOS
- uses: actions/download-artifact@v4
with:
name: build-iOS-Simulator
path: build-iOS-Simulator

- name: Unzip macOS Framework
run: |
unzip build-macOS-latest/install/Frameworks/lsl.framework.zip -d build-macOS-latest/Frameworks

- name: Unzip iOS Framework
run: |
unzip build-iOS/lsl.framework.zip -d build-iOS/Frameworks

- name: Unzip iOS Simulator Framework
run: |
unzip build-iOS-Simulator/lsl.framework.zip -d build-iOS-Simulator/Frameworks

- name: Install certificates and provisioning profiles
uses: ./.github/actions/install-apple-certs
with:
MACOS_CERTIFICATE_APP: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_INST: ${{ secrets.PROD_MACOS_CERTIFICATE_INST }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}

- name: Create and Sign XCFramework
run: |
xcodebuild -create-xcframework \
-framework build-macOS-latest/Frameworks/lsl.framework \
-framework build-iOS/Frameworks/lsl.framework \
-framework build-iOS-Simulator/Frameworks/lsl.framework \
-output lsl.xcframework

codesign -vvv --force --deep --sign "$APPLE_CODE_SIGN_IDENTITY_APP" lsl.xcframework
echo "✅ Verifying binary signatures in XCFramework..."
codesign -vvv --verify --deep --strict lsl.xcframework

ditto -c -k --sequesterRsrc --keepParent lsl.xcframework lsl.xcframework.$LSL_VERSION.zip

- name: upload artifacts
uses: actions/upload-artifact@v4
with:
name: mac-packages
path: |
lsl.xcframework.*.zip
package/

- name: upload to release page
if: github.event_name == 'release'
env:
TOKEN: "token ${{ secrets.GITHUB_TOKEN }}"
TAG: ${{ github.event.release.tag_name }}
UPLOAD_URL: ${{ github.event.release.upload_url }}
run: |
UPLOAD_URL=${UPLOAD_URL%\{*} # remove "{name,label}" suffix
for pkg in lsl.xcframework.zip package/*.*; do
NAME=$(basename $pkg)
MIME=$(file --mime-type $pkg|cut -d ' ' -f2)
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: $TOKEN" -H "Content-Type: $MIME" --data-binary @$pkg $UPLOAD_URL?name=$NAME
done
36 changes: 19 additions & 17 deletions .github/workflows/cppcmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ name: C/C++ CI

on:
push:
branches: ['*']
branches:
- main
- dev
tags: ['*']
paths:
- '**'
Expand Down Expand Up @@ -39,18 +41,10 @@ jobs:
- {name: "ubuntu-24.04", os: "ubuntu-24.04", cmake_extra: "-DLSL_BUNDLED_PUGIXML=OFF" }
- {name: "windows-x64", os: "windows-latest", cmake_extra: "-T v142,host=x86"}
- {name: "windows-32", os: "windows-latest", cmake_extra: "-T v142,host=x86 -A Win32"}
- {name: "macOS-latest", os: "macOS-latest"}

# runs all steps in the container configured in config.docker or as subprocesses when empty
container: ${{ matrix.config.docker }}

steps:
- uses: actions/checkout@v4
- name: set up build environment in container
run: |
set -x
apt update
apt install -y --no-install-recommends g++ git ninja-build file dpkg-dev lsb-release sudo curl cmake libpugixml-dev
if: ${{ matrix.config.docker }}

- name: Configure CMake
run: |
if [[ "${{ matrix.config.name }}" = ubuntu-2* ]]; then
Expand All @@ -67,9 +61,13 @@ jobs:
-Dlslgitbranch=${{ github.ref }} \
${{ matrix.config.cmake_extra }} \
${{ github.event.inputs.cmakeextra }}
echo ${PWD}
echo ${PWD}

- name: make
run: cmake --build build --target install --config Release -j
run: cmake --build build --config Release -j

- name: make install
run: cmake --build build --config Release --target install

- name: test install using examples
run: |
Expand All @@ -82,7 +80,7 @@ jobs:
${{ github.event.inputs.cmakeextra }}
cmake --build examples/build --target install --config Release -j
./examples/build/install/bin/HandleMetaData

- name: package
run: |
echo $GITHUB_REF
Expand All @@ -101,6 +99,7 @@ jobs:
fi
cmake -E remove_directory package/_CPack_Packages
cp testing/lslcfgs/default.cfg .

- name: upload install dir
uses: actions/upload-artifact@master
with:
Expand All @@ -112,6 +111,7 @@ jobs:
with:
name: pkg-${{ matrix.config.name }}
path: package

- name: print network config
run: |
which ifconfig && ifconfig
Expand All @@ -121,24 +121,26 @@ jobs:
ip route
ip -6 route
fi
# run internal tests, ignore test failures on docker (missing IPv6 connectivity)

# run internal tests
- name: unit tests
run: |
if [[ "${{ matrix.config.name }}" = ubuntu-2* ]]; then
ulimit -c unlimited
echo "$PWD/dumps/corefile-%e-%p-%t" | sudo tee /proc/sys/kernel/core_pattern
fi
mkdir -p dumps
install/bin/lsl_test_internal --order rand --wait-for-keypress never --durations yes || test ! -z "${{ matrix.config.docker }}"
install/bin/lsl_test_internal --order rand --wait-for-keypress never --durations yes
install/bin/lsl_test_exported --order rand --wait-for-keypress never --durations yes
timeout-minutes: 10

- name: upload dump
if: failure()
uses: actions/upload-artifact@master
with:
name: dumps-${{ matrix.config.name }}
path: dumps

- name: upload to release page
if: github.event_name == 'release'
env:
Expand Down
Loading
Loading