|
1 | | -name: Build iOS |
| 1 | +name: Build iOS (Unsigned) |
2 | 2 |
|
3 | 3 | on: |
4 | | - workflow_call: |
5 | | - inputs: |
6 | | - version: |
7 | | - type: string |
8 | | - default: '' |
9 | | - sign: |
10 | | - type: boolean |
11 | | - default: false |
12 | | - secrets: |
13 | | - APPLE_CERTIFICATE: |
14 | | - required: false |
15 | | - APPLE_CERTIFICATE_PASSWORD: |
16 | | - required: false |
17 | | - APPLE_PROVISIONING_PROFILE: |
18 | | - required: false |
19 | | - APPLE_TEAM_ID: |
20 | | - required: false |
| 4 | + workflow_dispatch: |
| 5 | + push: |
| 6 | + branches: |
| 7 | + - main |
| 8 | + paths: |
| 9 | + - 'russh-client/**' |
| 10 | + - '.github/workflows/_build-ios.yml' |
| 11 | + - '.github/scripts/patch_ios_project.py' |
21 | 12 |
|
22 | 13 | jobs: |
23 | | - build: |
24 | | - name: iOS |
25 | | - runs-on: macos-14 |
26 | | - env: |
27 | | - SIGNING_ENABLED: 'false' |
| 14 | + build-ios-unsigned: |
| 15 | + name: Build iOS (Unsigned) |
| 16 | + runs-on: macos-latest |
| 17 | + timeout-minutes: 60 |
| 18 | + |
28 | 19 | steps: |
29 | 20 | - uses: actions/checkout@v4 |
30 | 21 |
|
31 | | - - uses: ./.github/actions/setup-env |
| 22 | + - uses: pnpm/action-setup@v4 |
32 | 23 | with: |
33 | | - rust-targets: aarch64-apple-ios,aarch64-apple-ios-sim |
| 24 | + version: 9 |
34 | 25 |
|
35 | | - - uses: Swatinem/rust-cache@v2 |
| 26 | + - uses: actions/setup-node@v4 |
| 27 | + with: |
| 28 | + node-version: '22' |
| 29 | + cache: 'pnpm' |
| 30 | + cache-dependency-path: russh-client/pnpm-lock.yaml |
| 31 | + |
| 32 | + - uses: dtolnay/rust-toolchain@stable |
| 33 | + with: |
| 34 | + targets: aarch64-apple-ios |
| 35 | + |
| 36 | + - name: Rust cache |
| 37 | + uses: swatinem/rust-cache@v2 |
36 | 38 | with: |
37 | 39 | workspaces: "russh-client/src-tauri -> target" |
38 | | - key: ios |
39 | 40 |
|
40 | | - - name: Update version |
41 | | - if: inputs.version != '' |
| 41 | + - name: Install dependencies |
| 42 | + working-directory: russh-client |
| 43 | + run: pnpm install --frozen-lockfile |
| 44 | + |
| 45 | + - name: Build frontend |
| 46 | + working-directory: russh-client |
| 47 | + run: pnpm run build |
| 48 | + |
| 49 | + - name: Verify frontend dist exists |
42 | 50 | working-directory: russh-client |
43 | 51 | run: | |
44 | | - npm version ${{ inputs.version }} --no-git-tag-version || true |
45 | | - sed -i '' 's/^version = ".*"/version = "${{ inputs.version }}"/' src-tauri/Cargo.toml |
46 | | -
|
47 | | - - name: Setup signing |
48 | | - if: inputs.sign |
49 | | - env: |
50 | | - CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} |
51 | | - CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} |
52 | | - PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }} |
53 | | - run: | |
54 | | - KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db |
55 | | - KEYCHAIN_PASSWORD=$(openssl rand -base64 32) |
56 | | - echo "$CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 |
57 | | - security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH |
58 | | - security set-keychain-settings -lut 21600 $KEYCHAIN_PATH |
59 | | - security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH |
60 | | - security import $RUNNER_TEMP/certificate.p12 -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH |
61 | | - security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH |
62 | | - security list-keychain -d user -s $KEYCHAIN_PATH |
63 | | - mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles |
64 | | - echo "$PROVISIONING_PROFILE" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/build.mobileprovision |
65 | | - echo "SIGNING_ENABLED=true" >> $GITHUB_ENV |
66 | | -
|
67 | | - - name: Init iOS |
| 52 | + if [ ! -d "dist" ]; then |
| 53 | + echo "❌ Frontend dist folder not found!" |
| 54 | + exit 1 |
| 55 | + fi |
| 56 | + echo "✅ Frontend dist exists:" |
| 57 | + ls -la dist/ |
| 58 | +
|
| 59 | + - name: Initialize iOS project |
68 | 60 | working-directory: russh-client |
69 | 61 | run: pnpm tauri ios init |
70 | 62 |
|
71 | | - - name: Patch Xcode for Release |
72 | | - working-directory: russh-client/src-tauri/gen/apple |
73 | | - run: | |
74 | | - find . -name "*.xcscheme" -exec sed -i '' 's/buildConfiguration = "Debug"/buildConfiguration = "Release"/g' {} \; |
75 | | - find . -name "project.pbxproj" -exec sed -i '' 's/--configuration \${CONFIGURATION:\?}/--configuration Release/g' {} \; |
| 63 | + - name: Patch iOS project for unsigned release build |
| 64 | + working-directory: russh-client |
| 65 | + run: python3 ../.github/scripts/patch_ios_project.py |
76 | 66 |
|
77 | | - - name: Build frontend |
| 67 | + - name: Build Rust library for iOS device (Release) |
78 | 68 | working-directory: russh-client |
79 | | - run: pnpm build |
| 69 | + run: | |
| 70 | + cd src-tauri |
| 71 | + cargo build --release --target aarch64-apple-ios |
| 72 | + |
| 73 | + # Copy to both debug and release folders (Xcode may look in either) |
| 74 | + mkdir -p gen/apple/Externals/arm64/debug |
| 75 | + mkdir -p gen/apple/Externals/arm64/release |
| 76 | + |
| 77 | + # The library name from Cargo.toml is russh-client -> librussh_client.a |
| 78 | + LIB_FILE=$(find target/aarch64-apple-ios/release -name "librussh_client.a" -type f | head -1) |
| 79 | + |
| 80 | + if [ -z "$LIB_FILE" ]; then |
| 81 | + echo "Looking for any .a file..." |
| 82 | + LIB_FILE=$(find target/aarch64-apple-ios/release -name "*.a" -type f | head -1) |
| 83 | + fi |
| 84 | + |
| 85 | + if [ -z "$LIB_FILE" ] || [ ! -f "$LIB_FILE" ]; then |
| 86 | + echo "❌ No .a library found" |
| 87 | + ls -la target/aarch64-apple-ios/release/ || true |
| 88 | + exit 1 |
| 89 | + fi |
| 90 | + |
| 91 | + echo "Found library: $LIB_FILE" |
| 92 | + |
| 93 | + # Target destination for Tauri 2.0 structure |
| 94 | + # Usually it goes into gen/apple/Externals usually |
| 95 | + cp "$LIB_FILE" gen/apple/Externals/arm64/debug/libapp.a |
| 96 | + cp "$LIB_FILE" gen/apple/Externals/arm64/release/libapp.a |
| 97 | + |
| 98 | + echo "✅ Rust library built and copied" |
| 99 | + ls -la gen/apple/Externals/arm64/*/ |
80 | 100 |
|
81 | | - - name: Build Rust |
82 | | - working-directory: russh-client/src-tauri |
83 | | - run: cargo build --release --target aarch64-apple-ios |
| 101 | + - name: Copy frontend assets to iOS project |
| 102 | + working-directory: russh-client |
| 103 | + run: | |
| 104 | + # Tauri iOS expects the dist folder to be available |
| 105 | + echo "Frontend dist contents:" |
| 106 | + ls -la dist/ |
| 107 | + |
| 108 | + APPLE_DIR="src-tauri/gen/apple" |
| 109 | + |
| 110 | + # Check if assets are properly placed or recognized |
| 111 | + if [ -d "$APPLE_DIR/Sources" ]; then |
| 112 | + echo "Found Sources directory" |
| 113 | + ls -la "$APPLE_DIR/Sources/" || true |
| 114 | + fi |
84 | 115 |
|
85 | | - - name: Create IPA |
86 | | - working-directory: russh-client/src-tauri/gen/apple |
87 | | - env: |
88 | | - TAURI_CLI_NO_DEV_SERVER_WAIT: "true" |
| 116 | + - name: Build iOS Archive (Unsigned Release) |
| 117 | + working-directory: russh-client |
89 | 118 | run: | |
90 | | - PROJ=$(find . -maxdepth 1 -name "*.xcworkspace" -o -name "*.xcodeproj" | head -1) |
91 | | - BUILD_TYPE=$([[ "$PROJ" == *.xcworkspace ]] && echo "-workspace" || echo "-project") |
92 | | - SCHEME=$(xcodebuild -list $BUILD_TYPE "$PROJ" 2>/dev/null | awk '/Schemes:/{getline; print; exit}' | xargs) |
93 | | - |
94 | | - if [ "$SIGNING_ENABLED" = "true" ]; then |
95 | | - xcodebuild archive $BUILD_TYPE "$PROJ" -scheme "$SCHEME" \ |
96 | | - -archivePath "$GITHUB_WORKSPACE/app.xcarchive" \ |
97 | | - -configuration Release -destination "generic/platform=iOS" \ |
98 | | - DEVELOPMENT_TEAM="${{ secrets.APPLE_TEAM_ID }}" SKIP_INSTALL=NO |
99 | | - cat > export.plist << 'EOF' |
100 | | - <?xml version="1.0" encoding="UTF-8"?> |
101 | | - <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
102 | | - <plist version="1.0"><dict><key>method</key><string>development</string></dict></plist> |
103 | | - EOF |
104 | | - xcodebuild -exportArchive -archivePath "$GITHUB_WORKSPACE/app.xcarchive" \ |
105 | | - -exportPath "$GITHUB_WORKSPACE" -exportOptionsPlist export.plist |
106 | | - mv "$GITHUB_WORKSPACE"/*.ipa "$GITHUB_WORKSPACE/RUSSH-ios.ipa" |
107 | | - else |
108 | | - xcodebuild archive $BUILD_TYPE "$PROJ" -scheme "$SCHEME" \ |
109 | | - -archivePath "$GITHUB_WORKSPACE/app.xcarchive" \ |
110 | | - -configuration Release -destination "generic/platform=iOS" \ |
111 | | - CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO SKIP_INSTALL=NO |
112 | | - cd "$GITHUB_WORKSPACE/app.xcarchive/Products" && mv Applications Payload |
113 | | - zip -r "$GITHUB_WORKSPACE/RUSSH-ios-unsigned.ipa" Payload |
| 119 | + set -e |
| 120 | + |
| 121 | + XCODEPROJ=$(find src-tauri/gen/apple -name "*.xcodeproj" -type d | head -1) |
| 122 | + echo "Project: $XCODEPROJ" |
| 123 | + |
| 124 | + SCHEME=$(xcodebuild -project "$XCODEPROJ" -list 2>/dev/null | \ |
| 125 | + awk '/Schemes:/{flag=1; next} flag && /^$/{flag=0} flag' | \ |
| 126 | + sed 's/^[[:space:]]*//' | grep -i ios | head -1) |
| 127 | + |
| 128 | + [ -z "$SCHEME" ] && SCHEME=$(xcodebuild -project "$XCODEPROJ" -list 2>/dev/null | \ |
| 129 | + awk '/Schemes:/{flag=1; next} flag && /^$/{flag=0} flag' | \ |
| 130 | + sed 's/^[[:space:]]*//' | head -1) |
| 131 | + |
| 132 | + echo "Scheme: $SCHEME" |
| 133 | + |
| 134 | + # Use the xcconfig if it exists (created by patch script) |
| 135 | + XCCONFIG="" |
| 136 | + if [ -f "src-tauri/gen/apple/Release-Unsigned.xcconfig" ]; then |
| 137 | + XCCONFIG="-xcconfig src-tauri/gen/apple/Release-Unsigned.xcconfig" |
114 | 138 | fi |
| 139 | + |
| 140 | + # Build with Release configuration and proper settings |
| 141 | + xcodebuild archive \ |
| 142 | + -project "$XCODEPROJ" \ |
| 143 | + -scheme "$SCHEME" \ |
| 144 | + -configuration Release \ |
| 145 | + -archivePath ./unsigned.xcarchive \ |
| 146 | + -destination "generic/platform=iOS" \ |
| 147 | + $XCCONFIG \ |
| 148 | + CODE_SIGN_IDENTITY="" \ |
| 149 | + CODE_SIGNING_REQUIRED=NO \ |
| 150 | + CODE_SIGNING_ALLOWED=NO \ |
| 151 | + DEVELOPMENT_TEAM="" \ |
| 152 | + AD_HOC_CODE_SIGNING_ALLOWED=NO \ |
| 153 | + ONLY_ACTIVE_ARCH=NO \ |
| 154 | + GCC_PREPROCESSOR_DEFINITIONS='$(inherited) RELEASE=1 NDEBUG=1' \ |
| 155 | + SWIFT_ACTIVE_COMPILATION_CONDITIONS='RELEASE' \ |
| 156 | + ENABLE_BITCODE=NO |
| 157 | + |
| 158 | + echo "✅ Archive created" |
115 | 159 |
|
116 | | - - name: Cleanup signing |
117 | | - if: always() && inputs.sign |
118 | | - run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true |
| 160 | + - name: Package IPA from Archive |
| 161 | + working-directory: russh-client |
| 162 | + run: | |
| 163 | + set -e |
| 164 | + |
| 165 | + ARCHIVE_PATH="./unsigned.xcarchive" |
| 166 | + |
| 167 | + if [ ! -d "$ARCHIVE_PATH" ]; then |
| 168 | + echo "❌ Archive not found" |
| 169 | + exit 1 |
| 170 | + fi |
| 171 | + |
| 172 | + APP_PATH=$(find "$ARCHIVE_PATH/Products/Applications" -name "*.app" -type d 2>/dev/null | head -1) |
| 173 | + |
| 174 | + if [ -z "$APP_PATH" ]; then |
| 175 | + APP_PATH=$(find "$ARCHIVE_PATH" -name "*.app" -type d 2>/dev/null | head -1) |
| 176 | + fi |
| 177 | + |
| 178 | + if [ -z "$APP_PATH" ]; then |
| 179 | + echo "❌ No .app found in archive" |
| 180 | + exit 1 |
| 181 | + fi |
| 182 | + |
| 183 | + echo "✅ Found: $APP_PATH" |
| 184 | + |
| 185 | + mkdir -p Payload |
| 186 | + cp -R "$APP_PATH" Payload/ |
| 187 | + zip -r -q iMAGE-unsigned.ipa Payload/ |
| 188 | + rm -rf Payload |
| 189 | + |
| 190 | + echo "✅ IPA created: iMAGE-unsigned.ipa" |
| 191 | + ls -la iMAGE-unsigned.ipa |
119 | 192 |
|
120 | | - - uses: actions/upload-artifact@v4 |
| 193 | + - name: Upload IPA |
| 194 | + uses: actions/upload-artifact@v4 |
121 | 195 | with: |
122 | | - name: ios-ipa |
123 | | - path: | |
124 | | - RUSSH-ios.ipa |
125 | | - RUSSH-ios-unsigned.ipa |
126 | | - if-no-files-found: warn |
| 196 | + name: ios-unsigned-ipa |
| 197 | + path: russh-client/iMAGE-unsigned.ipa |
| 198 | + retention-days: 30 |
0 commit comments