diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf7e5c89..59bcd29b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,12 +17,16 @@ on: - Binary - Android - Apple - - app-store + - Apple-release - iOS - macOS - tvOS - macOS-standalone - publish-android + test_message: + description: "TestFlight test message" + required: false + type: string push: branches: - stable @@ -48,6 +52,19 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- + - name: Cache Go tool builds + uses: actions/cache/restore@v4 + with: + path: ~/.cache/go-build + key: go-build-tools-linux-${{ hashFiles('go.sum') }} + restore-keys: go-build-tools-linux- - name: Check input version if: github.event_name == 'workflow_dispatch' run: |- @@ -64,10 +81,11 @@ jobs: build: name: Build binary if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary' - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 needs: - calculate_version strategy: + max-parallel: 13 matrix: include: - { os: linux, arch: amd64, variant: purego, naive: true } @@ -119,12 +137,19 @@ jobs: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Setup Go if: ${{ ! matrix.legacy_win7 }} uses: actions/setup-go@v5 with: go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- - name: Cache Go for Windows 7 if: matrix.legacy_win7 id: cache-go-for-windows7 @@ -149,7 +174,6 @@ jobs: uses: nttld/setup-ndk@v1 with: ndk-version: r28 - local-cache: true - name: Clone cronet-go if: matrix.naive run: | @@ -167,17 +191,6 @@ jobs: rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg cd ~/cronet-go GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh - - name: Cache Chromium toolchain - if: matrix.naive - id: cache-chromium-toolchain - uses: actions/cache@v4 - with: - path: | - ~/cronet-go/naiveproxy/src/third_party/llvm-build/ - ~/cronet-go/naiveproxy/src/gn/out/ - ~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/ - ~/cronet-go/naiveproxy/src/out/sysroot-build/ - key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }} - name: Download Chromium toolchain if: matrix.naive run: | @@ -335,7 +348,6 @@ jobs: run: | set -xeuo pipefail sudo gem install fpm - sudo apt-get update sudo apt-get install -y debsigs cp .fpm_systemd .fpm fpm -t deb \ @@ -343,8 +355,6 @@ jobs: -p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \ --architecture ${{ matrix.debian }} \ dist/sing-box=/usr/bin/sing-box - curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff' - sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff' rm -rf $HOME/.gnupg gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <> "$env:GITHUB_ENV" @@ -634,31 +659,46 @@ jobs: with: name: binary-windows_${{ matrix.arch }} path: "dist" - build_android: - name: Build Android - if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable' + build_android_library: + name: Build Android library (${{ matrix.go_arch }}) + if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android' || inputs.build == 'publish-android') && github.ref != 'refs/heads/oldstable' runs-on: ubuntu-latest needs: - calculate_version + strategy: + fail-fast: false + matrix: + go_arch: + - "386" + - amd64 + - arm + - arm64 steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 - submodules: 'recursive' + fetch-depth: 1 - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 with: ndk-version: r28 - name: Setup OpenJDK - run: |- - sudo apt update && sudo apt install -y openjdk-17-jdk-headless - /usr/lib/jvm/java-17-openjdk-amd64/bin/java --version + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + with: + distribution: temurin + java-version: 17 - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" @@ -667,10 +707,82 @@ jobs: run: |- make lib_install export PATH="$PATH:$(go env GOPATH)/bin" - make lib_android + go run ./cmd/internal/build_libbox -target android -platform android/${{ matrix.go_arch }} + mkdir -p dist/${{ matrix.go_arch }} + mv libbox.aar libbox-legacy.aar dist/${{ matrix.go_arch }} env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + - name: Upload library + uses: actions/upload-artifact@v4 + with: + name: libbox-android-${{ matrix.go_arch }} + path: dist + compression-level: 0 + build_android: + name: Build Android (${{ matrix.variant }}) + if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable' + runs-on: ubuntu-latest + needs: + - calculate_version + - build_android_library + strategy: + fail-fast: false + matrix: + include: + - variant: other + task: :app:assembleOtherRelease + output: app/build/outputs/apk/other/release/*.apk + - variant: other-legacy + task: :app:assembleOtherLegacyRelease + output: app/build/outputs/apk/otherLegacy/release/*.apk + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + fetch-depth: 1 + - name: Checkout Android client + run: git submodule update --init clients/android + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- + - name: Cache Go tool builds + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: go-build-tools-linux-${{ hashFiles('go.sum') }} + restore-keys: go-build-tools-linux- + - name: Setup Android NDK + id: setup-ndk + uses: nttld/setup-ndk@v1 + with: + ndk-version: r28 + - name: Setup OpenJDK + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + with: + distribution: temurin + java-version: 17 + - name: Set tag + run: |- + git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" + git tag v${{ needs.calculate_version.outputs.version }} -f + - name: Download libraries + uses: actions/download-artifact@v4 + with: + pattern: libbox-android-* + path: libbox-inputs + merge-multiple: true + - name: Merge libraries + run: |- + go run ./cmd/internal/merge_aar -output libbox.aar libbox-inputs/*/libbox.aar + go run ./cmd/internal/merge_aar -output libbox-legacy.aar libbox-inputs/*/libbox-legacy.aar - name: Checkout main branch if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch' run: |- @@ -685,11 +797,10 @@ jobs: run: |- cd clients/android git submodule update --init --recursive -v - - name: Gradle cache - uses: actions/cache@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: - path: ~/.gradle - key: gradle-${{ hashFiles('**/*.gradle') }} + cache-provider: basic - name: Update version if: github.event_name == 'workflow_dispatch' run: |- @@ -700,20 +811,17 @@ jobs: go run -v ./cmd/internal/update_android_version --ci --nightly - name: Build run: |- - mkdir clients/android/app/libs + mkdir -p clients/android/app/libs cp *.aar clients/android/app/libs cd clients/android - ./gradlew :app:assembleOtherRelease :app:assembleOtherLegacyRelease + ./gradlew --build-cache --parallel ${{ matrix.task }} env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} - name: Prepare upload run: |- mkdir -p dist - #cp clients/android/app/build/outputs/apk/play/release/*.apk dist - cp clients/android/app/build/outputs/apk/other/release/*.apk dist - cp clients/android/app/build/outputs/apk/otherLegacy/release/*.apk dist + cp clients/android/${{ matrix.output }} dist VERSION_CODE=$(grep VERSION_CODE clients/android/version.properties | cut -d= -f2) VERSION_NAME=$(grep VERSION_NAME clients/android/version.properties | cut -d= -f2) cat > dist/SFA-version-metadata.json << EOF @@ -726,45 +834,63 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: binary-android-apks + name: binary-android-apks-${{ matrix.variant }} path: 'dist' publish_android: name: Publish Android - if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && github.ref != 'refs/heads/oldstable' + if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/testing') runs-on: ubuntu-latest needs: - calculate_version + - build_android_library steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 - submodules: 'recursive' + fetch-depth: 1 + - name: Checkout Android client + run: git submodule update --init clients/android - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.25.12 + cache: false + - name: Cache Go modules + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-linux-${{ hashFiles('go.sum') }} + restore-keys: go-mod-linux- + - name: Cache Go tool builds + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: go-build-tools-linux-${{ hashFiles('go.sum') }} + restore-keys: go-build-tools-linux- - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 with: ndk-version: r28 - name: Setup OpenJDK - run: |- - sudo apt update && sudo apt install -y openjdk-17-jdk-headless - /usr/lib/jvm/java-17-openjdk-amd64/bin/java --version + uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + with: + distribution: temurin + java-version: 17 - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f - - name: Build library + - name: Download libraries + uses: actions/download-artifact@v4 + with: + pattern: libbox-android-* + path: libbox-inputs + merge-multiple: true + - name: Merge libraries run: |- - make lib_install - export PATH="$PATH:$(go env GOPATH)/bin" - make lib_android - env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + go run ./cmd/internal/merge_aar -output libbox.aar libbox-inputs/*/libbox.aar + go run ./cmd/internal/merge_aar -output libbox-legacy.aar libbox-inputs/*/libbox-legacy.aar - name: Checkout main branch if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch' run: |- @@ -775,74 +901,156 @@ jobs: run: |- cd clients/android git checkout dev - - name: Gradle cache - uses: actions/cache@v4 + - name: Checkout submodule recursive + run: |- + cd clients/android + git submodule update --init --recursive -v + - name: Setup Gradle + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: - path: ~/.gradle - key: gradle-${{ hashFiles('**/*.gradle') }} - - name: Build + cache-provider: basic + - name: Prepare build run: |- go run -v ./cmd/internal/update_android_version --ci - mkdir clients/android/app/libs + mkdir -p clients/android/app/libs cp *.aar clients/android/app/libs cd clients/android echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json - ./gradlew :app:publishPlayReleaseBundle + chmod 600 service-account-credentials.json + jq -e ' + .type == "service_account" and + (.project_id | type == "string" and length > 0) and + (.private_key | type == "string" and length > 0) and + (.client_email | type == "string" and length > 0) and + (.token_uri | type == "string" and length > 0) + ' service-account-credentials.json > /dev/null + jq -r '.private_key' service-account-credentials.json | openssl pkey -check -noout env: - JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }} - build_apple: - name: Build Apple clients - runs-on: macos-26 - if: false # github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone' + - name: Publish to Google Play + run: |- + cd clients/android + ./gradlew --build-cache --parallel :app:publishPlayReleaseBundle + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} + build_apple_library: + name: Build Apple library (${{ matrix.artifact }}) + runs-on: ${{ matrix.build && 'macos-26' || 'ubuntu-latest' }} + if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone' needs: - calculate_version strategy: + fail-fast: false matrix: include: - - name: iOS - if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }} - platform: ios - scheme: SFI - destination: 'generic/platform=iOS' - archive: build/SFI.xcarchive - upload: SFI/Upload.plist - - name: macOS - if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }} - platform: macos - scheme: SFM - destination: 'generic/platform=macOS' - archive: build/SFM.xcarchive - upload: SFI/Upload.plist - - name: tvOS - if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }} - platform: tvos - scheme: SFT - destination: 'generic/platform=tvOS' - archive: build/SFT.xcarchive - upload: SFI/Upload.plist - - name: macOS-standalone - if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }} - platform: macos - scheme: SFM.System - destination: 'generic/platform=macOS' - archive: build/SFM.System.xcarchive - export: SFM.System/Export.plist - export_path: build/SFM.System + - artifact: ios-arm64 + platform: ios/arm64 + build: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' }} + - artifact: tvos-arm64 + platform: tvos/arm64 + build: ${{ github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'tvOS') }} + - artifact: macos-arm64 + platform: macos/arm64 + build: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'macOS' || inputs.build == 'macOS-standalone' }} + - artifact: macos-amd64 + platform: macos/amd64 + build: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'macOS' || inputs.build == 'macOS-standalone' }} steps: + - name: Checkout + if: matrix.build + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + fetch-depth: 1 + - name: Setup Go + if: matrix.build + uses: actions/setup-go@v5 + with: + go-version: 1.25.12 + cache: false + - name: Cache Go modules + if: matrix.build + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: go-mod-macos-${{ hashFiles('go.sum') }} + restore-keys: go-mod-macos- + - name: Cache Go build + if: matrix.build + uses: actions/cache@v4 + with: + path: ~/Library/Caches/go-build + key: libbox-go-build-${{ matrix.artifact }}-1.25.12-${{ hashFiles('go.sum') }} + restore-keys: libbox-go-build-${{ matrix.artifact }}-1.25.12- + - name: Set tag + if: matrix.build + run: git tag v${{ needs.calculate_version.outputs.version }} -f + - name: Build library + if: matrix.build + run: |- + make lib_install + export PATH="$PATH:$(go env GOPATH)/bin" + go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }} + mkdir -p dist + tar -cf dist/Libbox-${{ matrix.artifact }}.tar Libbox.xcframework + - name: Upload library + if: matrix.build + uses: actions/upload-artifact@v4 + with: + name: libbox-apple-${{ matrix.artifact }} + path: dist + compression-level: 1 + build_apple: + name: Build Apple release clients + runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }} + if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone' + needs: + - calculate_version + - build_apple_library + strategy: + fail-fast: false + matrix: + include: + - name: macOS-standalone + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone' }} + type: standalone + platform: macos + variant: all + library_arch: '*' + scheme: '' + destination: '' + archive: '' + upload: '' + steps: &build_apple_steps - name: Checkout if: matrix.if uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 - submodules: 'recursive' + fetch-depth: 1 + - name: Checkout Apple client + if: matrix.if + run: git submodule update --init --recursive clients/apple - name: Setup Go if: matrix.if uses: actions/setup-go@v5 with: go-version: 1.25.12 + cache: false + - name: Cache Go modules + if: matrix.if + uses: actions/cache/restore@v4 + with: + path: ~/go/pkg/mod + key: go-mod-macos-${{ hashFiles('go.sum') }} + restore-keys: go-mod-macos- + - name: Cache Go tool builds + if: matrix.if + uses: actions/cache@v4 + with: + path: ~/Library/Caches/go-build + key: go-build-tools-macos-${{ hashFiles('go.sum') }} + restore-keys: go-build-tools-macos- - name: Set tag if: matrix.if run: |- @@ -859,52 +1067,139 @@ jobs: run: |- cd clients/apple git checkout dev - - name: Setup certificates + - name: Cache Swift packages if: matrix.if + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/SourcePackages + key: apple-spm-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} + restore-keys: apple-spm- + - name: Cache Xcode build data + if: matrix.if + uses: actions/cache@v4 + with: + path: ${{ matrix.type == 'standalone' && 'clients/apple/build/SFM.System-*.dd' || matrix.type == 'jailbreak' && 'clients/apple/build/jailbreak/DerivedData' || format('{0}/DerivedData', runner.temp) }} + key: apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ github.sha }} + restore-keys: | + apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}- + apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}- + - name: Setup App Store Connect key + if: matrix.if && matrix.type != 'jailbreak' && github.event_name == 'workflow_dispatch' run: |- - CERTIFICATE_PATH=$RUNNER_TEMP/Certificates.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/certificates.keychain-db - echo -n "$CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH + ASC_KEY_PATH=$RUNNER_TEMP/AuthKey.p8 + echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH + echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV" + echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV" + echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV" + env: + ASC_KEY: ${{ secrets.ASC_KEY }} + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }} + - name: Install Apple provisioning profiles + if: matrix.if && matrix.type != 'jailbreak' + run: |- + PROFILES_ZIP_PATH=$RUNNER_TEMP/AppleProvisioningProfiles.zip + printf '%s%s%s%s%s%s%s%s' \ + "$PROVISIONING_PROFILES_1" \ + "$PROVISIONING_PROFILES_2" \ + "$PROVISIONING_PROFILES_3" \ + "$PROVISIONING_PROFILES_4" \ + "$PROVISIONING_PROFILES_5" \ + "$PROVISIONING_PROFILES_6" \ + "$PROVISIONING_PROFILES_7" \ + "$PROVISIONING_PROFILES_8" \ + | base64 --decode -o $PROFILES_ZIP_PATH + PROFILES_PATH="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" + mkdir -p "$PROFILES_PATH" + unzip -q $PROFILES_ZIP_PATH -d "$PROFILES_PATH" + env: + PROVISIONING_PROFILES_1: ${{ secrets.APPLE_PROVISIONING_PROFILES_1 }} + PROVISIONING_PROFILES_2: ${{ secrets.APPLE_PROVISIONING_PROFILES_2 }} + PROVISIONING_PROFILES_3: ${{ secrets.APPLE_PROVISIONING_PROFILES_3 }} + PROVISIONING_PROFILES_4: ${{ secrets.APPLE_PROVISIONING_PROFILES_4 }} + PROVISIONING_PROFILES_5: ${{ secrets.APPLE_PROVISIONING_PROFILES_5 }} + PROVISIONING_PROFILES_6: ${{ secrets.APPLE_PROVISIONING_PROFILES_6 }} + PROVISIONING_PROFILES_7: ${{ secrets.APPLE_PROVISIONING_PROFILES_7 }} + PROVISIONING_PROFILES_8: ${{ secrets.APPLE_PROVISIONING_PROFILES_8 }} + - name: Setup Apple Development certificate + if: matrix.if && matrix.type != 'jailbreak' + run: |- + CERTIFICATE_PATH=$RUNNER_TEMP/AppleDevelopment.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/apple-development.keychain-db + echo -n "$APPLE_DEVELOPMENT_CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security set-keychain-settings -lut 21600 $KEYCHAIN_PATH security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security list-keychain -d user -s $KEYCHAIN_PATH - - PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip - echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH - - PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles" - mkdir -p "$PROFILES_PATH" - unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH" - - ASC_KEY_PATH=$RUNNER_TEMP/Key.p12 - echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH - + security list-keychains -d user -s $KEYCHAIN_PATH + security default-keychain -d user -s $KEYCHAIN_PATH + security find-identity -v -p codesigning $KEYCHAIN_PATH + echo "APPLE_DEVELOPMENT_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + env: + APPLE_DEVELOPMENT_CERTIFICATES_P12: ${{ secrets.APPLE_DEVELOPMENT_CERTIFICATES_P12 }} + P12_PASSWORD: ${{ secrets.APPLE_DEVELOPMENT_P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + - name: Setup Developer ID certificates + if: matrix.if && matrix.type == 'standalone' + run: |- + CERTIFICATE_PATH=$RUNNER_TEMP/DeveloperIDCertificates.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/developer-id.keychain-db + echo -n "$DEVELOPER_ID_CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security list-keychains -d user -s $KEYCHAIN_PATH $APPLE_DEVELOPMENT_KEYCHAIN_PATH + security default-keychain -d user -s $KEYCHAIN_PATH + security find-identity -v -p codesigning $KEYCHAIN_PATH + echo "SIGNING_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + env: + DEVELOPER_ID_CERTIFICATES_P12: ${{ secrets.DEVELOPER_ID_CERTIFICATES_P12 }} + P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + - name: Setup notarization credentials + if: matrix.if && matrix.type == 'standalone' && github.event_name == 'workflow_dispatch' + run: |- + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $SIGNING_KEYCHAIN_PATH xcrun notarytool store-credentials "notarytool-password" \ --key $ASC_KEY_PATH \ --key-id $ASC_KEY_ID \ - --issuer $ASC_KEY_ISSUER_ID - - echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV" - echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV" - echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV" + --issuer $ASC_KEY_ISSUER_ID \ + --keychain $SIGNING_KEYCHAIN_PATH env: - CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }} - P12_PASSWORD: ${{ secrets.P12_PASSWORD }} - KEYCHAIN_PASSWORD: ${{ secrets.P12_PASSWORD }} - PROVISIONING_PROFILES: ${{ secrets.PROVISIONING_PROFILES }} - ASC_KEY: ${{ secrets.ASC_KEY }} - ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} - ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }} - - name: Build library + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + - name: Install jailbreak packaging tools + if: matrix.if && matrix.type == 'jailbreak' + run: |- + brew install dpkg ldid + - name: Trust build plugins + if: matrix.if && matrix.type != 'jailbreak' + run: |- + defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES + - name: Download library + if: matrix.if + uses: actions/download-artifact@v4 + with: + pattern: ${{ matrix.type == 'standalone' && format('libbox-apple-macos-{0}', matrix.library_arch) || format('libbox-apple-{0}-*', matrix.platform) }} + path: libbox-inputs + merge-multiple: true + - name: Prepare library if: matrix.if run: |- - make lib_install - export PATH="$PATH:$(go env GOPATH)/bin" - go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }} - mv Libbox.xcframework clients/apple + mkdir -p libbox-slices + library_inputs=() + for library_archive in libbox-inputs/*.tar; do + slice_name=$(basename "$library_archive" .tar) + slice_path="libbox-slices/$slice_name" + mkdir -p "$slice_path" + tar -xf "$library_archive" -C "$slice_path" + library_inputs+=("$slice_path/Libbox.xcframework") + done + go run ./cmd/internal/merge_apple_xcframework \ + -output clients/apple/Libbox.xcframework \ + "${library_inputs[@]}" - name: Update macOS version if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch' run: |- @@ -915,82 +1210,151 @@ jobs: if: matrix.if && matrix.name != 'iOS' run: |- go run -v ./cmd/internal/update_apple_version --ci - - name: Build - if: matrix.if + - name: Archive App Store build + if: matrix.if && matrix.type == 'app-store' run: |- cd clients/apple xcodebuild archive \ + -clonedSourcePackagesDirPath "$RUNNER_TEMP/SourcePackages" \ + -derivedDataPath "$RUNNER_TEMP/DerivedData" \ -scheme "${{ matrix.scheme }}" \ -configuration Release \ -destination "${{ matrix.destination }}" \ - -archivePath "${{ matrix.archive }}" \ - -allowProvisioningUpdates \ - -authenticationKeyPath $ASC_KEY_PATH \ - -authenticationKeyID $ASC_KEY_ID \ - -authenticationKeyIssuerID $ASC_KEY_ISSUER_ID + -archivePath "${{ matrix.archive }}" + env: + DISABLE_SWIFTLINT: "1" - name: Upload to App Store Connect - if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' + if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch' run: |- go run -v ./cmd/internal/app_store_connect cancel_app_store ${{ matrix.platform }} cd clients/apple xcodebuild -exportArchive \ -archivePath "${{ matrix.archive }}" \ -exportOptionsPlist ${{ matrix.upload }} \ - -allowProvisioningUpdates \ -authenticationKeyPath $ASC_KEY_PATH \ -authenticationKeyID $ASC_KEY_ID \ -authenticationKeyIssuerID $ASC_KEY_ISSUER_ID - name: Publish to TestFlight - if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/testing' + if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/testing' run: |- - go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }} - - name: Build image - if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch' + arguments=(publish_testflight "${{ matrix.platform }}") + if [[ -n "$TEST_MESSAGE" ]]; then + arguments+=("$TEST_MESSAGE") + fi + go run -v ./cmd/internal/app_store_connect "${arguments[@]}" + env: + TEST_MESSAGE: ${{ inputs.test_message }} + - name: Build standalone packages + if: matrix.if && matrix.type == 'standalone' run: |- - pushd clients/apple - xcodebuild -exportArchive \ - -archivePath "${{ matrix.archive }}" \ - -exportOptionsPlist ${{ matrix.export }} \ - -exportPath "${{ matrix.export_path }}" - brew install create-dmg - create-dmg \ - --volname "sing-box" \ - --volicon "${{ matrix.export_path }}/SFM.app/Contents/Resources/AppIcon.icns" \ - --icon "SFM.app" 0 0 \ - --hide-extension "SFM.app" \ - --app-drop-link 0 0 \ - --skip-jenkins \ - SFM.dmg "${{ matrix.export_path }}/SFM.app" - xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password" - cd "${{ matrix.archive }}" + make -C clients/apple build_macos_pkg_${{ matrix.variant }} + env: + DISABLE_SWIFTLINT: "1" + XCODEBUILD_FLAGS: -clonedSourcePackagesDirPath ${{ runner.temp }}/SourcePackages + - name: Notarize standalone packages + if: matrix.if && matrix.type == 'standalone' && github.event_name == 'workflow_dispatch' + run: |- + make -C clients/apple notarize_macos_pkg_${{ matrix.variant }} + - name: Prepare standalone packages + if: matrix.if && matrix.type == 'standalone' + run: |- + mkdir -p dist + for label in Apple Intel Universal; do + cp "clients/apple/build/SFM-${label}.pkg" "dist/SFM-${VERSION}-${label}.pkg" + done + pushd clients/apple/build/SFM.System-universal.xcarchive zip -r SFM.dSYMs.zip dSYMs popd - + cp clients/apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM-${VERSION}.dSYMs.zip" + - name: Build jailbreak package + if: matrix.if && matrix.type == 'jailbreak' + run: |- + make -C clients/apple build_ios_deb mkdir -p dist - cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg" - cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip" - - name: Upload image - if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch' + cp "clients/apple/build/jailbreak/SFI-${VERSION}-iphoneos-arm64.deb" dist + env: + XCODEBUILD_CLONED_SOURCE_PACKAGES_DIR_PATH: ${{ runner.temp }}/SourcePackages + - name: Report cache directory sizes + if: matrix.if + run: |- + du -sh "$RUNNER_TEMP/SourcePackages"/* 2>/dev/null || true + du -sh "$RUNNER_TEMP/DerivedData" 2>/dev/null || true + du -sh clients/apple/build/jailbreak/DerivedData 2>/dev/null || true + du -sh clients/apple/build/SFM.System-*.dd 2>/dev/null || true + - name: Prune Swift package repositories + if: matrix.if + run: rm -rf "$RUNNER_TEMP/SourcePackages/repositories" + - name: Upload release artifact + if: matrix.if && (matrix.type == 'standalone' || matrix.type == 'jailbreak') uses: actions/upload-artifact@v4 with: - name: binary-macos-dmg + name: binary-apple-${{ matrix.type }}${{ matrix.variant && format('-{0}', matrix.variant) || '' }} path: 'dist' + build_apple_app_store: + name: Build Apple App Store clients + runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }} + if: github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS') + needs: + - calculate_version + - build_apple_library + strategy: + fail-fast: false + matrix: + include: + - name: iOS + if: ${{ inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'iOS' }} + type: app-store + platform: ios + scheme: SFI + destination: 'generic/platform=iOS' + archive: build/SFI.xcarchive + upload: SFI/Upload.plist + variant: '' + library_arch: '' + - name: macOS + if: ${{ inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS' }} + type: app-store + platform: macos + scheme: SFM + destination: 'generic/platform=macOS' + archive: build/SFM.xcarchive + upload: SFI/Upload.plist + variant: '' + library_arch: '' + - name: tvOS + if: ${{ inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'tvOS' }} + type: app-store + platform: tvos + scheme: SFT + destination: 'generic/platform=tvOS' + archive: build/SFT.xcarchive + upload: SFI/Upload.plist + variant: '' + library_arch: '' + steps: *build_apple_steps upload: name: Upload builds - if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')" + if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone')" runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write needs: - calculate_version - build - build_darwin - build_windows + - build_android_library - build_android + - build_apple_library - build_apple steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Cache ghr uses: actions/cache@v4 id: cache-ghr @@ -1013,8 +1377,13 @@ jobs: - name: Download builds uses: actions/download-artifact@v5 with: + pattern: binary-* path: dist merge-multiple: true + - name: Attest build provenance + uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + with: + subject-path: 'dist/**' - name: Upload builds if: ${{ env.PUBLISHED == 'false' }} run: |- @@ -1029,3 +1398,70 @@ jobs: ghr --replace -p 5 "v${VERSION}" dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + cache_gc: + name: Cache GC + if: always() + runs-on: ubuntu-latest + permissions: + actions: write + needs: + - build + - build_darwin + - build_windows + - build_android_library + - build_android + - build_apple_library + - build_apple + - build_apple_app_store + - upload + steps: + - name: Delete superseded caches + env: + GH_TOKEN: ${{ github.token }} + run: |- + set -euo pipefail + gh api "repos/$GITHUB_REPOSITORY/actions/caches?per_page=100" --paginate \ + --jq '.actions_caches[] | [.id, .ref, .created_at, .last_accessed_at, .size_in_bytes, .key] | @tsv' > "$RUNNER_TEMP/caches.tsv" + python3 - "$GITHUB_REPOSITORY" "$RUNNER_TEMP/caches.tsv" <<'EOF' + import re + import subprocess + import sys + + repository, list_path = sys.argv[1], sys.argv[2] + budget = 9 * 1024 * 1024 * 1024 + rows = [] + with open(list_path) as list_file: + for line in list_file: + cache_id, ref, created_at, accessed_at, size, key = line.rstrip("\n").split("\t") + family = key + while True: + stripped = re.sub(r"-[0-9a-f]{16,}$", "", family) + if stripped == family: + break + family = stripped + rows.append({ + "id": cache_id, "ref": ref, "created": created_at, + "accessed": accessed_at, "size": int(size), "key": key, + "family": family, + }) + newest = {} + for row in rows: + group = (row["ref"], row["family"]) + if group not in newest or row["created"] > newest[group]["created"]: + newest[group] = row + doomed = [row for row in rows if newest[(row["ref"], row["family"])] is not row] + kept = [row for row in rows if newest[(row["ref"], row["family"])] is row] + total = sum(row["size"] for row in kept) + for row in sorted(kept, key=lambda entry: entry["accessed"]): + if total <= budget: + break + doomed.append(row) + total -= row["size"] + for row in doomed: + print(f"delete {row['size'] >> 20}MB {row['key']} ({row['ref']})") + subprocess.run( + ["gh", "api", "-X", "DELETE", f"repos/{repository}/actions/caches/{row['id']}"], + check=False, + ) + print(f"kept {len(rows) - len(doomed)} caches, {total >> 20}MB total") + EOF diff --git a/cmd/internal/app_store_connect/main.go b/cmd/internal/app_store_connect/main.go index d415abd6..0a1ab61a 100644 --- a/cmd/internal/app_store_connect/main.go +++ b/cmd/internal/app_store_connect/main.go @@ -289,7 +289,7 @@ func prepareAppStore(ctx context.Context) error { return err } if len(builds.Data) == 0 { - log.Fatal(platform, " ", tag, " no build found") + log.Fatal(string(platform), " ", tag, " no build found") } buildID := common.Ptr(builds.Data[0].ID) if version.ID == "" { diff --git a/cmd/internal/merge_aar/main.go b/cmd/internal/merge_aar/main.go new file mode 100644 index 00000000..5c832c78 --- /dev/null +++ b/cmd/internal/merge_aar/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "archive/zip" + "crypto/sha256" + "flag" + "io" + "os" + "path/filepath" + "strings" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" +) + +var outputPath string + +func init() { + flag.StringVar(&outputPath, "output", "", "output AAR path") +} + +func main() { + flag.Parse() + err := merge() + if err != nil { + log.Fatal(err) + } +} + +func merge() error { + inputPaths := flag.Args() + if outputPath == "" { + return E.New("missing output path") + } + if len(inputPaths) == 0 { + return E.New("missing input AAR paths") + } + archiveReaders := make([]*zip.ReadCloser, 0, len(inputPaths)) + for _, inputPath := range inputPaths { + archiveReader, err := zip.OpenReader(inputPath) + if err != nil { + return E.Cause(err, "open input AAR: ", inputPath) + } + archiveReaders = append(archiveReaders, archiveReader) + } + defer func() { + for _, archiveReader := range archiveReaders { + archiveReader.Close() + } + }() + + referenceEntries := make(map[string][sha256.Size]byte) + selectedEntries := make([]*zip.File, 0) + selectedJNIEntries := make(map[string]bool) + for inputIndex, archiveReader := range archiveReaders { + seenEntries := make(map[string]bool) + for _, archiveFile := range archiveReader.File { + if strings.HasPrefix(archiveFile.Name, "jni/") { + if archiveFile.FileInfo().IsDir() { + continue + } + if selectedJNIEntries[archiveFile.Name] { + return E.New("duplicate AAR JNI entry: ", archiveFile.Name) + } + selectedJNIEntries[archiveFile.Name] = true + selectedEntries = append(selectedEntries, archiveFile) + continue + } + entryDigest, err := digestEntry(archiveFile) + if err != nil { + return E.Cause(err, "read AAR entry: ", archiveFile.Name) + } + if inputIndex == 0 { + referenceEntries[archiveFile.Name] = entryDigest + selectedEntries = append(selectedEntries, archiveFile) + } else { + referenceDigest, loaded := referenceEntries[archiveFile.Name] + if !loaded { + return E.New("unexpected AAR entry: ", archiveFile.Name) + } + if referenceDigest != entryDigest { + return E.New("AAR entry differs between architectures: ", archiveFile.Name) + } + } + seenEntries[archiveFile.Name] = true + } + if inputIndex > 0 { + for referenceName := range referenceEntries { + if !seenEntries[referenceName] { + return E.New("missing AAR entry: ", referenceName) + } + } + } + } + + absoluteOutputPath, err := filepath.Abs(outputPath) + if err != nil { + return E.Cause(err, "resolve output AAR path") + } + err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755) + if err != nil { + return E.Cause(err, "create output AAR directory") + } + temporaryFile, err := os.CreateTemp(filepath.Dir(absoluteOutputPath), ".merge-aar-*.aar") + if err != nil { + return E.Cause(err, "create temporary output AAR") + } + temporaryPath := temporaryFile.Name() + defer os.Remove(temporaryPath) + archiveWriter := zip.NewWriter(temporaryFile) + for _, archiveFile := range selectedEntries { + rawReader, openErr := archiveFile.OpenRaw() + if openErr != nil { + archiveWriter.Close() + temporaryFile.Close() + return E.Cause(openErr, "open raw AAR entry: ", archiveFile.Name) + } + header := archiveFile.FileHeader + rawWriter, createErr := archiveWriter.CreateRaw(&header) + if createErr != nil { + archiveWriter.Close() + temporaryFile.Close() + return E.Cause(createErr, "create output AAR entry: ", archiveFile.Name) + } + _, copyErr := io.Copy(rawWriter, rawReader) + if copyErr != nil { + archiveWriter.Close() + temporaryFile.Close() + return E.Cause(copyErr, "copy output AAR entry: ", archiveFile.Name) + } + } + err = archiveWriter.Close() + if err != nil { + temporaryFile.Close() + return E.Cause(err, "finalize output AAR") + } + err = temporaryFile.Close() + if err != nil { + return E.Cause(err, "close output AAR") + } + err = os.Rename(temporaryPath, absoluteOutputPath) + if err != nil { + return E.Cause(err, "replace output AAR") + } + return nil +} + +func digestEntry(archiveFile *zip.File) ([sha256.Size]byte, error) { + entryReader, err := archiveFile.Open() + if err != nil { + return [sha256.Size]byte{}, err + } + digest := sha256.New() + _, err = io.Copy(digest, entryReader) + closeErr := entryReader.Close() + if err != nil { + return [sha256.Size]byte{}, err + } + if closeErr != nil { + return [sha256.Size]byte{}, closeErr + } + var result [sha256.Size]byte + copy(result[:], digest.Sum(nil)) + return result, nil +} diff --git a/cmd/internal/merge_apple_xcframework/main.go b/cmd/internal/merge_apple_xcframework/main.go new file mode 100644 index 00000000..f8f98bec --- /dev/null +++ b/cmd/internal/merge_apple_xcframework/main.go @@ -0,0 +1,172 @@ +package main + +import ( + "flag" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "howett.net/plist" +) + +type xcFrameworkInfo struct { + AvailableLibraries []xcFrameworkLibrary `plist:"AvailableLibraries"` +} + +type xcFrameworkLibrary struct { + BinaryPath string `plist:"BinaryPath"` + LibraryIdentifier string `plist:"LibraryIdentifier"` + LibraryPath string `plist:"LibraryPath"` + SupportedArchitectures []string `plist:"SupportedArchitectures"` + SupportedPlatform string `plist:"SupportedPlatform"` + SupportedPlatformVariant string `plist:"SupportedPlatformVariant"` +} + +type frameworkSlice struct { + rootPath string + library xcFrameworkLibrary +} + +var outputPath string + +func init() { + flag.StringVar(&outputPath, "output", "", "output XCFramework path") +} + +func main() { + flag.Parse() + err := merge() + if err != nil { + log.Fatal(err) + } +} + +func merge() error { + inputPaths := flag.Args() + if outputPath == "" { + return E.New("missing output path") + } + if len(inputPaths) == 0 { + return E.New("missing input XCFramework paths") + } + frameworkGroups := make(map[string][]frameworkSlice) + for _, inputPath := range inputPaths { + infoFile, err := os.Open(filepath.Join(inputPath, "Info.plist")) + if err != nil { + return E.Cause(err, "open XCFramework metadata: ", inputPath) + } + var info xcFrameworkInfo + decoder := plist.NewDecoder(infoFile) + err = decoder.Decode(&info) + closeErr := infoFile.Close() + if err != nil { + return E.Cause(err, "decode XCFramework metadata: ", inputPath) + } + if closeErr != nil { + return E.Cause(closeErr, "close XCFramework metadata: ", inputPath) + } + for _, library := range info.AvailableLibraries { + groupName := library.SupportedPlatform + "|" + library.SupportedPlatformVariant + frameworkGroups[groupName] = append(frameworkGroups[groupName], frameworkSlice{ + rootPath: inputPath, + library: library, + }) + } + } + groupNames := make([]string, 0, len(frameworkGroups)) + for groupName := range frameworkGroups { + groupNames = append(groupNames, groupName) + } + sort.Strings(groupNames) + absoluteOutputPath, err := filepath.Abs(outputPath) + if err != nil { + return E.Cause(err, "resolve output XCFramework path") + } + err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755) + if err != nil { + return E.Cause(err, "create output XCFramework directory") + } + temporaryDirectory, err := os.MkdirTemp(filepath.Dir(absoluteOutputPath), ".merge-xcframework-*") + if err != nil { + return E.Cause(err, "create XCFramework merge directory") + } + defer os.RemoveAll(temporaryDirectory) + frameworkPaths := make([]string, 0, len(groupNames)) + for groupIndex, groupName := range groupNames { + frameworkSlices := frameworkGroups[groupName] + firstSlice := frameworkSlices[0] + firstFrameworkPath := filepath.Join(firstSlice.rootPath, firstSlice.library.LibraryIdentifier, firstSlice.library.LibraryPath) + if len(frameworkSlices) == 1 { + frameworkPaths = append(frameworkPaths, firstFrameworkPath) + continue + } + architectures := make(map[string]bool) + binaryPaths := make([]string, 0, len(frameworkSlices)) + for _, currentSlice := range frameworkSlices { + if currentSlice.library.LibraryPath != firstSlice.library.LibraryPath || currentSlice.library.BinaryPath != firstSlice.library.BinaryPath { + return E.New("incompatible XCFramework slices for platform: ", currentSlice.library.SupportedPlatform) + } + for _, architecture := range currentSlice.library.SupportedArchitectures { + if architectures[architecture] { + return E.New("duplicate XCFramework architecture: ", architecture) + } + architectures[architecture] = true + } + binaryPaths = append(binaryPaths, filepath.Join(currentSlice.rootPath, currentSlice.library.LibraryIdentifier, currentSlice.library.BinaryPath)) + } + mergedFrameworkPath := filepath.Join(temporaryDirectory, "framework-"+strconv.Itoa(groupIndex), filepath.Base(firstSlice.library.LibraryPath)) + copyCommand := exec.Command("ditto", firstFrameworkPath, mergedFrameworkPath) + copyCommand.Stdout = os.Stdout + copyCommand.Stderr = os.Stderr + err = copyCommand.Run() + if err != nil { + return E.Cause(err, "copy XCFramework slice") + } + binaryRelativePath, relativeErr := filepath.Rel(firstSlice.library.LibraryPath, firstSlice.library.BinaryPath) + if relativeErr != nil { + return E.Cause(relativeErr, "resolve XCFramework binary path") + } + if binaryRelativePath == "." || strings.HasPrefix(binaryRelativePath, ".."+string(filepath.Separator)) { + return E.New("invalid XCFramework binary path: ", firstSlice.library.BinaryPath) + } + mergedBinaryPath := filepath.Join(mergedFrameworkPath, binaryRelativePath) + temporaryBinaryPath := mergedBinaryPath + ".merged" + lipoArguments := append([]string{"lipo", "-create"}, binaryPaths...) + lipoArguments = append(lipoArguments, "-output", temporaryBinaryPath) + lipoCommand := exec.Command("xcrun", lipoArguments...) + lipoCommand.Stdout = os.Stdout + lipoCommand.Stderr = os.Stderr + err = lipoCommand.Run() + if err != nil { + return E.Cause(err, "merge XCFramework binaries") + } + err = os.Rename(temporaryBinaryPath, mergedBinaryPath) + if err != nil { + return E.Cause(err, "replace merged XCFramework binary") + } + frameworkPaths = append(frameworkPaths, mergedFrameworkPath) + } + err = os.RemoveAll(absoluteOutputPath) + if err != nil { + return E.Cause(err, "remove output XCFramework") + } + xcodebuildArguments := []string{"-create-xcframework"} + for _, frameworkPath := range frameworkPaths { + xcodebuildArguments = append(xcodebuildArguments, "-framework", frameworkPath) + } + xcodebuildArguments = append(xcodebuildArguments, "-output", absoluteOutputPath) + xcodebuildCommand := exec.Command("xcodebuild", xcodebuildArguments...) + xcodebuildCommand.Stdout = os.Stdout + xcodebuildCommand.Stderr = os.Stderr + err = xcodebuildCommand.Run() + if err != nil { + return E.Cause(err, "create XCFramework") + } + return nil +} diff --git a/cmd/internal/update_apple_version/main.go b/cmd/internal/update_apple_version/main.go index 1b2d0db5..e6be8442 100644 --- a/cmd/internal/update_apple_version/main.go +++ b/cmd/internal/update_apple_version/main.go @@ -106,6 +106,7 @@ func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent stri } func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string { + globalSettings := collectBuildSettings(objectsMap) var objectKeyList []string for objectKey, object := range objectsMap { buildSettings := object.(map[string]any)["buildSettings"] @@ -116,13 +117,51 @@ func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string { if bundleIDObject == nil { continue } - if common.Contains(bundleIDList, bundleIDObject.(string)) { + bundleID := expandBuildVariables(bundleIDObject.(string), globalSettings) + if common.Contains(bundleIDList, bundleID) { objectKeyList = append(objectKeyList, objectKey) } } return objectKeyList } +func collectBuildSettings(objectsMap map[string]any) map[string]string { + settings := make(map[string]string) + for _, object := range objectsMap { + buildSettings, loaded := object.(map[string]any)["buildSettings"].(map[string]any) + if !loaded { + continue + } + for key, value := range buildSettings { + valueString, isString := value.(string) + if !isString { + continue + } + settings[key] = valueString + } + } + return settings +} + +var buildVariableRegexp = regexp.MustCompile(`\$[({]([A-Za-z0-9_]+)[)}]`) + +func expandBuildVariables(value string, settings map[string]string) string { + for { + expanded := buildVariableRegexp.ReplaceAllStringFunc(value, func(match string) string { + name := buildVariableRegexp.FindStringSubmatch(match)[1] + replacement, loaded := settings[name] + if !loaded { + return match + } + return replacement + }) + if expanded == value { + return expanded + } + value = expanded + } +} + func findObjectKeyByDirectory(objectsMap map[string]any, directoryList []string) []string { var objectKeyList []string for objectKey, object := range objectsMap {