Compare commits

...

66 Commits

Author SHA1 Message Date
polymo1
430ed56ca9 ci: actually use beta in pr.yml
Signed-off-by: polymo1 <git@wesbryie.com>
2025-09-02 18:22:54 -04:00
polymo1
c74dc28773 ci: fix version number in pr.yml
Signed-off-by: polymo1 <git@wesbryie.com>
2025-09-02 18:18:12 -04:00
polymo1
5058eb7709 ci: update pr.yml to Xcode 26 Beta
Signed-off-by: polymo1 <git@wesbryie.com>
2025-09-02 18:12:57 -04:00
polymo1
242429b161 ci: change stable to Xcode 26 beta
Signed-off-by: polymo1 <git@wesbryie.com>
2025-09-02 18:09:11 -04:00
mahee96
724e8db980 - [ConsoleLogger]: Fix writing large data by chunking + stale handle capture 2025-08-06 02:03:18 +05:30
mahee96
669d33183e - [ConsoleLogger]: Fix race conditions during shutdown and possible crashes by writing to closed stream handler 2025-07-30 23:35:24 +05:30
CelloSerenity
a12b6cd62b Small fixes (#1020)
* here

* Update PatreonComponents.swift

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Update ErrorLogViewController.swift

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Update README.md

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Update trustedapps.json

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

* Revert "Update trustedapps.json"

This reverts commit 10a38895ea.

* Update Build.xcconfig

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>

---------

Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-07-18 21:24:55 -04:00
Huge_Black
685d956775 Use main profile when refreshing (#1013) 2025-06-22 08:31:13 -04:00
Huge_Black
00ed6e61be Add option for using main bundle's profile for app extensions, update AltSign & libmd (#1012) 2025-06-22 04:13:15 -04:00
neo
7057d59992 Merge pull request #1011 from CelloSerenity/develop 2025-06-14 17:54:05 -04:00
CelloSerenity
6d308487c1 Update LaunchViewController.swift
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-06-14 14:04:20 -06:00
Huge_Black
6c45eb096f Add import/export certificate feature (#1008) 2025-06-12 00:47:33 -04:00
Stern
4b6ffa5d4a Merge pull request #995 from CelloSerenity/develop
Update OperationError.swift for idevice_pair
2025-05-24 21:06:24 -04:00
CelloSerenity
728b12d004 Update OperationError.swift
Co-authored-by: Stern <xsternent@gmail.com>
Signed-off-by: CelloSerenity <195480169+CelloSerenity@users.noreply.github.com>
2025-05-24 19:04:25 -06:00
CelloSerenity
97160569ba Update OperationError.swift 2025-05-24 18:52:56 -06:00
polymo1
1b754e137a Merge pull request #987 from owoellen/patch-1
Change front-facing references to AltStore
2025-05-19 18:35:07 -04:00
ellen!
4176b8c83c Update LockScreenWidget.swift
Signed-off-by: ellen! <78277148+owoellen@users.noreply.github.com>
2025-05-19 22:45:50 +01:00
ellen!
479f877dbf Update Authentication.storyboard
Signed-off-by: ellen! <78277148+owoellen@users.noreply.github.com>
2025-05-19 22:43:45 +01:00
Stossy11
4ff643805b Feat: Add Pairing File Export URL callback and replace Wireguard with StosVPN (#962)
* Add Pairing File Export URL callback and replace Wireguard with StosVPN

* Fix Data()

* Add Switching Tab

* Fix Pairing Callback Template and fix spelling mistake

* Add Observer for openExportPairingFileConfirm

* Add Logging and fix certificate callback

* add func to Scene Delegate

* Fix ToastView and Logging

* Remove ?

* Fix Logging

* Call AppDelegate instead of SceneDelegate

* Fix Scene Delegate

* Set back SceneDelegate

* Add Logging

* Fix error

* Fix Case Sensitive

* Remove openExportPairingFileConfirm and fix AppDelegate.exportPairingFileNotification

* Remove Alert

* Remove Unnecessary code

* rename export() to exportPairingFile()
2025-04-24 01:48:29 -04:00
Huge_Black
0e3c3dddfe Add export certificate feature (#959) 2025-04-19 20:48:50 -04:00
Jackson Coxson
7415fe6204 Bump minimuxer 2025-04-16 15:16:24 -04:00
Jackson Coxson
c128c9268b Update SideStore for new minimuxer 2025-04-16 15:16:24 -04:00
Magesh K
131a0289a2 Merge pull request #935 from mahee96/develop-minimuxer
Disable minimuxer ready check which shows NO WIFI/VPN error since stosVPN can work on cellular too
2025-04-09 00:03:04 -07:00
Magesh K
116f045e51 Merge pull request #945 from SideStore/develop-alpha
StosVPN-integration: Removed em_proxy and EmotionalDamage module in favour of stosVPN
2025-04-08 23:39:58 -07:00
Magesh K
c68efd2b44 Merge branch 'develop' into develop-alpha 2025-04-08 23:38:52 -07:00
mahee96
e519389780 - bump version to 0.6.2 for next release cycle 2025-04-08 23:37:25 -07:00
mahee96
e6135c6518 em_proxy: removed em_proxy submodule from git 2025-04-08 23:08:57 -07:00
mahee96
3d444f301d - stosVPN-integration: Removed em_proxy and EmotionalDamage module in favour of stosVPN 2025-04-08 23:03:49 -07:00
mahee96
7adfd3d3e8 Merge branch 'develop' into develop-alpha 2025-04-08 22:46:26 -07:00
polymo1
591913743e Merge pull request #940 from Br0des/develop
Added StosVPN to the EM Proxy part.
2025-04-05 22:38:53 -04:00
Brodes
77d95fe278 Added StosVPN to the EM Proxy part.
yeah i'm petty >:3

Signed-off-by: Brodes <144500576+Br0des@users.noreply.github.com>
2025-04-05 20:13:07 -06:00
mahee96
7ec6324b62 - minimuxer-ready: disabled minimuxer ready check which shows NO WIFI/VPN error since stosVPN can work on cellular too. 2025-04-03 00:49:07 -07:00
Joseph LaFreniere
0cd62d371a [Skip Ci] Fix typo "levaraging" -> "leveraging" (#926)
Signed-off-by: Joseph LaFreniere <git@lafreniere.xyz>
2025-03-31 15:12:25 -07:00
Magesh K
9771f6bb9a Merge pull request #920 from mahee96/develop
Migration: Fixes for migration issues when migrating from 0.5.9 to 0.6.0
2025-03-25 20:37:16 -07:00
mahee96
e553efbad5 - migration-fix: more reinforcements for 0.6.0 to 0.6.1 2025-03-24 01:32:01 -07:00
mahee96
a4dfd28a3c - [Fix]: migrations fix for coredata from v11(0.5.9) to v17_1(0.6.1) and v17(0.6.0 to v17_1(0.6.1) 2025-03-24 00:19:05 -07:00
mahee96
291d7fd8d9 - [Fix]: migrations fix for coredata from v11(0.5.9) to v17_1(0.6.1) and v17(0.6.0 to v17_1(0.6.1) 2025-03-23 21:44:06 -07:00
mahee96
a7496e08e3 - [WIP]: migrations fix for coredata from v11(0.5.9) to v17_1(0.6.1) and v17(0.6.0 to v17_1(0.6.1) 2025-03-23 12:09:58 -07:00
mahee96
2f3be07b5d - fix: attempt to fix "app no longer available" issues due to fetch profiles not updating appFeatures and appGroups (possible regression from PR#846) 2025-03-23 12:00:31 -07:00
mahee96
b57d279670 - [WIP]: migrations fix for coredata from v11(0.5.9) to v17_1(0.6.1) and v17(0.6.0 to v17_1(0.6.1) 2025-03-23 11:59:01 -07:00
mahee96
cbde3e6495 Revert "- Fixed: attempt migrations fix from 0.5.10 to 0.6.0"
This reverts commit ae8e9a3506.
2025-03-23 11:57:16 -07:00
polymo1
117f31e158 Merge pull request #915 from neoarz/patch-3
Update SettingsViewController.swift
2025-03-15 10:13:42 -04:00
neoarz
420efcbb11 Update SettingsViewController.swift
Signed-off-by: neoarz <164915254+neoarz@users.noreply.github.com>
2025-03-15 06:30:03 -04:00
mahee96
dc29b65bd5 - fix: attempt to fix "app no longer available" issues due to fetch profiles not updating appFeatures and appGroups (possible regression from PR#846) 2025-03-15 01:58:08 +05:30
Zero King
1e64f50ab9 fix: typo in hasUpdate comparison
Signed-off-by: Zero King <l2dy@icloud.com>
2025-03-13 03:29:02 +05:30
mahee96
ae8e9a3506 - Fixed: attempt migrations fix from 0.5.10 to 0.6.0 2025-03-11 04:44:23 +05:30
Magesh K
3785891923 Merge pull request #900 from l2dy/patch-1
fix: typo in hasUpdate comparison
2025-03-04 03:33:32 +05:30
Zero King
e85db67ac7 fix: typo in hasUpdate comparison
Signed-off-by: Zero King <l2dy@icloud.com>
2025-03-02 01:47:04 +08:00
mahee96
39d0835f5b - CI: updated stable.yml for recent fixes 2025-03-01 01:30:15 +05:30
mahee96
c8127fb3b9 - CI: updated stable.yml for recent fixes 2025-02-28 23:41:31 +05:30
Magesh K
729fca9100 Merge pull request #897 from SideStore/markdown-for-update-description
Feature: Render in-app update description as (full) markdown
2025-02-28 05:22:37 +05:30
mahee96
c6703d66c1 - Feature: Markdown view integration complete (if issues arrise can fix it asap) 2025-02-28 05:09:37 +05:30
mahee96
2197161d55 - Fix: CollapsingMarkdownView was not reloading properly during layoutSubviews phase 2025-02-28 02:28:34 +05:30
mahee96
cfaf79f878 - Fixes: disabled animations for the CollapsingMarkdownView when expanding/collapsing due to visual glitches 2025-02-28 01:02:41 +05:30
mahee96
2bea980d1f - Feature: Added markdown rendering for in-app update description field 2025-02-27 23:39:03 +05:30
mahee96
f11e27c712 - CI: serialize on whole workflow instead of individual jobs 2025-02-27 05:37:35 +05:30
mahee96
b316e84f0d - CI: serialize on whole workflow instead of individual jobs 2025-02-27 05:36:41 +05:30
mahee96
4668f8499b - CI: fix release notes updater for invalid revision as input 2025-02-27 05:23:20 +05:30
mahee96
f9aedaba04 - CI: fix release notes updater for invalid revision as input 2025-02-27 05:22:11 +05:30
mahee96
8cb3de9ab5 - CI: enable build on push for nightly temporarily 2025-02-27 05:03:42 +05:30
mahee96
ca57d58219 - CI: fix: deploy errors 2025-02-27 05:01:27 +05:30
mahee96
6a56fbd206 -CI: fix: deploy must run if tests were sccessful or skipped 2025-02-27 04:57:34 +05:30
mahee96
cec3825de0 - Makefile: build-tests need destination param same as run-tests 2025-02-27 04:44:35 +05:30
mahee96
b3e99d1ae3 - CI: Try composite build 2025-02-27 04:30:45 +05:30
mahee96
7243d79646 - UITests: Fix issue with UITests bundleID causing test failures in CI 2025-02-27 02:50:16 +05:30
Magesh K
e50da6603c - CI: replaced irgaly/xcode-cache with gh-actions-cache
- CI: serialization bug fix - reverted concurrency lock to workflow level instead of job level which was causing issues due to incorrect cache at expected jobs
- CI: Boot simulator in async instead of doing in a blocking step
- CI: integrate publishing release notes
- CI: moved posting to beta-build-num repo into build-job instead of deploy coz we want to roll the beta-build-num for each success of build-job even if the workflow failed.
2025-02-27 02:49:20 +05:30
78 changed files with 6276 additions and 1024 deletions

View File

@@ -47,13 +47,15 @@ on:
jobs:
serialize:
name: Wait for other jobs
concurrency:
group: build-number-increment # serialize for build num cache access
# since build cache, test-build cache, test-run cache are involved, out of order exec if serialization is on individual jobs will wreak all sorts of havoc
# so we serialize on the entire workflow
concurrency:
group: serialize-workflow
strategy:
fail-fast: false
runs-on: 'macos-15'
steps:
- run: echo "No other contending jobs are running now...Build is ready to start"
- run: echo "No other contending jobs are running now..."
- name: Set short commit hash
id: commit-id
run: |
@@ -72,7 +74,7 @@ jobs:
matrix:
include:
- os: 'macos-15'
version: '16.1'
version: '16.2'
runs-on: ${{ matrix.os }}
outputs:
@@ -88,6 +90,7 @@ jobs:
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install dependencies - ldid & xcbeautify
run: |
@@ -174,14 +177,32 @@ jobs:
with:
xcode-version: ${{ matrix.version }}
- name: (Build) Cache Build
uses: irgaly/xcode-cache@v1
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
key: xcode-cache-deriveddata-build-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-build-
swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-build-
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-
# - name: (Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
- name: (Build) Restore Pods from Cache (Exact match)
id: pods-restore
@@ -191,7 +212,7 @@ jobs:
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-${{ hashFiles('Podfile') }}
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
# restore-keys: | # commented out to strictly check cache for this particular podfile
# pods-cache-
@@ -204,7 +225,7 @@ jobs:
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-
key: pods-cache-build-${{ github.ref_name }}-
- name: (Build) Install CocoaPods
@@ -219,7 +240,7 @@ jobs:
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-${{ hashFiles('Podfile') }}
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Build) Clean previous build artifacts
# using 'tee' to intercept stdout and log for detailed build-log
@@ -266,6 +287,16 @@ jobs:
- name: Convert to IPA
run: make ipa | tee -a build/logs/build.log
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) List Files and Build artifacts
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
@@ -284,6 +315,10 @@ jobs:
find SideStore.xcarchive -maxdepth 3 -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: Encrypt build-logs for upload
id: encrypt-build-log
run: |
@@ -322,19 +357,69 @@ jobs:
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
- name: Keep rolling the build numbers for each successful build
if: ${{ inputs.is_beta }}
run: |
pushd SideStore/beta-build-num/
echo "Configure Git user (committer details)"
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
echo "Adding files to commit"
git add --verbose build_number.txt
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ needs.serialize.outputs.short-commit }} deployment" || echo "No changes to commit"
echo "Pushing to remote repo"
git push --verbose
popd
- name: Get last successful commit
id: get_last_commit
run: |
# Try to get the last successful workflow run commit
LAST_SUCCESS_SHA=$(gh run list --branch "${{ github.ref_name }}" --status success --json headSha --jq '.[0].headSha')
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_OUTPUT
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release notes
run: |
LAST_SUCCESS_SHA=${{ steps.get_last_commit.outputs.LAST_SUCCESS_SHA}}
echo "Last successful commit SHA: $LAST_SUCCESS_SHA"
FROM_COMMIT=$LAST_SUCCESS_SHA
# Check if we got a valid SHA
if [ -z "$LAST_SUCCESS_SHA" ] || [ "$LAST_SUCCESS_SHA" = "null" ]; then
echo "No successful run found, using initial commit of branch"
# Get the first commit of the branch (initial commit)
FROM_COMMIT=$(git rev-list --max-parents=0 HEAD)
fi
python3 update_release_notes.py $FROM_COMMIT ${{ inputs.release_tag }} ${{ github.ref_name }}
# cat release-notes.md
- name: Upload release-notes.md
uses: actions/upload-artifact@v4
with:
name: beta-build-num-${{ steps.version.outputs.version }}.zip
path: beta-build-num.zip
name: release-notes-${{ needs.serialize.outputs.short-commit }}.md
path: release-notes.md
- name: Upload update_release_notes.py
uses: actions/upload-artifact@v4
with:
name: update_release_notes-${{ needs.serialize.outputs.short-commit }}.py
path: update_release_notes.py
- name: Upload update_apps.py
uses: actions/upload-artifact@v4
with:
name: update_apps-${{ needs.serialize.outputs.short-commit }}.py
path: update_apps.py
tests-build:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
name: Tests-Build SideStore - ${{ inputs.release_tag }}
needs: serialize
strategy:
@@ -342,7 +427,7 @@ jobs:
matrix:
include:
- os: 'macos-15'
version: '16.1'
version: '16.2'
runs-on: ${{ matrix.os }}
steps:
@@ -358,16 +443,36 @@ jobs:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.1'
xcode-version: '16.2'
- name: (Tests-Build) Cache Build
uses: irgaly/xcode-cache@v1
# - name: (Tests-Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# # tests shouldn't restore cache unless it is same build
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
# delete-used-deriveddata-cache: true
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
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-
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-
- name: (Tests-Build) Restore Pods from Cache (Exact match)
id: pods-restore
@@ -377,7 +482,7 @@ jobs:
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ hashFiles('Podfile') }}
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Tests-Build) Restore Pods from Cache (Last Available)
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
@@ -388,7 +493,7 @@ jobs:
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-
key: pods-cache-test-${{ github.ref_name }}-
- name: (Tests-Build) Install CocoaPods
run: pod install
@@ -401,8 +506,15 @@ jobs:
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ hashFiles('Podfile') }}
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: Clean Derived Data (if required)
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/
make clean
xcodebuild clean
- name: (Tests-Build) Clean previous build artifacts
run: |
make clean
@@ -435,6 +547,16 @@ jobs:
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) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) List Files and Build artifacts
if: always()
run: |
@@ -446,6 +568,16 @@ jobs:
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-build-deriveddata-${{ needs.serialize.outputs.short-commit }}.txt
path: tests-build-deriveddata.txt
- name: Encrypt tests-build-logs for upload
id: encrypt-test-log
if: always()
@@ -471,6 +603,7 @@ jobs:
path: encrypted-tests-build-logs.zip
tests-run:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
name: Tests-Run SideStore - ${{ inputs.release_tag }}
needs: [serialize, tests-build]
strategy:
@@ -478,7 +611,7 @@ jobs:
matrix:
include:
- os: 'macos-15'
version: '16.1'
version: '16.2'
runs-on: ${{ matrix.os }}
steps:
@@ -487,25 +620,31 @@ jobs:
with:
submodules: recursive
- name: Boot Simulator for testing
- name: Boot Simulator async(nohup) for testing
run: |
mkdir -p build/logs
make -B boot-sim-async | tee -a build/logs/tests-run.log
exit ${PIPESTATUS[0]}
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.1'
xcode-version: '16.2'
- name: (Tests-Run) Cache Build
uses: irgaly/xcode-cache@v1
# - name: (Tests-Run) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# # This comes from
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
id: xcode-cache-restore
uses: actions/cache/restore@v3
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-
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Run) Restore Pods from Cache (Exact match)
id: pods-restore
@@ -515,7 +654,7 @@ jobs:
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ hashFiles('Podfile') }}
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Tests-Run) Restore Pods from Cache (Last Available)
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
@@ -526,7 +665,7 @@ jobs:
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-
key: pods-cache-test-${{ github.ref_name }}-
- name: (Tests-Run) Install CocoaPods
run: pod install
@@ -539,7 +678,7 @@ jobs:
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ hashFiles('Podfile') }}
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Tests-Run) Clean previous build artifacts
run: |
@@ -565,9 +704,15 @@ jobs:
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-run-deriveddata-${{ needs.serialize.outputs.short-commit }}.txt
path: tests-run-deriveddata.txt
# we expect simulator to have been booted by now, so exit otherwise
- name: Simulator Boot Check
run: |
@@ -689,34 +834,51 @@ jobs:
name: encrypted-build-logs-${{ needs.build.outputs.version }}.zip
- name: Download encrypted-tests-build-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-build-logs-${{ needs.serialize.outputs.short-commit }}.zip
- name: Download encrypted-tests-run-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-run-logs-${{ needs.serialize.outputs.short-commit }}.zip
- name: Download tests-recording artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: tests-recording-${{ needs.serialize.outputs.short-commit }}.mp4
- name: Download test-results artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
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: test-results-${{ needs.serialize.outputs.short-commit }}.zip
- name: Download release-notes.md
uses: actions/download-artifact@v4
with:
name: release-notes-${{ needs.serialize.outputs.short-commit }}.md
- name: Download update_release_notes.py
uses: actions/download-artifact@v4
with:
name: update_release_notes-${{ needs.serialize.outputs.short-commit }}.py
- name: Download update_apps.py
uses: actions/download-artifact@v4
with:
name: update_apps-${{ needs.serialize.outputs.short-commit }}.py
- name: Read release notes
id: release_notes
run: |
CONTENT=$(python3 update_release_notes.py --retrieve ${{ inputs.release_tag }})
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: List files before upload
run: |
@@ -754,22 +916,7 @@ jobs:
Commit SHA: `${{ github.sha }}`
Version: `${{ needs.build.outputs.version }}`
- name: Publish to SideStore/beta-build-num
if: ${{ inputs.is_beta }}
run: |
pushd SideStore/beta-build-num/
echo "Configure Git user (committer details)"
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
echo "Adding files to commit"
git add --verbose build_number.txt
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ needs.serialize.outputs.short-commit }} deployment" || echo "No changes to commit"
echo "Pushing to remote repo"
git push --verbose
popd
${{ steps.release_notes.outputs.content }}
- name: Get formatted date
run: |
@@ -803,6 +950,9 @@ jobs:
- version: "${{ needs.build.outputs.version }}"
- revision: "${{ needs.serialize.outputs.short-commit }}"
- timestamp: "${{ steps.date.outputs.date }}"
Release Notes:
${{ steps.release_notes.outputs.content }}
EOF
)

63
.github/maintenance/cache.py vendored Normal file
View File

@@ -0,0 +1,63 @@
import requests
import sys
import os
# Your GitHub Personal Access Token
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
# Repository details
REPO_OWNER = "SideStore"
REPO_NAME = "SideStore"
API_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/actions/caches"
# Common headers for GitHub API calls
HEADERS = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {GITHUB_TOKEN}"
}
def list_caches():
response = requests.get(API_URL, headers=HEADERS)
if response.status_code != 200:
print(f"Failed to list caches. HTTP {response.status_code}")
print("Response:", response.text)
sys.exit(1)
data = response.json()
return data.get("actions_caches", [])
def delete_cache(cache_id):
delete_url = f"{API_URL}/{cache_id}"
response = requests.delete(delete_url, headers=HEADERS)
return response.status_code
def main():
caches = list_caches()
if not caches:
print("No caches found.")
return
print("Found caches:")
for cache in caches:
print(f"ID: {cache.get('id')}, Key: {cache.get('key')}")
print("\nDeleting caches...")
for cache in caches:
cache_id = cache.get("id")
status = delete_cache(cache_id)
if status == 204:
print(f"Successfully deleted cache with ID: {cache_id}")
else:
print(f"Failed to delete cache with ID: {cache_id}. HTTP status code: {status}")
print("All caches processed.")
if __name__ == "__main__":
main()
### How to use
'''
just export the GITHUB_TOKEN and then run this script via `python3 cache.py' to delete the caches
'''

View File

@@ -10,8 +10,8 @@ concurrency:
cancel-in-progress: true
jobs:
Reuseable-build:
uses: ./.github/workflows/reusable-build-workflow.yml
Reusable-build:
uses: ./.github/workflows/reusable-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Alpha"
bundle_id: "com.SideStore.SideStore"

View File

@@ -58,13 +58,13 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LAST_SUCCESS: ${{ env.last_success }}
Reuseable-build:
Reusable-build:
if: |
always() &&
(github.event_name == 'push' ||
(github.event_name == 'schedule' && needs.check-changes.result == 'success' && needs.check-changes.outputs.has_changes == 'true'))
needs: check-changes
uses: ./.github/workflows/reusable-build-workflow.yml
uses: ./.github/workflows/reusable-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Nightly"
bundle_id: "com.SideStore.SideStore"

View File

@@ -12,8 +12,8 @@ jobs:
fail-fast: false
matrix:
include:
- os: 'macos-14'
version: '16.1'
- os: 'macos-15'
version: '26.0-beta'
runs-on: ${{ matrix.os }}
steps:

View File

@@ -0,0 +1,104 @@
name: Reusable SideStore Build
on:
workflow_call:
inputs:
is_beta:
required: false
default: false
type: boolean
publish:
required: false
default: false
type: boolean
is_shared_build_num:
required: false
default: true
type: boolean
release_name:
required: true
type: string
release_tag:
required: true
type: string
upstream_tag:
required: true
type: string
upstream_name:
required: true
type: string
bundle_id:
default: com.SideStore.SideStore
required: true
type: string
bundle_id_suffix:
default: ''
required: false
type: string
secrets:
# GITHUB_TOKEN:
# required: true
CROSS_REPO_PUSH_KEY:
required: true
BUILD_LOG_ZIP_PASSWORD:
required: false
# since build cache, test-build cache, test-run cache are involved, out of order exec if serialization is on individual jobs will wreak all sorts of havoc
# so we serialize on the entire workflow
concurrency:
group: serialize-workflow
jobs:
shared:
uses: ./.github/workflows/sidestore-shared.yml
secrets: inherit
build:
needs: shared
uses: ./.github/workflows/sidestore-build.yml
with:
is_beta: ${{ inputs.is_beta }}
is_shared_build_num: ${{ inputs.is_shared_build_num }}
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
bundle_id: ${{ inputs.bundle_id }}
bundle_id_suffix: ${{ inputs.bundle_id_suffix }}
secrets: inherit
tests-build:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
needs: shared
uses: ./.github/workflows/sidestore-tests-build.yml
with:
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
secrets: inherit
tests-run:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
needs: [shared, tests-build]
uses: ./.github/workflows/sidestore-tests-run.yml
with:
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
secrets: inherit
deploy:
needs: [shared, build, tests-build, tests-run] # Keep tests-run in needs
if: ${{ always() && (needs.tests-run.result == 'skipped' || needs.tests-run.result == 'success') }}
uses: ./.github/workflows/sidestore-deploy.yml
with:
is_beta: ${{ inputs.is_beta }}
publish: ${{ inputs.publish }}
release_name: ${{ inputs.release_name }}
release_tag: ${{ inputs.release_tag }}
upstream_tag: ${{ inputs.upstream_tag }}
upstream_name: ${{ inputs.upstream_name }}
version: ${{ needs.build.outputs.version }}
short_commit: ${{ needs.shared.outputs.short-commit }}
release_channel: ${{ needs.build.outputs.release-channel }}
marketing_version: ${{ needs.build.outputs.marketing-version }}
bundle_id: ${{ inputs.bundle_id }}
secrets: inherit

401
.github/workflows/sidestore-build.yml vendored Normal file
View File

@@ -0,0 +1,401 @@
name: SideStore Build
on:
workflow_call:
inputs:
is_beta:
type: boolean
is_shared_build_num:
type: boolean
release_tag:
type: string
bundle_id:
type: string
bundle_id_suffix:
type: string
short_commit:
type: string
secrets:
CROSS_REPO_PUSH_KEY:
required: true
BUILD_LOG_ZIP_PASSWORD:
required: false
outputs:
version:
value: ${{ jobs.build.outputs.version }}
marketing-version:
value: ${{ jobs.build.outputs.marketing-version }}
release-channel:
value: ${{ jobs.build.outputs.release-channel }}
jobs:
build:
name: Build SideStore - ${{ inputs.release_tag }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
runs-on: ${{ matrix.os }}
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
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
shell: bash
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install dependencies - ldid & xcbeautify
run: |
brew install ldid xcbeautify
- name: Set ref based on is_shared_build_num
if: ${{ inputs.is_beta }}
id: set_ref
run: |
if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then
echo "ref=main" >> $GITHUB_ENV
else
echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV
fi
shell: bash
- name: Checkout SideStore/beta-build-num repo
if: ${{ inputs.is_beta }}
uses: actions/checkout@v4
with:
repository: 'SideStore/beta-build-num'
ref: ${{ env.ref }}
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: 'SideStore/beta-build-num'
- name: Copy build_number.txt to repo root
if: ${{ inputs.is_beta }}
run: |
cp SideStore/beta-build-num/build_number.txt .
echo "cat build_number.txt"
cat build_number.txt
shell: bash
- name: Echo Build.xcconfig
run: |
echo "cat Build.xcconfig"
cat Build.xcconfig
shell: bash
- name: Set Release Channel info for build number bumper
id: release-channel
run: |
RELEASE_CHANNEL="${{ inputs.release_tag }}"
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_OUTPUT
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
shell: bash
- name: Increase build number for beta builds
if: ${{ inputs.is_beta }}
run: |
bash .github/workflows/increase-beta-build-num.sh
shell: bash
- name: Extract MARKETING_VERSION from Build.xcconfig
id: version
run: |
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
echo "version=$version" >> $GITHUB_OUTPUT
echo "version=$version"
shell: bash
- name: Set MARKETING_VERSION
if: ${{ inputs.is_beta }}
id: marketing-version
run: |
# 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/')
# Extract date (YYYYMMDD) (e.g., "20250205")
date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/')
# Extract build number (e.g., "2")
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
# Combine them into the final output
MARKETING_VERSION="${version}-${date}.${build_num}+${{ inputs.short_commit }}"
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
echo "MARKETING_VERSION=$MARKETING_VERSION"
shell: bash
- name: Echo Updated Build.xcconfig, build_number.txt
if: ${{ inputs.is_beta }}
run: |
cat Build.xcconfig
cat build_number.txt
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-
# - name: (Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
- name: (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-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
# restore-keys: | # commented out to strictly check cache for this particular podfile
# pods-cache-
- name: (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-build-${{ github.ref_name }}-
- name: (Build) Install CocoaPods
run: pod install
shell: bash
- name: (Build) Save Pods to Cache
id: save-pods
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Build) Clean previous build artifacts
# using 'tee' to intercept stdout and log for detailed build-log
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Build) List Files and derived data
if: always()
shell: bash
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: Set BundleID Suffix for Sidestore build
run: |
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
shell: bash
- name: Build SideStore.xcarchive
# using 'tee' to intercept stdout and log for detailed build-log
run: |
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
shell: bash
- name: Fakesign app
run: make fakesign | tee -a build/logs/build.log
shell: bash
- name: Convert to IPA
run: make ipa | tee -a build/logs/build.log
shell: bash
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- 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 ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
shell: bash
- 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"
shell: bash
- 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
shell: bash
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
path: SideStore.dSYMs.zip
- name: Keep rolling the build numbers for each successful build
if: ${{ inputs.is_beta }}
run: |
pushd SideStore/beta-build-num/
echo "Configure Git user (committer details)"
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
echo "Adding files to commit"
git add --verbose build_number.txt
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
echo "Pushing to remote repo"
git push --verbose
popd
shell: bash
- name: Get last successful commit
id: get_last_commit
run: |
# Try to get the last successful workflow run commit
LAST_SUCCESS_SHA=$(gh run list --branch "${{ github.ref_name }}" --status success --json headSha --jq '.[0].headSha')
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_OUTPUT
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
- name: Create release notes
run: |
LAST_SUCCESS_SHA=${{ steps.get_last_commit.outputs.LAST_SUCCESS_SHA}}
echo "Last successful commit SHA: $LAST_SUCCESS_SHA"
FROM_COMMIT=$LAST_SUCCESS_SHA
# Check if we got a valid SHA
if [ -z "$LAST_SUCCESS_SHA" ] || [ "$LAST_SUCCESS_SHA" = "null" ]; then
echo "No successful run found, using initial commit of branch"
# Get the first commit of the branch (initial commit)
FROM_COMMIT=$(git rev-list --max-parents=0 HEAD)
fi
python3 update_release_notes.py $FROM_COMMIT ${{ inputs.release_tag }} ${{ github.ref_name }}
# cat release-notes.md
shell: bash
- name: Upload release-notes.md
uses: actions/upload-artifact@v4
with:
name: release-notes-${{ inputs.short_commit }}.md
path: release-notes.md
- name: Upload update_release_notes.py
uses: actions/upload-artifact@v4
with:
name: update_release_notes-${{ inputs.short_commit }}.py
path: update_release_notes.py
- name: Upload update_apps.py
uses: actions/upload-artifact@v4
with:
name: update_apps-${{ inputs.short_commit }}.py
path: update_apps.py

235
.github/workflows/sidestore-deploy.yml vendored Normal file
View File

@@ -0,0 +1,235 @@
name: SideStore Deploy
on:
workflow_call:
inputs:
is_beta:
type: boolean
publish:
type: boolean
release_name:
type: string
release_tag:
type: string
upstream_tag:
type: string
upstream_name:
type: string
version:
type: string
short_commit:
type: string
marketing_version:
type: string
release_channel:
type: string
bundle_id:
type: string
secrets:
CROSS_REPO_PUSH_KEY:
required: true
# GITHUB_TOKEN:
# required: true
jobs:
deploy:
name: Deploy SideStore - ${{ inputs.release_tag }}
runs-on: macos-15
steps:
- name: Download IPA artifact
uses: actions/download-artifact@v4
with:
name: SideStore-${{ inputs.version }}.ipa
- name: Download dSYM artifact
uses: actions/download-artifact@v4
with:
name: SideStore-${{ inputs.version }}-dSYMs.zip
- name: Download encrypted-build-logs artifact
uses: actions/download-artifact@v4
with:
name: encrypted-build-logs-${{ inputs.version }}.zip
- name: Download encrypted-tests-build-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
- name: Download encrypted-tests-run-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
- name: Download tests-recording artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: tests-recording-${{ inputs.short_commit }}.mp4
- name: Download test-results artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: test-results-${{ inputs.short_commit }}.zip
- name: Download release-notes.md
uses: actions/download-artifact@v4
with:
name: release-notes-${{ inputs.short_commit }}.md
- name: Download update_release_notes.py
uses: actions/download-artifact@v4
with:
name: update_release_notes-${{ inputs.short_commit }}.py
- name: Download update_apps.py
uses: actions/download-artifact@v4
with:
name: update_apps-${{ inputs.short_commit }}.py
- name: Read release notes
id: release_notes
run: |
CONTENT=$(python3 update_release_notes.py --retrieve ${{ inputs.release_tag }})
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
shell: bash
- name: List files before upload
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
echo ""
shell: bash
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
shell: bash
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
shell: bash
- name: Upload to releases
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release: ${{ inputs.release_name }}
tag: ${{ inputs.release_tag }}
prerelease: ${{ inputs.is_beta }}
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: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!**
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }}).
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ inputs.version }}`
${{ steps.release_notes.outputs.content }}
- name: Get formatted date
run: |
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "Formatted date: $FORMATTED_DATE"
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
shell: bash
- name: Get size of IPA in bytes (macOS/Linux)
run: |
if [[ "$(uname)" == "Darwin" ]]; then
# macOS
IPA_SIZE=$(stat -f %z SideStore.ipa)
else
# Linux
IPA_SIZE=$(stat -c %s SideStore.ipa)
fi
echo "IPA size in bytes: $IPA_SIZE"
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
shell: bash
- name: Compute SHA-256 of IPA
run: |
SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }')
echo "SHA-256 Hash: $SHA256_HASH"
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
shell: bash
- name: Set Release Info variables
run: |
# Format localized description
LOCALIZED_DESCRIPTION=$(cat <<EOF
This is release for:
- version: "${{ inputs.version }}"
- revision: "${{ inputs.short_commit }}"
- timestamp: "${{ steps.date.outputs.date }}"
Release Notes:
${{ steps.release_notes.outputs.content }}
EOF
)
echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
echo "VERSION_IPA=${{ inputs.marketing_version }}" >> $GITHUB_ENV
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=${{ inputs.release_channel }}" >> $GITHUB_ENV
echo "SIZE=$IPA_SIZE" >> $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
# multiline strings
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
shell: bash
- name: Check if Publish updates is set
id: check_publish
run: |
echo "Publish updates to source.json = ${{ inputs.publish }}"
shell: bash
- name: Checkout SideStore/apps-v2.json
if: ${{ inputs.is_beta && inputs.publish }}
uses: actions/checkout@v4
with:
repository: 'SideStore/apps-v2.json'
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: 'SideStore/apps-v2.json'
# for stable builds, let the user manually edit the source.json
- name: Publish to SideStore/apps-v2.json
if: ${{ inputs.is_beta && inputs.publish }}
id: publish-release
shell: bash
run: |
# Copy and execute the update script
pushd SideStore/apps-v2.json/
# Configure Git user (committer details)
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
# update the source.json
python3 ../../update_apps.py "./_includes/source.json"
# Commit changes and push using SSH
git add --verbose ./_includes/source.json
git commit -m " - updated for ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
git push --verbose
popd

24
.github/workflows/sidestore-shared.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: SideStore Shared
on:
workflow_call:
outputs:
short-commit:
value: ${{ jobs.shared.outputs.short-commit }}
jobs:
shared:
name: Shared Steps
strategy:
fail-fast: false
runs-on: 'macos-15'
steps:
- 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 }}

View File

@@ -0,0 +1,204 @@
name: SideStore Tests Build
on:
workflow_call:
inputs:
release_tag:
type: string
short_commit:
type: string
secrets:
BUILD_LOG_ZIP_PASSWORD:
required: false
jobs:
tests-build:
name: Tests-Build SideStore - ${{ inputs.release_tag }}
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies - xcbeautify
run: |
brew install xcbeautify
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.2'
# - name: (Tests-Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# # tests shouldn't restore cache unless it is same build
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
# delete-used-deriveddata-cache: true
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-
- 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-${{ github.ref_name }}-${{ 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-${{ github.ref_name }}-
- name: (Tests-Build) Install CocoaPods
run: pod install
shell: bash
- 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-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: Clean Derived Data (if required)
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/
make clean
xcodebuild clean
shell: bash
- name: (Tests-Build) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Tests-Build) List Files and derived data
shell: bash
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
shell: bash
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) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) List Files and Build artifacts
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-build-deriveddata-${{ inputs.short_commit }}.txt
path: tests-build-deriveddata.txt
- name: Encrypt tests-build-logs for upload
id: encrypt-test-log
if: always()
shell: bash
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-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
- 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-${{ inputs.short_commit }}.zip
path: encrypted-tests-build-logs.zip

View File

@@ -0,0 +1,235 @@
name: SideStore Tests Run
on:
workflow_call:
inputs:
release_tag:
type: string
short_commit:
type: string
secrets:
BUILD_LOG_ZIP_PASSWORD:
required: false
jobs:
tests-run:
name: Tests-Run SideStore - ${{ inputs.release_tag }}
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Boot Simulator async(nohup) for testing
run: |
mkdir -p build/logs
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.2'
# - name: (Tests-Run) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# # This comes from
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- 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-${{ github.ref_name }}-${{ 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-${{ github.ref_name }}-
- name: (Tests-Run) Install CocoaPods
run: pod install
shell: bash
- 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-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Tests-Run) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Tests-Run) List Files and derived data
shell: bash
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 <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-run-deriveddata-${{ inputs.short_commit }}.txt
path: tests-run-deriveddata.txt
# 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]}
shell: bash
- 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
shell: bash
- 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]}
shell: bash
- name: Stop Recording tests
if: ${{ always() && env.RECORD_PID != '' }}
run: |
kill -INT ${{ env.RECORD_PID }}
shell: bash
- 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 ""
shell: bash
- 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"
shell: bash
- 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-${{ inputs.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
shell: bash
- 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
shell: bash
- 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-${{ inputs.short_commit }}.mp4
path: tests-recording.mp4
- name: Zip test-results
run: zip -r -9 ./test-results.zip ./build/tests
shell: bash
- name: Upload Test Artifacts
uses: actions/upload-artifact@v4
with:
name: test-results-${{ inputs.short_commit }}.zip
path: test-results.zip

View File

@@ -7,97 +7,277 @@ on:
jobs:
build:
name: Build and upload SideStore
name: Build SideStore - stable (on tag push)
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-14'
version: '15.4'
- os: 'macos-15'
version: '26.0'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: brew install ldid
- name: Echo Build.xcconfig
run: |
echo "cat Build.xcconfig"
cat Build.xcconfig
shell: bash
- name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build
# run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Get version
- name: Echo Updated Build.xcconfig
run: |
cat Build.xcconfig
shell: bash
- name: Extract MARKETING_VERSION from Build.xcconfig
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
run: |
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
echo "version=$version" >> $GITHUB_OUTPUT
echo "version=$version"
- name: Echo version
run: echo "${{ steps.version.outputs.version }}"
echo "MARKETING_VERSION=$version" >> $GITHUB_ENV
echo "MARKETING_VERSION=$version" >> $GITHUB_OUTPUT
echo "MARKETING_VERSION=$version"
shell: bash
- name: Fail the build if pushed tag and embedded MARKETING_VERSION in Build.xcconfig are mismatching
run: |
if [ "$MARKETING_VERSION" != "${{ github.ref_name }}" ]; then
echo 'Version mismatch: $tag != $marketing_version ... '
echo " expected-tag : $MARKETING_VERSION"
echo " pushed-tag : ${{ github.ref_name }}"
exit 1
fi
echo 'Version matches: $tag == $marketing_version ... '
echo " expected-tag : $MARKETING_VERSION"
echo " pushed-tag : ${{ github.ref_name }}"
shell: bash
- name: Install dependencies - ldid & xcbeautify
run: |
brew install ldid xcbeautify
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: Cache Build
uses: irgaly/xcode-cache@v1
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }}
- name: Build SideStore
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-
- name: (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-build-stable-${{ hashFiles('Podfile') }}
- name: (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-build-stable-
- name: (Build) Install CocoaPods
run: pod install
shell: bash
- name: (Build) Save Pods to Cache
id: save-pods
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-stable-${{ hashFiles('Podfile') }}
- name: (Build) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Build) List Files and derived data
if: always()
shell: bash
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.xcarchive
# using 'tee' to intercept stdout and log for detailed build-log
run: |
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
shell: bash
- name: Fakesign app
run: make fakesign
run: make fakesign | tee -a build/logs/build.log
shell: bash
- name: Convert to IPA
run: make ipa
run: make ipa | tee -a build/logs/build.log
shell: bash
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to new stable release
uses: softprops/action-gh-release@v1
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
draft: true
files: SideStore.ipa
body: |
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
## Changelog
- TODO
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}`
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }}
- name: (Build) List Files and Build artifacts
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
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 ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
shell: bash
- 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"
shell: bash
- 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-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa
- name: Zip dSYMs
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
shell: bash
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
path: SideStore.dSYMs.zip
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
shell: bash
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
shell: bash
- name: Upload to releases
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
release: ${{ github.ref_name }} # name
tag: ${{ github.ref_name }}
# stick with what the user pushed, do not use latest commit or anything,
# ex: if we want to go back to previous release due to hot issue, dev can create a new tag pointing to that older working tag/commit so as to keep it as an update (to revert major issue)
# in this case we do not want the tag to be auto-updated to latest
updateTag: false
prerelease: false
files: >
SideStore.ipa
SideStore.dSYMs.zip
encrypted-build-logs.zip
body: |
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
## Changelog
- TODO
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}`

6
.gitmodules vendored
View File

@@ -30,7 +30,7 @@
url = https://github.com/rileytestut/Roxas.git
[submodule "Dependencies/libimobiledevice"]
path = Dependencies/libimobiledevice
url = https://github.com/libimobiledevice/libimobiledevice
url = https://github.com/SideStore/libimobiledevice
[submodule "Dependencies/libusbmuxd"]
path = Dependencies/libusbmuxd
url = https://github.com/libimobiledevice/libusbmuxd.git
@@ -50,10 +50,6 @@
path = SideStore/minimuxer
url = https://github.com/SideStore/minimuxer
branch = master
[submodule "SideStore/em_proxy"]
path = SideStore/em_proxy
url = https://github.com/SideStore/em_proxy
branch = master
[submodule "SideStore/libfragmentzip"]
path = SideStore/libfragmentzip
url = https://github.com/SideStore/libfragmentzip

View File

@@ -44,7 +44,6 @@
0EE7FDC92BE8D07400D1E390 /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; };
0EE7FDCB2BE8D12B00D1E390 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7FDC32BE8BC7900D1E390 /* ALTLocalizedError.swift */; };
0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */; };
19104DB52909C06D00C49C7B /* EmotionalDamage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19104DB42909C06D00C49C7B /* EmotionalDamage.swift */; };
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */; };
551A15E55999499418AC1022 /* Pods_AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 707E746318F0B6F1A44935D3 /* Pods_AltStoreCore.framework */; };
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A800F7032CE28E2F00208744 /* View+AltWidget.swift */; };
@@ -53,12 +52,20 @@
A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */; };
A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */; };
A809F69E2D04D7AC00F0F0F3 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F68E2D04D71200F0F0F3 /* libminimuxer_static.a */; };
A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */; };
A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; };
A809F6A92D04DA1900F0F0F3 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */; };
A80D60D32D3DD85100CEF65D /* ReleaseTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */; };
A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; };
A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */; };
A815AA952D90D2A100929A9E /* StoreApp17To17_1MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A815AA942D90D2A100929A9E /* StoreApp17To17_1MigrationPolicy.swift */; };
A815AA972D90E16400929A9E /* ReleaseTrack17To17_1MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A815AA962D90E16400929A9E /* ReleaseTrack17To17_1MigrationPolicy.swift */; };
A815AA992D90E5E500929A9E /* AltStore17ToAltStore17_1.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = A815AA982D90E5E500929A9E /* AltStore17ToAltStore17_1.xcmappingmodel */; };
A815AA9F2D9104DD00929A9E /* BuildInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A815AA9E2D9104DD00929A9E /* BuildInfo.swift */; };
A815AAA12D9108BC00929A9E /* AltStore11ToAltStore17_1.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = A815AAA02D9108BC00929A9E /* AltStore11ToAltStore17_1.xcmappingmodel */; };
A815AAA72D9108CB00929A9E /* ReleaseTrack11To17_1MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A815AAA42D9108CB00929A9E /* ReleaseTrack11To17_1MigrationPolicy.swift */; };
A815AAA82D9108CB00929A9E /* AppPermission11To17_1MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A815AAA52D9108CB00929A9E /* AppPermission11To17_1MigrationPolicy.swift */; };
A815AAA92D9108CB00929A9E /* Source11To17_1MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A815AAA22D9108CB00929A9E /* Source11To17_1MigrationPolicy.swift */; };
A815AAAA2D9108CB00929A9E /* StoreApp11To17_1MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A815AAA32D9108CB00929A9E /* StoreApp11To17_1MigrationPolicy.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 */; };
@@ -69,6 +76,8 @@
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 */; };
A81BF9E52D84CB0C00768940 /* AppPermission17To17_1MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81BF9E22D84CB0C00768940 /* AppPermission17To17_1MigrationPolicy.swift */; };
A81BF9E72D84CB0C00768940 /* Source17To17_1MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81BF9E32D84CB0C00768940 /* Source17To17_1MigrationPolicy.swift */; };
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; };
A8228B5B2D6E2C0C00F7CE0E /* (null) in Sources */ = {isa = PBXBuildFile; };
@@ -77,7 +86,6 @@
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 */; };
A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */; };
A881E7C72D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = A881E7C62D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel */; };
A881E7CB2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A881E7CA2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift */; };
A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */; };
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */; };
@@ -87,6 +95,9 @@
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */; };
A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; };
A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; };
A8B645FC2D70C10300125819 /* CollapsingMarkdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */; };
A8B645FF2D70C1AD00125819 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A8B645FE2D70C1AD00125819 /* MarkdownKit */; };
A8B646012D70C23E00125819 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A8B646002D70C23E00125819 /* MarkdownKit */; };
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */; };
A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */; };
@@ -100,13 +111,13 @@
A8C6D5142D1EE8D700DF01F1 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
A8C6D5182D1EE95B00DF01F1 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A8C924242D8E73B1009D9953 /* AltStore11ToAltStore17.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = A8C924232D8E73B1009D9953 /* AltStore11ToAltStore17.xcmappingmodel */; };
A8D484D82D0CD306002C691D /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = A8D484D72D0CD306002C691D /* AltBackup.ipa */; };
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 */; };
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; };
A8F838932D048E8F00ED425D /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; };
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF45872B2298D31600BD7491 /* libimobiledevice.a */; };
A8FD915E2D046F5200322782 /* UserInfoValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FD915D2D046F5200322782 /* UserInfoValue.swift */; };
@@ -116,7 +127,6 @@
A8FD917C2D0478D200322782 /* VerificationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CF56812A0D83F9006D93E2 /* VerificationError.swift */; };
B33FFBAA295F8F78002259E6 /* preboard.c in Sources */ = {isa = PBXBuildFile; fileRef = B33FFBA9295F8F78002259E6 /* preboard.c */; };
B33FFBAC295F8F98002259E6 /* companion_proxy.c in Sources */ = {isa = PBXBuildFile; fileRef = B33FFBAB295F8F98002259E6 /* companion_proxy.c */; };
B343F86D295F759E002B1159 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B343F86C295F759E002B1159 /* libresolv.tbd */; };
B343F87C295F7C5D002B1159 /* opack.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F872295F7C5C002B1159 /* opack.c */; };
B343F87D295F7C5D002B1159 /* cbuf.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F873295F7C5C002B1159 /* cbuf.c */; };
B343F87E295F7C5D002B1159 /* collection.c in Sources */ = {isa = PBXBuildFile; fileRef = B343F874295F7C5D002B1159 /* collection.c */; };
@@ -437,13 +447,6 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
19104DB92909C0F200C49C7B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = BFD247622284B9A500981D42 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 19104DB12909C06C00C49C7B;
remoteInfo = EmotionalDamage;
};
191E5FB2290A5D9B001A3B7C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = BFD247622284B9A500981D42 /* Project object */;
@@ -458,20 +461,6 @@
remoteGlobalIDString = CA609C732349C7AAD9FA67C4;
remoteInfo = "minimuxer-staticlib";
};
A809F6932D04D71200F0F0F3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B343F84D295F6323002B1159 /* em_proxy.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = CA60C44C93D7916DE57E6EBD;
remoteInfo = "em_proxy-staticlib";
};
A809F6952D04D71200F0F0F3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B343F84D295F6323002B1159 /* em_proxy.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = CA60058A9FBE4D17AF51A7D5;
remoteInfo = "run-bin";
};
A809F6A02D04D7CB00F0F0F3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */;
@@ -635,8 +624,6 @@
0EE7FDC22BE8BC4200D1E390 /* ALTWrappedError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWrappedError.h; sourceTree = "<group>"; };
0EE7FDC32BE8BC7900D1E390 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = "<group>"; };
0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = "<group>"; };
19104DB22909C06C00C49C7B /* libEmotionalDamage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libEmotionalDamage.a; sourceTree = BUILT_PRODUCTS_DIR; };
19104DB42909C06D00C49C7B /* EmotionalDamage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmotionalDamage.swift; sourceTree = "<group>"; };
191E5FAB290A5D92001A3B7C /* libminimuxer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libminimuxer.a; sourceTree = BUILT_PRODUCTS_DIR; };
19B9B7442845E6DF0076EF69 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = "<group>"; };
32741831F952989EC7E74FFA /* Pods-SideStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SideStore.debug.xcconfig"; path = "Target Support Files/Pods-SideStore/Pods-SideStore.debug.xcconfig"; sourceTree = "<group>"; };
@@ -657,6 +644,15 @@
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>"; };
A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationDataHolder.swift; sourceTree = "<group>"; };
A815AA942D90D2A100929A9E /* StoreApp17To17_1MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp17To17_1MigrationPolicy.swift; sourceTree = "<group>"; };
A815AA962D90E16400929A9E /* ReleaseTrack17To17_1MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseTrack17To17_1MigrationPolicy.swift; sourceTree = "<group>"; };
A815AA982D90E5E500929A9E /* AltStore17ToAltStore17_1.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore17ToAltStore17_1.xcmappingmodel; sourceTree = "<group>"; };
A815AA9E2D9104DD00929A9E /* BuildInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildInfo.swift; sourceTree = "<group>"; };
A815AAA02D9108BC00929A9E /* AltStore11ToAltStore17_1.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore11ToAltStore17_1.xcmappingmodel; sourceTree = "<group>"; };
A815AAA22D9108CB00929A9E /* Source11To17_1MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source11To17_1MigrationPolicy.swift; sourceTree = "<group>"; };
A815AAA32D9108CB00929A9E /* StoreApp11To17_1MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp11To17_1MigrationPolicy.swift; sourceTree = "<group>"; };
A815AAA42D9108CB00929A9E /* ReleaseTrack11To17_1MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseTrack11To17_1MigrationPolicy.swift; sourceTree = "<group>"; };
A815AAA52D9108CB00929A9E /* AppPermission11To17_1MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission11To17_1MigrationPolicy.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>"; };
@@ -664,6 +660,9 @@
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>"; };
A81BF9E12D84C9E900768940 /* AltStore 17_1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 17_1.xcdatamodel"; sourceTree = "<group>"; };
A81BF9E22D84CB0C00768940 /* AppPermission17To17_1MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission17To17_1MigrationPolicy.swift; sourceTree = "<group>"; };
A81BF9E32D84CB0C00768940 /* Source17To17_1MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source17To17_1MigrationPolicy.swift; 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>"; };
A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = "<group>"; };
@@ -676,8 +675,8 @@
A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorProcessing.swift; sourceTree = "<group>"; };
A868CFE32D319988002F1201 /* SingletonGenericMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingletonGenericMap.swift; sourceTree = "<group>"; };
A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppExtensionsOperation.swift; sourceTree = "<group>"; };
A881E7C62D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore11ToAltStore17.xcmappingmodel; sourceTree = "<group>"; };
A881E7CA2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp11To17MigrationPolicy.swift; sourceTree = "<group>"; };
A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DataStructureTests.xcconfig; sourceTree = "<group>"; };
A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingContolView.swift; sourceTree = "<group>"; };
A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingControl.swift; sourceTree = "<group>"; };
A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -685,12 +684,14 @@
A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfoManager.swift; sourceTree = "<group>"; };
A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = "<group>"; };
A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = "<group>"; };
A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsingMarkdownView.swift; sourceTree = "<group>"; };
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLog.swift; sourceTree = "<group>"; };
A8C38C282D206AC100E83DBD /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = "<group>"; };
A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractClassError.swift; sourceTree = "<group>"; };
A8C38C312D206B2500E83DBD /* FileOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOutputStream.swift; sourceTree = "<group>"; };
A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogView.swift; sourceTree = "<group>"; };
A8C924232D8E73B1009D9953 /* AltStore11ToAltStore17.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore11ToAltStore17.xcmappingmodel; 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>"; };
A8E2DB212D684CBD009E5D31 /* UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -699,14 +700,12 @@
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>"; };
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>"; };
A8FD915B2D046EF100322782 /* ProcessError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessError.swift; sourceTree = "<group>"; };
A8FD915D2D046F5200322782 /* UserInfoValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoValue.swift; sourceTree = "<group>"; };
A8FD917A2D0472DD00322782 /* DeprecatedAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedAPIs.swift; sourceTree = "<group>"; };
B33FFBA9295F8F78002259E6 /* preboard.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = preboard.c; path = src/preboard.c; sourceTree = "<group>"; };
B33FFBAB295F8F98002259E6 /* companion_proxy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = companion_proxy.c; path = src/companion_proxy.c; sourceTree = "<group>"; };
B343F84D295F6323002B1159 /* em_proxy.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = em_proxy.xcodeproj; path = SideStore/em_proxy.xcodeproj; sourceTree = SOURCE_ROOT; };
B343F86C295F759E002B1159 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk/usr/lib/libresolv.tbd; sourceTree = DEVELOPER_DIR; };
B343F872295F7C5C002B1159 /* opack.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = opack.c; sourceTree = "<group>"; };
B343F873295F7C5C002B1159 /* cbuf.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = cbuf.c; sourceTree = "<group>"; };
@@ -1062,15 +1061,6 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
19104DAF2909C06C00C49C7B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */,
B343F86D295F759E002B1159 /* libresolv.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
191E5FA8290A5D92001A3B7C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -1136,13 +1126,14 @@
files = (
A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */,
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */,
A8B646012D70C23E00125819 /* MarkdownKit in Frameworks */,
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */,
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */,
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */,
A8F838932D048E8F00ED425D /* libminimuxer.a in Frameworks */,
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */,
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */,
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
A8B645FF2D70C1AD00125819 /* MarkdownKit in Frameworks */,
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1162,14 +1153,6 @@
path = Errors;
sourceTree = "<group>";
};
19104DB32909C06D00C49C7B /* EmotionalDamage */ = {
isa = PBXGroup;
children = (
19104DB42909C06D00C49C7B /* EmotionalDamage.swift */,
);
path = EmotionalDamage;
sourceTree = "<group>";
};
191E5FF4290B2663001A3B7C /* libimobiledevice-glue */ = {
isa = PBXGroup;
children = (
@@ -1221,15 +1204,6 @@
name = Products;
sourceTree = "<group>";
};
A809F68F2D04D71200F0F0F3 /* Products */ = {
isa = PBXGroup;
children = (
A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */,
A809F6962D04D71200F0F0F3 /* run */,
);
name = Products;
sourceTree = "<group>";
};
A80D790B2D2F209700A40F40 /* Intents */ = {
isa = PBXGroup;
children = (
@@ -1239,6 +1213,17 @@
path = Intents;
sourceTree = "<group>";
};
A815AAA62D9108CB00929A9E /* v11-to-v17_1 */ = {
isa = PBXGroup;
children = (
A815AAA22D9108CB00929A9E /* Source11To17_1MigrationPolicy.swift */,
A815AAA32D9108CB00929A9E /* StoreApp11To17_1MigrationPolicy.swift */,
A815AAA42D9108CB00929A9E /* ReleaseTrack11To17_1MigrationPolicy.swift */,
A815AAA52D9108CB00929A9E /* AppPermission11To17_1MigrationPolicy.swift */,
);
path = "v11-to-v17_1";
sourceTree = "<group>";
};
A81A8CB22D68B2030086C96F /* UnitTests */ = {
isa = PBXGroup;
children = (
@@ -1269,10 +1254,31 @@
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */,
A85ACB932D1F31C400AA3DE7 /* AltWidgetExtension.xcconfig */,
A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */,
A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */,
);
path = xcconfigs;
sourceTree = "<group>";
};
A85E43732D8AF82100E89240 /* v11-to-v17 */ = {
isa = PBXGroup;
children = (
D5185B812AE1E71D00646E33 /* Source11To17MigrationPolicy.swift */,
A881E7CA2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift */,
);
path = "v11-to-v17";
sourceTree = "<group>";
};
A85E43742D8AF84200E89240 /* v17-to-v17_1 */ = {
isa = PBXGroup;
children = (
A81BF9E32D84CB0C00768940 /* Source17To17_1MigrationPolicy.swift */,
A815AA942D90D2A100929A9E /* StoreApp17To17_1MigrationPolicy.swift */,
A815AA962D90E16400929A9E /* ReleaseTrack17To17_1MigrationPolicy.swift */,
A81BF9E22D84CB0C00768940 /* AppPermission17To17_1MigrationPolicy.swift */,
);
path = "v17-to-v17_1";
sourceTree = "<group>";
};
A86315DD2D3EB2BD0048FA40 /* errors */ = {
isa = PBXGroup;
children = (
@@ -1344,6 +1350,22 @@
path = database;
sourceTree = "<group>";
};
A8B645F82D70C0DD00125819 /* Views */ = {
isa = PBXGroup;
children = (
A8B645FA2D70C0F600125819 /* UIKit */,
);
path = Views;
sourceTree = "<group>";
};
A8B645FA2D70C0F600125819 /* UIKit */ = {
isa = PBXGroup;
children = (
A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */,
);
path = UIKit;
sourceTree = "<group>";
};
A8C38C1C2D2068D100E83DBD /* Utils */ = {
isa = PBXGroup;
children = (
@@ -1369,6 +1391,7 @@
A8C38C272D206AA500E83DBD /* common */ = {
isa = PBXGroup;
children = (
A815AA9E2D9104DD00929A9E /* BuildInfo.swift */,
A8B516E52D2668020047047C /* DateTimeUtil.swift */,
A8C38C282D206AC100E83DBD /* OutputStream.swift */,
A8C38C312D206B2500E83DBD /* FileOutputStream.swift */,
@@ -1425,26 +1448,16 @@
A8F66C072D04C025009689E6 /* SideStore */ = {
isa = PBXGroup;
children = (
A8B645F82D70C0DD00125819 /* Views */,
A8E2DB352D6850A9009E5D31 /* Tests */,
A8F66C5C2D04D433009689E6 /* minimuxer */,
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */,
A8F66C412D04D433009689E6 /* em_proxy */,
B343F84D295F6323002B1159 /* em_proxy.xcodeproj */,
19104DB32909C06D00C49C7B /* EmotionalDamage */,
B343F886295F7F9B002B1159 /* libfragmentzip.xcodeproj */,
A8C38C1C2D2068D100E83DBD /* Utils */,
);
path = SideStore;
sourceTree = "<group>";
};
A8F66C412D04D433009689E6 /* em_proxy */ = {
isa = PBXGroup;
children = (
A8F66C3C2D04D433009689E6 /* em_proxy.h */,
);
path = em_proxy;
sourceTree = "<group>";
};
A8F66C5C2D04D433009689E6 /* minimuxer */ = {
isa = PBXGroup;
children = (
@@ -1796,10 +1809,11 @@
isa = PBXGroup;
children = (
BF66EEAF2501AECA007EE018 /* InstalledAppPolicy.swift */,
D5185B812AE1E71D00646E33 /* Source11To17MigrationPolicy.swift */,
BF66EEAE2501AECA007EE018 /* StoreAppPolicy.swift */,
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */,
A881E7CA2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift */,
A85E43732D8AF82100E89240 /* v11-to-v17 */,
A85E43742D8AF84200E89240 /* v17-to-v17_1 */,
A815AAA62D9108CB00929A9E /* v11-to-v17_1 */,
);
path = Policies;
sourceTree = "<group>";
@@ -1807,7 +1821,9 @@
BF66EEB02501AECA007EE018 /* Mapping Models */ = {
isa = PBXGroup;
children = (
A881E7C62D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel */,
A815AAA02D9108BC00929A9E /* AltStore11ToAltStore17_1.xcmappingmodel */,
A815AA982D90E5E500929A9E /* AltStore17ToAltStore17_1.xcmappingmodel */,
A8C924232D8E73B1009D9953 /* AltStore11ToAltStore17.xcmappingmodel */,
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */,
D5CA0C4D280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel */,
BFBF331A2526762200B7B8C9 /* AltStore8ToAltStore9.xcmappingmodel */,
@@ -1988,7 +2004,6 @@
BF58047B246A28F7008AE704 /* AltBackup.app */,
BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */,
BF989167250AABF3002ACF50 /* AltWidgetExtension.appex */,
19104DB22909C06C00C49C7B /* libEmotionalDamage.a */,
191E5FAB290A5D92001A3B7C /* libminimuxer.a */,
D586D39828EF58B0000E101F /* AltTests.xctest */,
A8E2DB212D684CBD009E5D31 /* UITests.xctest */,
@@ -2440,22 +2455,6 @@
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
19104DB12909C06C00C49C7B /* EmotionalDamage */ = {
isa = PBXNativeTarget;
buildConfigurationList = 19104DB62909C06D00C49C7B /* Build configuration list for PBXNativeTarget "EmotionalDamage" */;
buildPhases = (
19104DAE2909C06C00C49C7B /* Sources */,
19104DAF2909C06C00C49C7B /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = EmotionalDamage;
productName = EmotionalDamage;
productReference = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */;
productType = "com.apple.product-type.library.static";
};
191E5FAA290A5D92001A3B7C /* minimuxer */ = {
isa = PBXNativeTarget;
buildConfigurationList = 191E5FAF290A5D92001A3B7C /* Build configuration list for PBXNativeTarget "minimuxer" */;
@@ -2608,7 +2607,6 @@
dependencies = (
A8E00D3E2D0C95B5000DD2C7 /* PBXTargetDependency */,
191E5FB3290A5D9B001A3B7C /* PBXTargetDependency */,
19104DBA2909C0F200C49C7B /* PBXTargetDependency */,
BF66EE842501AE50007EE018 /* PBXTargetDependency */,
BF989176250AABF4002ACF50 /* PBXTargetDependency */,
);
@@ -2627,9 +2625,6 @@
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = SideStore;
TargetAttributes = {
19104DB12909C06C00C49C7B = {
CreatedOnToolsVersion = 14.0;
};
191E5FAA290A5D92001A3B7C = {
CreatedOnToolsVersion = 14.0;
};
@@ -2680,14 +2675,11 @@
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */,
D5FB7A2C2AA2859400EF863D /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */,
A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */,
);
productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = A809F68F2D04D71200F0F0F3 /* Products */;
ProjectRef = B343F84D295F6323002B1159 /* em_proxy.xcodeproj */;
},
{
ProductGroup = A8A543222D04F0C100D72399 /* Products */;
ProjectRef = B343F886295F7F9B002B1159 /* libfragmentzip.xcodeproj */;
@@ -2704,7 +2696,6 @@
BF58047A246A28F7008AE704 /* AltBackup */,
BF66EE7D2501AE50007EE018 /* AltStoreCore */,
BF989166250AABF3002ACF50 /* AltWidgetExtension */,
19104DB12909C06C00C49C7B /* EmotionalDamage */,
191E5FAA290A5D92001A3B7C /* minimuxer */,
A8E2DB202D684CBD009E5D31 /* UITests */,
A81A8CC42D68BA610086C96F /* DataStructureTests */,
@@ -2720,20 +2711,6 @@
remoteRef = A809F68D2D04D71200F0F0F3 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libem_proxy_static.a;
remoteRef = A809F6932D04D71200F0F0F3 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
A809F6962D04D71200F0F0F3 /* run */ = {
isa = PBXReferenceProxy;
fileType = "compiled.mach-o.executable";
path = run;
remoteRef = A809F6952D04D71200F0F0F3 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
A8A543292D04F0C100D72399 /* libfragmentzip */ = {
isa = PBXReferenceProxy;
fileType = "compiled.mach-o.executable";
@@ -2935,14 +2912,6 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
19104DAE2909C06C00C49C7B /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
19104DB52909C06D00C49C7B /* EmotionalDamage.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
191E5FA7290A5D92001A3B7C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -3072,6 +3041,7 @@
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */,
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */,
BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
A815AA972D90E16400929A9E /* ReleaseTrack17To17_1MigrationPolicy.swift in Sources */,
BF66EEA52501AEC5007EE018 /* Benefit.swift in Sources */,
D52B4ABF2AF183F0005991C3 /* WebViewController.swift in Sources */,
BF66EED22501AECA007EE018 /* AltStore4ToAltStore5.xcmappingmodel in Sources */,
@@ -3080,17 +3050,26 @@
BF66EECD2501AECA007EE018 /* StoreAppPolicy.swift in Sources */,
BF66EEE82501AED0007EE018 /* UserDefaults+AltStore.swift in Sources */,
BF340E9A250AD39500A192CB /* ViewApp.intentdefinition in Sources */,
A8C924242D8E73B1009D9953 /* AltStore11ToAltStore17.xcmappingmodel in Sources */,
D552EB062AF453F900A3AB4D /* URL+Normalized.swift in Sources */,
BFAECC522501B0A400528F27 /* CodableError.swift in Sources */,
A8FD915F2D046F5200322782 /* UserInfoValue.swift in Sources */,
D5F9821D2AB900060045751F /* AppScreenshot.swift in Sources */,
BF66EE9E2501AEC1007EE018 /* Fetchable.swift in Sources */,
BF66EEDF2501AECA007EE018 /* PatreonAccount.swift in Sources */,
A815AAA12D9108BC00929A9E /* AltStore11ToAltStore17_1.xcmappingmodel in Sources */,
BFAECC532501B0A400528F27 /* ServerProtocol.swift in Sources */,
A81BF9E52D84CB0C00768940 /* AppPermission17To17_1MigrationPolicy.swift in Sources */,
A81BF9E72D84CB0C00768940 /* Source17To17_1MigrationPolicy.swift in Sources */,
BFAECC572501B0A400528F27 /* ConnectionManager.swift in Sources */,
BF66EE9D2501AEC1007EE018 /* AppProtocol.swift in Sources */,
D519AD46292D665B004B12F9 /* Managed.swift in Sources */,
D52A2F972ACB40F700BDF8E3 /* Logger+AltStore.swift in Sources */,
A815AA952D90D2A100929A9E /* StoreApp17To17_1MigrationPolicy.swift in Sources */,
A815AAA72D9108CB00929A9E /* ReleaseTrack11To17_1MigrationPolicy.swift in Sources */,
A815AAA82D9108CB00929A9E /* AppPermission11To17_1MigrationPolicy.swift in Sources */,
A815AAA92D9108CB00929A9E /* Source11To17_1MigrationPolicy.swift in Sources */,
A815AAAA2D9108CB00929A9E /* StoreApp11To17_1MigrationPolicy.swift in Sources */,
BFC712C42512D5F100AB5EBE /* XPCConnection.swift in Sources */,
D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */,
BF66EE8C2501AEB2007EE018 /* Keychain.swift in Sources */,
@@ -3153,11 +3132,11 @@
BF66EECE2501AECA007EE018 /* InstalledAppPolicy.swift in Sources */,
BF1FE359251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift in Sources */,
BF66EEA62501AEC5007EE018 /* PatreonAPI.swift in Sources */,
A881E7C72D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel in Sources */,
BF66EED02501AECA007EE018 /* AltStore6ToAltStore7.xcmappingmodel in Sources */,
BF66EEDC2501AECA007EE018 /* MergePolicy.swift in Sources */,
BF66EEE22501AECA007EE018 /* InstalledExtension.swift in Sources */,
BF66EED62501AECA007EE018 /* NewsItem.swift in Sources */,
A815AA992D90E5E500929A9E /* AltStore17ToAltStore17_1.xcmappingmodel in Sources */,
BF66EEA72501AEC5007EE018 /* Campaign.swift in Sources */,
D52C8F032AFC56F000CA0BDD /* StoreCategory.swift in Sources */,
BF66EE992501AEBC007EE018 /* ALTSourceUserInfoKey.m in Sources */,
@@ -3290,6 +3269,7 @@
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */,
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */,
A8B645FC2D70C10300125819 /* CollapsingMarkdownView.swift in Sources */,
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */,
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
@@ -3305,6 +3285,7 @@
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */,
A815AA9F2D9104DD00929A9E /* BuildInfo.swift in Sources */,
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */,
@@ -3342,11 +3323,6 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
19104DBA2909C0F200C49C7B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 19104DB12909C06C00C49C7B /* EmotionalDamage */;
targetProxy = 19104DB92909C0F200C49C7B /* PBXContainerItemProxy */;
};
191E5FB3290A5D9B001A3B7C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 191E5FAA290A5D92001A3B7C /* minimuxer */;
@@ -3412,68 +3388,6 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
19104DB72909C06D00C49C7B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)";
CODE_SIGN_STYLE = Automatic;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_WARN_DUPLICATE_LIBRARIES = YES;
LD_WARN_UNUSED_DYLIBS = YES;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/SideStore/em_proxy",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
OTHER_LDFLAGS = (
"-ObjC",
"$(inherited)",
);
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OBJC_BRIDGING_HEADER = SideStore/em_proxy/em_proxy.h;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 15.0;
WATCHOS_DEPLOYMENT_TARGET = 7.0;
};
name = Debug;
};
19104DB82909C06D00C49C7B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_WARN_DUPLICATE_LIBRARIES = YES;
LD_WARN_UNUSED_DYLIBS = YES;
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/SideStore/em_proxy",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
OTHER_LDFLAGS = (
"-ObjC",
"$(inherited)",
);
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = SideStore/em_proxy/em_proxy.h;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 15.0;
WATCHOS_DEPLOYMENT_TARGET = 7.0;
};
name = Release;
};
191E5FB0290A5D92001A3B7C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -3532,6 +3446,7 @@
};
A81A8CCA2D68BA610086C96F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -3546,7 +3461,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.SideStore.SideStore.DataStructuresTests;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = NO;
@@ -3557,6 +3472,7 @@
};
A81A8CCB2D68BA610086C96F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -3573,7 +3489,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.SideStore.SideStore.DataStructuresTests;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_COMPILATION_MODE = wholemodule;
@@ -3600,7 +3516,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "--PRODUCT-BUNDLE-IDENTIFIER-.UITests";
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = NO;
@@ -3618,6 +3534,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -3626,7 +3543,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "--PRODUCT-BUNDLE-IDENTIFIER-.UITests";
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = NO;
@@ -4153,15 +4070,6 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
19104DB62909C06D00C49C7B /* Build configuration list for PBXNativeTarget "EmotionalDamage" */ = {
isa = XCConfigurationList;
buildConfigurations = (
19104DB72909C06D00C49C7B /* Debug */,
19104DB82909C06D00C49C7B /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
191E5FAF290A5D92001A3B7C /* Build configuration list for PBXNativeTarget "minimuxer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -4249,9 +4157,17 @@
A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SwiftPackageIndex/SemanticVersion";
requirement = {
kind = exactVersion;
version = 0.4.0;
};
};
A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/bmoliveira/MarkdownKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.4.0;
minimumVersion = 1.7.1;
};
};
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = {
@@ -4278,6 +4194,16 @@
package = A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */;
productName = SemanticVersion;
};
A8B645FE2D70C1AD00125819 /* MarkdownKit */ = {
isa = XCSwiftPackageProductDependency;
package = A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */;
productName = MarkdownKit;
};
A8B646002D70C23E00125819 /* MarkdownKit */ = {
isa = XCSwiftPackageProductDependency;
package = A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */;
productName = MarkdownKit;
};
A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */ = {
isa = XCSwiftPackageProductDependency;
productName = "AltSign-Static";
@@ -4292,6 +4218,7 @@
BF66EEB72501AECA007EE018 /* AltStore.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
A81BF9E12D84C9E900768940 /* AltStore 17_1.xcdatamodel */,
D51E83802B86926B0092FC61 /* AltStore 17.xcdatamodel */,
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */,
D5CA0C4C280E242500469595 /* AltStore 10.xcdatamodel */,
@@ -4305,7 +4232,7 @@
BF66EEBD2501AECA007EE018 /* AltStore 2.xcdatamodel */,
BF66EEB92501AECA007EE018 /* AltStore.xcdatamodel */,
);
currentVersion = D51E83802B86926B0092FC61 /* AltStore 17.xcdatamodel */;
currentVersion = A81BF9E12D84C9E900768940 /* AltStore 17_1.xcdatamodel */;
path = AltStore.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View File

@@ -43,8 +43,10 @@ final class AppContentViewController: UITableViewController
}()
@IBOutlet private var subtitleLabel: UILabel!
@IBOutlet private var descriptionTextView: CollapsingTextView!
@IBOutlet private var versionDescriptionTextView: CollapsingTextView!
// @IBOutlet private var descriptionTextView: CollapsingTextView!
@IBOutlet private var descriptionTextView: CollapsingMarkdownView!
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
@IBOutlet private var versionLabel: UILabel!
@IBOutlet private var versionDateLabel: UILabel!
@IBOutlet private var sizeLabel: UILabel!
@@ -55,35 +57,32 @@ final class AppContentViewController: UITableViewController
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
override func viewDidLoad()
{
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.contentInset.bottom = 20
self.subtitleLabel.text = self.app.subtitle
self.descriptionTextView.text = self.app.localizedDescription
let desc = self.app.localizedDescription
self.descriptionTextView.text = desc
if let version = self.app.latestAvailableVersion
{
self.versionDescriptionTextView.text = version.localizedDescription
if let version = self.app.latestAvailableVersion {
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
}
else
{
self.versionDescriptionTextView.text = nil
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: version.size, countStyle: .file)
} else {
self.versionDescriptionTextView.text = "nil"
self.versionLabel.text = nil
self.versionDateLabel.text = nil
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
}
self.descriptionTextView.maximumNumberOfLines = 5
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.versionDescriptionTextView.maximumNumberOfLines = 5
self.versionDescriptionTextView.maximumNumberOfLines = 3
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.descriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
}
override func viewDidLayoutSubviews()
@@ -162,8 +161,12 @@ private extension AppContentViewController
switch sender
{
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
case self.descriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
default: return
}

View File

@@ -14,7 +14,6 @@ import Intents
import AltStoreCore
import AltSign
import Roxas
import EmotionalDamage
import Nuke
@@ -27,10 +26,12 @@ extension AppDelegate
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
static let exportCertificateNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ExportCertificateNotification")
static let importAppDeepLinkURLKey = "fileURL"
static let appBackupResultKey = "result"
static let addSourceDeepLinkURLKey = "sourceURL"
static let exportCertificateCallbackTemplateKey = "callback"
}
@UIApplicationMain
@@ -139,7 +140,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillEnterForeground(_ application: UIApplication)
{
AppManager.shared.update()
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
PatreonAPI.shared.refreshPatreonAccount()
}
@@ -292,6 +292,26 @@ private extension AppDelegate
return true
case "pairing":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let callbackTemplate = queryItems["urlName"]?.removingPercentEncoding else { return false }
DispatchQueue.main.async {
exportPairingFile(callbackTemplate)
}
return true
case "certificate":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let callbackTemplate = queryItems["callback_template"]?.removingPercentEncoding else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.exportCertificateNotification, object: nil, userInfo: [AppDelegate.exportCertificateCallbackTemplateKey: callbackTemplate])
}
return true
default: return false
}
}

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<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">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -14,7 +14,7 @@
<objects>
<navigationController storyboardIdentifier="navigationController" id="ZTo-53-dSL" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="Aej-RF-PfV" customClass="NavigationBar" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" name="SettingsBackground"/>
@@ -42,13 +42,13 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyW-Fd-ojD" userLabel="Sizing View">
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
</view>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" indicatorStyle="white" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="WXx-hX-AXv">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2wp-qG-f0Z">
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="603"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" spacing="50" translatesAutoresizingMaskIntoConstraints="NO" id="YmX-7v-pxh">
<rect key="frame" x="16" y="6" width="343" height="359.5"/>
@@ -57,13 +57,13 @@
<rect key="frame" x="0.0" y="0.0" width="343" height="67.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome to SideStore." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="EI2-V3-zQZ">
<rect key="frame" x="0.0" y="0.0" width="333.5" height="41"/>
<rect key="frame" x="0.0" y="0.0" width="343" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sign in with your Apple ID to get started." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="SNU-tv-8Au">
<rect key="frame" x="0.0" y="47" width="308.5" height="20.5"/>
<rect key="frame" x="0.0" y="47" width="306.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -160,7 +160,7 @@
</stackView>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2N5-zd-fUj">
<rect key="frame" x="0.0" y="191" width="343" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
@@ -179,7 +179,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="DBk-rT-ZE8">
<rect key="frame" x="16" y="518.5" width="343" height="96.5"/>
<rect key="frame" x="16" y="498.5" width="343" height="96.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Why do we need this?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p9U-0q-Kn8">
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
@@ -198,6 +198,10 @@
</stackView>
</subviews>
<constraints>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
<constraint firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
<constraint firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="YmX-7v-pxh" secondAttribute="bottom" constant="8" symbolic="YES" id="zTU-eY-DWd"/>
</constraints>
</view>
@@ -215,19 +219,15 @@
<constraints>
<constraint firstAttribute="bottom" secondItem="WXx-hX-AXv" secondAttribute="bottom" id="0jL-Ky-ju6"/>
<constraint firstAttribute="leadingMargin" secondItem="YmX-7v-pxh" secondAttribute="leading" id="2PO-lG-dmB"/>
<constraint firstItem="DBk-rT-ZE8" firstAttribute="leading" secondItem="2wp-qG-f0Z" secondAttribute="leadingMargin" id="5AT-nV-ZP9"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="top" secondItem="zMn-DV-fpy" secondAttribute="top" id="730-db-ukB"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="bottomMargin" secondItem="DBk-rT-ZE8" secondAttribute="bottom" id="HgY-oY-8KM"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="oyW-Fd-ojD" secondAttribute="trailing" id="KGE-CN-SWf"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="top" secondItem="mjy-4S-hyH" secondAttribute="top" id="LPQ-bF-ic0"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="trailing" secondItem="WXx-hX-AXv" secondAttribute="trailing" id="MG7-A6-pKp"/>
<constraint firstAttribute="trailingMargin" secondItem="YmX-7v-pxh" secondAttribute="trailing" id="O4T-nu-o3e"/>
<constraint firstItem="zMn-DV-fpy" firstAttribute="bottom" secondItem="oyW-Fd-ojD" secondAttribute="bottom" id="PuX-ab-cEq"/>
<constraint firstItem="oyW-Fd-ojD" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="SzC-gC-Nvi"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="trailingMargin" secondItem="DBk-rT-ZE8" secondAttribute="trailing" id="VCf-bW-2K4"/>
<constraint firstItem="WXx-hX-AXv" firstAttribute="leading" secondItem="zMn-DV-fpy" secondAttribute="leading" id="d08-zF-5X6"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="height" secondItem="oyW-Fd-ojD" secondAttribute="height" id="dFN-pw-TWt"/>
<constraint firstItem="YmX-7v-pxh" firstAttribute="top" secondItem="2wp-qG-f0Z" secondAttribute="top" constant="6" id="iUr-Nd-tkt"/>
<constraint firstItem="2wp-qG-f0Z" firstAttribute="width" secondItem="oyW-Fd-ojD" secondAttribute="width" id="rYO-GN-0Lk"/>
</constraints>
</view>
@@ -264,7 +264,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="bp6-55-IG2">
<rect key="frame" x="0.0" y="44" width="375" height="564"/>
<rect key="frame" x="0.0" y="64" width="375" height="544"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="FjP-tm-w7K">
<rect key="frame" x="16" y="35" width="343" height="95.5"/>
@@ -298,7 +298,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="LpI-Jt-SzX">
<rect key="frame" x="16" y="168" width="343" height="95.5"/>
<rect key="frame" x="16" y="161" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0LW-eE-qHa">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -310,7 +310,7 @@
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="dMu-eg-gIO">
<rect key="frame" x="79" y="15.5" width="264" height="64"/>
<rect key="frame" x="79" y="16" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connect to Wi-Fi and VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="esj-pD-D4A">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -318,7 +318,7 @@
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable SideStore VPN in Wireguard and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable StosVPN and be able to use Sidestore on the go." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="4rk-ge-FSj">
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -329,7 +329,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="tfb-ja-9UC">
<rect key="frame" x="16" y="300.5" width="343" height="95.5"/>
<rect key="frame" x="16" y="287.5" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="3" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nVr-El-Csi">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -341,7 +341,7 @@
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="z6Y-zi-teL">
<rect key="frame" x="79" y="16" width="264" height="64"/>
<rect key="frame" x="79" y="15.5" width="264" height="64"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Download Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JeJ-bk-UCA">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -360,7 +360,7 @@
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="X3r-G1-vf2">
<rect key="frame" x="16" y="433.5" width="343" height="95.5"/>
<rect key="frame" x="16" y="413.5" width="343" height="95.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="4" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i2U-NL-plG">
<rect key="frame" x="0.0" y="0.0" width="59" height="95.5"/>
@@ -372,7 +372,7 @@
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Xs6-pJ-PUz">
<rect key="frame" x="79" y="16" width="264" height="64"/>
<rect key="frame" x="79" y="17" width="264" height="62"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps Refresh Automatically" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="nvb-Aq-sYa">
<rect key="frame" x="0.0" y="0.0" width="264" height="20.5"/>
@@ -381,7 +381,7 @@
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Apps are refreshed in the background while you are on SideStore VPN!" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="HU5-Hv-E3d">
<rect key="frame" x="0.0" y="25.5" width="264" height="38.5"/>
<rect key="frame" x="0.0" y="25.5" width="264" height="36.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -431,7 +431,7 @@
</objects>
<point key="canvasLocation" x="1353" y="736"/>
</scene>
<!--Refresh AltStore-->
<!--Refresh SideStore-->
<scene sceneID="9Vh-dM-OqX">
<objects>
<viewController storyboardIdentifier="refreshAltStoreViewController" id="aoK-yE-UVT" customClass="RefreshAltStoreViewController" customModule="SideStore" customModuleProvider="target" sceneMemberID="viewController">
@@ -485,7 +485,7 @@
<constraint firstItem="tDQ-ao-1Jg" firstAttribute="leading" secondItem="R83-kV-365" secondAttribute="leadingMargin" id="zEt-Xr-kJx"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Refresh AltStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
<navigationItem key="navigationItem" title="Refresh SideStore" largeTitleDisplayMode="always" id="5nk-NR-jtV"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="placeholderView" destination="fpO-Bf-gFY" id="q7d-au-d94"/>

View File

@@ -287,7 +287,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="20" y="20" width="335" height="34"/>
<color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
@@ -353,7 +353,7 @@
</stackView>
</subviews>
</stackView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="20" y="59.5" width="335" height="34"/>
<color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>

View File

@@ -13,21 +13,18 @@ final class CollapsingTextView: UITextView
var isCollapsed = true {
didSet {
guard self.isCollapsed != oldValue else { return }
self.shouldResetLayout = true
self.setNeedsLayout()
}
}
var maximumNumberOfLines = 2 {
didSet {
self.shouldResetLayout = true
self.setNeedsLayout()
}
}
var lineSpacing: Double = 2 {
didSet {
self.shouldResetLayout = true
if #available(iOS 16, *)
{
@@ -42,7 +39,6 @@ final class CollapsingTextView: UITextView
override var text: String! {
didSet {
self.shouldResetLayout = true
guard #available(iOS 16, *) else { return }
self.updateText()
@@ -51,9 +47,6 @@ final class CollapsingTextView: UITextView
let moreButton = UIButton(type: .system)
private var shouldResetLayout: Bool = false
private var previousSize: CGSize?
override init(frame: CGRect, textContainer: NSTextContainer?)
{
super.init(frame: frame, textContainer: textContainer)
@@ -115,45 +108,39 @@ final class CollapsingTextView: UITextView
height: font.lineHeight)
self.moreButton.frame = moreButtonFrame
if self.shouldResetLayout || self.previousSize != self.bounds.size
if self.isCollapsed
{
if self.isCollapsed
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
self.moreButton.isHidden = false
}
else
{
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
self.moreButton.isHidden = false
}
else
{
self.textContainer.maximumNumberOfLines = 0
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
}
else
{
self.textContainer.maximumNumberOfLines = 0
self.textContainer.exclusionPaths = []
self.invalidateIntrinsicContentSize()
self.moreButton.isHidden = true
}
self.shouldResetLayout = false
self.previousSize = self.bounds.size
self.invalidateIntrinsicContentSize()
}
}

View File

@@ -8,7 +8,6 @@
import UIKit
import Roxas
import EmotionalDamage
import minimuxer
import WidgetKit
@@ -88,7 +87,6 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
#if !targetEnvironment(simulator)
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else {
displayError("Device pairing file not found.")
@@ -177,7 +175,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
//Add "help" button to take user to wiki
let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in
let wikiURL: String = "https://docs.sidestore.io/docs/getting-started/pairing-file"
let wikiURL: String = "https://docs.sidestore.io/docs/installation/pairing-file"
if let url = URL(string: wikiURL) {
UIApplication.shared.open(url)
}
@@ -255,13 +253,7 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
}
if #available(iOS 17, *) {
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
}
else {
start_auto_mounter(documentsDirectory)
}
start_auto_mounter(documentsDirectory)
// Create destinationViewController now so view controllers can register for receiving Notifications.
self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController
}

View File

@@ -1519,6 +1519,24 @@ private extension AppManager
patchAppOperation.addDependency(deactivateAppsOperation)
let modifyAppExBundleIdOperation = RSTAsyncBlockOperation { operation in
if !context.useMainProfile {
operation.finish()
return
}
if let app = context.app, let profile = context.provisioningProfiles?[app.bundleIdentifier] {
var appexBundleIds: [String: String] = [:]
for appex in app.appExtensions {
appexBundleIds[appex.bundleIdentifier] = appex.bundleIdentifier.replacingOccurrences(of: app.bundleIdentifier, with: profile.bundleIdentifier)
}
context.appexBundleIds = appexBundleIds
}
operation.finish()
}
modifyAppExBundleIdOperation.addDependency(fetchProvisioningProfilesOperation)
/* Resign */
let resignAppOperation = ResignAppOperation(context: context)
resignAppOperation.resultHandler = { (result) in
@@ -1533,6 +1551,7 @@ private extension AppManager
}
}
resignAppOperation.addDependency(patchAppOperation)
resignAppOperation.addDependency(modifyAppExBundleIdOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
@@ -1586,6 +1605,7 @@ private extension AppManager
patchAppOperation,
refreshAnisetteDataOperation,
fetchProvisioningProfilesOperation,
modifyAppExBundleIdOperation,
resignAppOperation,
sendAppOperation,
installOperation
@@ -1665,8 +1685,8 @@ private extension AppManager
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.app = ALTApplication(fileURL: app.fileURL)
// Since this doesn't involve modifying app bundle which will cause re-install, this is safe in refresh path
context.useMainProfile = app.useMainProfile
// Since this doesn't involve modifying app bundle which will cause re-install, this is safe in refresh path
//App-Extensions: Ensure DB data and disk state must match
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
let diskAppEx: Set<ALTApplication> = Set(context.app!.appExtensions)

View File

@@ -166,9 +166,11 @@ class MyAppsViewController: UICollectionViewController, PeekPopPreviewing
@IBAction func unwindToMyAppsViewController(_ segue: UIStoryboardSegue)
{
}
var minimuxerStatus: Bool {
guard minimuxer.ready() else {
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No WiFi or VPN!")).show(in: self)
// added isMinimuxerStatusCheckEnabled to forcefully ignore minimuxer status if status check is disabled in settings
guard !UserDefaults.standard.isMinimuxerStatusCheckEnabled || minimuxer.ready() else {
ToastView(error: (OperationError.noWiFi as NSError).withLocalizedTitle("No Wi-Fi or VPN!")).show(in: self)
return false
}
return true
@@ -235,7 +237,8 @@ private extension MyAppsViewController
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = app.tintColor ?? .altPrimary
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
cell.versionDescriptionTextView.maximumNumberOfLines = 2
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription ?? "nil"
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
@@ -281,12 +284,9 @@ private extension MyAppsViewController
cell.mode = .collapsed
}
cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
cell.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
cell.setNeedsLayout()
// Below lines are necessary to avoid "more" button layout issues.
cell.versionDescriptionTextView.setNeedsLayout()
cell.layoutIfNeeded()
}
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
@@ -724,22 +724,29 @@ private extension MyAppsViewController
let cell = self.collectionView.cellForItem(at: indexPath) as? UpdateCollectionViewCell
// Toggle the state
if self.expandedAppUpdates.contains(installedApp.bundleIdentifier)
{
self.expandedAppUpdates.remove(installedApp.bundleIdentifier)
// Set collapsed mode on the cell
cell?.mode = .collapsed
}
else
{
self.expandedAppUpdates.insert(installedApp.bundleIdentifier)
// Set expanded mode on the cell
cell?.mode = .expanded
}
// Clear cached size so it's recalculated
self.cachedUpdateSizes[installedApp.bundleIdentifier] = nil
self.collectionView.performBatchUpdates({
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
// Animate the change smoothly with a duration
UIView.animate(withDuration: 0.25) {
self.collectionView.performBatchUpdates({
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
}
}
@IBAction func refreshApp(_ sender: UIButton)
@@ -1480,15 +1487,6 @@ private extension MyAppsViewController
guard minimuxerStatus else { return }
}
if #available(iOS 17, *), !sidejitenabled {
let error = OperationError.tooNewError as NSError
let localizedError = error.withLocalizedTitle("No iOS 17 On Device JIT!")
ToastView(error: localizedError, opensLog: true).show(in: self)
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
return
}
AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async {
switch result {

View File

@@ -21,12 +21,19 @@ extension UpdateCollectionViewCell
{
var mode: Mode = .expanded {
didSet {
self.update()
switch self.mode {
case .collapsed:
self.versionDescriptionTextView.isCollapsed = true
case .expanded:
self.versionDescriptionTextView.isCollapsed = false
}
self.setNeedsLayout()
}
}
@IBOutlet var bannerView: AppBannerView!
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
// @IBOutlet var versionDescriptionTextView: CollapsingTextView!
@IBOutlet var versionDescriptionTextView: CollapsingMarkdownView!
@IBOutlet private var blurView: UIVisualEffectView!
@@ -85,16 +92,16 @@ extension UpdateCollectionViewCell
}
}
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
{
// Ensure cell is laid out so it will report correct size.
self.versionDescriptionTextView.setNeedsLayout()
self.versionDescriptionTextView.layoutIfNeeded()
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
return size
}
// override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
// {
// // Ensure cell is laid out so it will report correct size.
// self.versionDescriptionTextView.setNeedsLayout()
// self.versionDescriptionTextView.layoutIfNeeded()
//
// let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
//
// return size
// }
}
private extension UpdateCollectionViewCell

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -11,7 +11,7 @@
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
@@ -30,7 +30,7 @@
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
@@ -39,7 +39,7 @@
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
<rect key="frame" x="0.0" y="50" width="343" height="75"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="15" y="0.0" width="313" height="26"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -91,7 +91,7 @@
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -10,7 +10,6 @@ import UIKit
import CoreData
import AltStoreCore
import EmotionalDamage
import minimuxer
typealias RefreshError = RefreshErrorCode.Error
@@ -99,7 +98,6 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
self.finish(.failure(RefreshError(.noInstalledApps)))
return
}
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {

View File

@@ -220,16 +220,16 @@ struct OperationError: ALTLocalizedError {
case .openAppFailed:
let appName = self.appName ?? NSLocalizedString("The app", comment: "")
return String(format: NSLocalizedString("SideStore was denied permission to launch %@.", comment: ""), appName)
case .noWiFi: return NSLocalizedString("You do not appear to be connected to WiFi and/or the WireGuard VPN!\nSideStore will never be able to install or refresh applications without WiFi and the WireGuard VPN.", comment: "")
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled therefore SideStore cannot enable it without SideJITServer at this time, sorry for any inconvenience.\nWe will let everyone know once we have a solution!", comment: "")
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer Please check that you are on the Same Wi-Fi and your Firewall has been set correctly", comment: "")
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice Please make sure you have paired your Device by doing 'SideJITServer -y' or try Refreshing SideJITServer from Settings", comment: "")
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP Please make sure that you are on the Samw Wifi as SideJITServer", comment: "")
case .refreshsidejit: return NSLocalizedString("Unable to find App Please try Refreshing SideJITServer from Settings", comment: "")
case .anisetteV1Error: return NSLocalizedString("An error occurred when getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
case .provisioningError: return NSLocalizedString("An error occurred when provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .anisetteV3Error: return NSLocalizedString("An error occurred when getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .cacheClearError: return NSLocalizedString("An error occurred while clearing cache: %@", comment: "")
case .noWiFi: return NSLocalizedString("You do not appear to be connected to Wi-Fi and/or StosVPN!\nSideStore will never be able to install or refresh applications without Wi-Fi and StosVPN.", comment: "")
case .tooNewError: return NSLocalizedString("iOS 17 has changed how JIT is enabled, therefore, SideStore cannot enable JIT without SideJITServer, StikDebug, or SideStore-nightly at this time, sorry for any inconvenience.", comment: "")
case .unableToConnectSideJIT: return NSLocalizedString("Unable to connect to SideJITServer. Please check that you are on the same Wi-Fi of and your Firewall has been set correctly on your server", comment: "")
case .unableToRespondSideJITDevice: return NSLocalizedString("SideJITServer is unable to connect to your iDevice. Please make sure you have paired your Device by running 'SideJITServer -y', or try refreshing SideJITServer from Settings", comment: "")
case .wrongSideJITIP: return NSLocalizedString("Incorrect SideJITServer IP. Please make sure that you are on the same Wi-Fi as SideJITServer", comment: "")
case .refreshsidejit: return NSLocalizedString("Unable to find app; Please try refreshing SideJITServer from Settings", comment: "")
case .anisetteV1Error: return NSLocalizedString("An error occurred while getting anisette data from a V1 server: %@. Try using another anisette server.", comment: "")
case .provisioningError: return NSLocalizedString("An error occurred while provisioning: %@ %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .anisetteV3Error: return NSLocalizedString("An error occurred while getting anisette data from a V3 server: %@. Please try again. If the issue persists, report it on GitHub Issues!", comment: "")
case .cacheClearError: return NSLocalizedString("An error occurred while clearing the cache: %@", comment: "")
case .SideJITIssue: return NSLocalizedString("An error occurred while using SideJIT: %@", comment: "")
case .refreshAppFailed:
@@ -260,7 +260,7 @@ struct OperationError: ALTLocalizedError {
var recoverySuggestion: String? {
switch self.code
{
case .noWiFi: return NSLocalizedString("Make sure the VPN is toggled on and you are connected to any WiFi network!", comment: "")
case .noWiFi: return NSLocalizedString("Make sure StosVPN is toggled on and you are connected to any Wi-Fi network!", comment: "")
case .serverNotFound: return NSLocalizedString("Make sure you're on the same Wi-Fi network as a computer running AltServer, or try connecting this device to your computer via USB.", comment: "")
case .maximumAppIDLimitReached:
let baseMessage = NSLocalizedString("Delete sideloaded apps to free up App ID slots.", comment: "")
@@ -308,9 +308,9 @@ extension MinimuxerError: LocalizedError {
case .NoDevice:
return NSLocalizedString("Cannot fetch the device from the muxer", comment: "")
case .NoConnection:
return NSLocalizedString("Unable to connect to the device, make sure Wireguard is enabled and you're connected to WiFi. This could mean an invalid pairing.", comment: "")
return NSLocalizedString("Unable to connect to the device, make sure StosVPN is enabled and you're connected to Wi-Fi. This could mean an invalid pairing.", comment: "")
case .PairingFile:
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use jitterbugpair to generate it", comment: "")
return NSLocalizedString("Invalid pairing file. Your pairing file either didn't have a UDID, or it wasn't a valid plist. Please use idevice_pair to regenerate it", comment: "")
case .CreateDebug:
return self.createService(name: "debug")
@@ -350,6 +350,38 @@ extension MinimuxerError: LocalizedError {
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .ProfileRemove:
return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .CreateLockdown:
return NSLocalizedString("Unable to connect to lockdown", comment: "")
case .CreateCoreDevice:
return NSLocalizedString("Unable to connect to core device proxy", comment: "")
case .CreateSoftwareTunnel:
return NSLocalizedString("Unable to create software tunnel", comment: "")
case .CreateRemoteServer:
return NSLocalizedString("Unable to connect to remote server", comment: "")
case .CreateProcessControl:
return NSLocalizedString("Unable to connect to process control", comment: "")
case .GetLockdownValue:
return NSLocalizedString("Unable to get value from lockdown", comment: "")
case .Connect:
return NSLocalizedString("Unable to connect to TCP port", comment: "")
case .Close:
return NSLocalizedString("Unable to close TCP port", comment: "")
case .XpcHandshake:
return NSLocalizedString("Unable to get services from XPC", comment: "")
case .NoService:
return NSLocalizedString("Device did not contain service", comment: "")
case .InvalidProductVersion:
return NSLocalizedString("Service version was in an unexpected format", comment: "")
case .CreateFolder:
return NSLocalizedString("Unable to create DDI folder", comment: "")
case .DownloadImage:
return NSLocalizedString("Unable to download DDI", comment: "")
case .ImageLookup:
return NSLocalizedString("Unable to lookup DDI images", comment: "")
case .ImageRead:
return NSLocalizedString("Unable to read images to memory", comment: "")
case .Mount:
return NSLocalizedString("Mount failed", comment: "")
}
}

View File

@@ -67,20 +67,22 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
let dispatchGroup = DispatchGroup()
for appExtension in app.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
if !self.context.useMainProfile {
for appExtension in app.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
self.progress.completedUnitCount += 1
}
dispatchGroup.leave()
self.progress.completedUnitCount += 1
}
}
@@ -360,13 +362,6 @@ extension FetchProvisioningProfilesOperation
}
}
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
override init(context: AppOperationContext)
{
super.init(context: context)
}
}
class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{
override init(context: AppOperationContext)
{
@@ -613,3 +608,14 @@ class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperat
}
}
}
// <TEST> : users were reporting that refresh (though seemed like it refreshed the app becomes no longer available)
// possibly, this is caused since refesh was not updating appFeatures and AppGroups in the new profile? not sure.
// for now we are reverting by keeping same operation that happens during fetch in install path to see if it fixes issue #893
// class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesInstallOperation, @unchecked Sendable {
override init(context: AppOperationContext)
{
super.init(context: context)
}
}

View File

@@ -11,6 +11,7 @@ import CoreData
import AltStoreCore
import Roxas
import SemanticVersion
@objc(FetchSourceOperation)
final class FetchSourceOperation: ResultOperation<Source>
@@ -246,9 +247,20 @@ private extension FetchSourceOperation
#endif
}
if let previousSourceID = self.$source.identifier
let incomingSourceID = source.identifier
if let previousSourceID = self.$source.identifier,
incomingSourceID != previousSourceID
{
guard source.identifier == previousSourceID else { throw SourceError.changedID(source.identifier, previousID: previousSourceID, source: source) }
// if let version = BuildInfo().marketing_version,
// SemanticVersion(version)! <= SemanticVersion("0.6.1")!
// {
// // delete the source, so that incoming will be saved.
// self.source?.managedObjectContext?.delete(self.source!)
// }
// else
// {
throw SourceError.changedID(source.identifier, previousID: self.$source.identifier ?? "nil", source: source)
// }
}
}

View File

@@ -72,6 +72,8 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
}
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber, storeBuildVersion: storeBuildVersion)
installedApp.useMainProfile = self.context.useMainProfile
installedApp.needsResign = false
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)

View File

@@ -66,6 +66,8 @@ class AppOperationContext
var app: ALTApplication?
var provisioningProfiles: [String: ALTProvisioningProfile]?
var appexBundleIds: [String: String]?
var useMainProfile = false
var isFinished = false

View File

@@ -136,7 +136,11 @@ final class RemoveAppExtensionsOperation: ResultOperation<Void>
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
self.finish(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions (Use Main Profile)", comment: ""), style: .default) { (action) in
self.context.useMainProfile = true
self.finish(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions (Register App ID for Each Extension)", comment: ""), style: .default) { (action) in
self.finish(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in

View File

@@ -55,7 +55,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles, appexBundleIds: context.appexBundleIds ?? [:]) { (result) in
guard let appBundleURL = self.process(result) else { return }
// Resign app bundle
@@ -107,7 +107,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
private extension ResignAppOperation
{
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], appexBundleIds: [String: String], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
@@ -119,10 +119,15 @@ private extension ResignAppOperation
func prepare(_ bundle: Bundle, additionalInfoDictionaryValues: [String: Any] = [:]) throws
{
guard let identifier = bundle.bundleIdentifier else { throw ALTError(.missingAppBundle) }
guard let profile = profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
guard let profile = context.useMainProfile ? profiles.values.first : profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
if let forcedBundleIdentifier = appexBundleIds[identifier] {
infoDictionary[kCFBundleIdentifierKey as String] = forcedBundleIdentifier
} else {
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
}
infoDictionary[Bundle.Info.altBundleID] = identifier
infoDictionary[Bundle.Info.devicePairingString] = "<insert pairing file here>"
infoDictionary.removeValue(forKey: "DTXcode")

View File

@@ -8,7 +8,6 @@
import UIKit
import AltStoreCore
import EmotionalDamage
@available(iOS 13, *)
final class SceneDelegate: UIResponder, UIWindowSceneDelegate
@@ -40,7 +39,6 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate
guard DatabaseManager.shared.isStarted else { return }
AppManager.shared.update()
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
PatreonAPI.shared.refreshPatreonAccount()
}
@@ -142,8 +140,61 @@ private extension SceneDelegate
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
}
case "pairing":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
Logger.main.info("queryItems \(queryItems)")
guard let callbackTemplate = queryItems["urlname"]?.removingPercentEncoding else { return }
DispatchQueue.main.async {
exportPairingFile(callbackTemplate)
}
case "certificate":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let callbackTemplate = queryItems["callback_template"]?.removingPercentEncoding else { return }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.exportCertificateNotification, object: nil, userInfo: [AppDelegate.exportCertificateCallbackTemplateKey: callbackTemplate])
}
default: break
}
}
}
}
func exportPairingFile(_ urlname: String) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first, let viewcontroller = window.rootViewController {
let fm = FileManager.default
let documentsPath = fm.documentsDirectory.appendingPathComponent("ALTPairingFile.mobiledevicepairing")
guard let data = try? Data(contentsOf: documentsPath) else {
let toastView = ToastView(text: NSLocalizedString("Failed to find Pairing File!", comment: ""), detailText: nil)
toastView.show(in: viewcontroller)
return
}
let base64encodedCert = data.base64EncodedString()
var allowedQueryParamAndKey = NSCharacterSet.urlQueryAllowed
allowedQueryParamAndKey.remove(charactersIn: ";/?:@&=+$, ")
guard let encodedCert = base64encodedCert.addingPercentEncoding(withAllowedCharacters: allowedQueryParamAndKey) else {
let toastView = ToastView(text: NSLocalizedString("Failed to encode pairingFile!", comment: ""), detailText: nil)
toastView.show(in: viewcontroller)
return
}
var urlStr = "\(urlname)://pairingFile?data=$(BASE64_PAIRING)"
let finished = urlStr.replacingOccurrences(of: "$(BASE64_PAIRING)", with: encodedCert, options: .literal, range: nil)
print(finished)
guard let callbackUrl = URL(string: finished) else {
let toastView = ToastView(text: NSLocalizedString("Failed to initialize callback URL!", comment: ""), detailText: nil)
toastView.show(in: viewcontroller)
return
}
UIApplication.shared.open(callbackUrl)
}
}

View File

@@ -380,13 +380,8 @@ private extension ErrorLogViewController
func searchFAQ(for loggedError: LoggedError)
{
let baseURL = URL(string: "https://faq.altstore.io/getting-started/error-codes")!
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
let query = [loggedError.domain, "\(loggedError.error.displayCode)"].joined(separator: "+")
components.queryItems = [URLQueryItem(name: "q", value: query)]
let safariViewController = SFSafariViewController(url: components.url ?? baseURL)
let staticURL = URL(string: "https://docs.sidestore.io/docs/troubleshooting/error-codes")!
let safariViewController = SFSafariViewController(url: staticURL)
safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true)
}

View File

@@ -82,7 +82,7 @@ final class AboutPatreonHeaderView: UICollectionReusableView
imageView.layer.cornerRadius = imageView.bounds.midY
}
for button in [self.supportButton, self.accountButton].compactMap({$0})
for button in [self.supportButton, self.accountButton, self.twitterButton, self.instagramButton].compactMap({$0})
{
button.clipsToBounds = true
button.layer.cornerRadius = 16

View File

@@ -22,7 +22,7 @@
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<stackView key="tableFooterView" opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalCentering" alignment="center" spacing="15" id="48g-cT-stR">
<rect key="frame" x="0.0" y="1986.3333377838135" width="402" height="125"/>
<rect key="frame" x="0.0" y="2040.3333358764648" width="402" height="125"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="900" text="Follow SideStore for updates" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XFa-MY-7cV">
@@ -698,7 +698,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1071.6666698455811" width="402" height="51"/>
<rect key="frame" x="0.0" y="1107.6666698455811" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O5R-Al-lGj" id="CrG-Mr-xQq">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -739,19 +739,19 @@
<tableViewSection headerTitle="" id="OMa-EK-hRI">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1163.0000038146973" width="402" height="51"/>
<rect key="frame" x="0.0" y="1199.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FMZ-as-Ljo" id="JzL-Of-A3T">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF">
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="125.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/>
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView>
@@ -773,19 +773,19 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1214.0000038146973" width="402" height="51"/>
<rect key="frame" x="0.0" y="1250.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
<rect key="frame" x="30" y="15.333333333333334" width="187.66666666666666" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/>
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView>
@@ -810,19 +810,19 @@
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VrV-qI-zXF" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1265.0000038146973" width="402" height="51"/>
<rect key="frame" x="0.0" y="1301.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VrV-qI-zXF" id="w1r-uY-4pD">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/>
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView>
@@ -844,19 +844,19 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1316.0000038146973" width="402" height="51"/>
<rect key="frame" x="0.0" y="1352.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VNn-u4-cN8" id="4bh-qe-l2N">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm">
<rect key="frame" x="30" y="15.333333333333334" width="140" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/>
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView>
@@ -878,19 +878,19 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1367.0000038146973" width="402" height="51"/>
<rect key="frame" x="0.0" y="1403.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="e7s-hL-kv9" id="yjL-Mu-HTk">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
<rect key="frame" x="30" y="15.333333333333334" width="135.66666666666666" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/>
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView>
@@ -912,7 +912,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="XW5-Zc-nXH" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1418.0000038146973" width="402" height="51"/>
<rect key="frame" x="0.0" y="1454.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="XW5-Zc-nXH" id="AtM-bL-8pS">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -947,7 +947,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="qbY-8c-LYT" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1469.0000038146973" width="402" height="51"/>
<rect key="frame" x="0.0" y="1505.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qbY-8c-LYT" id="NxK-qB-w7Q">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -985,10 +985,70 @@
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="ZhW-yK-wdJ">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Y6h-Bo-yec" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1592.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Y6h-Bo-yec" id="4Jf-I6-v7z">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Import Signing Certificate..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rv6-S1-2gw">
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="227.33333333333337" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="rv6-S1-2gw" firstAttribute="leading" secondItem="4Jf-I6-v7z" secondAttribute="leadingMargin" id="7zH-bg-kLS"/>
<constraint firstItem="rv6-S1-2gw" firstAttribute="centerY" secondItem="4Jf-I6-v7z" secondAttribute="centerY" id="Yls-DF-HHr"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="dLk-d6-X4T" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1643.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="dLk-d6-X4T" id="Okl-3m-rde">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Export Signing Certificate..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WPe-mj-W7t">
<rect key="frame" x="29.999999999999986" y="15.333333333333334" width="226.66666666666663" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="WPe-mj-W7t" firstAttribute="leading" secondItem="Okl-3m-rde" secondAttribute="leadingMargin" id="9g7-9Z-ZZQ"/>
<constraint firstItem="WPe-mj-W7t" firstAttribute="centerY" secondItem="Okl-3m-rde" secondAttribute="centerY" id="RiS-WT-srl"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="3"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="" id="lLQ-K0-XSb">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="daQ-mk-yqC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1560.3333377838135" width="402" height="51"/>
<rect key="frame" x="0.0" y="1734.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="daQ-mk-yqC" id="ZkW-ZR-twy">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1023,7 +1083,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="hRP-jU-2hd" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1611.3333377838135" width="402" height="51"/>
<rect key="frame" x="0.0" y="1785.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hRP-jU-2hd" id="JhE-O4-pRg">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1058,7 +1118,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="JoN-Aj-XtZ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1662.3333377838135" width="402" height="51"/>
<rect key="frame" x="0.0" y="1836.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JoN-Aj-XtZ" id="v8Q-VQ-Q1h">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1093,7 +1153,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="QOO-bO-4M5" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1713.3333377838135" width="402" height="51"/>
<rect key="frame" x="0.0" y="1887.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="QOO-bO-4M5" id="VTT-z5-C89">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1121,7 +1181,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="ToB-H7-2lR" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1764.3333377838135" width="402" height="51"/>
<rect key="frame" x="0.0" y="1938.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ToB-H7-2lR" id="Acf-xV-Isn">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1149,7 +1209,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="xtI-eU-LFb" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1815.3333377838135" width="402" height="51"/>
<rect key="frame" x="0.0" y="1989.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="xtI-eU-LFb" id="bc9-41-6mE">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1183,7 +1243,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="pvu-IV-Poa" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1866.3333377838135" width="402" height="51"/>
<rect key="frame" x="0.0" y="2040.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pvu-IV-Poa" id="zck-an-8cK">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1218,7 +1278,7 @@
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="9By-QW-Jw9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1917.3333377838135" width="402" height="51"/>
<rect key="frame" x="0.0" y="2091.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="9By-QW-Jw9" id="Dzq-gE-zyT">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1246,6 +1306,41 @@
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="LzP-Qb-bmC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="2142.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="LzP-Qb-bmC" id="3rE-h0-8kb">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Minimuxer Status Check" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3BY-c1-QEV" userLabel="Minimuxer Status Check">
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="198.33333333333337" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="AB6-Ok-Faf" userLabel="Minimuxer Check Switch">
<rect key="frame" x="323" y="10" width="51" height="31"/>
<connections>
<action selector="toggleMinimuxerStatusCheck:" destination="aMk-Xp-UL8" eventType="valueChanged" id="Kwh-Km-aLj"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="AB6-Ok-Faf" secondAttribute="trailing" id="EtH-X0-plP"/>
<constraint firstItem="AB6-Ok-Faf" firstAttribute="centerY" secondItem="3rE-h0-8kb" secondAttribute="centerY" id="Tc3-56-q7s"/>
<constraint firstItem="3BY-c1-QEV" firstAttribute="centerY" secondItem="3rE-h0-8kb" secondAttribute="centerY" id="kIV-xS-Ava"/>
<constraint firstItem="3BY-c1-QEV" firstAttribute="leading" secondItem="3rE-h0-8kb" secondAttribute="leadingMargin" id="pJS-6V-6vh"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="3"/>

View File

@@ -16,6 +16,8 @@ import IntentsUI
import SemanticVersion
import AltStoreCore
import CAltSign
import UniformTypeIdentifiers
extension SettingsViewController
{
@@ -30,6 +32,7 @@ extension SettingsViewController
case techyThings
case credits
case advancedSettings
case signing
// diagnostics section, will be enabled on release builds only on swipe down with 3 fingers 3 times
case diagnostics
// case macDirtyCow
@@ -79,6 +82,11 @@ extension SettingsViewController
case betaTrack
// case hiddenSettings
}
fileprivate enum SigningSettingsRow: Int, CaseIterable {
case importCert
case exportCert
}
fileprivate enum DiagnosticsRow: Int, CaseIterable
{
@@ -143,6 +151,7 @@ final class SettingsViewController: UITableViewController
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openExportCertificateConfirm(_:)), name: AppDelegate.exportCertificateNotification, object: nil)
}
@@ -263,50 +272,6 @@ final class SettingsViewController: UITableViewController
}
private class BuildInfo{
private static let MARKETING_VERSION_TAG = "CFBundleShortVersionString"
private static let CURRENT_PROJECT_VERSION_TAG = kCFBundleVersionKey as String
private static let XCODE_VERSION_TAG = "DTXcode"
private static let XCODE_REVISION_TAG = "DTXcodeBuild"
let bundle: Bundle
public init(){
bundle = Bundle.main
}
enum BundleError: Swift.Error {
case invalidURL
}
public init(url: URL) throws {
guard let bundle = Bundle(url: url) else {
throw BundleError.invalidURL
}
self.bundle = bundle
}
public lazy var project_version: String? = {
let version = bundle.object(forInfoDictionaryKey: Self.CURRENT_PROJECT_VERSION_TAG) as? String
return version
}()
public lazy var marketing_version: String? = {
let version = bundle.object(forInfoDictionaryKey: Self.MARKETING_VERSION_TAG) as? String
return version
}()
public lazy var xcode: String? = {
let xcode = bundle.object(forInfoDictionaryKey: Self.XCODE_VERSION_TAG) as? String
return xcode
}()
public lazy var xcode_revision: String? = {
let revision = bundle.object(forInfoDictionaryKey: Self.XCODE_REVISION_TAG) as? String
return revision
}()
}
private extension SettingsViewController
{
@@ -477,7 +442,19 @@ private extension SettingsViewController
case .advancedSettings:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ADVANCED SETTINGS", comment: "")
case .signing:
// FIXME: Why "Enable Background Refresh ..." appear here if secondaryLabel is not specified???
if isHeader
{
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("SIGNING", comment: "")
}
else
{
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("", comment: "")
}
case .diagnostics:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DIAGNOSTICS", comment: "")
@@ -597,6 +574,11 @@ private extension SettingsViewController
UserDefaults.standard.isMinimuxerConsoleLoggingEnabled = sender.isOn
}
@IBAction func toggleMinimuxerStatusCheck(_ sender: UISwitch) {
// update it in database
UserDefaults.standard.isMinimuxerStatusCheckEnabled = sender.isOn
}
@IBAction func toggleRecreateDatabaseSwitch(_ sender: UISwitch) {
// Update the setting in UserDefaults
UserDefaults.standard.recreateDatabaseOnNextStart = sender.isOn
@@ -800,6 +782,48 @@ private extension SettingsViewController
self.performSegue(withIdentifier: "showErrorLog", sender: nil)
}
}
@objc func openExportCertificateConfirm(_ notification: Notification)
{
func export()
{
guard let template = notification.userInfo?[AppDelegate.exportCertificateCallbackTemplateKey] as? String,
template.contains("$(BASE64_CERT)") else {
let toastView = ToastView(text: NSLocalizedString("No $(BASE64_CERT) placeholder found", comment: ""), detailText: nil)
toastView.show(in: self)
return
}
guard let data = Keychain.shared.signingCertificate,
let password = Keychain.shared.signingCertificatePassword else {
let toastView = ToastView(text: NSLocalizedString("Failed to find certificate or password", comment: ""), detailText: nil)
toastView.show(in: self)
return
}
let base64encodedCert = data.base64EncodedString()
var allowedQueryParamAndKey = NSCharacterSet.urlQueryAllowed
allowedQueryParamAndKey.remove(charactersIn: ";/?:@&=+$, ")
guard let encodedCert = base64encodedCert.addingPercentEncoding(withAllowedCharacters: allowedQueryParamAndKey) else {
let toastView = ToastView(text: NSLocalizedString("Failed to encode certificate!", comment: ""), detailText: nil)
toastView.show(in: self)
return
}
var urlStr = template.replacingOccurrences(of: "$(BASE64_CERT)", with: encodedCert, options: .literal, range: nil)
urlStr = urlStr.replacingOccurrences(of: "$(PASSWORD)", with: password, options: .literal, range: nil)
print(urlStr)
guard let callbackUrl = URL(string: urlStr) else {
let toastView = ToastView(text: NSLocalizedString("Failed to initialize callback URL!", comment: ""), detailText: nil)
toastView.show(in: self)
return
}
UIApplication.shared.open(callbackUrl)
}
let alertController = UIAlertController(title: NSLocalizedString("Export Certificate", comment: ""), message: NSLocalizedString("Do you want to export your certificate to an external app? That app will be able to sign apps using your certificate.", comment: ""), preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Export", comment: ""), style: .default) { _ in export() })
alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
}
extension SettingsViewController
@@ -871,7 +895,7 @@ extension SettingsViewController
case _ where isSectionHidden(section): return nil
case .signIn where self.activeTeam != nil: return nil
case .account where self.activeTeam == nil: return nil
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .diagnostics /* ,.macDirtyCow */:
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .signing ,.diagnostics /* ,.macDirtyCow */:
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(headerView, for: section, isHeader: true)
return headerView
@@ -888,7 +912,7 @@ extension SettingsViewController
case _ where isSectionHidden(section): return nil
case .signIn where self.activeTeam != nil: return nil
// case .signIn, .patreon, .display, .appRefresh, .techyThings, .macDirtyCow:
case .signIn, .patreon, .display, .appRefresh, .techyThings:
case .signIn, .patreon, .display, .appRefresh, .techyThings, .signing:
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(footerView, for: section, isHeader: false)
return footerView
@@ -906,7 +930,7 @@ extension SettingsViewController
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
// case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .advanced:
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .diagnostics:
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .signing, .diagnostics:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
return height
@@ -923,7 +947,7 @@ extension SettingsViewController
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
// case .signIn, .patreon, .display, .appRefresh, .techyThings, .macDirtyCow:
case .signIn, .patreon, .display, .appRefresh, .techyThings, .diagnostics:
case .signIn, .patreon, .display, .appRefresh, .techyThings, .signing, .diagnostics:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height
@@ -993,7 +1017,7 @@ extension SettingsViewController
// Option 2: Discord
alertController.addAction(UIAlertAction(title: "Discord", style: .default) { _ in
if let discordURL = URL(string: "https://discord.gg/sidestore") {
if let discordURL = URL(string: "https://discord.gg/sidestore-949183273383395328") {
let safariViewController = SFSafariViewController(url: discordURL)
safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true, completion: nil)
@@ -1198,7 +1222,135 @@ extension SettingsViewController
case .refreshAttempts, .betaUpdates, .betaTrack: break
}
case .signing:
let row = SigningSettingsRow.allCases[indexPath.row]
switch row {
case .importCert:
Task {
let certUrl = await withUnsafeContinuation { c in
let importVc = UIDocumentPickerViewController(forOpeningContentTypes: [UTType(filenameExtension: "p12")!], asCopy: false)
ImportExport.documentPickerHandler = DocumentPickerHandler { url in
c.resume(returning: url)
}
importVc.delegate = ImportExport.documentPickerHandler
self.present(importVc, animated: true)
}
guard let certUrl else {
return
}
let password = await withUnsafeContinuation { (c: UnsafeContinuation<String?,Never>) in
let alertController = UIAlertController(title: NSLocalizedString("Please enter the password for the certificate.", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
}
let submitAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default) { (action) in
let textField = alertController.textFields?.first
let code = textField?.text ?? ""
c.resume(returning: code)
}
alertController.addAction(submitAction)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { (action) in
c.resume(returning: nil)
})
self.present(alertController, animated: true)
}
guard let password else {
return
}
let _ = certUrl.startAccessingSecurityScopedResource()
defer {
certUrl.stopAccessingSecurityScopedResource()
}
let certData : Data
do {
certData = try Data(contentsOf: certUrl)
} catch {
let toastView = ToastView(text: NSLocalizedString("Failed to import certificate!", comment: ""), detailText: error.localizedDescription)
toastView.show(in: self)
return
}
guard let altCert = ALTCertificate(p12Data: certData, password: password) else {
let toastView = ToastView(text: NSLocalizedString("Failed to import certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct.")
toastView.show(in: self)
return
}
Keychain.shared.signingCertificate = altCert.encryptedP12Data(withPassword: "")!
let toastView = ToastView(text: NSLocalizedString("Certificate imported successfully!", comment: ""), detailText: nil)
toastView.show(in: self)
}
break
case .exportCert:
Task {
guard let certData = Keychain.shared.signingCertificate else {
let toastView = ToastView(text: NSLocalizedString("Failed to export certificate!", comment: ""), detailText: "Certificate not found.")
toastView.show(in: self)
return
}
let password = await withUnsafeContinuation { (c: UnsafeContinuation<String?,Never>) in
let alertController = UIAlertController(title: NSLocalizedString("Please enter the password for the certificate.", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
}
let submitAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default) { (action) in
let textField = alertController.textFields?.first
let code = textField?.text ?? ""
c.resume(returning: code)
}
alertController.addAction(submitAction)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { (action) in
c.resume(returning: nil)
})
self.present(alertController, animated: true)
}
guard let password else {
return
}
guard let altCert = ALTCertificate(p12Data: certData, password: nil) else {
let toastView = ToastView(text: NSLocalizedString("Failed to export certificate!", comment: ""), detailText: "Failed to create ALTCertificate. Check if the password is correct.")
toastView.show(in: self)
return
}
guard let newCertData = altCert.encryptedP12Data(withPassword: password) else {
let toastView = ToastView(text: NSLocalizedString("Failed to export certificate!", comment: ""), detailText: "Failed to encrypt ALTCertificate.")
toastView.show(in: self)
return
}
let newCertTmpPath = FileManager.default.temporaryDirectory.appendingPathComponent("SideStoreSigningCertificate.p12")
do {
try newCertData.write(to: newCertTmpPath)
} catch {
let toastView = ToastView(text: NSLocalizedString("Failed to export certificate!", comment: ""), detailText: error.localizedDescription)
toastView.show(in: self)
return
}
let exportVC = UIDocumentPickerViewController(forExporting: [newCertTmpPath], asCopy: false)
self.present(exportVC, animated: true)
}
break
}
case .diagnostics:
let row = DiagnosticsRow.allCases[indexPath.row]
switch row {

View File

@@ -514,27 +514,27 @@ extension SourcesViewController: NSFetchedResultsControllerDelegate
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
_ = Source.make(name: "OatmealDome's AltStore Source",
identifier: "me.oatmealdome.altstore",
groupID: "me.oatmealdome.altstore",
sourceURL: URL(string: "https://altstore.oatmealdome.me")!,
context: context)
_ = Source.make(name: "UTM Repository",
identifier: "com.utmapp.repos.UTM",
groupID: "com.utmapp.repos.UTM",
sourceURL: URL(string: "https://alt.getutm.app")!,
context: context)
_ = Source.make(name: "Flyinghead",
identifier: "com.flyinghead.source",
groupID: "com.flyinghead.source",
sourceURL: URL(string: "https://flyinghead.github.io/flycast-builds/altstore.json")!,
context: context)
_ = Source.make(name: "Provenance",
identifier: "org.provenance-emu.AltStore",
groupID: "org.provenance-emu.AltStore",
sourceURL: URL(string: "https://provenance-emu.com/apps.json")!,
context: context)
_ = Source.make(name: "PojavLauncher Repository",
identifier: "dev.crystall1ne.repos.PojavLauncher",
groupID: "dev.crystall1ne.repos.PojavLauncher",
sourceURL: URL(string: "http://alt.crystall1ne.dev")!,
context: context)

View File

@@ -36,6 +36,7 @@ final class TabBarController: UITabBarController
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.presentSources(_:)), name: AppDelegate.addSourceDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.exportFiles(_:)), name: AppDelegate.exportCertificateNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
}
@@ -141,4 +142,9 @@ private extension TabBarController
{
self.selectedIndex = Tab.settings.rawValue
}
@objc func exportFiles(_ notification: Notification)
{
self.selectedIndex = Tab.settings.rawValue
}
}

View File

@@ -36,6 +36,8 @@ public extension UserDefaults
@NSManaged var isExportResignedAppEnabled: Bool
@NSManaged var isVerboseOperationsLoggingEnabled: Bool
@NSManaged var isMinimuxerConsoleLoggingEnabled: Bool
@NSManaged var isMinimuxerStatusCheckEnabled: Bool
@NSManaged var recreateDatabaseOnNextStart: Bool
@NSManaged var isPairingReset: Bool
@NSManaged var isDebugModeEnabled: Bool
@@ -132,6 +134,7 @@ public extension UserDefaults
#keyPath(UserDefaults.isDebugModeEnabled): false,
#keyPath(UserDefaults.isVerboseOperationsLoggingEnabled): false,
#keyPath(UserDefaults.isMinimuxerConsoleLoggingEnabled): false, // minimuxer logging is disabled by default for console loggin
#keyPath(UserDefaults.isMinimuxerStatusCheckEnabled): false, // minimuxer status check is disabled by default to support stosVPN based cellular refresh
#keyPath(UserDefaults.recreateDatabaseOnNextStart): false,
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
#keyPath(UserDefaults.isIdleTimeoutDisableEnabled): true,

View File

@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>AltStore 17.xcdatamodel</string>
<string>AltStore 17_1.xcdatamodel</string>
</dict>
</plist>

View File

@@ -0,0 +1,316 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24D70" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="v17.1">
<entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastName" attributeType="String"/>
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppID" representedClassName="AppID" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="features" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="appBundleID" optional="YES" attributeType="String"/>
<attribute name="permission" optional="YES" attributeType="String"/>
<attribute name="sourceID" optional="YES" attributeType="String"/>
<attribute name="type" attributeType="String"/>
<attribute name="usageDescription" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceID"/>
<constraint value="appBundleID"/>
<constraint value="type"/>
<constraint value="permission"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppScreenshot" representedClassName="AppScreenshot" syncable="YES">
<attribute name="appBundleID" optional="YES" attributeType="String"/>
<attribute name="deviceType" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="height" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
<attribute name="imageURL" attributeType="URI"/>
<attribute name="sourceID" optional="YES" attributeType="String"/>
<attribute name="width" optional="YES" attributeType="Integer 16" usesScalarValueType="NO"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="screenshots" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceID"/>
<constraint value="appBundleID"/>
<constraint value="imageURL"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
<attribute name="appBundleID" optional="YES" attributeType="String"/>
<attribute name="buildVersion" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="downloadURL" attributeType="URI"/>
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
<attribute name="maxOSVersion" optional="YES" attributeType="String"/>
<attribute name="minOSVersion" optional="YES" attributeType="String"/>
<attribute name="sha256" optional="YES" attributeType="String"/>
<attribute name="size" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceID" optional="YES" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="versions" inverseEntity="StoreApp"/>
<relationship name="latestVersionApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="latestVersion" inverseEntity="StoreApp"/>
<relationship name="releaseTrack" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ReleaseTrack" inverseName="releases" inverseEntity="ReleaseTrack"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceID"/>
<constraint value="appBundleID"/>
<constraint value="version"/>
<constraint value="buildVersion"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="buildVersion" optional="YES" attributeType="String"/>
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hasAlternateIcon" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hasUpdate" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="needsResign" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="storeBuildVersion" optional="YES" attributeType="String"/>
<attribute name="useMainProfile" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="version" attributeType="String"/>
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="installedApp" inverseEntity="LoggedError"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="resignedBundleIdentifier" attributeType="String"/>
<attribute name="version" attributeType="String"/>
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
</entity>
<entity name="LoggedError" representedClassName="LoggedError" syncable="YES">
<attribute name="appBundleID" attributeType="String"/>
<attribute name="appName" attributeType="String"/>
<attribute name="code" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="operation" optional="YES" attributeType="String"/>
<attribute name="userInfo" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="loggedErrors" inverseEntity="InstalledApp"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="loggedErrors" inverseEntity="StoreApp"/>
</entity>
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
<attribute name="appID" optional="YES" attributeType="String"/>
<attribute name="caption" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="externalURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="title" attributeType="String"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="sourceIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
<attribute name="firstName" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<relationship name="pledges" toMany="YES" deletionRule="Cascade" destinationEntity="Pledge" inverseName="account" inverseEntity="Pledge"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Patron" representedClassName="ManagedPatron" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Pledge" representedClassName="Pledge" syncable="YES">
<attribute name="amount" attributeType="Decimal" defaultValueString="0"/>
<attribute name="campaignURL" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PatreonAccount" inverseName="pledges" inverseEntity="PatreonAccount"/>
<relationship name="rewards" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="PledgeReward" inverseName="pledge" inverseEntity="PledgeReward"/>
<relationship name="tiers" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="PledgeTier" inverseName="pledge" inverseEntity="PledgeTier"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PledgeReward" representedClassName="PledgeReward" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<relationship name="pledge" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Pledge" inverseName="rewards" inverseEntity="Pledge"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PledgeTier" representedClassName="PledgeTier" syncable="YES">
<attribute name="amount" attributeType="Decimal" defaultValueString="0.0"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<relationship name="pledge" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Pledge" inverseName="tiers" inverseEntity="Pledge"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="errorDescription" optional="YES" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="ReleaseTrack" representedClassName="ReleaseTrack" syncable="YES">
<attribute name="appBundleID" optional="YES" attributeType="String"/>
<attribute name="sourceID" optional="YES" attributeType="String"/>
<attribute name="track" attributeType="String"/>
<relationship name="releases" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="releaseTrack" inverseEntity="AppVersion"/>
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="releaseTracks" inverseEntity="StoreApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceID"/>
<constraint value="appBundleID"/>
<constraint value="track"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
<attribute name="groupID" optional="YES" attributeType="String"/>
<attribute name="hasFeaturedApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="headerImageURL" optional="YES" attributeType="URI"/>
<attribute name="iconURL" optional="YES" attributeType="URI"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="localizedDescription" optional="YES" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="patreonURL" optional="YES" attributeType="URI"/>
<attribute name="sourceURL" attributeType="URI"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="version" attributeType="Integer 64" defaultValueString="1" usesScalarValueType="YES"/>
<attribute name="websiteURL" optional="YES" attributeType="URI"/>
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
<relationship name="featuredApps" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="featuringSource" inverseEntity="StoreApp"/>
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="category" optional="YES" attributeType="String"/>
<attribute name="developerName" attributeType="String"/>
<attribute name="downloadURL" optional="YES" attributeType="URI"/>
<attribute name="featuredSortID" optional="YES" attributeType="String"/>
<attribute name="iconURL" attributeType="URI"/>
<attribute name="isHiddenWithoutPledge" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isPledged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isPledgeRequired" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="localizedDescription" attributeType="String"/>
<attribute name="marketplaceID" optional="YES" attributeType="String"/>
<attribute name="name" attributeType="String"/>
<attribute name="pledgeAmount" optional="YES" attributeType="Decimal"/>
<attribute name="pledgeCurrency" optional="YES" attributeType="String"/>
<attribute name="prefersCustomPledge" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="screenshotURLs" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
<attribute name="subtitle" optional="YES" attributeType="String"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
<attribute name="version" optional="YES" attributeType="String"/>
<attribute name="versionDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="versionDescription" optional="YES" attributeType="String"/>
<relationship name="featuringSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="featuredApps" inverseEntity="Source"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
<relationship name="latestVersion" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AppVersion" inverseName="latestVersionApp" inverseEntity="AppVersion"/>
<relationship name="loggedErrors" toMany="YES" deletionRule="Nullify" destinationEntity="LoggedError" inverseName="storeApp" inverseEntity="LoggedError"/>
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
<relationship name="releaseTracks" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="ReleaseTrack" inverseName="storeApp" inverseEntity="ReleaseTrack"/>
<relationship name="screenshots" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppScreenshot" inverseName="app" inverseEntity="AppScreenshot"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
<relationship name="versions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppVersion" inverseName="app" inverseEntity="AppVersion"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="sourceIdentifier"/>
<constraint value="bundleIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View File

@@ -33,7 +33,7 @@ public class AppPermission: BaseEntity
default: return UnknownAppPermission(rawValue: self._permission)
}
}
@NSManaged @objc(permission) private var _permission: String
@NSManaged @objc(permission) private(set) public var _permission: String
// Set by StoreApp.
@NSManaged public var appBundleID: String

View File

@@ -59,6 +59,8 @@ public class InstalledApp: BaseEntity, InstalledAppProtocol
@NSManaged public var needsResign: Bool
@NSManaged public var hasAlternateIcon: Bool
@NSManaged public var useMainProfile: Bool
@NSManaged public var certificateSerialNumber: String?
@NSManaged public var storeBuildVersion: String?
@@ -141,7 +143,7 @@ public class InstalledApp: BaseEntity, InstalledAppProtocol
let latestVer = SemanticVersion("\(latestSemVer!.major).\(latestSemVer!.minor).\(latestSemVer!.patch)")
// Compare by major.minor.patch
if latestVer! > latestVer! {
if latestVer! > currentVer! {
return true
}

View File

@@ -213,6 +213,7 @@ extension MergePolicy{
}
default:
// break
// Unknown context-level conflict.
// assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
assertionFailure("Context Conflict Detected: is there ambigious data in your incoming sources?\nConflict:\(conflict)")

View File

@@ -505,13 +505,14 @@ cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QAIAAlUcm9vdIABrxESLAALAAwAGQA1ADYANwBjAGQAZQBm
<relationship name="relationshipmappings" type="0/0" destination="XDDEVRELATIONSHIPMAPPING" idrefs="z142 z119 z223 z116 z229 z139 z236 z136 z214 z216"></relationship>
</object>
<object type="XDDEVENTITYMAPPING" id="z198">
<attribute name="migrationpolicyclassname" type="string"></attribute>
<attribute name="sourcename" type="string">AppPermission</attribute>
<attribute name="mappingtypename" type="string">Undefined</attribute>
<attribute name="mappingnumber" type="int16">17</attribute>
<attribute name="destinationname" type="string">AppPermission</attribute>
<attribute name="autogenerateexpression" type="bool">1</attribute>
<relationship name="mappingmodel" type="1/1" destination="XDDEVMAPPINGMODEL" idrefs="z192"></relationship>
<relationship name="attributemappings" type="0/0" destination="XDDEVATTRIBUTEMAPPING" idrefs="z213 z240 z191 z123 z287"></relationship>
<relationship name="attributemappings" type="0/0" destination="XDDEVATTRIBUTEMAPPING" idrefs="z123 z213 z287 z240 z191"></relationship>
<relationship name="relationshipmappings" type="0/0" destination="XDDEVRELATIONSHIPMAPPING" idrefs="z288"></relationship>
</object>
<object type="XDDEVENTITYMAPPING" id="z199">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
//
// SourceMigrationPolicy.swift
// Source11To17MigrationPolicy.swift
// AltStoreCore
//
// Created by Riley Testut on 10/19/23.

View File

@@ -1,5 +1,5 @@
//
// StoreAppMigration11To17Policy.swift
// StoreApp11To17MigrationPolicy.swift
// AltStore
//
// Created by Magesh K on 25/02/25.
@@ -108,7 +108,7 @@ class StoreApp11To17MigrationPolicy: NSEntityMigrationPolicy {
// Create a new ReleaseTrack entity
let context = dInstance.managedObjectContext!
let releaseTrack = NSEntityDescription.insertNewObject(forEntityName: ReleaseTrack.entity().name!, into: context)
let releaseTrack = NSEntityDescription.insertNewObject(forEntityName: ReleaseTrack.entity().name ?? ReleaseTrack.description(), into: context)
releaseTrack.setValue(defaultChannel, forKey: #keyPath(ReleaseTrack._track))
// Connect the releaseTrack to the destination StoreApp
@@ -116,7 +116,7 @@ class StoreApp11To17MigrationPolicy: NSEntityMigrationPolicy {
// Find the mapping name for AppVersion (make sure this exactly matches your mapping model)
let appVersionMappingName = findEntityMappingName(for: AppVersion.entity().name!, in: manager)
let appVersionMappingName = findEntityMappingName(for: AppVersion.entity().name ?? AppVersion.description(), in: manager)
// Create a mutable ordered set for the destination AppVersion objects
let destinationVersionsSet = NSMutableOrderedSet()
@@ -145,5 +145,5 @@ class StoreApp11To17MigrationPolicy: NSEntityMigrationPolicy {
// dInstance.setValue(NSOrderedSet(), forKey: #keyPath(StoreApp._versions))
dInstance.setValue(nil, forKey: #keyPath(StoreApp._versions))
}
}

View File

@@ -0,0 +1,28 @@
//
// AppPermission17to17_1MigrationPolicy.swift
// AltStore
//
// Created by Magesh K on 15/03/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import CoreData
@objc(AppPermission11To17_1MigrationPolicy)
class AppPermission11To17_1MigrationPolicy: AppPermission17To17_1MigrationPolicy {
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
// Let the default implementation create the basic destination AppPermission
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
}
override func createRelationships(
forDestination dInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
}
}

View File

@@ -0,0 +1,26 @@
//
// ReleaseTrack11To17_1MigrationPolicy.swift
// AltStore
//
// Created by Magesh K on 15/03/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import CoreData
@objc(ReleaseTrack11To17_1MigrationPolicy)
class ReleaseTrack11To17_1MigrationPolicy: ReleaseTrack17To17_1MigrationPolicy {
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
}
override func createRelationships(
forDestination dInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
}
}

View File

@@ -0,0 +1,23 @@
//
// Source11To17_1MigrationPolicy.swift
// AltStore
//
// Created by Magesh K on 15/03/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import CoreData
@objc(Source11To17_1MigrationPolicy)
class Source11To17_1MigrationPolicy: Source17To17_1MigrationPolicy
{
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
// Let the default implementation create the basic destination AppPermission
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
}
override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
{
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
}
}

View File

@@ -0,0 +1,62 @@
//
// StoreApp11To17_1MigrationPolicy.swift
// AltStore
//
// Created by Magesh K on 15/03/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import CoreData
fileprivate extension NSManagedObject
{
var storeAppReleaseTracks: NSOrderedSet? {
let tracks = self.value(forKey: #keyPath(StoreApp._releaseTracks)) as? NSOrderedSet
return tracks
}
}
@objc(StoreApp11To17_1MigrationPolicy)
class StoreApp11To17_1MigrationPolicy: StoreApp11To17MigrationPolicy
{
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
}
override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
{
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
let appBundleID = dInstance.value(forKey: #keyPath(StoreApp.bundleIdentifier))
let sourceID = dInstance.value(forKey: #keyPath(StoreApp.sourceIdentifier))
for case let track as NSManagedObject in dInstance.storeAppReleaseTracks ?? []
{
track.setValue(appBundleID, forKey: #keyPath(ReleaseTrack._appBundleID))
track.setValue(sourceID, forKey: #keyPath(ReleaseTrack._sourceID))
guard let releases = track.value(forKey: #keyPath(ReleaseTrack._releases)) as? NSOrderedSet else {
continue
}
for case let version as NSManagedObject in releases {
version.setValue(appBundleID, forKey: #keyPath(AppVersion.appBundleID))
version.setValue(sourceID, forKey: #keyPath(AppVersion.sourceID))
}
}
if let permissions = dInstance.value(forKey: #keyPath(StoreApp._permissions)) as? NSSet {
for case let permission as NSManagedObject in permissions {
permission.setValue(appBundleID, forKey: #keyPath(AppPermission.appBundleID))
permission.setValue(sourceID, forKey: #keyPath(AppPermission.sourceID))
}
}
if let screenshots = dInstance.value(forKey: #keyPath(StoreApp._screenshots)) as? NSOrderedSet {
for case let screenshot as NSManagedObject in screenshots {
screenshot.setValue(appBundleID, forKey: #keyPath(AppScreenshot.appBundleID))
screenshot.setValue(sourceID, forKey: #keyPath(AppScreenshot.sourceID))
}
}
}
}

View File

@@ -0,0 +1,100 @@
//
// AppPermission17to17_1MigrationPolicy.swift
// AltStore
//
// Created by Magesh K on 15/03/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import CoreData
@objc(AppPermission17To17_1MigrationPolicy)
class AppPermission17To17_1MigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
// Let the default implementation create the basic destination AppPermission
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
// Get the destination AppPermission instance that was created
guard let destinationPermission = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first else {
print("Failed to locate destination AppPermission instance")
return
}
// Extract the type value from source
if let type = sInstance.value(forKey: #keyPath(AppPermission.type)) as? String {
// this is for backwards compatibility <0.5.10
// if older type was a valid permission, then consider it as "privacy" in the newer "type".
if let permission = self.derivePermissionFromType(type) {
destinationPermission.setValue(permission, forKey: #keyPath(AppPermission._permission))
destinationPermission.setValue("privacy", forKey: #keyPath(AppPermission.type))
}
}
// set initial values copied from source as-is
// (will be updated by StoreApp and Source migration policy in its createRelationship() method)
if let storeApp = sInstance.value(forKey: #keyPath(AppPermission.app)) as? NSManagedObject{
if let appBundle = storeApp.value(forKey: #keyPath(StoreApp.bundleIdentifier)) as? String{
destinationPermission.setValue(appBundle, forKey: #keyPath(AppPermission.appBundleID))
}
if let sourceID = storeApp.value(forKey: #keyPath(StoreApp.sourceIdentifier)) as? String {
destinationPermission.setValue(sourceID, forKey: #keyPath(AppPermission.sourceID))
}
}
}
override func createRelationships(
forDestination dInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
// Retrieve the source storeApp from the source appPermission
guard let storeApp = dInstance.value(forKey: #keyPath(AppPermission.app)) as? NSManagedObject else {
print("Destination \(AppPermission.description()) has no storeApp")
return
}
// set initial values copied from source as-is to satisfy unique constraints
// (will be updated by StoreApp and Source migration policy in its createRelationship() method)
if let appBundle = storeApp.value(forKey: #keyPath(StoreApp.bundleIdentifier)) as? String{
dInstance.setValue(appBundle, forKey: #keyPath(AppPermission.appBundleID))
}
if let sourceID = storeApp.value(forKey: #keyPath(StoreApp.sourceIdentifier)) as? String {
dInstance.setValue(sourceID, forKey: #keyPath(AppPermission.sourceID))
}
}
// Helper method to derive permission string from type
private func derivePermissionFromType(_ type: String) -> String? {
// Based on the code in the documents, we need to map the ALTAppPermissionType to permission strings
switch type {
case "photos": return "NSPhotosUsageDescription"
case "camera": return "NSCameraUsageDescription"
case "location": return "NSLocationUsageDescription"
case "contacts": return "NSContactsUsageDescription"
case "reminders": return "NSRemindersUsageDescription"
case "music": return "NSAppleMusicUsageDescription"
case "microphone": return "NSMicrophoneUsageDescription"
case "speech-recognition": return "NSSpeechRecognitionUsageDescription"
case "background-audio": return "NSBackgroundAudioUsageDescription"
case "background-fetch": return "NSBackgroundFetchUsageDescription"
case "bluetooth": return "NSBluetoothUsageDescription"
case "network": return "NSNetworkUsageDescription"
case "calendars": return "NSCalendarsUsageDescription"
case "touchID": return "NSTouchIDUsageDescription"
case "faceID": return "NSFaceIDUsageDescription"
case "siri": return "NSSiriUsageDescription"
case "motion": return "NSMotionUsageDescription"
default: return nil // Default fallback
}
}
}

View File

@@ -0,0 +1,57 @@
//
// ReleaseTrack17To17_1MigrationPolicy.swift
// AltStore
//
// Created by Magesh K on 15/03/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import CoreData
@objc(ReleaseTrack17To17_1MigrationPolicy)
class ReleaseTrack17To17_1MigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
}
override func createRelationships(
forDestination dInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
// Retrieve the source storeApp from the source ReleaseTrack
guard let storeApp = dInstance.value(forKey: #keyPath(ReleaseTrack.storeApp)) as? NSManagedObject else {
print("Destination \(ReleaseTrack.description()) has no storeApp")
return
}
// set initial values copied from source as-is to satisfy unique constraints
// (will be updated by StoreApp and Source migration policy in its createRelationship() method)
let appBundle = storeApp.value(forKey: #keyPath(StoreApp.bundleIdentifier)) as? String
let sourceID = storeApp.value(forKey: #keyPath(StoreApp.sourceIdentifier)) as? String
if let appBundle {
dInstance.setValue(appBundle, forKey: #keyPath(ReleaseTrack._appBundleID))
}
if let sourceID {
dInstance.setValue(sourceID, forKey: #keyPath(ReleaseTrack._sourceID))
}
if let releases = dInstance.value(forKey: #keyPath(ReleaseTrack._releases)) as? NSOrderedSet {
for case let version as NSManagedObject in releases {
if let appBundle {
version.setValue(appBundle, forKey: #keyPath(AppVersion.appBundleID))
}
if let sourceID {
version.setValue(sourceID, forKey: #keyPath(AppVersion.sourceID))
}
}
}
}
}

View File

@@ -0,0 +1,191 @@
//
// Source17To17_1MigrationPolicy.swift
// AltStore
//
// Created by Magesh K on 15/03/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import CoreData
fileprivate extension NSManagedObject
{
var sourceSourceURL: URL? {
let sourceURL = self.value(forKey: #keyPath(Source.sourceURL)) as? URL
return sourceURL
}
var sourceSourceId: String? {
let sourceId = self.value(forKey: #keyPath(Source.identifier)) as? String
return sourceId
}
var sourceApps: NSOrderedSet? {
let apps = self.value(forKey: #keyPath(Source._apps)) as? NSOrderedSet
return apps
}
var sourceNewsItems: NSOrderedSet? {
let newsItems = self.value(forKey: #keyPath(Source._newsItems)) as? NSOrderedSet
return newsItems
}
}
fileprivate extension NSManagedObject
{
func setSourceId(_ sourceID: String)
{
self.setValue(sourceID, forKey: #keyPath(Source.identifier))
}
func setGroupId(_ groupID: String)
{
self.setValue(groupID, forKey: #keyPath(Source.groupID))
}
func setSourceSourceUrl(_ sourceURL: URL)
{
self.setValue(sourceURL, forKey: #keyPath(Source.sourceURL))
}
func setSourceSourceID(_ sourceID: String)
{
self.setValue(sourceID, forKey: #keyPath(Source.identifier))
}
func setStoreAppSourceID(_ sourceID: String)
{
self.setValue(sourceID, forKey: #keyPath(StoreApp.sourceIdentifier))
}
func setNewsItemSourceID(_ sourceID: String)
{
self.setValue(sourceID, forKey: #keyPath(NewsItem.sourceIdentifier))
}
func setAppVersionSourceID(_ sourceID: String)
{
self.setValue(sourceID, forKey: #keyPath(AppVersion.sourceID))
}
func setAppPermissionSourceID(_ sourceID: String)
{
self.setValue(sourceID, forKey: #keyPath(AppPermission.sourceID))
}
func setAppScreenshotSourceID(_ sourceID: String)
{
self.setValue(sourceID, forKey: #keyPath(AppScreenshot.sourceID))
}
func setReleaseTracksSourceID(_ sourceID: String)
{
self.setValue(sourceID, forKey: #keyPath(ReleaseTrack._sourceID))
}
}
fileprivate extension NSManagedObject
{
var storeAppVersions: NSOrderedSet? {
let versions = self.value(forKey: #keyPath(StoreApp._versions)) as? NSOrderedSet
return versions
}
var storeAppPermissions: NSSet? {
let permissions = self.value(forKey: #keyPath(StoreApp._permissions)) as? NSSet
return permissions
}
var storeAppScreenshots: NSOrderedSet? {
let screenshots = self.value(forKey: #keyPath(StoreApp._screenshots)) as? NSOrderedSet
return screenshots
}
var storeAppReleaseTracks: NSOrderedSet? {
let tracks = self.value(forKey: #keyPath(StoreApp._releaseTracks)) as? NSOrderedSet
return tracks
}
}
@objc(Source17To17_1MigrationPolicy)
class Source17To17_1MigrationPolicy: NSEntityMigrationPolicy
{
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
// Let the default implementation create the basic destination AppPermission
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
// Get the destination Source instance that was created
guard let dInstance = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance]).first else {
print("Failed to locate destination Source instance")
return
}
// update new fields with initial values
if let sourceID = sInstance.value(forKey: #keyPath(Source.identifier)) as? String {
dInstance.setValue(sourceID, forKey: #keyPath(Source.groupID))
}
guard var sourceURL = dInstance.sourceSourceURL else {
return
}
// sidestore official source has been moved to sidestore.io/apps-v2.json
// if we don't switch, users will end up with 2 offical sources
let normalizedSourceURL = try? sourceURL.normalized()
if normalizedSourceURL == "apps.sidestore.io" // if using old source url (<0.5.9)
{
sourceURL = Source.altStoreSourceURL // switch to latest
dInstance.setSourceSourceUrl(sourceURL) // and use it for current
}
var sourceID = try Source.sourceID(from: sourceURL)
dInstance.setSourceId(sourceID)
// for older versions migrating to current (their sourceID is their groupID)
dInstance.setGroupId(sourceID)
if sourceID == "apps.sidestore.io" {
sourceID = Source.altStoreIdentifier
dInstance.setSourceId(sourceID)
}
}
override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
{
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
guard let sourceID = dInstance.sourceSourceId else { return }
for case let newsItem as NSManagedObject in dInstance.sourceNewsItems ?? []
{
newsItem.setNewsItemSourceID(sourceID)
}
for case let app as NSManagedObject in dInstance.sourceApps ?? []
{
app.setStoreAppSourceID(sourceID)
for case let screenshot as NSManagedObject in app.storeAppScreenshots ?? []
{
screenshot.setAppScreenshotSourceID(sourceID)
}
for case let track as NSManagedObject in app.storeAppReleaseTracks ?? []
{
// print("Source_17_1MigrationPolicy: processing track \(track.value(forKey: "track")!)")
track.setValue(sourceID, forKey: #keyPath(ReleaseTrack._sourceID))
guard let releases = track.value(forKey: #keyPath(ReleaseTrack._releases)) as? NSOrderedSet else {
// print("Source_17_1MigrationPolicy: releases not found for track: \(track.value(forKey: "track")!)")
continue
}
for case let version as NSManagedObject in releases {
// print("Source_17_1MigrationPolicy: updating sourceID for version: \(version.value(forKey: "version")!)")
version.setValue(sourceID, forKey: #keyPath(AppVersion.sourceID))
}
}
}
}
}

View File

@@ -0,0 +1,57 @@
//
// StoreApp17To17_1MigrationPolicy.swift
// AltStore
//
// Created by Magesh K on 15/03/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import CoreData
fileprivate extension NSManagedObject
{
var storeAppReleaseTracks: NSOrderedSet? {
let tracks = self.value(forKey: #keyPath(StoreApp._releaseTracks)) as? NSOrderedSet
return tracks
}
}
@objc(StoreApp17To17_1MigrationPolicy)
class StoreApp17To17_1MigrationPolicy: NSEntityMigrationPolicy
{
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
}
override func createRelationships(forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
{
try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager)
let appBundleID = dInstance.value(forKey: #keyPath(StoreApp.bundleIdentifier))
for case let track as NSManagedObject in dInstance.storeAppReleaseTracks ?? []
{
track.setValue(appBundleID, forKey: #keyPath(ReleaseTrack._appBundleID))
guard let releases = track.value(forKey: #keyPath(ReleaseTrack._releases)) as? NSOrderedSet else {
continue
}
for case let version as NSManagedObject in releases {
version.setValue(appBundleID, forKey: #keyPath(AppVersion.appBundleID))
}
}
if let permissions = dInstance.value(forKey: #keyPath(StoreApp._permissions)) as? NSSet {
for case let permission as NSManagedObject in permissions {
permission.setValue(appBundleID, forKey: #keyPath(AppPermission.appBundleID))
}
}
if let screenshots = dInstance.value(forKey: #keyPath(StoreApp._screenshots)) as? NSOrderedSet {
for case let screenshot as NSManagedObject in screenshots {
screenshot.setValue(appBundleID, forKey: #keyPath(AppScreenshot.appBundleID))
}
}
}
}

View File

@@ -75,6 +75,9 @@ public class NewsItem: BaseEntity, Decodable
self.imageURL = try container.decodeIfPresent(URL.self, forKey: .imageURL)
self.externalURL = try container.decodeIfPresent(URL.self, forKey: .externalURL)
// TODO: app specific news should be moved to appEntity (via refactoring)
// This can be done by: 1. having two newsItem schema, one for source and one for StoreApp
// 2. move this appID field into the newItem schema which is under StoreApp.
self.appID = try container.decodeIfPresent(String.self, forKey: .appID)
let notify = try container.decodeIfPresent(Bool.self, forKey: .notify) ?? false

View File

@@ -14,7 +14,9 @@ public class ReleaseTrack: BaseEntity, Decodable
{
// attributes
@NSManaged @objc(track) public private(set) var _track: String?
@NSManaged @objc(appBundleID) public private(set) var _appBundleID: String?
@NSManaged @objc(sourceID) public private(set) var _sourceID: String?
// RelationShips
@NSManaged @objc(releases) public private(set) var _releases: NSOrderedSet?
@NSManaged public private(set) var storeApp: StoreApp?
@@ -89,7 +91,7 @@ public extension ReleaseTrack{
}
// update it into the appVersion
_ = version.mutateForData(channel: track, appBundleID: storeApp.bundleIdentifier)
_ = version.mutateForData(channel: track, appBundleID: storeApp.bundleIdentifier, sourceID: storeApp.sourceIdentifier)
}
}
@@ -109,6 +111,10 @@ public extension ReleaseTrack{
if key == NSExpression(forKeyPath: #keyPath(ReleaseTrack.storeApp)).keyPath
{
updateVersions(for: storeApp)
// update unique constraint attribs
self._appBundleID = storeApp?.bundleIdentifier
self._sourceID = storeApp?.sourceIdentifier
}
}
}

View File

@@ -11,7 +11,11 @@ import UIKit
public extension Source
{
static let altStoreIdentifier = try! Source.sourceID(from: Source.altStoreSourceURL)
#if ALPHA
static let altStoreGroupIdentifier = Bundle.Info.appbundleIdentifier
#else
static let altStoreGroupIdentifier = Bundle.Info.appbundleIdentifier
#endif
#if STAGING
@@ -24,13 +28,15 @@ public extension Source
#else
#if ALPHA
static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
static let altStoreSourceURL = URL(string: "https://sidestore.io/apps-v2.json/")!
#else
// static let altStoreSourceURL = URL(string: "https://apps.sidestore.io/")!
static let altStoreSourceURL = URL(string: "https://sidestore.io/apps-v2.json/")! // using v2 for alpha releases
static let altStoreSourceURL = URL(string: "https://sidestore.io/apps-v2.json/")!
#endif
#endif
// normalized url is the source identifier (or) p-key!
static let altStoreIdentifier = try! Source.sourceID(from: altStoreSourceURL)
}
// public struct AppPermissionFeed: Codable {
@@ -205,7 +211,8 @@ public class Source: BaseEntity, Decodable
/* Properties */
@NSManaged public var version: Int
@NSManaged public var name: String
@NSManaged public private(set) var identifier: String
@NSManaged public private(set) var identifier: String // NOTE: sourceID is just normalized sourceURL
@NSManaged public private(set) var groupID: String?
@NSManaged public var sourceURL: URL
/* Source Detail */
@@ -280,6 +287,9 @@ public class Source: BaseEntity, Decodable
case news
case featuredApps
case userInfo
// case identifier
case groupID = "identifier"
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
@@ -359,6 +369,24 @@ public class Source: BaseEntity, Decodable
// Updates identifier + apps & newsItems
try self.setSourceURL(sourceURL)
// NOTE: Source ID is just normalized sourceURL. coz normalized url is the primary key which needs to be unique
// Hence if a source's URL changed, then it means it is a different source now.
// This also means that the identifier field in the source is irrelevant (if any)
// if we want grouping of sources from same author or something like that then we should have used groupID (a new field)
// shouldn't use the existing "identifier" field, hence the following is commented out
// // if an explicit identifier is present, then use it
// if let identifier = try container.decodeIfPresent(String.self, forKey: .identifier)
// {
// self.identifier = identifier
// }
// if an explicit (group)identifier is present, then use it as groupID else use sourceID as groupID too
self.groupID = try container.decodeIfPresent(String.self, forKey: .groupID) ?? self.identifier
}
catch
{
@@ -405,7 +433,7 @@ public extension Source
// TODO: Support alternate URLs
let isRecommended = recommendedSources.contains { source in
return source.identifier == self.identifier || source.sourceURL?.absoluteString.lowercased() == self.sourceURL.absoluteString
return source.identifier == self.identifier || source.sourceURL?.absoluteString.lowercased() == self.sourceURL.absoluteString.lowercased()
}
return isRecommended
}
@@ -418,14 +446,17 @@ public extension Source
}
}
internal extension Source
public extension Source
{
class func sourceID(from sourceURL: URL) throws -> String
{
let sourceID = try sourceURL.normalized()
return sourceID
}
}
internal extension Source
{
func setFeaturedApps(_ featuredApps: [StoreApp]?)
{
// Explicitly update relationships for all apps to ensure featuredApps merges correctly.
@@ -451,17 +482,23 @@ public extension Source
{
func setSourceURL(_ sourceURL: URL) throws
{
let identifier = try Source.sourceID(from: sourceURL)
self.identifier = identifier
self.sourceURL = sourceURL
// update the normalized sourceURL as the identifier
let identifier = try Source.sourceID(from: sourceURL)
try self.setSourceID(identifier)
}
func setSourceID(_ identifier: String) throws
{
self.identifier = identifier
for app in self.apps
{
app.sourceIdentifier = identifier
}
for newsItem in self.newsItems
for newsItem in self.newsItems
{
newsItem.sourceIdentifier = identifier
}
@@ -479,6 +516,7 @@ public extension Source
{
let source = Source(context: context)
source.name = "SideStore Offical"
source.groupID = Source.altStoreGroupIdentifier
source.identifier = Source.altStoreIdentifier
try! source.setSourceURL(Source.altStoreSourceURL)
@@ -491,13 +529,14 @@ public extension Source
return source
}
class func make(name: String, identifier: String, sourceURL: URL, context: NSManagedObjectContext) -> Source
class func make(name: String, groupID: String, sourceURL: URL, context: NSManagedObjectContext) -> Source
{
let source = Source(context: context)
source.name = name
source.identifier = identifier
source.sourceURL = sourceURL
source.sourceURL = sourceURL
source.identifier = try! Source.sourceID(from: sourceURL)
return source
}
}

View File

@@ -25,7 +25,7 @@ struct TextLockScreenWidget: Widget
}
.supportedFamilies([.accessoryCircular])
.configurationDisplayName("AltWidget (Text)")
.description("View remaining days until AltStore expires.")
.description("View remaining days until SideStore expires.")
}
else
{
@@ -48,7 +48,7 @@ struct IconLockScreenWidget: Widget
}
.supportedFamilies([.accessoryCircular])
.configurationDisplayName("AltWidget (Icon)")
.description("View remaining days until AltStore expires.")
.description("View remaining days until SideStore expires.")
}
else
{

View File

@@ -1,8 +1,8 @@
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 0.6.0
CURRENT_PROJECT_VERSION = 6000
MARKETING_VERSION = 0.6.3
CURRENT_PROJECT_VERSION = 0603
// Vars to be overwritten by `CodeSigning.xcconfig` if exists
DEVELOPMENT_TEAM = S32Z3HMYVQ

View File

@@ -4,7 +4,7 @@
DEVELOPMENT_TEAM = XYZ0123456
// Set this for dev-local xcode builds
MARKETING_VERSION_SUFFIX = -local
//MARKETING_VERSION_SUFFIX = -local
// Prefix of unique bundle IDs registered to you in Apple Developer Portal.
// You need to register:

View File

@@ -213,6 +213,7 @@ build-tests:
@echo "Performing a build-for-testing..."
@xcodebuild build-for-testing \
-enableCodeCoverage YES \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
$(COMMON_BUILD_SETTINGS)
run-tests:

View File

@@ -28,10 +28,10 @@ Why iOS 15? Targeting such a recent version of iOS allows us to accelerate devel
SideStore is a just regular, sandboxed iOS application. The AltStore app target contains the vast majority of SideStore's functionality, including all the logic for downloading and updating apps through SideStore. SideStore makes heavy use of standard iOS frameworks and technologies most iOS developers are familiar with.
### EM Proxy
[SideServer mobile](https://github.com/jkcoxson/em_proxy) powers the defining feature of SideStore: untethered app installation. By levaraging an App Store app with additional entitlements (WireGuard) to create the VPN tunnel for us, it allows SideStore to take advantage of [Jitterbug](https://github.com/osy/Jitterbug)'s loopback method without requiring a paid developer account.
[SideServer mobile](https://github.com/jkcoxson/em_proxy) powers the defining feature of SideStore: untethered app installation. By leveraging a custom-built App Store app with additional entitlements (StosVPN) to create the VPN tunnel for us, it allows SideStore to take advantage of [Jitterbug](https://github.com/osy/Jitterbug)'s loopback method without requiring a paid developer account.
### Minimuxer
[Minimuxer](https://github.com/jkcoxson/minimuxer) is a lockdown muxer that can run inside iOSs sandbox. It replicates Apples usbmuxd protocol on MacOS to “discover” devices to interface with wireguard On-Device.
[Minimuxer](https://github.com/jkcoxson/minimuxer) is a lockdown muxer that can run inside iOSs sandbox. It replicates Apples usbmuxd protocol on MacOS to “discover” devices to interface with StosVPN On-Device.
### Roxas
[Roxas](https://github.com/rileytestut/roxas) is Riley Testut's internal framework from AltStore used across many of their iOS projects, developed to simplify a variety of common tasks used in iOS development.

View File

@@ -1,18 +0,0 @@
//
// EmotionalDamage.swift
// EmotionalDamage
//
// Created by Jackson Coxson on 10/26/22.
//
import Foundation
public func start_em_proxy(bind_addr: String) {
let host = NSString(string: bind_addr)
let host_pointer = UnsafeMutablePointer<CChar>(mutating: host.utf8String)
let _ = start_emotional_damage(host_pointer)
}
public func stop_em_proxy() {
stop_emotional_damage()
}

View File

@@ -0,0 +1,54 @@
//
// BuildInfo.swift
// AltStore
//
// Created by Magesh K on 23/03/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import Foundation
public class BuildInfo{
private static let MARKETING_VERSION_TAG = "CFBundleShortVersionString"
private static let CURRENT_PROJECT_VERSION_TAG = kCFBundleVersionKey as String
private static let XCODE_VERSION_TAG = "DTXcode"
private static let XCODE_REVISION_TAG = "DTXcodeBuild"
let bundle: Bundle
public init(){
bundle = Bundle.main
}
enum BundleError: Swift.Error {
case invalidURL
}
public init(url: URL) throws {
guard let bundle = Bundle(url: url) else {
throw BundleError.invalidURL
}
self.bundle = bundle
}
public lazy var project_version: String? = {
let version = bundle.object(forInfoDictionaryKey: Self.CURRENT_PROJECT_VERSION_TAG) as? String
return version
}()
public lazy var marketing_version: String? = {
let version = bundle.object(forInfoDictionaryKey: Self.MARKETING_VERSION_TAG) as? String
return version
}()
public lazy var xcode: String? = {
let xcode = bundle.object(forInfoDictionaryKey: Self.XCODE_VERSION_TAG) as? String
return xcode
}()
public lazy var xcode_revision: String? = {
let revision = bundle.object(forInfoDictionaryKey: Self.XCODE_REVISION_TAG) as? String
return revision
}()
}

View File

@@ -12,7 +12,7 @@ import AltStoreCore
class ImportExport {
private static var documentPickerHandler: DocumentPickerHandler?
public static var documentPickerHandler: DocumentPickerHandler?
public static func getPreviousBackupURL(_ backupURL: URL) -> URL {
let backupParentDirectory = backupURL.deletingLastPathComponent()

View File

@@ -59,48 +59,97 @@ public class AbstractConsoleLogger<T: OutputStream>: ConsoleLogger{
originalStdout = dup(STDOUT_FILENO)
originalStderr = dup(STDERR_FILENO)
let redirectedOutStream = self.outPipe?.fileHandleForWriting.fileDescriptor ?? -1
let redirectedErrStream = self.errPipe?.fileHandleForWriting.fileDescriptor ?? -1
// Redirect stdout and stderr to our pipes
dup2(self.outPipe?.fileHandleForWriting.fileDescriptor ?? -1, STDOUT_FILENO)
dup2(self.errPipe?.fileHandleForWriting.fileDescriptor ?? -1, STDERR_FILENO)
dup2(redirectedOutStream, STDOUT_FILENO)
dup2(redirectedErrStream, STDERR_FILENO)
// Disable libc-level buffering
// (libc by default uses bufferring except its own console/TTYs such as for pipes)
// we do have our own buffering so we disable stdlib io level bufferring
setvbuf(stdout, nil, _IONBF, 0) // disable buffering for stdout
setvbuf(stderr, nil, _IONBF, 0) // disable buffering for stderr
// Setup readability handlers for raw data
setupReadabilityHandler(for: outputHandle, isError: false)
setupReadabilityHandler(for: errorHandle, isError: true)
}
let shutdownLock = NSLock()
private func setupReadabilityHandler(for handle: FileHandle?, isError: Bool) {
handle?.readabilityHandler = { [weak self] handle in
let data = handle.availableData
if !data.isEmpty {
self?.writeQueue.async {
try? self?.writeData(data)
}
// Forward to original std stream
if let originalFD = isError ? self?.originalStderr : self?.originalStdout {
data.withUnsafeBytes { (bufferPointer) -> Void in
if let baseAddress = bufferPointer.baseAddress, bufferPointer.count > 0 {
write(originalFD, baseAddress, bufferPointer.count)
}
handle?.readabilityHandler = readHandler(isError: isError)
}
private func readHandler(isError: Bool) -> (FileHandle) -> Void {
return { [weak self] _ in
// Lock first before touching anything
self?.shutdownLock.lock()
defer { self?.shutdownLock.unlock() }
// Capture strong self *after* lock is acquired
guard let self = self else { return }
let handle = isError ? self.errorHandle : self.outputHandle
guard let data = handle?.availableData else { return }
writeQueue.async {
try? self.writeData(data)
}
// 2. Echo to original stdout/stderr if still valid
guard let fd = isError ? self.originalStderr : self.originalStdout else {
return
}
let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "UnknownApp"
guard fcntl(fd, F_GETFD) != -1 else {
NSLog("[%@] ConsoleLogger: Original FD (%d) is invalid, skipping echo", appName, fd)
return
}
data.withUnsafeBytes { rawBufferPointer in
guard let base = rawBufferPointer.baseAddress else { return }
var remaining = data.count
var offset = 0
let maxChunkSize = 16 * 1024 // 16 KB chunks
// write in chunks, else will throw 'Result too large'
while remaining > 0 {
let chunkSize = min(maxChunkSize, remaining)
let written = write(fd, base.advanced(by: offset), chunkSize)
if written < 0 {
NSLog("[%@] ConsoleLogger: Failed to re-echo to FD %d: %s", appName, fd, strerror(errno))
break
}
remaining -= written
offset += written
}
}
}
}
func writeData(_ data: Data) throws {
throw AbstractClassError.abstractMethodInvoked
}
func stopCapturing() {
shutdownLock.lock()
defer { shutdownLock.unlock() }
ostream.close()
// Restore original stdout and stderr
if let stdout = originalStdout {
if let stdout = originalStdout, stdout != STDOUT_FILENO {
dup2(stdout, STDOUT_FILENO)
close(stdout)
}
if let stderr = originalStderr {
if let stderr = originalStderr, stderr != STDERR_FILENO {
dup2(stderr, STDERR_FILENO)
close(stderr)
}

View File

@@ -0,0 +1,301 @@
//
// CollapsingMarkdownView.swift
// AltStore
//
// Created by Magesh K on 27/02/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import UIKit
import MarkdownKit
struct MarkdownManager
{
struct Fonts{
static let body: UIFont = .systemFont(ofSize: UIFont.systemFontSize)
// static let body: UIFont = .systemFont(ofSize: UIFont.labelFontSize)
static let header: UIFont = .boldSystemFont(ofSize: 14)
static let list: UIFont = .systemFont(ofSize: 14)
static let bold: UIFont = .boldSystemFont(ofSize: 14)
static let italic: UIFont = .italicSystemFont(ofSize: 14)
static let quote: UIFont = .italicSystemFont(ofSize: 14)
}
struct Color{
static let header = UIColor { traitCollection in
traitCollection.userInterfaceStyle == .dark ? UIColor.white : UIColor.black
}
static let bold = UIColor { traitCollection in
traitCollection.userInterfaceStyle == .dark ? UIColor.lightText : UIColor.darkText
}
}
static var enabledElements: MarkdownParser.EnabledElements {
[
.header,
.list,
.quote,
.code,
.link,
.bold,
.italic,
]
}
var markdownParser: MarkdownParser {
MarkdownParser(
font: Self.Fonts.body,
color: Self.Color.bold
)
}
}
final class CollapsingMarkdownView: UIView {
/// Called when the collapse state toggles.
var didToggleCollapse: (() -> Void)?
// MARK: - Properties
var isCollapsed = true {
didSet {
guard self.isCollapsed != oldValue else { return }
self.updateCollapsedState()
}
}
var maximumNumberOfLines = 3 {
didSet {
self.checkIfNeedsCollapsing()
self.updateCollapsedState()
self.setNeedsLayout()
}
}
var text: String = "" {
didSet {
self.updateMarkdownContent()
self.setNeedsLayout()
}
}
var lineSpacing: Double = 2 {
didSet {
self.setNeedsLayout()
}
}
let toggleButton = UIButton(type: .system)
private let textView = UITextView()
private let markdownParser = MarkdownManager().markdownParser
private var previousSize: CGSize?
private var actualLineCount: Int = 0
private var needsCollapsing = false
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initialize()
}
override func awakeFromNib() {
super.awakeFromNib()
initialize()
}
private func checkIfNeedsCollapsing() {
guard bounds.width > 0, let font = textView.font, font.lineHeight > 0 else {
needsCollapsing = false
return
}
// Calculate the number of lines in the text
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
let lineHeight = font.lineHeight
// Safely calculate actual line count
actualLineCount = max(1, Int(ceil(textSize.height / lineHeight)))
// Only needs collapsing if actual lines exceed the maximum
needsCollapsing = actualLineCount > maximumNumberOfLines
// Update button visibility
toggleButton.isHidden = !needsCollapsing
}
private func updateCollapsedState() {
// Disable animations for this update
UIView.performWithoutAnimation {
// Update the button title
let title = isCollapsed ? NSLocalizedString("More", comment: "") : NSLocalizedString("Less", comment: "")
toggleButton.setTitle(title, for: .normal)
// Set max lines based on collapsed state
if isCollapsed && needsCollapsing {
textView.textContainer.maximumNumberOfLines = maximumNumberOfLines
} else {
textView.textContainer.maximumNumberOfLines = 0
}
// Button is only visible if content needs collapsing
toggleButton.isHidden = !needsCollapsing
// Force layout updates
textView.layoutIfNeeded()
self.layoutIfNeeded()
self.invalidateIntrinsicContentSize()
}
}
private func initialize() {
// Configure text view
textView.isEditable = false
textView.isScrollEnabled = false
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.textContainer.lineBreakMode = .byTruncatingTail
textView.backgroundColor = .clear
// Make textView selectable to enable link interactions
textView.isSelectable = true
textView.delegate = self
// Important: This prevents selection handles from appearing
textView.dataDetectorTypes = .link
// Configure markdown parser
configureMarkdownParser()
// Add subviews
addSubview(textView)
// Configure toggle button
toggleButton.addTarget(self, action: #selector(toggleCollapsed(_:)), for: .primaryActionTriggered)
addSubview(toggleButton)
setNeedsLayout()
}
private func configureMarkdownParser() {
// Configure markdown parser with desired settings
markdownParser.enabledElements = MarkdownManager.enabledElements
// You can also customize the styling if needed
markdownParser.header.font = MarkdownManager.Fonts.header
markdownParser.list.font = MarkdownManager.Fonts.list
markdownParser.bold.font = MarkdownManager.Fonts.bold
markdownParser.italic.font = MarkdownManager.Fonts.italic
markdownParser.quote.font = MarkdownManager.Fonts.quote
markdownParser.header.color = MarkdownManager.Color.header
markdownParser.bold.color = MarkdownManager.Color.bold
markdownParser.list.color = MarkdownManager.Color.bold
}
// MARK: - Layout
override func layoutSubviews() {
super.layoutSubviews()
UIView.performWithoutAnimation {
// Calculate button height (for spacing)
let buttonHeight = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000)).height
// Set textView frame to leave space for button
textView.frame = CGRect(
x: 0,
y: 0,
width: bounds.width,
height: bounds.height - buttonHeight
)
// Check if layout changed
if previousSize?.width != bounds.width {
checkIfNeedsCollapsing()
updateCollapsedState()
previousSize = bounds.size
}
// Position toggle button at bottom right
let buttonSize = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000))
toggleButton.frame = CGRect(
x: bounds.width - buttonSize.width,
y: textView.frame.maxY,
width: buttonSize.width,
height: buttonHeight
)
}
}
@objc private func toggleCollapsed(_ sender: UIButton) {
isCollapsed.toggle()
didToggleCollapse?()
}
override var intrinsicContentSize: CGSize {
guard bounds.width > 0, let font = textView.font, font.lineHeight > 0 else {
return CGSize(width: UIView.noIntrinsicMetric, height: 0)
}
let lineHeight = font.lineHeight
let buttonHeight = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000)).height
// Always add button height to reserve space for it
if isCollapsed && needsCollapsing {
// When collapsed and needs collapsing, use maximumNumberOfLines
let collapsedHeight = lineHeight * CGFloat(maximumNumberOfLines) +
lineSpacing * CGFloat(max(0, maximumNumberOfLines - 1))
return CGSize(width: UIView.noIntrinsicMetric, height: collapsedHeight + buttonHeight)
} else if !needsCollapsing {
// Text is shorter than max lines - use actual text height
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
return CGSize(width: UIView.noIntrinsicMetric, height: textSize.height + buttonHeight)
} else {
// When expanded and needs collapsing, use full text height plus button
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
return CGSize(width: UIView.noIntrinsicMetric, height: textSize.height + buttonHeight)
}
}
// MARK: - Markdown Processing
private func updateMarkdownContent() {
let attributedString = markdownParser.parse(text)
// Apply line spacing
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineSpacing
mutableAttributedString.addAttribute(
.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: mutableAttributedString.length)
)
textView.attributedText = mutableAttributedString
// Check if content needs collapsing after setting text
checkIfNeedsCollapsing()
updateCollapsedState()
}
}
extension CollapsingMarkdownView: UITextViewDelegate {
// This enables tapping on links while preventing text selection
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
// Open the URL using UIApplication
UIApplication.shared.open(URL)
return false // Return false to prevent the default behavior
}
// This prevents text selection
func textViewDidChangeSelection(_ textView: UITextView) {
textView.selectedTextRange = nil
}
}

View File

@@ -1,343 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 53;
objects = {
/* Begin PBXBuildFile section */
9987603429A4555300818586 /* em_proxy.h in Sources */ = {isa = PBXBuildFile; fileRef = 9999259129A45319005CF020 /* em_proxy.h */; };
/* End PBXBuildFile section */
/* Begin PBXBuildRule section */
CA6094FFF692AC6C1400ACA8 /* PBXBuildRule */ = {
isa = PBXBuildRule;
compilerSpec = com.apple.compilers.proxy.script;
filePatterns = "*/em_proxy.h";
fileType = pattern.proxy;
inputFiles = (
);
isEditable = 0;
name = "Cargo project build";
outputFiles = (
"$(OBJECT_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME)",
);
script = "# generated with cargo-xcode 1.5.0\n# modified to use prebuilt binaries\n\nset -eu;\n\nBUILT_SRC=\"./em_proxy/$LIB_FILE_NAME.a\"\necho Generating Static lib: $BUILT_SRC\nln -f -- \"$BUILT_SRC\" \"$TARGET_BUILD_DIR/$EXECUTABLE_PATH\" || cp \"$BUILT_SRC\" \"$TARGET_BUILD_DIR/$EXECUTABLE_PATH\"\necho \"$BUILT_SRC -> $TARGET_BUILD_DIR/$EXECUTABLE_PATH\"\n\n# xcode generates dep file, but for its own path, so append our rename to it\n#DEP_FILE_SRC=\"minimuxer/target/${CARGO_XCODE_TARGET_TRIPLE}/release/${CARGO_XCODE_CARGO_DEP_FILE_NAME}\"\n#if [ -f \"$DEP_FILE_SRC\" ]; then\n# DEP_FILE_DST=\"${DERIVED_FILE_DIR}/${CARGO_XCODE_TARGET_ARCH}-${EXECUTABLE_NAME}.d\"\n# cp -f \"$DEP_FILE_SRC\" \"$DEP_FILE_DST\"\n# echo >> \"$DEP_FILE_DST\" \"$SCRIPT_OUTPUT_FILE_0: $BUILT_SRC\"\n#fi\n\n# lipo script needs to know all the platform-specific files that have been built\n# archs is in the file name, so that paths don't stay around after archs change\n# must match input for LipoScript\n#FILE_LIST=\"${DERIVED_FILE_DIR}/${ARCHS}-${EXECUTABLE_NAME}.xcfilelist\"\n#touch \"$FILE_LIST\"\n#if ! egrep -q \"$SCRIPT_OUTPUT_FILE_0\" \"$FILE_LIST\" ; then\n# echo >> \"$FILE_LIST\" \"$SCRIPT_OUTPUT_FILE_0\"\n#fi\n";
};
/* End PBXBuildRule section */
/* Begin PBXFileReference section */
9999259129A45319005CF020 /* em_proxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = em_proxy.h; path = em_proxy/em_proxy.h; sourceTree = "<group>"; };
ADDEDBA66A6E1 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
CA60058A9FBE4D17AF51A7D5 /* run */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = run; sourceTree = BUILT_PRODUCTS_DIR; };
CA60C44C93D7916DE57E6EBD /* libem_proxy_static.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libem_proxy_static.a; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
ADDEDBA66A6E2 /* Required for static linking */ = {
isa = PBXGroup;
children = (
ADDEDBA66A6E1 /* libresolv.tbd */,
);
name = "Required for static linking";
sourceTree = "<group>";
};
CA6094FFF69222869D176AE5 /* Products */ = {
isa = PBXGroup;
children = (
CA60C44C93D7916DE57E6EBD /* libem_proxy_static.a */,
CA60058A9FBE4D17AF51A7D5 /* run */,
);
name = Products;
sourceTree = "<group>";
};
CA6094FFF69298AF0B5890DB /* Frameworks */ = {
isa = PBXGroup;
children = (
ADDEDBA66A6E2 /* Required for static linking */,
);
name = Frameworks;
sourceTree = "<group>";
};
CA6094FFF692D65BC3C892A8 = {
isa = PBXGroup;
children = (
9999259129A45319005CF020 /* em_proxy.h */,
CA6094FFF69222869D176AE5 /* Products */,
CA6094FFF69298AF0B5890DB /* Frameworks */,
);
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
CA60058A9FBE37FC563E4BCC /* run-bin */ = {
isa = PBXNativeTarget;
buildConfigurationList = CA603DD75FB437FC563E4BCC /* Build configuration list for PBXNativeTarget "run-bin" */;
buildPhases = (
CA60445C303637FC563E4BCC /* Sources */,
CA6094FFF692AF6EBB7F357C /* Universal Binary lipo */,
);
buildRules = (
CA6094FFF692AC6C1400ACA8 /* PBXBuildRule */,
);
dependencies = (
);
name = "run-bin";
productName = run;
productReference = CA60058A9FBE4D17AF51A7D5 /* run */;
productType = "com.apple.product-type.tool";
};
CA60C44C93D7A30E3695DD59 /* em_proxy-staticlib */ = {
isa = PBXNativeTarget;
buildConfigurationList = CA603DD75FB4A30E3695DD59 /* Build configuration list for PBXNativeTarget "em_proxy-staticlib" */;
buildPhases = (
9987603529A4610700818586 /* ShellScript */,
CA60445C3036A30E3695DD59 /* Sources */,
CA6094FFF692AF6EBB7F357C /* Universal Binary lipo */,
);
buildRules = (
CA6094FFF692AC6C1400ACA8 /* PBXBuildRule */,
);
dependencies = (
);
name = "em_proxy-staticlib";
productName = libem_proxy_static.a;
productReference = CA60C44C93D7916DE57E6EBD /* libem_proxy_static.a */;
productType = "com.apple.product-type.library.static";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
CA6094FFF692E04653AD465F /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
TargetAttributes = {
CA60058A9FBE37FC563E4BCC = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Automatic;
};
CA60C44C93D7A30E3695DD59 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = CA6094FFF69280E02D6C7F57 /* Build configuration list for PBXProject "em_proxy" */;
compatibilityVersion = "Xcode 11.4";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = CA6094FFF692D65BC3C892A8;
productRefGroup = CA6094FFF69222869D176AE5 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
CA60C44C93D7A30E3695DD59 /* em_proxy-staticlib */,
CA60058A9FBE37FC563E4BCC /* run-bin */,
);
};
/* End PBXProject section */
/* Begin PBXShellScriptBuildPhase section */
9987603529A4610700818586 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
./em_proxy/em_proxy.h,
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "bash ./fetch-prebuilt.sh em_proxy\n";
};
CA6094FFF692AF6EBB7F357C /* Universal Binary lipo */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).xcfilelist",
);
name = "Universal Binary lipo";
outputFileListPaths = (
);
outputPaths = (
"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# generated with cargo-xcode 1.5.0\n\n#set -eux; cat \"$DERIVED_FILE_DIR/$ARCHS-$EXECUTABLE_NAME.xcfilelist\" | tr '\\n' '\\0' | xargs -0 lipo -create -output \"$TARGET_BUILD_DIR/$EXECUTABLE_PATH\"\n#if [ ${LD_DYLIB_INSTALL_NAME:+1} ]; then\n# install_name_tool -id \"$LD_DYLIB_INSTALL_NAME\" \"$TARGET_BUILD_DIR/$EXECUTABLE_PATH\"\n#fi\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
CA60445C303637FC563E4BCC /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
CA60445C3036A30E3695DD59 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9987603429A4555300818586 /* em_proxy.h in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
CA604DFE779B37FC563E4BCC /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CARGO_XCODE_CARGO_DEP_FILE_NAME = run.d;
CARGO_XCODE_CARGO_FILE_NAME = run;
PRODUCT_NAME = run;
SUPPORTED_PLATFORMS = macosx;
};
name = Release;
};
CA604DFE779BA30E3695DD59 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CARGO_XCODE_CARGO_DEP_FILE_NAME = libem_proxy.d;
CARGO_XCODE_CARGO_FILE_NAME = libem_proxy.a;
INSTALL_GROUP = "";
INSTALL_MODE_FLAG = "";
INSTALL_OWNER = "";
LIB_FILE_NAME = "";
"LIB_FILE_NAME[sdk=iphoneos*]" = "libem_proxy-ios";
"LIB_FILE_NAME[sdk=iphonesimulator*]" = "libem_proxy-sim";
PRODUCT_NAME = em_proxy_static;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos";
};
name = Release;
};
CA609A517351228BE02872F8 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target";
CARGO_XCODE_BUILD_MODE = debug;
CARGO_XCODE_FEATURES = "";
"CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64;
"CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686;
"CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64;
"CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos;
"CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos;
"CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios;
"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim";
"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*][arch=x86_64*]" = ios;
"CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin;
CURRENT_PROJECT_VERSION = 0.1;
MARKETING_VERSION = 0.1.0;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = em_proxy;
SDKROOT = macosx;
SUPPORTS_MACCATALYST = YES;
};
name = Debug;
};
CA609A5173513CC16B37690B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target";
CARGO_XCODE_BUILD_MODE = release;
CARGO_XCODE_FEATURES = "";
"CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64;
"CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686;
"CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64;
"CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos;
"CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos;
"CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios;
"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim";
"CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*][arch=x86_64*]" = ios;
"CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin;
CURRENT_PROJECT_VERSION = 0.1;
MARKETING_VERSION = 0.1.0;
PRODUCT_NAME = em_proxy;
SDKROOT = macosx;
SUPPORTS_MACCATALYST = YES;
};
name = Release;
};
CA60DE07A83F37FC563E4BCC /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CARGO_XCODE_CARGO_DEP_FILE_NAME = run.d;
CARGO_XCODE_CARGO_FILE_NAME = run;
PRODUCT_NAME = run;
SUPPORTED_PLATFORMS = macosx;
};
name = Debug;
};
CA60DE07A83FA30E3695DD59 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CARGO_XCODE_CARGO_DEP_FILE_NAME = libem_proxy.d;
CARGO_XCODE_CARGO_FILE_NAME = libem_proxy.a;
INSTALL_GROUP = "";
INSTALL_MODE_FLAG = "";
INSTALL_OWNER = "";
LIB_FILE_NAME = "";
"LIB_FILE_NAME[sdk=iphoneos*]" = "libem_proxy-ios";
"LIB_FILE_NAME[sdk=iphonesimulator*]" = "libem_proxy-sim";
PRODUCT_NAME = em_proxy_static;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos";
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
CA603DD75FB437FC563E4BCC /* Build configuration list for PBXNativeTarget "run-bin" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CA604DFE779B37FC563E4BCC /* Release */,
CA60DE07A83F37FC563E4BCC /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
CA603DD75FB4A30E3695DD59 /* Build configuration list for PBXNativeTarget "em_proxy-staticlib" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CA604DFE779BA30E3695DD59 /* Release */,
CA60DE07A83FA30E3695DD59 /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
CA6094FFF69280E02D6C7F57 /* Build configuration list for PBXProject "em_proxy" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CA609A5173513CC16B37690B /* Release */,
CA609A517351228BE02872F8 /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = CA6094FFF692E04653AD465F /* Project object */;
}

381
update_release_notes.py Normal file
View File

@@ -0,0 +1,381 @@
#!/usr/bin/env python3
import subprocess
import sys
import os
import re
IGNORED_AUTHORS = [
]
TAG_MARKER = "###"
HEADER_MARKER = "####"
def run_command(cmd):
"""Run a shell command and return its trimmed output."""
return subprocess.check_output(cmd, shell=True, text=True).strip()
def get_head_commit():
"""Return the HEAD commit SHA."""
return run_command("git rev-parse HEAD")
def get_commit_messages(last_successful, current="HEAD"):
"""Return a list of commit messages between last_successful and current."""
cmd = f"git log {last_successful}..{current} --pretty=format:%s"
output = run_command(cmd)
if not output:
return []
return output.splitlines()
def get_authors_in_range(commit_range, fmt="%an"):
"""Return a set of commit authors in the given commit range using the given format."""
cmd = f"git log {commit_range} --pretty=format:{fmt}"
output = run_command(cmd)
if not output:
return set()
authors = set(line.strip() for line in output.splitlines() if line.strip())
authors = set(authors) - set(IGNORED_AUTHORS)
return authors
def get_first_commit_of_repo():
"""Return the first commit in the repository (root commit)."""
cmd = "git rev-list --max-parents=0 HEAD"
output = run_command(cmd)
return output.splitlines()[0]
def get_branch():
"""
Attempt to determine the branch base (the commit where the current branch diverged
from the default remote branch). Falls back to the repo's first commit.
"""
try:
default_ref = run_command("git rev-parse --abbrev-ref origin/HEAD")
default_branch = default_ref.split('/')[-1]
base_commit = run_command(f"git merge-base HEAD origin/{default_branch}")
return base_commit
except Exception:
return get_first_commit_of_repo()
def get_repo_url():
"""Extract and clean the repository URL from the remote 'origin'."""
url = run_command("git config --get remote.origin.url")
if url.startswith("git@"):
url = url.replace("git@", "https://").replace(":", "/")
if url.endswith(".git"):
url = url[:-4]
return url
def format_contributor(author):
"""
Convert an author name to a GitHub username or first name.
If the author already starts with '@', return it;
otherwise, take the first token and prepend '@'.
"""
if author.startswith('@'):
return author
return f"@{author.split()[0]}"
def format_commit_message(msg):
"""Format a commit message as a bullet point for the release notes."""
msg_clean = msg.lstrip() # remove leading spaces
if msg_clean.startswith("-"):
msg_clean = msg_clean[1:].strip() # remove leading '-' and spaces
return f"- {msg_clean}"
# def generate_release_notes(last_successful, tag, branch):
"""Generate release notes for the given tag."""
current_commit = get_head_commit()
messages = get_commit_messages(last_successful, current_commit)
# Start with the tag header
new_section = f"{TAG_MARKER} {tag}\n"
# What's Changed section (always present)
new_section += f"{HEADER_MARKER} What's Changed\n"
if not messages or last_successful == current_commit:
new_section += "- Nothing...\n"
else:
for msg in messages:
new_section += f"{format_commit_message(msg)}\n"
# New Contributors section (only if there are new contributors)
all_previous_authors = get_authors_in_range(f"{branch}")
recent_authors = get_authors_in_range(f"{last_successful}..{current_commit}")
new_contributors = recent_authors - all_previous_authors
if new_contributors:
new_section += f"\n{HEADER_MARKER} New Contributors\n"
for author in sorted(new_contributors):
new_section += f"- {format_contributor(author)} made their first contribution\n"
# Full Changelog section (only if there are changes)
if messages and last_successful != current_commit:
repo_url = get_repo_url()
changelog_link = f"{repo_url}/compare/{last_successful}...{current_commit}"
new_section += f"\n{HEADER_MARKER} Full Changelog: [{last_successful[:8]}...{current_commit[:8]}]({changelog_link})\n"
return new_section
def generate_release_notes(last_successful, tag, branch):
"""Generate release notes for the given tag."""
current_commit = get_head_commit()
try:
# Try to get commit messages using the provided last_successful commit
messages = get_commit_messages(last_successful, current_commit)
except subprocess.CalledProcessError:
# If the range is invalid (e.g. force push made last_successful obsolete),
# fall back to using the last 10 commits in the current branch.
print("\nInvalid revision range error, using last 10 commits as fallback.\n")
fallback_commit = run_command("git rev-parse HEAD~5")
messages = get_commit_messages(fallback_commit, current_commit)
last_successful = fallback_commit
# Start with the tag header
new_section = f"{TAG_MARKER} {tag}\n"
# What's Changed section (always present)
new_section += f"{HEADER_MARKER} What's Changed\n"
if not messages or last_successful == current_commit:
new_section += "- Nothing...\n"
else:
for msg in messages:
new_section += f"{format_commit_message(msg)}\n"
# New Contributors section (only if there are new contributors)
all_previous_authors = get_authors_in_range(f"{branch}")
recent_authors = get_authors_in_range(f"{last_successful}..{current_commit}")
new_contributors = recent_authors - all_previous_authors
if new_contributors:
new_section += f"\n{HEADER_MARKER} New Contributors\n"
for author in sorted(new_contributors):
new_section += f"- {format_contributor(author)} made their first contribution\n"
# Full Changelog section (only if there are changes)
if messages and last_successful != current_commit:
repo_url = get_repo_url()
changelog_link = f"{repo_url}/compare/{last_successful}...{current_commit}"
new_section += f"\n{HEADER_MARKER} Full Changelog: [{last_successful[:8]}...{current_commit[:8]}]({changelog_link})\n"
return new_section
def update_release_md(existing_content, new_section, tag):
"""
Update input based on rules:
1. If tag exists, update it
2. Special tags (alpha, beta, nightly) stay at the top in that order
3. Numbered tags follow special tags
4. Remove duplicate tags
5. Insert new numbered tags at the top of the numbered section
"""
tag_lower = tag.lower()
is_special_tag = tag_lower in ["alpha", "beta", "nightly"]
# Parse the existing content into sections
if not existing_content:
return new_section
# Split the content into sections by headers
pattern = fr'(^{TAG_MARKER} .*$)'
sections = re.split(pattern, existing_content, flags=re.MULTILINE)
# Create a list to store the processed content
processed_sections = []
# Track special tag positions and whether tag was found
special_tags_map = {"alpha": False, "beta": False, "nightly": False}
last_special_index = -1
tag_found = False
numbered_tag_index = -1
i = 0
while i < len(sections):
# Check if this is a header
if i % 2 == 1: # Headers are at odd indices
header = sections[i]
content = sections[i+1] if i+1 < len(sections) else ""
current_tag = header[3:].strip().lower()
# Check for special tags to track their positions
if current_tag in special_tags_map:
special_tags_map[current_tag] = True
last_special_index = len(processed_sections)
# Check if this is the first numbered tag
elif re.match(r'^[0-9]+\.[0-9]+(\.[0-9]+)?$', current_tag) and numbered_tag_index == -1:
numbered_tag_index = len(processed_sections)
# If this is the tag we're updating, mark it but don't add yet
if current_tag == tag_lower:
if not tag_found: # Replace the first occurrence
tag_found = True
i += 2 # Skip the content
continue
else: # Skip duplicate occurrences
i += 2
continue
# Add the current section
processed_sections.append(sections[i])
i += 1
# Determine where to insert the new section
if tag_found:
# We need to determine the insertion point
if is_special_tag:
# For special tags, insert after last special tag or at beginning
desired_index = -1
for pos, t in enumerate(["alpha", "beta", "nightly"]):
if t == tag_lower:
desired_index = pos
# Find position to insert
insert_pos = 0
for pos, t in enumerate(["alpha", "beta", "nightly"]):
if t == tag_lower:
break
if special_tags_map[t]:
insert_pos = processed_sections.index(f"{TAG_MARKER} {t}")
insert_pos += 2 # Move past the header and content
# Insert at the determined position
processed_sections.insert(insert_pos, new_section)
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
processed_sections.insert(insert_pos, '\n\n')
else:
# For numbered tags, insert after special tags but before other numbered tags
insert_pos = 0
if last_special_index >= 0:
# Insert after the last special tag
insert_pos = last_special_index + 2 # +2 to skip header and content
processed_sections.insert(insert_pos, new_section)
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
processed_sections.insert(insert_pos, '\n\n')
else:
# Tag doesn't exist yet, determine insertion point
if is_special_tag:
# For special tags, maintain alpha, beta, nightly order
special_tags = ["alpha", "beta", "nightly"]
insert_pos = 0
for i, t in enumerate(special_tags):
if t == tag_lower:
# Check if preceding special tags exist
for prev_tag in special_tags[:i]:
if special_tags_map[prev_tag]:
# Find the position after this tag
prev_index = processed_sections.index(f"{TAG_MARKER} {prev_tag}")
insert_pos = prev_index + 2 # Skip header and content
processed_sections.insert(insert_pos, new_section)
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
processed_sections.insert(insert_pos, '\n\n')
else:
# For numbered tags, insert after special tags but before other numbered tags
insert_pos = 0
if last_special_index >= 0:
# Insert after the last special tag
insert_pos = last_special_index + 2 # +2 to skip header and content
processed_sections.insert(insert_pos, new_section)
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
processed_sections.insert(insert_pos, '\n\n')
# Combine sections ensuring proper spacing
result = ""
for i, section in enumerate(processed_sections):
if i > 0 and section.startswith(f"{TAG_MARKER} "):
# Ensure single blank line before headers
if not result.endswith("\n\n"):
result = result.rstrip("\n") + "\n\n"
result += section
return result.rstrip() + "\n"
def retrieve_tag_content(tag, file_path):
if not os.path.exists(file_path):
return ""
with open(file_path, "r") as f:
content = f.read()
# Create a pattern for the tag header (case-insensitive)
pattern = re.compile(fr'^{TAG_MARKER} ' + re.escape(tag) + r'$', re.MULTILINE | re.IGNORECASE)
# Find the tag header
match = pattern.search(content)
if not match:
return ""
# Start after the tag line
start_pos = match.end()
# Skip a newline if present
if start_pos < len(content) and content[start_pos] == "\n":
start_pos += 1
# Find the next tag header after the current tag's content
next_tag_match = re.search(fr'^{TAG_MARKER} ', content[start_pos:], re.MULTILINE)
if next_tag_match:
end_pos = start_pos + next_tag_match.start()
return content[start_pos:end_pos].strip()
else:
# Return until the end of the file if this is the last tag
return content[start_pos:].strip()
def main():
# Update input file
release_file = "release-notes.md"
# Usage: python release.py <last_successful_commit> [tag] [branch]
# Or: python release.py --retrieve <tagname>
args = sys.argv[1:]
if len(args) < 1:
print("Usage: python release.py <last_successful_commit> [tag] [branch]")
print(" or: python release.py --retrieve <tagname>")
sys.exit(1)
# Check if we're retrieving a tag
if args[0] == "--retrieve":
if len(args) < 2:
print("Error: Missing tag name after --retrieve")
sys.exit(1)
tag_content = retrieve_tag_content(args[1], file_path=release_file)
if tag_content:
print(tag_content)
else:
print(f"Tag '{args[1]}' not found in '{release_file}'")
return
# Original functionality for generating release notes
last_successful = args[0]
tag = args[1] if len(args) > 1 else get_head_commit()
branch = args[2] if len(args) > 2 else (os.environ.get("GITHUB_REF") or get_branch())
# Generate release notes
new_section = generate_release_notes(last_successful, tag, branch)
existing_content = ""
if os.path.exists(release_file):
with open(release_file, "r") as f:
existing_content = f.read()
updated_content = update_release_md(existing_content, new_section, tag)
with open(release_file, "w") as f:
f.write(updated_content)
# Output the new section for display
print(new_section)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,3 @@
#include "../Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).DataStructureTests