Compare commits

...

136 Commits

Author SHA1 Message Date
mahee96
0b072e358a updated em_proxy submodule 2026-04-01 15:56:41 -07:00
mahee96
cc74be4b34 cleanup: removed Jailbreak stuff, PatchAppOperation etc. since no longer relevant 2026-04-01 15:44:34 -07:00
mahee96
eabf9dbaaa SendAppOperation: added missing import 2026-04-01 14:24:19 -07:00
mahee96
bd75d62c7b Merge branch 'develop' into merges
# Conflicts:
#	AltStore.xcodeproj/project.pbxproj
#	AltStore/Operations/InstallAppOperation.swift
#	AltStore/Operations/SendAppOperation.swift
#	Dependencies/minimuxer
#	SideStore/MinimuxerWrapper.swift
2026-04-01 14:21:38 -07:00
Huge_Black
75edfad132 Fix AltBackup.ipa is not included
Added a symlink in AltSotore/Resources/AltBackup.ipa that points to build/AltBackup.ipa

It seems Xcode reads all contents in AltSotore/Resources before ipa-altbackup runs, so AltBackup.ipa is missing in the first build. Adding a symlink will cause Xcode to always include that file
2026-03-21 15:30:49 +08:00
Huge_Black
84c5bf40ca Merge pull request #1218 from LiveContainer/develop-lc
Fix widget not working & Only run turn off data shortcut when minimuxer is not ready and below 26.4
2026-03-21 12:08:32 +08:00
Huge_Black
01e73328f8 Only run turn off data shortcut when minimuxer is not ready and below 26.4 2026-03-21 12:01:16 +08:00
Huge_Black
a1f71a8149 Fix widget not working 2026-03-21 11:58:20 +08:00
suprstarrd
8624a8e919 feat: add Mona to Trusted Sources (#1210)
* feat: add Mona to Trusted Sources

Signed-off-by: suprstarrd <business@suprstarrd.com>
2026-03-17 12:28:56 -04:00
mahee96
dfdac41a70 vpn-configuration: fix - properly report active status 2026-03-03 13:24:48 +05:30
mahee96
62ad755920 vpn-configuration: fix typo in sentence 2026-03-03 12:29:22 +05:30
mahee96
baf1aa7fff vpn-configuration: updated to reflect actual implementation 2026-03-03 12:27:12 +05:30
mahee96
395d2bb657 minimuxer: gutted discovery mechanism for finding the fake deviceIP from uTun routes coz it is impossible to walk subnet and kernel route table doesn't tell active associations but just possible routes attempted 2026-03-03 12:20:47 +05:30
mahee96
e32f4fbeae vpn-configuration: removed UI updating state despite bindTunnelConfig() driving it just behind 2026-03-03 10:57:12 +05:30
mahee96
b643f6ae01 minimuxer: fix some stale state issues during VPN up/down cycles or changing config 2026-03-03 10:49:36 +05:30
mahee96
2f0fb3cb71 minimuxer: added back file headers info about authors 2026-03-03 09:59:48 +05:30
mahee96
0f90f9bf2d DeviceEndpoint: fix all minimuxer woes by properly handling usbmuxd signals for listdevices and listen 2026-03-03 09:20:39 +05:30
mahee96
1c8541fdd4 MinimuxerWrapper: remove spam logs 2026-03-03 04:33:07 +05:30
mahee96
4e6882423d Heartbeat: improved logging intent 2026-03-03 04:32:46 +05:30
mahee96
7c3add0e70 IfaceScanner: fix some state update issues 2026-03-03 04:02:07 +05:30
mahee96
c4139473fa IfaceScanner: used the user specified IP to directly test if device is available there instead of assuming it to be under the subnet of tunnel coz it is fake endpoint interceptable by the tunnel's included routes 2026-03-03 03:23:00 +05:30
mahee96
0837f82a69 minimuxer: added binding for the cached tunnel config values so that application layer can provide and listen to updates. 2026-03-03 02:57:39 +05:30
mahee96
c04ebd3ec6 vpn-configuration: added new swiftUI view to settings to view discovered vpn settings and provide an override for deviceIP (if required) by user. 2026-03-03 02:54:58 +05:30
mahee96
0e81515af9 minimuxer: fix usbmuxd socket error handling - restart on first error and dont wait for 5 lol 2026-03-02 17:05:38 +05:30
mahee96
dd4c0be75d minimuxer: match API name 2026-03-02 16:57:56 +05:30
mahee96
7500dd5b17 minimuxer: usbmuxer was not handling socket kill or death properly when app was backgrounded and came into foreground 2026-03-02 16:57:37 +05:30
mahee96
dba84ed015 minimuxer: IfaceScanner - updated to skim thru kernel routing tables and find the included routes of our uTun 2026-03-02 14:59:08 +05:30
mahee96
dbb1e0ff3e minimuxer: gut checks for allowRange/whitelist since we control it on the VPN side 2026-03-02 10:14:21 +05:30
mahee96
431900368a pbxproj - switch to sidestore repos for minimuxer 2026-03-02 08:56:45 +05:30
mahee96
8843a95bfc minimuxer: added empty header dirs required by xcframework 2026-03-02 08:28:25 +05:30
mahee96
4810f3a624 minimuxer: removed target dir from exlcudes since it is in .gitignore 2026-03-02 08:25:37 +05:30
mahee96
954a96b988 minimuxer: added the xcframework libs which was missed before 2026-03-02 08:22:49 +05:30
mahee96
afd32a285d pbxproj - removed local references and used github references for altsign and minimuxer spm packages 2026-03-02 08:11:36 +05:30
mahee96
f8a0ee2c84 cellular-refresh: disable shortcuts spawning until this feature is complete since it interferes with testing for now. 2026-03-02 08:03:41 +05:30
mahee96
9d01a1c116 minimuxer: allow specific network address for sidestore tunnels 2026-03-02 07:58:21 +05:30
mahee96
f6db823816 bug-fix: be on right thread when doing UI work or accessing UI references/UIApplication 2026-03-02 07:34:35 +05:30
mahee96
dfb018bb89 minimuxer: added dynamic lookup of peer for utun(VPN) when not in P2P which is required for sidestore 2026-03-02 07:34:07 +05:30
mahee96
adb91439e4 minimuxer: added dynamic lookup of peer for utun(VPN) when not in P2P which is required for sidestore 2026-03-02 07:33:51 +05:30
mahee96
368a2afad4 bug-fix: ensure main thread when using UIApplication.shared 2026-03-02 04:54:40 +05:30
mahee96
830b66c364 MinimuxerWrapper - for now lets use hardcoded addresses as before - so commented out dynamic discovery 2026-03-02 04:54:20 +05:30
mahee96
40a2137069 minimuxer: install - added device connection check before requesting afc services 2026-03-02 04:53:37 +05:30
mahee96
fa752e3ae6 minimuxer: muxer works now - but has heartbeat issue due to getDevice() failing sometimes, will investigate next. 2026-03-02 03:24:45 +05:30
mahee96
71b319c8e6 pbxproj: restore required slowly - added all missing source files (.h, .c, .cpp) for libimobiledevice and added missing entries under products for libimobiledevice, libem_proxy-swift 2026-03-01 18:00:37 +05:30
mahee96
fd12d36980 pbxproj: restore required slowly - added back libimobiledevice compilation sources (.h, .c, .cpp) 2026-03-01 16:49:03 +05:30
mahee96
cdc41c7954 submodules: updated to latest 2026-03-01 16:31:45 +05:30
mahee96
1fe3e5a6ca pbxproj: restore required slowly - cleanup + added em_proxy target libs properly 2026-03-01 16:31:45 +05:30
mahee96
bf65bce7b5 em_proxy: updated submodule to latest 2026-03-01 16:13:05 +05:30
mahee96
7f3647a230 pbxproj: restore required slowly - streamline em_proxy dependencies 2026-03-01 16:08:38 +05:30
mahee96
850180273c em_proxy: consolidate em_proxy stuff into its own repo (moved em_proxy.xcodeproj, fetch-prebuilt.sh into its repo) 2026-03-01 16:08:07 +05:30
mahee96
6a20d50fb6 pbxproj: restore required slowly - added back xcode auto-discovered roxas related framework, product and xcodeproj (xcode won't let me live in peace otherwise) - now total pbxproj lines = 2.5K 2026-03-01 15:41:35 +05:30
mahee96
79dc65f1c8 pbxproj: restore required slowly - added back xcode auto-discovered framework, xcodeproj (xcode is persistent on adding this back into pbxproj even if I ignore or remove it). 2026-03-01 15:38:00 +05:30
mahee96
a9a2d8780b pbxproj: restore required slowly - added Dependencies/** references after careful vetting by GROUP reference instead of FOLDER reference in xcode 2026-03-01 15:36:22 +05:30
mahee96
2286e78a55 pbxproj: restore required slowly - added Dependencies bare reference 2026-03-01 15:23:01 +05:30
mahee96
3aa3b5df65 pbxproj: restore required slowly - removed Dependencies as a folder and will add back as group coz xcode scans the frameworks and xcodeproj files again and again each time due to it being folder type reference 2026-03-01 15:04:06 +05:30
mahee96
6cdfc211ea minimuxer: check-in package.resolved 2026-03-01 14:57:35 +05:30
mahee96
58ff20c690 minimuxer: reverted to using pre-compiled(and checked-in) RustBridge.xcframework which will be accessible only by MinimuxerBridge which will expose it to minimuxer swift layer under Sources/*.swift . 2026-03-01 14:55:17 +05:30
mahee96
0b865729a7 pbxproj: restore required slowly - added em_proxy swift source and static lib reference 2026-03-01 14:55:12 +05:30
mahee96
7c4cf2ae62 pbxproj: cleanup - removed obsolete xcscheme 2026-03-01 14:54:09 +05:30
mahee96
b0c65b04f2 pbxproj: restore required slowly - added depdencies dir reference 2026-03-01 13:11:39 +05:30
mahee96
f3f8036693 pbxproj: massive cleanup - removed duplicate and stale entries - bring down from 10K to 1.5K lines 2026-03-01 13:06:27 +05:30
ny
6e9e0aee0a fix: revert 26.4 fix partially to fix everywhere 2026-02-28 18:03:35 -05:00
mahee96
ee7c97fd3e workspace: added minimuxer SPM package 2026-03-01 01:49:12 +05:30
mahee96
f5eb669a74 MinimuxerWrapper: cleanup 2026-03-01 01:48:45 +05:30
mahee96
3af3aa071c MinimuxerWrapper: streamline the logging 2026-03-01 01:21:06 +05:30
mahee96
857c1e03e0 minimuxer: use newly exposed Swift wrappers in MinimuxerWrapper.swift 2026-03-01 01:20:47 +05:30
mahee96
c09c290973 minimuxer: removed old minimuxer binaries fetch from the fetch script 2026-03-01 01:20:47 +05:30
mahee96
7b8884a8ac pbxproj: removed minimuxer as a static lib and switched to SPM based 2026-03-01 01:18:18 +05:30
mahee96
fd797fdf88 minimuxer: removed old xcodeproj 2026-03-01 01:18:18 +05:30
mahee96
b18ce43a64 minimuxer: removed old xcodeproj 2026-03-01 01:18:18 +05:30
mahee96
e90b8a0e32 minimuxer: switch to build tool plugin for rust build with SPM target 2026-03-01 01:18:18 +05:30
mahee96
bb578316f1 minimuxer: use zip-foundation instead of process for zip/unzip 2026-02-28 15:08:17 +05:30
mahee96
9579d46bed minimuxer: added few more files that were missed 2026-02-28 14:56:17 +05:30
mahee96
a916293585 minimuxer: added Package.swift which was missed 2026-02-28 14:46:51 +05:30
mahee96
f634b52e44 minimuxer: updated wrapper to use spm based reference of swift-minimuxer 2026-02-28 14:23:18 +05:30
mahee96
a6baf59649 minimuxer: full rewrite in swift from rust 2026-02-28 14:22:55 +05:30
ny
a505d04215 fix: 26.4 patch, add correct dest
- update minimuxer
2026-02-27 22:21:41 -05:00
mahee96
95953ca0e9 IfManager: added String representation for readability 2026-02-28 06:45:12 +05:30
mahee96
3b9b45a06f pbxproj: was missing references to IfManager.swift 2026-02-28 04:41:33 +05:30
mahee96
ac277aa6eb minimuxer: added @nythepegasus's patch as-is 2026-02-28 04:24:03 +05:30
mahee96
7926452661 minimuxer: added @nythepegasus's patch as-is 2026-02-28 04:23:58 +05:30
mahee96
dfb01c2ae5 minimuxer: updated submodule to latest 2026-02-28 03:52:35 +05:30
mahee96
bff192be4e ALTSign cleanup 2026-02-25 08:16:42 +05:30
mahee96
a863b2f39a ALTSign cleanup 2026-02-25 08:15:43 +05:30
mahee96
bf72a0edfc CI: add what xcode is suggesting (xcode noise from auto create schemes) 2026-02-25 08:15:31 +05:30
mahee96
06ad6488cc CI: changelog in md was incorrect in release notes 2026-02-25 08:08:46 +05:30
mahee96
12f84b2365 CI: fixes for author 2026-02-25 07:46:54 +05:30
mahee96
118f64de8a CI: multiple fixes 2026-02-25 07:33:55 +05:30
mahee96
555bb3d985 CI: remove harcoded repo name and use incoming 2026-02-25 03:30:57 +05:30
mahee96
25925aceef CI: create tag if required 2026-02-25 03:18:31 +05:30
mahee96
5444fdd9bb CI: handle zipping irrespective of encryption is possible or not 2026-02-25 03:14:50 +05:30
mahee96
f91e0a6295 fix compilation issue after recent merge 2026-02-25 03:00:34 +05:30
mahee96
4e4f0f6a3f Merge branch 'cell-staging' into develop 2026-02-25 02:50:13 +05:30
mahee96
9706a43bc1 ci - decouple source_metadata.json generation from deploy 2026-02-25 02:41:58 +05:30
mahee96
d748a89b47 ci - do not encrypt build logs if password unavailable (forks CI friendly) 2026-02-25 01:45:23 +05:30
mahee96
f412f6df23 Merge branch 'cell-shortcut' into cell-staging
# Conflicts:
#	AltStore/Operations/SendAppOperation.swift
2026-02-25 01:20:56 +05:30
mahee96
b8354d3d0e cleanup - xcode brought these new entries for schemes 2026-02-25 01:18:09 +05:30
mahee96
191ce15d55 Merge branch 'staging' into develop 2026-02-25 00:50:28 +05:30
mahee96
7f8f4fa9a7 ci: fixes for yml 2026-02-25 00:42:02 +05:30
mahee96
ad54cbadc1 ci: fixes for yml 2026-02-25 00:40:09 +05:30
mahee96
eb251b89c9 ci: fixes for yml 2026-02-25 00:30:07 +05:30
mahee96
9efea00d09 ci: use runner number as build number 2026-02-25 00:25:11 +05:30
mahee96
7df29ca23b ci: fixes in workflow.py 2026-02-25 00:10:40 +05:30
mahee96
d06520d46f ci: fix - tag was not pushed 2026-02-25 00:08:41 +05:30
mahee96
1ad0fe23fc ci: fix - version number was inconsistent across deployment of beta channels 2026-02-25 00:04:42 +05:30
mahee96
21bbcd69f8 ci: updated stable workflow as per workflow.py and other workflows 2026-02-24 22:07:36 +05:30
mahee96
fd920be3bf ci: updated PR workflow to reflect latest 2026-02-24 21:15:28 +05:30
mahee96
16bb57c825 ci: use proper messaging for upstream tag 2026-02-24 20:51:25 +05:30
mahee96
bf5b1c935c ci: cleanup - removed obsolete stuffs 2026-02-24 20:43:35 +05:30
mahee96
1b1c7c58e2 ci: moved cache maintenance script into obsolete 2026-02-24 20:43:03 +05:30
Huge_Black
31e8eb7996 Merge pull request #1183 from LiveContainer/develop
Fix issues profile/afc and use main profile issue
2026-02-24 23:07:11 +08:00
mahee96
47db2c3d5d ci: fix typo in brew command 2026-02-24 20:35:50 +05:30
mahee96
967b9f7572 ci: removed caching for ldid and xcbeautify coz they take much more time to restore than install 2026-02-24 20:32:02 +05:30
mahee96
b91bcee70f ci: added some names to few steps 2026-02-24 20:27:46 +05:30
mahee96
25f34c6f69 ci: added back the nightly schedule checking for new commits and only then build 2026-02-24 20:23:12 +05:30
mahee96
b7085aaeca ci: improve speed by caching brew install step and reading project settings from dumped xcodebuild -showProjectSettings instead of invoking each time. 2026-02-24 18:54:41 +05:30
mahee96
046d2788b9 ci: fix indentation in release notes 2026-02-24 18:20:27 +05:30
mahee96
f44ed0a947 ci: fix indentation in release notes 2026-02-24 18:19:54 +05:30
Huge_Black
d356045b5d bug-fix: fix crash when viewing app ids 2026-02-24 16:08:15 +08:00
Huge_Black
a54881a1c8 bug-fix: fix profile not installed 2026-02-24 16:08:15 +08:00
Huge_Black
e27d44eece fix build issue 2026-02-24 16:08:15 +08:00
Huge_Black
c2cecb63ac bug-fix: fix useMainProfile not saved / not set during initialization
SideStore is killed by iOS before CoreData commit changes to db.

Detect if the app is installed with useMainProfile by checking if applicationIdentifier matches
2026-02-24 16:08:15 +08:00
Huge_Black
381e09d87c bug-fix: Fix crash when installing apps 2026-02-24 16:08:15 +08:00
mahee96
efbb40982e ci: fix indentation in release notes 2026-02-24 13:20:14 +05:30
mahee96
c1a033a627 staging: added some redundant files to gitignore 2026-02-24 13:14:40 +05:30
mahee96
1449f8c74f staging: prepare new branch for alpha release channels and high velocity development 2026-02-24 13:04:44 +05:30
spidy123222
625389ab96 Add Exit Shortcut 2025-04-08 15:19:33 -07:00
spidy123222
f7e34cbbe9 Rewrite SendAppOperation execution to allow to wait for shortcut execution. 2025-04-08 15:19:33 -07:00
spidy123222
0fe8d7fed9 Move to SendAppOperation 2025-04-08 15:19:33 -07:00
spidy123222
1a1aa42e02 move it behind pendiungunitcount 60 2025-04-08 15:19:33 -07:00
spidy123222
7ff4b48223 Move attempt to a higher Stage. 2025-04-08 15:19:33 -07:00
spidy123222
4801f6e8f2 Attempt a million 2025-04-08 15:19:33 -07:00
Spidy123222
ff28f6fa8f Add files via upload
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2025-04-08 15:19:33 -07:00
Spidy123222
2d141afbaf remove from install apps
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2025-04-08 15:19:33 -07:00
Spidy123222
06e38aae00 Hopefully fix problem
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2025-04-08 15:19:33 -07:00
Spidy123222
d8783230a7 fix error for open link
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2025-04-08 15:19:33 -07:00
Spidy123222
6c479bfede test open URL
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2025-04-08 15:19:33 -07:00
66 changed files with 2014 additions and 10910 deletions

View File

@@ -1,63 +0,0 @@
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

@@ -2,7 +2,7 @@ name: Alpha SideStore Build
on: on:
push: push:
branches: [alpha] branches: [staging]
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@@ -13,6 +13,8 @@ jobs:
build: build:
runs-on: macos-26 runs-on: macos-26
env: env:
DEPLOY_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NAME: Alpha RELEASE_NAME: Alpha
CHANNEL: alpha CHANNEL: alpha
UPSTREAM_CHANNEL: "nightly" UPSTREAM_CHANNEL: "nightly"
@@ -23,36 +25,33 @@ jobs:
submodules: recursive submodules: recursive
fetch-depth: 0 fetch-depth: 0
- name: Find Last Successful commit
run: |
LAST_SUCCESSFUL_COMMIT=$(python3 scripts/ci/workflow.py last-successful-commit \
"false" "${{ env.CHANNEL }}" || echo "")
echo "LAST_SUCCESSFUL_COMMIT=$LAST_SUCCESSFUL_COMMIT" | tee -a $GITHUB_ENV
- run: brew install ldid xcbeautify - run: brew install ldid xcbeautify
# -------------------------------------------------- # --------------------------------------------------
# runtime env setup # runtime env setup
# -------------------------------------------------- # --------------------------------------------------
- uses: actions/checkout@v4
with:
repository: "SideStore/beta-build-num"
ref: ${{ env.CHANNEL }}
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: "Dependencies/beta-build-num"
fetch-depth: 1
- name: Setup Env - name: Setup Env
run: | run: |
BUILD_NUM=$(python3 scripts/ci/workflow.py reserve_build_number 'Dependencies/beta-build-num') BUILD_NUM="${{ github.run_number }}"
MARKETING_VERSION=$(python3 scripts/ci/workflow.py get-marketing-version) MARKETING_VERSION=$(python3 scripts/ci/workflow.py get-marketing-version)
SHORT_COMMIT=$(python3 scripts/ci/workflow.py commid-id) SHORT_COMMIT=$(python3 scripts/ci/workflow.py commit-id)
QUALIFIED_VERSION=$(python3 scripts/ci/workflow.py compute-qualified \ NORMALIZED_VERSION=$(python3 scripts/ci/workflow.py compute-normalized \
"$MARKETING_VERSION" \ "$MARKETING_VERSION" \
"$BUILD_NUM" \ "$BUILD_NUM" \
"${{ env.CHANNEL }}" \
"$SHORT_COMMIT") "$SHORT_COMMIT")
python3 scripts/ci/workflow.py set-marketing-version "$QUALIFIED_VERSION" python3 scripts/ci/workflow.py set-marketing-version "$NORMALIZED_VERSION"
echo "BUILD_NUM=$BUILD_NUM" | tee -a $GITHUB_ENV echo "BUILD_NUM=$BUILD_NUM" | tee -a $GITHUB_ENV
echo "SHORT_COMMIT=$SHORT_COMMIT" | tee -a $GITHUB_ENV echo "SHORT_COMMIT=$SHORT_COMMIT" | tee -a $GITHUB_ENV
echo "MARKETING_VERSION=$QUALIFIED_VERSION" | tee -a $GITHUB_ENV echo "MARKETING_VERSION=$NORMALIZED_VERSION" | tee -a $GITHUB_ENV
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0 uses: maxim-lobanov/setup-xcode@v1.6.0
@@ -89,7 +88,9 @@ jobs:
python3 scripts/ci/workflow.py clean-spm-cache python3 scripts/ci/workflow.py clean-spm-cache
- name: Boot simulator (async) - name: Boot simulator (async)
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }} if: >
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_RUN == '1'
run: | run: |
mkdir -p build/logs mkdir -p build/logs
python3 scripts/ci/workflow.py boot-sim-async "iPhone 17 Pro" python3 scripts/ci/workflow.py boot-sim-async "iPhone 17 Pro"
@@ -106,7 +107,9 @@ jobs:
- name: Tests Build - name: Tests Build
id: test-build id: test-build
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }} if: >
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_BUILD == '1'
env: env:
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }} BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
run: | run: |
@@ -125,7 +128,9 @@ jobs:
- name: Tests Run - name: Tests Run
id: test-run id: test-run
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }} if: >
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_RUN == '1'
env: env:
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }} BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
run: | run: |
@@ -138,20 +143,24 @@ jobs:
# -------------------------------------------------- # --------------------------------------------------
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: encrypted-build-logs-${{ env.MARKETING_VERSION }}.zip name: build-logs-${{ env.MARKETING_VERSION }}.zip
path: encrypted-build-logs.zip path: build-logs.zip
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }} if: >
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_BUILD == '1'
with: with:
name: encrypted-tests-build-logs-${{ env.SHORT_COMMIT }}.zip name: tests-build-logs-${{ env.SHORT_COMMIT }}.zip
path: encrypted-tests-build-logs.zip path: tests-build-logs.zip
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }} if: >
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_RUN == '1'
with: with:
name: encrypted-tests-run-logs-${{ env.SHORT_COMMIT }}.zip name: tests-run-logs-${{ env.SHORT_COMMIT }}.zip
path: encrypted-tests-run-logs.zip path: tests-run-logs.zip
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
@@ -163,30 +172,21 @@ jobs:
path: SideStore.dSYMs.zip path: SideStore.dSYMs.zip
- uses: actions/checkout@v4 - uses: actions/checkout@v4
if: env.DEPLOY_KEY != ''
with: with:
repository: "SideStore/apps-v2.json" repository: "SideStore/apps-v2.json"
ref: "main" ref: "main"
token: ${{ secrets.CROSS_REPO_PUSH_KEY }} token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: "SideStore/apps-v2.json" path: "SideStore/apps-v2.json"
# -------------------------------------------------- - name: Generate Metadata
# deploy
# --------------------------------------------------
- name: Deploy
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
PRODUCT_NAME=$(python3 scripts/ci/workflow.py get-product-name) python3 scripts/ci/workflow.py dump-project-settings
BUNDLE_ID=$(python3 scripts/ci/workflow.py get-bundle-id) PRODUCT_NAME=$(python3 scripts/ci/workflow.py read-product-name)
SOURCE_JSON="_includes/source.json" BUNDLE_ID=$(python3 scripts/ci/workflow.py read-bundle-id)
IPA_NAME="$PRODUCT_NAME.ipa" IPA_NAME="$PRODUCT_NAME.ipa"
LAST_SUCCESSFUL_COMMIT=$(python3 scripts/ci/workflow.py last-successful-commit \ python3 scripts/ci/workflow.py generate-metadata \
"${{ github.workflow }}" "$CHANNEL")
python3 scripts/ci/workflow.py deploy \
SideStore/apps-v2.json \
"$SOURCE_JSON" \
"$CHANNEL" \ "$CHANNEL" \
"$SHORT_COMMIT" \ "$SHORT_COMMIT" \
"$MARKETING_VERSION" \ "$MARKETING_VERSION" \
@@ -195,8 +195,23 @@ jobs:
"$IPA_NAME" \ "$IPA_NAME" \
"$LAST_SUCCESSFUL_COMMIT" "$LAST_SUCCESSFUL_COMMIT"
RELEASE_NOTES=$(python3 scripts/ci/workflow.py retrieve-release-notes "$CHANNEL") - name: Deploy
if: env.DEPLOY_KEY != ''
run: |
SOURCE_JSON="_includes/source.json"
python3 scripts/ci/workflow.py deploy \
SideStore/apps-v2.json \
"$SOURCE_JSON" \
"$CHANNEL" \
"$MARKETING_VERSION"
# --------------------------------------------------
# upload release to GH
# --------------------------------------------------
- name: Upload Release
run: |
python3 scripts/ci/workflow.py upload-release \ python3 scripts/ci/workflow.py upload-release \
"$RELEASE_NAME" \ "$RELEASE_NAME" \
"$CHANNEL" \ "$CHANNEL" \

View File

@@ -15,6 +15,8 @@ jobs:
build: build:
runs-on: macos-26 runs-on: macos-26
env: env:
DEPLOY_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NAME: Nightly RELEASE_NAME: Nightly
CHANNEL: nightly CHANNEL: nightly
UPSTREAM_CHANNEL: "" UPSTREAM_CHANNEL: ""
@@ -25,43 +27,63 @@ jobs:
submodules: recursive submodules: recursive
fetch-depth: 0 fetch-depth: 0
- name: Find Last Successful commit
run: |
LAST_SUCCESSFUL_COMMIT=$(python3 scripts/ci/workflow.py last-successful-commit \
"false" "${{ env.CHANNEL }}" || echo "")
echo "LAST_SUCCESSFUL_COMMIT=$LAST_SUCCESSFUL_COMMIT" | tee -a $GITHUB_ENV
- name: Check for new changes (on schedule)
id: check_changes
if: github.event_name == 'schedule'
run: |
NEW_COMMITS=$(python3 scripts/ci/workflow.py count-new-commits "$LAST_SUCCESSFUL_COMMIT")
SHOULD_BUILD=$([ "${NEW_COMMITS:-0}" -ge 1 ] && echo true || echo false)
echo "should_build=$SHOULD_BUILD" >> $GITHUB_OUTPUT
echo "NEW_COMMITS=$NEW_COMMITS" | tee -a $GITHUB_ENV
- name: Should Skip Building (on schedule)
id: build_gate
run: |
SHOULD_SKIP=$(
{ [ "${{ github.event_name }}" = "schedule" ] && \
[ "${{ steps.check_changes.outputs.should_build }}" != "true" ]; \
} && echo true || echo false
)
echo "should_skip=$SHOULD_SKIP" >> $GITHUB_OUTPUT
- run: brew install ldid xcbeautify - run: brew install ldid xcbeautify
if: steps.build_gate.outputs.should_skip != 'true'
# -------------------------------------------------- # --------------------------------------------------
# runtime env setup # runtime env setup
# -------------------------------------------------- # --------------------------------------------------
- uses: actions/checkout@v4
with:
repository: "SideStore/beta-build-num"
ref: ${{ env.CHANNEL }}
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: "Dependencies/beta-build-num"
fetch-depth: 1
- name: Setup Env - name: Setup Env
if: steps.build_gate.outputs.should_skip != 'true'
run: | run: |
BUILD_NUM=$(python3 scripts/ci/workflow.py reserve_build_number 'Dependencies/beta-build-num') BUILD_NUM="${{ github.run_number }}"
MARKETING_VERSION=$(python3 scripts/ci/workflow.py get-marketing-version) MARKETING_VERSION=$(python3 scripts/ci/workflow.py get-marketing-version)
SHORT_COMMIT=$(python3 scripts/ci/workflow.py commid-id) SHORT_COMMIT=$(python3 scripts/ci/workflow.py commit-id)
QUALIFIED_VERSION=$(python3 scripts/ci/workflow.py compute-qualified \ NORMALIZED_VERSION=$(python3 scripts/ci/workflow.py compute-normalized \
"$MARKETING_VERSION" \ "$MARKETING_VERSION" \
"$BUILD_NUM" \ "$BUILD_NUM" \
"${{ env.CHANNEL }}" \
"$SHORT_COMMIT") "$SHORT_COMMIT")
python3 scripts/ci/workflow.py set-marketing-version "$QUALIFIED_VERSION" python3 scripts/ci/workflow.py set-marketing-version "$NORMALIZED_VERSION"
echo "BUILD_NUM=$BUILD_NUM" | tee -a $GITHUB_ENV echo "BUILD_NUM=$BUILD_NUM" | tee -a $GITHUB_ENV
echo "SHORT_COMMIT=$SHORT_COMMIT" | tee -a $GITHUB_ENV echo "SHORT_COMMIT=$SHORT_COMMIT" | tee -a $GITHUB_ENV
echo "MARKETING_VERSION=$QUALIFIED_VERSION" | tee -a $GITHUB_ENV echo "MARKETING_VERSION=$NORMALIZED_VERSION" | tee -a $GITHUB_ENV
- name: Setup Xcode - name: Setup Xcode
if: steps.build_gate.outputs.should_skip != 'true'
uses: maxim-lobanov/setup-xcode@v1.6.0 uses: maxim-lobanov/setup-xcode@v1.6.0
with: with:
xcode-version: "26.2" xcode-version: "26.2"
- name: Restore Cache (exact) - name: Restore Cache (exact)
if: steps.build_gate.outputs.should_skip != 'true'
id: xcode-cache-exact id: xcode-cache-exact
uses: actions/cache/restore@v3 uses: actions/cache/restore@v3
with: with:
@@ -71,7 +93,9 @@ jobs:
key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }} key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }}
- name: Restore Cache (last) - name: Restore Cache (last)
if: steps.xcode-cache-exact.outputs.cache-hit != 'true' if: >
steps.build_gate.outputs.should_skip != 'true' &&
steps.xcode-cache-exact.outputs.cache-hit != 'true'
id: xcode-cache-fallback id: xcode-cache-fallback
uses: actions/cache/restore@v3 uses: actions/cache/restore@v3
with: with:
@@ -84,19 +108,23 @@ jobs:
# build and test # build and test
# -------------------------------------------------- # --------------------------------------------------
- name: Clean - name: Clean
if: contains(github.event.head_commit.message, '[--clean-build]') if: steps.build_gate.outputs.should_skip != 'true' && contains(github.event.head_commit.message, '[--clean-build]')
run: | run: |
python3 scripts/ci/workflow.py clean python3 scripts/ci/workflow.py clean
python3 scripts/ci/workflow.py clean-derived-data python3 scripts/ci/workflow.py clean-derived-data
python3 scripts/ci/workflow.py clean-spm-cache python3 scripts/ci/workflow.py clean-spm-cache
- name: Boot simulator (async) - name: Boot simulator (async)
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }} if: >
steps.build_gate.outputs.should_skip != 'true' &&
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_RUN == '1'
run: | run: |
mkdir -p build/logs mkdir -p build/logs
python3 scripts/ci/workflow.py boot-sim-async "iPhone 17 Pro" python3 scripts/ci/workflow.py boot-sim-async "iPhone 17 Pro"
- name: Build - name: Build
if: steps.build_gate.outputs.should_skip != 'true'
id: build id: build
env: env:
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }} BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
@@ -107,8 +135,11 @@ jobs:
exit $STATUS exit $STATUS
- name: Tests Build - name: Tests Build
if: >
steps.build_gate.outputs.should_skip != 'true' &&
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_BUILD == '1'
id: test-build id: test-build
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
env: env:
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }} BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
run: | run: |
@@ -117,7 +148,9 @@ jobs:
exit $STATUS exit $STATUS
- name: Save Cache - name: Save Cache
if: ${{ steps.xcode-cache-fallback.outputs.cache-hit != 'true' }} if: >
steps.build_gate.outputs.should_skip != 'true' &&
steps.xcode-cache-fallback.outputs.cache-hit != 'true'
uses: actions/cache/save@v3 uses: actions/cache/save@v3
with: with:
path: | path: |
@@ -126,8 +159,11 @@ jobs:
key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }} key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }}
- name: Tests Run - name: Tests Run
if: >
steps.build_gate.outputs.should_skip != 'true' &&
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_RUN == '1'
id: test-run id: test-run
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
env: env:
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }} BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
run: | run: |
@@ -139,56 +175,57 @@ jobs:
# artifacts # artifacts
# -------------------------------------------------- # --------------------------------------------------
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.build_gate.outputs.should_skip != 'true'
with: with:
name: encrypted-build-logs-${{ env.MARKETING_VERSION }}.zip name: build-logs-${{ env.MARKETING_VERSION }}.zip
path: encrypted-build-logs.zip path: build-logs.zip
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }} if: >
steps.build_gate.outputs.should_skip != 'true' &&
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_BUILD == '1'
with: with:
name: encrypted-tests-build-logs-${{ env.SHORT_COMMIT }}.zip name: tests-build-logs-${{ env.SHORT_COMMIT }}.zip
path: encrypted-tests-build-logs.zip path: tests-build-logs.zip
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }} if: >
steps.build_gate.outputs.should_skip != 'true' &&
vars.ENABLE_TESTS == '1' &&
vars.ENABLE_TESTS_RUN == '1'
with: with:
name: encrypted-tests-run-logs-${{ env.SHORT_COMMIT }}.zip name: tests-run-logs-${{ env.SHORT_COMMIT }}.zip
path: encrypted-tests-run-logs.zip path: tests-run-logs.zip
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.build_gate.outputs.should_skip != 'true'
with: with:
name: SideStore-${{ env.MARKETING_VERSION }}.ipa name: SideStore-${{ env.MARKETING_VERSION }}.ipa
path: SideStore.ipa path: SideStore.ipa
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.build_gate.outputs.should_skip != 'true'
with: with:
name: SideStore-${{ env.MARKETING_VERSION }}-dSYMs.zip name: SideStore-${{ env.MARKETING_VERSION }}-dSYMs.zip
path: SideStore.dSYMs.zip path: SideStore.dSYMs.zip
- uses: actions/checkout@v4 - uses: actions/checkout@v4
if: steps.build_gate.outputs.should_skip != 'true' && env.DEPLOY_KEY != ''
with: with:
repository: "SideStore/apps-v2.json" repository: "SideStore/apps-v2.json"
ref: "main" ref: "main"
token: ${{ secrets.CROSS_REPO_PUSH_KEY }} token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: "SideStore/apps-v2.json" path: "SideStore/apps-v2.json"
# -------------------------------------------------- - name: Generate Metadata
# deploy if: steps.build_gate.outputs.should_skip != 'true'
# --------------------------------------------------
- name: Deploy
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
PRODUCT_NAME=$(python3 scripts/ci/workflow.py get-product-name) python3 scripts/ci/workflow.py dump-project-settings
BUNDLE_ID=$(python3 scripts/ci/workflow.py get-bundle-id) PRODUCT_NAME=$(python3 scripts/ci/workflow.py read-product-name)
SOURCE_JSON="_includes/source.json" BUNDLE_ID=$(python3 scripts/ci/workflow.py read-bundle-id)
IPA_NAME="$PRODUCT_NAME.ipa" IPA_NAME="$PRODUCT_NAME.ipa"
LAST_SUCCESSFUL_COMMIT=$(python3 scripts/ci/workflow.py last-successful-commit \ python3 scripts/ci/workflow.py generate-metadata \
"${{ github.workflow }}" "$CHANNEL")
python3 scripts/ci/workflow.py deploy \
SideStore/apps-v2.json \
"$SOURCE_JSON" \
"$CHANNEL" \ "$CHANNEL" \
"$SHORT_COMMIT" \ "$SHORT_COMMIT" \
"$MARKETING_VERSION" \ "$MARKETING_VERSION" \
@@ -197,8 +234,24 @@ jobs:
"$IPA_NAME" \ "$IPA_NAME" \
"$LAST_SUCCESSFUL_COMMIT" "$LAST_SUCCESSFUL_COMMIT"
RELEASE_NOTES=$(python3 scripts/ci/workflow.py retrieve-release-notes "$CHANNEL") - name: Deploy
if: steps.build_gate.outputs.should_skip != 'true' && env.DEPLOY_KEY != ''
run: |
SOURCE_JSON="_includes/source.json"
python3 scripts/ci/workflow.py deploy \
SideStore/apps-v2.json \
"$SOURCE_JSON" \
"$CHANNEL" \
"$MARKETING_VERSION"
# --------------------------------------------------
# upload release to GH
# --------------------------------------------------
- name: Upload Release
if: steps.build_gate.outputs.should_skip != 'true'
run: |
python3 scripts/ci/workflow.py upload-release \ python3 scripts/ci/workflow.py upload-release \
"$RELEASE_NAME" \ "$RELEASE_NAME" \
"$CHANNEL" \ "$CHANNEL" \

View File

@@ -1,28 +0,0 @@
name: Alpha SideStore build
on:
push:
branches:
- develop-alpha
# cancel duplicate run if from same branch
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
Reusable-build:
uses: ./.github/workflows/reusable-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Alpha"
bundle_id: "com.SideStore.SideStore"
# bundle_id_suffix: ".Alpha"
is_beta: true
publish: ${{ vars.PUBLISH_ALPHA_UPDATES == 'true' }}
is_shared_build_num: false
release_tag: "alpha"
release_name: "Alpha"
upstream_tag: "nightly"
upstream_name: "Nightly"
secrets:
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}

View File

@@ -1,103 +0,0 @@
name: Beta SideStore build
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' # example: 1.0.0-beta.1
jobs:
build:
name: Build and upload SideStore Beta
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-14'
version: '15.4'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: brew install ldid
- name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Echo version
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.version }}
- name: Cache Build
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata
- name: Build SideStore
run: make build | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: make fakesign
- name: Convert to IPA
run: make ipa
- 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 beta release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
draft: true
prerelease: 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. -->
Beta builds are hand-picked builds from development commits that will allow you to try out new features earlier than normal. However, **they might contain bugs and other issues. Use at your own risk!**
## 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 }}`
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
# Ensure we are in root directory
cd "$(dirname "$0")/../.."
DATE=`date -u +'%Y.%m.%d'`
BUILD_NUM=1
# Use RELEASE_CHANNEL from the environment variable or default to "beta"
RELEASE_CHANNEL=${RELEASE_CHANNEL:-"beta"}
write() {
sed -e "/MARKETING_VERSION = .*/s/$/-$RELEASE_CHANNEL.$DATE.$BUILD_NUM+$(git rev-parse --short HEAD)/" -i '' Build.xcconfig
echo "$DATE,$BUILD_NUM" > build_number.txt
}
if [ ! -f "build_number.txt" ]; then
write
exit 0
fi
LAST_DATE=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $1'`
LAST_BUILD_NUM=`cat build_number.txt | perl -n -e '/([^,]*),([^ ]*)$/ && print $2'`
# if [[ "$DATE" != "$LAST_DATE" ]]; then
# write
# else
# BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
# write
# fi
# Build number is always incremental
BUILD_NUM=`expr $LAST_BUILD_NUM + 1`
write

View File

@@ -1,82 +0,0 @@
name: Nightly SideStore Build
on:
push:
branches:
- develop
schedule:
- cron: '0 0 * * *' # Runs every night at midnight UTC
workflow_dispatch: # Allows manual trigger
# cancel duplicate run if from same branch
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
check-changes:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
outputs:
has_changes: ${{ steps.check.outputs.has_changes }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Ensure full history
- name: Get last successful workflow run
id: get_last_success
run: |
LAST_SUCCESS=$(gh run list --workflow "Nightly SideStore Build" --json createdAt,conclusion \
--jq '[.[] | select(.conclusion=="success")][0].createdAt' || echo "")
echo "Last successful run: $LAST_SUCCESS"
echo "last_success=$LAST_SUCCESS" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for new commits since last successful build
id: check
run: |
if [ -n "$LAST_SUCCESS" ]; then
NEW_COMMITS=$(git rev-list --count --since="$LAST_SUCCESS" origin/develop)
COMMIT_LOG=$(git log --since="$LAST_SUCCESS" --pretty=format:"%h %s" origin/develop)
else
NEW_COMMITS=1
COMMIT_LOG=$(git log -n 10 --pretty=format:"%h %s" origin/develop) # Show last 10 commits if no history
fi
echo "Has changes: $NEW_COMMITS"
echo "New commits since last successful build:"
echo "$COMMIT_LOG"
if [ "$NEW_COMMITS" -gt 0 ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LAST_SUCCESS: ${{ env.last_success }}
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-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Nightly"
bundle_id: "com.SideStore.SideStore"
# bundle_id_suffix: ".Nightly"
is_beta: true
publish: ${{ vars.PUBLISH_NIGHTLY_UPDATES == 'true' }}
is_shared_build_num: false
release_tag: "nightly"
release_name: "Nightly"
upstream_tag: "0.5.10"
upstream_name: "Stable"
secrets:
CROSS_REPO_PUSH_KEY: ${{ secrets.CROSS_REPO_PUSH_KEY }}
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}

View File

@@ -1,105 +0,0 @@
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
needs: [shared, build] # 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

View File

@@ -1,358 +0,0 @@
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-26'
version: '26.0'
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) 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 ">>>>>>>>> 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

View File

@@ -1,281 +0,0 @@
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: List files to upload
id: list_uploads
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
echo ""
FILES="SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip"
if [[ "${{ vars.ENABLE_TESTS }}" == "1" && "${{ vars.ENABLE_TESTS_BUILD }}" == "1" ]]; then
FILES="$FILES encrypted-tests-build-logs.zip"
fi
if [[ "${{ vars.ENABLE_TESTS }}" == "1" && "${{ vars.ENABLE_TESTS_RUN }}" == "1" ]]; then
FILES="$FILES encrypted-tests-run-logs.zip test-results.zip tests-recording.mp4"
fi
echo "Final upload list:"
for f in $FILES; do
if [[ -f "$f" ]]; then
echo " ✓ $f"
else
echo " - $f (missing)"
fi
done
echo "files=$FILES" >> $GITHUB_OUTPUT
- name: Set Upstream Recommendation
id: upstream_recommendation
run: |
UPSTREAM_NAME=$(echo "${{ inputs.upstream_name }}" | tr '[:upper:]' '[:lower:]')
if [[ "$UPSTREAM_NAME" != "nightly" ]]; then
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "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 }})." >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "content=" >> $GITHUB_OUTPUT
fi
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: ${{ steps.list_uploads.outputs.files }}
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!**
${{ steps.upstream_recommendation.outputs.content }}
## 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: |
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
# Format localized description
get_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
}
LOCALIZED_DESCRIPTION=$(get_description)
echo "$LOCALIZED_DESCRIPTION"
# 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

View File

@@ -1,24 +0,0 @@
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

@@ -1,165 +0,0 @@
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-26'
version: '26.0'
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: '26.0'
# - 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: 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 ">>>>>>>>> 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

@@ -1,196 +0,0 @@
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-26'
version: '26.0'
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: '26.0'
# - 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) 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 ">>>>>>>>> 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

@@ -1,98 +1,90 @@
name: Pull Request SideStore build name: Pull Request SideStore build
on: on:
pull_request: pull_request:
# types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] # types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs: jobs:
build: build:
name: Build and upload SideStore name: Build and upload SideStore
if: ${{ github.event.pull_request.draft == false }} if: ${{ github.event.pull_request.draft == false }}
strategy: runs-on: macos-26
fail-fast: false
matrix:
include:
- os: 'macos-14'
version: '16.1'
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - uses: actions/checkout@v4
uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
fetch-depth: 1 # shallow clone just for PR
- name: Install dependencies - run: brew install ldid xcbeautify
run: brew install ldid
- name: Install xcbeautify - name: Setup Env
run: brew install xcbeautify run: |
MARKETING_VERSION=$(python3 scripts/ci/workflow.py get-marketing-version)
- name: Add PR suffix to version SHORT_COMMIT=$(git rev-parse --short ${{ github.event.pull_request.head.sha }})
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig NORMALIZED_VERSION="${MARKETING_VERSION}-pr.${{ github.event.pull_request.number }}+${SHORT_COMMIT}"
env: python3 scripts/ci/workflow.py set-marketing-version "$NORMALIZED_VERSION"
COMMIT: ${{ github.event.pull_request.head.sha }} echo "SHORT_COMMIT=$SHORT_COMMIT" | tee -a $GITHUB_ENV
echo "MARKETING_VERSION=$NORMALIZED_VERSION" | tee -a $GITHUB_ENV
- name: Get version
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
- name: Echo version
run: echo "${{ steps.version.outputs.version }}"
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0 uses: maxim-lobanov/setup-xcode@v1.6.0
with: with:
xcode-version: ${{ matrix.version }} xcode-version: "26.2"
- name: Cache Build - name: Restore Cache (exact)
uses: irgaly/xcode-cache@v1 id: xcode-cache-exact
uses: actions/cache/restore@v3
with: with:
key: xcode-cache-deriveddata-${{ github.sha }} path: |
restore-keys: xcode-cache-deriveddata- ~/Library/Developer/Xcode/DerivedData
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }} ~/Library/Caches/org.swift.swiftpm
swiftpm-cache-restore-keys: | key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }}
xcode-cache-sourcedata-
- name: List Files and derived data - name: Restore Cache (last)
if: steps.xcode-cache-exact.outputs.cache-hit != 'true'
id: xcode-cache-fallback
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-build-cache-${{ github.ref_name }}-
- name: Build
env:
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
run: | run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<" python3 scripts/ci/workflow.py build; STATUS=$?
ls -la . python3 scripts/ci/workflow.py encrypt-build
echo "" mv SideStore.ipa SideStore-${{ env.MARKETING_VERSION }}.ipa
exit $STATUS
echo ">>>>>>>>> SideStore <<<<<<<<<<" - name: Save Cache
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists if: ${{ steps.xcode-cache-fallback.outputs.cache-hit != 'true' }}
echo "" uses: actions/cache/save@v3
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
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: make fakesign
- name: Convert to IPA
run: make ipa
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v4
with: with:
name: SideStore-${{ steps.version.outputs.version }}.ipa path: |
path: SideStore-${{ steps.version.outputs.version }}.ipa ~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-build-cache-${{ github.ref_name }}-${{ github.sha }}
- name: Upload *.dSYM Artifact - uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v4
with: with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM name: build-logs-${{ env.MARKETING_VERSION }}.zip
path: ./SideStore.xcarchive/dSYMs/* path: build-logs.zip
- uses: actions/upload-artifact@v4
with:
name: SideStore-${{ env.MARKETING_VERSION }}.ipa
path: SideStore-${{ env.MARKETING_VERSION }}.ipa
- uses: actions/upload-artifact@v4
with:
name: SideStore-${{ env.MARKETING_VERSION }}-dSYMs.zip
path: SideStore.dSYMs.zip

View File

@@ -1,242 +1,135 @@
name: Stable SideStore build name: Stable SideStore build
on: on:
push: push:
tags: tags:
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0 - '[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch: workflow_dispatch:
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
name: Build SideStore - stable (on tag push) name: Build SideStore - stable
strategy: runs-on: macos-26
fail-fast: false
matrix: env:
include: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- os: 'macos-26' CHANNEL: stable
version: '26.0' UPSTREAM_CHANNEL: ""
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout code - uses: actions/checkout@v4
uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0
- name: Echo Build.xcconfig - name: Find Last Successful commit
run: | run: |
echo "cat Build.xcconfig" LAST_SUCCESSFUL_COMMIT=$(python3 scripts/ci/workflow.py last-successful-commit \
cat Build.xcconfig "true" || echo "")
shell: bash echo "LAST_SUCCESSFUL_COMMIT=$LAST_SUCCESSFUL_COMMIT" | tee -a $GITHUB_ENV
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build - run: brew install ldid xcbeautify
# run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Echo Updated Build.xcconfig - name: Setup Env
run: | run: |
cat Build.xcconfig MARKETING_VERSION=$(python3 scripts/ci/workflow.py get-marketing-version)
shell: bash SHORT_COMMIT=$(python3 scripts/ci/workflow.py commit-id)
- 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"
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 if [ "$MARKETING_VERSION" != "${{ github.ref_name }}" ]; then
echo 'Version mismatch: $tag != $marketing_version ... ' echo "Version mismatch"
echo " expected-tag : $MARKETING_VERSION" echo "Build.xcconfig: $MARKETING_VERSION"
echo " pushed-tag : ${{ github.ref_name }}" echo "Tag: ${{ github.ref_name }}"
exit 1 exit 1
fi 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 echo "MARKETING_VERSION=$MARKETING_VERSION" | tee -a $GITHUB_ENV
run: | echo "SHORT_COMMIT=$SHORT_COMMIT" | tee -a $GITHUB_ENV
brew install ldid xcbeautify
- name: Setup Xcode - name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0 uses: maxim-lobanov/setup-xcode@v1.6.0
with: with:
xcode-version: ${{ matrix.version }} xcode-version: "26.0"
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match) - name: Restore Cache (exact)
id: xcode-cache-restore id: xcode-cache-exact
uses: actions/cache/restore@v3 uses: actions/cache/restore@v3
with: with:
path: | path: |
~/Library/Developer/Xcode/DerivedData ~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm ~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }} key: xcode-build-cache-stable-${{ github.sha }}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available) - name: Restore Cache (last)
id: xcode-cache-restore-recent if: steps.xcode-cache-exact.outputs.cache-hit != 'true'
id: xcode-cache-fallback
uses: actions/cache/restore@v3 uses: actions/cache/restore@v3
with: with:
path: | path: |
~/Library/Developer/Xcode/DerivedData ~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm ~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable- key: xcode-build-cache-stable-
- name: (Build) Clean previous build artifacts - name: Build
id: build
env:
BUILD_LOG_ZIP_PASSWORD: ${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
run: | run: |
make clean python3 scripts/ci/workflow.py build; STATUS=$?
mkdir -p build/logs python3 scripts/ci/workflow.py encrypt-build
shell: bash exit $STATUS
- name: (Build) List Files and derived data - name: Save Cache
if: always() if: ${{ steps.xcode-cache-fallback.outputs.cache-hit != 'true' }}
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
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 | 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 uses: actions/cache/save@v3
with: with:
path: | path: |
~/Library/Developer/Xcode/DerivedData ~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm ~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }} key: xcode-build-cache-stable-${{ github.sha }}
- name: (Build) List Files and Build artifacts - uses: actions/upload-artifact@v4
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: with:
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip name: build-logs-${{ env.MARKETING_VERSION }}.zip
path: encrypted-build-logs.zip path: build-logs.zip
- name: Upload SideStore.ipa Artifact - uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v4
with: with:
name: SideStore-${{ steps.version.outputs.version }}.ipa name: SideStore-${{ env.MARKETING_VERSION }}.ipa
path: SideStore.ipa path: SideStore.ipa
- name: Zip dSYMs - uses: actions/upload-artifact@v4
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
shell: bash
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with: with:
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip name: SideStore-${{ env.MARKETING_VERSION }}-dSYMs.zip
path: SideStore.dSYMs.zip path: SideStore.dSYMs.zip
- name: Get current date - name: Generate Metadata
id: date run: |
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT python3 scripts/ci/workflow.py dump-project-settings
shell: bash PRODUCT_NAME=$(python3 scripts/ci/workflow.py read-product-name)
BUNDLE_ID=$(python3 scripts/ci/workflow.py read-bundle-id)
IPA_NAME="$PRODUCT_NAME.ipa"
- name: Get current date in AltStore date form python3 scripts/ci/workflow.py generate-metadata \
id: date_altstore "${{ github.ref_name }}" \
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT "$SHORT_COMMIT" \
shell: bash "$MARKETING_VERSION" \
"$CHANNEL" \
"$BUNDLE_ID" \
"$IPA_NAME" \
"$LAST_SUCCESSFUL_COMMIT"
- name: Upload to releases - name: Upload to releases
uses: IsaacShelton/update-existing-release@v1.3.1 env:
with: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }} run: |
draft: true python3 scripts/ci/workflow.py upload-release \
release: ${{ github.ref_name }} # name "${{ github.ref_name }}" \
tag: ${{ github.ref_name }} "${{ github.ref_name }}" \
# stick with what the user pushed, do not use latest commit or anything, "$GITHUB_SHA" \
# 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) "$GITHUB_REPOSITORY" \
# in this case we do not want the tag to be auto-updated to latest "$UPSTREAM_CHANNEL" \
updateTag: false "true"
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 }}`

3
.gitignore vendored
View File

@@ -70,3 +70,6 @@ test-recording.mp4
test-recording.log test-recording.log
altstore-sources.md altstore-sources.md
local-build.sh local-build.sh
source-metadata.json
release-notes.md

4
.gitmodules vendored
View File

@@ -54,10 +54,6 @@
path = Dependencies/em_proxy path = Dependencies/em_proxy
url = https://github.com/SideStore/em_proxy url = https://github.com/SideStore/em_proxy
branch = master branch = master
[submodule "Dependencies/libfragmentzip"]
path = Dependencies/libfragmentzip
url = https://github.com/SideStore/libfragmentzip
branch = master
[submodule "Dependencies/apps-v2.json"] [submodule "Dependencies/apps-v2.json"]
path = Dependencies/apps-v2.json path = Dependencies/apps-v2.json
url = https://github.com/SideStore/apps-v2.json url = https://github.com/SideStore/apps-v2.json

File diff suppressed because it is too large Load Diff

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CA60C44C93D7A30E3695DD59"
BuildableName = "libem_proxy_static.a"
BlueprintName = "em_proxy-staticlib"
ReferencedContainer = "container:Dependencies/em_proxy.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CA60C44C93D7A30E3695DD59"
BuildableName = "libem_proxy_static.a"
BlueprintName = "em_proxy-staticlib"
ReferencedContainer = "container:Dependencies/em_proxy.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "2620" LastUpgradeVersion = "2630"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@@ -15,10 +15,10 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "CA60C44C93D7A30E3695DD59" BlueprintIdentifier = "A85A51412F4B4532002E2E11"
BuildableName = "libem_proxy_static.a" BuildableName = "libem_proxy_swift.a"
BlueprintName = "em_proxy-staticlib" BlueprintName = "em_proxy-swift"
ReferencedContainer = "container:em_proxy.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
@@ -50,10 +50,10 @@
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "CA60C44C93D7A30E3695DD59" BlueprintIdentifier = "A85A51412F4B4532002E2E11"
BuildableName = "libem_proxy_static.a" BuildableName = "libem_proxy_swift.a"
BlueprintName = "em_proxy-staticlib" BlueprintName = "em_proxy-swift"
ReferencedContainer = "container:em_proxy.xcodeproj"> ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
</ProfileAction> </ProfileAction>

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CA609C732349A560B9642892"
BuildableName = "libminimuxer_static.a"
BlueprintName = "minimuxer-staticlib"
ReferencedContainer = "container:Dependencies/minimuxer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CA609C732349A560B9642892"
BuildableName = "libminimuxer_static.a"
BlueprintName = "minimuxer-staticlib"
ReferencedContainer = "container:Dependencies/minimuxer.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -7,6 +7,9 @@
<FileRef <FileRef
location = "group:Dependencies/AltSign"> location = "group:Dependencies/AltSign">
</FileRef> </FileRef>
<FileRef
location = "group:Dependencies/minimuxer">
</FileRef>
<FileRef <FileRef
location = "group:Dependencies/Roxas/Roxas.xcodeproj"> location = "group:Dependencies/Roxas/Roxas.xcodeproj">
</FileRef> </FileRef>

View File

@@ -3,6 +3,3 @@
// //
#import "NSAttributedString+Markdown.h" #import "NSAttributedString+Markdown.h"
#import "ALTAppPatcher.h"
#include "fragmentzip.h"

View File

@@ -92,7 +92,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
} }
} }
self.setTintColor()
self.setTintColor() self.setTintColor()
self.prepareImageCache() self.prepareImageCache()

View File

@@ -12,6 +12,7 @@ import AltStoreCore
import Roxas import Roxas
import Nuke import Nuke
import Minimuxer
class BrowseViewController: UICollectionViewController, PeekPopPreviewing class BrowseViewController: UICollectionViewController, PeekPopPreviewing
{ {

View File

@@ -368,7 +368,7 @@ private extension FeaturedViewController
#keyPath(StoreApp._source._apps), #keyPath(StoreApp._source._apps),
#keyPath(StoreApp.bundleIdentifier), #keyPath(StoreApp.bundleIdentifier),
StoreApp.altstoreAppID, StoreApp.altstoreAppID,
#keyPath(StoreApp.installedApp), #keyPath(StoreApp.installedApp)
) )
let primaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp> let primaryFetchRequest = fetchRequest.copy() as! NSFetchRequest<StoreApp>

View File

@@ -87,7 +87,7 @@ final class LaunchViewController: UIViewController, UIDocumentPickerDelegate {
} }
func start_minimuxer_threads(_ pairing_file: String) { func start_minimuxer_threads(_ pairing_file: String) {
targetMinimuxerAddress() retargetUsbmuxdAddr()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do { do {
let loggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled let loggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
@@ -96,7 +96,7 @@ final class LaunchViewController: UIViewController, UIDocumentPickerDelegate {
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName)) try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName))
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR")") displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR")")
} }
start_auto_mounter(documentsDirectory) startAutoMounter(documentsDirectory)
} }
func fetchPairingFile() -> String? { PairingFileManager.shared.fetchPairingFile(presentingVC: self) } func fetchPairingFile() -> String? { PairingFileManager.shared.fetchPairingFile(presentingVC: self) }

View File

@@ -16,6 +16,7 @@ import WidgetKit
import AltStoreCore import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
import Minimuxer
extension AppManager extension AppManager
{ {
@@ -941,69 +942,6 @@ extension AppManager
self.run([enableJITOperation], context: context, requiresSerialQueue: true) self.run([enableJITOperation], context: context, requiresSerialQueue: true)
} }
func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> PatchAppOperation
{
final class Context: InstallAppOperationContext, PatchAppContext
{
}
guard let originalBundleID = resignedApp.bundle.infoDictionary?[Bundle.Info.altBundleID] as? String else {
let context = Context(bundleIdentifier: resignedApp.bundleIdentifier, authenticatedContext: authContext)
completionHandler(.failure(OperationError.invalidApp))
return PatchAppOperation(context: context)
}
let context = Context(bundleIdentifier: originalBundleID, authenticatedContext: authContext)
context.resignedApp = resignedApp
let patchAppOperation = PatchAppOperation(context: context)
let sendAppOperation = SendAppOperation(context: context)
let installOperation = InstallAppOperation(context: context)
let installationProgress = Progress.discreteProgress(totalUnitCount: 100)
installationProgress.addChild(sendAppOperation.progress, withPendingUnitCount: 40)
installationProgress.addChild(installOperation.progress, withPendingUnitCount: 60)
/* Patch */
patchAppOperation.resultHandler = { [weak patchAppOperation] (result) in
switch result
{
case .failure(let error):
context.error = error
case .success:
// Kinda hacky that we're calling patchAppOperation's progressHandler manually, but YOLO.
patchAppOperation?.progressHandler?(installationProgress, NSLocalizedString("Patching placeholder app...", comment: ""))
}
}
/* Send */
sendAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error):
context.error = error
completionHandler(.failure(error))
case .success(_): print("App sent over AFC")
}
}
sendAppOperation.addDependency(patchAppOperation)
/* Install */
installOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let installedApp): completionHandler(.success(installedApp))
}
}
installOperation.addDependency(sendAppOperation)
self.run([patchAppOperation, sendAppOperation, installOperation], context: context.authenticatedContext)
return patchAppOperation
}
func installationProgress(for app: AppProtocol) -> Progress? func installationProgress(for app: AppProtocol) -> Progress?
{ {
os_unfair_lock_lock(self.progressLock) os_unfair_lock_lock(self.progressLock)
@@ -1391,80 +1329,6 @@ private extension AppManager
} }
deactivateAppsOperation.addDependency(fetchProvisioningProfilesOperation) deactivateAppsOperation.addDependency(fetchProvisioningProfilesOperation)
/* Patch App */
let patchAppOperation = RSTAsyncBlockOperation { operation in
do
{
// Only attempt to patch app if we're installing a new app, not refreshing existing app.
// Post reboot, we install the correct jailbreak app by refreshing the patched app,
// so this check avoids infinite recursion.
guard case .install = appOperation else {
operation.finish()
return
}
guard let presentingViewController = context.presentingViewController else { return operation.finish() }
if let error = context.error
{
throw error
}
guard let app = context.app else {
throw OperationError.invalidParameters("AppManager._install.patchAppOperation: context.app is nil")
}
guard let isUntetherRequired = app.bundle.infoDictionary?[Bundle.Info.untetherRequired] as? Bool,
let minimumiOSVersionString = app.bundle.infoDictionary?[Bundle.Info.untetherMinimumiOSVersion] as? String,
let maximumiOSVersionString = app.bundle.infoDictionary?[Bundle.Info.untetherMaximumiOSVersion] as? String,
case let minimumiOSVersion = OperatingSystemVersion(string: minimumiOSVersionString),
case let maximumiOSVersion = OperatingSystemVersion(string: maximumiOSVersionString)
else { return operation.finish() }
let iOSVersion = ProcessInfo.processInfo.operatingSystemVersion
let iOSVersionSupported = ProcessInfo.processInfo.isOperatingSystemAtLeast(minimumiOSVersion) &&
(!ProcessInfo.processInfo.isOperatingSystemAtLeast(maximumiOSVersion) || maximumiOSVersion == iOSVersion)
guard isUntetherRequired, iOSVersionSupported, UIDevice.current.supportsFugu14 else { return operation.finish() }
guard let patchAppLink = app.bundle.infoDictionary?[Bundle.Info.untetherURL] as? String,
let patchAppURL = URL(string: patchAppLink)
else { throw OperationError.invalidApp }
let patchApp = AnyApp(name: app.name, bundleIdentifier: context.bundleIdentifier, url: patchAppURL, storeApp: nil)
DispatchQueue.main.async {
let storyboard = UIStoryboard(name: "PatchApp", bundle: nil)
let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
let patchViewController = navigationController.topViewController as! PatchViewController
patchViewController.patchApp = patchApp
patchViewController.completionHandler = { [weak presentingViewController] (result) in
switch result
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error): group.context.error = error
case .success: group.context.error = OperationError.cancelled
}
operation.finish()
DispatchQueue.main.async {
presentingViewController?.dismiss(animated: true, completion: nil)
}
}
presentingViewController.present(navigationController, animated: true, completion: nil)
}
}
catch
{
group.context.error = error
operation.finish()
}
}
patchAppOperation.addDependency(deactivateAppsOperation)
let modifyAppExBundleIdOperation = RSTAsyncBlockOperation { operation in let modifyAppExBundleIdOperation = RSTAsyncBlockOperation { operation in
if !context.useMainProfile { if !context.useMainProfile {
operation.finish() operation.finish()
@@ -1496,7 +1360,7 @@ private extension AppManager
self.exportResginedAppsToDocsDir(resignedApp) self.exportResginedAppsToDocsDir(resignedApp)
} }
} }
resignAppOperation.addDependency(patchAppOperation) resignAppOperation.addDependency(deactivateAppsOperation)
resignAppOperation.addDependency(modifyAppExBundleIdOperation) resignAppOperation.addDependency(modifyAppExBundleIdOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20) progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
@@ -1547,7 +1411,6 @@ private extension AppManager
verifyOperation, verifyOperation,
removeAppExtensionsOperation, removeAppExtensionsOperation,
deactivateAppsOperation, deactivateAppsOperation,
patchAppOperation,
refreshAnisetteDataOperation, refreshAnisetteDataOperation,
fetchProvisioningProfilesOperation, fetchProvisioningProfilesOperation,
modifyAppExBundleIdOperation, modifyAppExBundleIdOperation,

View File

@@ -701,7 +701,7 @@ private extension AuthenticationOperation
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void) func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{ {
guard let udid = fetch_udid()?.toString() else { guard let udid = fetchUDID() else {
return completionHandler(.failure(OperationError.unknownUDID)) return completionHandler(.failure(OperationError.unknownUDID))
} }

View File

@@ -101,7 +101,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
if UserDefaults.standard.enableEMPforWireguard { if UserDefaults.standard.enableEMPforWireguard {
startEMProxy(bind_addr: AppConstants.Proxy.serverURL) startEMProxy(bind_addr: AppConstants.Proxy.serverURL)
} }
targetMinimuxerAddress() retargetUsbmuxdAddr()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do { do {
// enable minimuxer console logging only if enabled in settings // enable minimuxer console logging only if enabled in settings
@@ -118,7 +118,7 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
if #available(iOS 17, *) { if #available(iOS 17, *) {
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :( // TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
} else { } else {
start_auto_mounter(documentsDirectory) startAutoMounter(documentsDirectory)
} }
self.managedObjectContext.perform { self.managedObjectContext.perform {

View File

@@ -42,7 +42,7 @@ final class DeactivateAppOperation: ResultOperation<InstalledApp>
for profile in allIdentifiers { for profile in allIdentifiers {
do { do {
try remove_provisioning_profile(profile) try removeProvisioningProfile(profile)
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
installedApp.isActive = false installedApp.isActive = false
self.finish(.success(installedApp)) self.finish(.success(installedApp))

View File

@@ -88,7 +88,7 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
var retries = 3 var retries = 3
while (retries > 0){ while (retries > 0){
do { do {
try debug_app(installedApp.resignedBundleIdentifier) try debugApp(installedApp.resignedBundleIdentifier)
self.finish(.success(())) self.finish(.success(()))
retries = 0 retries = 0
} catch { } catch {
@@ -105,7 +105,7 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
@available(iOS 17, *) @available(iOS 17, *)
func enableJITSideJITServer(serverURL: URL, installedApp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) { func enableJITSideJITServer(serverURL: URL, installedApp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
guard let udid = fetch_udid()?.toString() else { guard let udid = fetchUDID() else {
completion(.failure(.other("Unable to get UDID"))) completion(.failure(.other("Unable to get UDID")))
return return
} }

View File

@@ -11,6 +11,8 @@ import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
let shortcutURLonDelay = URL(string: "shortcuts://run-shortcut?name=TurnOnDataDelay")!
@objc(InstallAppOperation) @objc(InstallAppOperation)
final class InstallAppOperation: ResultOperation<InstalledApp> final class InstallAppOperation: ResultOperation<InstalledApp>
{ {
@@ -176,6 +178,13 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
var installing = true var installing = true
if installedApp.storeApp?.bundleIdentifier.range(of: Bundle.Info.appbundleIdentifier) != nil { if installedApp.storeApp?.bundleIdentifier.range(of: Bundle.Info.appbundleIdentifier) != nil {
do {
// we need to flush changes to the disk now in case the changes are lost when iOS kills current process
try installedApp.managedObjectContext?.save()
} catch {
print("Failed to flush installedApp to disk: \(error)")
}
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing // Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if UIApplication.shared.applicationState != .active { if UIApplication.shared.applicationState != .active {
@@ -204,6 +213,11 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
let alert = UIAlertController(title: "Finish Refresh", message: "Please reopen SideStore after the process is finished.To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen manually or by hitting Continue. Please reopen SideStore after doing this.", preferredStyle: .alert) let alert = UIAlertController(title: "Finish Refresh", message: "Please reopen SideStore after the process is finished.To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen manually or by hitting Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
print("Going home") print("Going home")
// Cell Shortcut
if self.context.shouldTurnOffData {
UIApplication.shared.open(shortcutURLonDelay, options: [:]) { _ in
print("Cell OFF Shortcut finished execution.")}
}
UIApplication.shared.perform(#selector(NSXPCConnection.suspend)) UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
})) }))
@@ -220,6 +234,10 @@ final class InstallAppOperation: ResultOperation<InstalledApp>
} }
} }
} }
// Cell Shortcut
if self.context.shouldTurnOffData {
UIApplication.shared.open(shortcutURLonDelay, options: [:]) { _ in print("Cell OFF Shortcut finished execution.")}
}
UIApplication.shared.perform(#selector(NSXPCConnection.suspend)) UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
} }
} }

View File

@@ -123,6 +123,8 @@ class InstallAppOperationContext: AppOperationContext
var alternateIconURL: URL? var alternateIconURL: URL?
var shouldTurnOffData: Bool = false
// Non-nil when installing from a source. // Non-nil when installing from a source.
@AsyncManaged @AsyncManaged
var appVersion: AppVersion? var appVersion: AppVersion?

View File

@@ -1,19 +0,0 @@
//
// ALTAppPatcher.h
// AltStore
//
// Created by Riley Testut on 10/18/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ALTAppPatcher : NSObject
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *_Nullable *)error;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,143 +0,0 @@
//
// ALTAppPatcher.m
// AltStore
//
// Created by Riley Testut on 10/18/21.
// Copied with minor modifications from sample code provided by Linus Henze.
//
#import "ALTAppPatcher.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@import Roxas;
#define CPU_SUBTYPE_PAC 0x80000000
#define FAT_MAGIC 0xcafebabe
#define ROUND_TO_PAGE(val) (((val % 0x4000) == 0) ? val : (val + (0x4000 - (val & 0x3FFF))))
typedef struct {
uint32_t magic;
uint32_t cpuType;
uint32_t cpuSubType;
// Incomplete, we don't need anything else
} MachOHeader;
typedef struct {
uint32_t cpuType;
uint32_t cpuSubType;
uint32_t fileOffset;
uint32_t size;
uint32_t alignment;
} FatArch;
typedef struct {
uint32_t magic;
uint32_t archCount;
FatArch archs[0];
} FatHeader;
// Given two MachO files, return a FAT file with the following properties:
// 1. installd will still see the original MachO and validate it's code signature
// 2. The kernel will only see the injected MachO instead
//
// Only arm64e for now
void *injectApp(void *originalApp, size_t originalAppSize, void *appToInject, size_t appToInjectSize, size_t *outputSize) {
*outputSize = 0;
// First validate the App to inject: It must be an arm64e application
if (appToInjectSize < sizeof(MachOHeader)) {
return NULL;
}
MachOHeader *injectedHeader = (MachOHeader*) appToInject;
if (injectedHeader->cpuType != CPU_TYPE_ARM64) {
return NULL;
}
if (injectedHeader->cpuSubType != (CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC)) {
return NULL;
}
// Ok, the App to inject is ok
// Now build a fat header
size_t originalAppSizeRounded = ROUND_TO_PAGE(originalAppSize);
size_t appToInjectSizeRounded = ROUND_TO_PAGE(appToInjectSize);
size_t totalSize = 0x4000 /* Fat Header + Alignment */ + originalAppSizeRounded + appToInjectSizeRounded;
void *fatBuf = malloc(totalSize);
if (fatBuf == NULL) {
return NULL;
}
bzero(fatBuf, totalSize);
FatHeader *fatHeader = (FatHeader*) fatBuf;
fatHeader->magic = htonl(FAT_MAGIC);
fatHeader->archCount = htonl(2);
// Write first arch (original app)
fatHeader->archs[0].cpuType = htonl(CPU_TYPE_ARM64);
fatHeader->archs[0].cpuSubType = htonl(CPU_SUBTYPE_ARM64E); /* Note that this is not a valid cpu subtype */
fatHeader->archs[0].fileOffset = htonl(0x4000);
fatHeader->archs[0].size = htonl(originalAppSize);
fatHeader->archs[0].alignment = htonl(0xE);
// Write second arch (injected app)
fatHeader->archs[1].cpuType = htonl(CPU_TYPE_ARM64);
fatHeader->archs[1].cpuSubType = htonl(CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC);
fatHeader->archs[1].fileOffset = htonl(0x4000 + originalAppSizeRounded);
fatHeader->archs[1].size = htonl(appToInjectSize);
fatHeader->archs[1].alignment = htonl(0xE);
// Ok, now write the MachOs
memcpy(fatBuf + 0x4000, originalApp, originalAppSize);
memcpy(fatBuf + 0x4000 + originalAppSizeRounded, appToInject, appToInjectSize);
// We're done!
*outputSize = totalSize;
return fatBuf;
}
@implementation ALTAppPatcher
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *__autoreleasing *)error
{
NSMutableData *originalApp = [NSMutableData dataWithContentsOfURL:appFileURL options:0 error:error];
if (originalApp == nil)
{
return NO;
}
NSMutableData *injectedApp = [NSMutableData dataWithContentsOfURL:patchFileURL options:0 error:error];
if (injectedApp == nil)
{
return NO;
}
size_t outputSize = 0;
void *output = injectApp(originalApp.mutableBytes, originalApp.length, injectedApp.mutableBytes, injectedApp.length, &outputSize);
if (output == NULL)
{
if (error)
{
// If injectApp fails, it means the patch app is in the wrong format.
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: patchFileURL}];
}
return NO;
}
NSData *outputData = [NSData dataWithBytesNoCopy:output length:outputSize freeWhenDone:YES];
if (![outputData writeToURL:appFileURL options:NSDataWritingAtomic error:error])
{
return NO;
}
return YES;
}
@end

View File

@@ -1,124 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="WBb-E1-bN8">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19144"/>
<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"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="dx2-fp-qDX">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="WBb-E1-bN8" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="cVa-8m-fW6" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<color key="barTintColor" name="SettingsBackground"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="idH-XF-rK8" kind="relationship" relationship="rootViewController" id="hSJ-tL-4nB"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="i7K-pi-SRe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="137.68115942028987" y="137.94642857142856"/>
</scene>
<!--Patch View Controller-->
<scene sceneID="gJ4-4F-79r">
<objects>
<viewController id="idH-XF-rK8" customClass="PatchViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="4bV-S5-z7S">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RHK-C5-7wu" customClass="RSTPlaceholderView">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="PjB-Dc-9n3">
<rect key="frame" x="20" y="736.5" width="374" height="117.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GQF-6P-Fit">
<rect key="frame" x="0.0" y="0.0" width="374" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" name="Text"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SNn-Ad-ICf" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="28.5" width="374" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
<constraint firstAttribute="height" constant="51" id="s4X-uf-nl9"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<color key="tintColor" name="SettingsHighlighted"/>
<state key="normal" title="Install Untethered Jailbreak">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="performButtonAction" destination="idH-XF-rK8" eventType="primaryActionTriggered" id="FxO-1Y-IML"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="leJ-09-giz">
<rect key="frame" x="0.0" y="87.5" width="374" height="30"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" title="Install Without Untethering">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="installRegularJailbreak" destination="idH-XF-rK8" eventType="primaryActionTriggered" id="1XB-11-Kdn"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="H6g-xA-DdL"/>
<color key="backgroundColor" name="SettingsBackground"/>
<constraints>
<constraint firstItem="RHK-C5-7wu" firstAttribute="top" secondItem="4bV-S5-z7S" secondAttribute="top" id="CVK-6E-iA6"/>
<constraint firstAttribute="trailingMargin" secondItem="PjB-Dc-9n3" secondAttribute="trailing" id="GOg-JU-LIP"/>
<constraint firstItem="RHK-C5-7wu" firstAttribute="bottom" secondItem="4bV-S5-z7S" secondAttribute="bottom" id="LPh-J8-IVx"/>
<constraint firstItem="PjB-Dc-9n3" firstAttribute="leading" secondItem="4bV-S5-z7S" secondAttribute="leadingMargin" id="Rlg-PC-5ZN"/>
<constraint firstItem="RHK-C5-7wu" firstAttribute="trailing" secondItem="H6g-xA-DdL" secondAttribute="trailing" id="XdZ-36-6yS"/>
<constraint firstAttribute="bottomMargin" secondItem="PjB-Dc-9n3" secondAttribute="bottom" id="hTS-nX-0xv"/>
<constraint firstItem="RHK-C5-7wu" firstAttribute="leading" secondItem="H6g-xA-DdL" secondAttribute="leading" id="lzV-fG-Xv6"/>
</constraints>
</view>
<navigationItem key="navigationItem" largeTitleDisplayMode="always" id="0J1-80-RD8">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="vtw-PQ-Dk1">
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<action selector="cancel" destination="idH-XF-rK8" id="4Wk-dv-RYG"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="cancelBarButtonItem" destination="vtw-PQ-Dk1" id="8Mh-GU-KD5"/>
<outlet property="cancelButton" destination="leJ-09-giz" id="BNh-I3-vXc"/>
<outlet property="pillButton" destination="SNn-Ad-ICf" id="iJg-TC-p8q"/>
<outlet property="placeholderView" destination="RHK-C5-7wu" id="5x0-sg-HAH"/>
<outlet property="taskDescriptionLabel" destination="GQF-6P-Fit" id="C4c-xy-kvU"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8ev-19-qsi" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1001" y="138"/>
</scene>
</scenes>
<resources>
<namedColor name="SettingsBackground">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SettingsHighlighted">
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Text">
<color red="1" green="1" blue="1" alpha="0.75" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -1,257 +0,0 @@
//
// PatchAppOperation.swift
// AltStore
//
// Created by Riley Testut on 10/13/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
import Combine
import AppleArchive
import System
import AltStoreCore
import AltSign
import Roxas
protocol PatchAppContext
{
var bundleIdentifier: String { get }
var temporaryDirectory: URL { get }
var resignedApp: ALTApplication? { get }
var error: Error? { get }
}
extension PatchAppError
{
enum Code: Int, ALTErrorCode, CaseIterable {
typealias Error = PatchAppError
case unsupportedOperatingSystemVersion
}
static func unsupportedOperatingSystemVersion(_ osVersion: OperatingSystemVersion) -> PatchAppError {
PatchAppError(code: .unsupportedOperatingSystemVersion, osVersion: osVersion)
}
}
struct PatchAppError: ALTLocalizedError {
let code: Code
var errorTitle: String?
var errorFailure: String?
var osVersion: OperatingSystemVersion?
var errorFailureReason: String {
switch self.code {
case .unsupportedOperatingSystemVersion:
let osVersionString: String
if let osVersion = self.osVersion?.stringValue {
osVersionString = NSLocalizedString("iOS", comment: "") + " " + osVersion
} else {
osVersionString = NSLocalizedString("your device's iOS version", comment: "")
}
return String(format: NSLocalizedString("The OTA download URL for %@ could not be determined.", comment: ""), osVersionString)
}
}
}
private struct OTAUpdate
{
var url: URL
var archivePath: String
}
@available(iOS 14, *)
final class PatchAppOperation: ResultOperation<Void>
{
let context: PatchAppContext
var progressHandler: ((Progress, String) -> Void)?
private let appPatcher = ALTAppPatcher()
private lazy var patchDirectory: URL = self.context.temporaryDirectory.appendingPathComponent("Patch", isDirectory: true)
private var cancellable: AnyCancellable?
init(context: PatchAppContext)
{
self.context = context
super.init()
self.progress.totalUnitCount = 100
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let resignedApp = self.context.resignedApp else {
return self.finish(.failure(OperationError.invalidParameters("PatchAppOperation.main: self.context.resignedApp is nil")))
}
self.progressHandler?(self.progress, NSLocalizedString("Downloading iOS firmware...", comment: ""))
self.cancellable = self.fetchOTAUpdate()
.flatMap { self.downloadArchive(from: $0) }
.flatMap { self.extractSpotlightFromArchive(at: $0) }
.flatMap { self.patch(resignedApp, withBinaryAt: $0) }
.tryMap { try FileManager.default.zipAppBundle(at: $0) }
.tryMap { (fileURL) in
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL, storeApp: nil)
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
}
.receive(on: RunLoop.main)
.sink { completion in
switch completion
{
case .failure(let error): self.finish(.failure(error))
case .finished: self.finish(.success(()))
}
} receiveValue: { _ in }
}
override func cancel()
{
super.cancel()
self.cancellable?.cancel()
self.cancellable = nil
}
}
private let ALTFragmentZipCallback: @convention(c) (UInt32) -> Void = { (percentageComplete) in
guard let progress = Progress.current() else { return }
if percentageComplete == 100 && progress.completedUnitCount == 0
{
// Ignore first percentageComplete, which is always 100.
return
}
progress.completedUnitCount = Int64(percentageComplete)
}
private extension PatchAppOperation
{
func fetchOTAUpdate() -> AnyPublisher<OTAUpdate, Error>
{
Just(()).tryMap {
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
switch (osVersion.majorVersion, osVersion.minorVersion)
{
case (14, 3):
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2020WinterFCS/patches/001-87330/99E29969-F6B6-422A-B946-70DE2E2D73BE/com_apple_MobileAsset_SoftwareUpdate/67f9e42f5e57a20e0a87eaf81b69dd2a61311d3f.zip")!,
archivePath: "AssetData/payloadv2/payload.042")
case (14, 4):
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021WinterFCS/patches/001-98606/43AF99A1-F286-43B1-A101-F9F856EA395A/com_apple_MobileAsset_SoftwareUpdate/c4985c32c344beb7b49c61919b4e39d1fd336c90.zip")!,
archivePath: "AssetData/payloadv2/payload.042")
case (14, 5):
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021SpringFCS/patches/061-84483/AB525139-066E-46F8-8E85-DCE802C03BA8/com_apple_MobileAsset_SoftwareUpdate/788573ae93113881db04269acedeecabbaa643e3.zip")!,
archivePath: "AssetData/payloadv2/payload.043")
default: throw PatchAppError.unsupportedOperatingSystemVersion(osVersion)
}
}
.eraseToAnyPublisher()
}
func downloadArchive(from update: OTAUpdate) -> AnyPublisher<URL, Error>
{
Just(()).tryMap {
#if targetEnvironment(simulator)
throw PatchAppError.unsupportedOperatingSystemVersion(ProcessInfo.processInfo.operatingSystemVersion)
#else
try FileManager.default.createDirectory(at: self.patchDirectory, withIntermediateDirectories: true, attributes: nil)
let archiveURL = self.patchDirectory.appendingPathComponent("ota.archive")
try archiveURL.withUnsafeFileSystemRepresentation { archivePath in
guard let fz = fragmentzip_open((update.url.absoluteString as NSString).utf8String!) else {
throw URLError(.cannotConnectToHost, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("The connection failed because a connection cannot be made to the host.", comment: ""),
NSURLErrorKey: update.url])
}
defer { fragmentzip_close(fz) }
self.progress.becomeCurrent(withPendingUnitCount: 100)
defer { self.progress.resignCurrent() }
guard fragmentzip_download_file(fz, update.archivePath, archivePath!, ALTFragmentZipCallback) == 0 else {
throw URLError(.networkConnectionLost, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("The connection failed because the network connection was lost.", comment: ""),
NSURLErrorKey: update.url])
}
}
Logger.fugu14.notice("Downloaded iOS OTA archive.")
return archiveURL
#endif
}
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not download OTA archive.", comment: "")) }
.eraseToAnyPublisher()
}
func extractSpotlightFromArchive(at archiveURL: URL) -> AnyPublisher<URL, Error>
{
Just(()).tryMap {
#if targetEnvironment(simulator)
throw PatchAppError.unsupportedOperatingSystemVersion(ProcessInfo.processInfo.operatingSystemVersion)
#else
let spotlightPath = "Applications/Spotlight.app/Spotlight"
let spotlightFileURL = self.patchDirectory.appendingPathComponent(spotlightPath)
guard let readFileStream = ArchiveByteStream.fileStream(path: FilePath(archiveURL.path), mode: .readOnly, options: [], permissions: FilePermissions(rawValue: 0o644)),
let decompressStream = ArchiveByteStream.decompressionStream(readingFrom: readFileStream),
let decodeStream = ArchiveStream.decodeStream(readingFrom: decompressStream),
let readStream = ArchiveStream.extractStream(extractingTo: FilePath(self.patchDirectory.path))
else { throw CocoaError(.fileReadCorruptFile, userInfo: [NSURLErrorKey: archiveURL]) }
_ = try ArchiveStream.process(readingFrom: decodeStream, writingTo: readStream) { message, filePath, data in
guard filePath == FilePath(spotlightPath) else { return .skip }
return .ok
}
Logger.fugu14.notice("Extracted Spotlight from OTA archive.")
return spotlightFileURL
#endif
}
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not extract Spotlight from OTA archive.", comment: "")) }
.eraseToAnyPublisher()
}
func patch(_ app: ALTApplication, withBinaryAt patchFileURL: URL) -> AnyPublisher<URL, Error>
{
Just(()).tryMap {
// executableURL may be nil, so use infoDictionary instead to determine executable name.
// guard let appName = app.bundle.executableURL?.lastPathComponent else { throw OperationError.invalidApp }
guard let appName = app.bundle.infoDictionary?[kCFBundleExecutableKey as String] as? String else { throw OperationError.invalidApp }
let temporaryAppURL = self.patchDirectory.appendingPathComponent("Patched.app", isDirectory: true)
try FileManager.default.copyItem(at: app.fileURL, to: temporaryAppURL)
let appBinaryURL = temporaryAppURL.appendingPathComponent(appName, isDirectory: false)
try self.appPatcher.patchAppBinary(at: appBinaryURL, withBinaryAt: patchFileURL)
Logger.fugu14.notice("Patched \(app.name, privacy: .public)!")
return temporaryAppURL
}
.mapError { ($0 as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), app.name)) }
.eraseToAnyPublisher()
}
}

View File

@@ -1,494 +0,0 @@
//
// PatchViewController.swift
// AltStore
//
// Created by Riley Testut on 10/20/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
import Combine
import AltStoreCore
import AltSign
import Roxas
extension PatchViewController
{
enum Step
{
case confirm
case install
case openApp
case patchApp
case reboot
case refresh
case finish
}
}
@available(iOS 14.0, *)
final class PatchViewController: UIViewController
{
var patchApp: AnyApp?
var installedApp: InstalledApp?
var completionHandler: ((Result<Void, Error>) -> Void)?
private let context = AuthenticatedOperationContext()
private var currentStep: Step = .confirm {
didSet {
DispatchQueue.main.async {
self.update()
}
}
}
private var buttonHandler: (() -> Void)?
private var resignedApp: ALTApplication?
private lazy var temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
private var didEnterBackgroundObservation: NSObjectProtocol?
private weak var cancellableProgress: Progress?
@IBOutlet private var placeholderView: RSTPlaceholderView!
@IBOutlet private var taskDescriptionLabel: UILabel!
@IBOutlet private var pillButton: PillButton!
@IBOutlet private var cancelBarButtonItem: UIBarButtonItem!
@IBOutlet private var cancelButton: UIButton!
override func viewDidLoad()
{
super.viewDidLoad()
self.isModalInPresentation = true
self.placeholderView.stackView.spacing = 20
self.placeholderView.textLabel.textColor = .white
self.placeholderView.detailTextLabel.textAlignment = .left
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
self.buttonHandler = { [weak self] in
self?.startProcess()
}
do
{
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
}
catch
{
Logger.fugu14.error("Failed to create temporary directory \(self.temporaryDirectory.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
self.update()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if self.installedApp != nil
{
self.refreshApp()
}
}
}
private extension PatchViewController
{
func update()
{
self.cancelButton.alpha = 0.0
switch self.currentStep
{
case .confirm:
guard let app = self.patchApp else { break }
if UIDevice.current.isUntetheredJailbreakRequired
{
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Requires Untethering", comment: "")
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak is untethered, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but SideStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
}
else
{
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Supports Untethering", comment: "")
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak has an untethered version, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but SideStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
}
self.pillButton.setTitle(NSLocalizedString("Install Untethered Jailbreak", comment: ""), for: .normal)
self.cancelButton.alpha = 1.0
case .install:
guard let app = self.patchApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Installing %@ placeholder…", comment: ""), app.name)
self.placeholderView.detailTextLabel.text = NSLocalizedString("A placeholder app needs to be installed in order to prepare your device for untethering.\n\nThis may take a few moments.", comment: "")
case .openApp:
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
case .patchApp:
guard let app = self.patchApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Patching %@ placeholder…", comment: ""), app.name)
self.placeholderView.detailTextLabel.text = NSLocalizedString("This will take a few moments. Please do not turn off the screen or leave the app until patching is complete.", comment: "")
self.pillButton.setTitle(NSLocalizedString("Patch Placeholder", comment: ""), for: .normal)
case .reboot:
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
case .refresh:
guard let installedApp = self.installedApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish installing %@?", comment: ""), installedApp.name)
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("In order to finish jailbreaking this device, you need to install %@ then follow the instructions in the app.", comment: ""), installedApp.name)
self.pillButton.setTitle(String(format: NSLocalizedString("Install %@", comment: ""), installedApp.name), for: .normal)
case .finish:
guard let installedApp = self.installedApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish in %@", comment: ""), installedApp.name)
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("Follow the instructions in %@ to finish jailbreaking this device.", comment: ""), installedApp.name)
self.pillButton.setTitle(String(format: NSLocalizedString("Open %@", comment: ""), installedApp.name), for: .normal)
}
}
func present(_ error: Error, title: String)
{
DispatchQueue.main.async {
let nsError = error as NSError
let alertController = UIAlertController(title: nsError.localizedFailure ?? title, message: error.localizedDescription, preferredStyle: .alert)
alertController.addAction(.ok)
self.present(alertController, animated: true, completion: nil)
self.setProgress(nil, description: nil)
}
}
func setProgress(_ progress: Progress?, description: String?)
{
DispatchQueue.main.async {
self.pillButton.progress = progress
self.taskDescriptionLabel.text = description ?? " " // Use non-empty string to prevent label resizing itself.
}
}
func finish(with result: Result<Void, Error>)
{
do
{
try FileManager.default.removeItem(at: self.temporaryDirectory)
}
catch
{
Logger.fugu14.error("Failed to remove temporary directory \(self.temporaryDirectory.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
if let observation = self.didEnterBackgroundObservation
{
NotificationCenter.default.removeObserver(observation)
}
self.completionHandler?(result)
self.completionHandler = nil
}
}
private extension PatchViewController
{
@IBAction func performButtonAction()
{
self.buttonHandler?()
}
@IBAction func cancel()
{
self.finish(with: .success(()))
self.cancellableProgress?.cancel()
}
@IBAction func installRegularJailbreak()
{
guard let app = self.patchApp else { return }
let title: String
let message: String
if UIDevice.current.isUntetheredJailbreakRequired
{
title = NSLocalizedString("Untethering Required", comment: "")
message = String(format: NSLocalizedString("%@ can not jailbreak this device unless you untether it first. Are you sure you want to install without untethering?", comment: ""), app.name)
}
else
{
title = NSLocalizedString("Untethering Recommended", comment: "")
message = String(format: NSLocalizedString("Untethering this jailbreak will prevent %@ from expiring, even after 7 days or rebooting the device. Are you sure you want to install without untethering?", comment: ""), app.name)
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Install Without Untethering", comment: ""), style: .default) { _ in
self.finish(with: .failure(OperationError.cancelled))
})
alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
}
private extension PatchViewController
{
func startProcess()
{
guard let patchApp = self.patchApp else { return }
self.currentStep = .install
if let progress = AppManager.shared.installationProgress(for: patchApp)
{
// Cancel pending jailbreak app installation so we can start a new one.
progress.cancel()
}
let appURL = InstalledApp.fileURL(for: patchApp)
let cachedAppURL = self.temporaryDirectory.appendingPathComponent("Cached.app")
do
{
// Make copy of original app, so we can replace the cached patch app with it later.
try FileManager.default.copyItem(at: appURL, to: cachedAppURL, shouldReplace: true)
}
catch
{
self.present(error, title: NSLocalizedString("Could not back up jailbreak app.", comment: ""))
return
}
var unzippingError: Error?
let refreshGroup = AppManager.shared.install(patchApp, presentingViewController: self, context: self.context) { result in
do
{
_ = try result.get()
if let unzippingError = unzippingError
{
throw unzippingError
}
// Replace cached patch app with original app so we can resume installing it post-reboot.
try FileManager.default.copyItem(at: cachedAppURL, to: appURL, shouldReplace: true)
self.openApp()
}
catch
{
self.present(error, title: String(format: NSLocalizedString("Could not install %@ placeholder.", comment: ""), patchApp.name))
}
}
refreshGroup.beginInstallationHandler = { (installedApp) in
do
{
// Replace patch app name with correct name.
installedApp.name = patchApp.name
let ipaURL = installedApp.refreshedIPAURL
let resignedAppURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: self.temporaryDirectory)
self.resignedApp = ALTApplication(fileURL: resignedAppURL)
}
catch
{
Logger.fugu14.error("Error unzipping app bundle: \(error.localizedDescription, privacy: .public)")
unzippingError = error
}
}
self.setProgress(refreshGroup.progress, description: nil)
self.cancellableProgress = refreshGroup.progress
}
func openApp()
{
guard let patchApp = self.patchApp else { return }
self.setProgress(nil, description: nil)
self.currentStep = .openApp
// This observation is willEnterForeground because patching starts immediately upon return.
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { (notification) in
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
self.patchApplication()
}
self.buttonHandler = { [weak self] in
guard let self = self else { return }
#if !targetEnvironment(simulator)
let openURL = InstalledApp.openAppURL(for: patchApp)
UIApplication.shared.open(openURL) { success in
guard !success else { return }
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
}
#endif
}
}
func patchApplication()
{
guard let resignedApp = self.resignedApp else { return }
self.currentStep = .patchApp
self.buttonHandler = { [weak self] in
self?.patchApplication()
}
let patchAppOperation = AppManager.shared.patch(resignedApp: resignedApp, presentingViewController: self, context: self.context) { result in
switch result
{
case .failure(let error): self.present(error, title: String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), resignedApp.name))
case .success: self.rebootDevice()
}
}
patchAppOperation.progressHandler = { (progress, description) in
self.setProgress(progress, description: description)
}
self.cancellableProgress = patchAppOperation.progress
}
func rebootDevice()
{
guard let patchApp = self.patchApp else { return }
self.setProgress(nil, description: nil)
self.currentStep = .reboot
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
var patchedApps = UserDefaults.standard.patchedApps ?? []
if !patchedApps.contains(patchApp.bundleIdentifier)
{
patchedApps.append(patchApp.bundleIdentifier)
UserDefaults.standard.patchedApps = patchedApps
}
self.finish(with: .success(()))
}
self.buttonHandler = { [weak self] in
guard let self = self else { return }
#if !targetEnvironment(simulator)
let openURL = InstalledApp.openAppURL(for: patchApp)
UIApplication.shared.open(openURL) { success in
guard !success else { return }
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
}
#endif
}
}
func refreshApp()
{
guard let installedApp = self.installedApp else { return }
self.currentStep = .refresh
self.buttonHandler = { [weak self] in
guard let self = self else { return }
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
tempApp.needsResign = true
let errorTitle = String(format: NSLocalizedString("Could not install %@.", comment: ""), tempApp.name)
do
{
try context.save()
installedApp.managedObjectContext?.perform {
// Refreshing ensures we don't attempt to patch the app again,
// since that is only checked when installing a new app.
let refreshGroup = AppManager.shared.refresh([installedApp], presentingViewController: self, group: nil)
refreshGroup.completionHandler = { [weak refreshGroup, weak self] (results) in
guard let self = self else { return }
do
{
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown() }
_ = try result.get()
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
{
patchedApps.remove(at: index)
UserDefaults.standard.patchedApps = patchedApps
}
self.finish()
}
catch
{
self.present(error, title: errorTitle)
}
}
self.setProgress(refreshGroup.progress, description: String(format: NSLocalizedString("Installing %@...", comment: ""), installedApp.name))
}
}
catch
{
self.present(error, title: errorTitle)
}
}
}
}
func finish()
{
guard let installedApp = self.installedApp else { return }
self.setProgress(nil, description: nil)
self.currentStep = .finish
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
self.finish(with: .success(()))
}
installedApp.managedObjectContext?.perform {
let appName = installedApp.name
let openURL = installedApp.openAppURL
self.buttonHandler = { [weak self] in
guard let self = self else { return }
#if !targetEnvironment(simulator)
UIApplication.shared.open(openURL) { success in
guard !success else { return }
self.present(OperationError.openAppFailed(name: appName), title: String(format: NSLocalizedString("Could not open %@.", comment: ""), appName))
}
#endif
}
}
}
}

View File

@@ -1,18 +0,0 @@
//
// fragmentzip.h
// AltStore
//
// Created by Riley Testut on 10/25/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
#ifndef fragmentzip_h
#define fragmentzip_h
typedef void fragmentzip_t;
typedef void (*fragmentzip_process_callback_t)(unsigned int progress);
fragmentzip_t *fragmentzip_open(const char *url);
int fragmentzip_download_file(fragmentzip_t *info, const char *remotepath, const char *savepath, fragmentzip_process_callback_t callback);
void fragmentzip_close(fragmentzip_t *info);
#endif /* fragmentzip_h */

View File

@@ -11,6 +11,8 @@ import AltStoreCore
import AltSign import AltSign
import Roxas import Roxas
import Minimuxer
@objc(RefreshAppOperation) @objc(RefreshAppOperation)
final class RefreshAppOperation: ResultOperation<InstalledApp> final class RefreshAppOperation: ResultOperation<InstalledApp>
{ {
@@ -46,7 +48,6 @@ final class RefreshAppOperation: ResultOperation<InstalledApp>
for p in profiles { for p in profiles {
do { do {
let bytes =
try installProvisioningProfiles(p.value.data) try installProvisioningProfiles(p.value.data)
} catch { } catch {
self.finish(.failure(MinimuxerError.ProfileInstall)) self.finish(.failure(MinimuxerError.ProfileInstall))

View File

@@ -201,7 +201,7 @@ private extension ResignAppOperation
if app.isAltStoreApp if app.isAltStoreApp
{ {
guard let udid = fetch_udid()?.toString() as? String else { throw OperationError.unknownUDID } guard let udid = fetchUDID() else { throw OperationError.unknownUDID }
guard Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) is String else { throw OperationError.unknownUDID } guard Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) is String else { throw OperationError.unknownUDID }
additionalValues[Bundle.Info.devicePairingString] = "<insert pairing file here>" additionalValues[Bundle.Info.devicePairingString] = "<insert pairing file here>"
additionalValues[Bundle.Info.deviceID] = udid additionalValues[Bundle.Info.deviceID] = udid

View File

@@ -8,6 +8,7 @@
import Foundation import Foundation
import Network import Network
import AltStoreCore import AltStoreCore
import Minimuxer
@objc(SendAppOperation) @objc(SendAppOperation)
final class SendAppOperation: ResultOperation<()> final class SendAppOperation: ResultOperation<()>
@@ -25,12 +26,10 @@ final class SendAppOperation: ResultOperation<()>
self.progress.totalUnitCount = 1 self.progress.totalUnitCount = 1
} }
override func main() override func main() {
{
super.main() super.main()
if let error = self.context.error if let error = self.context.error {
{
return self.finish(.failure(error)) return self.finish(.failure(error))
} }
@@ -38,26 +37,52 @@ final class SendAppOperation: ResultOperation<()>
return self.finish(.failure(OperationError.invalidParameters("SendAppOperation.main: self.resignedApp is nil"))) return self.finish(.failure(OperationError.invalidParameters("SendAppOperation.main: self.resignedApp is nil")))
} }
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa. let shortcutURLoff = URL(string: "shortcuts://run-shortcut?name=TurnOffData")!
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL, storeApp: nil) let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL, storeApp: nil)
let fileURL = InstalledApp.refreshedIPAURL(for: app) let fileURL = InstalledApp.refreshedIPAURL(for: app)
print("AFC App `fileURL`: \(fileURL.absoluteString)") print("AFC App `fileURL`: \(fileURL.absoluteString)")
if let data = NSData(contentsOf: fileURL) { // only when minimuxer is not ready and below 26.4 should we turn off data
do { if #available(iOS 26.4, *) {
let bytes = Data(data) context.shouldTurnOffData = false
try yeetAppAFC(app.bundleIdentifier, bytes) } else if !isMinimuxerReady {
self.progress.completedUnitCount += 1 context.shouldTurnOffData = true
self.finish(.success(())) } else {
} catch { context.shouldTurnOffData = false
self.finish(.failure(MinimuxerError.RwAfc)) }
self.progress.completedUnitCount += 1
self.finish(.success(())) if context.shouldTurnOffData {
// Wait for Shortcut to Finish Before Proceeding
UIApplication.shared.open(shortcutURLoff, options: [:]) { _ in
print("Shortcut finished execution. Proceeding with file transfer.")
DispatchQueue.global().async {
self.processFile(at: fileURL, for: app.bundleIdentifier)
}
} }
} else { } else {
DispatchQueue.global().async {
self.processFile(at: fileURL, for: app.bundleIdentifier)
}
}
}
private func processFile(at fileURL: URL, for bundleIdentifier: String) {
guard let data = NSData(contentsOf: fileURL) else {
print("IPA doesn't exist????") print("IPA doesn't exist????")
self.finish(.failure(OperationError(.appNotFound(name: resignedApp.name)))) return self.finish(.failure(OperationError(.appNotFound(name: bundleIdentifier))))
}
do {
let bytes = Data(data)
try yeetAppAFC(bundleIdentifier, bytes)
self.progress.completedUnitCount += 1
self.finish(.success(()))
} catch {
self.finish(.failure(MinimuxerError.RwAfc))
self.progress.completedUnitCount += 1
self.finish(.success(()))
} }
} }
} }

View File

@@ -0,0 +1 @@
../../build/AltBackup.ipa

View File

@@ -22,7 +22,7 @@
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" 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"> <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="2442.6666641235352" width="402" height="125"/> <rect key="frame" x="0.0" y="2469.3333282470703" width="402" height="125"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews> <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"> <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">
@@ -494,7 +494,7 @@
<tableViewSection id="1fc-f1-ALD"> <tableViewSection id="1fc-f1-ALD">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="7Ek-Ls-QVO" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="7Ek-Ls-QVO" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="793.66666412353516" width="402" height="51"/> <rect key="frame" x="0.0" y="794.66666412353516" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="7Ek-Ls-QVO" id="KjD-M3-oNg"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="7Ek-Ls-QVO" id="KjD-M3-oNg">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -531,13 +531,13 @@
</connections> </connections>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="hFh-X1-ZAi" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="hFh-X1-ZAi" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="844.66666412353516" width="402" height="51"/> <rect key="frame" x="0.0" y="845.66666412353516" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hFh-X1-ZAi" id="nCs-Ro-A6t"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hFh-X1-ZAi" id="nCs-Ro-A6t">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Clear Cache…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j4e-Mz-DlL"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Clear Cache…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j4e-Mz-DlL">
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="114.33333333333331" height="20.333333333333329"/> <rect key="frame" x="29.999999999999993" y="15.333333333333334" width="114.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -563,28 +563,28 @@
<tableViewSection headerTitle="" id="J90-vn-u2O"> <tableViewSection headerTitle="" id="J90-vn-u2O">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="i4T-2q-jF3" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="i4T-2q-jF3" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="931.33333015441895" width="402" height="51"/> <rect key="frame" x="0.0" y="932.33333015441895" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i4T-2q-jF3" id="VTQ-H4-aCM"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i4T-2q-jF3" id="VTQ-H4-aCM">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Developers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRA-OK-Vjw"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Developers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRA-OK-Vjw">
<rect key="frame" x="30" y="15.333333333333334" width="86" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="86" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk"> <stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
<rect key="frame" x="217" y="15.333333333333336" width="155" height="20.333333333333329"/> <rect key="frame" x="217" y="15.333333333333334" width="155" height="20.333333333333329"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideStore Team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore Team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb">
<rect key="frame" x="0.0" y="0.0" width="125.33333333333333" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="125.33333333333333" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
<rect key="frame" x="139.33333333333331" y="-1" width="15.666666666666657" height="22.333333333333332"/> <rect key="frame" x="139.33333333333331" y="-1" width="15.666666666666657" height="22.333333333333332"/>
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/> <imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView> </imageView>
@@ -608,29 +608,29 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="oHX-oR-nwJ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="oHX-oR-nwJ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="982.33333015441895" width="402" height="51"/> <rect key="frame" x="0.0" y="983.33333015441895" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oHX-oR-nwJ" id="hN4-i5-igu"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oHX-oR-nwJ" id="hN4-i5-igu">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
<rect key="frame" x="30" y="15.333333333333334" width="89" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="89" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X"> <stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
<rect key="frame" x="227.33333333333337" y="15.333333333333336" width="144.66666666666663" height="20.333333333333329"/> <rect key="frame" x="227.33333333333337" y="15.333333333333334" width="144.66666666666663" height="20.333333333333329"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
<rect key="frame" x="0.0" y="0.0" width="115" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="115" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
<rect key="frame" x="129" y="-1" width="15.666666666666657" height="22.333333333333332"/> <rect key="frame" x="128.99999999999997" y="-1" width="15.666666666666657" height="22.333333333333332"/>
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/> <imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -653,29 +653,29 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="0MT-ht-Sit" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="0MT-ht-Sit" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1033.3333301544189" width="402" height="51"/> <rect key="frame" x="0.0" y="1034.3333301544189" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0MT-ht-Sit" id="OZp-WM-5H7"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0MT-ht-Sit" id="OZp-WM-5H7">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM">
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/> <rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY"> <stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
<rect key="frame" x="235.33333333333337" y="15.333333333333336" width="136.66666666666663" height="20.333333333333329"/> <rect key="frame" x="235.33333333333337" y="15.333333333333334" width="136.66666666666663" height="20.333333333333329"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
<rect key="frame" x="0.0" y="0.0" width="107" height="20.333333333333332"/> <rect key="frame" x="0.0" y="0.0" width="107" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
<rect key="frame" x="120.99999999999997" y="-1" width="15.666666666666657" height="22.333333333333332"/> <rect key="frame" x="120.99999999999999" y="-1" width="15.666666666666671" height="22.333333333333332"/>
<imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/> <imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView> </imageView>
</subviews> </subviews>
@@ -698,19 +698,19 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1084.3333301544189" width="402" height="51"/> <rect key="frame" x="0.0" y="1085.3333301544189" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK">
<rect key="frame" x="30" y="15.333333333333334" width="67.333333333333329" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="67.333333333333329" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/> <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"/> <imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView> </imageView>
@@ -739,19 +739,19 @@
<tableViewSection headerTitle="" id="swj-Wc-IR6"> <tableViewSection headerTitle="" id="swj-Wc-IR6">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="xGq-wV-SCd" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="xGq-wV-SCd" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1170.9999961853027" width="402" height="51"/> <rect key="frame" x="0.0" y="1171.9999961853027" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="xGq-wV-SCd" id="G7G-sK-oO3"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="xGq-wV-SCd" id="G7G-sK-oO3">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Enable Beta Updates" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1B5-BJ-Rkb"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable Beta Updates" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1B5-BJ-Rkb">
<rect key="frame" x="30" y="15.333333333333334" width="169" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="169" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Blb-Dp-9QF"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Blb-Dp-9QF">
<rect key="frame" x="311" y="11.666666666666664" width="63" height="28"/> <rect key="frame" x="311" y="11.666666666666664" width="63" height="28"/>
<connections> <connections>
<action selector="toggleEnableBetaUpdates:" destination="aMk-Xp-UL8" eventType="valueChanged" id="9Ea-BQ-DAE"/> <action selector="toggleEnableBetaUpdates:" destination="aMk-Xp-UL8" eventType="valueChanged" id="9Ea-BQ-DAE"/>
@@ -774,19 +774,19 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="eHa-Cd-p4h" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="eHa-Cd-p4h" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1221.9999961853027" width="402" height="51"/> <rect key="frame" x="0.0" y="1222.9999961853027" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="eHa-Cd-p4h" id="V9s-7b-vkR"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="eHa-Cd-p4h" id="V9s-7b-vkR">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Beta Updates Track" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nSh-L8-ca0" userLabel="Beta Track Label"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Beta Updates Track" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nSh-L8-ca0" userLabel="Beta Track Label">
<rect key="frame" x="30" y="15.333333333333334" width="159.66666666666666" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="159.66666666666666" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<button opaque="NO" contentMode="scaleToFill" ambiguous="YES" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="right" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" changesSelectionAsPrimaryAction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kYQ-zz-vjQ" userLabel="Beta Track Drop Down Button"> <button opaque="NO" contentMode="scaleToFill" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="right" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" changesSelectionAsPrimaryAction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kYQ-zz-vjQ" userLabel="Beta Track Drop Down Button">
<rect key="frame" x="301.66666666666669" y="8.3333333333333321" width="70.333333333333314" height="34.333333333333343"/> <rect key="frame" x="301.66666666666669" y="8.3333333333333321" width="70.333333333333314" height="34.333333333333343"/>
<constraints> <constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="50" id="n1r-LA-2uh"/> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="50" id="n1r-LA-2uh"/>
@@ -815,19 +815,19 @@
<tableViewSection headerTitle="" id="OMa-EK-hRI"> <tableViewSection headerTitle="" id="OMa-EK-hRI">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1308.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1309.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<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"> <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">
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="125.33333333333331" height="20.333333333333329"/> <rect key="frame" x="29.999999999999993" y="15.333333333333334" width="125.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/> <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"/> <imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView> </imageView>
@@ -849,19 +849,19 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1359.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1360.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<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"> <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">
<rect key="frame" x="30" y="15.333333333333334" width="187.66666666666666" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="187.66666666666666" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/> <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"/> <imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView> </imageView>
@@ -886,19 +886,19 @@
</connections> </connections>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VrV-qI-zXF" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1410.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1411.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<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"> <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">
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/> <rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/> <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"/> <imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView> </imageView>
@@ -920,19 +920,19 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="VNn-u4-cN8" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1461.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1462.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<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"> <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">
<rect key="frame" x="30" y="15.333333333333334" width="140" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="140" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/> <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"/> <imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView> </imageView>
@@ -954,19 +954,19 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="e7s-hL-kv9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1512.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1513.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<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"> <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">
<rect key="frame" x="30" y="15.333333333333334" width="135.66666666666666" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="135.66666666666666" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
<rect key="frame" x="356.33333333333331" y="14.333333333333334" width="15.666666666666686" height="22.333333333333329"/> <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"/> <imageReference key="image" image="chevron.right" catalog="system" symbolScale="large"/>
</imageView> </imageView>
@@ -987,20 +987,54 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/> <userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="LEH-wv-o2q" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1564.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="LEH-wv-o2q" id="AP9-UO-vZk">
<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="VPN Configuration" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iie-Nj-lki" userLabel="VPN Configuration">
<rect key="frame" x="30" y="15.333333333333334" width="151.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="639-RC-1ii">
<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>
</subviews>
<constraints>
<constraint firstItem="iie-Nj-lki" firstAttribute="centerY" secondItem="AP9-UO-vZk" secondAttribute="centerY" id="Mxs-Yx-TRW"/>
<constraint firstItem="iie-Nj-lki" firstAttribute="leading" secondItem="AP9-UO-vZk" secondAttribute="leadingMargin" id="f7R-Ql-UHL"/>
<constraint firstItem="639-RC-1ii" firstAttribute="centerY" secondItem="AP9-UO-vZk" secondAttribute="centerY" id="tew-j2-B3V"/>
<constraint firstAttribute="trailingMargin" secondItem="639-RC-1ii" secondAttribute="trailing" id="ygE-k7-OKf"/>
</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="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="7rt-MT-kFH" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="7rt-MT-kFH" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1563.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1615.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="7rt-MT-kFH" id="mZL-UA-6V0"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="7rt-MT-kFH" id="mZL-UA-6V0">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Enable EMP for wireguard" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZH7-ZA-Epf" userLabel="Enable EMP for wireguard"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable EMP for wireguard" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZH7-ZA-Epf" userLabel="Enable EMP for wireguard">
<rect key="frame" x="30" y="15.333333333333334" width="209" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="209" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8qE-hE-Ujn"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8qE-hE-Ujn">
<rect key="frame" x="311" y="11.666666666666664" width="63" height="28"/> <rect key="frame" x="311" y="11.666666666666664" width="63" height="28"/>
<connections> <connections>
<action selector="toggleEnableEMPforWireguard:" destination="aMk-Xp-UL8" eventType="valueChanged" id="B0Q-Jb-fox"/> <action selector="toggleEnableEMPforWireguard:" destination="aMk-Xp-UL8" eventType="valueChanged" id="B0Q-Jb-fox"/>
@@ -1023,19 +1057,19 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="q6e-PG-mTq" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="q6e-PG-mTq" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1614.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1666.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="q6e-PG-mTq" id="PRJ-Ed-P86"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="q6e-PG-mTq" id="PRJ-Ed-P86">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Enable AppId Customization" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fq4-2u-Lgd" userLabel="Enable AppId Customization"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable AppId Customization" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fq4-2u-Lgd" userLabel="Enable AppId Customization">
<rect key="frame" x="30" y="15.333333333333334" width="230" height="20.333333333333329"/> <rect key="frame" x="30" y="15.333333333333334" width="230" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="fXx-wl-F5H"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="fXx-wl-F5H">
<rect key="frame" x="311" y="11.666666666666664" width="63" height="28"/> <rect key="frame" x="311" y="11.666666666666664" width="63" height="28"/>
<connections> <connections>
<action selector="toggleEnableAppIdCustomization:" destination="aMk-Xp-UL8" eventType="valueChanged" id="gtP-5M-9Ms"/> <action selector="toggleEnableAppIdCustomization:" destination="aMk-Xp-UL8" eventType="valueChanged" id="gtP-5M-9Ms"/>
@@ -1062,7 +1096,7 @@
<tableViewSection id="ZhW-yK-wdJ"> <tableViewSection id="ZhW-yK-wdJ">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="qjD-UK-myl" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="qjD-UK-myl" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1701.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1752.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qjD-UK-myl" id="bcu-KT-Xee"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qjD-UK-myl" id="bcu-KT-Xee">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1090,7 +1124,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="dNh-fp-vBs" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="dNh-fp-vBs" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1752.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1803.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="dNh-fp-vBs" id="Meb-tV-6br"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="dNh-fp-vBs" id="Meb-tV-6br">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1118,7 +1152,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Y6h-Bo-yec" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1803.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1854.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1146,7 +1180,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="dLk-d6-X4T" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1854.6666622161865" width="402" height="51"/> <rect key="frame" x="0.0" y="1905.6666622161865" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1178,7 +1212,7 @@
<tableViewSection headerTitle="" id="lLQ-K0-XSb"> <tableViewSection headerTitle="" id="lLQ-K0-XSb">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="daQ-mk-yqC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1941.3333282470703" width="402" height="51"/> <rect key="frame" x="0.0" y="1992.3333282470703" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1213,7 +1247,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="hRP-jU-2hd" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="1992.3333282470703" width="402" height="51"/> <rect key="frame" x="0.0" y="2043.3333282470703" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1248,7 +1282,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="JoN-Aj-XtZ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="2043.3333282470703" width="402" height="51"/> <rect key="frame" x="0.0" y="2094.3333282470703" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1283,7 +1317,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="QOO-bO-4M5" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="2094.3333282470703" width="402" height="51"/> <rect key="frame" x="0.0" y="2145.3333282470703" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1311,7 +1345,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="ToB-H7-2lR" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="2145.3333282470703" width="402" height="51"/> <rect key="frame" x="0.0" y="2196.3333282470703" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1339,7 +1373,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="xtI-eU-LFb" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="2196.3333282470703" width="402" height="51"/> <rect key="frame" x="0.0" y="2247.3333282470703" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1373,7 +1407,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="pvu-IV-Poa" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="2247.3333282470703" width="402" height="51"/> <rect key="frame" x="0.0" y="2298.3333282470703" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1408,7 +1442,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="9By-QW-Jw9" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="2298.3333282470703" width="402" height="51"/> <rect key="frame" x="0.0" y="2349.3333282470703" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
@@ -1443,7 +1477,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="LzP-Qb-bmC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target"> <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="2349.3333282470703" width="402" height="51"/> <rect key="frame" x="0.0" y="2400.3333282470703" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="402" height="51"/>

View File

@@ -77,6 +77,7 @@ extension SettingsViewController
case refreshSideJITServer case refreshSideJITServer
case resetPairingFile case resetPairingFile
case anisetteServers case anisetteServers
case vpnConfiguration
case enableEMPForWiregaurd case enableEMPForWiregaurd
case customizeAppId case customizeAppId
} }
@@ -1364,9 +1365,19 @@ extension SettingsViewController
handleRefreshResult(result) handleRefreshResult(result)
}) })
let anisetteServersController = UIHostingController(rootView: anisetteServersView) let vc = UIHostingController(rootView: anisetteServersView)
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: vc), sender: nil)
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: anisetteServersController), sender: nil) case .vpnConfiguration:
let vpnConfigurationView = VPNConfigurationView()
let vc = UIHostingController(rootView: vpnConfigurationView)
let appearance = UINavigationBarAppearance()
appearance.configureWithDefaultBackground() // gives solid background
vc.navigationItem.scrollEdgeAppearance = appearance
vc.navigationItem.standardAppearance = appearance
navigationController?.pushViewController(vc, animated: true)
case .refreshAttempts, .enableEMPForWiregaurd, .customizeAppId: break case .refreshAttempts, .enableEMPForWiregaurd, .customizeAppId: break
} }
case .signing: case .signing:

View File

@@ -0,0 +1,119 @@
//
// VPNConfiguration.swift
// AltStore
//
// Created by Magesh K on 02/03/26.
// Copyright © 2026 SideStore. All rights reserved.
//
import SwiftUI
import Combine
private typealias SButton = SwiftUI.Button
struct VPNConfigurationView: View {
@Environment(\.presentationMode) var presentationMode
@StateObject private var config = TunnelConfig.shared
var body: some View {
List {
Section(header: Text("Discovered from network")) {
Group {
networkConfigRow(label: "Tunnel IP", text: $config.deviceIP, editable: false)
networkConfigRow(label: "Device IP", text: $config.fakeIP, editable: false)
networkConfigRow(label: "Subnet Mask", text: $config.subnetMask, editable: false)
}
}
Section {
networkConfigRow(
label: "Device IP",
text: Binding<String?>(get: { config.overrideFakeIP }, set: { config.overrideFakeIP = $0 ?? "" }),
editable: true
)
networkConfigRow(
label: "Active",
text: Binding<String?>(get: { config.overrideActive }, set: { _ in }),
editable: false
)
} header: {
Text("User Configuration")
} footer: {
HStack(alignment: .top, spacing: 0) {
Text("Note: ")
Text("'Device IP' is mandatory and should match exactly as in the VPN's configuration")
}
}
}
.navigationTitle("VPN Configuration")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
SButton("Confirm") {
commitChanges()
}
}
}
}
private func commitChanges() {
bindTunnelConfig()
}
private func dismiss() {
presentationMode.wrappedValue.dismiss()
}
private func networkConfigRow(
label: LocalizedStringKey,
text: Binding<String?>,
editable: Bool
) -> some View {
let proxy = Binding<String>(
get: { text.wrappedValue ?? "N/A" },
set: { text.wrappedValue = $0.isEmpty || $0 == "N/A" ? nil : $0 }
)
return HStack {
Text(label)
.foregroundColor(editable ? .primary : .gray)
Spacer()
TextField(label, text: proxy)
.multilineTextAlignment(.trailing)
.foregroundColor(editable ? .secondary : .gray)
.disabled(!editable)
.keyboardType(.numbersAndPunctuation)
.onChange(of: proxy.wrappedValue) { newValue in
guard editable else { return }
proxy.wrappedValue =
newValue.filter { "0123456789.".contains($0) }
}
}
}
}
final class TunnelConfig: ObservableObject {
static let shared = TunnelConfig()
private static let defaultOverrideIP: String = {
if #available(iOS 26.4, *) { return "192.168.1.50" }
return "10.7.0.1"
}()
@Published var deviceIP: String?
@Published var subnetMask: String?
@Published var fakeIP: String?
@Published var overrideFakeIP: String = overrideIPStorage {
didSet { Self.overrideIPStorage = overrideFakeIP }
}
@Published var overrideEffective: Bool = false
private static var overrideIPStorage: String {
get { UserDefaults.standard.string(forKey: "TunnelOverrideFakeIP") ?? defaultOverrideIP }
set { UserDefaults.standard.set(newValue, forKey: "TunnelOverrideFakeIP") }
}
var overrideActive: String { overrideEffective ? "Yes" : "No" }
}

View File

@@ -61,35 +61,6 @@ final class TabBarController: UITabBarController
self.initialSegue = nil self.initialSegue = nil
self.performSegue(withIdentifier: identifier, sender: sender) self.performSegue(withIdentifier: identifier, sender: sender)
} }
else if let patchedApps = UserDefaults.standard.patchedApps, !patchedApps.isEmpty
{
// Check if we need to finish installing untethered jailbreak.
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
guard let patchedApp = activeApps.first(where: { patchedApps.contains($0.bundleIdentifier) }) else { return }
self.performSegue(withIdentifier: "finishJailbreak", sender: patchedApp)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard let identifier = segue.identifier else { return }
switch identifier
{
case "finishJailbreak":
guard let installedApp = sender as? InstalledApp else { return }
let navigationController = segue.destination as! UINavigationController
let patchViewController = navigationController.viewControllers.first as! PatchViewController
patchViewController.installedApp = installedApp
patchViewController.completionHandler = { [weak self] _ in
self?.dismiss(animated: true, completion: nil)
}
default: break
}
} }
override func performSegue(withIdentifier identifier: String, sender: Any?) override func performSegue(withIdentifier identifier: String, sender: Any?)

View File

@@ -399,6 +399,40 @@ private extension DatabaseManager
// For backwards compatibility reasons, we cannot use localApp's buildVersion as storeBuildVersion, // For backwards compatibility reasons, we cannot use localApp's buildVersion as storeBuildVersion,
// or else the latest update will _always_ be considered new because we don't use buildVersions in our source (yet). // or else the latest update will _always_ be considered new because we don't use buildVersions in our source (yet).
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, storeBuildVersion: nil, context: context) installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, storeBuildVersion: nil, context: context)
// figure out if the current AltStoreApp is signed with "Use Main Profie" option
// by checking if the first extension's entitlement's application-identifier matches current one
repeat {
guard let pluginURL = Bundle.main.builtInPlugInsURL else {
installedApp.useMainProfile = true
break
}
guard let pluginFolders = try? FileManager.default.contentsOfDirectory(at: pluginURL, includingPropertiesForKeys: nil) else {
installedApp.useMainProfile = true
break
}
guard let pluginFolder = pluginFolders.first, let altPluginApp = ALTApplication(fileURL: pluginFolder) else {
installedApp.useMainProfile = true
break
}
let entitlements = altPluginApp.entitlements
guard let appId = entitlements[ALTEntitlement.applicationIdentifier] as? String else {
installedApp.useMainProfile = false
print("no ALTEntitlementApplicationIdentifier???")
break
}
if appId.hasSuffix(Bundle.main.bundleIdentifier!) {
installedApp.useMainProfile = true
} else {
installedApp.useMainProfile = false
}
} while(false)
installedApp.storeApp = storeApp installedApp.storeApp = storeApp
} }

View File

@@ -1,254 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 53;
objects = {
/* Begin PBXBuildFile section */
A8A5B08D2F4C387300572B4A /* 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)",
"$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)",
"$(SRCROOT)/Dependencies/em_proxy/em_proxy.h",
);
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>"; };
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 */
CA6094FFF69222869D176AE5 /* Products */ = {
isa = PBXGroup;
children = (
CA60C44C93D7916DE57E6EBD /* libem_proxy_static.a */,
);
name = Products;
sourceTree = "<group>";
};
CA6094FFF692D65BC3C892A8 = {
isa = PBXGroup;
children = (
9999259129A45319005CF020 /* em_proxy.h */,
CA6094FFF69222869D176AE5 /* Products */,
);
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
CA60C44C93D7A30E3695DD59 /* em_proxy-staticlib */ = {
isa = PBXNativeTarget;
buildConfigurationList = CA603DD75FB4A30E3695DD59 /* Build configuration list for PBXNativeTarget "em_proxy-staticlib" */;
buildPhases = (
9987603529A4610700818586 /* Fetch prebuilt binaries */,
CA60445C3036A30E3695DD59 /* Sources */,
);
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 = {
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 */,
);
};
/* End PBXProject section */
/* Begin PBXShellScriptBuildPhase section */
9987603529A4610700818586 /* Fetch prebuilt binaries */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Fetch prebuilt binaries";
outputFileListPaths = (
);
outputPaths = (
./em_proxy/em_proxy.h,
./em_proxy/em_proxy.swift,
"./em_proxy/libem_proxy-ios.a",
"./em_proxy/libem_proxy-sim.a",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "#!bash\npwd\nchmod +x ./fetch-prebuilt.sh \n./fetch-prebuilt.sh em_proxy\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
CA60445C3036A30E3695DD59 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A8A5B08D2F4C387300572B4A /* em_proxy.h in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
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";
MACOSX_DEPLOYMENT_TARGET = 11.5;
PRODUCT_NAME = em_proxy_static;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos";
TVOS_DEPLOYMENT_TARGET = 11.5;
};
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;
};
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";
MACOSX_DEPLOYMENT_TARGET = 11.5;
PRODUCT_NAME = em_proxy_static;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos";
TVOS_DEPLOYMENT_TARGET = 11.5;
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
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 */;
}

View File

@@ -1,118 +0,0 @@
#!/usr/bin/env bash
# Ensure we are in Dependencies directory
cd "$(dirname "$0")"
# Detect if Homebrew is in /opt/homebrew (Apple Silicon) or /usr/local (Intel)
if [[ -d "/opt/homebrew" ]]; then
export PATH="/opt/homebrew/bin:$PATH"
elif [[ -d "/usr/local" ]]; then
export PATH="/usr/local/bin:$PATH"
fi
# Check if wget and curl are installed; if not, install them via Homebrew
if ! command -v wget &> /dev/null; then
echo "wget not found, attempting to install via Homebrew..."
if command -v brew &> /dev/null; then
brew install wget
else
echo "Homebrew is not installed. Please install Homebrew and rerun the script."
exit 1
fi
fi
if ! command -v curl &> /dev/null; then
echo "curl not found, attempting to install via Homebrew..."
if command -v brew &> /dev/null; then
brew install curl
else
echo "Homebrew is not installed. Please install Homebrew and rerun the script."
exit 1
fi
fi
check_for_update() {
if [ -f ".skip-prebuilt-fetch-$1" ]; then
echo "Skipping prebuilt fetch for $1 since .skip-prebuilt-fetch-$1 exists. If you are developing $1 alongside SideStore, don't remove this file, or this script will replace your locally built binaries with the ones built by GitHub Actions."
return
fi
if [ ! -f ".last-prebuilt-fetch-$1" ]; then
echo "0,none" > ".last-prebuilt-fetch-$1"
fi
LAST_FETCH=`cat .last-prebuilt-fetch-$1 | perl -n -e '/([0-9]*),([^ ]*)$/ && print $1'`
LAST_COMMIT=`cat .last-prebuilt-fetch-$1 | perl -n -e '/([0-9]*),([^ ]*)$/ && print $2'`
# Check if required library files exist
FORCE_DOWNLOAD=false
if [ ! -f "$1/lib$1-sim.a" ] || [ ! -f "$1/lib$1-ios.a" ]; then
echo "Required libraries missing for $1, forcing download..."
FORCE_DOWNLOAD=true
fi
# Download if:
# 1. Libraries are missing (FORCE_DOWNLOAD), or
# 2. Last fetch was over 1 hour ago, or
# 3. Force flag was passed
if [ "$FORCE_DOWNLOAD" = true ] || [[ $LAST_FETCH -lt $(expr $(date +%s) - 3600) ]] || [[ "$2" == "force" ]]; then
echo "Checking $1 for update"
echo
LATEST_COMMIT=`curl https://api.github.com/repos/SideStore/$1/releases/latest | perl -n -e '/Commit: https:\\/\\/github\\.com\\/[^\\/]*\\/[^\\/]*\\/commit\\/([^"]*)/ && print $1'`
echo
echo "Last commit: $LAST_COMMIT"
echo "Latest commit: $LATEST_COMMIT"
NOT_UPTODATE=false
if [[ "$LAST_COMMIT" != "$LATEST_COMMIT" ]]; then
echo "Found update on the remote: https://api.github.com/repos/SideStore/$1/releases/latest"
NOT_UPTODATE=true
fi
# Download if:
# 1. Libraries are missing (FORCE_DOWNLOAD), or
# 2. New commit is available
if [ "$FORCE_DOWNLOAD" = true ] || [ "$NOT_UPTODATE" = true ] ;then
echo "downloading binaries"
echo
if [[ "$1" != "minimuxer" ]]; then
wget -O "$1/lib$1-sim.a" "https://github.com/SideStore/$1/releases/latest/download/lib$1-sim.a"
wget -O "$1/lib$1-ios.a" "https://github.com/SideStore/$1/releases/latest/download/lib$1-ios.a"
wget -O "$1/$1.h" "https://github.com/SideStore/$1/releases/latest/download/$1.h"
wget -O "$1/$1.swift" "https://github.com/SideStore/$1/releases/latest/download/$1.swift"
echo
else
wget -O "$1/lib$1-sim.a" "https://github.com/SideStore/$1/releases/latest/download/lib$1-sim.a"
wget -O "$1/lib$1-ios.a" "https://github.com/SideStore/$1/releases/latest/download/lib$1-ios.a"
wget -O "$1/generated.zip" "https://github.com/SideStore/$1/releases/latest/download/generated.zip"
echo
echo "Unzipping generated.zip"
cd "$1"
unzip ./generated.zip
cp -v generated/* .
# Remove all files except ones that comes checked-in from minimuxer repository
find generated -type f ! -name 'minimuxer-Bridging-Header.h' ! -name 'minimuxer-helpers.swift' -exec rm -v {} \;
rm generated.zip
rmdir generated/
cd ..
echo "Done"
fi
else
echo "Up-to-date"
fi
echo "$(date +%s),$LATEST_COMMIT" > ".last-prebuilt-fetch-$1"
else
echo "It hasn't been 1 hour and force was not specified, skipping update check for $1"
fi
}
# Allow for Xcode to check minimuxer and em_proxy separately by skipping the update check if the other one is specified as an argument
if [[ "$1" != "em_proxy" ]]; then
check_for_update minimuxer "$1"
if [[ "$1" != "minimuxer" ]]; then
echo
fi
fi
if [[ "$1" != "minimuxer" ]]; then
check_for_update em_proxy "$1"
fi

Binary file not shown.

View File

@@ -1,264 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 53;
objects = {
/* Begin PBXBuildFile section */
A8A5B07F2F4C355800572B4A /* minimuxer.h in Sources */ = {isa = PBXBuildFile; fileRef = A8A5B07D2F4C34FF00572B4A /* minimuxer.h */; };
A8A5B0802F4C355800572B4A /* SwiftBridgeCore.h in Sources */ = {isa = PBXBuildFile; fileRef = A8A5B07E2F4C34FF00572B4A /* SwiftBridgeCore.h */; };
/* End PBXBuildFile section */
/* Begin PBXBuildRule section */
CA6012A875F9AC6C1400ACA8 /* PBXBuildRule */ = {
isa = PBXBuildRule;
compilerSpec = com.apple.compilers.proxy.script;
filePatterns = "*/minimuxer.h";
fileType = pattern.proxy;
inputFiles = (
);
isEditable = 0;
name = "Cargo project build";
outputFiles = (
"$(OBJECT_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME)",
"$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)",
"$(SRCROOT)/Dependencies/minimuxer/minimuxer.h",
);
script = "# generated with cargo-xcode 1.5.0\n# modified to use prebuilt binaries\n\nset -eu;\n\nBUILT_SRC=\"./minimuxer/${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 */
A8A5B07D2F4C34FF00572B4A /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = minimuxer.h; path = minimuxer/minimuxer.h; sourceTree = "<group>"; };
A8A5B07E2F4C34FF00572B4A /* SwiftBridgeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SwiftBridgeCore.h; path = minimuxer/SwiftBridgeCore.h; sourceTree = "<group>"; };
A8A5B09C2F4C454900572B4A /* minimuxer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "minimuxer-Bridging-Header.h"; path = "minimuxer/minimuxer-Bridging-Header.h"; sourceTree = "<group>"; };
CA609C732349C7AAD9FA67C4 /* libminimuxer_static.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libminimuxer_static.a; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
CA6012A875F922869D176AE5 /* Products */ = {
isa = PBXGroup;
children = (
CA609C732349C7AAD9FA67C4 /* libminimuxer_static.a */,
);
name = Products;
sourceTree = "<group>";
};
CA6012A875F9D65BC3C892A8 = {
isa = PBXGroup;
children = (
A8A5B07D2F4C34FF00572B4A /* minimuxer.h */,
A8A5B07E2F4C34FF00572B4A /* SwiftBridgeCore.h */,
A8A5B09C2F4C454900572B4A /* minimuxer-Bridging-Header.h */,
CA6012A875F922869D176AE5 /* Products */,
);
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
CA609C732349A560B9642892 /* minimuxer-staticlib */ = {
isa = PBXNativeTarget;
buildConfigurationList = CA600589A243A560B9642892 /* Build configuration list for PBXNativeTarget "minimuxer-staticlib" */;
buildPhases = (
9987603629A4611D00818586 /* Fetch prebuilt binaries */,
CA600F638141A560B9642892 /* Sources */,
);
buildRules = (
CA6012A875F9AC6C1400ACA8 /* PBXBuildRule */,
);
dependencies = (
);
name = "minimuxer-staticlib";
productName = libminimuxer_static.a;
productReference = CA609C732349C7AAD9FA67C4 /* libminimuxer_static.a */;
productType = "com.apple.product-type.library.static";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
CA6012A875F9E04653AD465F /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
TargetAttributes = {
CA609C732349A560B9642892 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = CA6012A875F980E02D6C7F57 /* Build configuration list for PBXProject "minimuxer" */;
compatibilityVersion = "Xcode 11.4";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = CA6012A875F9D65BC3C892A8;
productRefGroup = CA6012A875F922869D176AE5 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
CA609C732349A560B9642892 /* minimuxer-staticlib */,
);
};
/* End PBXProject section */
/* Begin PBXShellScriptBuildPhase section */
9987603629A4611D00818586 /* Fetch prebuilt binaries */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Fetch prebuilt binaries";
outputFileListPaths = (
);
outputPaths = (
./minimuxer/minimuxer.h,
./minimuxer/SwiftBridgeCore.h,
./minimuxer/minimuxer.swift,
./minimuxer/SwiftBridgeCore.swift,
"./minimuxer/minimuxer-Bridging-Header.h",
"./minimuxer/minimuxer-helpers.swift",
"./minimuxer/libminimuxer-ios.a",
"./minimuxer/libminimuxer-sim.a",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "#!bash\npwd\nchmod +x ./fetch-prebuilt.sh \n./fetch-prebuilt.sh minimuxer\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
CA600F638141A560B9642892 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A8A5B07F2F4C355800572B4A /* minimuxer.h in Sources */,
A8A5B0802F4C355800572B4A /* SwiftBridgeCore.h in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
CA6008D36272A560B9642892 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CARGO_XCODE_CARGO_DEP_FILE_NAME = libminimuxer.d;
CARGO_XCODE_CARGO_FILE_NAME = libminimuxer.a;
INSTALL_GROUP = "";
INSTALL_MODE_FLAG = "";
INSTALL_OWNER = "";
LIB_FILE_NAME = "";
"LIB_FILE_NAME[sdk=iphoneos*]" = "libminimuxer-ios";
"LIB_FILE_NAME[sdk=iphonesimulator*]" = "libminimuxer-sim";
MACOSX_DEPLOYMENT_TARGET = 11.5;
PRODUCT_NAME = minimuxer_static;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos";
TVOS_DEPLOYMENT_TARGET = 11.5;
};
name = Debug;
};
CA602DE9FCEDA560B9642892 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CARGO_XCODE_CARGO_DEP_FILE_NAME = libminimuxer.d;
CARGO_XCODE_CARGO_FILE_NAME = libminimuxer.a;
INSTALL_GROUP = "";
INSTALL_MODE_FLAG = "";
INSTALL_OWNER = "";
LIB_FILE_NAME = "";
"LIB_FILE_NAME[sdk=iphoneos*]" = "libminimuxer-ios";
"LIB_FILE_NAME[sdk=iphonesimulator*]" = "libminimuxer-sim";
MACOSX_DEPLOYMENT_TARGET = 11.5;
PRODUCT_NAME = minimuxer_static;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos";
TVOS_DEPLOYMENT_TARGET = 11.5;
};
name = Release;
};
CA60A20F8EA6228BE02872F8 /* 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 = minimuxer;
SDKROOT = macosx;
SUPPORTS_MACCATALYST = YES;
};
name = Debug;
};
CA60A20F8EA63CC16B37690B /* 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 = minimuxer;
SDKROOT = macosx;
SUPPORTS_MACCATALYST = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
CA600589A243A560B9642892 /* Build configuration list for PBXNativeTarget "minimuxer-staticlib" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CA602DE9FCEDA560B9642892 /* Release */,
CA6008D36272A560B9642892 /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
CA6012A875F980E02D6C7F57 /* Build configuration list for PBXProject "minimuxer" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CA60A20F8EA63CC16B37690B /* Release */,
CA60A20F8EA6228BE02872F8 /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = CA6012A875F9E04653AD465F /* Project object */;
}

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CA609C732349A560B9642892"
BuildableName = "libminimuxer_static.a"
BlueprintName = "minimuxer-staticlib"
ReferencedContainer = "container:minimuxer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CA609C732349A560B9642892"
BuildableName = "libminimuxer_static.a"
BlueprintName = "minimuxer-staticlib"
ReferencedContainer = "container:minimuxer.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -374,8 +374,7 @@ ipa-altbackup: checkPaths copy-altbackup
@echo " Copying from $(ALT_APP_SRC) into $(ALT_APP_PAYLOAD_DST)" @echo " Copying from $(ALT_APP_SRC) into $(ALT_APP_PAYLOAD_DST)"
@cp -R -f "$(ALT_APP_SRC)/." "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)" @cp -R -f "$(ALT_APP_SRC)/." "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
@pushd "$(ALT_APP_DST_ARCHIVE)" && zip -r "../../$(ALT_APP_IPA_DST)" Payload || popd @pushd "$(ALT_APP_DST_ARCHIVE)" && zip -r "../../$(ALT_APP_IPA_DST)" Payload || popd
@cp -f "$(ALT_APP_IPA_DST)" AltStore/Resources @echo " IPA created: build/AltBackup.ipa"
@echo " IPA created: AltStore/Resources/AltBackup.ipa"
clean-altbackup: clean-altbackup:
@echo "" @echo ""

View File

@@ -6,79 +6,178 @@
// //
import Foundation import Foundation
private import minimuxer import Minimuxer
func bindTunnelConfig() {
defer { print("[SideStore] bindTunnelConfig() completed") }
#if targetEnvironment(simulator)
print("[SideStore] bindTunnelConfig() is no-op on simulator")
#else
print("[SideStore] bindTunnelConfig() invoked")
Task { @MainActor in
let config = TunnelConfig.shared
Minimuxer.bindTunnelConfig(
TunnelConfigBinding(
setDeviceIP: { value in Task { @MainActor in config.deviceIP = value } },
setFakeIP: { value in Task { @MainActor in config.fakeIP = value } },
setSubnetMask: { value in Task { @MainActor in config.subnetMask = value } },
getOverrideFakeIP: { config.overrideFakeIP },
setOverrideEffective: { value in Task { @MainActor in config.overrideEffective = value } }
)
)
}
#endif
}
var isMinimuxerReady: Bool { var isMinimuxerReady: Bool {
var result = true
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
print("isMinimuxerReady property is always true on simulator") print("[SideStore] isMinimuxerReady = true on simulator")
return true
#else #else
return minimuxer.ready() result = Minimuxer.ready()
print("[SideStore] isMinimuxerReady = \(result)")
#endif
return result
}
func retargetUsbmuxdAddr() {
defer { print("[SideStore] retargetUsbmuxdAddr() completed") }
#if targetEnvironment(simulator)
print("[SideStore] retargetUsbmuxdAddr() is no-op on simulator")
#else
print("[SideStore] retargetUsbmuxdAddr() invoked")
Minimuxer.retargetUsbmuxdAddr()
#endif #endif
} }
func minimuxerStartWithLogger(_ pairingFile: String,_ logPath: String,_ loggingEnabled: Bool) throws { func minimuxerStartWithLogger(_ pairingFile: String, _ logPath: String, _ loggingEnabled: Bool) throws {
defer { print("[SideStore] minimuxerStartWithLogger(pairingFile, logPath, dest, loggingEnabled) completed") }
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
print("minimuxerStartWithLogger(\(pairingFile), \(logPath), \(loggingEnabled) is no-op on simulator") print("[SideStore] minimuxerStartWithLogger(pairingFile, logPath, loggingEnabled) is no-op on simulator")
#else #else
try minimuxer.startWithLogger(pairingFile, logPath, loggingEnabled) // refresh config if any
#endif bindTunnelConfig()
} // observe network route changes (and update device endpoint from vpn(utun))
NetworkObserver.shared.start()
func targetMinimuxerAddress() { print("[SideStore] minimuxerStartWithLogger(pairingFile, logPath, dest, loggingEnabled) invoked")
#if targetEnvironment(simulator) try Minimuxer.startWithLogger(pairingFile: pairingFile,
print("targetMinimuxerAddress() is no-op on simulator") logPath: logPath,
#else isConsoleLoggingEnabled: loggingEnabled)
minimuxer.target_minimuxer_address()
#endif #endif
} }
func installProvisioningProfiles(_ profileData: Data) throws { func installProvisioningProfiles(_ profileData: Data) throws {
defer { print("[SideStore] installProvisioningProfiles(profileData) completed") }
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
print("installProvisioningProfiles(\(profileData)) is no-op on simulator") print("[SideStore] installProvisioningProfiles(profileData) is no-op on simulator")
#else #else
try minimuxer.install_provisioning_profile(profileData.toRustByteSlice().forRust()) print("[SideStore] installProvisioningProfiles(profileData) invoked")
try Minimuxer.installProvisioningProfile(profile: profileData)
#endif #endif
} }
func removeProvisioningProfile(_ id: String) throws {
defer { print("[SideStore] removeProvisioningProfile(id) completed") }
#if targetEnvironment(simulator)
print("[SideStore] removeProvisioningProfile(id) is no-op on simulator")
#else
print("[SideStore] removeProvisioningProfile(id) invoked")
try Minimuxer.removeProvisioningProfile(id: id)
#endif
}
func removeApp(_ bundleId: String) throws { func removeApp(_ bundleId: String) throws {
defer { print("[SideStore] removeApp(bundleId) completed") }
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
print("removeApp(\(bundleId)) is no-op on simulator") print("[SideStore] removeApp(bundleId) is no-op on simulator")
#else #else
try minimuxer.remove_app(bundleId) print("[SideStore] removeApp(bundleId) invoked")
try Minimuxer.removeApp(bundleId: bundleId)
#endif #endif
} }
func yeetAppAFC(_ bundleId: String, _ rawBytes: Data) throws { func yeetAppAFC(_ bundleId: String, _ rawBytes: Data) throws {
defer { print("[SideStore] yeetAppAFC(bundleId, rawBytes) completed") }
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
print("yeetAppAFC(\(bundleId), \(rawBytes)) is no-op on simulator") print("[SideStore] yeetAppAFC(bundleId, rawBytes) is no-op on simulator")
#else #else
try minimuxer.yeet_app_afc(bundleId, rawBytes.toRustByteSlice().forRust()) print("[SideStore] yeetAppAFC(bundleId, rawBytes) invoked")
try Minimuxer.yeetAppAfc(bundleId: bundleId, ipaBytes: rawBytes)
#endif #endif
} }
func installIPA(_ bundleId: String) throws { func installIPA(_ bundleId: String) throws {
defer { print("[SideStore] installIPA(bundleId) completed") }
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
print("installIPA(\(bundleId)) is no-op on simulator") print("[SideStore] installIPA(bundleId) is no-op on simulator")
#else #else
try minimuxer.install_ipa(bundleId) print("[SideStore] installIPA(bundleId) invoked")
try Minimuxer.installIpa(bundleId: bundleId)
#endif #endif
} }
func fetchUDID() -> String? { func fetchUDID() -> String? {
defer { print("[SideStore] fetchUDID() completed") }
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
print("fetchUDID() is no-op on simulator") print("[SideStore] fetchUDID() is no-op on simulator")
return "XXXXX-XXXX-XXXXX-XXXX" return "XXXXX-XXXX-XXXXX-XXXX"
#else #else
return minimuxer.fetch_udid()?.toString() print("[SideStore] fetchUDID() invoked")
return Minimuxer.fetchUDID()
#endif #endif
} }
func debugApp(_ appId: String) throws {
defer { print("[SideStore] debugApp(appId) completed") }
#if targetEnvironment(simulator)
print("[SideStore] debugApp(appId) is no-op on simulator")
#else
print("[SideStore] debugApp(appId) invoked")
try Minimuxer.debugApp(appId: appId)
#endif
}
func attachDebugger(_ pid: UInt32) throws {
defer { print("[SideStore] attachDebugger(pid) completed") }
#if targetEnvironment(simulator)
print("[SideStore] attachDebugger(pid) is no-op on simulator")
#else
print("[SideStore] attachDebugger(pid) invoked")
try Minimuxer.attachDebugger(pid: pid)
#endif
}
func startAutoMounter(_ docsPath: String) {
defer { print("[SideStore] startAutoMounter(docsPath) completed") }
#if targetEnvironment(simulator)
print("[SideStore] startAutoMounter(docsPath) is no-op on simulator")
#else
print("[SideStore] startAutoMounter(docsPath) invoked")
Minimuxer.startAutoMounter(docsPath: docsPath)
#endif
}
func dumpProfiles(_ docsPath: String) throws -> String {
defer { print("[SideStore] dumpProfiles(docsPath) completed") }
#if targetEnvironment(simulator)
print("[SideStore] dumpProfiles(docsPath) is no-op on simulator")
return ""
#else
print("[SideStore] dumpProfiles(docsPath) invoked")
return try Minimuxer.dumpProfiles(docsPath: docsPath)
#endif
}
func setMinimuxerDebug(_ debug: Bool) {
defer { print("[SideStore] setMinimuxerDebug(debug) completed") }
print("[SideStore] setMinimuxerDebug(debug) invoked")
Minimuxer.setDebug(debug)
}
extension MinimuxerError: @retroactive LocalizedError { extension MinimuxerError: @retroactive LocalizedError {
public var failureReason: String? { public var failureReason: String? {
@@ -89,41 +188,38 @@ extension MinimuxerError: @retroactive LocalizedError {
return NSLocalizedString("Unable to connect to the device, make sure LocalDevVPN is enabled and you're connected to Wi-Fi. This could mean an invalid pairing.", comment: "") return NSLocalizedString("Unable to connect to the device, make sure LocalDevVPN is enabled and you're connected to Wi-Fi. This could mean an invalid pairing.", comment: "")
case .PairingFile: 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 iloader to replace 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 iloader to replace it.", comment: "")
case .CreateDebug: case .CreateDebug:
return self.createService(name: "debug") return createService(name: "debug")
case .LookupApps: case .LookupApps:
return self.getFromDevice(name: "installed apps") return getFromDevice(name: "installed apps")
case .FindApp: case .FindApp:
return self.getFromDevice(name: "path to the app") return getFromDevice(name: "path to the app")
case .BundlePath: case .BundlePath:
return self.getFromDevice(name: "bundle path") return getFromDevice(name: "bundle path")
case .MaxPacket: case .MaxPacket:
return self.setArgument(name: "max packet") return setArgument(name: "max packet")
case .WorkingDirectory: case .WorkingDirectory:
return self.setArgument(name: "working directory") return setArgument(name: "working directory")
case .Argv: case .Argv:
return self.setArgument(name: "argv") return setArgument(name: "argv")
case .LaunchSuccess: case .LaunchSuccess:
return self.getFromDevice(name: "launch success") return getFromDevice(name: "launch success")
case .Detach: case .Detach:
return NSLocalizedString("Unable to detach from the app's process", comment: "") return NSLocalizedString("Unable to detach from the app's process", comment: "")
case .Attach: case .Attach:
return NSLocalizedString("Unable to attach to the app's process", comment: "") return NSLocalizedString("Unable to attach to the app's process", comment: "")
case .CreateInstproxy: case .CreateInstproxy:
return self.createService(name: "instproxy") return createService(name: "instproxy")
case .CreateAfc: case .CreateAfc:
return self.createService(name: "AFC") return createService(name: "AFC")
case .RwAfc: case .RwAfc:
return NSLocalizedString("AFC was unable to manage files on the device. Ensure Wi-Fi and LocalDevVPN are connected. If they both are, replace your pairing using iloader.", comment: "") return NSLocalizedString("AFC was unable to manage files on the device.", comment: "")
case .InstallApp(let message): case .InstallApp(let message):
return NSLocalizedString("Unable to install the app: \(message.toString())", comment: "") return NSLocalizedString("Unable to install the app: \(message)", comment: "")
case .UninstallApp: case .UninstallApp:
return NSLocalizedString("Unable to uninstall the app", comment: "") return NSLocalizedString("Unable to uninstall the app", comment: "")
case .CreateMisagent: case .CreateMisagent:
return self.createService(name: "misagent") return createService(name: "misagent")
case .ProfileInstall: case .ProfileInstall:
return NSLocalizedString("Unable to manage profiles on the device", comment: "") return NSLocalizedString("Unable to manage profiles on the device", comment: "")
case .ProfileRemove: case .ProfileRemove:
@@ -164,14 +260,14 @@ extension MinimuxerError: @retroactive LocalizedError {
} }
fileprivate func createService(name: String) -> String { fileprivate func createService(name: String) -> String {
return String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name) String(format: NSLocalizedString("Cannot start a %@ server on the device.", comment: ""), name)
} }
fileprivate func getFromDevice(name: String) -> String { fileprivate func getFromDevice(name: String) -> String {
return String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name) String(format: NSLocalizedString("Cannot fetch %@ from the device.", comment: ""), name)
} }
fileprivate func setArgument(name: String) -> String { fileprivate func setArgument(name: String) -> String {
return String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name) String(format: NSLocalizedString("Cannot set %@ on the device.", comment: ""), name)
} }
} }

View File

@@ -1,37 +0,0 @@
### nightly
#### What's Changed
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- altsign updated to latest
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- re added openSSL from new path
- updated altsign to use xcframework for openSSL which was causing huge download of 1.2 GB each time
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: improve more ci worflow
- CI: full rewrite - moved logic into ci.py and kept workflow scripts mostly dummy
#### Full Changelog: [38715283...99712f00](https://github.com/SideStore/SideStore/compare/38715283073ea37949a462b889ce3cad403ea499...99712f0020a4f2ae57d8d781514fa735f893c23a)

View File

@@ -95,6 +95,7 @@ def resolve_start_commit(last_successful: str):
except Exception: except Exception:
return first_commit() return first_commit()
def generate_release_notes(last_successful, tag, branch): def generate_release_notes(last_successful, tag, branch):
current = head_commit() current = head_commit()
@@ -124,7 +125,12 @@ def generate_release_notes(last_successful, tag, branch):
for m in messages: for m in messages:
section += f"{fmt_msg(m)}\n" section += f"{fmt_msg(m)}\n"
prev_authors = authors(branch) if commit_exists(branch):
previous_range = branch
else:
previous_range = last_successful
prev_authors = authors(previous_range)
recent_authors = authors(f"{last_successful}..{current}") recent_authors = authors(f"{last_successful}..{current}")
new_authors = recent_authors - prev_authors new_authors = recent_authors - prev_authors
@@ -137,13 +143,26 @@ def generate_release_notes(last_successful, tag, branch):
url = repo_url() url = repo_url()
section += ( section += (
f"\n{HEADER_MARKER} Full Changelog: " f"\n{HEADER_MARKER} Full Changelog: "
f"[{last_successful[:8]}...{current[:8]}]" f"[{ref_display(last_successful)}...{ref_display(current)}]"
f"({url}/compare/{last_successful}...{current})\n" f"({url}/compare/{last_successful}...{current})\n"
) )
return section return section
def ref_display(ref):
try:
tag = run(f'git describe --tags --exact-match "{ref}" 2>/dev/null || true').strip()
# allow only semantic version tags: X.Y.Z
if re.fullmatch(r'\d+\.\d+\.\d+', tag):
return tag
except Exception:
pass
return ref[:8]
# ---------------------------------------------------------- # ----------------------------------------------------------
# markdown update # markdown update
# ---------------------------------------------------------- # ----------------------------------------------------------
@@ -216,7 +235,7 @@ def retrieve_tag(tag, file_path: Path):
fr"^{TAG_MARKER} {re.escape(tag)}$", fr"^{TAG_MARKER} {re.escape(tag)}$",
content, content,
re.MULTILINE | re.IGNORECASE, re.MULTILINE | re.IGNORECASE,
) )
if not match: if not match:
return "" return ""

View File

@@ -5,7 +5,6 @@ import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
import argparse import argparse
import textwrap
import sys import sys
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
@@ -108,13 +107,13 @@ def main():
gen_cmd = ( gen_cmd = (
f"python3 {script} " f"python3 {script} "
f"{args.last_successful_commit} {args.release_tag} " f"{args.last_successful_commit} {args.release_tag} "
f"--output-dir \"{notes_dir}\"" f'--output-dir "{notes_dir}"'
) )
else: else:
gen_cmd = ( gen_cmd = (
f"python3 {script} " f"python3 {script} "
f"{args.short_commit} {args.release_tag} " f"{args.short_commit} {args.release_tag} "
f"--output-dir \"{notes_dir}\"" f'--output-dir "{notes_dir}"'
) )
sh(gen_cmd, cwd=repo_root) sh(gen_cmd, cwd=repo_root)
@@ -140,15 +139,7 @@ def main():
formatted = now.strftime("%Y-%m-%dT%H:%M:%SZ") formatted = now.strftime("%Y-%m-%dT%H:%M:%SZ")
human = now.strftime("%c") human = now.strftime("%c")
localized_description = textwrap.dedent(f""" localized_description = getFormattedLocalizedDescription(args.marketing_version, args.short_commit, human, notes)
This is release for:
- version: "{args.marketing_version}"
- revision: "{args.short_commit}"
- timestamp: "{human}"
Release Notes:
{notes}
""").strip()
metadata = { metadata = {
"is_beta": bool(args.is_beta), "is_beta": bool(args.is_beta),
@@ -170,6 +161,16 @@ def main():
print(f"Wrote {out_file}") print(f"Wrote {out_file}")
def getFormattedLocalizedDescription(marketing_version, short_commit, human, notes):
return f"""
This is release for:
- version: "{marketing_version}"
- revision: "{short_commit}"
- timestamp: "{human}"
Release Notes:
{notes}
""".lstrip("\n")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -6,12 +6,13 @@ import datetime
from pathlib import Path from pathlib import Path
import time import time
import json import json
import inspect
import re import re
from posix import getcwd
# REPO ROOT relative to script dir # REPO ROOT relative to script dir
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
SCRIPTS = ROOT / 'scripts/ci' SCRIPTS = ROOT / 'scripts/ci'
BUILD_SETTINGS_OUTFILE = "project-build-settings.txt"
# ---------------------------------------------------------- # ----------------------------------------------------------
# helpers # helpers
@@ -57,106 +58,69 @@ def getenv(name, default=""):
def short_commit(): def short_commit():
return runAndGet("git rev-parse --short HEAD") return runAndGet("git rev-parse --short HEAD")
# ---------------------------------------------------------- def count_new_commits(last_commit):
# BUILD NUMBER RESERVATION if not last_commit or not last_commit.strip():
# ---------------------------------------------------------- return 0
def reserve_build_number(repo, max_attempts=5):
repo = Path(repo).resolve()
version_json = repo / "version.json"
def utc_now(): try:
return datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") total = int(runAndGet("git rev-list --count HEAD"))
if total == 1:
head = runAndGet("git rev-parse HEAD")
return 1 if head != last_commit else 0
def current_branch(): out = runAndGet(f"git rev-list --count {last_commit}..HEAD")
return runAndGet("git rev-parse --abbrev-ref HEAD", cwd=repo) return int(out)
except Exception:
def sync_with_remote(branch): return 0
run(f"git fetch --depth=1 origin {branch}", check=False, cwd=repo)
run(f"git reset --hard origin/{branch}", check=False, cwd=repo)
def read(branch):
defaults = {
"build": 0,
"issued_at": utc_now(),
"tag": branch,
}
if version_json.exists():
data = json.loads(version_json.read_text())
else:
data = {}
for k, v in defaults.items():
data.setdefault(k, v)
data["tag"] = branch
version_json.write_text(json.dumps(data, indent=2) + "\n")
return data
def write(data):
version_json.write_text(json.dumps(data, indent=2) + "\n")
for attempt in range(max_attempts):
branch = current_branch()
sync_with_remote(branch)
data = read(branch)
data["build"] += 1
data["issued_at"] = utc_now()
write(data)
run("git add version.json", check=False, cwd=repo)
run(
f"git commit -m '{branch} - build no: {data['build']}' || true",
check=False,
cwd=repo,
)
rc = subprocess.call(f"git push origin {branch}", shell=True, cwd=repo)
if rc == 0:
print(f"Reserved build #{data['build']}", file=sys.stderr)
return data["build"]
print("Push rejected, retrying...", file=sys.stderr)
time.sleep(2)
raise SystemExit("Failed reserving build number")
# ---------------------------------------------------------- # ----------------------------------------------------------
# PROJECT INFO # PROJECT INFO
# ---------------------------------------------------------- # ----------------------------------------------------------
def dump_project_settings(outdir=None):
outfile = Path(outdir).resolve() / BUILD_SETTINGS_OUTFILE if outdir else BUILD_SETTINGS_OUTFILE
run(f"xcodebuild -showBuildSettings 2>&1 > '{outfile}'")
def get_product_name(): def _extract_setting(cmd):
return runAndGet( out = runAndGet(cmd + " || true").strip() # prevent grep failure from aborting
"xcodebuild -showBuildSettings " return out if out else None
"| grep PRODUCT_NAME "
def _read_dumped_build_setting(name):
return _extract_setting(
f"cat '{BUILD_SETTINGS_OUTFILE}' "
f"| grep '{name} = ' "
"| tail -1 " "| tail -1 "
"| sed -e 's/.*= //g'" "| sed -e 's/.*= //g'"
) )
def get_bundle_id(): def query_build_setting(name):
return runAndGet( return _extract_setting(
"xcodebuild -showBuildSettings 2>&1 " f"xcodebuild -showBuildSettings 2>&1 "
"| grep 'PRODUCT_BUNDLE_IDENTIFIER = ' " f"| grep '{name} = ' "
"| tail -1 " "| tail -1 "
"| sed -e 's/.*= //g'" "| sed -e 's/.*= //g'"
) )
def get_product_name(): return query_build_setting("PRODUCT_NAME")
def get_bundle_id(): return query_build_setting("PRODUCT_BUNDLE_IDENTIFIER")
def read_product_name(): return _read_dumped_build_setting("PRODUCT_NAME")
def read_bundle_id(): return _read_dumped_build_setting("PRODUCT_BUNDLE_IDENTIFIER")
def get_marketing_version(): def get_marketing_version():
return runAndGet(f"grep MARKETING_VERSION {ROOT}/Build.xcconfig | sed -e 's/MARKETING_VERSION = //g'") return runAndGet(f"grep MARKETING_VERSION {ROOT}/Build.xcconfig | sed -e 's/MARKETING_VERSION = //g'")
def set_marketing_version(qualified): def set_marketing_version(version):
run( run(
f"sed -E -i '' " f"sed -E -i '' "
f"'s/^MARKETING_VERSION = .*/MARKETING_VERSION = {qualified}/' " f"'s/^MARKETING_VERSION = .*/MARKETING_VERSION = {version}/' "
f"{ROOT}/Build.xcconfig" f"{ROOT}/Build.xcconfig"
) )
def compute_qualified_version(marketing, build_num, channel, short):
date = datetime.datetime.now(datetime.UTC).strftime("%Y.%m.%d") def compute_normalized_version(marketing, build_num, short):
return f"{marketing}-{channel}.{date}.{build_num}+{short}" now = datetime.datetime.now(datetime.UTC)
date = now.strftime("%Y%m%d") # normalized date
base = marketing.strip()
return f"{base}-{date}.{build_num}+{short}"
# ---------------------------------------------------------- # ----------------------------------------------------------
# CLEAN # CLEAN
@@ -251,13 +215,13 @@ def tests_run(model):
# ---------------------------------------------------------- # ----------------------------------------------------------
def encrypt_logs(name): def encrypt_logs(name):
default_pwd = "12345" pwd = getenv("BUILD_LOG_ZIP_PASSWORD")
pwd = getenv("BUILD_LOG_ZIP_PASSWORD", default_pwd) cwd = getcwd()
if not pwd or not pwd.strip():
if pwd == default_pwd: print("BUILD_LOG_ZIP_PASSWORD not set — logs will be uploaded UNENCRYPTED", file=sys.stderr)
print("Warning: BUILD_LOG_ZIP_PASSWORD not set, using fallback password", file=sys.stderr) run(f'cd {cwd}/build/logs && zip -r {cwd}/{name}.zip *')
return
run(f'cd build/logs && zip -e -P "{pwd}" ../../{name}.zip *') run(f'cd {cwd}/build/logs && zip -e -P "{pwd}" {cwd}/{name}.zip *')
# ---------------------------------------------------------- # ----------------------------------------------------------
# RELEASE NOTES # RELEASE NOTES
@@ -281,22 +245,13 @@ def retrieve_release_notes(tag):
# ---------------------------------------------------------- # ----------------------------------------------------------
# DEPLOY SOURCE.JSON # DEPLOY SOURCE.JSON
# ---------------------------------------------------------- # ----------------------------------------------------------
def generate_metadata(release_tag, short_commit, marketing_version, channel, bundle_id, ipa_name, last_successful_commit=None):
def deploy(repo, source_json, release_tag, short_commit, marketing_version, channel, bundle_id, ipa_name, last_successful_commit=None):
repo = (ROOT / repo).resolve()
ipa_path = ROOT / ipa_name ipa_path = ROOT / ipa_name
source_json_path = repo / source_json
metadata = 'source-metadata.json' metadata = 'source-metadata.json'
if not repo.exists():
raise SystemExit(f"{repo} repo missing")
if not ipa_path.exists(): if not ipa_path.exists():
raise SystemExit(f"{ipa_path} missing") raise SystemExit(f"{ipa_path} missing")
if not source_json_path.exists():
raise SystemExit(f"{source_json} missing inside repo")
cmd = ( cmd = (
f"python3 {SCRIPTS}/generate_source_metadata.py " f"python3 {SCRIPTS}/generate_source_metadata.py "
f"--repo-root {ROOT} " f"--repo-root {ROOT} "
@@ -311,12 +266,26 @@ def deploy(repo, source_json, release_tag, short_commit, marketing_version, chan
f"--bundle-id {bundle_id}" f"--bundle-id {bundle_id}"
) )
# pass only if provided
if last_successful_commit: if last_successful_commit:
cmd += f" --last-successful-commit {last_successful_commit}" cmd += f" --last-successful-commit {last_successful_commit}"
run(cmd) run(cmd)
def deploy(repo, source_json, release_tag, marketing_version):
repo = (ROOT / repo).resolve()
source_json_path = repo / source_json
metadata = 'source-metadata.json'
if not repo.exists():
raise SystemExit(f"{repo} repo missing")
if not (repo / ".git").exists():
print("Repo is not a git repository, skipping deploy", file=sys.stderr)
return
if not source_json_path.exists():
raise SystemExit(f"{source_json} missing inside repo")
run("git config user.name 'GitHub Actions'", check=False) run("git config user.name 'GitHub Actions'", check=False)
run("git config user.email 'github-actions@github.com'", check=False) run("git config user.email 'github-actions@github.com'", check=False)
@@ -348,28 +317,49 @@ def deploy(repo, source_json, release_tag, short_commit, marketing_version, chan
else: else:
raise SystemExit("Deploy push failed after retries") raise SystemExit("Deploy push failed after retries")
def last_successful_commit(workflow, branch):
import json def last_successful_commit(is_stable, tag=None):
is_stable = str(is_stable).lower() in ("1", "true", "yes")
try: try:
out = runAndGet( if is_stable:
f'gh run list ' prev_tag = runAndGet(
f'--workflow "{workflow}" ' r'git tag --sort=-v:refname '
f'--json headSha,conclusion,headBranch' r'| grep -E "^[0-9]+\.[0-9]+\.[0-9]+$" '
) r'| sed -n "2p" || true'
).strip()
runs = json.loads(out) if prev_tag:
return runAndGet(f'git rev-parse "{prev_tag}^{{commit}}"')
for r in runs: return None # ← changed
if r.get("conclusion") == "success" and r.get("headBranch") == branch:
return r["headSha"] if tag:
exists = subprocess.call(
f'git rev-parse -q --verify "refs/tags/{tag}"',
shell=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
) == 0
if exists:
return runAndGet(f'git rev-parse "{tag}^{{commit}}"')
except Exception: except Exception:
pass pass
return None return None
def upload_release(release_name, release_tag, commit_sha, repo, upstream_recommendation): def upload_release(release_name, release_tag, commit_sha, repo, upstream_tag_recommended, is_stable=False):
is_stable = str(is_stable).lower() in ("1", "true", "yes")
draft = False
prerelease = True
latest = False
if is_stable:
prerelease = False
latest = True
token = getenv("GH_TOKEN") token = getenv("GH_TOKEN")
if token: if token:
os.environ["GH_TOKEN"] = token os.environ["GH_TOKEN"] = token
@@ -382,7 +372,6 @@ def upload_release(release_name, release_tag, commit_sha, repo, upstream_recomme
meta = json.loads(metadata_path.read_text()) meta = json.loads(metadata_path.read_text())
marketing_version = meta.get("version_ipa") marketing_version = meta.get("version_ipa")
is_beta = bool(meta.get("is_beta"))
build_datetime = meta.get("version_date") build_datetime = meta.get("version_date")
dt = datetime.datetime.fromisoformat( dt = datetime.datetime.fromisoformat(
@@ -396,52 +385,92 @@ def upload_release(release_name, release_tag, commit_sha, repo, upstream_recomme
f"--retrieve {release_tag} " f"--retrieve {release_tag} "
f"--output-dir {ROOT}" f"--output-dir {ROOT}"
) )
# normalize section header
release_notes = re.sub( if is_stable:
r'^\s*#{1,6}\s*what(?:\'?s|\s+is)?\s+(?:new|changed).*', release_notes = re.sub(
"## What's Changed", r'(?im)^[ \t]*#{1,6}[ \t]*what[\']?s[ \t]+changed[ \t]*$',
release_notes, "## What's Changed",
flags=re.IGNORECASE | re.MULTILINE, release_notes,
) flags=re.IGNORECASE | re.MULTILINE,
)
upstream_block = "" upstream_block = ""
if upstream_recommendation and upstream_recommendation.strip(): if upstream_tag_recommended and upstream_tag_recommended.strip():
upstream_block = upstream_recommendation.strip() + "\n\n" tag = upstream_tag_recommended.strip()
upstream_block = (
f"If you want to try out new features early but want a lower chance of bugs, "
f"you can look at [{repo} {tag}]"
f"(https://github.com/{repo}/releases?q={tag}).\n\n"
)
raw_body = f""" header = getFormattedUploadMsg(
This is an ⚠️ **EXPERIMENTAL** ⚠️ {release_name} build for commit [{commit_sha}](https://github.com/{repo}/commit/{commit_sha}). release_name, commit_sha, repo, upstream_block,
built_time, built_date, marketing_version, is_stable,
)
{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!** body = header + release_notes.lstrip() + "\n"
{upstream_block}## Build Info
Built at (UTC): `{built_time}`
Built at (UTC date): `{built_date}`
Commit SHA: `{commit_sha}`
Version: `{marketing_version}`
"""
header = inspect.cleandoc(raw_body)
body = header + "\n\n" + release_notes.lstrip() + "\n"
body_file = ROOT / "release_body.md" body_file = ROOT / "release_body.md"
body_file.write_text(body, encoding="utf-8") body_file.write_text(body, encoding="utf-8")
prerelease_flag = "--prerelease" if is_beta else "" draft_flag = "--draft" if draft else ""
prerelease_flag = "--prerelease" if prerelease else ""
latest_flag = "--latest=true" if latest else ""
run( # create release if it doesn't exist
f'gh release edit "{release_tag}" ' exists = subprocess.call(
f'--title "{release_name}" ' f'gh release view "{release_tag}"',
f'--notes-file "{body_file}" ' shell=True,
f'{prerelease_flag}' cwd=ROOT,
) stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
) == 0
if exists:
run(
f'gh release edit "{release_tag}" '
f'--title "{release_name}" '
f'--notes-file "{body_file}" '
f'{draft_flag} {prerelease_flag} {latest_flag}'
)
else:
run(
f'gh release create "{release_tag}" '
f'--title "{release_name}" '
f'--notes-file "{body_file}" '
f'{draft_flag} {prerelease_flag} {latest_flag}'
)
run( run(
f'gh release upload "{release_tag}" ' f'gh release upload "{release_tag}" '
f'SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip ' f'SideStore.ipa SideStore.dSYMs.zip build-logs.zip '
f'--clobber' f'--clobber'
) )
run(f'git tag -f "{release_tag}" "{commit_sha}"')
run(f'git push origin "refs/tags/{release_tag}" --force')
def getFormattedUploadMsg(release_name, commit_sha, repo, upstream_block, built_time, built_date, marketing_version, is_stable):
experimental_header = ""
if not is_stable:
experimental_header = f"""
This is an ⚠️ **EXPERIMENTAL** ⚠️ {release_name} build for commit [{commit_sha}](https://github.com/{repo}/commit/{commit_sha}).
{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!**
""".lstrip("\n")
header = f"""
{experimental_header}{upstream_block}## Build Info
Built at (UTC): `{built_time}`
Built at (UTC date): `{built_date}`
Commit SHA: `{commit_sha}`
Version: `{marketing_version}`
""".lstrip("\n")
return header
# ---------------------------------------------------------- # ----------------------------------------------------------
# ENTRYPOINT # ENTRYPOINT
# ---------------------------------------------------------- # ----------------------------------------------------------
@@ -450,17 +479,20 @@ COMMANDS = {
# ---------------------------------------------------------- # ----------------------------------------------------------
# SHARED # SHARED
# ---------------------------------------------------------- # ----------------------------------------------------------
"commid-id" : (short_commit, 0, ""), "commit-id" : (short_commit, 0, ""),
"count-new-commits" : (count_new_commits, 1, "<last_successful_commit>"),
# ---------------------------------------------------------- # ----------------------------------------------------------
# PROJECT INFO # PROJECT INFO
# ---------------------------------------------------------- # ----------------------------------------------------------
"get-marketing-version" : (get_marketing_version, 0, ""), "get-marketing-version" : (get_marketing_version, 0, ""),
"set-marketing-version" : (set_marketing_version, 1, "<qualified_version>"), "set-marketing-version" : (set_marketing_version, 1, "<normalized_version>"),
"compute-qualified" : (compute_qualified_version, 4, "<marketing> <build_num> <channel> <short_commit>"), "compute-normalized" : (compute_normalized_version,3, "<marketing> <build_num> <short_commit>"),
"reserve_build_number" : (reserve_build_number, 1, "<repo>"),
"get-product-name" : (get_product_name, 0, ""), "get-product-name" : (get_product_name, 0, ""),
"get-bundle-id" : (get_bundle_id, 0, ""), "get-bundle-id" : (get_bundle_id, 0, ""),
"dump-project-settings" : (dump_project_settings, 0, ""),
"read-product-name" : (read_product_name, 0, ""),
"read-bundle-id" : (read_bundle_id, 0, ""),
# ---------------------------------------------------------- # ----------------------------------------------------------
# CLEAN # CLEAN
@@ -485,20 +517,22 @@ COMMANDS = {
# ---------------------------------------------------------- # ----------------------------------------------------------
# LOG ENCRYPTION # LOG ENCRYPTION
# ---------------------------------------------------------- # ----------------------------------------------------------
"encrypt-build" : (lambda: encrypt_logs("encrypted-build-logs"), 0, ""), "encrypt-build" : (lambda: encrypt_logs("build-logs"), 0, ""),
"encrypt-tests-build" : (lambda: encrypt_logs("encrypted-tests-build-logs"), 0, ""), "encrypt-tests-build" : (lambda: encrypt_logs("tests-build-logs"), 0, ""),
"encrypt-tests-run" : (lambda: encrypt_logs("encrypted-tests-run-logs"), 0, ""), "encrypt-tests-run" : (lambda: encrypt_logs("tests-run-logs"), 0, ""),
# ---------------------------------------------------------- # ----------------------------------------------------------
# RELEASE / DEPLOY # RELEASE / DEPLOY
# ---------------------------------------------------------- # ----------------------------------------------------------
"last-successful-commit" : (last_successful_commit, 2, "<workflow_name> <branch>"), "last-successful-commit" : (last_successful_commit, 1, "<is_stable> [tag]"),
"release-notes" : (release_notes, 1, "<tag>"), "release-notes" : (release_notes, 1, "<tag>"),
"retrieve-release-notes" : (retrieve_release_notes, 1, "<tag>"), "retrieve-release-notes" : (retrieve_release_notes, 1, "<tag>"),
"deploy" : (deploy, 9, "generate-metadata" : (generate_metadata, 7,
"<repo> <source_json> <release_tag> <short_commit> <marketing_version> <channel> <bundle_id> <ipa_name> [last_successful_commit]"), "<release_tag> <short_commit> <marketing_version> <channel> <bundle_id> <ipa_name> [last_successful_commit]"),
"upload-release" : (upload_release, 5, "<release_name> <release_tag> <commit_sha> <repo> <upstream_recommendation>"), "deploy" : (deploy, 4,
} "<repo> <source_json> <release_tag> <marketing_version>"),
"upload-release" : (upload_release, 5,
"<release_name> <release_tag> <commit_sha> <repo> <upstream_tag_recommended> [is_stable]"),}
def main(): def main():
def usage(): def usage():
@@ -524,9 +558,9 @@ def main():
suffix = f" {arg_usage}" if arg_usage else "" suffix = f" {arg_usage}" if arg_usage else ""
raise SystemExit(f"Usage: workflow.py {cmd}{suffix}") raise SystemExit(f"Usage: workflow.py {cmd}{suffix}")
args = sys.argv[2:2 + argc] args = sys.argv[2:]
result = func(*args) if argc else func() result = func(*args) if args else func()
# ONLY real outputs go to stdout # ONLY real outputs go to stdout
if result is not None: if result is not None:

View File

@@ -1,11 +0,0 @@
{
"is_beta": false,
"bundle_identifier": "com.SideStore.SideStore",
"version_ipa": "0.6.3",
"version_date": "2026-02-23T23:38:22Z",
"release_channel": "nightly",
"size": 29313346,
"sha256": "51ec327bca0b0056ccd4c2eb1a130cb7c5bb21de2f303251eea3e0a7336699c4",
"download_url": "https://github.com/SideStore/SideStore/releases/download/nightly/SideStore.ipa",
"localized_description": "This is release for:\n - version: \"0.6.3-nightly.2026.02.24.42+abc123de\"\n - revision: \"99712f00\"\n - timestamp: \"Mon Feb 23 23:38:22 2026\"\n\nRelease Notes:\n#### What's Changed\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- altsign updated to latest\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- re added openSSL from new path\n- updated altsign to use xcframework for openSSL which was causing huge download of 1.2 GB each time\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: improve more ci worflow\n- CI: full rewrite - moved logic into ci.py and kept workflow scripts mostly dummy\n\n#### Full Changelog: [38715283...99712f00](https://github.com/SideStore/SideStore/compare/38715283073ea37949a462b889ce3cad403ea499...99712f0020a4f2ae57d8d781514fa735f893c23a)"
}

View File

@@ -75,6 +75,10 @@
{ {
"identifier": "thatstel.la.altsource", "identifier": "thatstel.la.altsource",
"sourceURL": "https://alt.thatstel.la/" "sourceURL": "https://alt.thatstel.la/"
},
{
"identifier": "com.deliacheminot.mona",
"sourceURL": "https://raw.githubusercontent.com/delia-cheminot/mona-hrt/refs/heads/main/ios_source.json"
} }
], ],
"sources": [ "sources": [
@@ -148,6 +152,10 @@
{ {
"identifier": "thatstel.la.altsource", "identifier": "thatstel.la.altsource",
"sourceURL": "https://alt.thatstel.la/" "sourceURL": "https://alt.thatstel.la/"
},
{
"identifier": "com.deliacheminot.mona",
"sourceURL": "https://raw.githubusercontent.com/delia-cheminot/mona-hrt/refs/heads/main/ios_source.json"
} }
] ]
} }