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