diff --git a/.github/workflows/deploy-ios.yml b/.github/workflows/deploy-ios.yml new file mode 100644 index 0000000..3151256 --- /dev/null +++ b/.github/workflows/deploy-ios.yml @@ -0,0 +1,167 @@ +name: deploy-ios + +on: + workflow_call: + inputs: + project-name: + description: 'Name of the application to build (e.g. qTox).' + required: false + type: string + cmake-args: + description: 'Arguments to pass to CMake.' + required: false + type: string + need-qt: + description: | + Whether the project needs Qt (built from source); default is + true. Set to false if the project doesn't need Qt to save time. + required: false + type: boolean + default: true + +jobs: + build: + name: Build + strategy: + matrix: + arch: [arm64, x86_64, arm64-iphonesimulator] + ios: ["15.0"] + exclude: + - arch: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'packaging') && 'x86_64' }} + - arch: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'packaging') && 'arm64-iphonesimulator' }} + runs-on: macos-14 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + - name: Checkout ci-tools + if: github.repository != 'TokTok/ci-tools' + uses: actions/checkout@v4 + with: + repository: TokTok/ci-tools + path: third_party/ci-tools + submodules: true + - name: Link ci-tools + if: github.repository == 'TokTok/ci-tools' + run: ln -s .. third_party/ci-tools + - name: Checkout dockerfiles + uses: actions/checkout@v4 + with: + repository: TokTok/dockerfiles + path: third_party/dockerfiles + submodules: true + + - name: Determine artifact file name + id: artifact + run: | + PROJECT_NAME="${{ inputs.project-name }}" + if [ -z "$PROJECT_NAME" ]; then + PROJECT_NAME="$(pcre2grep -M -o1 'project\(\s*(\S+)' CMakeLists.txt)" + fi + echo "project-name=$PROJECT_NAME" >>$GITHUB_OUTPUT + + ARTIFACT="$PROJECT_NAME-${{ matrix.arch }}-${{ matrix.ios }}.ipa" + echo "artifact=$ARTIFACT" >>$GITHUB_OUTPUT + echo "artifact-ref=$PROJECT_NAME-${{ github.sha }}-ios-${{ matrix.ios }}-${{ matrix.arch }}" >>$GITHUB_OUTPUT + + if [ "${{ matrix.arch }}" = "x86_64" ]; then + echo "qt_arch=iphonesimulator-x86_64" >>$GITHUB_OUTPUT + elif [ "${{ matrix.arch }}" = "arm64-iphonesimulator" ]; then + echo "qt_arch=iphonesimulator-arm64" >>$GITHUB_OUTPUT + else + echo "qt_arch=ios-arm64" >>$GITHUB_OUTPUT + fi + - name: Download Qt + if: inputs.need-qt + run: | + third_party/dockerfiles/qtox/deps/download_qt.sh \ + --arch ${{ steps.artifact.outputs.qt_arch }} \ + --dep-prefix /Users/runner/work/deps + mkdir -p /Users/runner/work/host-qt + third_party/dockerfiles/qtox/deps/download_qt.sh \ + --arch $(uname -m) \ + --macos-version 12.0 \ + --dep-prefix /Users/runner/work/host-qt + - name: Cache dependencies (except Qt) + id: cache-deps + uses: actions/cache@v4 + with: + path: | + /Users/runner/work/deps/bin + /Users/runner/work/deps/include + /Users/runner/work/deps/lib + /Users/runner/work/deps/share + key: ${{ github.job }}-ios-distributable-${{ matrix.arch }}-${{ matrix.ios }}-deps + - name: Homebrew dependencies to build dependencies + run: brew bundle --file platform/ios/Brewfile-static + - name: Build dependencies + if: steps.cache-deps.outputs.cache-hit != 'true' + run: third_party/dockerfiles/qtox/deps/local_install_deps.sh + --arch ${{ steps.artifact.outputs.qt_arch }} + --dep-file platform/deps.depfile + --dep-prefix /Users/runner/work/deps + - name: Cache compiler output + uses: actions/cache@v4 + with: + path: .cache/ccache + key: ${{ github.job }}-ios-distributable-${{ matrix.arch }}-${{ matrix.ios }}-ccache + - name: Set up ccache + run: ccache + --set-config=max_size=200M + --set-config=cache_dir="$PWD/.cache/ccache" && ccache --show-config + - name: Build application bundle + run: third_party/ci-tools/platform/ios/build.sh + --project-name ${{ steps.artifact.outputs.project-name }} + --arch "${{ matrix.arch }}" + --ios-version "${{ matrix.ios }}" + --dep-prefix /Users/runner/work/deps + --host-path /Users/runner/work/host-qt/qt + -- + ${{ inputs.cmake-args }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.artifact.outputs.artifact-ref }} + path: | + ${{ steps.artifact.outputs.artifact }} + ${{ steps.artifact.outputs.artifact }}.sha256 + if-no-files-found: error + - name: Get tag name for release file name + if: contains(github.ref, 'refs/tags/v') + id: get_version + run: | + VERSION="$(echo "$GITHUB_REF" | cut -d / -f 3)" + echo "version_tag=$VERSION" >>$GITHUB_OUTPUT + echo "release_artifact=${{ steps.artifact.outputs.project-name }}-$VERSION.${{ matrix.arch }}-${{ matrix.ios }}.ipa" >>$GITHUB_OUTPUT + - name: Rename artifact for release upload + if: contains(github.ref, 'refs/tags/v') + run: | + cp "${{ steps.artifact.outputs.artifact }}" "${{ steps.get_version.outputs.release_artifact }}" + cp "${{ steps.artifact.outputs.artifact }}.sha256" "${{ steps.get_version.outputs.release_artifact }}.sha256" + - name: Upload to versioned release + if: contains(github.ref, 'refs/tags/v') + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + draft: true + artifacts: "${{ steps.get_version.outputs.release_artifact }},${{ steps.get_version.outputs.release_artifact }}.sha256" + - name: Rename artifact for nightly upload + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + run: | + cp "${{ steps.artifact.outputs.artifact }}" ${{ steps.artifact.outputs.project-name }}-nightly-${{ matrix.arch }}-${{ matrix.ios }}.ipa + cp "${{ steps.artifact.outputs.artifact }}.sha256" ${{ steps.artifact.outputs.project-name }}-nightly-${{ matrix.arch }}-${{ matrix.ios }}.ipa.sha256 + - name: Upload to nightly release + uses: ncipollo/release-action@v1 + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + with: + allowUpdates: true + tag: nightly + omitBodyDuringUpdate: true + omitNameDuringUpdate: true + prerelease: true + replacesArtifacts: true + artifacts: "${{ steps.artifact.outputs.project-name }}-nightly-${{ matrix.arch }}-${{ matrix.ios }}.ipa,${{ steps.artifact.outputs.project-name }}-nightly-${{ matrix.arch }}-${{ matrix.ios }}.ipa.sha256" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d2ab678..6ce43be 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -59,6 +59,11 @@ jobs: smoke-test: test/smoke-test.sh test-files: test/smoke-test.sh + ios: + name: iOS + uses: ./.github/workflows/deploy-ios.yml + needs: [prepare] + windows: name: Windows uses: ./.github/workflows/deploy-windows.yml diff --git a/platform/ios/Brewfile-static b/platform/ios/Brewfile-static new file mode 100644 index 0000000..e4ee1a0 --- /dev/null +++ b/platform/ios/Brewfile-static @@ -0,0 +1,10 @@ +# Dependencies for building all dependencies from source as static libraries. +# Needed for distributable builds. +brew "bash" # macOS bash is too old +brew "cmake" +brew "git" +brew "ninja" +brew "pcre2" # for pcre2grep +brew "zip" +# accelerate builds with ccache +brew "ccache" diff --git a/platform/ios/Info.plist b/platform/ios/Info.plist new file mode 100644 index 0000000..af0d0df --- /dev/null +++ b/platform/ios/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDisplayName + CiTools + CFBundleExecutable + citools + CFBundleIdentifier + chat.tox.citools + CFBundleName + CiTools + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.0.1 + CFBundleSignature + toxcitools + CFBundleVersion + 0.0.1 + LSRequiresIPhoneOS + + MinimumOSVersion + 15.0 + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/platform/ios/build.sh b/platform/ios/build.sh new file mode 100755 index 0000000..4790dfb --- /dev/null +++ b/platform/ios/build.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright © 2026 The TokTok team + +# Fail out on error +set -eux -o pipefail + +IOS_ARCH="arm64" +IOS_VERSION="15.0" +IOS_PLATFORM="iphoneos" +HOST_PATH="" + +GIT_ROOT=$(git rev-parse --show-toplevel) +DEP_PREFIX="$GIT_ROOT/third_party/deps" + +usage() { + echo "Usage: $0 --project-name [options]" + echo "Options:" + echo " --project-name Name of the project (required)" + echo " --dep-prefix Dependency prefix (default: third_party/deps)" + echo " --arch Architecture (arm64 or x86_64)" + echo " --ios-version iOS version (default: 15.0)" + echo " --host-path Host Qt path (optional)" + echo " --help, -h Show this help message" +} + +while (($# > 0)); do + case $1 in + --project-name) + PROJECT_NAME=$2 + shift 2 + ;; + --dep-prefix) + DEP_PREFIX=$2 + shift 2 + ;; + --arch) + IOS_ARCH=$2 + shift 2 + ;; + --ios-version) + IOS_VERSION=$2 + shift 2 + ;; + --host-path) + HOST_PATH=$2 + shift 2 + ;; + --help | -h) + usage + exit 1 + ;; + --) + shift + break + ;; + *) + echo "Unexpected argument $1" + usage + exit 1 + ;; + esac +done + +if [ -z "${PROJECT_NAME+x}" ]; then + echo "--project-name is a required argument" + usage + exit 1 +fi + +if [[ "$IOS_ARCH" == *"iphonesimulator"* ]] || [[ "$IOS_ARCH" == "x86_64" ]]; then + IOS_PLATFORM="iphonesimulator" +fi + +# Extract the base architecture (e.g., "arm64" from "arm64-iphonesimulator") +CMAKE_ARCH="${IOS_ARCH%-iphonesimulator}" + +readonly BIN_NAME="$PROJECT_NAME-$IOS_ARCH-$IOS_VERSION.ipa" +CMAKE="$DEP_PREFIX/qt/bin/qt-cmake" +PREFIX_PATH="$DEP_PREFIX" + +# Build project. +ccache --zero-stats + +# We use the Xcode generator for iOS to ensure proper bundle structure. +# CODE_SIGNING_ALLOWED=NO allows us to build in CI without a developer cert. +# ONLY_ACTIVE_ARCH=NO ensures we build for the specified architecture. +EXTRA_CMAKE_ARGS=() +if [ -f "platform/ios/Info.plist" ]; then + EXTRA_CMAKE_ARGS+=("-DCMAKE_MACOSX_BUNDLE_INFO_PLIST=$(realpath platform/ios/Info.plist)") +fi + +if [ -n "$HOST_PATH" ]; then + EXTRA_CMAKE_ARGS+=("-DQT_HOST_PATH=$HOST_PATH") +fi + +"$CMAKE" \ + -DCMAKE_CXX_FLAGS="-isystem/usr/local/include" \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_OSX_SYSROOT="$IOS_PLATFORM" \ + -DCMAKE_OSX_ARCHITECTURES="$CMAKE_ARCH" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="$IOS_VERSION" \ + -DCMAKE_PREFIX_PATH="$PREFIX_PATH" \ + -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO \ + -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY="" \ + -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=NO \ + -DCMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT="dwarf" \ + -DCMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH=NO \ + "${EXTRA_CMAKE_ARGS[@]}" \ + -G Xcode \ + -B_build \ + "$@" \ + . + +cmake --build _build --config Release + +# Packaging into IPA +mkdir -p _build/Payload +# Search for the .app bundle. Xcode output paths can vary depending on project structure. +APP_BUNDLE=$(find "_build/Release-$IOS_PLATFORM" -name "*.app" -type d | head -n 1) +if [ -z "$APP_BUNDLE" ]; then + echo "Could not find .app bundle in _build/Release-$IOS_PLATFORM" + exit 1 +fi + +cp -r "$APP_BUNDLE" _build/Payload/ +pushd _build +zip -r "../$BIN_NAME" Payload +popd + +ccache --show-stats + +# Check if the binary exists. +if [[ ! -s "$BIN_NAME" ]]; then + echo "There's no $BIN_NAME!" + exit 1 +fi + +# Create a sha256 checksum. +shasum -a 256 "$BIN_NAME" >"$BIN_NAME".sha256