mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 11:43:24 +01:00
Merge pull request #888 from SideStore/develop-alpha
Feature: Bulk sources-add in sources screen with capability to stage them before persisting into database
This commit is contained in:
2
.github/workflows/alpha.yml
vendored
2
.github/workflows/alpha.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
bundle_id: "com.SideStore.SideStore"
|
bundle_id: "com.SideStore.SideStore"
|
||||||
# bundle_id_suffix: ".Alpha"
|
# bundle_id_suffix: ".Alpha"
|
||||||
is_beta: true
|
is_beta: true
|
||||||
publish: true
|
publish: ${{ vars.PUBLISH_ALPHA_UPDATES == 'true' }}
|
||||||
is_shared_build_num: false
|
is_shared_build_num: false
|
||||||
release_tag: "alpha"
|
release_tag: "alpha"
|
||||||
release_name: "Alpha"
|
release_name: "Alpha"
|
||||||
|
|||||||
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
|||||||
bundle_id: "com.SideStore.SideStore"
|
bundle_id: "com.SideStore.SideStore"
|
||||||
# bundle_id_suffix: ".Nightly"
|
# bundle_id_suffix: ".Nightly"
|
||||||
is_beta: true
|
is_beta: true
|
||||||
publish: true
|
publish: ${{ vars.PUBLISH_NIGHTLY_UPDATES == 'true' }}
|
||||||
is_shared_build_num: false
|
is_shared_build_num: false
|
||||||
release_tag: "nightly"
|
release_tag: "nightly"
|
||||||
release_name: "Nightly"
|
release_name: "Nightly"
|
||||||
|
|||||||
600
.github/workflows/reusable-build-workflow.yml
vendored
600
.github/workflows/reusable-build-workflow.yml
vendored
@@ -1,4 +1,5 @@
|
|||||||
name: Reusable SideStore Build
|
name: Reusable SideStore Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -44,10 +45,28 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
serialize:
|
||||||
name: Build and upload SideStore ${{ inputs.release_tag }} releases
|
name: Wait for other jobs
|
||||||
concurrency:
|
concurrency:
|
||||||
group: build-number-increment # serialize for build num cache access
|
group: build-number-increment # serialize for build num cache access
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
runs-on: 'macos-15'
|
||||||
|
steps:
|
||||||
|
- run: echo "No other contending jobs are running now...Build is ready to start"
|
||||||
|
- name: Set short commit hash
|
||||||
|
id: commit-id
|
||||||
|
run: |
|
||||||
|
# SHORT_COMMIT="${{ github.sha }}"
|
||||||
|
SHORT_COMMIT=${GITHUB_SHA:0:7}
|
||||||
|
echo "Short commit hash: $SHORT_COMMIT"
|
||||||
|
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_OUTPUT
|
||||||
|
outputs:
|
||||||
|
short-commit: ${{ steps.commit-id.outputs.SHORT_COMMIT }}
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build SideStore - ${{ inputs.release_tag }}
|
||||||
|
needs: serialize
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -56,8 +75,12 @@ jobs:
|
|||||||
version: '16.1'
|
version: '16.1'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
marketing-version: ${{ steps.marketing-version.outputs.MARKETING_VERSION }}
|
||||||
|
release-channel: ${{ steps.release-channel.outputs.RELEASE_CHANNEL }}
|
||||||
|
|
||||||
|
steps:
|
||||||
- name: Set beta status
|
- name: Set beta status
|
||||||
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
@@ -67,7 +90,8 @@ jobs:
|
|||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install dependencies - ldid & xcbeautify
|
- name: Install dependencies - ldid & xcbeautify
|
||||||
run: brew install ldid xcbeautify
|
run: |
|
||||||
|
brew install ldid xcbeautify
|
||||||
|
|
||||||
- name: Set ref based on is_shared_build_num
|
- name: Set ref based on is_shared_build_num
|
||||||
if: ${{ inputs.is_beta }}
|
if: ${{ inputs.is_beta }}
|
||||||
@@ -87,7 +111,7 @@ jobs:
|
|||||||
ref: ${{ env.ref }}
|
ref: ${{ env.ref }}
|
||||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||||
path: 'SideStore/beta-build-num'
|
path: 'SideStore/beta-build-num'
|
||||||
|
|
||||||
- name: Copy build_number.txt to repo root
|
- name: Copy build_number.txt to repo root
|
||||||
if: ${{ inputs.is_beta }}
|
if: ${{ inputs.is_beta }}
|
||||||
run: |
|
run: |
|
||||||
@@ -99,13 +123,16 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "cat Build.xcconfig"
|
echo "cat Build.xcconfig"
|
||||||
cat Build.xcconfig
|
cat Build.xcconfig
|
||||||
|
|
||||||
- name: Set Release Channel info for build number bumper
|
- name: Set Release Channel info for build number bumper
|
||||||
|
id: release-channel
|
||||||
run: |
|
run: |
|
||||||
echo "RELEASE_CHANNEL=${{ inputs.release_tag }}" >> $GITHUB_ENV
|
RELEASE_CHANNEL="${{ inputs.release_tag }}"
|
||||||
|
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_OUTPUT
|
||||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
|
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
|
||||||
|
|
||||||
|
|
||||||
- name: Increase build number for beta builds
|
- name: Increase build number for beta builds
|
||||||
if: ${{ inputs.is_beta }}
|
if: ${{ inputs.is_beta }}
|
||||||
run: |
|
run: |
|
||||||
@@ -118,15 +145,9 @@ jobs:
|
|||||||
echo "version=$version" >> $GITHUB_OUTPUT
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
echo "version=$version"
|
echo "version=$version"
|
||||||
|
|
||||||
- name: Get short commit hash
|
|
||||||
run: |
|
|
||||||
# SHORT_COMMIT="${{ github.sha }}"
|
|
||||||
SHORT_COMMIT=${GITHUB_SHA:0:7}
|
|
||||||
echo "Short commit hash: $SHORT_COMMIT"
|
|
||||||
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set MARKETING_VERSION
|
- name: Set MARKETING_VERSION
|
||||||
if: ${{ inputs.is_beta }}
|
if: ${{ inputs.is_beta }}
|
||||||
|
id: marketing-version
|
||||||
run: |
|
run: |
|
||||||
# Extract version number (e.g., "0.6.0")
|
# Extract version number (e.g., "0.6.0")
|
||||||
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||||
@@ -136,9 +157,10 @@ jobs:
|
|||||||
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
|
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
|
||||||
|
|
||||||
# Combine them into the final output
|
# Combine them into the final output
|
||||||
MARKETING_VERSION="${version}-${date}.${build_num}+${SHORT_COMMIT}"
|
MARKETING_VERSION="${version}-${date}.${build_num}+${{ needs.serialize.outputs.short-commit }}"
|
||||||
|
|
||||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||||
|
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "MARKETING_VERSION=$MARKETING_VERSION"
|
echo "MARKETING_VERSION=$MARKETING_VERSION"
|
||||||
|
|
||||||
- name: Echo Updated Build.xcconfig, build_number.txt
|
- name: Echo Updated Build.xcconfig, build_number.txt
|
||||||
@@ -152,16 +174,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
xcode-version: ${{ matrix.version }}
|
xcode-version: ${{ matrix.version }}
|
||||||
|
|
||||||
- name: Cache Build
|
- name: (Build) Cache Build
|
||||||
uses: irgaly/xcode-cache@v1
|
uses: irgaly/xcode-cache@v1
|
||||||
with:
|
with:
|
||||||
key: xcode-cache-deriveddata-${{ github.sha }}
|
key: xcode-cache-deriveddata-build-${{ github.sha }}
|
||||||
restore-keys: xcode-cache-deriveddata-
|
restore-keys: xcode-cache-deriveddata-build-
|
||||||
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
|
swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.sha }}
|
||||||
swiftpm-cache-restore-keys: |
|
swiftpm-cache-restore-keys: |
|
||||||
xcode-cache-sourcedata-
|
xcode-cache-sourcedata-build-
|
||||||
|
|
||||||
- name: Restore Pods from Cache (Exact match)
|
- name: (Build) Restore Pods from Cache (Exact match)
|
||||||
id: pods-restore
|
id: pods-restore
|
||||||
uses: actions/cache/restore@v3
|
uses: actions/cache/restore@v3
|
||||||
with:
|
with:
|
||||||
@@ -169,12 +191,12 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-${{ hashFiles('Podfile') }}
|
key: pods-cache-build-${{ hashFiles('Podfile') }}
|
||||||
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
||||||
# pods-cache-
|
# pods-cache-
|
||||||
|
|
||||||
- name: Restore Pods from Cache (Last Available)
|
- name: (Build) Restore Pods from Cache (Last Available)
|
||||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||||
id: pods-restore-recent
|
id: pods-restore-recent
|
||||||
uses: actions/cache/restore@v3
|
uses: actions/cache/restore@v3
|
||||||
with:
|
with:
|
||||||
@@ -182,13 +204,13 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-
|
key: pods-cache-build-
|
||||||
|
|
||||||
|
|
||||||
- name: Install CocoaPods
|
- name: (Build) Install CocoaPods
|
||||||
run: pod install
|
run: pod install
|
||||||
|
|
||||||
- name: Save Pods to Cache
|
- name: (Build) Save Pods to Cache
|
||||||
id: save-pods
|
id: save-pods
|
||||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||||
uses: actions/cache/save@v3
|
uses: actions/cache/save@v3
|
||||||
@@ -197,9 +219,16 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-${{ hashFiles('Podfile') }}
|
key: pods-cache-build-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
- name: List Files and derived data
|
- name: (Build) Clean previous build artifacts
|
||||||
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
|
run: |
|
||||||
|
make clean
|
||||||
|
mkdir -p build/logs
|
||||||
|
|
||||||
|
- name: (Build) List Files and derived data
|
||||||
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
ls -la .
|
ls -la .
|
||||||
@@ -221,23 +250,205 @@ jobs:
|
|||||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
||||||
- name: Set BundleID Suffix for Sidestore build
|
- name: Set BundleID Suffix for Sidestore build
|
||||||
run: |
|
run: |
|
||||||
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
|
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build SideStore
|
|
||||||
|
- name: Build SideStore.xcarchive
|
||||||
# using 'tee' to intercept stdout and log for detailed build-log
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
run: |
|
run: |
|
||||||
NSUnbufferedIO=YES make build 2>&1 | tee build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
- name: Fakesign app
|
- name: Fakesign app
|
||||||
run: make fakesign | tee -a build.log
|
run: make fakesign | tee -a build/logs/build.log
|
||||||
|
|
||||||
- name: Convert to IPA
|
- name: Convert to IPA
|
||||||
run: make ipa | tee -a build.log
|
run: make ipa | tee -a build/logs/build.log
|
||||||
|
|
||||||
- name: Encrypt build.log generated from SideStore build for upload
|
- name: (Build) List Files and Build artifacts
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||||
|
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
|
||||||
|
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- name: Encrypt build-logs for upload
|
||||||
|
id: encrypt-build-log
|
||||||
|
run: |
|
||||||
|
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||||
|
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||||
|
|
||||||
|
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||||
|
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
|
||||||
|
echo "::set-output name=encrypted::true"
|
||||||
|
|
||||||
|
- name: Upload encrypted-build-logs.zip
|
||||||
|
id: attach-encrypted-build-log
|
||||||
|
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
|
||||||
|
path: encrypted-build-logs.zip
|
||||||
|
|
||||||
|
- name: Upload SideStore.ipa Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||||
|
path: SideStore.ipa
|
||||||
|
|
||||||
|
- name: Zip dSYMs
|
||||||
|
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
|
||||||
|
|
||||||
|
- name: Upload *.dSYM Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
||||||
|
path: SideStore.dSYMs.zip
|
||||||
|
|
||||||
|
- name: Zip beta-beta-build-num & update_apps.py
|
||||||
|
run: |
|
||||||
|
zip -r -9 ./beta-build-num.zip ./SideStore/beta-build-num update_apps.py
|
||||||
|
|
||||||
|
- name: Upload beta-build-num artifact
|
||||||
|
if: ${{ inputs.is_beta }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: beta-build-num-${{ steps.version.outputs.version }}.zip
|
||||||
|
path: beta-build-num.zip
|
||||||
|
|
||||||
|
|
||||||
|
tests-build:
|
||||||
|
name: Tests-Build SideStore - ${{ inputs.release_tag }}
|
||||||
|
needs: serialize
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-15'
|
||||||
|
version: '16.1'
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install dependencies - xcbeautify
|
||||||
|
run: |
|
||||||
|
brew install xcbeautify
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
|
with:
|
||||||
|
xcode-version: '16.1'
|
||||||
|
|
||||||
|
- name: (Tests-Build) Cache Build
|
||||||
|
uses: irgaly/xcode-cache@v1
|
||||||
|
with:
|
||||||
|
key: xcode-cache-deriveddata-test-${{ github.sha }}
|
||||||
|
restore-keys: xcode-cache-deriveddata-test-
|
||||||
|
swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.sha }}
|
||||||
|
swiftpm-cache-restore-keys: |
|
||||||
|
xcode-cache-sourcedata-test-
|
||||||
|
|
||||||
|
- name: (Tests-Build) Restore Pods from Cache (Exact match)
|
||||||
|
id: pods-restore
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./Podfile.lock
|
||||||
|
./Pods/
|
||||||
|
./AltStore.xcworkspace/
|
||||||
|
key: pods-cache-test-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
|
- name: (Tests-Build) Restore Pods from Cache (Last Available)
|
||||||
|
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||||
|
id: pods-restore-recent
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./Podfile.lock
|
||||||
|
./Pods/
|
||||||
|
./AltStore.xcworkspace/
|
||||||
|
key: pods-cache-test-
|
||||||
|
|
||||||
|
- name: (Tests-Build) Install CocoaPods
|
||||||
|
run: pod install
|
||||||
|
|
||||||
|
- name: (Tests-Build) Save Pods to Cache
|
||||||
|
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||||
|
uses: actions/cache/save@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./Podfile.lock
|
||||||
|
./Pods/
|
||||||
|
./AltStore.xcworkspace/
|
||||||
|
key: pods-cache-test-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
|
- name: (Tests-Build) Clean previous build artifacts
|
||||||
|
run: |
|
||||||
|
make clean
|
||||||
|
mkdir -p build/logs
|
||||||
|
|
||||||
|
- name: (Tests-Build) List Files and derived data
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||||
|
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||||
|
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- name: Build SideStore Tests
|
||||||
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
|
run: |
|
||||||
|
NSUnbufferedIO=YES make -B build-tests 2>&1 | tee -a build/logs/tests-build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: (Tests-Build) List Files and Build artifacts
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||||
|
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- name: Encrypt tests-build-logs for upload
|
||||||
|
id: encrypt-test-log
|
||||||
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||||
|
|
||||||
@@ -248,19 +459,271 @@ jobs:
|
|||||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f build.log ]; then
|
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-build-logs.zip * || popd
|
||||||
echo "Warning: build.log is missing, creating a dummy log..."
|
echo "::set-output name=encrypted::true"
|
||||||
echo "Error: build.log was missing, This is a dummy placeholder file..." > build.log
|
|
||||||
fi
|
- name: Upload encrypted-tests-build-logs.zip
|
||||||
|
id: attach-encrypted-test-log
|
||||||
|
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-build-logs-${{ needs.serialize.outputs.short-commit }}.zip
|
||||||
|
path: encrypted-tests-build-logs.zip
|
||||||
|
|
||||||
zip -e -P "$BUILD_LOG_ZIP_PASSWORD" encrypted-build_log.zip build.log
|
tests-run:
|
||||||
|
name: Tests-Run SideStore - ${{ inputs.release_tag }}
|
||||||
|
needs: [serialize, tests-build]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: 'macos-15'
|
||||||
|
version: '16.1'
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
- name: List Files after SideStore build
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Boot Simulator for testing
|
||||||
|
run: |
|
||||||
|
mkdir -p build/logs
|
||||||
|
make -B boot-sim-async | tee -a build/logs/tests-run.log
|
||||||
|
exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: Setup Xcode
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
|
with:
|
||||||
|
xcode-version: '16.1'
|
||||||
|
|
||||||
|
- name: (Tests-Run) Cache Build
|
||||||
|
uses: irgaly/xcode-cache@v1
|
||||||
|
with:
|
||||||
|
key: xcode-cache-deriveddata-test-${{ github.sha }}
|
||||||
|
restore-keys: xcode-cache-deriveddata-test-
|
||||||
|
swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.sha }}
|
||||||
|
swiftpm-cache-restore-keys: |
|
||||||
|
xcode-cache-sourcedata-test-
|
||||||
|
|
||||||
|
- name: (Tests-Run) Restore Pods from Cache (Exact match)
|
||||||
|
id: pods-restore
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./Podfile.lock
|
||||||
|
./Pods/
|
||||||
|
./AltStore.xcworkspace/
|
||||||
|
key: pods-cache-test-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
|
- name: (Tests-Run) Restore Pods from Cache (Last Available)
|
||||||
|
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||||
|
id: pods-restore-recent
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./Podfile.lock
|
||||||
|
./Pods/
|
||||||
|
./AltStore.xcworkspace/
|
||||||
|
key: pods-cache-test-
|
||||||
|
|
||||||
|
- name: (Tests-Run) Install CocoaPods
|
||||||
|
run: pod install
|
||||||
|
|
||||||
|
- name: (Tests-Run) Save Pods to Cache
|
||||||
|
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||||
|
uses: actions/cache/save@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
./Podfile.lock
|
||||||
|
./Pods/
|
||||||
|
./AltStore.xcworkspace/
|
||||||
|
key: pods-cache-test-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
|
- name: (Tests-Run) Clean previous build artifacts
|
||||||
|
run: |
|
||||||
|
make clean
|
||||||
|
mkdir -p build/logs
|
||||||
|
|
||||||
|
- name: (Tests-Run) List Files and derived data
|
||||||
run: |
|
run: |
|
||||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
ls -la .
|
ls -la .
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||||
|
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||||
|
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||||
|
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||||
|
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# we expect simulator to have been booted by now, so exit otherwise
|
||||||
|
- name: Simulator Boot Check
|
||||||
|
run: |
|
||||||
|
mkdir -p build/logs
|
||||||
|
make -B sim-boot-check | tee -a build/logs/tests-run.log
|
||||||
|
exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: Start Recording UI tests (if DEBUG_RECORD_TESTS is set to 1)
|
||||||
|
if: ${{ vars.DEBUG_RECORD_TESTS == '1' }}
|
||||||
|
run: |
|
||||||
|
nohup xcrun simctl io booted recordVideo -f tests-recording.mp4 --codec h264 </dev/null > tests-recording.log 2>&1 &
|
||||||
|
RECORD_PID=$!
|
||||||
|
echo "RECORD_PID=$RECORD_PID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Run SideStore Tests
|
||||||
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
|
run: |
|
||||||
|
make run-tests 2>&1 | tee -a build/logs/tests-run.log && exit ${PIPESTATUS[0]}
|
||||||
|
# NSUnbufferedIO=YES make -B run-tests 2>&1 | tee build/logs/tests-run.log | xcpretty -r junit --output ./build/tests/test-results.xml && exit ${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
- name: Stop Recording tests
|
||||||
|
if: ${{ always() && env.RECORD_PID != '' }}
|
||||||
|
run: |
|
||||||
|
kill -INT ${{ env.RECORD_PID }}
|
||||||
|
|
||||||
|
- name: (Tests-Run) List Files and Build artifacts
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
ls -la .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||||
|
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- name: Encrypt tests-run-logs for upload
|
||||||
|
id: encrypt-test-log
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||||
|
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||||
|
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||||
|
|
||||||
|
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||||
|
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-run-logs.zip * || popd
|
||||||
|
echo "::set-output name=encrypted::true"
|
||||||
|
|
||||||
|
- name: Upload encrypted-tests-run-logs.zip
|
||||||
|
id: attach-encrypted-test-log
|
||||||
|
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-run-logs-${{ needs.serialize.outputs.short-commit }}.zip
|
||||||
|
path: encrypted-tests-run-logs.zip
|
||||||
|
|
||||||
|
- name: Print tests-recording.log contents (if exists)
|
||||||
|
if: ${{ always() && env.RECORD_PID != '' }}
|
||||||
|
run: |
|
||||||
|
if [ -f tests-recording.log ]; then
|
||||||
|
echo "tests-recording.log found. Its contents:"
|
||||||
|
cat tests-recording.log
|
||||||
|
else
|
||||||
|
echo "tests-recording.log not found."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check for tests-recording.mp4 presence
|
||||||
|
id: check-recording
|
||||||
|
if: ${{ always() && env.RECORD_PID != '' }}
|
||||||
|
run: |
|
||||||
|
if [ -f tests-recording.mp4 ]; then
|
||||||
|
echo "::set-output name=found::true"
|
||||||
|
echo "tests-recording.mp4 found."
|
||||||
|
else
|
||||||
|
echo "tests-recording.mp4 not found, skipping upload."
|
||||||
|
echo "::set-output name=found::false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload tests-recording.mp4
|
||||||
|
id: upload-recording
|
||||||
|
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tests-recording-${{ needs.serialize.outputs.short-commit }}.mp4
|
||||||
|
path: tests-recording.mp4
|
||||||
|
|
||||||
|
- name: Zip test-results
|
||||||
|
run: zip -r -9 ./test-results.zip ./build/tests
|
||||||
|
|
||||||
|
- name: Upload Test Artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results-${{ needs.serialize.outputs.short-commit }}.zip
|
||||||
|
path: test-results.zip
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy SideStore - ${{ inputs.release_tag }}
|
||||||
|
runs-on: macos-15
|
||||||
|
# needs: [serialize, build]
|
||||||
|
needs: [serialize, build, tests-build, tests-run]
|
||||||
|
steps:
|
||||||
|
- name: Download IPA artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ needs.build.outputs.version }}.ipa
|
||||||
|
|
||||||
|
- name: Download dSYM artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SideStore-${{ needs.build.outputs.version }}-dSYMs.zip
|
||||||
|
|
||||||
|
- name: Download encrypted-build-logs artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-build-logs-${{ needs.build.outputs.version }}.zip
|
||||||
|
|
||||||
|
- name: Download encrypted-tests-build-logs artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-build-logs-${{ needs.serialize.outputs.short-commit }}.zip
|
||||||
|
|
||||||
|
- name: Download encrypted-tests-run-logs artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: encrypted-tests-run-logs-${{ needs.serialize.outputs.short-commit }}.zip
|
||||||
|
|
||||||
|
- name: Download tests-recording artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tests-recording-${{ needs.serialize.outputs.short-commit }}.mp4
|
||||||
|
|
||||||
|
- name: Download test-results artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results-${{ needs.serialize.outputs.short-commit }}.zip
|
||||||
|
|
||||||
|
- name: Download beta-build-num artifact
|
||||||
|
if: ${{ inputs.is_beta }}
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: beta-build-num-${{ needs.build.outputs.version }}.zip
|
||||||
|
- name: Un-Zip beta-beta-build-num & update_apps.py
|
||||||
|
run: |
|
||||||
|
unzip beta-build-num.zip -d .
|
||||||
|
|
||||||
|
|
||||||
|
- name: List files before upload
|
||||||
|
run: |
|
||||||
|
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||||
|
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
|
||||||
|
echo ""
|
||||||
|
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
id: date
|
id: date
|
||||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||||
@@ -269,9 +732,6 @@ jobs:
|
|||||||
id: date_altstore
|
id: date_altstore
|
||||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create dSYMs zip
|
|
||||||
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs/*
|
|
||||||
|
|
||||||
- name: Upload to releases
|
- name: Upload to releases
|
||||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||||
with:
|
with:
|
||||||
@@ -279,7 +739,7 @@ jobs:
|
|||||||
release: ${{ inputs.release_name }}
|
release: ${{ inputs.release_name }}
|
||||||
tag: ${{ inputs.release_tag }}
|
tag: ${{ inputs.release_tag }}
|
||||||
prerelease: ${{ inputs.is_beta }}
|
prerelease: ${{ inputs.is_beta }}
|
||||||
files: SideStore.ipa SideStore.dSYMs.zip encrypted-build_log.zip
|
files: SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip encrypted-tests-build-logs.zip encrypted-tests-run-logs.zip test-results.zip tests-recording.mp4
|
||||||
body: |
|
body: |
|
||||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||||
|
|
||||||
@@ -292,14 +752,11 @@ jobs:
|
|||||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||||
Commit SHA: `${{ github.sha }}`
|
Commit SHA: `${{ github.sha }}`
|
||||||
Version: `${{ steps.version.outputs.version }}`
|
Version: `${{ needs.build.outputs.version }}`
|
||||||
|
|
||||||
# save it
|
|
||||||
- name: Publish to SideStore/beta-build-num
|
- name: Publish to SideStore/beta-build-num
|
||||||
if: ${{ inputs.is_beta }}
|
if: ${{ inputs.is_beta }}
|
||||||
run: |
|
run: |
|
||||||
rm SideStore/beta-build-num/build_number.txt
|
|
||||||
mv build_number.txt SideStore/beta-build-num/build_number.txt
|
|
||||||
pushd SideStore/beta-build-num/
|
pushd SideStore/beta-build-num/
|
||||||
|
|
||||||
echo "Configure Git user (committer details)"
|
echo "Configure Git user (committer details)"
|
||||||
@@ -308,33 +765,12 @@ jobs:
|
|||||||
|
|
||||||
echo "Adding files to commit"
|
echo "Adding files to commit"
|
||||||
git add --verbose build_number.txt
|
git add --verbose build_number.txt
|
||||||
git commit -m " - updated for ${{ inputs.release_tag }} - $SHORT_COMMIT deployment" || echo "No changes to commit"
|
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ needs.serialize.outputs.short-commit }} deployment" || echo "No changes to commit"
|
||||||
|
|
||||||
echo "Pushing to remote repo"
|
echo "Pushing to remote repo"
|
||||||
git push --verbose
|
git push --verbose
|
||||||
popd
|
popd
|
||||||
|
|
||||||
- name: Add version to IPA file name
|
|
||||||
run: cp SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
|
|
||||||
|
|
||||||
- name: Upload SideStore.ipa Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
|
||||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
|
||||||
|
|
||||||
- name: Upload *.dSYM Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
|
||||||
path: ./SideStore.xcarchive/dSYMs/*
|
|
||||||
|
|
||||||
- name: Upload encrypted-build_log.zip
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: encrypted-build_log.zip
|
|
||||||
path: encrypted-build_log.zip
|
|
||||||
|
|
||||||
- name: Get formatted date
|
- name: Get formatted date
|
||||||
run: |
|
run: |
|
||||||
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
@@ -364,15 +800,17 @@ jobs:
|
|||||||
# Format localized description
|
# Format localized description
|
||||||
LOCALIZED_DESCRIPTION=$(cat <<EOF
|
LOCALIZED_DESCRIPTION=$(cat <<EOF
|
||||||
This is release for:
|
This is release for:
|
||||||
- version: "${{ steps.version.outputs.version }}"
|
- version: "${{ needs.build.outputs.version }}"
|
||||||
- revision: "$SHORT_COMMIT"
|
- revision: "${{ needs.serialize.outputs.short-commit }}"
|
||||||
- timestamp: "${{ steps.date.outputs.date }}"
|
- timestamp: "${{ steps.date.outputs.date }}"
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||||
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
|
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
|
||||||
echo "VERSION_IPA=$MARKETING_VERSION" >> $GITHUB_ENV
|
echo "VERSION_IPA=${{ needs.build.outputs.marketing-version }}" >> $GITHUB_ENV
|
||||||
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_CHANNEL=${{ needs.build.outputs.release-channel }}" >> $GITHUB_ENV
|
||||||
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||||
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
||||||
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
|
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
|
||||||
@@ -391,10 +829,10 @@ jobs:
|
|||||||
if: ${{ inputs.is_beta && inputs.publish }}
|
if: ${{ inputs.is_beta && inputs.publish }}
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: 'SideStore/apps-v2.json'
|
repository: 'SideStore/apps-v2.json'
|
||||||
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
|
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
|
||||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||||
path: 'SideStore/apps-v2.json'
|
path: 'SideStore/apps-v2.json'
|
||||||
|
|
||||||
# for stable builds, let the user manually edit the source.json
|
# for stable builds, let the user manually edit the source.json
|
||||||
- name: Publish to SideStore/apps-v2.json
|
- name: Publish to SideStore/apps-v2.json
|
||||||
@@ -413,7 +851,7 @@ jobs:
|
|||||||
|
|
||||||
# Commit changes and push using SSH
|
# Commit changes and push using SSH
|
||||||
git add --verbose ./_includes/source.json
|
git add --verbose ./_includes/source.json
|
||||||
git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
|
git commit -m " - updated for ${{ needs.serialize.outputs.short-commit }} deployment" || echo "No changes to commit"
|
||||||
|
|
||||||
git push --verbose
|
git push --verbose
|
||||||
popd
|
popd
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -63,4 +63,10 @@ SideStore/.skip-prebuilt-fetch-em_proxy
|
|||||||
# Never check-in this package.resolved file
|
# Never check-in this package.resolved file
|
||||||
# coz SPM then resolves packages using the stale entries in this file
|
# coz SPM then resolves packages using the stale entries in this file
|
||||||
*.xcodeproj/**/Package.resolved
|
*.xcodeproj/**/Package.resolved
|
||||||
*.xcworkspace/**/Package.resolved
|
*.xcworkspace/**/Package.resolved
|
||||||
|
|
||||||
|
# some more commandline build artifacts
|
||||||
|
test-recording.mp4
|
||||||
|
test-recording.log
|
||||||
|
altstore-sources.md
|
||||||
|
local-build.sh
|
||||||
@@ -59,12 +59,21 @@
|
|||||||
A80D60D32D3DD85100CEF65D /* ReleaseTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */; };
|
A80D60D32D3DD85100CEF65D /* ReleaseTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */; };
|
||||||
A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; };
|
A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; };
|
||||||
A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */; };
|
A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */; };
|
||||||
|
A81A8CB92D68B30B0086C96F /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; };
|
||||||
|
A81A8CBA2D68B3110086C96F /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CB02D68B0320086C96F /* TreeMap.swift */; };
|
||||||
|
A81A8CBD2D68B43F0086C96F /* LinkedHashMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */; };
|
||||||
|
A81A8CC82D68BA610086C96F /* DataStructuresTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CC72D68BA610086C96F /* DataStructuresTests.swift */; };
|
||||||
|
A81A8CCE2D68BA8D0086C96F /* LinkedHashMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CBF2D68B4520086C96F /* LinkedHashMapTests.swift */; };
|
||||||
|
A81A8CCF2D68BA8D0086C96F /* TreeMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CB42D68B2180086C96F /* TreeMapTests.swift */; };
|
||||||
|
A81A8CD02D68BA9B0086C96F /* LinkedHashMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */; };
|
||||||
|
A81A8CD12D68BA9B0086C96F /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CB02D68B0320086C96F /* TreeMap.swift */; };
|
||||||
|
A81A8CD22D68BAA30086C96F /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; };
|
||||||
|
A81A8CD42D68BAFF0086C96F /* DataStructureTests.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = A81A8CD32D68BAFF0086C96F /* DataStructureTests.xctestplan */; };
|
||||||
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
|
||||||
A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; };
|
A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; };
|
||||||
A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
|
A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
|
||||||
A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */; };
|
A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */; };
|
||||||
A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; };
|
|
||||||
A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */; };
|
A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */; };
|
||||||
A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */; };
|
A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */; };
|
||||||
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */; };
|
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */; };
|
||||||
@@ -89,6 +98,9 @@
|
|||||||
A8C6D5182D1EE95B00DF01F1 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
A8C6D5182D1EE95B00DF01F1 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
A8D484D82D0CD306002C691D /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = A8D484D72D0CD306002C691D /* AltBackup.ipa */; };
|
A8D484D82D0CD306002C691D /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = A8D484D72D0CD306002C691D /* AltBackup.ipa */; };
|
||||||
A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */; };
|
A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */; };
|
||||||
|
A8E2DB312D684E2A009E5D31 /* UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8E2DB2E2D684E2A009E5D31 /* UITests.swift */; };
|
||||||
|
A8E2DB322D684E2A009E5D31 /* UITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8E2DB2F2D684E2A009E5D31 /* UITestsLaunchTests.swift */; };
|
||||||
|
A8E2DB342D68507F009E5D31 /* SideStoreTests.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = A8E2DB332D68507F009E5D31 /* SideStoreTests.xctestplan */; };
|
||||||
A8EA195F2D4982D600DC6322 /* BaseEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8EA195E2D4982D600DC6322 /* BaseEntity.swift */; };
|
A8EA195F2D4982D600DC6322 /* BaseEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8EA195E2D4982D600DC6322 /* BaseEntity.swift */; };
|
||||||
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; };
|
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; };
|
||||||
A8F838932D048E8F00ED425D /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; };
|
A8F838932D048E8F00ED425D /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; };
|
||||||
@@ -504,6 +516,13 @@
|
|||||||
remoteGlobalIDString = BF58047A246A28F7008AE704;
|
remoteGlobalIDString = BF58047A246A28F7008AE704;
|
||||||
remoteInfo = AltBackup;
|
remoteInfo = AltBackup;
|
||||||
};
|
};
|
||||||
|
A8E2DB272D684CBD009E5D31 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = BFD247622284B9A500981D42 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = BFD247692284B9A500981D42;
|
||||||
|
remoteInfo = SideStore;
|
||||||
|
};
|
||||||
BF66EE832501AE50007EE018 /* PBXContainerItemProxy */ = {
|
BF66EE832501AE50007EE018 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = BFD247622284B9A500981D42 /* Project object */;
|
containerPortal = BFD247622284B9A500981D42 /* Project object */;
|
||||||
@@ -640,6 +659,13 @@
|
|||||||
A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseTrack.swift; sourceTree = "<group>"; };
|
A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseTrack.swift; sourceTree = "<group>"; };
|
||||||
A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIntent.swift; sourceTree = "<group>"; };
|
A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIntent.swift; sourceTree = "<group>"; };
|
||||||
A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationDataHolder.swift; sourceTree = "<group>"; };
|
A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationDataHolder.swift; sourceTree = "<group>"; };
|
||||||
|
A81A8CB02D68B0320086C96F /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = "<group>"; };
|
||||||
|
A81A8CB42D68B2180086C96F /* TreeMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMapTests.swift; sourceTree = "<group>"; };
|
||||||
|
A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedHashMap.swift; sourceTree = "<group>"; };
|
||||||
|
A81A8CBF2D68B4520086C96F /* LinkedHashMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedHashMapTests.swift; sourceTree = "<group>"; };
|
||||||
|
A81A8CC52D68BA610086C96F /* DataStructureTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DataStructureTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
A81A8CC72D68BA610086C96F /* DataStructuresTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStructuresTests.swift; sourceTree = "<group>"; };
|
||||||
|
A81A8CD32D68BAFF0086C96F /* DataStructureTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DataStructureTests.xctestplan; sourceTree = "<group>"; };
|
||||||
A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = "<group>"; };
|
A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = "<group>"; };
|
||||||
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = "<group>"; };
|
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = "<group>"; };
|
||||||
A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = "<group>"; };
|
A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -667,6 +693,11 @@
|
|||||||
A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogView.swift; sourceTree = "<group>"; };
|
A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogView.swift; sourceTree = "<group>"; };
|
||||||
A8D484D72D0CD306002C691D /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = "<group>"; };
|
A8D484D72D0CD306002C691D /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = "<group>"; };
|
||||||
A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+AltStore.swift"; sourceTree = "<group>"; };
|
A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+AltStore.swift"; sourceTree = "<group>"; };
|
||||||
|
A8E2DB212D684CBD009E5D31 /* UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UITests.xcconfig; sourceTree = "<group>"; };
|
||||||
|
A8E2DB2E2D684E2A009E5D31 /* UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITests.swift; sourceTree = "<group>"; };
|
||||||
|
A8E2DB2F2D684E2A009E5D31 /* UITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||||
|
A8E2DB332D68507F009E5D31 /* SideStoreTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SideStoreTests.xctestplan; sourceTree = "<group>"; };
|
||||||
A8EA195E2D4982D600DC6322 /* BaseEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEntity.swift; sourceTree = "<group>"; };
|
A8EA195E2D4982D600DC6322 /* BaseEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEntity.swift; sourceTree = "<group>"; };
|
||||||
A8F66C3C2D04D433009689E6 /* em_proxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = em_proxy.h; sourceTree = "<group>"; };
|
A8F66C3C2D04D433009689E6 /* em_proxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = em_proxy.h; sourceTree = "<group>"; };
|
||||||
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = minimuxer.xcodeproj; sourceTree = "<group>"; };
|
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = minimuxer.xcodeproj; sourceTree = "<group>"; };
|
||||||
@@ -1066,6 +1097,20 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
A81A8CC22D68BA610086C96F /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
A8E2DB1E2D684CBD009E5D31 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
BF580478246A28F7008AE704 /* Frameworks */ = {
|
BF580478246A28F7008AE704 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -1205,6 +1250,24 @@
|
|||||||
path = Intents;
|
path = Intents;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A81A8CB22D68B2030086C96F /* UnitTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A81A8CB32D68B20F0086C96F /* datastructures */,
|
||||||
|
);
|
||||||
|
path = UnitTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A81A8CB32D68B20F0086C96F /* datastructures */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A81A8CBF2D68B4520086C96F /* LinkedHashMapTests.swift */,
|
||||||
|
A81A8CB42D68B2180086C96F /* TreeMapTests.swift */,
|
||||||
|
A81A8CC72D68BA610086C96F /* DataStructuresTests.swift */,
|
||||||
|
);
|
||||||
|
path = datastructures;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A85ACB942D1F31C400AA3DE7 /* xcconfigs */ = {
|
A85ACB942D1F31C400AA3DE7 /* xcconfigs */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1216,6 +1279,7 @@
|
|||||||
A85ACB902D1F31C400AA3DE7 /* AltStore.release.xcconfig */,
|
A85ACB902D1F31C400AA3DE7 /* AltStore.release.xcconfig */,
|
||||||
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */,
|
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */,
|
||||||
A85ACB932D1F31C400AA3DE7 /* AltWidgetExtension.xcconfig */,
|
A85ACB932D1F31C400AA3DE7 /* AltWidgetExtension.xcconfig */,
|
||||||
|
A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */,
|
||||||
);
|
);
|
||||||
path = xcconfigs;
|
path = xcconfigs;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1266,6 +1330,8 @@
|
|||||||
A8AD35572D31BEB2003A28B4 /* datastructures */ = {
|
A8AD35572D31BEB2003A28B4 /* datastructures */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */,
|
||||||
|
A81A8CB02D68B0320086C96F /* TreeMap.swift */,
|
||||||
A868CFE32D319988002F1201 /* SingletonGenericMap.swift */,
|
A868CFE32D319988002F1201 /* SingletonGenericMap.swift */,
|
||||||
);
|
);
|
||||||
path = datastructures;
|
path = datastructures;
|
||||||
@@ -1322,6 +1388,26 @@
|
|||||||
path = common;
|
path = common;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A8E2DB302D684E2A009E5D31 /* UITests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A8E2DB2E2D684E2A009E5D31 /* UITests.swift */,
|
||||||
|
A8E2DB2F2D684E2A009E5D31 /* UITestsLaunchTests.swift */,
|
||||||
|
);
|
||||||
|
path = UITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A8E2DB352D6850A9009E5D31 /* Tests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A81A8CB22D68B2030086C96F /* UnitTests */,
|
||||||
|
A8E2DB302D684E2A009E5D31 /* UITests */,
|
||||||
|
A8E2DB332D68507F009E5D31 /* SideStoreTests.xctestplan */,
|
||||||
|
A81A8CD32D68BAFF0086C96F /* DataStructureTests.xctestplan */,
|
||||||
|
);
|
||||||
|
path = Tests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A8EA19602D4982E300DC6322 /* DatabaseManager */ = {
|
A8EA19602D4982E300DC6322 /* DatabaseManager */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1350,6 +1436,7 @@
|
|||||||
A8F66C072D04C025009689E6 /* SideStore */ = {
|
A8F66C072D04C025009689E6 /* SideStore */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A8E2DB352D6850A9009E5D31 /* Tests */,
|
||||||
A8F66C5C2D04D433009689E6 /* minimuxer */,
|
A8F66C5C2D04D433009689E6 /* minimuxer */,
|
||||||
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */,
|
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */,
|
||||||
A8F66C412D04D433009689E6 /* em_proxy */,
|
A8F66C412D04D433009689E6 /* em_proxy */,
|
||||||
@@ -1919,6 +2006,8 @@
|
|||||||
19104DB22909C06C00C49C7B /* libEmotionalDamage.a */,
|
19104DB22909C06C00C49C7B /* libEmotionalDamage.a */,
|
||||||
191E5FAB290A5D92001A3B7C /* libminimuxer.a */,
|
191E5FAB290A5D92001A3B7C /* libminimuxer.a */,
|
||||||
D586D39828EF58B0000E101F /* AltTests.xctest */,
|
D586D39828EF58B0000E101F /* AltTests.xctest */,
|
||||||
|
A8E2DB212D684CBD009E5D31 /* UITests.xctest */,
|
||||||
|
A81A8CC52D68BA610086C96F /* DataStructureTests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2399,6 +2488,45 @@
|
|||||||
productReference = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */;
|
productReference = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */;
|
||||||
productType = "com.apple.product-type.library.static";
|
productType = "com.apple.product-type.library.static";
|
||||||
};
|
};
|
||||||
|
A81A8CC42D68BA610086C96F /* DataStructureTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = A81A8CC92D68BA610086C96F /* Build configuration list for PBXNativeTarget "DataStructureTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
A81A8CC12D68BA610086C96F /* Sources */,
|
||||||
|
A81A8CC22D68BA610086C96F /* Frameworks */,
|
||||||
|
A81A8CC32D68BA610086C96F /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = DataStructureTests;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = DataStructuresTests;
|
||||||
|
productReference = A81A8CC52D68BA610086C96F /* DataStructureTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
A8E2DB202D684CBD009E5D31 /* UITests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = A8E2DB292D684CBD009E5D31 /* Build configuration list for PBXNativeTarget "UITests" */;
|
||||||
|
buildPhases = (
|
||||||
|
A8E2DB1D2D684CBD009E5D31 /* Sources */,
|
||||||
|
A8E2DB1E2D684CBD009E5D31 /* Frameworks */,
|
||||||
|
A8E2DB1F2D684CBD009E5D31 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
A8E2DB282D684CBD009E5D31 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = UITests;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = UITests;
|
||||||
|
productReference = A8E2DB212D684CBD009E5D31 /* UITests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
|
};
|
||||||
BF45872A2298D31600BD7491 /* libimobiledevice */ = {
|
BF45872A2298D31600BD7491 /* libimobiledevice */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = BF4587332298D31600BD7491 /* Build configuration list for PBXNativeTarget "libimobiledevice" */;
|
buildConfigurationList = BF4587332298D31600BD7491 /* Build configuration list for PBXNativeTarget "libimobiledevice" */;
|
||||||
@@ -2510,7 +2638,7 @@
|
|||||||
BFD247622284B9A500981D42 /* Project object */ = {
|
BFD247622284B9A500981D42 /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 1400;
|
LastSwiftUpdateCheck = 1620;
|
||||||
LastUpgradeCheck = 1020;
|
LastUpgradeCheck = 1020;
|
||||||
ORGANIZATIONNAME = SideStore;
|
ORGANIZATIONNAME = SideStore;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
@@ -2520,6 +2648,13 @@
|
|||||||
191E5FAA290A5D92001A3B7C = {
|
191E5FAA290A5D92001A3B7C = {
|
||||||
CreatedOnToolsVersion = 14.0;
|
CreatedOnToolsVersion = 14.0;
|
||||||
};
|
};
|
||||||
|
A81A8CC42D68BA610086C96F = {
|
||||||
|
CreatedOnToolsVersion = 16.2;
|
||||||
|
};
|
||||||
|
A8E2DB202D684CBD009E5D31 = {
|
||||||
|
CreatedOnToolsVersion = 16.2;
|
||||||
|
TestTargetID = BFD247692284B9A500981D42;
|
||||||
|
};
|
||||||
BF45872A2298D31600BD7491 = {
|
BF45872A2298D31600BD7491 = {
|
||||||
CreatedOnToolsVersion = 10.2.1;
|
CreatedOnToolsVersion = 10.2.1;
|
||||||
};
|
};
|
||||||
@@ -2586,6 +2721,8 @@
|
|||||||
BF989166250AABF3002ACF50 /* AltWidgetExtension */,
|
BF989166250AABF3002ACF50 /* AltWidgetExtension */,
|
||||||
19104DB12909C06C00C49C7B /* EmotionalDamage */,
|
19104DB12909C06C00C49C7B /* EmotionalDamage */,
|
||||||
191E5FAA290A5D92001A3B7C /* minimuxer */,
|
191E5FAA290A5D92001A3B7C /* minimuxer */,
|
||||||
|
A8E2DB202D684CBD009E5D31 /* UITests */,
|
||||||
|
A81A8CC42D68BA610086C96F /* DataStructureTests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -2643,6 +2780,22 @@
|
|||||||
/* End PBXReferenceProxy section */
|
/* End PBXReferenceProxy section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
A81A8CC32D68BA610086C96F /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
A8E2DB1F2D684CBD009E5D31 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A81A8CD42D68BAFF0086C96F /* DataStructureTests.xctestplan in Resources */,
|
||||||
|
A8E2DB342D68507F009E5D31 /* SideStoreTests.xctestplan in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
BF580479246A28F7008AE704 /* Resources */ = {
|
BF580479246A28F7008AE704 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -2814,6 +2967,28 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
A81A8CC12D68BA610086C96F /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A81A8CC82D68BA610086C96F /* DataStructuresTests.swift in Sources */,
|
||||||
|
A81A8CD02D68BA9B0086C96F /* LinkedHashMap.swift in Sources */,
|
||||||
|
A81A8CD22D68BAA30086C96F /* SingletonGenericMap.swift in Sources */,
|
||||||
|
A81A8CD12D68BA9B0086C96F /* TreeMap.swift in Sources */,
|
||||||
|
A81A8CCE2D68BA8D0086C96F /* LinkedHashMapTests.swift in Sources */,
|
||||||
|
A81A8CCF2D68BA8D0086C96F /* TreeMapTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
A8E2DB1D2D684CBD009E5D31 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A8E2DB312D684E2A009E5D31 /* UITests.swift in Sources */,
|
||||||
|
A8E2DB322D684E2A009E5D31 /* UITestsLaunchTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
BF4587282298D31600BD7491 /* Sources */ = {
|
BF4587282298D31600BD7491 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -3015,6 +3190,7 @@
|
|||||||
files = (
|
files = (
|
||||||
D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */,
|
D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */,
|
||||||
D577AB7B2A967DF5007FE952 /* AppsTimelineProvider.swift in Sources */,
|
D577AB7B2A967DF5007FE952 /* AppsTimelineProvider.swift in Sources */,
|
||||||
|
A81A8CB92D68B30B0086C96F /* SingletonGenericMap.swift in Sources */,
|
||||||
D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */,
|
D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */,
|
||||||
BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */,
|
BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */,
|
||||||
D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
|
D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
|
||||||
@@ -3023,7 +3199,6 @@
|
|||||||
D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */,
|
D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */,
|
||||||
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */,
|
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */,
|
||||||
D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */,
|
D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */,
|
||||||
A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */,
|
|
||||||
A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */,
|
A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */,
|
||||||
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
|
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
|
||||||
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */,
|
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */,
|
||||||
@@ -3057,6 +3232,7 @@
|
|||||||
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */,
|
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */,
|
||||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||||
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
|
D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */,
|
||||||
|
A81A8CBD2D68B43F0086C96F /* LinkedHashMap.swift in Sources */,
|
||||||
D5390C3C2AC3A43900D17E62 /* AddSourceViewController.swift in Sources */,
|
D5390C3C2AC3A43900D17E62 /* AddSourceViewController.swift in Sources */,
|
||||||
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */,
|
D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */,
|
||||||
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */,
|
D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */,
|
||||||
@@ -3139,6 +3315,7 @@
|
|||||||
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
|
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
|
||||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||||
A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */,
|
A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */,
|
||||||
|
A81A8CBA2D68B3110086C96F /* TreeMap.swift in Sources */,
|
||||||
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
|
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
|
||||||
A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */,
|
A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */,
|
||||||
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
|
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
|
||||||
@@ -3202,6 +3379,11 @@
|
|||||||
target = BF58047A246A28F7008AE704 /* AltBackup */;
|
target = BF58047A246A28F7008AE704 /* AltBackup */;
|
||||||
targetProxy = A8E00D3D2D0C95B5000DD2C7 /* PBXContainerItemProxy */;
|
targetProxy = A8E00D3D2D0C95B5000DD2C7 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
A8E2DB282D684CBD009E5D31 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = BFD247692284B9A500981D42 /* SideStore */;
|
||||||
|
targetProxy = A8E2DB272D684CBD009E5D31 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
BF66EE842501AE50007EE018 /* PBXTargetDependency */ = {
|
BF66EE842501AE50007EE018 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = BF66EE7D2501AE50007EE018 /* AltStoreCore */;
|
target = BF66EE7D2501AE50007EE018 /* AltStoreCore */;
|
||||||
@@ -3365,6 +3547,112 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
A81A8CCA2D68BA610086C96F /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.SideStore.SideStore.DataStructuresTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
A81A8CCB2D68BA610086C96F /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.SideStore.SideStore.DataStructuresTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
A8E2DB2A2D684CBD009E5D31 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "--PRODUCT-BUNDLE-IDENTIFIER-.UITests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_TARGET_NAME = SideStore;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
A8E2DB2B2D684CBD009E5D31 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "--PRODUCT-BUNDLE-IDENTIFIER-.UITests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_TARGET_NAME = SideStore;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
BF4587342298D31600BD7491 /* Debug */ = {
|
BF4587342298D31600BD7491 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -3900,6 +4188,24 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
A81A8CC92D68BA610086C96F /* Build configuration list for PBXNativeTarget "DataStructureTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
A81A8CCA2D68BA610086C96F /* Debug */,
|
||||||
|
A81A8CCB2D68BA610086C96F /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
A8E2DB292D684CBD009E5D31 /* Build configuration list for PBXNativeTarget "UITests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
A8E2DB2A2D684CBD009E5D31 /* Debug */,
|
||||||
|
A8E2DB2B2D684CBD009E5D31 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
BF4587332298D31600BD7491 /* Build configuration list for PBXNativeTarget "libimobiledevice" */ = {
|
BF4587332298D31600BD7491 /* Build configuration list for PBXNativeTarget "libimobiledevice" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1620"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<TestPlans>
|
||||||
|
<TestPlanReference
|
||||||
|
reference = "container:SideStore/Tests/DataStructureTests.xctestplan"
|
||||||
|
default = "YES">
|
||||||
|
</TestPlanReference>
|
||||||
|
</TestPlans>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A81A8CC42D68BA610086C96F"
|
||||||
|
BuildableName = "DataStructureTests.xctest"
|
||||||
|
BlueprintName = "DataStructureTests"
|
||||||
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -28,18 +28,27 @@
|
|||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<!-- shouldAutocreateTestPlan = "YES"> -->
|
<TestPlans>
|
||||||
|
<TestPlanReference
|
||||||
|
reference = "container:SideStore/Tests/SideStoreTests.xctestplan"
|
||||||
|
default = "YES">
|
||||||
|
</TestPlanReference>
|
||||||
|
</TestPlans>
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO"
|
skipped = "NO">
|
||||||
parallelizable = "YES">
|
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "D586D39728EF58B0000E101F"
|
BlueprintIdentifier = "A8E2DB202D684CBD009E5D31"
|
||||||
BuildableName = "AltTests.xctest"
|
BuildableName = "UITests.xctest"
|
||||||
BlueprintName = "AltTests"
|
BlueprintName = "UITests"
|
||||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
<SelectedTests>
|
||||||
|
<Test
|
||||||
|
Identifier = "UITests/testExample()">
|
||||||
|
</Test>
|
||||||
|
</SelectedTests>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ extension AddSourceViewController
|
|||||||
var sourceAddress: String = ""
|
var sourceAddress: String = ""
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var sourceURL: URL?
|
var sourceURLs: [URL] = []
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var sourcePreviewResult: SourcePreviewResult?
|
var sourcePreviewResults: [SourcePreviewResult] = []
|
||||||
|
|
||||||
|
|
||||||
/* State */
|
/* State */
|
||||||
@@ -60,6 +60,8 @@ extension AddSourceViewController
|
|||||||
|
|
||||||
class AddSourceViewController: UICollectionViewController
|
class AddSourceViewController: UICollectionViewController
|
||||||
{
|
{
|
||||||
|
private var stagedForAdd: LinkedHashMap<Source, Bool> = LinkedHashMap()
|
||||||
|
|
||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
private lazy var addSourceDataSource = self.makeAddSourceDataSource()
|
private lazy var addSourceDataSource = self.makeAddSourceDataSource()
|
||||||
private lazy var sourcePreviewDataSource = self.makeSourcePreviewDataSource()
|
private lazy var sourcePreviewDataSource = self.makeSourcePreviewDataSource()
|
||||||
@@ -117,6 +119,7 @@ private extension AddSourceViewController
|
|||||||
layoutConfig.contentInsetsReference = .safeArea
|
layoutConfig.contentInsetsReference = .safeArea
|
||||||
|
|
||||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||||
|
|
||||||
guard let self, let section = Section(rawValue: sectionIndex) else { return nil }
|
guard let self, let section = Section(rawValue: sectionIndex) else { return nil }
|
||||||
switch section
|
switch section
|
||||||
{
|
{
|
||||||
@@ -140,14 +143,19 @@ private extension AddSourceViewController
|
|||||||
configuration.showsSeparators = false
|
configuration.showsSeparators = false
|
||||||
configuration.backgroundColor = .clear
|
configuration.backgroundColor = .clear
|
||||||
|
|
||||||
if self.viewModel.sourceURL != nil && self.viewModel.isShowingPreviewStatus
|
if !self.viewModel.sourceURLs.isEmpty && self.viewModel.isShowingPreviewStatus
|
||||||
{
|
{
|
||||||
switch self.viewModel.sourcePreviewResult
|
for result in self.viewModel.sourcePreviewResults
|
||||||
{
|
{
|
||||||
case (_, .success)?: configuration.footerMode = .none
|
switch result
|
||||||
case (_, .failure)?: configuration.footerMode = .supplementary
|
{
|
||||||
case nil where self.viewModel.isLoadingPreview: configuration.footerMode = .supplementary
|
case (_, .success): configuration.footerMode = .none
|
||||||
default: configuration.footerMode = .none
|
case (_, .failure): configuration.footerMode = .supplementary
|
||||||
|
break
|
||||||
|
// case nil where self.viewModel.isLoadingPreview: configuration.footerMode = .supplementary
|
||||||
|
// break
|
||||||
|
// default: configuration.footerMode = .none
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -303,50 +311,71 @@ private extension AddSourceViewController
|
|||||||
{
|
{
|
||||||
/* Pipeline */
|
/* Pipeline */
|
||||||
|
|
||||||
// Map UITextField text -> URL
|
// Map UITextField text -> URLs
|
||||||
self.viewModel.$sourceAddress
|
self.viewModel.$sourceAddress
|
||||||
.map { [weak self] in self?.sourceURL(from: $0) }
|
.map { [weak self] in
|
||||||
.assign(to: &self.viewModel.$sourceURL)
|
guard let self else { return [] }
|
||||||
|
|
||||||
|
// Preserve order of parsed URLs
|
||||||
|
let lines = $0.split(whereSeparator: { $0.isWhitespace })
|
||||||
|
.map(String.init)
|
||||||
|
.compactMap(self.sourceURL)
|
||||||
|
|
||||||
|
return NSOrderedSet(array: lines).array as! [URL] // de-duplicate while preserving order
|
||||||
|
}
|
||||||
|
.assign(to: &self.viewModel.$sourceURLs)
|
||||||
|
|
||||||
let showPreviewStatusPublisher = self.viewModel.$isShowingPreviewStatus
|
let showPreviewStatusPublisher = self.viewModel.$isShowingPreviewStatus
|
||||||
.filter { $0 == true }
|
.filter { $0 == true }
|
||||||
|
|
||||||
let sourceURLPublisher = self.viewModel.$sourceURL
|
let sourceURLsPublisher = self.viewModel.$sourceURLs
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.debounce(for: 0.2, scheduler: RunLoop.main)
|
.debounce(for: 0.2, scheduler: RunLoop.main)
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.map { [weak self] sourceURL in
|
.map { [weak self] sourceURLs in
|
||||||
// Only set sourcePreviewResult to nil if sourceURL actually changes.
|
// Only set sourcePreviewResult to nil if sourceURL actually changes.
|
||||||
self?.viewModel.sourcePreviewResult = nil
|
self?.viewModel.sourcePreviewResults = []
|
||||||
return sourceURL
|
return sourceURLs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map URL -> Source Preview
|
// Map URL -> Source Preview
|
||||||
Publishers.CombineLatest(sourceURLPublisher, showPreviewStatusPublisher.prepend(false))
|
Publishers.CombineLatest(sourceURLsPublisher, showPreviewStatusPublisher.prepend(false))
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.map { $0.0 }
|
.map { $0.0 }
|
||||||
.compactMap { [weak self] (sourceURL: URL?) -> AnyPublisher<SourcePreviewResult?, Never>? in
|
.flatMap { [weak self] (sourceURLs: [URL]) -> AnyPublisher<[SourcePreviewResult?], Never> in
|
||||||
guard let self else { return nil }
|
guard let self else { return Just([]).eraseToAnyPublisher() }
|
||||||
|
|
||||||
guard let sourceURL else {
|
|
||||||
// Unlike above guard, this continues the pipeline with nil value.
|
|
||||||
return Just(nil).eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.viewModel.isLoadingPreview = true
|
self.viewModel.isLoadingPreview = true
|
||||||
return self.fetchSourcePreview(sourceURL: sourceURL).eraseToAnyPublisher()
|
|
||||||
|
// Create publishers maintaining order
|
||||||
|
let publishers = sourceURLs.enumerated().map { index, sourceURL in
|
||||||
|
self.fetchSourcePreview(sourceURL: sourceURL)
|
||||||
|
.map { result in
|
||||||
|
// Add index to maintain order
|
||||||
|
(index: index, result: result)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
// since network requests are concurrent, we sort the values when they are received
|
||||||
|
return publishers.isEmpty
|
||||||
|
? Just([]).eraseToAnyPublisher()
|
||||||
|
: Publishers.MergeMany(publishers)
|
||||||
|
.collect() // await all publishers to emit the results
|
||||||
|
.map { results in // perform sorting of the collected results
|
||||||
|
// Sort by original index before returning
|
||||||
|
results.sorted { $0.index < $1.index }
|
||||||
|
.map { $0.result }
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
.switchToLatest() // Cancels previous publisher
|
.sink { [weak self] sourcePreviewResults in
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.sink { [weak self] sourcePreviewResult in
|
|
||||||
self?.viewModel.isLoadingPreview = false
|
self?.viewModel.isLoadingPreview = false
|
||||||
self?.viewModel.sourcePreviewResult = sourcePreviewResult
|
self?.viewModel.sourcePreviewResults = sourcePreviewResults.compactMap{$0}
|
||||||
}
|
}
|
||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
|
|
||||||
|
|
||||||
/* Update UI */
|
/* Update UI */
|
||||||
|
|
||||||
Publishers.CombineLatest(self.viewModel.$isLoadingPreview.removeDuplicates(),
|
Publishers.CombineLatest(self.viewModel.$isLoadingPreview.removeDuplicates(),
|
||||||
self.viewModel.$isShowingPreviewStatus.removeDuplicates())
|
self.viewModel.$isShowingPreviewStatus.removeDuplicates())
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
@@ -359,7 +388,7 @@ private extension AddSourceViewController
|
|||||||
|
|
||||||
if let footerView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath) as? PlaceholderCollectionReusableView
|
if let footerView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath) as? PlaceholderCollectionReusableView
|
||||||
{
|
{
|
||||||
self.configure(footerView, with: self.viewModel.sourcePreviewResult)
|
self.configure(footerView, with: self.viewModel.sourcePreviewResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = UICollectionViewLayoutInvalidationContext()
|
let context = UICollectionViewLayoutInvalidationContext()
|
||||||
@@ -370,27 +399,38 @@ private extension AddSourceViewController
|
|||||||
}
|
}
|
||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
|
|
||||||
self.viewModel.$sourcePreviewResult
|
self.viewModel.$sourcePreviewResults
|
||||||
.map { $0?.1 }
|
.map { sourcePreviewResults -> [Source] in
|
||||||
.map { result -> Managed<Source>? in
|
// Maintain order based on original sourceURLs array
|
||||||
switch result
|
let orderedSources = self.viewModel.sourceURLs.compactMap { sourceURL -> Source? in
|
||||||
{
|
// Find the preview result matching this URL
|
||||||
case .success(let source): return source
|
guard let previewResult = sourcePreviewResults.first(where: { $0.sourceURL == sourceURL }),
|
||||||
case .failure, nil: return nil
|
case .success(let managedSource) = previewResult.result
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return managedSource.wrappedValue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.removeDuplicates { (sourceA: Managed<Source>?, sourceB: Managed<Source>?) in
|
return orderedSources
|
||||||
sourceA?.identifier == sourceB?.identifier
|
|
||||||
}
|
}
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] source in
|
.sink { [weak self] sources in
|
||||||
self?.updateSourcePreview(for: source?.wrappedValue)
|
self?.updateSourcesPreview(for: sources)
|
||||||
}
|
}
|
||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
|
|
||||||
|
|
||||||
let addPublisher = NotificationCenter.default.publisher(for: AppManager.didAddSourceNotification)
|
let mergedNotificationPublisher = Publishers.Merge(
|
||||||
let removePublisher = NotificationCenter.default.publisher(for: AppManager.didRemoveSourceNotification)
|
NotificationCenter.default.publisher(for: AppManager.didAddSourceNotification),
|
||||||
Publishers.Merge(addPublisher, removePublisher)
|
NotificationCenter.default.publisher(for: AppManager.didRemoveSourceNotification)
|
||||||
|
)
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.share() // Shares the upstream publisher with multiple subscribers
|
||||||
|
|
||||||
|
// Update recommended sources section when sources are added/removed
|
||||||
|
mergedNotificationPublisher
|
||||||
.compactMap { notification -> String? in
|
.compactMap { notification -> String? in
|
||||||
guard let source = notification.object as? Source,
|
guard let source = notification.object as? Source,
|
||||||
let context = source.managedObjectContext
|
let context = source.managedObjectContext
|
||||||
@@ -399,7 +439,6 @@ private extension AddSourceViewController
|
|||||||
let sourceID = context.performAndWait { source.identifier }
|
let sourceID = context.performAndWait { source.identifier }
|
||||||
return sourceID
|
return sourceID
|
||||||
}
|
}
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.compactMap { [dataSource = recommendedSourcesDataSource] sourceID -> IndexPath? in
|
.compactMap { [dataSource = recommendedSourcesDataSource] sourceID -> IndexPath? in
|
||||||
guard let index = dataSource.items.firstIndex(where: { $0.identifier == sourceID }) else { return nil }
|
guard let index = dataSource.items.firstIndex(where: { $0.identifier == sourceID }) else { return nil }
|
||||||
|
|
||||||
@@ -411,6 +450,32 @@ private extension AddSourceViewController
|
|||||||
self?.collectionView.reloadItems(at: [indexPath])
|
self?.collectionView.reloadItems(at: [indexPath])
|
||||||
}
|
}
|
||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
|
|
||||||
|
// Update previews section when sources are added/removed
|
||||||
|
// mergedNotificationPublisher
|
||||||
|
// .sink { [weak self] _ in
|
||||||
|
// // reload the entire of previews section to get latest state
|
||||||
|
// self?.collectionView.reloadSections(IndexSet(integer: Section.preview.rawValue))
|
||||||
|
// }
|
||||||
|
// .store(in: &self.cancellables)
|
||||||
|
|
||||||
|
mergedNotificationPublisher
|
||||||
|
.compactMap { notification -> String? in
|
||||||
|
guard let source = notification.object as? Source,
|
||||||
|
let context = source.managedObjectContext
|
||||||
|
else { return nil }
|
||||||
|
return context.performAndWait { source.identifier }
|
||||||
|
}
|
||||||
|
.compactMap { [weak self] sourceID -> IndexPath? in
|
||||||
|
guard let dataSource = self?.sourcePreviewDataSource,
|
||||||
|
let index = dataSource.items.firstIndex(where: { $0.identifier == sourceID })
|
||||||
|
else { return nil }
|
||||||
|
return IndexPath(item: index, section: Section.preview.rawValue)
|
||||||
|
}
|
||||||
|
.sink { [weak self] indexPath in
|
||||||
|
self?.collectionView.reloadItems(at: [indexPath])
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sourceURL(from address: String) -> URL?
|
func sourceURL(from address: String) -> URL?
|
||||||
@@ -458,35 +523,51 @@ private extension AddSourceViewController
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSourcePreview(for source: Source?)
|
func updateSourcesPreview(for sources: [Source]) {
|
||||||
{
|
// Calculate changes needed to go from current items to new items
|
||||||
let items = [source].compactMap { $0 }
|
let currentItemCount = self.sourcePreviewDataSource.items.count
|
||||||
|
let newItemCount = sources.count
|
||||||
|
|
||||||
// Have to provide changes in terms of sourcePreviewDataSource.
|
var changes: [RSTCellContentChange] = []
|
||||||
let indexPath = IndexPath(row: 0, section: 0)
|
|
||||||
|
|
||||||
if !items.isEmpty && self.sourcePreviewDataSource.items.isEmpty
|
if currentItemCount == 0 && newItemCount > 0 {
|
||||||
{
|
// Insert all items if we currently have none
|
||||||
let change = RSTCellContentChange(type: .insert, currentIndexPath: nil, destinationIndexPath: indexPath)
|
for i in 0..<newItemCount {
|
||||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
let indexPath = IndexPath(row: i, section: 0)
|
||||||
}
|
let change = RSTCellContentChange(type: .insert,
|
||||||
else if items.isEmpty && !self.sourcePreviewDataSource.items.isEmpty
|
currentIndexPath: nil,
|
||||||
{
|
destinationIndexPath: indexPath)
|
||||||
let change = RSTCellContentChange(type: .delete, currentIndexPath: indexPath, destinationIndexPath: nil)
|
changes.append(change)
|
||||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
}
|
||||||
}
|
} else if currentItemCount > 0 && newItemCount == 0 {
|
||||||
else if !items.isEmpty && !self.sourcePreviewDataSource.items.isEmpty
|
// Delete all items if we're going to have none
|
||||||
{
|
for i in 0..<currentItemCount {
|
||||||
let change = RSTCellContentChange(type: .update, currentIndexPath: indexPath, destinationIndexPath: indexPath)
|
let indexPath = IndexPath(row: i, section: 0)
|
||||||
self.sourcePreviewDataSource.setItems(items, with: [change])
|
let change = RSTCellContentChange(type: .delete,
|
||||||
|
currentIndexPath: indexPath,
|
||||||
|
destinationIndexPath: nil)
|
||||||
|
changes.append(change)
|
||||||
|
}
|
||||||
|
} else if currentItemCount != newItemCount {
|
||||||
|
// If counts differ, do a section update
|
||||||
|
let change = RSTCellContentChange(type: .update, sectionIndex: 0)
|
||||||
|
changes = [change]
|
||||||
|
} else {
|
||||||
|
// Update existing items in place
|
||||||
|
for i in 0..<newItemCount {
|
||||||
|
let indexPath = IndexPath(row: i, section: 0)
|
||||||
|
let change = RSTCellContentChange(type: .update,
|
||||||
|
currentIndexPath: indexPath,
|
||||||
|
destinationIndexPath: indexPath)
|
||||||
|
changes.append(change)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == nil
|
self.sourcePreviewDataSource.setItems(sources, with: changes)
|
||||||
{
|
|
||||||
|
if sources.isEmpty {
|
||||||
self.collectionView.reloadSections([Section.preview.rawValue])
|
self.collectionView.reloadSections([Section.preview.rawValue])
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,9 +591,6 @@ private extension AddSourceViewController
|
|||||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
|
|
||||||
let config = UIImage.SymbolConfiguration(scale: .medium)
|
let config = UIImage.SymbolConfiguration(scale: .medium)
|
||||||
let image = UIImage(systemName: "plus.circle.fill", withConfiguration: config)?.withTintColor(.white, renderingMode: .alwaysOriginal)
|
|
||||||
cell.bannerView.button.setImage(image, for: .normal)
|
|
||||||
cell.bannerView.button.setImage(image, for: .highlighted)
|
|
||||||
cell.bannerView.button.setTitle(nil, for: .normal)
|
cell.bannerView.button.setTitle(nil, for: .normal)
|
||||||
cell.bannerView.button.imageView?.contentMode = .scaleAspectFit
|
cell.bannerView.button.imageView?.contentMode = .scaleAspectFit
|
||||||
cell.bannerView.button.contentHorizontalAlignment = .fill // Fill entire button with imageView
|
cell.bannerView.button.contentHorizontalAlignment = .fill // Fill entire button with imageView
|
||||||
@@ -521,28 +599,54 @@ private extension AddSourceViewController
|
|||||||
cell.bannerView.button.tintColor = .clear
|
cell.bannerView.button.tintColor = .clear
|
||||||
cell.bannerView.button.isHidden = false
|
cell.bannerView.button.isHidden = false
|
||||||
|
|
||||||
|
// mark the button with label (useful for accessibility and for UITests)
|
||||||
|
cell.bannerView.button.accessibilityIdentifier = "add"
|
||||||
|
|
||||||
|
func setButtonIcon()
|
||||||
|
{
|
||||||
|
Task<Void, Never>(priority: .userInitiated) { [weak cell] in
|
||||||
|
guard let cell else { return }
|
||||||
|
|
||||||
|
var isSourceAlreadyPersisted = false
|
||||||
|
do
|
||||||
|
{
|
||||||
|
isSourceAlreadyPersisted = try await source.isAdded
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to determine if source is added.", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the plus icon by default
|
||||||
|
var buttonIcon = UIImage(systemName: "plus.circle.fill", withConfiguration: config)?.withTintColor(.white, renderingMode: .alwaysOriginal)
|
||||||
|
|
||||||
|
// if the source is already added/staged for adding, use the checkmark icon
|
||||||
|
let isStagedForAdd = self.stagedForAdd[source] == true
|
||||||
|
if isStagedForAdd || isSourceAlreadyPersisted
|
||||||
|
{
|
||||||
|
buttonIcon = UIImage(systemName: "checkmark.circle.fill", withConfiguration: config)?
|
||||||
|
.withTintColor(isSourceAlreadyPersisted ? .green : .white, renderingMode: .alwaysOriginal)
|
||||||
|
}
|
||||||
|
cell.bannerView.button.setImage(buttonIcon, for: .normal)
|
||||||
|
cell.bannerView.button.isEnabled = !isSourceAlreadyPersisted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the icon
|
||||||
|
setButtonIcon()
|
||||||
|
|
||||||
let action = UIAction(identifier: .addSource) { [weak self] _ in
|
let action = UIAction(identifier: .addSource) { [weak self] _ in
|
||||||
self?.add(source)
|
guard let self else { return }
|
||||||
|
|
||||||
|
self.stagedForAdd[source, default: false].toggle()
|
||||||
|
|
||||||
|
// update the button icon
|
||||||
|
setButtonIcon()
|
||||||
}
|
}
|
||||||
cell.bannerView.button.addAction(action, for: .primaryActionTriggered)
|
cell.bannerView.button.addAction(action, for: .primaryActionTriggered)
|
||||||
|
|
||||||
Task<Void, Never>(priority: .userInitiated) {
|
|
||||||
do
|
|
||||||
{
|
|
||||||
let isAdded = try await source.isAdded
|
|
||||||
if isAdded
|
|
||||||
{
|
|
||||||
cell.bannerView.button.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
print("Failed to determine if source is added.", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(_ footerView: PlaceholderCollectionReusableView, with sourcePreviewResult: SourcePreviewResult?)
|
func configure(_ footerView: PlaceholderCollectionReusableView, with sourcePreviewResults: [SourcePreviewResult?])
|
||||||
{
|
{
|
||||||
footerView.placeholderView.stackView.isLayoutMarginsRelativeArrangement = false
|
footerView.placeholderView.stackView.isLayoutMarginsRelativeArrangement = false
|
||||||
|
|
||||||
@@ -552,23 +656,33 @@ private extension AddSourceViewController
|
|||||||
|
|
||||||
footerView.placeholderView.detailTextLabel.isHidden = true
|
footerView.placeholderView.detailTextLabel.isHidden = true
|
||||||
|
|
||||||
switch sourcePreviewResult
|
var errorText: String? = nil
|
||||||
|
var isError: Bool = false
|
||||||
|
for result in sourcePreviewResults
|
||||||
{
|
{
|
||||||
case (let sourceURL, .failure(let previewError))? where self.viewModel.sourceURL == sourceURL && !self.viewModel.isLoadingPreview:
|
switch result
|
||||||
// The current URL matches the error being displayed, and we're not loading another preview, so show error.
|
{
|
||||||
|
case (let sourceURL, .failure(let previewError))? where (self.viewModel.sourceURLs.contains(sourceURL) && !self.viewModel.isLoadingPreview):
|
||||||
footerView.placeholderView.textLabel.text = (previewError as NSError).localizedDebugDescription ?? previewError.localizedDescription
|
// The current URL matches the error being displayed, and we're not loading another preview, so show error.
|
||||||
footerView.placeholderView.textLabel.isHidden = false
|
|
||||||
|
errorText = (previewError as NSError).localizedDebugDescription ?? previewError.localizedDescription
|
||||||
footerView.placeholderView.activityIndicatorView.stopAnimating()
|
footerView.placeholderView.textLabel.text = errorText
|
||||||
|
footerView.placeholderView.textLabel.isHidden = false
|
||||||
default:
|
|
||||||
// The current URL does not match the URL of the source/error being displayed, so show loading indicator.
|
isError = true
|
||||||
|
|
||||||
footerView.placeholderView.textLabel.text = nil
|
default:
|
||||||
footerView.placeholderView.textLabel.isHidden = true
|
// The current URL does not match the URL of the source/error being displayed, so show loading indicator.
|
||||||
|
errorText = nil
|
||||||
|
footerView.placeholderView.textLabel.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
footerView.placeholderView.textLabel.text = errorText
|
||||||
|
|
||||||
|
if !isError{
|
||||||
footerView.placeholderView.activityIndicatorView.startAnimating()
|
footerView.placeholderView.activityIndicatorView.startAnimating()
|
||||||
|
} else{
|
||||||
|
footerView.placeholderView.activityIndicatorView.stopAnimating()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,30 +766,60 @@ private extension AddSourceViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func add(@AsyncManaged _ source: Source)
|
@IBAction func commitChanges(_ sender: UIBarButtonItem)
|
||||||
{
|
{
|
||||||
Task<Void, Never> {
|
struct StagedSource: Hashable {
|
||||||
do
|
@AsyncManaged var source: Source
|
||||||
{
|
|
||||||
let isRecommended = await $source.isRecommended
|
// Conformance for Equatable/Hashable by comparing the underlying source
|
||||||
if isRecommended
|
static func == (lhs: StagedSource, rhs: StagedSource) -> Bool {
|
||||||
{
|
return lhs.source.identifier == rhs.source.identifier
|
||||||
try await AppManager.shared.add(source, message: nil, presentingViewController: self)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Use default message
|
|
||||||
try await AppManager.shared.add(source, presentingViewController: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.dismiss()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch is CancellationError {}
|
|
||||||
catch
|
func hash(into hasher: inout Hasher) {
|
||||||
{
|
hasher.combine(source)
|
||||||
let errorTitle = NSLocalizedString("Unable to Add Source", comment: "")
|
}
|
||||||
await self.presentAlert(title: errorTitle, message: error.localizedDescription)
|
}
|
||||||
|
|
||||||
|
Task<Void, Never> {
|
||||||
|
var isCancelled = false
|
||||||
|
// OK: COMMIT the staged changes now
|
||||||
|
// Convert the stagedForAdd dictionary into an array of StagedSource
|
||||||
|
let stagedSources: [StagedSource] = self.stagedForAdd.filter { $0.value }
|
||||||
|
.map { StagedSource(source: $0.key) }
|
||||||
|
|
||||||
|
for staged in stagedSources {
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Use the projected value to safely access isRecommended asynchronously
|
||||||
|
let isRecommended = await staged.$source.isRecommended
|
||||||
|
if isRecommended
|
||||||
|
{
|
||||||
|
try await AppManager.shared.add(staged.source, message: nil, presentingViewController: self)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use default message
|
||||||
|
try await AppManager.shared.add(staged.source, presentingViewController: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove this kv pair
|
||||||
|
_ = self.stagedForAdd.removeValue(forKey: staged.source)
|
||||||
|
}
|
||||||
|
catch is CancellationError {
|
||||||
|
isCancelled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
let errorTitle = NSLocalizedString("Unable to Add Source", comment: "")
|
||||||
|
await self.presentAlert(title: errorTitle, message: error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isCancelled {
|
||||||
|
// finally dismiss the sheet/viewcontroller
|
||||||
|
self.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -737,7 +881,7 @@ extension AddSourceViewController: UICollectionViewDelegateFlowLayout
|
|||||||
case (.preview, UICollectionView.elementKindSectionFooter):
|
case (.preview, UICollectionView.elementKindSectionFooter):
|
||||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseID.placeholderFooter.rawValue, for: indexPath) as! PlaceholderCollectionReusableView
|
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ReuseID.placeholderFooter.rawValue, for: indexPath) as! PlaceholderCollectionReusableView
|
||||||
|
|
||||||
self.configure(footerView, with: self.viewModel.sourcePreviewResult)
|
self.configure(footerView, with: self.viewModel.sourcePreviewResults)
|
||||||
|
|
||||||
return footerView
|
return footerView
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class AddSourceTextFieldCell: UICollectionViewCell
|
|||||||
self.textField.translatesAutoresizingMaskIntoConstraints = false
|
self.textField.translatesAutoresizingMaskIntoConstraints = false
|
||||||
self.textField.placeholder = "apps.sidestore.io"
|
self.textField.placeholder = "apps.sidestore.io"
|
||||||
self.textField.textContentType = .URL
|
self.textField.textContentType = .URL
|
||||||
self.textField.keyboardType = .URL
|
// self.textField.keyboardType = .URL // we can add multiple sources now delimited by spaces/newline so we use normal keyboard not url keyboard
|
||||||
self.textField.returnKeyType = .done
|
self.textField.returnKeyType = .done
|
||||||
self.textField.autocapitalizationType = .none
|
self.textField.autocapitalizationType = .none
|
||||||
self.textField.autocorrectionType = .no
|
self.textField.autocorrectionType = .no
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv">
|
||||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="collection view cell content view" minToolsVersion="11.0"/>
|
<capability name="collection view cell content view" minToolsVersion="11.0"/>
|
||||||
@@ -224,6 +224,11 @@
|
|||||||
<segue destination="qr8-ss-Ghz" kind="unwind" unwindAction="unwindFromAddSource:" id="Pba-Kh-qfH"/>
|
<segue destination="qr8-ss-Ghz" kind="unwind" unwindAction="unwindFromAddSource:" id="Pba-Kh-qfH"/>
|
||||||
</connections>
|
</connections>
|
||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
|
<barButtonItem key="rightBarButtonItem" systemItem="done" id="oza-rj-JhC">
|
||||||
|
<connections>
|
||||||
|
<action selector="commitChanges:" destination="bbz-wy-kaK" id="4FB-Sj-E14"/>
|
||||||
|
</connections>
|
||||||
|
</barButtonItem>
|
||||||
</navigationItem>
|
</navigationItem>
|
||||||
<connections>
|
<connections>
|
||||||
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="LO9-iP-zZC"/>
|
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="LO9-iP-zZC"/>
|
||||||
|
|||||||
97
Makefile
97
Makefile
@@ -167,22 +167,86 @@ test:
|
|||||||
BUILD_CONFIG ?= Release
|
BUILD_CONFIG ?= Release
|
||||||
MARKETING_VERSION ?=
|
MARKETING_VERSION ?=
|
||||||
BUNDLE_ID_SUFFIX ?=
|
BUNDLE_ID_SUFFIX ?=
|
||||||
|
# Common build settings for xcodebuild
|
||||||
|
COMMON_BUILD_SETTINGS = \
|
||||||
|
-workspace AltStore.xcworkspace \
|
||||||
|
-scheme SideStore \
|
||||||
|
-sdk iphoneos \
|
||||||
|
-configuration $(BUILD_CONFIG) \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
AD_HOC_CODE_SIGNING_ALLOWED=YES \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
DEVELOPMENT_TEAM=XYZ0123456 \
|
||||||
|
ORG_IDENTIFIER=com.SideStore
|
||||||
|
|
||||||
|
# Append MARKETING_VERSION if it’s not empty (coz otherwise the blank entry becomes override)
|
||||||
|
ifneq ($(strip $(MARKETING_VERSION)),)
|
||||||
|
COMMON_BUILD_SETTINGS += MARKETING_VERSION=$(MARKETING_VERSION)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Append BUNDLE_ID_SUFFIX if it’s not empty (coz otherwise the blank entry becomes override)
|
||||||
|
ifneq ($(strip $(BUNDLE_ID_SUFFIX)),)
|
||||||
|
COMMON_BUILD_SETTINGS += BUNDLE_ID_SUFFIX=$(BUNDLE_ID_SUFFIX)
|
||||||
|
endif
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
||||||
@echo ""
|
@echo ""
|
||||||
@xcodebuild -workspace AltStore.xcworkspace \
|
@xcodebuild archive -archivePath ./SideStore \
|
||||||
-scheme SideStore \
|
$(COMMON_BUILD_SETTINGS)
|
||||||
-sdk iphoneos \
|
|
||||||
-configuration $(BUILD_CONFIG) \
|
build-and-test:
|
||||||
archive -archivePath ./SideStore \
|
@rm -rf build/tests/test-results.xcresult
|
||||||
CODE_SIGNING_REQUIRED=NO \
|
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
||||||
AD_HOC_CODE_SIGNING_ALLOWED=YES \
|
@echo ""
|
||||||
CODE_SIGNING_ALLOWED=NO \
|
@echo "Performing a build and running tests..."
|
||||||
DEVELOPMENT_TEAM=XYZ0123456 \
|
@xcodebuild test \
|
||||||
ORG_IDENTIFIER=com.SideStore \
|
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
|
||||||
MARKETING_VERSION=$(MARKETING_VERSION) \
|
-resultBundlePath build/tests/test-results.xcresult \
|
||||||
BUNDLE_ID_SUFFIX=$(BUNDLE_ID_SUFFIX)
|
-enableCodeCoverage YES \
|
||||||
# DWARF_DSYM_FOLDER_PATH="."
|
$(COMMON_BUILD_SETTINGS)
|
||||||
|
|
||||||
|
build-tests:
|
||||||
|
@rm -rf build/tests/test-results.xcresult
|
||||||
|
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building Tests for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
||||||
|
@echo ""
|
||||||
|
@echo "Performing a build-for-testing..."
|
||||||
|
@xcodebuild build-for-testing \
|
||||||
|
-enableCodeCoverage YES \
|
||||||
|
$(COMMON_BUILD_SETTINGS)
|
||||||
|
|
||||||
|
run-tests:
|
||||||
|
@rm -rf build/tests/test-results.xcresult
|
||||||
|
@echo ">>>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Testing for $(BUILD_CONFIG) mode! <<<<<<<<<<"
|
||||||
|
@echo ""
|
||||||
|
@echo "Performing a test-without-building..."
|
||||||
|
@xcodebuild test-without-building \
|
||||||
|
-enableCodeCoverage YES \
|
||||||
|
-resultBundlePath build/tests/test-results.xcresult \
|
||||||
|
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
|
||||||
|
$(COMMON_BUILD_SETTINGS)
|
||||||
|
|
||||||
|
boot-sim-async:
|
||||||
|
@if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \
|
||||||
|
echo "Simulator 'iPhone 16 Pro' is already booted."; \
|
||||||
|
else \
|
||||||
|
echo "Booting simulator 'iPhone 16 Pro' asynchronously..."; \
|
||||||
|
xcrun simctl boot "iPhone 16 Pro" & \
|
||||||
|
echo "Simulator boot command dispatched."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
sim-boot-check:
|
||||||
|
@echo "Checking simulator boot status..."
|
||||||
|
@if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \
|
||||||
|
echo "Simulator 'iPhone 16 Pro' is booted."; \
|
||||||
|
else \
|
||||||
|
echo "Simulator bootup failed or is not booted yet."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
clean-build:
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
@xcodebuild clean -workspace AltStore.xcworkspace -scheme SideStore
|
||||||
|
|
||||||
fakesign-apps:
|
fakesign-apps:
|
||||||
rm -rf SideStore.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
|
rm -rf SideStore.xcarchive/Products/Applications/SideStore.app/Frameworks/AltStoreCore.framework/Frameworks/
|
||||||
@@ -308,7 +372,7 @@ ipa-altbackup: checkPaths copy-altbackup
|
|||||||
@mkdir -p "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
@mkdir -p "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
||||||
@echo " Copying from $(ALT_APP_SRC) into $(ALT_APP_PAYLOAD_DST)"
|
@echo " Copying from $(ALT_APP_SRC) into $(ALT_APP_PAYLOAD_DST)"
|
||||||
@cp -R -f "$(ALT_APP_SRC)/." "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
@cp -R -f "$(ALT_APP_SRC)/." "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
|
||||||
@pushd "$(ALT_APP_DST_ARCHIVE)" && zip -r "../../$(ALT_APP_IPA_DST)" Payload && popd
|
@pushd "$(ALT_APP_DST_ARCHIVE)" && zip -r "../../$(ALT_APP_IPA_DST)" Payload || popd
|
||||||
@cp -f "$(ALT_APP_IPA_DST)" AltStore/Resources
|
@cp -f "$(ALT_APP_IPA_DST)" AltStore/Resources
|
||||||
@echo " IPA created: AltStore/Resources/AltBackup.ipa"
|
@echo " IPA created: AltStore/Resources/AltBackup.ipa"
|
||||||
|
|
||||||
@@ -317,11 +381,8 @@ clean-altbackup:
|
|||||||
@echo "====> Cleaning up AltBackup related artifacts <===="
|
@echo "====> Cleaning up AltBackup related artifacts <===="
|
||||||
@rm -rf build/altbackup.xcarchive/
|
@rm -rf build/altbackup.xcarchive/
|
||||||
@rm -f build/AltBackup.ipa
|
@rm -f build/AltBackup.ipa
|
||||||
@rm -f AltStore/Resources/AltBackup.ipa
|
#@rm -f AltStore/Resources/AltBackup.ipa
|
||||||
|
|
||||||
clean: clean-altbackup
|
clean: clean-altbackup
|
||||||
@rm -rf *.xcarchive/
|
|
||||||
@rm -rf *.dSYM/
|
|
||||||
@rm -rf SideStore.ipa
|
@rm -rf SideStore.ipa
|
||||||
@rm -rf build/
|
@rm -rf build/
|
||||||
@rm -rf Payload/
|
|
||||||
|
|||||||
35
SideStore/Tests/DataStructureTests.xctestplan
Normal file
35
SideStore/Tests/DataStructureTests.xctestplan
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"configurations" : [
|
||||||
|
{
|
||||||
|
"id" : "93E5E265-DC67-47F3-A214-8082A3421288",
|
||||||
|
"name" : "Test Scheme Action",
|
||||||
|
"options" : {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultOptions" : {
|
||||||
|
"targetForVariableExpansion" : {
|
||||||
|
"containerPath" : "container:AltStore.xcodeproj",
|
||||||
|
"identifier" : "BFD247692284B9A500981D42",
|
||||||
|
"name" : "SideStore"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"testTargets" : [
|
||||||
|
{
|
||||||
|
"skippedTests" : {
|
||||||
|
"suites" : [
|
||||||
|
{
|
||||||
|
"name" : "DataStructuresTests"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:AltStore.xcodeproj",
|
||||||
|
"identifier" : "A81A8CC42D68BA610086C96F",
|
||||||
|
"name" : "DataStructureTests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
33
SideStore/Tests/SideStoreTests.xctestplan
Normal file
33
SideStore/Tests/SideStoreTests.xctestplan
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"configurations" : [
|
||||||
|
{
|
||||||
|
"id" : "93E5E265-DC67-47F3-A214-8082A3421288",
|
||||||
|
"name" : "Test Scheme Action",
|
||||||
|
"options" : {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultOptions" : {
|
||||||
|
"targetForVariableExpansion" : {
|
||||||
|
"containerPath" : "container:AltStore.xcodeproj",
|
||||||
|
"identifier" : "BFD247692284B9A500981D42",
|
||||||
|
"name" : "SideStore"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"testTargets" : [
|
||||||
|
{
|
||||||
|
"skippedTests" : [
|
||||||
|
"UITests\/testLaunchPerformance()",
|
||||||
|
"UITestsLaunchTests",
|
||||||
|
"UITestsLaunchTests\/testLaunch()"
|
||||||
|
],
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:AltStore.xcodeproj",
|
||||||
|
"identifier" : "A8E2DB202D684CBD009E5D31",
|
||||||
|
"name" : "UITests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
459
SideStore/Tests/UITests/UITests.swift
Normal file
459
SideStore/Tests/UITests/UITests.swift
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
//
|
||||||
|
// UITests.swift
|
||||||
|
// UITests
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 21/02/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class UITests: XCTestCase {
|
||||||
|
|
||||||
|
// Handle to the homescreen UI
|
||||||
|
private static let springboard_app = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||||
|
private static let spotlight_app = XCUIApplication(bundleIdentifier: "com.apple.Spotlight")
|
||||||
|
|
||||||
|
private static let searchBar = spotlight_app.textFields["SpotlightSearchField"]
|
||||||
|
|
||||||
|
private static let APP_NAME = "SideStore"
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
// Self.dismissSpotlight()
|
||||||
|
// Self.deleteMyApp()
|
||||||
|
Self.deleteMyApp2()
|
||||||
|
|
||||||
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||||
|
continueAfterFailure = false
|
||||||
|
|
||||||
|
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// @MainActor // Xcode 16.2 bug: UITest Record Button Disabled with @MainActor, see: https://stackoverflow.com/a/79445950/11971304
|
||||||
|
func testBulkAddRecommendedSources() throws {
|
||||||
|
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
let systemAlert = Self.springboard_app.alerts["“\(Self.APP_NAME)” Would Like to Send You Notifications"]
|
||||||
|
|
||||||
|
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||||
|
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
|
||||||
|
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
|
||||||
|
|
||||||
|
// Do the actual validation
|
||||||
|
try performBulkAddingRecommendedSources(for: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBulkAddInputSources() throws {
|
||||||
|
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
let systemAlert = Self.springboard_app.alerts["“\(Self.APP_NAME)” Would Like to Send You Notifications"]
|
||||||
|
|
||||||
|
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||||
|
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
|
||||||
|
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
|
||||||
|
|
||||||
|
// Do the actual validation
|
||||||
|
try performBulkAddingInputSources(for: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRepeatabilityForStagingInputSources() throws {
|
||||||
|
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
let systemAlert = Self.springboard_app.alerts["“\(Self.APP_NAME)” Would Like to Send You Notifications"]
|
||||||
|
|
||||||
|
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||||
|
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
|
||||||
|
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
|
||||||
|
|
||||||
|
// Do the actual validation
|
||||||
|
try performRepeatabilityForStagingInputSources(for: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRepeatabilityForStagingRecommendedSources() throws {
|
||||||
|
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
let systemAlert = Self.springboard_app.alerts["“\(Self.APP_NAME)” Would Like to Send You Notifications"]
|
||||||
|
|
||||||
|
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||||
|
XCTAssertTrue(systemAlert.exists || systemAlert.waitForExistence(timeout: 5), "Notifications alert did not appear")
|
||||||
|
systemAlert.scrollViews.otherElements.buttons["Allow"].tap()
|
||||||
|
|
||||||
|
// Do the actual validation
|
||||||
|
try performRepeatabilityForStagingRecommendedSources(for: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// @MainActor
|
||||||
|
// func testLaunchPerformance() throws {
|
||||||
|
// if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
|
||||||
|
// // This measures how long it takes to launch your application.
|
||||||
|
// measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||||
|
// XCUIApplication().launch()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
private extension UITests {
|
||||||
|
|
||||||
|
class func dismissSpotlight(){
|
||||||
|
// ignore spotlight if it was shown
|
||||||
|
if searchBar.exists {
|
||||||
|
let clearButton = searchBar.buttons["Clear text"]
|
||||||
|
if clearButton.exists{
|
||||||
|
clearButton.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
springboard_app.tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
class func deleteMyApp() {
|
||||||
|
XCUIApplication().terminate()
|
||||||
|
dismissSpringboardAlerts()
|
||||||
|
|
||||||
|
// XCUIDevice.shared.press(.home)
|
||||||
|
springboard_app.swipeDown()
|
||||||
|
|
||||||
|
let searchBar = Self.searchBar
|
||||||
|
_ = searchBar.exists || searchBar.waitForExistence(timeout: 5)
|
||||||
|
searchBar.typeText(APP_NAME)
|
||||||
|
|
||||||
|
// Rest of the deletion flow...
|
||||||
|
let appIcon = spotlight_app.icons[APP_NAME]
|
||||||
|
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||||
|
if appIcon.exists || appIcon.waitForExistence(timeout: 5) {
|
||||||
|
appIcon.press(forDuration: 1)
|
||||||
|
|
||||||
|
let deleteAppButton = spotlight_app.buttons["Delete App"]
|
||||||
|
_ = deleteAppButton.exists || deleteAppButton.waitForExistence(timeout: 5)
|
||||||
|
deleteAppButton.tap()
|
||||||
|
|
||||||
|
let confirmDeleteButton = springboard_app.alerts["Delete “\(APP_NAME)”?"]
|
||||||
|
_ = confirmDeleteButton.exists || confirmDeleteButton.waitForExistence(timeout: 5)
|
||||||
|
confirmDeleteButton.scrollViews.otherElements.buttons["Delete"].tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
let clearButton = searchBar.buttons["Clear text"]
|
||||||
|
_ = clearButton.exists || clearButton.waitForExistence(timeout: 5)
|
||||||
|
clearButton.tap()
|
||||||
|
|
||||||
|
springboard_app.tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
class func deleteMyApp2() {
|
||||||
|
XCUIApplication().terminate()
|
||||||
|
dismissSpringboardAlerts()
|
||||||
|
|
||||||
|
// Rest of the deletion flow...
|
||||||
|
let appIcon = springboard_app.icons[APP_NAME]
|
||||||
|
// if it exists keep going immediately else wait for upto 5 sec with polling every 1 sec for existence
|
||||||
|
if appIcon.exists || appIcon.waitForExistence(timeout: 5) {
|
||||||
|
appIcon.press(forDuration: 1)
|
||||||
|
|
||||||
|
do {
|
||||||
|
let button = springboard_app.buttons["Remove App"]
|
||||||
|
_ = button.exists || button.waitForExistence(timeout: 5)
|
||||||
|
button.tap()
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let button = springboard_app.buttons["Delete App"]
|
||||||
|
_ = button.waitForExistence(timeout: 0.3)
|
||||||
|
button.tap()
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let button = springboard_app.buttons["Delete"]
|
||||||
|
_ = button.waitForExistence(timeout: 0.3)
|
||||||
|
button.tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Press home once to make the icons stop wiggling
|
||||||
|
// XCUIDevice.shared.press(.home)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class func dismissSpringboardAlerts() {
|
||||||
|
for alert in springboard_app.alerts.allElementsBoundByIndex {
|
||||||
|
if alert.exists {
|
||||||
|
// If there's a "Cancel" button, tap it; otherwise, tap the first button.
|
||||||
|
if alert.buttons["Cancel"].exists {
|
||||||
|
alert.buttons["Cancel"].tap()
|
||||||
|
} else if let firstButton = alert.buttons.allElementsBoundByIndex.first {
|
||||||
|
firstButton.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct SeededGenerator: RandomNumberGenerator {
|
||||||
|
var seed: UInt64
|
||||||
|
|
||||||
|
mutating func next() -> UInt64 {
|
||||||
|
// A basic LCG (not cryptographically secure, but fine for testing)
|
||||||
|
seed = 6364136223846793005 &* seed &+ 1
|
||||||
|
return seed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Test guts (definition)
|
||||||
|
private extension UITests {
|
||||||
|
|
||||||
|
|
||||||
|
private func performBulkAdd(
|
||||||
|
app: XCUIApplication,
|
||||||
|
sourceMappings: [(identifier: String, alertTitle: String, requiresSwipe: Bool)],
|
||||||
|
cellsQuery: XCUIElementQuery
|
||||||
|
) throws {
|
||||||
|
|
||||||
|
// Tap on each sourceMappings source's "add" button.
|
||||||
|
try tapAddForThesePickedSources(app: app, sourceMappings: sourceMappings, cellsQuery: cellsQuery)
|
||||||
|
|
||||||
|
// Commit the changes by tapping "Done".
|
||||||
|
app.navigationBars["Add Source"].buttons["Done"].tap()
|
||||||
|
|
||||||
|
// Accept each source addition via alert.
|
||||||
|
for source in sourceMappings {
|
||||||
|
let alertIdentifier = "Would you like to add the source “\(source.alertTitle)”?"
|
||||||
|
let addSourceButton = app.alerts[alertIdentifier]
|
||||||
|
.scrollViews.otherElements.buttons["Add Source"]
|
||||||
|
_ = addSourceButton.exists || addSourceButton.waitForExistence(timeout: 0.3)
|
||||||
|
addSourceButton.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private func performBulkAddingInputSources(for app: XCUIApplication) throws {
|
||||||
|
|
||||||
|
// set content into clipboard (for bulk add (paste))
|
||||||
|
// NOTE: THIS IS AN ORDERED SEQUENCE AND MUST MATCH THE ORDER in textInputSources BELOW (Remember to take this into account when adding more entries)
|
||||||
|
UIPasteboard.general.string = """
|
||||||
|
https://alts.lao.sb
|
||||||
|
https://taurine.app/altstore/taurinestore.json
|
||||||
|
https://randomblock1.com/altstore/apps.json
|
||||||
|
https://burritosoftware.github.io/altstore/channels/burritosource.json
|
||||||
|
https://bit.ly/40Isul6
|
||||||
|
https://bit.ly/wuxuslibraryplus
|
||||||
|
https://bit.ly/Quantumsource-plus
|
||||||
|
https://bit.ly/Altstore-complete
|
||||||
|
https://bit.ly/Quantumsource
|
||||||
|
""".trimmedIndentation
|
||||||
|
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.tabBars["Tab Bar"].buttons["Sources"].tap()
|
||||||
|
app.navigationBars["Sources"].buttons["Add"].tap()
|
||||||
|
|
||||||
|
let collectionViewsQuery = app.collectionViews
|
||||||
|
let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"]
|
||||||
|
_ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5)
|
||||||
|
appsSidestoreIoTextField.tap()
|
||||||
|
appsSidestoreIoTextField.tap()
|
||||||
|
collectionViewsQuery.staticTexts["Paste"].tap()
|
||||||
|
|
||||||
|
// if app.keyboards.buttons["Return"].exists {
|
||||||
|
// app.keyboards.buttons["Return"].tap()
|
||||||
|
// } else if app.keyboards.buttons["Done"].exists {
|
||||||
|
// app.keyboards.buttons["Done"].tap()
|
||||||
|
// } else {
|
||||||
|
// // if still exists try tapping outside of text field focus
|
||||||
|
// app.tap()
|
||||||
|
// }
|
||||||
|
|
||||||
|
if app.keyboards.count > 0 {
|
||||||
|
appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed
|
||||||
|
}
|
||||||
|
|
||||||
|
let cellsQuery = collectionViewsQuery.cells
|
||||||
|
|
||||||
|
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
|
||||||
|
let textInputSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
|
||||||
|
("Laoalts\nalts.lao.sb", "Laoalts", false),
|
||||||
|
("Taurine\ntaurine.app/altstore/taurinestore.json", "Taurine", false),
|
||||||
|
("RandomSource\nrandomblock1.com/altstore/apps.json", "RandomSource", false),
|
||||||
|
("Burrito's AltStore\nburritosoftware.github.io/altstore/channels/burritosource.json", "Burrito's AltStore", true),
|
||||||
|
("Qn_'s AltStore Repo\nbit.ly/40Isul6", "Qn_'s AltStore Repo", false),
|
||||||
|
("WuXu's Library++\nThe Most Up-To-Date IPA Library on AltStore.", "WuXu's Library++", false),
|
||||||
|
("Quantum Source++\nContains tweaked apps, free streaming, cracked apps, and more.", "Quantum Source++", false),
|
||||||
|
("AltStore Complete\nContains tweaked apps, free streaming, cracked apps, and more.", "AltStore Complete", false),
|
||||||
|
("Quantum Source\nContains all of your favorite emulators, games, jailbreaks, utilities, and more.", "Quantum Source", false),
|
||||||
|
]
|
||||||
|
|
||||||
|
try performBulkAdd(app: app, sourceMappings: textInputSources, cellsQuery: cellsQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func performRepeatabilityForStagingInputSources(for app: XCUIApplication) throws {
|
||||||
|
|
||||||
|
// set content into clipboard (for bulk add (paste))
|
||||||
|
// NOTE: THIS IS AN ORDERED SEQUENCE AND MUST MATCH THE ORDER in textInputSources BELOW (Remember to take this into account when adding more entries)
|
||||||
|
UIPasteboard.general.string = """
|
||||||
|
https://alts.lao.sb
|
||||||
|
https://taurine.app/altstore/taurinestore.json
|
||||||
|
https://randomblock1.com/altstore/apps.json
|
||||||
|
https://burritosoftware.github.io/altstore/channels/burritosource.json
|
||||||
|
https://bit.ly/40Isul6
|
||||||
|
""".trimmedIndentation
|
||||||
|
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.tabBars["Tab Bar"].buttons["Sources"].tap()
|
||||||
|
app.navigationBars["Sources"].buttons["Add"].tap()
|
||||||
|
|
||||||
|
let collectionViewsQuery = app.collectionViews
|
||||||
|
let appsSidestoreIoTextField = collectionViewsQuery.textFields["apps.sidestore.io"]
|
||||||
|
_ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5)
|
||||||
|
appsSidestoreIoTextField.tap()
|
||||||
|
appsSidestoreIoTextField.tap()
|
||||||
|
_ = appsSidestoreIoTextField.exists || appsSidestoreIoTextField.waitForExistence(timeout: 5)
|
||||||
|
collectionViewsQuery.staticTexts["Paste"].tap()
|
||||||
|
|
||||||
|
if app.keyboards.count > 0 {
|
||||||
|
appsSidestoreIoTextField.typeText("\n") // Fallback to newline so that soft kb is dismissed
|
||||||
|
}
|
||||||
|
|
||||||
|
let cellsQuery = collectionViewsQuery.cells
|
||||||
|
|
||||||
|
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
|
||||||
|
let textInputSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
|
||||||
|
("Laoalts\nalts.lao.sb", "Laoalts", false),
|
||||||
|
("Taurine\ntaurine.app/altstore/taurinestore.json", "Taurine", false),
|
||||||
|
("RandomSource\nrandomblock1.com/altstore/apps.json", "RandomSource", false),
|
||||||
|
("Burrito's AltStore\nburritosoftware.github.io/altstore/channels/burritosource.json", "Burrito's AltStore", false),
|
||||||
|
("Qn_'s AltStore Repo\nbit.ly/40Isul6", "Qn_'s AltStore Repo", false),
|
||||||
|
]
|
||||||
|
|
||||||
|
let repeatCount = 3 // number of times to run the entire sequence
|
||||||
|
let timeSeed = UInt64(Date().timeIntervalSince1970) // time is unique (upto microseconds) - uncomment this to use non-deterministic seed based RNG (random number generator)
|
||||||
|
|
||||||
|
try repeatabilityTest(app: app, sourceMappings: textInputSources, cellsQuery: cellsQuery, repeatCount: repeatCount, seed: timeSeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func repeatabilityTest(
|
||||||
|
app: XCUIApplication,
|
||||||
|
sourceMappings: [(identifier: String, alertTitle: String, requiresSwipe: Bool)],
|
||||||
|
cellsQuery: XCUIElementQuery,
|
||||||
|
repeatCount: Int = 1, // number of times to run the entire sequence
|
||||||
|
seed: UInt64 = 42 // default = fixed seed for deterministic start of this generator
|
||||||
|
) throws {
|
||||||
|
let seededGenerator = SeededGenerator(seed: seed)
|
||||||
|
|
||||||
|
for _ in 0..<repeatCount {
|
||||||
|
// The same fixed seeded generator will yield the same permutation if not advanced, so you might want to reinitialize or use a fresh copy for each iteration:
|
||||||
|
var seededGenerator = seededGenerator // uncomment this for repeats to use same(shuffled once due to inital seed) order for all repeats
|
||||||
|
|
||||||
|
// let sourceMappings = sourceMappings.shuffled() // use this for non-deterministic shuffling
|
||||||
|
let sourceMappings = sourceMappings.shuffled(using: &seededGenerator) // use this for deterministic shuffling based on seed
|
||||||
|
try tapAddForThesePickedSources(app: app, sourceMappings: sourceMappings, cellsQuery: cellsQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tapAddForThesePickedSources(
|
||||||
|
app: XCUIApplication,
|
||||||
|
sourceMappings: [(identifier: String, alertTitle: String, requiresSwipe: Bool)],
|
||||||
|
cellsQuery: XCUIElementQuery
|
||||||
|
) throws {
|
||||||
|
|
||||||
|
// Tap on each sourceMappings source's "add" button.
|
||||||
|
for source in sourceMappings {
|
||||||
|
let sourceButton = cellsQuery.otherElements
|
||||||
|
.containing(.button, identifier: source.identifier)
|
||||||
|
.children(matching: .button)[source.identifier]
|
||||||
|
XCTAssert(sourceButton.exists || sourceButton.waitForExistence(timeout: 10), "Source preview for id: '\(source.alertTitle)' not found in the view")
|
||||||
|
|
||||||
|
// let addButton = sourceButton.children(matching: .button).firstMatch
|
||||||
|
let addButton = sourceButton.children(matching: .button)["add"]
|
||||||
|
XCTAssert(addButton.exists || addButton.waitForExistence(timeout: 0.3), " `+` button for id: '\(source.alertTitle)' not found in the preview container")
|
||||||
|
addButton.tap()
|
||||||
|
|
||||||
|
if source.requiresSwipe {
|
||||||
|
sourceButton.swipeUp(velocity: .slow) // Swipe up if needed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performBulkAddingRecommendedSources(for app: XCUIApplication) throws {
|
||||||
|
// Navigate to the Sources screen and open the Add Source view.
|
||||||
|
app.tabBars["Tab Bar"].buttons["Sources"].tap()
|
||||||
|
app.navigationBars["Sources"].buttons["Add"].tap()
|
||||||
|
|
||||||
|
let cellsQuery = app.collectionViews.cells
|
||||||
|
|
||||||
|
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
|
||||||
|
let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
|
||||||
|
("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false),
|
||||||
|
("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false),
|
||||||
|
("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false),
|
||||||
|
("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", true),
|
||||||
|
("UTM Repository\nVirtual machines for iOS", "UTM Repository", false),
|
||||||
|
("Flyinghead\nflyinghead.github.io/flycast-builds/altstore.json", "Flyinghead", false),
|
||||||
|
// ("PojavLauncher Repository\nalt.crystall1ne.dev", "PojavLauncher Repository", false), // not a stable source, sometimes becomes unreachable, so disabled
|
||||||
|
("PokeMMO\npokemmo.eu/altstore/", "PokeMMO", true),
|
||||||
|
("Odyssey\ntheodyssey.dev/altstore/odysseysource.json", "Odyssey", false),
|
||||||
|
("Yattee\nrepos.yattee.stream/alt/apps.json", "Yattee", false),
|
||||||
|
("ThatStella7922 Source\nThe home for all apps ThatStella7922", "ThatStella7922 Source", false)
|
||||||
|
]
|
||||||
|
|
||||||
|
try performBulkAdd(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performRepeatabilityForStagingRecommendedSources(for app: XCUIApplication) throws {
|
||||||
|
// Navigate to the Sources screen and open the Add Source view.
|
||||||
|
app.tabBars["Tab Bar"].buttons["Sources"].tap()
|
||||||
|
app.navigationBars["Sources"].buttons["Add"].tap()
|
||||||
|
|
||||||
|
let cellsQuery = app.collectionViews.cells
|
||||||
|
|
||||||
|
// Data model for recommended sources. NOTE: This list order is required to be the same as that of "Add Source" Screen
|
||||||
|
let recommendedSources: [(identifier: String, alertTitle: String, requiresSwipe: Bool)] = [
|
||||||
|
("SideStore Team Picks\ncommunity-apps.sidestore.io/sidecommunity.json", "SideStore Team Picks", false),
|
||||||
|
("Provenance EMU\nprovenance-emu.com/apps.json", "Provenance EMU", false),
|
||||||
|
("Countdown Respository\nneoarz.github.io/Countdown-App/Countdown.json", "Countdown Respository", false),
|
||||||
|
("OatmealDome's AltStore Source\naltstore.oatmealdome.me", "OatmealDome's AltStore Source", false),
|
||||||
|
("UTM Repository\nVirtual machines for iOS", "UTM Repository", false),
|
||||||
|
]
|
||||||
|
|
||||||
|
let repeatCount = 3 // number of times to run the entire sequence
|
||||||
|
let timeSeed = UInt64(Date().timeIntervalSince1970) // time is unique (upto microseconds) - uncomment this to use non-deterministic seed based RNG (random number generator)
|
||||||
|
|
||||||
|
try repeatabilityTest(app: app, sourceMappings: recommendedSources, cellsQuery: cellsQuery, repeatCount: repeatCount, seed: timeSeed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
var trimmedIndentation: String {
|
||||||
|
let lines = self.split(separator: "\n", omittingEmptySubsequences: false)
|
||||||
|
let minIndent = lines
|
||||||
|
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } // Ignore empty lines
|
||||||
|
.map { $0.prefix { $0.isWhitespace }.count }
|
||||||
|
.min() ?? 0
|
||||||
|
|
||||||
|
return lines.map { line in
|
||||||
|
String(line.dropFirst(minIndent))
|
||||||
|
}.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
34
SideStore/Tests/UITests/UITestsLaunchTests.swift
Normal file
34
SideStore/Tests/UITests/UITestsLaunchTests.swift
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//
|
||||||
|
// UITestsLaunchTests.swift
|
||||||
|
// UITests
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 21/02/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class UITestsLaunchTests: XCTestCase {
|
||||||
|
|
||||||
|
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// @MainActor
|
||||||
|
// func testLaunch() throws {
|
||||||
|
// let app = XCUIApplication()
|
||||||
|
// app.launch()
|
||||||
|
//
|
||||||
|
// // Insert steps here to perform after app launch but before taking a screenshot,
|
||||||
|
// // such as logging into a test account or navigating somewhere in the app
|
||||||
|
//
|
||||||
|
// let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||||
|
// attachment.name = "Launch Screen"
|
||||||
|
// attachment.lifetime = .keepAlways
|
||||||
|
// add(attachment)
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// DataStructuresTests.swift
|
||||||
|
// DataStructuresTests
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 21/02/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
struct DataStructuresTests {
|
||||||
|
|
||||||
|
@Test func example() async throws {
|
||||||
|
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
//
|
||||||
|
// LinkedHashMapTests.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 21/02/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
// A helper class that signals when it is deallocated.
|
||||||
|
class LeakTester {
|
||||||
|
let id: Int
|
||||||
|
var onDeinit: (() -> Void)?
|
||||||
|
init(id: Int, onDeinit: (() -> Void)? = nil) {
|
||||||
|
self.id = id
|
||||||
|
self.onDeinit = onDeinit
|
||||||
|
}
|
||||||
|
deinit {
|
||||||
|
onDeinit?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LinkedHashMapTests: XCTestCase {
|
||||||
|
|
||||||
|
// Test that insertion preserves order and that iteration returns items in insertion order.
|
||||||
|
func testInsertionAndOrder() {
|
||||||
|
let map = LinkedHashMap<String, Int>()
|
||||||
|
map.put(key: "one", value: 1)
|
||||||
|
map.put(key: "two", value: 2)
|
||||||
|
map.put(key: "three", value: 3)
|
||||||
|
|
||||||
|
XCTAssertEqual(map.count, 3)
|
||||||
|
XCTAssertEqual(map.keys, ["one", "two", "three"], "Insertion order should be preserved")
|
||||||
|
|
||||||
|
var iteratedKeys = [String]()
|
||||||
|
for (key, _) in map {
|
||||||
|
iteratedKeys.append(key)
|
||||||
|
}
|
||||||
|
XCTAssertEqual(iteratedKeys, ["one", "two", "three"], "Iterator should follow insertion order")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that updating a key does not change its order.
|
||||||
|
func testUpdateDoesNotChangeOrder() {
|
||||||
|
let map = LinkedHashMap<String, Int>()
|
||||||
|
map.put(key: "a", value: 1)
|
||||||
|
map.put(key: "b", value: 2)
|
||||||
|
map.put(key: "c", value: 3)
|
||||||
|
// Update key "b"
|
||||||
|
map.put(key: "b", value: 20)
|
||||||
|
XCTAssertEqual(map.get(key: "b"), 20)
|
||||||
|
|
||||||
|
XCTAssertEqual(map.keys, ["a", "b", "c"], "Order should not change on update")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test removal functionality and behavior when removing a non-existent key.
|
||||||
|
func testRemoval() {
|
||||||
|
let map = LinkedHashMap<Int, String>()
|
||||||
|
map.put(key: 1, value: "one")
|
||||||
|
map.put(key: 2, value: "two")
|
||||||
|
map.put(key: 3, value: "three")
|
||||||
|
|
||||||
|
let removed = map.remove(key: 2)
|
||||||
|
XCTAssertEqual(removed, "two")
|
||||||
|
XCTAssertEqual(map.count, 2)
|
||||||
|
XCTAssertEqual(map.keys, [1, 3])
|
||||||
|
|
||||||
|
// Removing a key that doesn't exist should return nil.
|
||||||
|
let removedNil = map.remove(key: 4)
|
||||||
|
XCTAssertNil(removedNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test clearing the map.
|
||||||
|
func testClear() {
|
||||||
|
let map = LinkedHashMap<String, Int>()
|
||||||
|
map.put(key: "x", value: 100)
|
||||||
|
map.put(key: "y", value: 200)
|
||||||
|
XCTAssertEqual(map.count, 2)
|
||||||
|
|
||||||
|
map.clear()
|
||||||
|
XCTAssertEqual(map.count, 0)
|
||||||
|
XCTAssertTrue(map.isEmpty)
|
||||||
|
XCTAssertEqual(map.keys, [])
|
||||||
|
XCTAssertEqual(map.values, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test subscript access for getting, updating, and removal.
|
||||||
|
func testSubscript() {
|
||||||
|
let map = LinkedHashMap<String, Int>()
|
||||||
|
map["alpha"] = 10
|
||||||
|
XCTAssertEqual(map["alpha"], 10)
|
||||||
|
|
||||||
|
map["alpha"] = 20
|
||||||
|
XCTAssertEqual(map["alpha"], 20)
|
||||||
|
|
||||||
|
// Setting a key to nil should remove the mapping.
|
||||||
|
map["alpha"] = nil
|
||||||
|
XCTAssertNil(map["alpha"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test containsKey and containsValue.
|
||||||
|
func testContains() {
|
||||||
|
let map = LinkedHashMap<String, Int>()
|
||||||
|
map.put(key: "key1", value: 1)
|
||||||
|
map.put(key: "key2", value: 2)
|
||||||
|
|
||||||
|
XCTAssertTrue(map.containsKey("key1"))
|
||||||
|
XCTAssertFalse(map.containsKey("key3"))
|
||||||
|
|
||||||
|
XCTAssertTrue(map.containsValue(1))
|
||||||
|
XCTAssertFalse(map.containsValue(99))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test initialization from a dictionary.
|
||||||
|
func testInitializationFromDictionary() {
|
||||||
|
// Note: Swift dictionaries preserve insertion order for literals.
|
||||||
|
let dictionary: [String: Int] = ["a": 1, "b": 2, "c": 3]
|
||||||
|
let map = LinkedHashMap(dictionary)
|
||||||
|
XCTAssertEqual(map.count, 3)
|
||||||
|
// Order may differ since Dictionary order is not strictly defined – here we verify membership.
|
||||||
|
XCTAssertEqual(Set(map.keys), Set(["a", "b", "c"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revised test that iterates over the map and compares key-value pairs element by element.
|
||||||
|
func testIteration() {
|
||||||
|
let map = LinkedHashMap<Int, String>()
|
||||||
|
let pairs = [(1, "one"), (2, "two"), (3, "three")]
|
||||||
|
for (key, value) in pairs {
|
||||||
|
map.put(key: key, value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var iteratedPairs = [(Int, String)]()
|
||||||
|
for (key, value) in map {
|
||||||
|
iteratedPairs.append((key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(iteratedPairs.count, pairs.count, "Iterated count should match inserted count")
|
||||||
|
for (iter, expected) in zip(iteratedPairs, pairs) {
|
||||||
|
XCTAssertEqual(iter.0, expected.0, "Keys should match in order")
|
||||||
|
XCTAssertEqual(iter.1, expected.1, "Values should match in order")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that the values stored in the map are deallocated when the map is deallocated.
|
||||||
|
func testMemoryLeak() {
|
||||||
|
weak var weakMap: LinkedHashMap<Int, LeakTester>?
|
||||||
|
var deinitCalled = false
|
||||||
|
|
||||||
|
do {
|
||||||
|
let map = LinkedHashMap<Int, LeakTester>()
|
||||||
|
let tester = LeakTester(id: 1) { deinitCalled = true }
|
||||||
|
map.put(key: 1, value: tester)
|
||||||
|
weakMap = map
|
||||||
|
XCTAssertNotNil(map.get(key: 1))
|
||||||
|
}
|
||||||
|
// At this point the map (and its stored objects) should be deallocated.
|
||||||
|
XCTAssertNil(weakMap, "LinkedHashMap should be deallocated when out of scope")
|
||||||
|
XCTAssertTrue(deinitCalled, "LeakTester should be deallocated, indicating no memory leak")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that removal from the map correctly frees stored objects.
|
||||||
|
func testMemoryLeakOnRemoval() {
|
||||||
|
var deinitCalledForTester1 = false
|
||||||
|
var deinitCalledForTester2 = false
|
||||||
|
|
||||||
|
let map = LinkedHashMap<Int, LeakTester>()
|
||||||
|
autoreleasepool {
|
||||||
|
let tester1 = LeakTester(id: 1) { deinitCalledForTester1 = true }
|
||||||
|
let tester2 = LeakTester(id: 2) { deinitCalledForTester2 = true }
|
||||||
|
map.put(key: 1, value: tester1)
|
||||||
|
map.put(key: 2, value: tester2)
|
||||||
|
|
||||||
|
XCTAssertNotNil(map.get(key: 1))
|
||||||
|
XCTAssertNotNil(map.get(key: 2))
|
||||||
|
|
||||||
|
// Remove tester1; it should be deallocated if no retain cycle exists.
|
||||||
|
_ = map.remove(key: 1)
|
||||||
|
}
|
||||||
|
// tester1 should be deallocated immediately after removal.
|
||||||
|
XCTAssertTrue(deinitCalledForTester1, "Tester1 should be deallocated after removal")
|
||||||
|
// tester2 is still in the map.
|
||||||
|
XCTAssertNotNil(map.get(key: 2))
|
||||||
|
|
||||||
|
// Clear the map and tester2 should be deallocated.
|
||||||
|
map.clear()
|
||||||
|
XCTAssertTrue(deinitCalledForTester2, "Tester2 should be deallocated after clearing the map")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDefaultSubscriptExtension() {
|
||||||
|
// Create an instance of LinkedHashMap with String keys and Bool values.
|
||||||
|
let map = LinkedHashMap<String, Bool>()
|
||||||
|
|
||||||
|
// Verify that accessing a non-existent key returns the default value (false).
|
||||||
|
XCTAssertEqual(map["testKey", default: false], false)
|
||||||
|
|
||||||
|
// Use the default subscript setter to assign 'true' for the key.
|
||||||
|
map["testKey", default: false] = true
|
||||||
|
XCTAssertEqual(map["testKey", default: false], true)
|
||||||
|
|
||||||
|
// Simulate in-place toggle: read the value, toggle it, then write it back.
|
||||||
|
var current = map["testKey", default: false]
|
||||||
|
current.toggle() // now false
|
||||||
|
map["testKey", default: false] = current
|
||||||
|
XCTAssertEqual(map["testKey", default: false], false)
|
||||||
|
}
|
||||||
|
}
|
||||||
154
SideStore/Tests/UnitTests/datastructures/TreeMapTests.swift
Normal file
154
SideStore/Tests/UnitTests/datastructures/TreeMapTests.swift
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
//
|
||||||
|
// TreeMapTests.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 21/02/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class TreeMapTests: XCTestCase {
|
||||||
|
|
||||||
|
func testInsertionAndRetrieval() {
|
||||||
|
let map = TreeMap<Int, String>()
|
||||||
|
XCTAssertNil(map[10])
|
||||||
|
map[10] = "ten"
|
||||||
|
XCTAssertEqual(map[10], "ten")
|
||||||
|
|
||||||
|
map[5] = "five"
|
||||||
|
map[15] = "fifteen"
|
||||||
|
XCTAssertEqual(map.count, 3)
|
||||||
|
XCTAssertEqual(map[5], "five")
|
||||||
|
XCTAssertEqual(map[15], "fifteen")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdateValue() {
|
||||||
|
let map = TreeMap<Int, String>()
|
||||||
|
map[10] = "ten"
|
||||||
|
let oldValue = map.insert(key: 10, value: "TEN")
|
||||||
|
XCTAssertEqual(oldValue, "ten")
|
||||||
|
XCTAssertEqual(map[10], "TEN")
|
||||||
|
XCTAssertEqual(map.count, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeletion() {
|
||||||
|
let map = TreeMap<Int, String>()
|
||||||
|
// Setup: Inserting three nodes.
|
||||||
|
map[20] = "twenty"
|
||||||
|
map[10] = "ten"
|
||||||
|
map[30] = "thirty"
|
||||||
|
|
||||||
|
// Remove a leaf node.
|
||||||
|
let removedLeaf = map.remove(key: 10)
|
||||||
|
XCTAssertEqual(removedLeaf, "ten")
|
||||||
|
XCTAssertNil(map[10])
|
||||||
|
XCTAssertEqual(map.count, 2)
|
||||||
|
|
||||||
|
// Setup additional nodes to create a one-child scenario.
|
||||||
|
map[25] = "twenty-five"
|
||||||
|
map[27] = "twenty-seven" // Right child for 25.
|
||||||
|
// Remove a node with one child.
|
||||||
|
let removedOneChild = map.remove(key: 25)
|
||||||
|
XCTAssertEqual(removedOneChild, "twenty-five")
|
||||||
|
XCTAssertNil(map[25])
|
||||||
|
XCTAssertEqual(map.count, 3)
|
||||||
|
|
||||||
|
// Setup for a node with two children.
|
||||||
|
map[40] = "forty"
|
||||||
|
map[35] = "thirty-five"
|
||||||
|
map[45] = "forty-five"
|
||||||
|
// Remove a node with two children.
|
||||||
|
let removedTwoChildren = map.remove(key: 40)
|
||||||
|
XCTAssertEqual(removedTwoChildren, "forty")
|
||||||
|
XCTAssertNil(map[40])
|
||||||
|
XCTAssertEqual(map.count, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeletionOfRoot() {
|
||||||
|
let map = TreeMap<Int, String>()
|
||||||
|
map[50] = "fifty"
|
||||||
|
map[30] = "thirty"
|
||||||
|
map[70] = "seventy"
|
||||||
|
|
||||||
|
// Delete the root node.
|
||||||
|
let removedRoot = map.remove(key: 50)
|
||||||
|
XCTAssertEqual(removedRoot, "fifty")
|
||||||
|
XCTAssertNil(map[50])
|
||||||
|
// After deletion, remaining keys should be in sorted order.
|
||||||
|
XCTAssertEqual(map.keys, [30, 70])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSortedIteration() {
|
||||||
|
let map = TreeMap<Int, String>()
|
||||||
|
let keys = [20, 10, 30, 5, 15, 25, 35]
|
||||||
|
for key in keys {
|
||||||
|
map[key] = "\(key)"
|
||||||
|
}
|
||||||
|
let sortedKeys = map.keys
|
||||||
|
XCTAssertEqual(sortedKeys, keys.sorted())
|
||||||
|
|
||||||
|
// Verify in-order traversal.
|
||||||
|
var previous: Int? = nil
|
||||||
|
for (key, value) in map {
|
||||||
|
if let prev = previous {
|
||||||
|
XCTAssertLessThanOrEqual(prev, key)
|
||||||
|
}
|
||||||
|
previous = key
|
||||||
|
XCTAssertEqual(value, "\(key)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemoveAll() {
|
||||||
|
let map = TreeMap<Int, String>()
|
||||||
|
for i in 0..<100 {
|
||||||
|
map[i] = "\(i)"
|
||||||
|
}
|
||||||
|
XCTAssertEqual(map.count, 100)
|
||||||
|
map.removeAll()
|
||||||
|
XCTAssertEqual(map.count, 0)
|
||||||
|
XCTAssertTrue(map.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBalancing() {
|
||||||
|
let map = TreeMap<Int, Int>()
|
||||||
|
// Insert elements in ascending order to challenge the balancing.
|
||||||
|
for i in 1...1000 {
|
||||||
|
map[i] = i
|
||||||
|
}
|
||||||
|
// Verify in-order traversal produces sorted order.
|
||||||
|
var expected = 1
|
||||||
|
for (key, value) in map {
|
||||||
|
XCTAssertEqual(key, expected)
|
||||||
|
XCTAssertEqual(value, expected)
|
||||||
|
expected += 1
|
||||||
|
}
|
||||||
|
XCTAssertEqual(expected - 1, 1000)
|
||||||
|
|
||||||
|
// Remove odd keys to force rebalancing.
|
||||||
|
for i in stride(from: 1, through: 1000, by: 2) {
|
||||||
|
_ = map.remove(key: i)
|
||||||
|
}
|
||||||
|
let expectedEvenKeys = (1...1000).filter { $0 % 2 == 0 }
|
||||||
|
XCTAssertEqual(map.keys, expectedEvenKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNonExistentDeletion() {
|
||||||
|
let map = TreeMap<Int, String>()
|
||||||
|
map[10] = "ten"
|
||||||
|
let removed = map.remove(key: 20)
|
||||||
|
XCTAssertNil(removed)
|
||||||
|
XCTAssertEqual(map.count, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDuplicateInsertion() {
|
||||||
|
let map = TreeMap<String, String>()
|
||||||
|
map["a"] = "first"
|
||||||
|
XCTAssertEqual(map["a"], "first")
|
||||||
|
let oldValue = map.insert(key: "a", value: "second")
|
||||||
|
XCTAssertEqual(oldValue, "first")
|
||||||
|
XCTAssertEqual(map["a"], "second")
|
||||||
|
XCTAssertEqual(map.count, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
226
SideStore/Utils/datastructures/LinkedHashMap.swift
Normal file
226
SideStore/Utils/datastructures/LinkedHashMap.swift
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
//
|
||||||
|
// LinkedHashMap.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 21/02/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/// A generic LinkedHashMap implementation in Swift.
|
||||||
|
/// It provides constant-time lookup along with predictable (insertion) ordering.
|
||||||
|
public final class LinkedHashMap<Key: Hashable, Value>: Sequence {
|
||||||
|
|
||||||
|
/// Internal doubly-linked list node
|
||||||
|
fileprivate final class Node {
|
||||||
|
let key: Key
|
||||||
|
var value: Value
|
||||||
|
var next: Node?
|
||||||
|
weak var prev: Node? // weak to avoid strong reference cycle
|
||||||
|
|
||||||
|
init(key: Key, value: Value) {
|
||||||
|
self.key = key
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Storage
|
||||||
|
|
||||||
|
/// Dictionary for fast lookup from key to node.
|
||||||
|
private var dict: [Key: Node] = [:]
|
||||||
|
|
||||||
|
/// Head and tail of the doubly-linked list to maintain order.
|
||||||
|
private var head: Node?
|
||||||
|
private var tail: Node?
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
/// Creates an empty LinkedHashMap.
|
||||||
|
public init() { }
|
||||||
|
|
||||||
|
/// Creates a LinkedHashMap from a standard dictionary.
|
||||||
|
public init(_ dictionary: [Key: Value]) {
|
||||||
|
for (key, value) in dictionary {
|
||||||
|
_ = self.put(key: key, value: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public API
|
||||||
|
|
||||||
|
/// The number of key-value pairs in the map.
|
||||||
|
public var count: Int {
|
||||||
|
return dict.count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Boolean value indicating whether the map is empty.
|
||||||
|
public var isEmpty: Bool {
|
||||||
|
return dict.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the value for the given key, or `nil` if the key is not found.
|
||||||
|
public func get(key: Key) -> Value? {
|
||||||
|
return dict[key]?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts or updates the value for the given key.
|
||||||
|
/// - Returns: The previous value for the key if it existed; otherwise, `nil`.
|
||||||
|
@discardableResult
|
||||||
|
public func put(key: Key, value: Value) -> Value? {
|
||||||
|
if let node = dict[key] {
|
||||||
|
let oldValue = node.value
|
||||||
|
node.value = value
|
||||||
|
return oldValue
|
||||||
|
} else {
|
||||||
|
let newNode = Node(key: key, value: value)
|
||||||
|
dict[key] = newNode
|
||||||
|
appendNode(newNode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the value for the given key.
|
||||||
|
/// - Returns: The removed value if it existed; otherwise, `nil`.
|
||||||
|
@discardableResult
|
||||||
|
public func remove(key: Key) -> Value? {
|
||||||
|
guard let node = dict.removeValue(forKey: key) else { return nil }
|
||||||
|
removeNode(node)
|
||||||
|
return node.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all key-value pairs from the map.
|
||||||
|
public func clear() {
|
||||||
|
dict.removeAll()
|
||||||
|
head = nil
|
||||||
|
tail = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines whether the map contains the given key.
|
||||||
|
public func containsKey(_ key: Key) -> Bool {
|
||||||
|
return dict[key] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines whether the map contains the given value.
|
||||||
|
/// Note: This method requires that Value conforms to Equatable.
|
||||||
|
public func containsValue(_ value: Value) -> Bool where Value: Equatable {
|
||||||
|
var current = head
|
||||||
|
while let node = current {
|
||||||
|
if node.value == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
current = node.next
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all keys in insertion order.
|
||||||
|
public var keys: [Key] {
|
||||||
|
var result = [Key]()
|
||||||
|
var current = head
|
||||||
|
while let node = current {
|
||||||
|
result.append(node.key)
|
||||||
|
current = node.next
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all values in insertion order.
|
||||||
|
public var values: [Value] {
|
||||||
|
var result = [Value]()
|
||||||
|
var current = head
|
||||||
|
while let node = current {
|
||||||
|
result.append(node.value)
|
||||||
|
current = node.next
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscript for getting and setting values.
|
||||||
|
public subscript(key: Key) -> Value? {
|
||||||
|
get {
|
||||||
|
return get(key: key)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if let newValue = newValue {
|
||||||
|
_ = put(key: key, value: newValue)
|
||||||
|
} else {
|
||||||
|
_ = remove(key: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Sequence Conformance
|
||||||
|
|
||||||
|
/// Iterator that yields key-value pairs in insertion order.
|
||||||
|
public struct Iterator: IteratorProtocol {
|
||||||
|
private var current: Node?
|
||||||
|
|
||||||
|
fileprivate init(start: Node?) {
|
||||||
|
self.current = start
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func next() -> (key: Key, value: Value)? {
|
||||||
|
guard let node = current else { return nil }
|
||||||
|
current = node.next
|
||||||
|
return (node.key, node.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeIterator() -> Iterator {
|
||||||
|
return Iterator(start: head)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
/// Appends a new node to the end of the linked list.
|
||||||
|
private func appendNode(_ node: Node) {
|
||||||
|
if let tailNode = tail {
|
||||||
|
tailNode.next = node
|
||||||
|
node.prev = tailNode
|
||||||
|
tail = node
|
||||||
|
} else {
|
||||||
|
head = node
|
||||||
|
tail = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the given node from the linked list.
|
||||||
|
private func removeNode(_ node: Node) {
|
||||||
|
let prevNode = node.prev
|
||||||
|
let nextNode = node.next
|
||||||
|
|
||||||
|
if let prevNode = prevNode {
|
||||||
|
prevNode.next = nextNode
|
||||||
|
} else {
|
||||||
|
head = nextNode
|
||||||
|
}
|
||||||
|
|
||||||
|
if let nextNode = nextNode {
|
||||||
|
nextNode.prev = prevNode
|
||||||
|
} else {
|
||||||
|
tail = prevNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect node's pointers.
|
||||||
|
node.prev = nil
|
||||||
|
node.next = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func removeValue(forKey key: Key) -> Value? {
|
||||||
|
return remove(key: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LinkedHashMap {
|
||||||
|
public subscript(key: Key, default defaultValue: @autoclosure () -> Value) -> Value {
|
||||||
|
get {
|
||||||
|
if let value = self[key] {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
|
return defaultValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
self[key] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
397
SideStore/Utils/datastructures/TreeMap.swift
Normal file
397
SideStore/Utils/datastructures/TreeMap.swift
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
//
|
||||||
|
// TreeMap.swift
|
||||||
|
// SideStore
|
||||||
|
//
|
||||||
|
// Created by Magesh K on 21/02/25.
|
||||||
|
// Copyright © 2025 SideStore. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
public class TreeMap<Key: Comparable, Value>: Sequence {
|
||||||
|
|
||||||
|
// MARK: - Node and Color Definitions
|
||||||
|
|
||||||
|
fileprivate enum Color {
|
||||||
|
case red
|
||||||
|
case black
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate class Node {
|
||||||
|
var key: Key
|
||||||
|
var value: Value
|
||||||
|
var left: Node?
|
||||||
|
var right: Node?
|
||||||
|
weak var parent: Node?
|
||||||
|
var color: Color
|
||||||
|
|
||||||
|
init(key: Key, value: Value, color: Color = .red, parent: Node? = nil) {
|
||||||
|
self.key = key
|
||||||
|
self.value = value
|
||||||
|
self.color = color
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TreeMap Properties and Initializer
|
||||||
|
|
||||||
|
private var root: Node?
|
||||||
|
public private(set) var count: Int = 0
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
// MARK: - Public Dictionary-like API
|
||||||
|
|
||||||
|
/// Subscript: Get or set value for a given key.
|
||||||
|
public subscript(key: Key) -> Value? {
|
||||||
|
get { return get(key: key) }
|
||||||
|
set {
|
||||||
|
if let newValue = newValue {
|
||||||
|
_ = insert(key: key, value: newValue)
|
||||||
|
} else {
|
||||||
|
_ = remove(key: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the value associated with the given key.
|
||||||
|
public func get(key: Key) -> Value? {
|
||||||
|
guard let node = getNode(forKey: key) else { return nil }
|
||||||
|
return node.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts (or updates) the key with the given value.
|
||||||
|
/// Returns the old value if the key was already present.
|
||||||
|
@discardableResult
|
||||||
|
public func insert(key: Key, value: Value) -> Value? {
|
||||||
|
if let node = getNode(forKey: key) {
|
||||||
|
let oldValue = node.value
|
||||||
|
node.value = value
|
||||||
|
return oldValue
|
||||||
|
}
|
||||||
|
// Create new node
|
||||||
|
let newNode = Node(key: key, value: value)
|
||||||
|
var parent: Node? = nil
|
||||||
|
var current = root
|
||||||
|
while let cur = current {
|
||||||
|
parent = cur
|
||||||
|
if newNode.key < cur.key {
|
||||||
|
current = cur.left
|
||||||
|
} else {
|
||||||
|
current = cur.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newNode.parent = parent
|
||||||
|
if parent == nil {
|
||||||
|
root = newNode
|
||||||
|
} else if newNode.key < parent!.key {
|
||||||
|
parent!.left = newNode
|
||||||
|
} else {
|
||||||
|
parent!.right = newNode
|
||||||
|
}
|
||||||
|
count += 1
|
||||||
|
fixAfterInsertion(newNode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the node with the given key.
|
||||||
|
/// Returns the removed value if it existed.
|
||||||
|
@discardableResult
|
||||||
|
public func remove(key: Key) -> Value? {
|
||||||
|
guard let node = getNode(forKey: key) else { return nil }
|
||||||
|
let removedValue = node.value
|
||||||
|
deleteNode(node)
|
||||||
|
count -= 1
|
||||||
|
return removedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the map is empty.
|
||||||
|
public var isEmpty: Bool {
|
||||||
|
return count == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all keys in sorted order.
|
||||||
|
public var keys: [Key] {
|
||||||
|
var result = [Key]()
|
||||||
|
for (k, _) in self { result.append(k) }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all values in order of their keys.
|
||||||
|
public var values: [Value] {
|
||||||
|
var result = [Value]()
|
||||||
|
for (_, v) in self { result.append(v) }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all entries.
|
||||||
|
public func removeAll() {
|
||||||
|
root = nil
|
||||||
|
count = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Internal Helper Methods
|
||||||
|
|
||||||
|
/// Standard BST search for a node matching the key.
|
||||||
|
private func getNode(forKey key: Key) -> Node? {
|
||||||
|
var current = root
|
||||||
|
while let node = current {
|
||||||
|
if key == node.key {
|
||||||
|
return node
|
||||||
|
} else if key < node.key {
|
||||||
|
current = node.left
|
||||||
|
} else {
|
||||||
|
current = node.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the minimum node in the subtree rooted at `node`.
|
||||||
|
private func minimum(_ node: Node) -> Node {
|
||||||
|
var current = node
|
||||||
|
while let next = current.left {
|
||||||
|
current = next
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Rotation Methods
|
||||||
|
|
||||||
|
private func rotateLeft(_ x: Node) {
|
||||||
|
guard let y = x.right else { return }
|
||||||
|
x.right = y.left
|
||||||
|
if let leftChild = y.left {
|
||||||
|
leftChild.parent = x
|
||||||
|
}
|
||||||
|
y.parent = x.parent
|
||||||
|
if x.parent == nil {
|
||||||
|
root = y
|
||||||
|
} else if x === x.parent?.left {
|
||||||
|
x.parent?.left = y
|
||||||
|
} else {
|
||||||
|
x.parent?.right = y
|
||||||
|
}
|
||||||
|
y.left = x
|
||||||
|
x.parent = y
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rotateRight(_ x: Node) {
|
||||||
|
guard let y = x.left else { return }
|
||||||
|
x.left = y.right
|
||||||
|
if let rightChild = y.right {
|
||||||
|
rightChild.parent = x
|
||||||
|
}
|
||||||
|
y.parent = x.parent
|
||||||
|
if x.parent == nil {
|
||||||
|
root = y
|
||||||
|
} else if x === x.parent?.right {
|
||||||
|
x.parent?.right = y
|
||||||
|
} else {
|
||||||
|
x.parent?.left = y
|
||||||
|
}
|
||||||
|
y.right = x
|
||||||
|
x.parent = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Insertion Fix-Up
|
||||||
|
|
||||||
|
/// Restores red–black properties after insertion.
|
||||||
|
private func fixAfterInsertion(_ x: Node) {
|
||||||
|
var node = x
|
||||||
|
node.color = .red
|
||||||
|
while node !== root, let parent = node.parent, parent.color == .red {
|
||||||
|
if parent === parent.parent?.left {
|
||||||
|
if let uncle = parent.parent?.right, uncle.color == .red {
|
||||||
|
parent.color = .black
|
||||||
|
uncle.color = .black
|
||||||
|
parent.parent?.color = .red
|
||||||
|
if let grandparent = parent.parent {
|
||||||
|
node = grandparent
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if node === parent.right {
|
||||||
|
node = parent
|
||||||
|
rotateLeft(node)
|
||||||
|
}
|
||||||
|
node.parent?.color = .black
|
||||||
|
node.parent?.parent?.color = .red
|
||||||
|
if let grandparent = node.parent?.parent {
|
||||||
|
rotateRight(grandparent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let uncle = parent.parent?.left, uncle.color == .red {
|
||||||
|
parent.color = .black
|
||||||
|
uncle.color = .black
|
||||||
|
parent.parent?.color = .red
|
||||||
|
if let grandparent = parent.parent {
|
||||||
|
node = grandparent
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if node === parent.left {
|
||||||
|
node = parent
|
||||||
|
rotateRight(node)
|
||||||
|
}
|
||||||
|
node.parent?.color = .black
|
||||||
|
node.parent?.parent?.color = .red
|
||||||
|
if let grandparent = node.parent?.parent {
|
||||||
|
rotateLeft(grandparent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root?.color = .black
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Deletion Helpers
|
||||||
|
|
||||||
|
/// Replaces subtree rooted at u with subtree rooted at v.
|
||||||
|
private func transplant(_ u: Node, _ v: Node?) {
|
||||||
|
if u.parent == nil {
|
||||||
|
root = v
|
||||||
|
} else if u === u.parent?.left {
|
||||||
|
u.parent?.left = v
|
||||||
|
} else {
|
||||||
|
u.parent?.right = v
|
||||||
|
}
|
||||||
|
if let vNode = v {
|
||||||
|
vNode.parent = u.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes node z and fixes red–black properties.
|
||||||
|
private func deleteNode(_ z: Node) {
|
||||||
|
var y = z
|
||||||
|
let originalColor = y.color
|
||||||
|
var x: Node?
|
||||||
|
|
||||||
|
if z.left == nil {
|
||||||
|
x = z.right
|
||||||
|
transplant(z, z.right)
|
||||||
|
} else if z.right == nil {
|
||||||
|
x = z.left
|
||||||
|
transplant(z, z.left)
|
||||||
|
} else {
|
||||||
|
y = minimum(z.right!)
|
||||||
|
let yOriginalColor = y.color
|
||||||
|
x = y.right
|
||||||
|
if y.parent === z {
|
||||||
|
if x != nil { x!.parent = y }
|
||||||
|
} else {
|
||||||
|
transplant(y, y.right)
|
||||||
|
y.right = z.right
|
||||||
|
y.right?.parent = y
|
||||||
|
}
|
||||||
|
transplant(z, y)
|
||||||
|
y.left = z.left
|
||||||
|
y.left?.parent = y
|
||||||
|
y.color = z.color
|
||||||
|
if yOriginalColor == .black {
|
||||||
|
fixAfterDeletion(x, parent: y.parent)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if originalColor == .black {
|
||||||
|
fixAfterDeletion(x, parent: z.parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores red–black properties after deletion.
|
||||||
|
private func fixAfterDeletion(_ x: Node?, parent: Node?) {
|
||||||
|
var x = x
|
||||||
|
var parent = parent
|
||||||
|
while (x == nil || x!.color == .black) && (x !== root) {
|
||||||
|
if x === parent?.left {
|
||||||
|
var w = parent?.right
|
||||||
|
if w?.color == .red {
|
||||||
|
w?.color = .black
|
||||||
|
parent?.color = .red
|
||||||
|
rotateLeft(parent!)
|
||||||
|
w = parent?.right
|
||||||
|
}
|
||||||
|
if (w?.left == nil || w?.left?.color == .black) &&
|
||||||
|
(w?.right == nil || w?.right?.color == .black) {
|
||||||
|
w?.color = .red
|
||||||
|
x = parent
|
||||||
|
parent = x?.parent
|
||||||
|
} else {
|
||||||
|
if w?.right == nil || w?.right?.color == .black {
|
||||||
|
w?.left?.color = .black
|
||||||
|
w?.color = .red
|
||||||
|
if let wUnwrapped = w { rotateRight(wUnwrapped) }
|
||||||
|
w = parent?.right
|
||||||
|
}
|
||||||
|
w?.color = parent?.color ?? .black
|
||||||
|
parent?.color = .black
|
||||||
|
w?.right?.color = .black
|
||||||
|
rotateLeft(parent!)
|
||||||
|
x = root
|
||||||
|
parent = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var w = parent?.left
|
||||||
|
if w?.color == .red {
|
||||||
|
w?.color = .black
|
||||||
|
parent?.color = .red
|
||||||
|
rotateRight(parent!)
|
||||||
|
w = parent?.left
|
||||||
|
}
|
||||||
|
if (w?.left == nil || w?.left?.color == .black) &&
|
||||||
|
(w?.right == nil || w?.right?.color == .black) {
|
||||||
|
w?.color = .red
|
||||||
|
x = parent
|
||||||
|
parent = x?.parent
|
||||||
|
} else {
|
||||||
|
if w?.left == nil || w?.left?.color == .black {
|
||||||
|
w?.right?.color = .black
|
||||||
|
w?.color = .red
|
||||||
|
if let wUnwrapped = w { rotateLeft(wUnwrapped) }
|
||||||
|
w = parent?.left
|
||||||
|
}
|
||||||
|
w?.color = parent?.color ?? .black
|
||||||
|
parent?.color = .black
|
||||||
|
w?.left?.color = .black
|
||||||
|
rotateRight(parent!)
|
||||||
|
x = root
|
||||||
|
parent = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x?.color = .black
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience overload if parent is not separately tracked.
|
||||||
|
private func fixAfterDeletion(_ x: Node?) {
|
||||||
|
fixAfterDeletion(x, parent: x?.parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Sequence Conformance (In-Order Traversal)
|
||||||
|
|
||||||
|
public struct Iterator: IteratorProtocol {
|
||||||
|
private var stack: [Node] = []
|
||||||
|
|
||||||
|
// Marked as private because Node is a private type.
|
||||||
|
fileprivate init(root: Node?) {
|
||||||
|
var current = root
|
||||||
|
while let node = current {
|
||||||
|
stack.append(node)
|
||||||
|
current = node.left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func next() -> (Key, Value)? {
|
||||||
|
if stack.isEmpty { return nil }
|
||||||
|
let node = stack.removeLast()
|
||||||
|
let result = (node.key, node.value)
|
||||||
|
var current = node.right
|
||||||
|
while let n = current {
|
||||||
|
stack.append(n)
|
||||||
|
current = n.left
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeIterator() -> Iterator {
|
||||||
|
return Iterator(root: root)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
xcconfigs/UITests.xcconfig
Normal file
3
xcconfigs/UITests.xcconfig
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#include "../Build.xcconfig"
|
||||||
|
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).UITests
|
||||||
Reference in New Issue
Block a user