name: Build iOS on: workflow_dispatch: inputs: patch: description: "Optional patch number, matching the main build workflow." required: false type: string default: "0" workflow_call: inputs: source_tag: description: "Existing source tag for a standalone iOS release." required: false type: string default: "" patch: description: "Optional patch number from the main build workflow." required: false type: string default: "0" outputs: version: description: "Independent iOS version and build date." value: ${{ jobs.build.outputs.version }} pull_request: paths: - ".github/workflows/build-ios.yml" - ".github/workflows/build.yml" - ".github/workflows/release-mobile.yml" - ".gitmodules" - "LICENSE" - "apps/ios/**" - "deps/submodules/minirtc" - "libs/wire/**" - "xmake.lua" permissions: contents: read jobs: build: name: Build on iOS (arm64) runs-on: macos-26 timeout-minutes: 120 outputs: version: ${{ steps.ios_version.outputs.version }} env: GITHUB_TOKEN: ${{ github.token }} CROSSDESK_SOURCE_TAG: ${{ inputs.source_tag || '' }} IOS_DEPLOYMENT_TARGET: "16.0" EXPECTED_IOS_SDK: "26.5" XMAKE_VERSION: "3.1.1" XMAKE_REPO_REVISION: "121109352da272cd1a3d4ed52f2dcda34f839955" steps: - name: Extract version number shell: bash env: PATCH_NUMBER: ${{ inputs.patch || '0' }} run: | VERSION_REF="${CROSSDESK_SOURCE_TAG:-${GITHUB_REF##*/}}" VERSION_REF="${VERSION_REF#ios-}" VERSION_BASE="${VERSION_REF#v}" BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then PATCH_NUMBER=0 fi if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then VERSION_BASE="${BASH_REMATCH[1]}" PATCH_NUMBER="${BASH_REMATCH[3]}" BUILD_DATE="${BASH_REMATCH[4]}" elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then VERSION_BASE="${BASH_REMATCH[1]}" BUILD_DATE="${BASH_REMATCH[3]}" PATCH_NUMBER="${BASH_REMATCH[4]}" elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then VERSION_BASE="${BASH_REMATCH[1]}" BUILD_DATE="${BASH_REMATCH[3]}" elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then VERSION_BASE="${BASH_REMATCH[1]}" PATCH_NUMBER="${BASH_REMATCH[3]}" fi if [[ "${PATCH_NUMBER}" != "0" ]]; then VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}" else VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}" fi echo "VERSION_NUM=${VERSION_NUM}" >> "${GITHUB_ENV}" echo "VERSION_NUM=${VERSION_NUM}" - name: Select Xcode 26.6 shell: bash run: | sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer xcodebuild -version SDK_VERSION="$(xcrun --sdk iphoneos --show-sdk-version)" if [[ "${SDK_VERSION}" != "${EXPECTED_IOS_SDK}" ]]; then echo "Unexpected iOS SDK: expected ${EXPECTED_IOS_SDK}, got ${SDK_VERSION}" >&2 exit 1 fi - name: Checkout code uses: actions/checkout@v5 with: fetch-depth: 0 ref: ${{ inputs.source_tag || github.ref }} - name: Select corresponding source tag shell: bash run: | if [[ -n "${CROSSDESK_SOURCE_TAG}" ]]; then echo "CROSSDESK_RELEASE_BUILD=YES" >> "${GITHUB_ENV}" elif [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then echo "CROSSDESK_SOURCE_TAG=${GITHUB_REF_NAME}" >> "${GITHUB_ENV}" echo "CROSSDESK_RELEASE_BUILD=YES" >> "${GITHUB_ENV}" fi - name: Read independent iOS version id: ios_version shell: bash run: | IOS_VERSION_BASE="$(xcodebuild \ -project apps/ios/CrossDeskMobile.xcodeproj \ -scheme CrossDeskMobile \ -configuration Release \ -destination 'generic/platform=iOS' \ -showBuildSettings CODE_SIGNING_ALLOWED=NO | \ awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }')" if [[ ! "${IOS_VERSION_BASE}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Invalid iOS marketing version: ${IOS_VERSION_BASE}" >&2 exit 1 fi VERSION_NUM="v${IOS_VERSION_BASE}-${VERSION_NUM##*-}" if [[ "${CROSSDESK_SOURCE_TAG}" == ios-v* && "${CROSSDESK_SOURCE_TAG#ios-}" != "${VERSION_NUM}" ]]; then echo "iOS source tag does not match the app version: ${CROSSDESK_SOURCE_TAG} vs ${VERSION_NUM}" >&2 exit 1 fi echo "IOS_VERSION_BASE=${IOS_VERSION_BASE}" >> "${GITHUB_ENV}" echo "VERSION_NUM=${VERSION_NUM}" >> "${GITHUB_ENV}" echo "version=${VERSION_NUM}" >> "${GITHUB_OUTPUT}" - name: Initialize submodules run: git submodule update --init --recursive - name: Cache xmake dependencies uses: actions/cache@v5 with: path: apps/ios/.xmake/packages key: "${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-source-v1-xcode26.6-xrepo1211093-min16-${{ github.run_id }}" restore-keys: | ${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-source-v1-xcode26.6-xrepo1211093-min16- - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.13" - name: Verify iOS license catalog run: | python3 -B -m unittest discover -s apps/ios/tests -p 'third_party_licenses_test.py' -v python3 -B apps/ios/scripts/generate_third_party_licenses.py - name: Verify iOS privacy controls run: python3 -B -m unittest discover -s apps/ios/tests -p 'privacy_controls_test.py' -v - name: Install xmake uses: xmake-io/github-action-setup-xmake@v1 with: xmake-version: "${{ env.XMAKE_VERSION }}" - name: Verify xmake version shell: bash run: | XMAKE_VERSION_OUTPUT="$(xmake --version)" echo "${XMAKE_VERSION_OUTPUT}" if [[ "${XMAKE_VERSION_OUTPUT}" != *"xmake v${XMAKE_VERSION}"* ]]; then echo "Unexpected xmake version: expected ${XMAKE_VERSION}" >&2 exit 1 fi - name: Pin xmake package repository shell: bash run: | xmake repo -u XMAKE_REPO_DIR="${HOME}/.xmake/repositories/xmake-repo" git -C "${XMAKE_REPO_DIR}" fetch --depth=1 origin "${XMAKE_REPO_REVISION}" git -C "${XMAKE_REPO_DIR}" checkout --detach "${XMAKE_REPO_REVISION}" test "$(git -C "${XMAKE_REPO_DIR}" rev-parse HEAD)" = "${XMAKE_REPO_REVISION}" - name: Build CrossDesk Mobile shell: bash run: | DERIVED_DATA="${RUNNER_TEMP}/CrossDeskMobileDerivedData" echo "DERIVED_DATA=${DERIVED_DATA}" >> "${GITHUB_ENV}" xcodebuild \ -project apps/ios/CrossDeskMobile.xcodeproj \ -scheme CrossDeskMobile \ -configuration Release \ -destination 'generic/platform=iOS' \ -derivedDataPath "${DERIVED_DATA}" \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ COMPILER_INDEX_STORE_ENABLE=NO \ IPHONEOS_DEPLOYMENT_TARGET="${IOS_DEPLOYMENT_TARGET}" \ build - name: Verify CrossDesk Mobile shell: bash run: | APP_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app" EXECUTABLE_PATH="${APP_PATH}/CrossDeskMobile" INFO_PLIST="${APP_PATH}/Info.plist" if [[ ! -x "${EXECUTABLE_PATH}" ]]; then echo "CrossDeskMobile executable is missing from ${APP_PATH}" >&2 exit 1 fi xcrun lipo "${EXECUTABLE_PATH}" -verify_arch arm64 APP_VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "${INFO_PLIST}") if [[ "${APP_VERSION}" != "${IOS_VERSION_BASE}" ]]; then echo "Unexpected iOS app version: expected ${IOS_VERSION_BASE}, got ${APP_VERSION}" >&2 exit 1 fi MINIMUM_OS=$(/usr/libexec/PlistBuddy -c 'Print :MinimumOSVersion' "${INFO_PLIST}") if [[ "${MINIMUM_OS}" != "${IOS_DEPLOYMENT_TARGET}" ]]; then echo "Unexpected minimum iOS version: expected ${IOS_DEPLOYMENT_TARGET}, got ${MINIMUM_OS}" >&2 exit 1 fi VERIFY_FLAGS=() if [[ "${CROSSDESK_RELEASE_BUILD:-NO}" == "YES" ]]; then VERIFY_FLAGS+=(--require-release) fi python3 apps/ios/scripts/verify_app_bundle.py "${APP_PATH}" "${VERIFY_FLAGS[@]}" xcrun lipo -info "${EXECUTABLE_PATH}" echo "Minimum iOS version: ${MINIMUM_OS}" - name: Package CrossDesk Mobile shell: bash run: | APP_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app" PACKAGE_DIR="${RUNNER_TEMP}/crossdesk-ios-arm64-unsigned-${VERSION_NUM}" PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-ios-arm64-unsigned-${VERSION_NUM}.zip" mkdir -p "${PACKAGE_DIR}" cp -R "${APP_PATH}" "${PACKAGE_DIR}/" cp apps/ios/licenses/sources.json "${PACKAGE_DIR}/ThirdPartySources.json" cp apps/ios/licenses/OPEN_SOURCE_NOTICE.txt "${PACKAGE_DIR}/OPEN_SOURCE_NOTICE.txt" cp "${APP_PATH}/SourceMetadata.json" "${PACKAGE_DIR}/SourceMetadata.json" DSYM_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app.dSYM" if [[ -d "${DSYM_PATH}" ]]; then cp -R "${DSYM_PATH}" "${PACKAGE_DIR}/" fi ditto -c -k --sequesterRsrc --keepParent "${PACKAGE_DIR}" "${PACKAGE_FILE}" echo "PACKAGE_FILE=${PACKAGE_FILE}" >> "${GITHUB_ENV}" - name: Upload build artifact uses: actions/upload-artifact@v6 with: name: crossdesk-ios-arm64-unsigned-${{ env.VERSION_NUM }} path: ${{ env.PACKAGE_FILE }} if-no-files-found: error