Build NeoFreeBird #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build NeoFreeBird | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| decrypted_ipa_url: | |
| description: "Direct URL of the decrypted Twitter/X ipa" | |
| required: true | |
| type: string | |
| sdk_version: | |
| description: "iOS SDK Version for Theos" | |
| default: "16.5" | |
| required: true | |
| type: string | |
| target_version: | |
| description: "Target iOS Version for Theos" | |
| default: "14.0" | |
| required: true | |
| type: string | |
| repo_commit: | |
| description: "(Optional) Commit for main repo (patches)" | |
| default: "" | |
| required: false | |
| type: string | |
| tweak_commit: | |
| description: "(Optional) Commit for tweak repo" | |
| default: "" | |
| required: false | |
| type: string | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-and-inject: | |
| name: Build NeoFreeBird | |
| runs-on: macos-13 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout main repo | |
| uses: actions/checkout@v4 | |
| with: | |
| path: main | |
| ref: ${{ inputs.repo_commit || github.ref }} | |
| submodules: recursive | |
| - name: Checkout tweak repo (NeoFreeBird/tweak) | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: NeoFreeBird/tweak | |
| path: tweak | |
| ref: ${{ inputs.tweak_commit || 'refs/heads/master' }} | |
| submodules: recursive | |
| - name: Install utilities | |
| run: | | |
| brew update || true | |
| brew install make ldid unzip zip rsync || true | |
| python3 -m pip install --upgrade pip setuptools || true | |
| - name: Add GNU Make to PATH | |
| run: | | |
| echo "$(brew --prefix make)/libexec/gnubin" >> "$GITHUB_PATH" | |
| - name: Download Theos | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: theos/theos | |
| ref: master | |
| path: theos | |
| submodules: recursive | |
| - name: Install cyan (pyzule-rw) for injection | |
| run: | | |
| python3 -m pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip | |
| - name: Download iOS SDK | |
| if: steps.SDK.outputs.cache-hit != 'true' | |
| env: | |
| THEOS: ${{ github.workspace }}/theos | |
| run: | | |
| git clone -n --depth=1 --filter=tree:0 https://github.com/theos/sdks/ sdks | |
| cd sdks | |
| git sparse-checkout set --no-cone iPhoneOS${{ inputs.sdk_version }}.sdk | |
| git checkout | |
| mv ./*.sdk "${THEOS}/sdks" | |
| - name: iOS SDK caching | |
| id: SDK | |
| uses: actions/cache@v4 | |
| env: | |
| cache-name: iOS-${{ inputs.sdk_version }}-SDK | |
| with: | |
| path: theos/sdks/ | |
| key: ${{ env.cache-name }} | |
| restore-keys: ${{ env.cache-name }} | |
| - name: Build tweak with Theos | |
| id: build_tweak | |
| env: | |
| THEOS: ${{ github.workspace }}/theos | |
| SDK_VER: ${{ inputs.sdk_version }} | |
| TARGET_IOS: ${{ inputs.target_version }} | |
| run: | | |
| set -e | |
| cd tweak | |
| # Try to set TARGET in Makefile if present (portable sed for macOS) | |
| if [ -f Makefile ]; then | |
| sed -i '' "s/^TARGET.*$/TARGET := iphone:clang:${SDK_VER}:${TARGET_IOS}/" Makefile || true | |
| fi | |
| # Ensure build dirs exist | |
| mkdir -p packages | |
| # Build package (this creates the .deb in typical Theos projects) | |
| make clean || true | |
| make package || make || true | |
| # Find .deb (fail if not found) | |
| DEB=$(find . -type f -name "*.deb" -print | head -n1 || true) | |
| if [ -z "$DEB" ]; then | |
| # try common packages dir | |
| DEB=$(find packages -type f -name "*.deb" -print | head -n1 || true) | |
| fi | |
| if [ -z "$DEB" ]; then | |
| echo "ERROR: could not find built .deb in tweak repo. Build may have failed." | |
| ls -R . || true | |
| exit 1 | |
| fi | |
| echo "deb_path=$PWD/$DEB" >> "$GITHUB_OUTPUT" | |
| echo "Built tweak deb: $DEB" | |
| shell: bash | |
| - name: Download & unpack decrypted Twitter/X ipa | |
| id: unpack | |
| env: | |
| IPA_URL: ${{ inputs.decrypted_ipa_url }} | |
| run: | | |
| set -e | |
| mkdir -p main/packages main/tmp | |
| echo "Downloading decrypted ipa from $IPA_URL" | |
| wget "$IPA_URL" --no-verbose -O main/packages/original_x.ipa | |
| unzip -q main/packages/original_x.ipa -d main/tmp | |
| TARGET_APP_DIR=$(find main/tmp/Payload -maxdepth 1 -type d -name "*.app" | head -n1 || true) | |
| if [ -z "$TARGET_APP_DIR" ]; then | |
| echo "ERROR: could not find .app inside Payload" | |
| ls -R main/tmp || true | |
| exit 1 | |
| fi | |
| X_VERSION=$(grep -A 1 '<key>CFBundleShortVersionString</key>' "$TARGET_APP_DIR/Info.plist" \ | |
| | grep '<string>' | awk -F'[><]' '{print $3}' || echo "unknown") | |
| echo "TARGET_APP_DIR=$TARGET_APP_DIR" >> "$GITHUB_OUTPUT" | |
| echo "X_VERSION=$X_VERSION" >> "$GITHUB_ENV" | |
| echo "X_VERSION=$X_VERSION" | |
| - name: Apply patches into Twitter.app | |
| run: | | |
| set -e | |
| PATCH_SRC="app/NeoFreeBird/Patches" | |
| TARGET_APP_DIR=$(find main/tmp/Payload -maxdepth 1 -type d -name "*.app" | head -n1) | |
| if [ ! -d "${PATCH_SRC}" ]; then | |
| echo "ERROR: patch source not found at ${PATCH_SRC}" | |
| exit 1 | |
| fi | |
| if [ -z "${TARGET_APP_DIR}" ]; then | |
| echo "ERROR: could not find .app inside unpacked Payload" | |
| exit 1 | |
| fi | |
| # Mirror patches into the .app root, replacing files. --delete makes patches authoritative. | |
| rsync -av --delete "${PATCH_SRC}/" "${TARGET_APP_DIR}/" | |
| echo "Patched files (sample):" | |
| find "${TARGET_APP_DIR}" -maxdepth 2 -type f -print | sed -n '1,200p' | |
| shell: bash | |
| - name: Repack Payload into IPA | |
| id: repack | |
| run: | | |
| set -e | |
| mkdir -p main/packages | |
| BASE="NeoFreeBird_base_${{ env.GITHUB_RUN_ID }}_${{ env.X_VERSION }}" | |
| cd main/tmp | |
| rm -f "../packages/${BASE}.ipa" "../packages/${BASE}.tipa" | |
| zip -qr "../packages/${BASE}.ipa" Payload | |
| cp "../packages/${BASE}.ipa" "../packages/${BASE}.tipa" | |
| echo "base_ipa=main/packages/${BASE}.ipa" >> "$GITHUB_OUTPUT" | |
| echo "base_tipa=main/packages/${BASE}.tipa" >> "$GITHUB_OUTPUT" | |
| ls -lah ../packages || true | |
| shell: bash | |
| - name: Inject compiled tweak into IPA using cyan | |
| id: inject | |
| run: | | |
| set -e | |
| DEB_PATH="${{ steps.build_tweak.outputs.deb_path }}" | |
| BASE_IPA="${{ steps.repack.outputs.base_ipa }}" | |
| BASE_TIPA="${{ steps.repack.outputs.base_tipa }}" | |
| if [ -z "$DEB_PATH" ] || [ ! -f "$DEB_PATH" ]; then | |
| echo "ERROR: deb not found at $DEB_PATH" | |
| ls -lah tweak || true | |
| exit 1 | |
| fi | |
| # produce injected outputs | |
| INJECTED_IPA="main/packages/$(basename "${BASE_IPA%.*}")-injected.ipa" | |
| INJECTED_TIPA="main/packages/$(basename "${BASE_TIPA%.*}")-injected.tipa" | |
| # Use cyan to inject the .deb into the .ipa | |
| # cyan CLI supports: cyan -i input.ipa -f file_or_deb -o output.ipa | |
| cyan -i "${BASE_IPA}" -f "${DEB_PATH}" -o "${INJECTED_IPA}" | |
| # Make tipa copy (same content) and also attempt inject there too (redundant but safe) | |
| cp "${INJECTED_IPA}" "${INJECTED_TIPA}" | |
| echo "injected_ipa=${INJECTED_IPA}" >> "$GITHUB_OUTPUT" | |
| echo "injected_tipa=${INJECTED_TIPA}" >> "$GITHUB_OUTPUT" | |
| ls -lah main/packages || true | |
| shell: bash | |
| - name: Upload IPA | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: NeoFreeBird-injected-IPA | |
| path: ${{ steps.inject.outputs.injected_ipa }} | |
| if-no-files-found: error | |
| - name: Upload TIPA | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: NeoFreeBird-injected-TIPA | |
| path: ${{ steps.inject.outputs.injected_tipa }} | |
| if-no-files-found: error |