Compare commits
140 Commits
xcode-26-b
...
v1.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd335601ce | ||
|
|
79210e94d2 | ||
|
|
adaf7a2560 | ||
|
|
6b42e82e3e | ||
|
|
5d9f03114c | ||
|
|
34610d57bf | ||
|
|
e751876159 | ||
|
|
6fa21b7206 | ||
|
|
82701956d9 | ||
|
|
7970258d50 | ||
|
|
8677966f96 | ||
|
|
77e35a1bc8 | ||
|
|
f5d8589ded | ||
|
|
186abfd4bc | ||
|
|
98ffb974ff | ||
|
|
bb90a5709d | ||
|
|
1769b2152a | ||
|
|
696745df4d | ||
|
|
e93457de2f | ||
|
|
28d7fd0f6c | ||
|
|
913db5131b | ||
|
|
c6476b6b4a | ||
|
|
7c1b951098 | ||
|
|
94a8d79a5f | ||
|
|
46341773e6 | ||
|
|
053cc9feda | ||
|
|
3adfc9db6d | ||
|
|
eeea64f780 | ||
|
|
90b4fa7605 | ||
|
|
57299ebb16 | ||
|
|
038784b421 | ||
|
|
003312a6f3 | ||
|
|
4549f86f0c | ||
|
|
957a53ecd6 | ||
|
|
4f10984482 | ||
|
|
4c7c6bc905 | ||
|
|
16e205b9c6 | ||
|
|
ec54468783 | ||
|
|
b6bdd13167 | ||
|
|
57d248adfe | ||
|
|
b67dfbf0c3 | ||
|
|
a9bb0c11a4 | ||
|
|
39cc0b5da5 | ||
|
|
503d535823 | ||
|
|
5efb9f8448 | ||
|
|
28344232d3 | ||
|
|
df8debb386 | ||
|
|
af5281dc90 | ||
|
|
2f7bb83560 | ||
|
|
d85957b70b | ||
|
|
5e833192de | ||
|
|
794385a935 | ||
|
|
ab2aef6b2a | ||
|
|
08dcd9d146 | ||
|
|
8d8d8fdd61 | ||
|
|
12f521321f | ||
|
|
b850a3192a | ||
|
|
3df1a12018 | ||
|
|
0e29d2bc22 | ||
|
|
e6cbd21d12 | ||
|
|
223495cdb9 | ||
|
|
b89462016f | ||
|
|
a105c7f9b4 | ||
|
|
444e50eae8 | ||
|
|
f0a022c9ec | ||
|
|
ca9c0596f8 | ||
|
|
4278e7db2b | ||
|
|
d5f4f4c625 | ||
|
|
96dec5329d | ||
|
|
579885acd6 | ||
|
|
61bcb31709 | ||
|
|
40af2fe313 | ||
|
|
dbd71050ac | ||
|
|
2ec38aa579 | ||
|
|
cf84a23dc7 | ||
|
|
16a7dea55b | ||
|
|
60b7276ac4 | ||
|
|
1444afa2b5 | ||
|
|
bc6b0cb722 | ||
|
|
bd84f65fb4 | ||
|
|
657dcbe3c0 | ||
|
|
152c12b380 | ||
|
|
1993ece7f0 | ||
|
|
e8b0fa3c6d | ||
|
|
1cfd83bbb7 | ||
|
|
3574ed2ed9 | ||
|
|
a03a5ac76c | ||
|
|
5b20ce13bc | ||
|
|
4fc4d6ba39 | ||
|
|
ac8fb8851f | ||
|
|
b181b2e7a4 | ||
|
|
701065b073 | ||
|
|
8609209821 | ||
|
|
7ea1ad5af0 | ||
|
|
324ec7907a | ||
|
|
cf9d577050 | ||
|
|
9f91db9642 | ||
|
|
f445467598 | ||
|
|
266ac1ee50 | ||
|
|
a4078159ac | ||
|
|
020fb74a1c | ||
|
|
103fe10123 | ||
|
|
7d48f5a7f3 | ||
|
|
1ce7681a31 | ||
|
|
d8a7f8cb54 | ||
|
|
ce68ea74ad | ||
|
|
fede5b29cb | ||
|
|
4ef94d22e4 | ||
|
|
41db3e42ce | ||
|
|
915dd83015 | ||
|
|
42d433a11d | ||
|
|
17f28439fe | ||
|
|
3b38d725d7 | ||
|
|
0c4fe98370 | ||
|
|
e2df5a46e6 | ||
|
|
7aed564d73 | ||
|
|
29382b4170 | ||
|
|
61086e8b1d | ||
|
|
6cfbe1e0e4 | ||
|
|
8896cd835c | ||
|
|
c8f3c6515d | ||
|
|
81b079650c | ||
|
|
b2ad3e7f4c | ||
|
|
1d598708c3 | ||
|
|
d5beb0758b | ||
|
|
074d4295f3 | ||
|
|
79bb74700a | ||
|
|
6eeb0ca19f | ||
|
|
ddcc7eee61 | ||
|
|
e80a49e705 | ||
|
|
42824744e5 | ||
|
|
b90000ecf3 | ||
|
|
bcea3359ef | ||
|
|
46cd2be37d | ||
|
|
33674397b1 | ||
|
|
b8e136005a | ||
|
|
6ec47bbe0c | ||
|
|
6d081c2bbb | ||
|
|
00439fe292 | ||
|
|
c232ef5044 |
@@ -1,39 +0,0 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,py}]
|
||||
charset = utf-8# 4 space indentation
|
||||
|
||||
# Swift files
|
||||
[*.swift]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8# 4 space indentation
|
||||
|
||||
# 4 space indentation
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Indentation override for all JS under lib directory
|
||||
[lib/**.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
1007
.github/.obsolete/reusable-build-workflow.yml
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
* @JoeMatt @lonkelle @nythepegasus @Spidy123222 @SternXD
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Report a bug
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Please note that the issue tracker is not for support
|
||||
Thanks for taking the time to fill out this bug report! Before you continue filling out the report, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the bug you are experiencing** in case it has already been reported.
|
||||
|
||||
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: What is the bug and how did you discover it?
|
||||
placeholder: Please be clear and concise with your description.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: how-to-reproduce
|
||||
attributes:
|
||||
label: Instructions to reproduce
|
||||
description: Please include clear and consistent instructions for reproducing the bug to make it easier for us to fix it.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: What version of SideStore are you using?
|
||||
description: To retrieve this, go to `Settings` in the SideStore app and scroll down to the bottom.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other-info
|
||||
attributes:
|
||||
label: Other info
|
||||
description: If you have any other comments, other info that might be useful, or if you found a workaround, please put it here.
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,10 +0,0 @@
|
||||
# force issue template usage
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: Discord
|
||||
url: https://discord.gg/sidestore-949183273383395328
|
||||
about: If you need support, please go here first instead of making an issue!
|
||||
- name: GitHub Discussions
|
||||
url: https://github.com/SideStore/SideStore/discussions
|
||||
about: As an alternative to Discord, you can also make a new GitHub discussion.
|
||||
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Feature Request
|
||||
description: Suggest a feature
|
||||
title: "[FEATURE REQUEST] "
|
||||
labels: ["enhancement"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request! Before you continue filling out the form, please **[search in GitHub Issues](https://github.com/SideStore/SideStore/issues?q=is%3Aissue+is%3Aopen) for the feature you are suggestion** in case it has already been suggested.
|
||||
|
||||
**Please use [Discord](https://discord.gg/sidestore-949183273383395328) or [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) for support.**
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: What is the feature? How would it work?
|
||||
placeholder: Please be clear and concise with your description.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: use-cases
|
||||
attributes:
|
||||
label: Use cases
|
||||
description: Please include multiple use cases where this feature would be useful.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: If you have alternative ideas of how this feature could work, you can put them here.
|
||||
63
.github/maintenance/cache.py
vendored
@@ -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
|
||||
'''
|
||||
12
.github/pull_request_template.md
vendored
@@ -1,12 +0,0 @@
|
||||
### Changes
|
||||
|
||||
<!-- Fill this list with what your PR changes. Example: -->
|
||||
- Fix bug
|
||||
- Change UI for QOL
|
||||
|
||||
<!-- If your PR is ready to be merged, you can remove this section. -->
|
||||
### Todo before merge
|
||||
|
||||
<!-- Example: -->
|
||||
- [x] Finish UI changes
|
||||
- [ ] Test
|
||||
28
.github/workflows/alpha.yml
vendored
@@ -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 }}
|
||||
77
.github/workflows/attach_build_products.yml
vendored
@@ -1,77 +0,0 @@
|
||||
name: Add artifact links to pull request and related issues
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Pull Request SideStore build]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
artifacts-url-comments:
|
||||
name: add artifact links to pull request and related issues job
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: add artifact links to pull request and related issues step
|
||||
uses: tonyhallett/artifacts-url-comments@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
prefix: Builds for this Pull Request are available at
|
||||
suffix: Have a nice day.
|
||||
format: name
|
||||
addTo: pull
|
||||
# addTo: pullandissues
|
||||
nightly-link-comment:
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
# This snippet is public-domain, taken from
|
||||
# https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml
|
||||
script: |
|
||||
async function upsertComment(owner, repo, issue_number, purpose, body) {
|
||||
const {data: comments} = await github.rest.issues.listComments(
|
||||
{owner, repo, issue_number});
|
||||
|
||||
const marker = `<!-- bot: ${purpose} -->`;
|
||||
body = marker + "\n" + body;
|
||||
|
||||
const existing = comments.filter((c) => c.body.includes(marker));
|
||||
if (existing.length > 0) {
|
||||
const last = existing[existing.length - 1];
|
||||
core.info(`Updating comment ${last.id}`);
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo,
|
||||
body,
|
||||
comment_id: last.id,
|
||||
});
|
||||
} else {
|
||||
core.info(`Creating a comment in issue / PR #${issue_number}`);
|
||||
await github.rest.issues.createComment({issue_number, body, owner, repo});
|
||||
}
|
||||
}
|
||||
|
||||
const {owner, repo} = context.repo;
|
||||
const run_id = ${{github.event.workflow_run.id}};
|
||||
|
||||
const pull_requests = ${{ toJSON(github.event.workflow_run.pull_requests) }};
|
||||
if (!pull_requests.length) {
|
||||
return core.error("This workflow doesn't match any pull requests!");
|
||||
}
|
||||
|
||||
const artifacts = await github.paginate(
|
||||
github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id});
|
||||
if (!artifacts.length) {
|
||||
return core.error(`No artifacts found`);
|
||||
}
|
||||
let body = `Download the artifacts for this pull request (nightly.link):\n`;
|
||||
for (const art of artifacts) {
|
||||
body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
|
||||
}
|
||||
|
||||
core.info("Review thread message body:", body);
|
||||
|
||||
for (const pr of pull_requests) {
|
||||
await upsertComment(owner, repo, pr.number,
|
||||
"nightly-link", body);
|
||||
}
|
||||
103
.github/workflows/beta.yml
vendored
@@ -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/
|
||||
34
.github/workflows/increase-beta-build-num.sh
vendored
@@ -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
|
||||
82
.github/workflows/nightly.yml
vendored
@@ -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 }}
|
||||
|
||||
143
.github/workflows/pr.yml
vendored
@@ -1,143 +0,0 @@
|
||||
name: Pull Request SideStore build
|
||||
on:
|
||||
pull_request:
|
||||
# types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload SideStore
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '26.0-beta'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install ldid
|
||||
|
||||
- name: Install xcbeautify
|
||||
run: brew install xcbeautify
|
||||
|
||||
- name: Add PR suffix to version
|
||||
run: sed -e "/MARKETING_VERSION = .*/s/\$/-pr.${{ github.event.pull_request.number }}+$(git rev-parse --short ${COMMIT:-HEAD})/" -i '' Build.xcconfig
|
||||
env:
|
||||
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- 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.6.0
|
||||
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-
|
||||
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
|
||||
swiftpm-cache-restore-keys: |
|
||||
xcode-cache-sourcedata-
|
||||
|
||||
|
||||
- name: Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-${{ hashFiles('Podfile') }}
|
||||
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
||||
# pods-cache-
|
||||
|
||||
- name: Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-
|
||||
|
||||
- name: Install CocoaPods
|
||||
# if: ${{ steps.pods-restore.outputs.cache-hit != 'true'}}
|
||||
id: pods-install
|
||||
run: |
|
||||
pod install
|
||||
|
||||
- name: Save Pods to Cache
|
||||
id: save-pods
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: List Files and derived data
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
|
||||
- name: Build SideStore
|
||||
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:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYM
|
||||
path: ./SideStore.xcarchive/dSYMs/*
|
||||
104
.github/workflows/reusable-sidestore-build.yml
vendored
@@ -1,104 +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
|
||||
if: ${{ always() && (needs.tests-run.result == 'skipped' || needs.tests-run.result == 'success') }}
|
||||
uses: ./.github/workflows/sidestore-deploy.yml
|
||||
with:
|
||||
is_beta: ${{ inputs.is_beta }}
|
||||
publish: ${{ inputs.publish }}
|
||||
release_name: ${{ inputs.release_name }}
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
upstream_tag: ${{ inputs.upstream_tag }}
|
||||
upstream_name: ${{ inputs.upstream_name }}
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
short_commit: ${{ needs.shared.outputs.short-commit }}
|
||||
release_channel: ${{ needs.build.outputs.release-channel }}
|
||||
marketing_version: ${{ needs.build.outputs.marketing-version }}
|
||||
bundle_id: ${{ inputs.bundle_id }}
|
||||
secrets: inherit
|
||||
401
.github/workflows/sidestore-build.yml
vendored
@@ -1,401 +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-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
marketing-version: ${{ steps.marketing-version.outputs.MARKETING_VERSION }}
|
||||
release-channel: ${{ steps.release-channel.outputs.RELEASE_CHANNEL }}
|
||||
|
||||
steps:
|
||||
- name: Set beta status
|
||||
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies - ldid & xcbeautify
|
||||
run: |
|
||||
brew install ldid xcbeautify
|
||||
|
||||
- name: Set ref based on is_shared_build_num
|
||||
if: ${{ inputs.is_beta }}
|
||||
id: set_ref
|
||||
run: |
|
||||
if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then
|
||||
echo "ref=main" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Checkout SideStore/beta-build-num repo
|
||||
if: ${{ inputs.is_beta }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'SideStore/beta-build-num'
|
||||
ref: ${{ env.ref }}
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/beta-build-num'
|
||||
|
||||
- name: Copy build_number.txt to repo root
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
cp SideStore/beta-build-num/build_number.txt .
|
||||
echo "cat build_number.txt"
|
||||
cat build_number.txt
|
||||
shell: bash
|
||||
|
||||
- name: Echo Build.xcconfig
|
||||
run: |
|
||||
echo "cat Build.xcconfig"
|
||||
cat Build.xcconfig
|
||||
shell: bash
|
||||
|
||||
- name: Set Release Channel info for build number bumper
|
||||
id: release-channel
|
||||
run: |
|
||||
RELEASE_CHANNEL="${{ inputs.release_tag }}"
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_OUTPUT
|
||||
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
|
||||
shell: bash
|
||||
|
||||
- name: Increase build number for beta builds
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
bash .github/workflows/increase-beta-build-num.sh
|
||||
shell: bash
|
||||
|
||||
- name: Extract MARKETING_VERSION from Build.xcconfig
|
||||
id: version
|
||||
run: |
|
||||
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
echo "version=$version"
|
||||
shell: bash
|
||||
|
||||
- name: Set MARKETING_VERSION
|
||||
if: ${{ inputs.is_beta }}
|
||||
id: marketing-version
|
||||
run: |
|
||||
# Extract version number (e.g., "0.6.0")
|
||||
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||
# Extract date (YYYYMMDD) (e.g., "20250205")
|
||||
date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/')
|
||||
# Extract build number (e.g., "2")
|
||||
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
|
||||
|
||||
# Combine them into the final output
|
||||
MARKETING_VERSION="${version}-${date}.${build_num}+${{ inputs.short_commit }}"
|
||||
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "MARKETING_VERSION=$MARKETING_VERSION"
|
||||
shell: bash
|
||||
|
||||
- name: Echo Updated Build.xcconfig, build_number.txt
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
cat build_number.txt
|
||||
shell: bash
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||
id: xcode-cache-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-${{ github.ref_name }}-
|
||||
|
||||
# - name: (Build) Cache Build
|
||||
# uses: irgaly/xcode-cache@v1.8.1
|
||||
# with:
|
||||
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
|
||||
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
# swiftpm-cache-restore-keys: |
|
||||
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
||||
# pods-cache-
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-${{ github.ref_name }}-
|
||||
|
||||
|
||||
- name: (Build) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Pods to Cache
|
||||
id: save-pods
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Build) Clean previous build artifacts
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Build) List Files and derived data
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
- name: Set BundleID Suffix for Sidestore build
|
||||
run: |
|
||||
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
|
||||
- name: Build SideStore.xcarchive
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: Convert to IPA
|
||||
run: make ipa | tee -a build/logs/build.log
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Xcode & SwiftPM Cache
|
||||
id: cache-save
|
||||
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Build) List Files and Build artifacts
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
|
||||
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Encrypt build-logs for upload
|
||||
id: encrypt-build-log
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
shell: bash
|
||||
|
||||
- name: Upload encrypted-build-logs.zip
|
||||
id: attach-encrypted-build-log
|
||||
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
|
||||
path: encrypted-build-logs.zip
|
||||
|
||||
- name: Upload SideStore.ipa Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}.ipa
|
||||
path: SideStore.ipa
|
||||
|
||||
- name: Zip dSYMs
|
||||
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
|
||||
shell: bash
|
||||
|
||||
- name: Upload *.dSYM Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
||||
path: SideStore.dSYMs.zip
|
||||
|
||||
- name: Keep rolling the build numbers for each successful build
|
||||
if: ${{ inputs.is_beta }}
|
||||
run: |
|
||||
pushd SideStore/beta-build-num/
|
||||
|
||||
echo "Configure Git user (committer details)"
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
echo "Adding files to commit"
|
||||
git add --verbose build_number.txt
|
||||
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
|
||||
|
||||
echo "Pushing to remote repo"
|
||||
git push --verbose
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
- name: Get last successful commit
|
||||
id: get_last_commit
|
||||
run: |
|
||||
# Try to get the last successful workflow run commit
|
||||
LAST_SUCCESS_SHA=$(gh run list --branch "${{ github.ref_name }}" --status success --json headSha --jq '.[0].headSha')
|
||||
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_OUTPUT
|
||||
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_ENV
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
|
||||
- name: Create release notes
|
||||
run: |
|
||||
LAST_SUCCESS_SHA=${{ steps.get_last_commit.outputs.LAST_SUCCESS_SHA}}
|
||||
echo "Last successful commit SHA: $LAST_SUCCESS_SHA"
|
||||
|
||||
FROM_COMMIT=$LAST_SUCCESS_SHA
|
||||
# Check if we got a valid SHA
|
||||
if [ -z "$LAST_SUCCESS_SHA" ] || [ "$LAST_SUCCESS_SHA" = "null" ]; then
|
||||
echo "No successful run found, using initial commit of branch"
|
||||
# Get the first commit of the branch (initial commit)
|
||||
FROM_COMMIT=$(git rev-list --max-parents=0 HEAD)
|
||||
fi
|
||||
|
||||
python3 update_release_notes.py $FROM_COMMIT ${{ inputs.release_tag }} ${{ github.ref_name }}
|
||||
# cat release-notes.md
|
||||
shell: bash
|
||||
|
||||
- name: Upload release-notes.md
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-notes-${{ inputs.short_commit }}.md
|
||||
path: release-notes.md
|
||||
|
||||
- name: Upload update_release_notes.py
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: update_release_notes-${{ inputs.short_commit }}.py
|
||||
path: update_release_notes.py
|
||||
|
||||
- name: Upload update_apps.py
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: update_apps-${{ inputs.short_commit }}.py
|
||||
path: update_apps.py
|
||||
235
.github/workflows/sidestore-deploy.yml
vendored
@@ -1,235 +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: Upload to releases
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release: ${{ inputs.release_name }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
prerelease: ${{ inputs.is_beta }}
|
||||
files: SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip encrypted-tests-build-logs.zip encrypted-tests-run-logs.zip test-results.zip tests-recording.mp4
|
||||
body: |
|
||||
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
|
||||
|
||||
${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!**
|
||||
|
||||
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }}).
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ inputs.version }}`
|
||||
|
||||
${{ steps.release_notes.outputs.content }}
|
||||
|
||||
- name: Get formatted date
|
||||
run: |
|
||||
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
echo "Formatted date: $FORMATTED_DATE"
|
||||
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Get size of IPA in bytes (macOS/Linux)
|
||||
run: |
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
# macOS
|
||||
IPA_SIZE=$(stat -f %z SideStore.ipa)
|
||||
else
|
||||
# Linux
|
||||
IPA_SIZE=$(stat -c %s SideStore.ipa)
|
||||
fi
|
||||
echo "IPA size in bytes: $IPA_SIZE"
|
||||
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Compute SHA-256 of IPA
|
||||
run: |
|
||||
SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }')
|
||||
echo "SHA-256 Hash: $SHA256_HASH"
|
||||
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Set Release Info variables
|
||||
run: |
|
||||
# Format localized description
|
||||
LOCALIZED_DESCRIPTION=$(cat <<EOF
|
||||
This is release for:
|
||||
- version: "${{ inputs.version }}"
|
||||
- revision: "${{ inputs.short_commit }}"
|
||||
- timestamp: "${{ steps.date.outputs.date }}"
|
||||
|
||||
Release Notes:
|
||||
${{ steps.release_notes.outputs.content }}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
|
||||
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
|
||||
echo "VERSION_IPA=${{ inputs.marketing_version }}" >> $GITHUB_ENV
|
||||
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=${{ inputs.release_channel }}" >> $GITHUB_ENV
|
||||
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
|
||||
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
|
||||
|
||||
# multiline strings
|
||||
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
|
||||
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Check if Publish updates is set
|
||||
id: check_publish
|
||||
run: |
|
||||
echo "Publish updates to source.json = ${{ inputs.publish }}"
|
||||
shell: bash
|
||||
|
||||
- name: Checkout SideStore/apps-v2.json
|
||||
if: ${{ inputs.is_beta && inputs.publish }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'SideStore/apps-v2.json'
|
||||
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
|
||||
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
|
||||
path: 'SideStore/apps-v2.json'
|
||||
|
||||
# for stable builds, let the user manually edit the source.json
|
||||
- name: Publish to SideStore/apps-v2.json
|
||||
if: ${{ inputs.is_beta && inputs.publish }}
|
||||
id: publish-release
|
||||
shell: bash
|
||||
run: |
|
||||
# Copy and execute the update script
|
||||
pushd SideStore/apps-v2.json/
|
||||
|
||||
# Configure Git user (committer details)
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
# update the source.json
|
||||
python3 ../../update_apps.py "./_includes/source.json"
|
||||
|
||||
# Commit changes and push using SSH
|
||||
git add --verbose ./_includes/source.json
|
||||
git commit -m " - updated for ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
|
||||
|
||||
git push --verbose
|
||||
popd
|
||||
24
.github/workflows/sidestore-shared.yml
vendored
@@ -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 }}
|
||||
204
.github/workflows/sidestore-tests-build.yml
vendored
@@ -1,204 +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-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies - xcbeautify
|
||||
run: |
|
||||
brew install xcbeautify
|
||||
shell: bash
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
|
||||
# - name: (Tests-Build) Cache Build
|
||||
# uses: irgaly/xcode-cache@v1.8.1
|
||||
# with:
|
||||
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
# # tests shouldn't restore cache unless it is same build
|
||||
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
|
||||
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
# swiftpm-cache-restore-keys: |
|
||||
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
|
||||
# delete-used-deriveddata-cache: true
|
||||
|
||||
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
|
||||
id: xcode-cache-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-
|
||||
|
||||
- name: (Tests-Build) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Tests-Build) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-
|
||||
|
||||
- name: (Tests-Build) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Build) Save Pods to Cache
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: Clean Derived Data (if required)
|
||||
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
|
||||
run: |
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/
|
||||
make clean
|
||||
xcodebuild clean
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Build) Clean previous build artifacts
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Build) List Files and derived data
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
- name: Build SideStore Tests
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
shell: bash
|
||||
run: |
|
||||
NSUnbufferedIO=YES make -B build-tests 2>&1 | tee -a build/logs/tests-build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: (Tests-Build) Save Xcode & SwiftPM Cache
|
||||
id: cache-save
|
||||
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Build) List Files and Build artifacts
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
|
||||
echo ""
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: tests-build-deriveddata-${{ inputs.short_commit }}.txt
|
||||
path: tests-build-deriveddata.txt
|
||||
|
||||
- name: Encrypt tests-build-logs for upload
|
||||
id: encrypt-test-log
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-build-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
|
||||
- name: Upload encrypted-tests-build-logs.zip
|
||||
id: attach-encrypted-test-log
|
||||
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
|
||||
path: encrypted-tests-build-logs.zip
|
||||
235
.github/workflows/sidestore-tests-run.yml
vendored
@@ -1,235 +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-15'
|
||||
version: '16.2'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Boot Simulator async(nohup) for testing
|
||||
run: |
|
||||
mkdir -p build/logs
|
||||
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
|
||||
shell: bash
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
|
||||
# - name: (Tests-Run) Cache Build
|
||||
# uses: irgaly/xcode-cache@v1.8.1
|
||||
# with:
|
||||
# # This comes from
|
||||
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
|
||||
id: xcode-cache-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Developer/Xcode/DerivedData
|
||||
~/Library/Caches/org.swift.swiftpm
|
||||
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
- name: (Tests-Run) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Tests-Run) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-
|
||||
|
||||
- name: (Tests-Run) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Run) Save Pods to Cache
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Tests-Run) Clean previous build artifacts
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Run) List Files and derived data
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
|
||||
echo ""
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: tests-run-deriveddata-${{ inputs.short_commit }}.txt
|
||||
path: tests-run-deriveddata.txt
|
||||
|
||||
# we expect simulator to have been booted by now, so exit otherwise
|
||||
- name: Simulator Boot Check
|
||||
run: |
|
||||
mkdir -p build/logs
|
||||
make -B sim-boot-check | tee -a build/logs/tests-run.log
|
||||
exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Start Recording UI tests (if DEBUG_RECORD_TESTS is set to 1)
|
||||
if: ${{ vars.DEBUG_RECORD_TESTS == '1' }}
|
||||
run: |
|
||||
nohup xcrun simctl io booted recordVideo -f tests-recording.mp4 --codec h264 </dev/null > tests-recording.log 2>&1 &
|
||||
RECORD_PID=$!
|
||||
echo "RECORD_PID=$RECORD_PID" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Run SideStore Tests
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
make run-tests 2>&1 | tee -a build/logs/tests-run.log && exit ${PIPESTATUS[0]}
|
||||
# NSUnbufferedIO=YES make -B run-tests 2>&1 | tee build/logs/tests-run.log | xcpretty -r junit --output ./build/tests/test-results.xml && exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Stop Recording tests
|
||||
if: ${{ always() && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
kill -INT ${{ env.RECORD_PID }}
|
||||
shell: bash
|
||||
|
||||
- name: (Tests-Run) List Files and Build artifacts
|
||||
if: always()
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Build <<<<<<<<<<"
|
||||
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Encrypt tests-run-logs for upload
|
||||
id: encrypt-test-log
|
||||
if: always()
|
||||
run: |
|
||||
DEFAULT_BUILD_LOG_PASSWORD=12345
|
||||
|
||||
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
|
||||
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
|
||||
|
||||
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
|
||||
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
|
||||
fi
|
||||
|
||||
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-run-logs.zip * || popd
|
||||
echo "::set-output name=encrypted::true"
|
||||
shell: bash
|
||||
|
||||
- name: Upload encrypted-tests-run-logs.zip
|
||||
id: attach-encrypted-test-log
|
||||
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
|
||||
path: encrypted-tests-run-logs.zip
|
||||
|
||||
- name: Print tests-recording.log contents (if exists)
|
||||
if: ${{ always() && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
if [ -f tests-recording.log ]; then
|
||||
echo "tests-recording.log found. Its contents:"
|
||||
cat tests-recording.log
|
||||
else
|
||||
echo "tests-recording.log not found."
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Check for tests-recording.mp4 presence
|
||||
id: check-recording
|
||||
if: ${{ always() && env.RECORD_PID != '' }}
|
||||
run: |
|
||||
if [ -f tests-recording.mp4 ]; then
|
||||
echo "::set-output name=found::true"
|
||||
echo "tests-recording.mp4 found."
|
||||
else
|
||||
echo "tests-recording.mp4 not found, skipping upload."
|
||||
echo "::set-output name=found::false"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Upload tests-recording.mp4
|
||||
id: upload-recording
|
||||
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tests-recording-${{ inputs.short_commit }}.mp4
|
||||
path: tests-recording.mp4
|
||||
|
||||
- name: Zip test-results
|
||||
run: zip -r -9 ./test-results.zip ./build/tests
|
||||
shell: bash
|
||||
|
||||
- name: Upload Test Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results-${{ inputs.short_commit }}.zip
|
||||
path: test-results.zip
|
||||
283
.github/workflows/stable.yml
vendored
@@ -1,283 +0,0 @@
|
||||
name: Stable SideStore build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build SideStore - stable (on tag push)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'macos-15'
|
||||
version: '26.0'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Echo Build.xcconfig
|
||||
run: |
|
||||
echo "cat Build.xcconfig"
|
||||
cat Build.xcconfig
|
||||
shell: bash
|
||||
|
||||
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build
|
||||
# run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
|
||||
|
||||
- name: Echo Updated Build.xcconfig
|
||||
run: |
|
||||
cat Build.xcconfig
|
||||
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"
|
||||
|
||||
echo "MARKETING_VERSION=$version" >> $GITHUB_ENV
|
||||
echo "MARKETING_VERSION=$version" >> $GITHUB_OUTPUT
|
||||
echo "MARKETING_VERSION=$version"
|
||||
|
||||
shell: bash
|
||||
|
||||
- name: Fail the build if pushed tag and embedded MARKETING_VERSION in Build.xcconfig are mismatching
|
||||
run: |
|
||||
if [ "$MARKETING_VERSION" != "${{ github.ref_name }}" ]; then
|
||||
echo 'Version mismatch: $tag != $marketing_version ... '
|
||||
echo " expected-tag : $MARKETING_VERSION"
|
||||
echo " pushed-tag : ${{ github.ref_name }}"
|
||||
exit 1
|
||||
fi
|
||||
echo 'Version matches: $tag == $marketing_version ... '
|
||||
echo " expected-tag : $MARKETING_VERSION"
|
||||
echo " pushed-tag : ${{ github.ref_name }}"
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies - ldid & xcbeautify
|
||||
run: |
|
||||
brew install ldid xcbeautify
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: ${{ matrix.version }}
|
||||
|
||||
- name: (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-stable-${{ 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-stable-
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Exact match)
|
||||
id: pods-restore
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-stable-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Build) Restore Pods from Cache (Last Available)
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
id: pods-restore-recent
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-stable-
|
||||
|
||||
|
||||
- name: (Build) Install CocoaPods
|
||||
run: pod install
|
||||
shell: bash
|
||||
|
||||
- name: (Build) Save Pods to Cache
|
||||
id: save-pods
|
||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: |
|
||||
./Podfile.lock
|
||||
./Pods/
|
||||
./AltStore.xcworkspace/
|
||||
key: pods-cache-build-stable-${{ hashFiles('Podfile') }}
|
||||
|
||||
- name: (Build) Clean previous build artifacts
|
||||
run: |
|
||||
make clean
|
||||
mkdir -p build/logs
|
||||
shell: bash
|
||||
|
||||
- name: (Build) List Files and derived data
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo ">>>>>>>>> Workdir <<<<<<<<<<"
|
||||
ls -la .
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Pods <<<<<<<<<<"
|
||||
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> SideStore <<<<<<<<<<"
|
||||
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
|
||||
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
|
||||
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
|
||||
echo ""
|
||||
|
||||
- name: Build SideStore.xcarchive
|
||||
# using 'tee' to intercept stdout and log for detailed build-log
|
||||
run: |
|
||||
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
|
||||
shell: bash
|
||||
|
||||
- name: Fakesign app
|
||||
run: make fakesign | 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-stable-${{ 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: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Get current date in AltStore date form
|
||||
id: date_altstore
|
||||
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Upload to releases
|
||||
uses: IsaacShelton/update-existing-release@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
release: ${{ github.ref_name }} # name
|
||||
tag: ${{ github.ref_name }}
|
||||
# stick with what the user pushed, do not use latest commit or anything,
|
||||
# ex: if we want to go back to previous release due to hot issue, dev can create a new tag pointing to that older working tag/commit so as to keep it as an update (to revert major issue)
|
||||
# in this case we do not want the tag to be auto-updated to latest
|
||||
updateTag: false
|
||||
prerelease: false
|
||||
files: >
|
||||
SideStore.ipa
|
||||
SideStore.dSYMs.zip
|
||||
encrypted-build-logs.zip
|
||||
body: |
|
||||
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
|
||||
## Changelog
|
||||
|
||||
- TODO
|
||||
|
||||
## Build Info
|
||||
|
||||
Built at (UTC): `${{ steps.date.outputs.date }}`
|
||||
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
|
||||
Commit SHA: `${{ github.sha }}`
|
||||
Version: `${{ steps.version.outputs.version }}`
|
||||
46
.gitignore
vendored
@@ -1,18 +1,14 @@
|
||||
# macOS
|
||||
#
|
||||
**/*.DS_Store
|
||||
*.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
|
||||
## CocoaPods
|
||||
Pods/
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData
|
||||
|
||||
SideStore.xcarchive
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
@@ -31,42 +27,4 @@ xcuserdata
|
||||
*.xcscmblueprint
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
/newrelic_agent.log
|
||||
/CodeSigning.xcconfig
|
||||
/.vscode
|
||||
|
||||
## AppCode specific
|
||||
.idea/
|
||||
|
||||
Payload/
|
||||
**/SideStore.ipa
|
||||
**/AltBackup.ipa
|
||||
**/*.dSYM
|
||||
|
||||
Dependencies/.*-prebuilt-fetch-*
|
||||
SideStore/minimuxer/*
|
||||
SideStore/em_proxy/*
|
||||
!Dependencies/**/.gitkeep
|
||||
.nightly-build-num
|
||||
|
||||
## em_proxy and minimuxer biaries
|
||||
**/.last-prebuilt-fetch-em_proxy
|
||||
**/.last-prebuilt-fetch-minimuxer
|
||||
|
||||
# misc
|
||||
**/output.txt
|
||||
SideStore/.skip-prebuilt-fetch-minimuxer
|
||||
SideStore/.skip-prebuilt-fetch-em_proxy
|
||||
|
||||
.git.bkp/
|
||||
# Never check-in this package.resolved file
|
||||
# coz SPM then resolves packages using the stale entries in this file
|
||||
*.xcodeproj/**/Package.resolved
|
||||
*.xcworkspace/**/Package.resolved
|
||||
|
||||
# some more commandline build artifacts
|
||||
test-recording.mp4
|
||||
test-recording.log
|
||||
altstore-sources.md
|
||||
local-build.sh
|
||||
*.hmap
|
||||
72
.gitmodules
vendored
@@ -1,64 +1,18 @@
|
||||
#-------------------------------
|
||||
# When changing url/branch in this .gitmodules file,
|
||||
# Always ensure you run:
|
||||
# 1. `git rm --cached <submodule_relative_path>` # this removes the submodule entry from general git tracking
|
||||
# 2. `rm -rf .git/modules/<submodule_relative_path>` # this removes the stale name entries in submodule tracker
|
||||
# 3. `rm -rf <submodule_relative_path>` # removes the submodule completely
|
||||
# 4. `git submodule --deinit <submodule_relative_path>` # make sure that the submodule is de-inited too (ignore errors at this point)
|
||||
# 5. `git submodule add [-b <branch_name>] <repo_url> <submodule_relative_path>` # This adds the submodule back into general git tracking and also adds to the submodule tracker
|
||||
# 6. Step 5 creates an entry in the .gitmodules when a submodule is added,
|
||||
# So if you already had one entry, try to remove duplicates at this point
|
||||
# 7. `git submodule sync --recursive` # this now sets/updates the submodule repo url tracker into git config
|
||||
# 8. `git submodule update --init --recursive` # this now clones the updated repo set by .gitmodules
|
||||
# But this will always fetch the latest commit sepecified by the custom(if set)/default branch
|
||||
# 9. If you do want to have a specific commit in that submodule branch and not latest, you need to perform normal detached head checkout and check-in as follows:
|
||||
# `pushd <submodule_relative_path>` # switch to the submodule repo
|
||||
# `git checkout <commit-id>` # this creates a detached head state
|
||||
# `popd` # get back to parent repo
|
||||
# `git add <submodule_relative_path>` # check-in the changes in parent for this submodule link (tracker)
|
||||
# `git commit -m <commit-message>` # commit it to parent repo
|
||||
# `git push` # push to parent repo to preserve this entire change in the submodule repo/link file
|
||||
#
|
||||
# NOTES:
|
||||
# 1. updating just this .gitmodules file is NOT ENOUGH when changing repo url and performing a simple `git submodule update --init --recursive`, need to do all the above listed steps for proper tracking
|
||||
# 2. updating the branch in this .gitmodules for same repo is okay as long as `git submodule update --init --recursive` is also performed followed by it
|
||||
# 3. Ensure there is no stale entries or duplicate entries in this .gitmodules file coz, `git submodule add ...` creates an entry here.
|
||||
#-------------------------------
|
||||
|
||||
[submodule "Dependencies/Roxas"]
|
||||
path = Dependencies/Roxas
|
||||
url = https://github.com/rileytestut/Roxas.git
|
||||
path = Dependencies/Roxas
|
||||
url = https://github.com/rileytestut/Roxas.git
|
||||
[submodule "Dependencies/AltSign"]
|
||||
path = Dependencies/AltSign
|
||||
url = https://github.com/rileytestut/AltSign.git
|
||||
[submodule "Dependencies/libimobiledevice"]
|
||||
path = Dependencies/libimobiledevice
|
||||
url = https://github.com/SideStore/libimobiledevice
|
||||
path = Dependencies/libimobiledevice
|
||||
url = https://github.com/rileytestut/libimobiledevice.git
|
||||
[submodule "Dependencies/libusbmuxd"]
|
||||
path = Dependencies/libusbmuxd
|
||||
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||
path = Dependencies/libusbmuxd
|
||||
url = https://github.com/libimobiledevice/libusbmuxd.git
|
||||
[submodule "Dependencies/libplist"]
|
||||
path = Dependencies/libplist
|
||||
url = https://github.com/SideStore/libplist.git
|
||||
path = Dependencies/libplist
|
||||
url = https://github.com/libimobiledevice/libplist.git
|
||||
[submodule "Dependencies/MarkdownAttributedString"]
|
||||
path = Dependencies/MarkdownAttributedString
|
||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||
[submodule "Dependencies/libimobiledevice-glue"]
|
||||
path = Dependencies/libimobiledevice-glue
|
||||
url = https://github.com/libimobiledevice/libimobiledevice-glue
|
||||
|
||||
|
||||
#sidestore dependencies
|
||||
[submodule "SideStore/minimuxer"]
|
||||
path = SideStore/minimuxer
|
||||
url = https://github.com/SideStore/minimuxer
|
||||
branch = master
|
||||
[submodule "SideStore/libfragmentzip"]
|
||||
path = SideStore/libfragmentzip
|
||||
url = https://github.com/SideStore/libfragmentzip
|
||||
branch = master
|
||||
[submodule "SideStore/apps-v2.json"]
|
||||
path = SideStore/apps-v2.json
|
||||
url = https://github.com/SideStore/apps-v2.json
|
||||
branch = main
|
||||
[submodule "SideStore/AltSign"]
|
||||
path = SideStore/AltSign
|
||||
url = https://github.com/SideStore/AltSign
|
||||
branch = master
|
||||
path = Dependencies/MarkdownAttributedString
|
||||
url = https://github.com/chockenberry/MarkdownAttributedString.git
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -10,10 +10,10 @@ import UIKit
|
||||
|
||||
extension AppDelegate
|
||||
{
|
||||
static let startBackupNotification = Notification.Name("io.sidestore.StartBackup")
|
||||
static let startRestoreNotification = Notification.Name("io.sidestore.StartRestore")
|
||||
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
|
||||
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
|
||||
|
||||
static let operationDidFinishNotification = Notification.Name("io.sidestore.BackupOperationFinished")
|
||||
static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
|
||||
|
||||
static let operationResultKey = "result"
|
||||
}
|
||||
@@ -88,25 +88,14 @@ private extension AppDelegate
|
||||
|
||||
@objc func operationDidFinish(_ notification: Notification)
|
||||
{
|
||||
defer {
|
||||
self.currentBackupReturnURL = nil
|
||||
}
|
||||
defer { self.currentBackupReturnURL = nil }
|
||||
|
||||
// TODO: @mahee96: This doesn't account cases where backup is too long and user switched to other apps
|
||||
// The check for self.currentBackupReturnURL when backup/restore was still in progress but app switched
|
||||
// between FG/BG is improper, since it will ignore(eat up) the response(success/failure) to parent
|
||||
//
|
||||
// This leaves the backup/restore to show dummy animation forever
|
||||
guard
|
||||
let returnURL = self.currentBackupReturnURL,
|
||||
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
||||
else {
|
||||
return // This is bad (Needs fixing - never eat up response like this unless there is no context to post response to!)
|
||||
}
|
||||
else { return }
|
||||
|
||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else {
|
||||
return // This is ASSERTION Failure, ie RETURN URL needs to be valid. So ignoring (eating up) response is not the solution
|
||||
}
|
||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
|
||||
|
||||
switch result
|
||||
{
|
||||
@@ -123,7 +112,6 @@ private extension AppDelegate
|
||||
guard let responseURL = components.url else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Response to the caller/parent app is posted here (url is provided by caller in incoming query params)
|
||||
UIApplication.shared.open(responseURL, options: [:]) { (success) in
|
||||
print("Sent response to app with success:", success)
|
||||
}
|
||||
|
||||
@@ -1,151 +1,91 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "57.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "114.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "50.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "50x50"
|
||||
},
|
||||
{
|
||||
"filename" : "100.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "50x50"
|
||||
},
|
||||
{
|
||||
"filename" : "72.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "72x72"
|
||||
},
|
||||
{
|
||||
"filename" : "144.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "72x72"
|
||||
},
|
||||
{
|
||||
"filename" : "76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
57
AltBackup/BackupController.swift
Executable file → Normal file
@@ -26,60 +26,20 @@ extension Error
|
||||
|
||||
struct BackupError: ALTLocalizedError
|
||||
{
|
||||
enum Code: ALTErrorEnum, RawRepresentable
|
||||
enum Code
|
||||
{
|
||||
case invalidBundleID
|
||||
case appGroupNotFound(String?)
|
||||
case randomError // Used for debugging.
|
||||
|
||||
// Provide failure reason for each error code
|
||||
var errorFailureReason: String {
|
||||
switch self {
|
||||
case .invalidBundleID:
|
||||
return NSLocalizedString("The bundle identifier is invalid.", comment: "")
|
||||
case .appGroupNotFound(let appGroup):
|
||||
if let appGroup = appGroup {
|
||||
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
|
||||
} else {
|
||||
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
|
||||
}
|
||||
case .randomError:
|
||||
return NSLocalizedString("A random error occurred.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
static var errorDomain: String {
|
||||
return "com.sidestore.BackupError"
|
||||
}
|
||||
|
||||
// Add a raw value for RawRepresentable conformance
|
||||
var rawValue: Int {
|
||||
switch self {
|
||||
case .invalidBundleID: return 0
|
||||
case .appGroupNotFound: return 1
|
||||
case .randomError: return 2
|
||||
}
|
||||
}
|
||||
|
||||
// Initializer for RawRepresentable
|
||||
init?(rawValue: Int) {
|
||||
switch rawValue {
|
||||
case 0: self = .invalidBundleID
|
||||
case 1: self = .appGroupNotFound(nil)
|
||||
case 2: self = .randomError
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let code: Code
|
||||
|
||||
let sourceFile: String
|
||||
let sourceFileLine: Int
|
||||
|
||||
var failure: String?
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
|
||||
var failureReason: String? {
|
||||
switch self.code
|
||||
{
|
||||
@@ -106,19 +66,12 @@ struct BackupError: ALTLocalizedError
|
||||
return userInfo.compactMapValues { $0 }
|
||||
}
|
||||
|
||||
// Implement description for CustomStringConvertible
|
||||
var description: String {
|
||||
return "\(errorTitle ?? "Unknown Error"): \(failureReason ?? "No reason available")"
|
||||
}
|
||||
|
||||
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
|
||||
{
|
||||
self.code = code
|
||||
self.failure = description
|
||||
self.sourceFile = file
|
||||
self.sourceFileLine = line
|
||||
self.errorTitle = NSLocalizedString("Backup Error", comment: "")
|
||||
self.errorFailure = description
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,9 +96,7 @@ class BackupController: NSObject
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else {
|
||||
throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: ""))
|
||||
}
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) }
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<dict>
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.$(APP_GROUP_IDENTIFIER)</string>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
</array>
|
||||
<key>ALTBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<string>com.rileytestut.AltBackup</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
@@ -28,15 +28,15 @@
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>SideBackup General</string>
|
||||
<string>AltBackup General</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>sidebackup</string>
|
||||
<string>altbackup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>XYZ0123456.com.SideStore.SideStore.AltBackup</string>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>XYZ0123456</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.SideStore.SideStore</string>
|
||||
</array>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -82,25 +82,23 @@ class ViewController: UIViewController
|
||||
self.activityIndicatorView.color = .altstoreText
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
// TODO: @mahee96: Disabled these backup/restore buttons in altbackup.app screen which were present for debugging purpose.
|
||||
// Can find something useful for these later, but these are not required by this backup/restore app
|
||||
// #if DEBUG
|
||||
// let button1 = UIButton(type: .system)
|
||||
// button1.setTitle("Backup", for: .normal)
|
||||
// button1.setTitleColor(.white, for: .normal)
|
||||
// button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
// button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||
//
|
||||
// let button2 = UIButton(type: .system)
|
||||
// button2.setTitle("Restore", for: .normal)
|
||||
// button2.setTitleColor(.white, for: .normal)
|
||||
// button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
// button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||
//
|
||||
// let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
||||
// #else
|
||||
#if DEBUG
|
||||
let button1 = UIButton(type: .system)
|
||||
button1.setTitle("Backup", for: .normal)
|
||||
button1.setTitleColor(.white, for: .normal)
|
||||
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||
|
||||
let button2 = UIButton(type: .system)
|
||||
button2.setTitle("Restore", for: .normal)
|
||||
button2.setTitleColor(.white, for: .normal)
|
||||
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
||||
#else
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
|
||||
// #endif
|
||||
#endif
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -157,13 +155,12 @@ private extension ViewController
|
||||
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
|
||||
self.detailTextLabel.isHidden = true
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
// TODO: @mahee96: This is pointless since, app going in bg/fg should still report its last operation properly
|
||||
|
||||
case .none:
|
||||
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
|
||||
|
||||
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in SideStore to continue using it.", comment: ""),
|
||||
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in AltStore to continue using it.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
|
||||
|
||||
self.detailTextLabel.isHidden = false
|
||||
@@ -201,9 +198,6 @@ private extension ViewController
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: @mahee96: This doesn't account cases where backup is too long and user switched to other apps
|
||||
// Now the user has lost his progress since current operation was cancelled due to switch between FG and BG
|
||||
// if this just the reset for enum such that UI stops showing progress circle, then this is fine!
|
||||
@objc func didEnterBackground(_ notification: Notification)
|
||||
{
|
||||
// Reset UI once we've left app (but not before).
|
||||
|
||||
59
AltDaemon/AltDaemon-Bridging-Header.h
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Shared
|
||||
#import "ALTConstants.h"
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
|
||||
// libproc
|
||||
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
|
||||
|
||||
// Security.framework
|
||||
CF_ENUM(uint32_t) {
|
||||
kSecCSInternalInformation = 1 << 0,
|
||||
kSecCSSigningInformation = 1 << 1,
|
||||
kSecCSRequirementInformation = 1 << 2,
|
||||
kSecCSDynamicInformation = 1 << 3,
|
||||
kSecCSContentInformation = 1 << 4,
|
||||
kSecCSSkipResourceDirectory = 1 << 5,
|
||||
kSecCSCalculateCMSDigest = 1 << 6,
|
||||
};
|
||||
|
||||
OSStatus SecStaticCodeCreateWithPath(CFURLRef path, uint32_t flags, void ** __nonnull CF_RETURNS_RETAINED staticCode);
|
||||
OSStatus SecCodeCopySigningInformation(void *code, uint32_t flags, CFDictionaryRef * __nonnull CF_RETURNS_RETAINED information);
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AKDevice : NSObject
|
||||
|
||||
@property (class, readonly) AKDevice *currentDevice;
|
||||
|
||||
@property (strong, readonly) NSString *serialNumber;
|
||||
@property (strong, readonly) NSString *uniqueDeviceIdentifier;
|
||||
@property (strong, readonly) NSString *serverFriendlyDescription;
|
||||
|
||||
@end
|
||||
|
||||
@interface AKAppleIDSession : NSObject
|
||||
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier;
|
||||
|
||||
- (NSDictionary<NSString *, NSString *> *)appleIDHeadersForRequest:(NSURLRequest *)request;
|
||||
|
||||
@end
|
||||
|
||||
@interface LSApplicationWorkspace : NSObject
|
||||
|
||||
@property (class, readonly) LSApplicationWorkspace *defaultWorkspace;
|
||||
|
||||
- (BOOL)installApplication:(NSURL *)fileURL withOptions:(nullable NSDictionary<NSString *, id> *)options error:(NSError *_Nullable *)error;
|
||||
- (BOOL)uninstallApplication:(NSString *)bundleIdentifier withOptions:(nullable NSDictionary *)options;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
22
AltDaemon/AltDaemon.entitlements
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>6XVY5G3U44.com.rileytestut.AltDaemon</string>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
<key>platform-application</key>
|
||||
<true/>
|
||||
<key>com.apple.authkit.client.private</key>
|
||||
<true/>
|
||||
<key>com.apple.private.mobileinstall.allowedSPI</key>
|
||||
<array>
|
||||
<string>Install</string>
|
||||
<string>Uninstall</string>
|
||||
<string>InstallForLaunchServices</string>
|
||||
<string>UninstallForLaunchServices</string>
|
||||
<string>InstallLocalProvisioned</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
65
AltDaemon/AnisetteDataManager.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// AnisetteDataManager.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension UserDefaults
|
||||
{
|
||||
@objc var localUserID: String? {
|
||||
get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
|
||||
}
|
||||
}
|
||||
|
||||
struct AnisetteDataManager
|
||||
{
|
||||
static let shared = AnisetteDataManager()
|
||||
|
||||
private let dateFormatter = ISO8601DateFormatter()
|
||||
|
||||
private init()
|
||||
{
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
||||
}
|
||||
|
||||
func requestAnisetteData() throws -> ALTAnisetteData
|
||||
{
|
||||
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
|
||||
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
|
||||
|
||||
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
|
||||
let headers = session.appleIDHeaders(for: request)
|
||||
|
||||
let device = akDevice.current
|
||||
let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
|
||||
|
||||
var localUserID = UserDefaults.standard.localUserID
|
||||
if localUserID == nil
|
||||
{
|
||||
localUserID = UUID().uuidString
|
||||
UserDefaults.standard.localUserID = localUserID
|
||||
}
|
||||
|
||||
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
|
||||
oneTimePassword: headers["X-Apple-I-MD"] ?? "",
|
||||
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",
|
||||
routingInfo: UInt64(headers["X-Apple-I-MD-RINFO"] ?? "") ?? 0,
|
||||
deviceUniqueIdentifier: device.uniqueDeviceIdentifier,
|
||||
deviceSerialNumber: device.serialNumber,
|
||||
deviceDescription: "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
|
||||
date: date,
|
||||
locale: .current,
|
||||
timeZone: .current)
|
||||
return anisetteData
|
||||
}
|
||||
}
|
||||
138
AltDaemon/AppManager.swift
Normal file
@@ -0,0 +1,138 @@
|
||||
//
|
||||
// AppManager.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
private extension URL
|
||||
{
|
||||
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
|
||||
}
|
||||
|
||||
private extension CFNotificationName
|
||||
{
|
||||
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
|
||||
}
|
||||
|
||||
struct AppManager
|
||||
{
|
||||
static let shared = AppManager()
|
||||
|
||||
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
|
||||
private let profilesQueue = OperationQueue()
|
||||
|
||||
private let fileCoordinator = NSFileCoordinator()
|
||||
|
||||
private init()
|
||||
{
|
||||
self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
|
||||
self.profilesQueue.qualityOfService = .userInitiated
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
|
||||
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
|
||||
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
||||
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.appQueue.async {
|
||||
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
|
||||
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
let installingBundleIDs = Set(profiles.map(\.bundleIdentifier))
|
||||
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
|
||||
for fileURL in profileURLs
|
||||
{
|
||||
// Use memory mapping to reduce peak memory usage and stay within limit.
|
||||
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
|
||||
|
||||
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
for profile in profiles
|
||||
{
|
||||
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
|
||||
try profile.data.write(to: destinationURL, options: .atomic)
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
|
||||
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
|
||||
do
|
||||
{
|
||||
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
for fileURL in profileURLs
|
||||
{
|
||||
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
|
||||
|
||||
if bundleIdentifiers.contains(profile.bundleIdentifier)
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
// Notify system to prevent accidentally untrusting developer certificate.
|
||||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
AltDaemon/DaemonRequestHandler.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// DaemonRequestHandler.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
|
||||
|
||||
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
|
||||
connectionHandlers: [XPCConnectionHandler()])
|
||||
|
||||
extension DaemonConnectionManager
|
||||
{
|
||||
static var shared: ConnectionManager {
|
||||
return connectionManager
|
||||
}
|
||||
}
|
||||
|
||||
struct DaemonRequestHandler: RequestHandler
|
||||
{
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
|
||||
|
||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
||||
{
|
||||
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
do
|
||||
{
|
||||
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
|
||||
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
|
||||
|
||||
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
|
||||
let result = result.map { InstallationProgressResponse(progress: 1.0) }
|
||||
print("Installed app with result:", result)
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||
|
||||
let response = InstallProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Removed profiles:", request.bundleIdentifiers)
|
||||
|
||||
let response = RemoveProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
||||
{
|
||||
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Removed app:", request.bundleIdentifier)
|
||||
|
||||
let response = RemoveAppResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
AltDaemon/XPCConnectionHandler.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// XPCConnectionHandler.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 9/14/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
class XPCConnectionHandler: NSObject, ConnectionHandler
|
||||
{
|
||||
var connectionHandler: ((Connection) -> Void)?
|
||||
var disconnectionHandler: ((Connection) -> Void)?
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
|
||||
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
|
||||
|
||||
deinit
|
||||
{
|
||||
self.stopListening()
|
||||
}
|
||||
|
||||
func startListening()
|
||||
{
|
||||
for listener in self.listeners
|
||||
{
|
||||
listener.delegate = self
|
||||
listener.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func stopListening()
|
||||
{
|
||||
self.listeners.forEach { $0.suspend() }
|
||||
}
|
||||
}
|
||||
|
||||
private extension XPCConnectionHandler
|
||||
{
|
||||
func disconnect(_ connection: Connection)
|
||||
{
|
||||
connection.disconnect()
|
||||
|
||||
self.disconnectionHandler?(connection)
|
||||
}
|
||||
}
|
||||
|
||||
extension XPCConnectionHandler: NSXPCListenerDelegate
|
||||
{
|
||||
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
|
||||
{
|
||||
let maximumPathLength = 4 * UInt32(MAXPATHLEN)
|
||||
|
||||
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
|
||||
defer { pathBuffer.deallocate() }
|
||||
|
||||
proc_pidpath(newConnection.processIdentifier, pathBuffer, maximumPathLength)
|
||||
|
||||
let path = String(cString: pathBuffer)
|
||||
let fileURL = URL(fileURLWithPath: path)
|
||||
|
||||
var code: UnsafeMutableRawPointer?
|
||||
defer { code.map { Unmanaged<AnyObject>.fromOpaque($0).release() } }
|
||||
|
||||
var status = SecStaticCodeCreateWithPath(fileURL as CFURL, 0, &code)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
var signingInfo: CFDictionary?
|
||||
defer { signingInfo.map { Unmanaged<AnyObject>.passUnretained($0).release() } }
|
||||
|
||||
status = SecCodeCopySigningInformation(code, kSecCSInternalInformation | kSecCSSigningInformation, &signingInfo)
|
||||
guard status == 0 else { return false }
|
||||
|
||||
// Only accept connections from AltStore.
|
||||
guard
|
||||
let codeSigningInfo = signingInfo as? [String: Any],
|
||||
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
|
||||
bundleIdentifier.contains("com.rileytestut.AltStore")
|
||||
else { return false }
|
||||
|
||||
let connection = XPCConnection(newConnection)
|
||||
newConnection.invalidationHandler = { [weak self, weak connection] in
|
||||
guard let self = self, let connection = connection else { return }
|
||||
self.disconnect(connection)
|
||||
}
|
||||
|
||||
self.connectionHandler?(connection)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
14
AltDaemon/main.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// main.swift
|
||||
// AltDaemon
|
||||
//
|
||||
// Created by Riley Testut on 6/2/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
autoreleasepool {
|
||||
DaemonConnectionManager.shared.start()
|
||||
RunLoop.current.run()
|
||||
}
|
||||
10
AltDaemon/package/DEBIAN/control
Normal file
@@ -0,0 +1,10 @@
|
||||
Package: com.rileytestut.altdaemon
|
||||
Name: AltDaemon
|
||||
Depends:
|
||||
Version: 1.0
|
||||
Architecture: iphoneos-arm
|
||||
Description: AltDaemon allows AltStore to install and refresh apps without a computer.
|
||||
Maintainer: Riley Testut
|
||||
Author: Riley Testut
|
||||
Homepage: https://altstore.io
|
||||
Section: System
|
||||
2
AltDaemon/package/DEBIAN/postinst
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
launchctl load /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||
2
AltDaemon/package/DEBIAN/preinst
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist >> /dev/null 2>&1
|
||||
2
AltDaemon/package/DEBIAN/prerm
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.rileytestut.altdaemon</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/bin/env</string>
|
||||
<string>_MSSafeMode=1</string>
|
||||
<string>_SafeMode=1</string>
|
||||
<string>/usr/bin/AltDaemon</string>
|
||||
</array>
|
||||
<key>UserName</key>
|
||||
<string>mobile</string>
|
||||
<key>KeepAlive</key>
|
||||
<false/>
|
||||
<key>RunAtLoad</key>
|
||||
<false/>
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
<key>cy:io.altstore.altdaemon</key>
|
||||
<true/>
|
||||
<key>lh:io.altstore.altdaemon</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
AltDaemon/package/usr/bin/AltDaemon
Executable file
23
AltPlugin/ALTPluginService.h
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// ALTPluginService.h
|
||||
// AltPlugin
|
||||
//
|
||||
// Created by Riley Testut on 11/14/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class ALTAnisetteData;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTPluginService : NSObject
|
||||
|
||||
@property (class, nonatomic, readonly) ALTPluginService *sharedService;
|
||||
|
||||
- (ALTAnisetteData *)requestAnisetteData;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
105
AltPlugin/ALTPluginService.m
Normal file
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// ALTPluginService.m
|
||||
// AltPlugin
|
||||
//
|
||||
// Created by Riley Testut on 11/14/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTPluginService.h"
|
||||
|
||||
#import <dlfcn.h>
|
||||
|
||||
#import "ALTAnisetteData.h"
|
||||
|
||||
@import AppKit;
|
||||
|
||||
@interface AKAppleIDSession : NSObject
|
||||
- (id)appleIDHeadersForRequest:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface AKDevice
|
||||
+ (AKDevice *)currentDevice;
|
||||
- (NSString *)uniqueDeviceIdentifier;
|
||||
- (nullable NSString *)serialNumber;
|
||||
- (NSString *)serverFriendlyDescription;
|
||||
@end
|
||||
|
||||
@interface ALTPluginService ()
|
||||
|
||||
@property (nonatomic, readonly) NSISO8601DateFormatter *dateFormatter;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ALTPluginService
|
||||
|
||||
+ (instancetype)sharedService
|
||||
{
|
||||
static ALTPluginService *_service = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_service = [[self alloc] init];
|
||||
});
|
||||
|
||||
return _service;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_dateFormatter = [[NSISO8601DateFormatter alloc] init];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
[[ALTPluginService sharedService] start];
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
|
||||
|
||||
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"com.rileytestut.AltServer.FetchAnisetteData" object:nil];
|
||||
}
|
||||
|
||||
- (ALTAnisetteData *)requestAnisetteData
|
||||
{
|
||||
NSMutableURLRequest* req = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:@"https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA"]];
|
||||
[req setHTTPMethod:@"POST"];
|
||||
|
||||
AKAppleIDSession *session = [[NSClassFromString(@"AKAppleIDSession") alloc] initWithIdentifier:@"com.apple.gs.xcode.auth"];
|
||||
NSDictionary *headers = [session appleIDHeadersForRequest:req];
|
||||
|
||||
AKDevice *device = [NSClassFromString(@"AKDevice") currentDevice];
|
||||
NSDate *date = [self.dateFormatter dateFromString:headers[@"X-Apple-I-Client-Time"]];
|
||||
|
||||
ALTAnisetteData *anisetteData = [[NSClassFromString(@"ALTAnisetteData") alloc] initWithMachineID:headers[@"X-Apple-I-MD-M"]
|
||||
oneTimePassword:headers[@"X-Apple-I-MD"]
|
||||
localUserID:headers[@"X-Apple-I-MD-LU"]
|
||||
routingInfo:[headers[@"X-Apple-I-MD-RINFO"] longLongValue]
|
||||
deviceUniqueIdentifier:device.uniqueDeviceIdentifier
|
||||
deviceSerialNumber:device.serialNumber ?: @"C02LKHBBFD57" // serialNumber can be nil, so provide valid fallback serial number.
|
||||
deviceDescription:device.serverFriendlyDescription
|
||||
date:date
|
||||
locale:[NSLocale currentLocale]
|
||||
timeZone:[NSTimeZone localTimeZone]];
|
||||
|
||||
return anisetteData;
|
||||
}
|
||||
|
||||
- (void)receiveNotification:(NSNotification *)notification
|
||||
{
|
||||
NSString *requestUUID = notification.userInfo[@"requestUUID"];
|
||||
|
||||
ALTAnisetteData *anisetteData = [self requestAnisetteData];
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:anisetteData requiringSecureCoding:YES error:nil];
|
||||
|
||||
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.rileytestut.AltServer.AnisetteDataResponse" object:nil userInfo:@{@"requestUUID": requestUUID, @"anisetteData": data} deliverImmediately:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
214
AltPlugin/Info.plist
Normal file
@@ -0,0 +1,214 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>ALTPluginService</string>
|
||||
<key>Supported10.14PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string># UUIDs for versions from 10.12 to 99.99.99</string>
|
||||
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
|
||||
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
|
||||
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
|
||||
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
|
||||
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
|
||||
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
|
||||
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
|
||||
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
|
||||
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
|
||||
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
|
||||
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
|
||||
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
|
||||
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
|
||||
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
|
||||
</array>
|
||||
<key>Supported10.15PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string># UUIDs for versions from 10.12 to 99.99.99</string>
|
||||
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
|
||||
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
|
||||
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
|
||||
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
|
||||
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
|
||||
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
|
||||
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
|
||||
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
|
||||
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
|
||||
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
|
||||
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
|
||||
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
|
||||
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
|
||||
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
|
||||
</array>
|
||||
<key>Supported11.0PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.10PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.1PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.2PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.3PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.4PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.5PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.6PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.7PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.8PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported11.9PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
</array>
|
||||
<key>Supported12.0PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
</array>
|
||||
<key>Supported12.1PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
</array>
|
||||
<key>Supported12.2PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
</array>
|
||||
<key>Supported12.3PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.4PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.5PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.6PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.7PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.8PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported12.9PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported13.0PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
</array>
|
||||
<key>Supported13.1PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
</array>
|
||||
<key>Supported13.2PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
</array>
|
||||
<key>Supported13.3PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.4PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.5PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.6PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.7PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.8PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.9PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
AltServer/AltPlugin.zip
Normal file
16
AltServer/AltServer-Bridging-Header.h
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "ALTDeviceManager.h"
|
||||
#import "ALTWiredConnection.h"
|
||||
#import "ALTNotificationConnection.h"
|
||||
#import "ALTDebugConnection.h"
|
||||
|
||||
// Shared
|
||||
#import "ALTConstants.h"
|
||||
#import "ALTConnection.h"
|
||||
#import "AltXPCProtocol.h"
|
||||
#import "ALTWrappedError.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
5
AltServer/AltServer.entitlements
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
156
AltServer/AnisetteDataManager.swift
Normal file
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// AnisetteDataManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 11/16/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private extension Bundle
|
||||
{
|
||||
struct ID
|
||||
{
|
||||
static let mail = "com.apple.mail"
|
||||
static let altXPC = "com.rileytestut.AltXPC"
|
||||
}
|
||||
}
|
||||
|
||||
private extension ALTAnisetteData
|
||||
{
|
||||
func sanitize(byReplacingBundleID bundleID: String)
|
||||
{
|
||||
guard let range = self.deviceDescription.lowercased().range(of: "(" + bundleID.lowercased()) else { return }
|
||||
|
||||
var adjustedDescription = self.deviceDescription[..<range.lowerBound]
|
||||
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
|
||||
|
||||
self.deviceDescription = String(adjustedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
class AnisetteDataManager: NSObject
|
||||
{
|
||||
static let shared = AnisetteDataManager()
|
||||
|
||||
private var anisetteDataCompletionHandlers: [String: (Result<ALTAnisetteData, Error>) -> Void] = [:]
|
||||
private var anisetteDataTimers: [String: Timer] = [:]
|
||||
|
||||
private lazy var xpcConnection: NSXPCConnection = {
|
||||
let connection = NSXPCConnection(serviceName: Bundle.ID.altXPC)
|
||||
connection.remoteObjectInterface = NSXPCInterface(with: AltXPCProtocol.self)
|
||||
connection.resume()
|
||||
return connection
|
||||
}()
|
||||
|
||||
private override init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
DistributedNotificationCenter.default().addObserver(self, selector: #selector(AnisetteDataManager.handleAnisetteDataResponse(_:)), name: Notification.Name("com.rileytestut.AltServer.AnisetteDataResponse"), object: nil)
|
||||
}
|
||||
|
||||
func requestAnisetteData(_ completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
||||
{
|
||||
if #available(macOS 10.15, *)
|
||||
{
|
||||
self.requestAnisetteDataFromXPCService { (result) in
|
||||
do
|
||||
{
|
||||
let anisetteData = try result.get()
|
||||
completion(.success(anisetteData))
|
||||
}
|
||||
catch CocoaError.xpcConnectionInterrupted
|
||||
{
|
||||
// SIP and/or AMFI are not disabled, so fall back to Mail plug-in.
|
||||
self.requestAnisetteDataFromPlugin { (result) in
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.requestAnisetteDataFromPlugin { (result) in
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isXPCAvailable(completion: @escaping (Bool) -> Void)
|
||||
{
|
||||
guard let proxy = self.xpcConnection.remoteObjectProxyWithErrorHandler({ (error) in
|
||||
completion(false)
|
||||
}) as? AltXPCProtocol else { return }
|
||||
|
||||
proxy.ping {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AnisetteDataManager
|
||||
{
|
||||
@available(macOS 10.15, *)
|
||||
func requestAnisetteDataFromXPCService(completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
||||
{
|
||||
guard let proxy = self.xpcConnection.remoteObjectProxyWithErrorHandler({ (error) in
|
||||
print("Anisette XPC Error:", error)
|
||||
completion(.failure(error))
|
||||
}) as? AltXPCProtocol else { return }
|
||||
|
||||
proxy.requestAnisetteData { (anisetteData, error) in
|
||||
anisetteData?.sanitize(byReplacingBundleID: Bundle.ID.altXPC)
|
||||
completion(Result(anisetteData, error))
|
||||
}
|
||||
}
|
||||
|
||||
func requestAnisetteDataFromPlugin(completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
|
||||
{
|
||||
let requestUUID = UUID().uuidString
|
||||
self.anisetteDataCompletionHandlers[requestUUID] = completion
|
||||
|
||||
let timer = Timer(timeInterval: 1.0, repeats: false) { (timer) in
|
||||
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.pluginNotFound)))
|
||||
}
|
||||
self.anisetteDataTimers[requestUUID] = timer
|
||||
|
||||
RunLoop.main.add(timer, forMode: .default)
|
||||
|
||||
DistributedNotificationCenter.default().postNotificationName(Notification.Name("com.rileytestut.AltServer.FetchAnisetteData"), object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately)
|
||||
}
|
||||
|
||||
@objc func handleAnisetteDataResponse(_ notification: Notification)
|
||||
{
|
||||
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
|
||||
|
||||
if
|
||||
let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
||||
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData)
|
||||
{
|
||||
anisetteData.sanitize(byReplacingBundleID: Bundle.ID.mail)
|
||||
self.finishRequest(forUUID: requestUUID, result: .success(anisetteData))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.finishRequest(forUUID: requestUUID, result: .failure(ALTServerError(.invalidAnisetteData)))
|
||||
}
|
||||
}
|
||||
|
||||
func finishRequest(forUUID requestUUID: String, result: Result<ALTAnisetteData, Error>)
|
||||
{
|
||||
let completionHandler = self.anisetteDataCompletionHandlers[requestUUID]
|
||||
self.anisetteDataCompletionHandlers[requestUUID] = nil
|
||||
|
||||
let timer = self.anisetteDataTimers[requestUUID]
|
||||
self.anisetteDataTimers[requestUUID] = nil
|
||||
|
||||
timer?.invalidate()
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
586
AltServer/AppDelegate.swift
Normal file
@@ -0,0 +1,586 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/24/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import UserNotifications
|
||||
|
||||
import AltSign
|
||||
|
||||
import LaunchAtLogin
|
||||
import Sparkle
|
||||
|
||||
extension ALTDevice: MenuDisplayable {}
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private let pluginManager = PluginManager()
|
||||
|
||||
private var statusItem: NSStatusItem?
|
||||
|
||||
private var connectedDevices = [ALTDevice]()
|
||||
|
||||
private weak var authenticationAlert: NSAlert?
|
||||
|
||||
@IBOutlet private var appMenu: NSMenu!
|
||||
@IBOutlet private var connectedDevicesMenu: NSMenu!
|
||||
@IBOutlet private var sideloadIPAConnectedDevicesMenu: NSMenu!
|
||||
@IBOutlet private var enableJITMenu: NSMenu!
|
||||
|
||||
@IBOutlet private var launchAtLoginMenuItem: NSMenuItem!
|
||||
@IBOutlet private var installMailPluginMenuItem: NSMenuItem!
|
||||
@IBOutlet private var installAltStoreMenuItem: NSMenuItem!
|
||||
@IBOutlet private var sideloadAppMenuItem: NSMenuItem!
|
||||
|
||||
private weak var authenticationAppleIDTextField: NSTextField?
|
||||
private weak var authenticationPasswordTextField: NSSecureTextField?
|
||||
|
||||
private var connectedDevicesMenuController: MenuController<ALTDevice>!
|
||||
private var sideloadIPAConnectedDevicesMenuController: MenuController<ALTDevice>!
|
||||
private var enableJITMenuController: MenuController<ALTDevice>!
|
||||
|
||||
private var _jitAppListMenuControllers = [AnyObject]()
|
||||
|
||||
private var isAltPluginUpdateAvailable = false
|
||||
|
||||
private var popoverController: NSPopover?
|
||||
private var popoverError: NSError?
|
||||
private var errorAlert: NSAlert?
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification)
|
||||
{
|
||||
UserDefaults.standard.registerDefaults()
|
||||
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
ServerConnectionManager.shared.start()
|
||||
ALTDeviceManager.shared.start()
|
||||
|
||||
#if STAGING
|
||||
SUUpdater.shared().feedURL = URL(string: "https://altstore.io/altserver/sparkle-macos-staging.xml")
|
||||
#else
|
||||
SUUpdater.shared().feedURL = URL(string: "https://altstore.io/altserver/sparkle-macos.xml")
|
||||
#endif
|
||||
|
||||
let item = NSStatusBar.system.statusItem(withLength: -1)
|
||||
item.menu = self.appMenu
|
||||
item.button?.image = NSImage(named: "MenuBarIcon")
|
||||
self.statusItem = item
|
||||
|
||||
self.appMenu.delegate = self
|
||||
|
||||
self.sideloadAppMenuItem.keyEquivalentModifierMask = .option
|
||||
self.sideloadAppMenuItem.isAlternate = true
|
||||
|
||||
let placeholder = NSLocalizedString("No Connected Devices", comment: "")
|
||||
|
||||
self.connectedDevicesMenuController = MenuController<ALTDevice>(menu: self.connectedDevicesMenu, items: [])
|
||||
self.connectedDevicesMenuController.placeholder = placeholder
|
||||
self.connectedDevicesMenuController.action = { [weak self] device in
|
||||
self?.installAltStore(to: device)
|
||||
}
|
||||
|
||||
self.sideloadIPAConnectedDevicesMenuController = MenuController<ALTDevice>(menu: self.sideloadIPAConnectedDevicesMenu, items: [])
|
||||
self.sideloadIPAConnectedDevicesMenuController.placeholder = placeholder
|
||||
self.sideloadIPAConnectedDevicesMenuController.action = { [weak self] device in
|
||||
self?.sideloadIPA(to: device)
|
||||
}
|
||||
|
||||
self.enableJITMenuController = MenuController<ALTDevice>(menu: self.enableJITMenu, items: [])
|
||||
self.enableJITMenuController.placeholder = placeholder
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in
|
||||
guard success else { return }
|
||||
|
||||
if !UserDefaults.standard.didPresentInitialNotification
|
||||
{
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("AltServer Running", comment: "")
|
||||
content.body = NSLocalizedString("AltServer runs in the background as a menu bar app listening for AltStore.", comment: "")
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
UserDefaults.standard.didPresentInitialNotification = true
|
||||
}
|
||||
}
|
||||
|
||||
self.pluginManager.isUpdateAvailable { result in
|
||||
guard let isUpdateAvailable = try? result.get() else { return }
|
||||
self.isAltPluginUpdateAvailable = isUpdateAvailable
|
||||
|
||||
if isUpdateAvailable
|
||||
{
|
||||
self.installMailPlugin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification)
|
||||
{
|
||||
// Insert code here to tear down your application
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
{
|
||||
@objc func installAltStore(to device: ALTDevice)
|
||||
{
|
||||
self.installApplication(at: nil, to: device)
|
||||
}
|
||||
|
||||
@objc func sideloadIPA(to device: ALTDevice)
|
||||
{
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let openPanel = NSOpenPanel()
|
||||
openPanel.canChooseDirectories = false
|
||||
openPanel.allowsMultipleSelection = false
|
||||
openPanel.allowedFileTypes = ["ipa"]
|
||||
openPanel.begin { (response) in
|
||||
guard let fileURL = openPanel.url, response == .OK else { return }
|
||||
self.installApplication(at: fileURL, to: device)
|
||||
}
|
||||
}
|
||||
|
||||
func enableJIT(for app: InstalledApp, on device: ALTDevice)
|
||||
{
|
||||
func finish(_ result: Result<Void, Error>)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(let error as NSError):
|
||||
let localizedTitle = String(format: NSLocalizedString("JIT could not be enabled for %@.", comment: ""), app.name)
|
||||
self.showErrorAlert(error: error.withLocalizedTitle(localizedTitle))
|
||||
|
||||
case .success:
|
||||
let alert = NSAlert()
|
||||
alert.messageText = String(format: NSLocalizedString("Successfully enabled JIT for %@.", comment: ""), app.name)
|
||||
alert.informativeText = String(format: NSLocalizedString("JIT will remain enabled until you quit the app. You can now disconnect %@ from your computer.", comment: ""), device.name)
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALTDeviceManager.shared.prepare(device) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error as NSError): return finish(.failure(error))
|
||||
case .success:
|
||||
ALTDeviceManager.shared.startDebugConnection(to: device) { (connection, error) in
|
||||
guard let connection = connection else {
|
||||
return finish(.failure(error! as NSError))
|
||||
}
|
||||
|
||||
connection.enableUnsignedCodeExecutionForProcess(withName: app.executableName) { (success, error) in
|
||||
guard success else {
|
||||
return finish(.failure(error!))
|
||||
}
|
||||
|
||||
finish(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installApplication(at fileURL: URL?, to device: ALTDevice)
|
||||
{
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "")
|
||||
|
||||
let textFieldSize = NSSize(width: 300, height: 22)
|
||||
|
||||
let appleIDTextField = NSTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height))
|
||||
appleIDTextField.delegate = self
|
||||
appleIDTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
appleIDTextField.placeholderString = NSLocalizedString("Apple ID", comment: "")
|
||||
alert.window.initialFirstResponder = appleIDTextField
|
||||
self.authenticationAppleIDTextField = appleIDTextField
|
||||
|
||||
let passwordTextField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height))
|
||||
passwordTextField.delegate = self
|
||||
passwordTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
passwordTextField.placeholderString = NSLocalizedString("Password", comment: "")
|
||||
self.authenticationPasswordTextField = passwordTextField
|
||||
|
||||
appleIDTextField.nextKeyView = passwordTextField
|
||||
|
||||
let stackView = NSStackView(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height * 2))
|
||||
stackView.orientation = .vertical
|
||||
stackView.distribution = .equalSpacing
|
||||
stackView.spacing = 0
|
||||
stackView.addArrangedSubview(appleIDTextField)
|
||||
stackView.addArrangedSubview(passwordTextField)
|
||||
alert.accessoryView = stackView
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Install", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
self.authenticationAlert = alert
|
||||
self.validate()
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { return }
|
||||
|
||||
let username = appleIDTextField.stringValue
|
||||
let password = passwordTextField.stringValue
|
||||
|
||||
func finish(_ result: Result<ALTApplication, Error>)
|
||||
{
|
||||
switch result
|
||||
{
|
||||
case .success(let application):
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("Installation Succeeded", comment: "")
|
||||
content.body = String(format: NSLocalizedString("%@ was successfully installed on %@.", comment: ""), application.name, device.name)
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
case .failure(OperationError.cancelled), .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
||||
// Ignore
|
||||
break
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.showErrorAlert(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func install()
|
||||
{
|
||||
ALTDeviceManager.shared.installApplication(at: fileURL, to: device, appleID: username, password: password, completion: finish(_:))
|
||||
}
|
||||
|
||||
AnisetteDataManager.shared.isXPCAvailable { isAvailable in
|
||||
if isAvailable
|
||||
{
|
||||
// XPC service is available, so we don't need to install/update Mail plug-in.
|
||||
// Users can still manually do so from the AltServer menu.
|
||||
install()
|
||||
}
|
||||
else
|
||||
{
|
||||
self.pluginManager.isUpdateAvailable { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
let error = (error as NSError).withLocalizedTitle(NSLocalizedString("Could not check for Mail plug-in updates.", comment: ""))
|
||||
finish(.failure(error))
|
||||
|
||||
case .success(let isUpdateAvailable):
|
||||
self.isAltPluginUpdateAvailable = isUpdateAvailable
|
||||
|
||||
if !self.pluginManager.isMailPluginInstalled || isUpdateAvailable
|
||||
{
|
||||
self.installMailPlugin { result in
|
||||
switch result
|
||||
{
|
||||
case .failure: break
|
||||
case .success: install()
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
install()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showErrorAlert(error: Error)
|
||||
{
|
||||
self.popoverError = error as NSError
|
||||
|
||||
let nsError = error as NSError
|
||||
|
||||
var messageComponents = [error.localizedDescription]
|
||||
if let recoverySuggestion = nsError.localizedRecoverySuggestion
|
||||
{
|
||||
messageComponents.append(recoverySuggestion)
|
||||
}
|
||||
|
||||
let title = nsError.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
|
||||
let message = messageComponents.joined(separator: "\n\n")
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = title
|
||||
alert.informativeText = message
|
||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("View More Details", comment: ""))
|
||||
|
||||
if let viewMoreButton = alert.buttons.last
|
||||
{
|
||||
viewMoreButton.target = self
|
||||
viewMoreButton.action = #selector(AppDelegate.showDetailedErrorDescription)
|
||||
|
||||
self.errorAlert = alert
|
||||
}
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
alert.runModal()
|
||||
|
||||
self.popoverController = nil
|
||||
self.errorAlert = nil
|
||||
self.popoverError = nil
|
||||
}
|
||||
|
||||
@objc func showDetailedErrorDescription()
|
||||
{
|
||||
guard let errorAlert, let contentView = errorAlert.window.contentView else { return }
|
||||
|
||||
let errorDetailsViewController = NSStoryboard(name: "Main", bundle: .main).instantiateController(withIdentifier: "errorDetailsViewController") as! ErrorDetailsViewController
|
||||
errorDetailsViewController.error = self.popoverError
|
||||
|
||||
let fittingSize = errorDetailsViewController.view.fittingSize
|
||||
errorDetailsViewController.view.frame.size = fittingSize
|
||||
|
||||
let popoverController = NSPopover()
|
||||
popoverController.contentViewController = errorDetailsViewController
|
||||
popoverController.contentSize = fittingSize
|
||||
popoverController.behavior = .transient
|
||||
popoverController.show(relativeTo: contentView.bounds, of: contentView, preferredEdge: .maxX)
|
||||
self.popoverController = popoverController
|
||||
}
|
||||
|
||||
@objc func toggleLaunchAtLogin(_ item: NSMenuItem)
|
||||
{
|
||||
LaunchAtLogin.isEnabled.toggle()
|
||||
}
|
||||
|
||||
@objc func handleInstallMailPluginMenuItem(_ item: NSMenuItem)
|
||||
{
|
||||
if !self.pluginManager.isMailPluginInstalled || self.isAltPluginUpdateAvailable
|
||||
{
|
||||
self.installMailPlugin()
|
||||
}
|
||||
else
|
||||
{
|
||||
self.uninstallMailPlugin()
|
||||
}
|
||||
}
|
||||
|
||||
private func installMailPlugin(completion: ((Result<Void, Error>) -> Void)? = nil)
|
||||
{
|
||||
self.pluginManager.installMailPlugin { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(PluginError.cancelled): break
|
||||
case .failure(let error):
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
|
||||
case .success:
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
|
||||
alert.runModal()
|
||||
|
||||
self.isAltPluginUpdateAvailable = false
|
||||
}
|
||||
|
||||
completion?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func uninstallMailPlugin()
|
||||
{
|
||||
self.pluginManager.uninstallMailPlugin { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(PluginError.cancelled): break
|
||||
case .failure(let error):
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Failed to Uninstall Mail Plug-in", comment: "")
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.runModal()
|
||||
|
||||
case .success:
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Mail Plug-in Uninstalled", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Please restart Mail for changes to take effect. You will not be able to use AltServer until the plug-in is reinstalled.", comment: "")
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: NSMenuDelegate
|
||||
{
|
||||
func menuWillOpen(_ menu: NSMenu)
|
||||
{
|
||||
guard menu == self.appMenu else { return }
|
||||
|
||||
// Clear any cached _jitAppListMenuControllers.
|
||||
self._jitAppListMenuControllers.removeAll()
|
||||
|
||||
self.connectedDevices = ALTDeviceManager.shared.availableDevices
|
||||
|
||||
self.connectedDevicesMenuController.items = self.connectedDevices
|
||||
self.sideloadIPAConnectedDevicesMenuController.items = self.connectedDevices
|
||||
self.enableJITMenuController.items = self.connectedDevices
|
||||
|
||||
self.launchAtLoginMenuItem.target = self
|
||||
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
|
||||
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
|
||||
|
||||
if self.isAltPluginUpdateAvailable
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Update Mail Plug-in…", comment: "")
|
||||
}
|
||||
else if self.pluginManager.isMailPluginInstalled
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Uninstall Mail Plug-in…", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
self.installMailPluginMenuItem.title = NSLocalizedString("Install Mail Plug-in…", comment: "")
|
||||
}
|
||||
self.installMailPluginMenuItem.target = self
|
||||
self.installMailPluginMenuItem.action = #selector(AppDelegate.handleInstallMailPluginMenuItem(_:))
|
||||
|
||||
// Need to re-set this every time menu appears so we can refresh device app list.
|
||||
self.enableJITMenuController.submenuHandler = { [weak self] device in
|
||||
let submenu = NSMenu(title: NSLocalizedString("Sideloaded Apps", comment: ""))
|
||||
|
||||
guard let `self` = self else { return submenu }
|
||||
|
||||
let submenuController = MenuController<InstalledApp>(menu: submenu, items: [])
|
||||
submenuController.placeholder = NSLocalizedString("Loading...", comment: "")
|
||||
submenuController.action = { [weak self] (appInfo) in
|
||||
self?.enableJIT(for: appInfo, on: device)
|
||||
}
|
||||
|
||||
// Keep strong reference
|
||||
self._jitAppListMenuControllers.append(submenuController)
|
||||
|
||||
ALTDeviceManager.shared.fetchInstalledApps(on: device) { (installedApps, error) in
|
||||
DispatchQueue.main.async {
|
||||
guard let installedApps = installedApps else {
|
||||
print("Failed to fetch installed apps from \(device).", error!)
|
||||
submenuController.placeholder = error?.localizedDescription
|
||||
return
|
||||
}
|
||||
|
||||
print("Fetched \(installedApps.count) apps for \(device).")
|
||||
|
||||
let sortedApps = installedApps.sorted { (app1, app2) in
|
||||
if app1.name == app2.name
|
||||
{
|
||||
return app1.bundleIdentifier < app2.bundleIdentifier
|
||||
}
|
||||
else
|
||||
{
|
||||
return app1.name < app2.name
|
||||
}
|
||||
}
|
||||
|
||||
submenuController.items = sortedApps
|
||||
|
||||
if submenuController.items.isEmpty
|
||||
{
|
||||
submenuController.placeholder = NSLocalizedString("No Sideloaded Apps", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return submenu
|
||||
}
|
||||
}
|
||||
|
||||
func menuDidClose(_ menu: NSMenu)
|
||||
{
|
||||
guard menu == self.appMenu else { return }
|
||||
|
||||
// Clearing _jitAppListMenuControllers now prevents action handler from being called.
|
||||
// self._jitAppListMenuControllers = []
|
||||
|
||||
// Set `submenuHandler` to nil to prevent prematurely fetching installed apps in menuWillOpen(_:)
|
||||
// when assigning self.connectedDevices to `items` (which implicitly calls `submenuHandler`)
|
||||
self.enableJITMenuController.submenuHandler = nil
|
||||
}
|
||||
|
||||
func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?)
|
||||
{
|
||||
guard menu == self.appMenu else { return }
|
||||
|
||||
// The submenu won't update correctly if the user holds/releases
|
||||
// the Option key while the submenu is visible.
|
||||
// Workaround: temporarily set submenu to nil to dismiss it,
|
||||
// which will then cause the correct submenu to appear.
|
||||
|
||||
let previousItem: NSMenuItem
|
||||
switch item
|
||||
{
|
||||
case self.sideloadAppMenuItem: previousItem = self.installAltStoreMenuItem
|
||||
case self.installAltStoreMenuItem: previousItem = self.sideloadAppMenuItem
|
||||
default: return
|
||||
}
|
||||
|
||||
let submenu = previousItem.submenu
|
||||
previousItem.submenu = nil
|
||||
previousItem.submenu = submenu
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: NSTextFieldDelegate
|
||||
{
|
||||
func controlTextDidChange(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
func controlTextDidEndEditing(_ obj: Notification)
|
||||
{
|
||||
self.validate()
|
||||
}
|
||||
|
||||
private func validate()
|
||||
{
|
||||
guard
|
||||
let appleID = self.authenticationAppleIDTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
let password = self.authenticationPasswordTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
else { return }
|
||||
|
||||
if appleID.isEmpty || password.isEmpty
|
||||
{
|
||||
self.authenticationAlert?.buttons.first?.isEnabled = false
|
||||
}
|
||||
else
|
||||
{
|
||||
self.authenticationAlert?.buttons.first?.isEnabled = true
|
||||
}
|
||||
|
||||
self.authenticationAlert?.layout()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: UNUserNotificationCenterDelegate
|
||||
{
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
|
||||
{
|
||||
completionHandler([.alert, .sound, .badge])
|
||||
}
|
||||
}
|
||||
68
AltServer/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@32-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@256-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@512-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "Icon@1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@1024.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@128.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@16.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@256-1.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@256.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@32-1.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@32.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@512-1.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@512.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
AltServer/Assets.xcassets/AppIcon.appiconset/Icon@64.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
6
AltServer/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
25
AltServer/Assets.xcassets/MenuBarIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MenuBar@19.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MenuBar@38.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
AltServer/Assets.xcassets/MenuBarIcon.imageset/MenuBar@19.png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
AltServer/Assets.xcassets/MenuBarIcon.imageset/MenuBar@38.png
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
468
AltServer/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,468 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="4" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" id="urc-xw-Dhc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="46"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zLd-d8-ghZ">
|
||||
<rect key="frame" x="0.0" y="25" width="300" height="21"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Apple ID" drawsBackground="YES" id="BXa-Re-rs3">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="QtW-r2-Vuh"/>
|
||||
<outlet property="nextKeyView" destination="9rp-Vx-rvB" id="bQY-qj-Sej"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9rp-Vx-rvB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="300" height="21"/>
|
||||
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Password" drawsBackground="YES" usesSingleLineMode="YES" id="xqJ-wt-DlP">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<allowedInputSourceLocales>
|
||||
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
|
||||
</allowedInputSourceLocales>
|
||||
</secureTextFieldCell>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="qav-xj-izy"/>
|
||||
</connections>
|
||||
</secureTextField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="9rp-Vx-rvB" firstAttribute="width" secondItem="urc-xw-Dhc" secondAttribute="width" id="Eht-pU-Gyh"/>
|
||||
<constraint firstItem="zLd-d8-ghZ" firstAttribute="width" secondItem="urc-xw-Dhc" secondAttribute="width" id="mg7-Kq-abL"/>
|
||||
<constraint firstAttribute="width" constant="300" id="zqf-x6-BET"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="AltServer" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="appMenu" destination="uQy-DD-JDr" id="7cY-Ov-AOW"/>
|
||||
<outlet property="authenticationAppleIDTextField" destination="zLd-d8-ghZ" id="wW5-0J-zdq"/>
|
||||
<outlet property="authenticationPasswordTextField" destination="9rp-Vx-rvB" id="ZoC-DI-jzQ"/>
|
||||
<outlet property="connectedDevicesMenu" destination="KJ9-WY-pW1" id="Mcv-64-iFU"/>
|
||||
<outlet property="enableJITMenu" destination="la4-Sa-L3C" id="YrW-hR-uA7"/>
|
||||
<outlet property="installAltStoreMenuItem" destination="MJ8-Lt-SSV" id="KYp-c5-8Ru"/>
|
||||
<outlet property="installMailPluginMenuItem" destination="3CM-gV-X2G" id="lio-ha-z0S"/>
|
||||
<outlet property="launchAtLoginMenuItem" destination="IyR-FQ-upe" id="Fxn-EP-hwH"/>
|
||||
<outlet property="sideloadAppMenuItem" destination="x0e-zI-0A2" id="GJo-FY-1GO"/>
|
||||
<outlet property="sideloadIPAConnectedDevicesMenu" destination="IuI-bV-fTY" id="QQw-St-HfG"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="Arf-IC-5eb" customClass="SUUpdater"/>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="AltServer" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="AltServer" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About AltServer" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Install AltStore…" id="MJ8-Lt-SSV">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Install AltStore…" systemMenu="recentDocuments" id="KJ9-WY-pW1">
|
||||
<items>
|
||||
<menuItem title="No Connected Devices" id="N5N-3K-XuR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="DKG-yI-Ujv"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Sideload .ipa…" id="x0e-zI-0A2" userLabel="Install .ipa">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Sideload .ipa…" systemMenu="recentDocuments" id="IuI-bV-fTY">
|
||||
<items>
|
||||
<menuItem title="No Connected Devices" id="in5-an-MD0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="aUE-On-axK"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Enable JIT" id="TvL-8M-oPZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Enable JIT" systemMenu="recentDocuments" id="la4-Sa-L3C">
|
||||
<items>
|
||||
<menuItem title="No Connected Devices" id="Aya-FP-EzE">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="1ZZ-BB-xHy"/>
|
||||
<menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Install Mail Plug-in…" id="3CM-gV-X2G" userLabel="Mail Plug-in">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="mVM-Nm-Zi9"/>
|
||||
<menuItem title="Check for Updates..." id="Tnq-gD-Eic" userLabel="Check for Updates">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="checkForUpdates:" target="Arf-IC-5eb" id="7JG-du-nr4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="hmG-xg-qgm"/>
|
||||
<menuItem title="Quit AltServer" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="AltServer Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="0.0"/>
|
||||
</scene>
|
||||
<!--Error Details View Controller-->
|
||||
<scene sceneID="IpC-sd-lPu">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="errorDetailsViewController" id="vPa-1q-slD" customClass="ErrorDetailsViewController" customModule="AltServer" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="umL-zC-zP8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="450" height="124"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="16" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="aUr-m2-nXm">
|
||||
<rect key="frame" x="20" y="20" width="410" height="84"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8GZ-nV-XXA">
|
||||
<rect key="frame" x="-2" y="68" width="40" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" selectable="YES" title="Label" id="V5D-v5-MVX">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="j2o-2b-63k">
|
||||
<rect key="frame" x="-2" y="36" width="92" height="16"/>
|
||||
<textFieldCell key="cell" selectable="YES" title="Multiline Label" id="3jf-Z7-88l">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<stackView distribution="equalSpacing" orientation="vertical" alignment="trailing" spacing="0.0" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XOa-gj-Rz2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="410" height="20"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="iS8-eX-YvA">
|
||||
<rect key="frame" x="312" y="-7" width="105" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Search FAQ" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Fpi-GA-lVc">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="searchFAQ:" target="vPa-1q-slD" id="WIx-Uo-GVu"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="XOa-gj-Rz2" firstAttribute="width" secondItem="aUr-m2-nXm" secondAttribute="width" id="AGA-99-d6r"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="aUr-m2-nXm" secondAttribute="trailing" constant="20" id="RnW-JC-q92"/>
|
||||
<constraint firstAttribute="bottom" secondItem="aUr-m2-nXm" secondAttribute="bottom" constant="20" id="eGe-BS-TFJ"/>
|
||||
<constraint firstItem="aUr-m2-nXm" firstAttribute="top" secondItem="umL-zC-zP8" secondAttribute="top" constant="20" id="fel-H8-WFO"/>
|
||||
<constraint firstItem="aUr-m2-nXm" firstAttribute="leading" secondItem="umL-zC-zP8" secondAttribute="leading" constant="20" id="tcH-p8-4QH"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="detailedDescriptionLabel" destination="j2o-2b-63k" id="de3-yf-oTt"/>
|
||||
<outlet property="errorCodeLabel" destination="8GZ-nV-XXA" id="cXZ-11-wrJ"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="wrv-m0-8zg" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="537" y="17"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
25
AltServer/Categories/NSError+libimobiledevice.h
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// NSError+libimobiledevice.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 3/23/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <libimobiledevice/mobile_image_mounter.h>
|
||||
#import <libimobiledevice/debugserver.h>
|
||||
#import <libimobiledevice/installation_proxy.h>
|
||||
|
||||
#import <AltSign/ALTDevice.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSError (libimobiledevice)
|
||||
|
||||
+ (nullable instancetype)errorWithMobileImageMounterError:(mobile_image_mounter_error_t)error device:(nullable ALTDevice *)device;
|
||||
+ (nullable instancetype)errorWithDebugServerError:(debugserver_error_t)error device:(nullable ALTDevice *)device;
|
||||
+ (nullable instancetype)errorWithInstallationProxyError:(instproxy_error_t)error device:(nullable ALTDevice *)device;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
86
AltServer/Categories/NSError+libimobiledevice.mm
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// NSError+libimobiledevice.m
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 3/23/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSError+libimobiledevice.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
@implementation NSError (libimobiledevice)
|
||||
|
||||
+ (nullable instancetype)errorWithMobileImageMounterError:(mobile_image_mounter_error_t)error device:(nullable ALTDevice *)device
|
||||
{
|
||||
NSMutableDictionary *userInfo = [@{
|
||||
ALTUnderlyingErrorDomainErrorKey: @"Mobile Image Mounter",
|
||||
ALTUnderlyingErrorCodeErrorKey: [@(error) description],
|
||||
} mutableCopy];
|
||||
|
||||
if (device)
|
||||
{
|
||||
userInfo[ALTDeviceNameErrorKey] = device.name;
|
||||
}
|
||||
|
||||
switch (error)
|
||||
{
|
||||
case MOBILE_IMAGE_MOUNTER_E_SUCCESS: return nil;
|
||||
case MOBILE_IMAGE_MOUNTER_E_INVALID_ARG: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidRequest userInfo:userInfo];
|
||||
case MOBILE_IMAGE_MOUNTER_E_PLIST_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidResponse userInfo:userInfo];
|
||||
case MOBILE_IMAGE_MOUNTER_E_CONN_FAILED: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUsbmuxd userInfo:userInfo];
|
||||
case MOBILE_IMAGE_MOUNTER_E_COMMAND_FAILED: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidRequest userInfo:userInfo];
|
||||
case MOBILE_IMAGE_MOUNTER_E_DEVICE_LOCKED: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorDeviceLocked userInfo:userInfo];
|
||||
case MOBILE_IMAGE_MOUNTER_E_UNKNOWN_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:userInfo];
|
||||
}
|
||||
}
|
||||
|
||||
+ (nullable instancetype)errorWithDebugServerError:(debugserver_error_t)error device:(nullable ALTDevice *)device
|
||||
{
|
||||
NSMutableDictionary *userInfo = [@{
|
||||
ALTUnderlyingErrorDomainErrorKey: @"Debug Server",
|
||||
ALTUnderlyingErrorCodeErrorKey: [@(error) description],
|
||||
} mutableCopy];
|
||||
|
||||
if (device)
|
||||
{
|
||||
userInfo[ALTDeviceNameErrorKey] = device.name;
|
||||
}
|
||||
|
||||
switch (error)
|
||||
{
|
||||
case DEBUGSERVER_E_SUCCESS: return nil;
|
||||
case DEBUGSERVER_E_INVALID_ARG: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidRequest userInfo:userInfo];
|
||||
case DEBUGSERVER_E_MUX_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUsbmuxd userInfo:userInfo];
|
||||
case DEBUGSERVER_E_SSL_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorSSL userInfo:userInfo];
|
||||
case DEBUGSERVER_E_RESPONSE_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidResponse userInfo:userInfo];
|
||||
case DEBUGSERVER_E_TIMEOUT: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorTimedOut userInfo:userInfo];
|
||||
case DEBUGSERVER_E_UNKNOWN_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:userInfo];
|
||||
}
|
||||
}
|
||||
|
||||
+ (nullable instancetype)errorWithInstallationProxyError:(instproxy_error_t)error device:(nullable ALTDevice *)device
|
||||
{
|
||||
NSMutableDictionary *userInfo = [@{
|
||||
ALTUnderlyingErrorDomainErrorKey: @"Installation Proxy",
|
||||
ALTUnderlyingErrorCodeErrorKey: [@(error) description],
|
||||
} mutableCopy];
|
||||
|
||||
if (device)
|
||||
{
|
||||
userInfo[ALTDeviceNameErrorKey] = device.name;
|
||||
}
|
||||
|
||||
switch (error)
|
||||
{
|
||||
case INSTPROXY_E_SUCCESS: return nil;
|
||||
case INSTPROXY_E_INVALID_ARG: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidRequest userInfo:userInfo];
|
||||
case INSTPROXY_E_PLIST_ERROR: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorInvalidResponse userInfo:userInfo];
|
||||
case INSTPROXY_E_CONN_FAILED: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUsbmuxd userInfo:userInfo];
|
||||
case INSTPROXY_E_RECEIVE_TIMEOUT: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorTimedOut userInfo:userInfo];
|
||||
// case INSTPROXY_E_DEVICE_OS_VERSION_TOO_LOW: return [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnsupportediOSVersion userInfo:nil]; // Error message assumes we're installing AltStore
|
||||
default: return [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:userInfo];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
28
AltServer/Connections/ALTDebugConnection+Private.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// ALTDebugConnection+Private.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 2/19/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTDebugConnection.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
#include <libimobiledevice/debugserver.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTDebugConnection ()
|
||||
|
||||
@property (nonatomic, readonly) dispatch_queue_t connectionQueue;
|
||||
|
||||
@property (nonatomic, nullable) debugserver_client_t client;
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device;
|
||||
|
||||
- (void)connectWithCompletionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
25
AltServer/Connections/ALTDebugConnection.h
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// ALTDebugConnection.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 2/19/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AltSign.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NS_SWIFT_NAME(DebugConnection)
|
||||
@interface ALTDebugConnection : NSObject
|
||||
|
||||
@property (nonatomic, copy, readonly) ALTDevice *device;
|
||||
|
||||
- (void)enableUnsignedCodeExecutionForProcessWithName:(NSString *)processName completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
- (void)enableUnsignedCodeExecutionForProcessWithID:(NSInteger)pid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
318
AltServer/Connections/ALTDebugConnection.mm
Normal file
@@ -0,0 +1,318 @@
|
||||
//
|
||||
// ALTDebugConnection.m
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 2/19/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTDebugConnection+Private.h"
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "NSError+libimobiledevice.h"
|
||||
|
||||
char *bin2hex(const unsigned char *bin, size_t length)
|
||||
{
|
||||
if (bin == NULL || length == 0)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *hex = (char *)malloc(length * 2 + 1);
|
||||
for (size_t i = 0; i < length; i++)
|
||||
{
|
||||
hex[i * 2] = "0123456789ABCDEF"[bin[i] >> 4];
|
||||
hex[i * 2 + 1] = "0123456789ABCDEF"[bin[i] & 0x0F];
|
||||
}
|
||||
hex[length * 2] = '\0';
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
||||
@implementation ALTDebugConnection
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_device = device;
|
||||
_connectionQueue = dispatch_queue_create_with_target("io.altstore.AltServer.DebugConnection",
|
||||
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0));
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self disconnect];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
if (_client == nil)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
debugserver_client_free(_client);
|
||||
_client = nil;
|
||||
}
|
||||
|
||||
- (void)connectWithCompletionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
||||
{
|
||||
__block idevice_t device = NULL;
|
||||
|
||||
void (^finish)(BOOL, NSError *) = ^(BOOL success, NSError *error) {
|
||||
if (device)
|
||||
{
|
||||
idevice_free(device);
|
||||
}
|
||||
|
||||
completionHandler(success, error);
|
||||
};
|
||||
|
||||
dispatch_async(self.connectionQueue, ^{
|
||||
/* Find Device */
|
||||
if (idevice_new_with_options(&device, self.device.identifier.UTF8String, (enum idevice_options)((int)IDEVICE_LOOKUP_NETWORK | (int)IDEVICE_LOOKUP_USBMUX)) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
|
||||
}
|
||||
|
||||
/* Connect to debugserver */
|
||||
debugserver_client_t client = NULL;
|
||||
debugserver_error_t error = debugserver_client_start_service(device, &client, "AltServer");
|
||||
if (error != DEBUGSERVER_E_SUCCESS)
|
||||
{
|
||||
return finish(NO, [NSError errorWithDebugServerError:error device:self.device]);
|
||||
}
|
||||
|
||||
self.client = client;
|
||||
|
||||
finish(YES, nil);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)enableUnsignedCodeExecutionForProcessWithName:(NSString *)processName completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
||||
{
|
||||
[self _enableUnsignedCodeExecutionForProcessWithName:processName pid:0 completionHandler:completionHandler];
|
||||
}
|
||||
|
||||
- (void)enableUnsignedCodeExecutionForProcessWithID:(NSInteger)pid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
||||
{
|
||||
[self _enableUnsignedCodeExecutionForProcessWithName:nil pid:(int32_t)pid completionHandler:completionHandler];
|
||||
}
|
||||
|
||||
- (void)_enableUnsignedCodeExecutionForProcessWithName:(nullable NSString *)processName pid:(int32_t)pid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
||||
{
|
||||
dispatch_async(self.connectionQueue, ^{
|
||||
NSString *attachCommand = nil;
|
||||
|
||||
if (processName)
|
||||
{
|
||||
NSString *encodedName = @(bin2hex((const unsigned char *)processName.UTF8String, (size_t)strlen(processName.UTF8String)));
|
||||
attachCommand = [NSString stringWithFormat:@"vAttachOrWait;%@", encodedName];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Convert to Big-endian.
|
||||
int32_t rawPID = CFSwapInt32HostToBig(pid);
|
||||
|
||||
NSString *encodedName = @(bin2hex((const unsigned char *)&rawPID, 4));
|
||||
attachCommand = [NSString stringWithFormat:@"vAttach;%@", encodedName];
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
if (![self sendCommand:attachCommand arguments:nil error:&error])
|
||||
{
|
||||
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
|
||||
userInfo[ALTAppNameErrorKey] = processName;
|
||||
userInfo[ALTDeviceNameErrorKey] = self.device.name;
|
||||
|
||||
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
|
||||
return completionHandler(NO, returnError);
|
||||
}
|
||||
|
||||
NSString *detachCommand = @"D";
|
||||
if (![self sendCommand:detachCommand arguments:nil error:&error])
|
||||
{
|
||||
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
|
||||
userInfo[ALTAppNameErrorKey] = processName;
|
||||
userInfo[ALTDeviceNameErrorKey] = self.device.name;
|
||||
|
||||
NSError *returnError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
|
||||
return completionHandler(NO, returnError);
|
||||
}
|
||||
|
||||
completionHandler(YES, nil);
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Private -
|
||||
|
||||
- (BOOL)sendCommand:(NSString *)command arguments:(nullable NSArray<NSString *> *)arguments error:(NSError **)error
|
||||
{
|
||||
int argc = (int)arguments.count;
|
||||
char **argv = new char*[argc + 1];
|
||||
|
||||
for (int i = 0; i < argc; i++)
|
||||
{
|
||||
NSString *argument = arguments[i];
|
||||
argv[i] = (char *)argument.UTF8String;
|
||||
}
|
||||
|
||||
argv[argc] = NULL;
|
||||
|
||||
debugserver_command_t debugCommand = NULL;
|
||||
debugserver_command_new(command.UTF8String, argc, argv, &debugCommand);
|
||||
|
||||
delete[] argv;
|
||||
|
||||
char *response = NULL;
|
||||
size_t responseSize = 0;
|
||||
debugserver_error_t debugServerError = debugserver_client_send_command(self.client, debugCommand, &response, &responseSize);
|
||||
debugserver_command_free(debugCommand);
|
||||
|
||||
if (debugServerError != DEBUGSERVER_E_SUCCESS)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
*error = [NSError errorWithDebugServerError:debugServerError device:self.device];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString *rawResponse = (response != nil) ? @(response) : nil;
|
||||
if (![self processResponse:rawResponse error:error])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)processResponse:(NSString *)rawResponse error:(NSError **)error
|
||||
{
|
||||
if (rawResponse == nil)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorRequestedAppNotRunning userInfo:nil];
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
if (rawResponse.length == 0 || [rawResponse isEqualToString:@"OK"])
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
char type = [rawResponse characterAtIndex:0];
|
||||
NSString *response = [rawResponse substringFromIndex:1];
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case 'O':
|
||||
{
|
||||
// stdout/stderr
|
||||
|
||||
char *decodedResponse = NULL;
|
||||
debugserver_decode_string(response.UTF8String, response.length, &decodedResponse);
|
||||
|
||||
NSLog(@"Response: %@", @(decodedResponse));
|
||||
|
||||
if (decodedResponse)
|
||||
{
|
||||
free(decodedResponse);
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
case 'T':
|
||||
{
|
||||
// Thread Information
|
||||
|
||||
NSLog(@"Thread stopped. Details:\n%s", response.UTF8String + 1);
|
||||
|
||||
// Parse thread state to determine if app is running.
|
||||
NSArray<NSString *> *components = [response componentsSeparatedByString:@";"];
|
||||
if (components.count <= 1)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
*error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
|
||||
NSLocalizedFailureReasonErrorKey: response,
|
||||
ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
|
||||
ALTSourceLineErrorKey: @(__LINE__)
|
||||
}];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString *mainThread = components[1];
|
||||
NSString *threadState = [[mainThread componentsSeparatedByString:@":"] lastObject];
|
||||
|
||||
NSScanner *scanner = [NSScanner scannerWithString:threadState];
|
||||
|
||||
unsigned long long mainThreadState = 0;
|
||||
[scanner scanHexLongLong:&mainThreadState];
|
||||
|
||||
// If main thread state == 0, app is not running.
|
||||
if (mainThreadState == 0)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorRequestedAppNotRunning userInfo:nil];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
case 'E':
|
||||
{
|
||||
// Error
|
||||
|
||||
if (error)
|
||||
{
|
||||
NSInteger errorCode = [[[response componentsSeparatedByString:@";"] firstObject] integerValue];
|
||||
|
||||
switch (errorCode)
|
||||
{
|
||||
case 96:
|
||||
*error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorRequestedAppNotRunning userInfo:nil];
|
||||
break;
|
||||
|
||||
default:
|
||||
*error = [NSError errorWithDomain:AltServerConnectionErrorDomain code:ALTServerConnectionErrorUnknown userInfo:@{
|
||||
NSLocalizedFailureReasonErrorKey: response,
|
||||
ALTSourceFileErrorKey: @(__FILE__).lastPathComponent,
|
||||
ALTSourceLineErrorKey: @(__LINE__)
|
||||
}];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
case 'W':
|
||||
{
|
||||
// Warning
|
||||
|
||||
NSLog(@"WARNING: %@", response);
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
24
AltServer/Connections/ALTNotificationConnection+Private.h
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// ALTNotificationConnection+Private.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTNotificationConnection.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
#include <libimobiledevice/notification_proxy.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTNotificationConnection ()
|
||||
|
||||
@property (nonatomic, readonly) np_client_t client;
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
30
AltServer/Connections/ALTNotificationConnection.h
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// ALTNotificationConnection.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AltSign.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NS_SWIFT_NAME(NotificationConnection)
|
||||
@interface ALTNotificationConnection : NSObject
|
||||
|
||||
@property (nonatomic, copy, readonly) ALTDevice *device;
|
||||
|
||||
@property (nonatomic, copy, nullable) void (^receivedNotificationHandler)(CFNotificationName notification);
|
||||
|
||||
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications
|
||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)sendNotification:(CFNotificationName)notification
|
||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
94
AltServer/Connections/ALTNotificationConnection.mm
Normal file
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// ALTNotificationConnection.m
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTNotificationConnection+Private.h"
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
void ALTDeviceReceivedNotification(const char *notification, void *user_data);
|
||||
|
||||
@implementation ALTNotificationConnection
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device client:(np_client_t)client
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_device = [device copy];
|
||||
_client = client;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self disconnect];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
np_client_free(self.client);
|
||||
_client = nil;
|
||||
}
|
||||
|
||||
- (void)startListeningForNotifications:(NSArray<NSString *> *)notifications completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
const char **notificationNames = (const char **)malloc((notifications.count + 1) * sizeof(char *));
|
||||
for (int i = 0; i < notifications.count; i++)
|
||||
{
|
||||
NSString *name = notifications[i];
|
||||
notificationNames[i] = name.UTF8String;
|
||||
}
|
||||
notificationNames[notifications.count] = NULL; // Must have terminating NULL entry.
|
||||
|
||||
np_error_t result = np_observe_notifications(self.client, notificationNames);
|
||||
if (result != NP_E_SUCCESS)
|
||||
{
|
||||
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
result = np_set_notify_callback(self.client, ALTDeviceReceivedNotification, (__bridge void *)self);
|
||||
if (result != NP_E_SUCCESS)
|
||||
{
|
||||
return completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
completionHandler(YES, nil);
|
||||
|
||||
free(notificationNames);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)sendNotification:(CFNotificationName)notification completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
np_error_t result = np_post_notification(self.client, [(__bridge NSString *)notification UTF8String]);
|
||||
if (result == NP_E_SUCCESS)
|
||||
{
|
||||
completionHandler(YES, nil);
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(NO, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void ALTDeviceReceivedNotification(const char *notification, void *user_data)
|
||||
{
|
||||
ALTNotificationConnection *connection = (__bridge ALTNotificationConnection *)user_data;
|
||||
|
||||
if (connection.receivedNotificationHandler)
|
||||
{
|
||||
connection.receivedNotificationHandler((__bridge CFNotificationName)@(notification));
|
||||
}
|
||||
}
|
||||
25
AltServer/Connections/ALTWiredConnection+Private.h
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// ALTWiredConnection+Private.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTWiredConnection.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ALTWiredConnection ()
|
||||
|
||||
@property (nonatomic, readwrite, getter=isConnected) BOOL connected;
|
||||
|
||||
@property (nonatomic, readonly) idevice_connection_t connection;
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
29
AltServer/Connections/ALTWiredConnection.h
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// ALTWiredConnection.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AltSign.h"
|
||||
|
||||
#import "ALTConnection.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NS_SWIFT_NAME(WiredConnection)
|
||||
@interface ALTWiredConnection : NSObject <ALTConnection>
|
||||
|
||||
@property (nonatomic, readonly, getter=isConnected) BOOL connected;
|
||||
|
||||
@property (nonatomic, copy, readonly) ALTDevice *device;
|
||||
|
||||
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler;
|
||||
- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler;
|
||||
|
||||
- (void)disconnect;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
119
AltServer/Connections/ALTWiredConnection.mm
Normal file
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// ALTWiredConnection.m
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 1/10/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ALTWiredConnection+Private.h"
|
||||
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
@implementation ALTWiredConnection
|
||||
|
||||
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_device = [device copy];
|
||||
_connection = connection;
|
||||
|
||||
self.connected = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self disconnect];
|
||||
}
|
||||
|
||||
- (void)disconnect
|
||||
{
|
||||
if (![self isConnected])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
idevice_disconnect(self.connection);
|
||||
_connection = nil;
|
||||
|
||||
self.connected = NO;
|
||||
}
|
||||
|
||||
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
|
||||
{
|
||||
void (^finish)(NSError *error) = ^(NSError *error) {
|
||||
if (error != nil)
|
||||
{
|
||||
NSLog(@"Send Error: %@", error);
|
||||
completionHandler(NO, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(YES, nil);
|
||||
}
|
||||
};
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
NSMutableData *mutableData = [data mutableCopy];
|
||||
while (mutableData.length > 0)
|
||||
{
|
||||
uint32_t sentBytes = 0;
|
||||
if (idevice_connection_send(self.connection, (const char *)mutableData.bytes, (int32_t)mutableData.length, &sentBytes) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
[mutableData replaceBytesInRange:NSMakeRange(0, sentBytes) withBytes:NULL length:0];
|
||||
}
|
||||
|
||||
finish(nil);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)receiveDataWithExpectedSize:(NSInteger)expectedSize completionHandler:(void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler
|
||||
{
|
||||
void (^finish)(NSData *data, NSError *error) = ^(NSData *data, NSError *error) {
|
||||
if (error != nil)
|
||||
{
|
||||
NSLog(@"Receive Data Error: %@", error);
|
||||
}
|
||||
|
||||
completionHandler(data, error);
|
||||
};
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||
char bytes[4096];
|
||||
NSMutableData *receivedData = [NSMutableData dataWithCapacity:expectedSize];
|
||||
|
||||
while (receivedData.length < expectedSize)
|
||||
{
|
||||
uint32_t size = MIN(4096, (uint32_t)expectedSize - (uint32_t)receivedData.length);
|
||||
|
||||
uint32_t receivedBytes = 0;
|
||||
if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 10000) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
NSData *data = [NSData dataWithBytesNoCopy:bytes length:receivedBytes freeWhenDone:NO];
|
||||
[receivedData appendData:data];
|
||||
}
|
||||
|
||||
finish(receivedData, nil);
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - NSObject -
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@ (Wired)", self.device.name];
|
||||
}
|
||||
|
||||
@end
|
||||
263
AltServer/Connections/RequestHandler.swift
Normal file
@@ -0,0 +1,263 @@
|
||||
//
|
||||
// RequestHandler.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias ServerConnectionManager = ConnectionManager<ServerRequestHandler>
|
||||
|
||||
private let connectionManager = ConnectionManager(requestHandler: ServerRequestHandler(),
|
||||
connectionHandlers: [WirelessConnectionHandler(), WiredConnectionHandler()])
|
||||
|
||||
extension ServerConnectionManager
|
||||
{
|
||||
static var shared: ConnectionManager {
|
||||
return connectionManager
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerRequestHandler: RequestHandler
|
||||
{
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||
{
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let anisetteData):
|
||||
let response = AnisetteDataResponse(anisetteData: anisetteData)
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
|
||||
{
|
||||
var temporaryURL: URL?
|
||||
|
||||
func finish(_ result: Result<InstallationProgressResponse, Error>)
|
||||
{
|
||||
if let temporaryURL = temporaryURL
|
||||
{
|
||||
do { try FileManager.default.removeItem(at: temporaryURL) }
|
||||
catch { print("Failed to remove .ipa.", error) }
|
||||
}
|
||||
|
||||
completionHandler(result)
|
||||
}
|
||||
|
||||
self.receiveApp(for: request, from: connection) { (result) in
|
||||
print("Received app with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let fileURL):
|
||||
temporaryURL = fileURL
|
||||
|
||||
print("Awaiting begin installation request...")
|
||||
|
||||
connection.receiveRequest() { (result) in
|
||||
print("Received begin installation request with result:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(.beginInstallation(let installRequest)):
|
||||
print("Installing app to device \(request.udid)...")
|
||||
|
||||
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, activeProvisioningProfiles: installRequest.activeProfiles, connection: connection) { (result) in
|
||||
print("Installed app to device with result:", result)
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success:
|
||||
let response = InstallationProgressResponse(progress: 1.0)
|
||||
finish(.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
case .success: finish(.failure(ALTServerError(.unknownRequest)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
ALTDeviceManager.shared.installProvisioningProfiles(request.provisioningProfiles, toDeviceWithUDID: request.udid, activeProvisioningProfiles: request.activeProfiles) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
|
||||
|
||||
let response = InstallProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
|
||||
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
|
||||
{
|
||||
ALTDeviceManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers, fromDeviceWithUDID: request.udid) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Removed profiles:", request.bundleIdentifiers)
|
||||
|
||||
let response = RemoveProvisioningProfilesResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
|
||||
{
|
||||
ALTDeviceManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier, fromDeviceWithUDID: request.udid) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Removed app:", request.bundleIdentifier)
|
||||
|
||||
let response = RemoveAppResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnableUnsignedCodeExecutionRequest(_ request: EnableUnsignedCodeExecutionRequest, for connection: Connection, completionHandler: @escaping (Result<EnableUnsignedCodeExecutionResponse, Error>) -> Void)
|
||||
{
|
||||
guard let device = ALTDeviceManager.shared.availableDevices.first(where: { $0.identifier == request.udid }) else { return completionHandler(.failure(ALTServerError(.deviceNotFound))) }
|
||||
|
||||
ALTDeviceManager.shared.prepare(device) { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success:
|
||||
ALTDeviceManager.shared.startDebugConnection(to: device) { (connection, error) in
|
||||
guard let connection = connection else { return completionHandler(.failure(error!)) }
|
||||
|
||||
func finish(success: Bool, error: Error?)
|
||||
{
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to enable unsigned code execution for process \(request.processID?.description ?? request.processName ?? "nil"):", error)
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Enabled unsigned code execution for process:", request.processID ?? request.processName ?? "nil")
|
||||
|
||||
let response = EnableUnsignedCodeExecutionResponse()
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
if let processID = request.processID
|
||||
{
|
||||
connection.enableUnsignedCodeExecutionForProcess(withID: processID, completionHandler: finish)
|
||||
}
|
||||
else if let processName = request.processName
|
||||
{
|
||||
connection.enableUnsignedCodeExecutionForProcess(withName: processName, completionHandler: finish)
|
||||
}
|
||||
else
|
||||
{
|
||||
finish(success: false, error: ALTServerError(.invalidRequest))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension RequestHandler
|
||||
{
|
||||
func receiveApp(for request: PrepareAppRequest, from connection: Connection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
|
||||
{
|
||||
connection.receiveData(expectedSize: request.contentSize) { (result) in
|
||||
do
|
||||
{
|
||||
print("Received app data!")
|
||||
|
||||
let data = try result.get()
|
||||
|
||||
guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
|
||||
|
||||
print("Writing app data...")
|
||||
|
||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".ipa")
|
||||
try data.write(to: temporaryURL, options: .atomic)
|
||||
|
||||
print("Wrote app to URL:", temporaryURL)
|
||||
|
||||
completionHandler(.success(temporaryURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error processing app data:", error)
|
||||
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, activeProvisioningProfiles: Set<String>?, connection: Connection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
{
|
||||
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
|
||||
var isSending = false
|
||||
|
||||
var observation: NSKeyValueObservation?
|
||||
|
||||
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid, activeProvisioningProfiles: activeProvisioningProfiles) { (success, error) in
|
||||
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
|
||||
|
||||
if let error = error.map({ ALTServerError($0) })
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
|
||||
observation?.invalidate()
|
||||
observation = nil
|
||||
}
|
||||
|
||||
observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, change) in
|
||||
serialQueue.async {
|
||||
guard !isSending else { return }
|
||||
isSending = true
|
||||
|
||||
print("Progress:", progress.fractionCompleted)
|
||||
let response = InstallationProgressResponse(progress: progress.fractionCompleted)
|
||||
|
||||
connection.send(response) { (result) in
|
||||
serialQueue.async {
|
||||
isSending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
122
AltServer/Connections/WiredConnectionHandler.swift
Normal file
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// WiredConnectionHandler.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class WiredConnectionHandler: ConnectionHandler
|
||||
{
|
||||
var connectionHandler: ((Connection) -> Void)?
|
||||
var disconnectionHandler: ((Connection) -> Void)?
|
||||
|
||||
private var notificationConnections = [ALTDevice: NotificationConnection]()
|
||||
private let queue = DispatchQueue(label: "WiredConnectionHandler", autoreleaseFrequency: .workItem, target: .global(qos: .utility))
|
||||
|
||||
func startListening()
|
||||
{
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(WiredConnectionHandler.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(WiredConnectionHandler.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil)
|
||||
}
|
||||
|
||||
func stopListening()
|
||||
{
|
||||
NotificationCenter.default.removeObserver(self, name: .deviceManagerDeviceDidConnect, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: .deviceManagerDeviceDidDisconnect, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension WiredConnectionHandler
|
||||
{
|
||||
func startNotificationConnection(to device: ALTDevice)
|
||||
{
|
||||
self.queue.async {
|
||||
ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in
|
||||
guard let connection = connection else { return }
|
||||
|
||||
let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest]
|
||||
connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in
|
||||
guard success else { return }
|
||||
|
||||
self.queue.async {
|
||||
connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in
|
||||
guard let self = self, let connection = connection else { return }
|
||||
self.handle(notification, for: connection)
|
||||
}
|
||||
|
||||
self.notificationConnections[device] = connection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopNotificationConnection(to device: ALTDevice)
|
||||
{
|
||||
self.queue.async {
|
||||
guard let connection = self.notificationConnections[device] else { return }
|
||||
connection.disconnect()
|
||||
|
||||
self.notificationConnections[device] = nil
|
||||
}
|
||||
}
|
||||
|
||||
func handle(_ notification: CFNotificationName, for connection: NotificationConnection)
|
||||
{
|
||||
switch notification
|
||||
{
|
||||
case .wiredServerConnectionAvailableRequest:
|
||||
connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Error sending wired server connection response.", error)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Sent wired server connection available response!")
|
||||
}
|
||||
}
|
||||
|
||||
case .wiredServerConnectionStartRequest:
|
||||
ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in
|
||||
if let wiredConnection = wiredConnection
|
||||
{
|
||||
print("Started wired server connection!")
|
||||
self.connectionHandler?(wiredConnection)
|
||||
|
||||
var observation: NSKeyValueObservation?
|
||||
observation = wiredConnection.observe(\.isConnected) { [weak self] (connection, change) in
|
||||
guard !connection.isConnected else { return }
|
||||
self?.disconnectionHandler?(connection)
|
||||
|
||||
observation?.invalidate()
|
||||
}
|
||||
}
|
||||
else if let error = error
|
||||
{
|
||||
print("Error starting wired server connection.", error)
|
||||
}
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension WiredConnectionHandler
|
||||
{
|
||||
@objc func deviceDidConnect(_ notification: Notification)
|
||||
{
|
||||
guard let device = notification.object as? ALTDevice else { return }
|
||||
self.startNotificationConnection(to: device)
|
||||
}
|
||||
|
||||
@objc func deviceDidDisconnect(_ notification: Notification)
|
||||
{
|
||||
guard let device = notification.object as? ALTDevice else { return }
|
||||
self.stopNotificationConnection(to: device)
|
||||
}
|
||||
}
|
||||
153
AltServer/Connections/WirelessConnectionHandler.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// WirelessConnectionHandler.swift
|
||||
// AltKit
|
||||
//
|
||||
// Created by Riley Testut on 6/1/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
extension WirelessConnectionHandler
|
||||
{
|
||||
public enum State
|
||||
{
|
||||
case notRunning
|
||||
case connecting
|
||||
case running(NWListener.Service)
|
||||
case failed(Swift.Error)
|
||||
}
|
||||
}
|
||||
|
||||
public class WirelessConnectionHandler: ConnectionHandler
|
||||
{
|
||||
public var connectionHandler: ((Connection) -> Void)?
|
||||
public var disconnectionHandler: ((Connection) -> Void)?
|
||||
|
||||
public var stateUpdateHandler: ((State) -> Void)?
|
||||
|
||||
public private(set) var state: State = .notRunning {
|
||||
didSet {
|
||||
self.stateUpdateHandler?(self.state)
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var listener = self.makeListener()
|
||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.WirelessConnectionListener", qos: .utility)
|
||||
|
||||
public func startListening()
|
||||
{
|
||||
switch self.state
|
||||
{
|
||||
case .notRunning, .failed: self.listener.start(queue: self.dispatchQueue)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
public func stopListening()
|
||||
{
|
||||
switch self.state
|
||||
{
|
||||
case .running: self.listener.cancel()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension WirelessConnectionHandler
|
||||
{
|
||||
func makeListener() -> NWListener
|
||||
{
|
||||
let listener = try! NWListener(using: .tcp)
|
||||
|
||||
let service: NWListener.Service
|
||||
|
||||
if let serverID = UserDefaults.standard.serverID?.data(using: .utf8)
|
||||
{
|
||||
let txtDictionary = ["serverID": serverID]
|
||||
let txtData = NetService.data(fromTXTRecord: txtDictionary)
|
||||
|
||||
service = NWListener.Service(name: nil, type: ALTServerServiceType, domain: nil, txtRecord: txtData)
|
||||
}
|
||||
else
|
||||
{
|
||||
service = NWListener.Service(type: ALTServerServiceType)
|
||||
}
|
||||
|
||||
listener.service = service
|
||||
|
||||
listener.serviceRegistrationUpdateHandler = { (serviceChange) in
|
||||
switch serviceChange
|
||||
{
|
||||
case .add(.service(let name, let type, let domain, _)):
|
||||
let service = NWListener.Service(name: name, type: type, domain: domain, txtRecord: nil)
|
||||
self.state = .running(service)
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
listener.stateUpdateHandler = { (state) in
|
||||
switch state
|
||||
{
|
||||
case .ready: break
|
||||
case .waiting, .setup: self.state = .connecting
|
||||
case .cancelled: self.state = .notRunning
|
||||
case .failed(let error): self.state = .failed(error)
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
listener.newConnectionHandler = { [weak self] (connection) in
|
||||
self?.prepare(connection)
|
||||
}
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
func prepare(_ nwConnection: NWConnection)
|
||||
{
|
||||
print("Preparing:", nwConnection)
|
||||
|
||||
// Use same instance for all callbacks.
|
||||
let connection = NetworkConnection(nwConnection)
|
||||
|
||||
nwConnection.stateUpdateHandler = { [weak self] (state) in
|
||||
switch state
|
||||
{
|
||||
case .setup, .preparing: break
|
||||
|
||||
case .ready:
|
||||
print("Connected to client:", connection)
|
||||
self?.connectionHandler?(connection)
|
||||
|
||||
case .waiting:
|
||||
print("Waiting for connection...")
|
||||
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(nwConnection.endpoint).", error)
|
||||
self?.disconnect(connection)
|
||||
|
||||
case .cancelled:
|
||||
self?.disconnect(connection)
|
||||
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
nwConnection.start(queue: self.dispatchQueue)
|
||||
}
|
||||
|
||||
func disconnect(_ connection: Connection)
|
||||
{
|
||||
connection.disconnect()
|
||||
|
||||
self.disconnectionHandler?(connection)
|
||||
|
||||
if let networkConnection = connection as? NetworkConnection
|
||||
{
|
||||
networkConnection.nwConnection.stateUpdateHandler = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
328
AltServer/DeveloperDiskManager.swift
Normal file
@@ -0,0 +1,328 @@
|
||||
//
|
||||
// DeveloperDiskManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 2/19/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
|
||||
typealias DeveloperDiskError = DeveloperDiskErrorCode.Error
|
||||
enum DeveloperDiskErrorCode: Int, ALTErrorEnum
|
||||
{
|
||||
case unknownDownloadURL
|
||||
case unsupportedOperatingSystem
|
||||
case downloadedDiskNotFound
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self
|
||||
{
|
||||
case .unknownDownloadURL: return NSLocalizedString("The URL to download the Developer disk image could not be determined.", comment: "")
|
||||
case .unsupportedOperatingSystem: return NSLocalizedString("The device's operating system does not support installing Developer disk images.", comment: "")
|
||||
case .downloadedDiskNotFound: return NSLocalizedString("DeveloperDiskImage.dmg and its signature could not be found in the downloaded archive.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension URL
|
||||
{
|
||||
#if STAGING
|
||||
static let developerDiskDownloadURLs = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altserver/developerdisks.json")!
|
||||
#else
|
||||
static let developerDiskDownloadURLs = URL(string: "https://cdn.altstore.io/file/altstore/altserver/developerdisks.json")!
|
||||
#endif
|
||||
}
|
||||
|
||||
private extension DeveloperDiskManager
|
||||
{
|
||||
struct FetchURLsResponse: Decodable
|
||||
{
|
||||
struct Disks: Decodable
|
||||
{
|
||||
var iOS: [String: DeveloperDiskURL]?
|
||||
var tvOS: [String: DeveloperDiskURL]?
|
||||
}
|
||||
|
||||
var version: Int
|
||||
var disks: Disks
|
||||
}
|
||||
|
||||
enum DeveloperDiskURL: Decodable
|
||||
{
|
||||
case archive(URL)
|
||||
case separate(diskURL: URL, signatureURL: URL)
|
||||
|
||||
private enum CodingKeys: CodingKey
|
||||
{
|
||||
case archive
|
||||
case disk
|
||||
case signature
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if container.contains(.archive)
|
||||
{
|
||||
let archiveURL = try container.decode(URL.self, forKey: .archive)
|
||||
self = .archive(archiveURL)
|
||||
}
|
||||
else
|
||||
{
|
||||
let diskURL = try container.decode(URL.self, forKey: .disk)
|
||||
let signatureURL = try container.decode(URL.self, forKey: .signature)
|
||||
self = .separate(diskURL: diskURL, signatureURL: signatureURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DeveloperDiskManager
|
||||
{
|
||||
private let session = URLSession(configuration: .ephemeral)
|
||||
|
||||
func downloadDeveloperDisk(for device: ALTDevice, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let osName = device.type.osName else { throw DeveloperDiskError(.unsupportedOperatingSystem) }
|
||||
|
||||
let osKeyPath: KeyPath<FetchURLsResponse.Disks, [String: DeveloperDiskURL]?>
|
||||
switch device.type
|
||||
{
|
||||
case .iphone, .ipad: osKeyPath = \FetchURLsResponse.Disks.iOS
|
||||
case .appletv: osKeyPath = \FetchURLsResponse.Disks.tvOS
|
||||
default: throw DeveloperDiskError(.unsupportedOperatingSystem)
|
||||
}
|
||||
|
||||
var osVersion = device.osVersion
|
||||
osVersion.patchVersion = 0 // Patch is irrelevant for developer disks
|
||||
|
||||
let osDirectoryURL = FileManager.default.developerDisksDirectory.appendingPathComponent(osName)
|
||||
let developerDiskDirectoryURL = osDirectoryURL.appendingPathComponent(osVersion.stringValue, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: developerDiskDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let developerDiskURL = developerDiskDirectoryURL.appendingPathComponent("DeveloperDiskImage.dmg")
|
||||
let developerDiskSignatureURL = developerDiskDirectoryURL.appendingPathComponent("DeveloperDiskImage.dmg.signature")
|
||||
|
||||
let isCachedDiskCompatible = self.isDeveloperDiskCompatible(with: device)
|
||||
if isCachedDiskCompatible && FileManager.default.fileExists(atPath: developerDiskURL.path) && FileManager.default.fileExists(atPath: developerDiskSignatureURL.path)
|
||||
{
|
||||
// The developer disk is cached and we've confirmed it works, so re-use it.
|
||||
return completionHandler(.success((developerDiskURL, developerDiskSignatureURL)))
|
||||
}
|
||||
|
||||
func finish(_ result: Result<(URL, URL), Error>)
|
||||
{
|
||||
do
|
||||
{
|
||||
let (diskFileURL, signatureFileURL) = try result.get()
|
||||
|
||||
if FileManager.default.fileExists(atPath: developerDiskURL.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: developerDiskURL)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: developerDiskSignatureURL.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: developerDiskSignatureURL)
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: diskFileURL, to: developerDiskURL)
|
||||
try FileManager.default.copyItem(at: signatureFileURL, to: developerDiskSignatureURL)
|
||||
|
||||
completionHandler(.success((developerDiskURL, developerDiskSignatureURL)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
self.fetchDeveloperDiskURLs { (result) in
|
||||
do
|
||||
{
|
||||
let developerDiskURLs = try result.get()
|
||||
guard let diskURL = developerDiskURLs[keyPath: osKeyPath]?[osVersion.stringValue] else { throw DeveloperDiskError(.unknownDownloadURL) }
|
||||
|
||||
switch diskURL
|
||||
{
|
||||
case .archive(let archiveURL): self.downloadDiskArchive(from: archiveURL, completionHandler: finish(_:))
|
||||
case .separate(let diskURL, let signatureURL): self.downloadDisk(from: diskURL, signatureURL: signatureURL, completionHandler: finish(_:))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func setDeveloperDiskCompatible(_ isCompatible: Bool, with device: ALTDevice)
|
||||
{
|
||||
guard let id = self.developerDiskCompatibilityID(for: device) else { return }
|
||||
UserDefaults.standard.set(isCompatible, forKey: id)
|
||||
}
|
||||
|
||||
func isDeveloperDiskCompatible(with device: ALTDevice) -> Bool
|
||||
{
|
||||
guard let id = self.developerDiskCompatibilityID(for: device) else { return false }
|
||||
|
||||
let isCompatible = UserDefaults.standard.bool(forKey: id)
|
||||
return isCompatible
|
||||
}
|
||||
}
|
||||
|
||||
private extension DeveloperDiskManager
|
||||
{
|
||||
func developerDiskCompatibilityID(for device: ALTDevice) -> String?
|
||||
{
|
||||
guard let osName = device.type.osName else { return nil }
|
||||
|
||||
var osVersion = device.osVersion
|
||||
osVersion.patchVersion = 0 // Patch is irrelevant for developer disks
|
||||
|
||||
let id = ["ALTDeveloperDiskCompatible", osName, device.osVersion.stringValue].joined(separator: "_")
|
||||
return id
|
||||
}
|
||||
|
||||
func fetchDeveloperDiskURLs(completionHandler: @escaping (Result<FetchURLsResponse.Disks, Error>) -> Void)
|
||||
{
|
||||
let dataTask = self.session.dataTask(with: .developerDiskDownloadURLs) { (data, response, error) in
|
||||
do
|
||||
{
|
||||
guard let data = data else { throw error! }
|
||||
|
||||
let response = try Foundation.JSONDecoder().decode(FetchURLsResponse.self, from: data)
|
||||
completionHandler(.success(response.disks))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
dataTask.resume()
|
||||
}
|
||||
|
||||
func downloadDiskArchive(from url: URL, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
|
||||
{
|
||||
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
guard let fileURL = fileURL else { throw error! }
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
|
||||
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
|
||||
|
||||
try FileManager.default.unzipArchive(at: fileURL, toDirectory: temporaryDirectory)
|
||||
|
||||
guard let enumerator = FileManager.default.enumerator(at: temporaryDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
|
||||
throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: temporaryDirectory])
|
||||
}
|
||||
|
||||
var tempDiskFileURL: URL?
|
||||
var tempSignatureFileURL: URL?
|
||||
|
||||
for case let fileURL as URL in enumerator
|
||||
{
|
||||
switch fileURL.pathExtension.lowercased()
|
||||
{
|
||||
case "dmg": tempDiskFileURL = fileURL
|
||||
case "signature": tempSignatureFileURL = fileURL
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
guard let diskFileURL = tempDiskFileURL, let signatureFileURL = tempSignatureFileURL else { throw DeveloperDiskError(.downloadedDiskNotFound) }
|
||||
|
||||
completionHandler(.success((diskFileURL, signatureFileURL)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func downloadDisk(from diskURL: URL, signatureURL: URL, completionHandler: @escaping (Result<(URL, URL), Error>) -> Void)
|
||||
{
|
||||
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
|
||||
catch { return completionHandler(.failure(error)) }
|
||||
|
||||
var diskFileURL: URL?
|
||||
var signatureFileURL: URL?
|
||||
|
||||
var downloadError: Error?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
dispatchGroup.enter()
|
||||
dispatchGroup.enter()
|
||||
|
||||
let diskDownloadTask = URLSession.shared.downloadTask(with: diskURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
guard let fileURL = fileURL else { throw error! }
|
||||
|
||||
let destinationURL = temporaryDirectory.appendingPathComponent("DeveloperDiskImage.dmg")
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||
|
||||
diskFileURL = destinationURL
|
||||
}
|
||||
catch
|
||||
{
|
||||
downloadError = error
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let signatureDownloadTask = URLSession.shared.downloadTask(with: signatureURL) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
guard let fileURL = fileURL else { throw error! }
|
||||
|
||||
let destinationURL = temporaryDirectory.appendingPathComponent("DeveloperDiskImage.dmg.signature")
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||
|
||||
signatureFileURL = destinationURL
|
||||
}
|
||||
catch
|
||||
{
|
||||
downloadError = error
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
diskDownloadTask.resume()
|
||||
signatureDownloadTask.resume()
|
||||
|
||||
dispatchGroup.notify(queue: .global(qos: .userInitiated)) {
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||
}
|
||||
|
||||
guard let diskFileURL = diskFileURL, let signatureFileURL = signatureFileURL else {
|
||||
return completionHandler(.failure(downloadError ?? DeveloperDiskError(.downloadedDiskNotFound)))
|
||||
}
|
||||
|
||||
completionHandler(.success((diskFileURL, signatureFileURL)))
|
||||
}
|
||||
}
|
||||
}
|
||||
1097
AltServer/Devices/ALTDeviceManager+Installation.swift
Normal file
56
AltServer/Devices/ALTDeviceManager.h
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// ALTDeviceManager.h
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/24/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "AltSign.h"
|
||||
|
||||
@class ALTWiredConnection;
|
||||
@class ALTNotificationConnection;
|
||||
@class ALTDebugConnection;
|
||||
|
||||
@class ALTInstalledApp;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidConnect);
|
||||
extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification NS_SWIFT_NAME(deviceManagerDeviceDidDisconnect);
|
||||
|
||||
@interface ALTDeviceManager : NSObject
|
||||
|
||||
@property (class, nonatomic, readonly) ALTDeviceManager *sharedManager;
|
||||
|
||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *connectedDevices;
|
||||
@property (nonatomic, readonly) NSArray<ALTDevice *> *availableDevices;
|
||||
|
||||
- (void)start;
|
||||
|
||||
/* App Installation */
|
||||
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
- (void)removeAppForBundleIdentifier:(NSString *)bundleIdentifier fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
/* Provisioning Profiles */
|
||||
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet<NSString *> *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
/* Developer Disk Image */
|
||||
- (void)isDeveloperDiskImageMountedForDevice:(ALTDevice *)device
|
||||
completionHandler:(void (^)(BOOL isMounted, NSError *_Nullable error))completionHandler;
|
||||
- (void)installDeveloperDiskImageAtURL:(NSURL *)diskURL signatureURL:(NSURL *)signatureURL toDevice:(ALTDevice *)device
|
||||
completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
/* Apps */
|
||||
- (void)fetchInstalledAppsOnDevice:(ALTDevice *)altDevice completionHandler:(void (^)(NSSet<ALTInstalledApp *> *_Nullable installedApps, NSError *_Nullable error))completionHandler;
|
||||
|
||||
/* Connections */
|
||||
- (void)startWiredConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTWiredConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
||||
- (void)startNotificationConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTNotificationConnection *_Nullable connection, NSError *_Nullable error))completionHandler;
|
||||
- (void)startDebugConnectionToDevice:(ALTDevice *)device completionHandler:(void (^)(ALTDebugConnection *_Nullable connection, NSError * _Nullable error))completionHandler;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
1812
AltServer/Devices/ALTDeviceManager.mm
Normal file
63
AltServer/ErrorDetailsViewController.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// ErrorDetailsViewController.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 10/4/22.
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
class ErrorDetailsViewController: NSViewController
|
||||
{
|
||||
var error: NSError? {
|
||||
didSet {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet private var errorCodeLabel: NSTextField!
|
||||
@IBOutlet private var detailedDescriptionLabel: NSTextField!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.detailedDescriptionLabel.preferredMaxLayoutWidth = 800
|
||||
}
|
||||
}
|
||||
|
||||
private extension ErrorDetailsViewController
|
||||
{
|
||||
func update()
|
||||
{
|
||||
if !self.isViewLoaded
|
||||
{
|
||||
self.loadView()
|
||||
}
|
||||
|
||||
guard let error = self.error else { return }
|
||||
|
||||
self.errorCodeLabel.stringValue = error.localizedErrorCode
|
||||
|
||||
let font = self.detailedDescriptionLabel.font ?? NSFont.systemFont(ofSize: 12)
|
||||
let detailedDescription = error.formattedDetailedDescription(with: font)
|
||||
self.detailedDescriptionLabel.attributedStringValue = detailedDescription
|
||||
}
|
||||
|
||||
@IBAction func searchFAQ(_ sender: NSButton)
|
||||
{
|
||||
guard let error else { return }
|
||||
|
||||
let baseURL = URL(string: "https://faq.altstore.io/getting-started/error-codes")!
|
||||
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
||||
|
||||
let nsError = error as NSError
|
||||
let query = [nsError.domain, "\(error.displayCode)"].joined(separator: "+")
|
||||
components.queryItems = [URLQueryItem(name: "q", value: query)]
|
||||
|
||||
let url = components.url ?? baseURL
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
29
AltServer/Extensions/FileManager+URLs.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// FileManager+URLs.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 2/23/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager
|
||||
{
|
||||
var altserverDirectory: URL {
|
||||
let applicationSupportDirectoryURL = self.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
|
||||
let altserverDirectoryURL = applicationSupportDirectoryURL.appendingPathComponent("com.rileytestut.AltServer")
|
||||
return altserverDirectoryURL
|
||||
}
|
||||
|
||||
var certificatesDirectory: URL {
|
||||
let certificatesDirectoryURL = self.altserverDirectory.appendingPathComponent("Certificates")
|
||||
return certificatesDirectoryURL
|
||||
}
|
||||
|
||||
var developerDisksDirectory: URL {
|
||||
let developerDisksDirectoryURL = self.altserverDirectory.appendingPathComponent("DeveloperDiskImages")
|
||||
return developerDisksDirectoryURL
|
||||
}
|
||||
}
|
||||
38
AltServer/Extensions/UserDefaults+AltServer.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// UserDefaults+AltServer.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 7/31/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension UserDefaults
|
||||
{
|
||||
var serverID: String? {
|
||||
get {
|
||||
return self.string(forKey: "serverID")
|
||||
}
|
||||
set {
|
||||
self.set(newValue, forKey: "serverID")
|
||||
}
|
||||
}
|
||||
|
||||
var didPresentInitialNotification: Bool {
|
||||
get {
|
||||
return self.bool(forKey: "didPresentInitialNotification")
|
||||
}
|
||||
set {
|
||||
self.set(newValue, forKey: "didPresentInitialNotification")
|
||||
}
|
||||
}
|
||||
|
||||
func registerDefaults()
|
||||
{
|
||||
if self.serverID == nil
|
||||
{
|
||||
self.serverID = UUID().uuidString
|
||||
}
|
||||
}
|
||||
}
|
||||
36
AltServer/Info.plist
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://altstore.io/altserver/sparkle-macos.xml</string>
|
||||
</dict>
|
||||
</plist>
|
||||
29
AltServer/InstalledApp.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// InstalledApp.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 5/25/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc(ALTInstalledApp) @objcMembers
|
||||
class InstalledApp: NSObject, MenuDisplayable
|
||||
{
|
||||
let name: String
|
||||
let bundleIdentifier: String
|
||||
let executableName: String
|
||||
|
||||
init?(dictionary: [String: Any])
|
||||
{
|
||||
guard let name = dictionary[kCFBundleNameKey as String] as? String,
|
||||
let bundleIdentifier = dictionary[kCFBundleIdentifierKey as String] as? String,
|
||||
let executableName = dictionary[kCFBundleExecutableKey as String] as? String
|
||||
else { return nil }
|
||||
|
||||
self.name = name
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
self.executableName = executableName
|
||||
}
|
||||
}
|
||||
115
AltServer/MenuController.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// MenuController.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 3/3/21.
|
||||
// Copyright © 2021 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
protocol MenuDisplayable
|
||||
{
|
||||
var name: String { get }
|
||||
}
|
||||
|
||||
class MenuController<T: MenuDisplayable & Hashable>: NSObject, NSMenuDelegate
|
||||
{
|
||||
let menu: NSMenu
|
||||
|
||||
var items: [T] {
|
||||
didSet {
|
||||
self.submenus.removeAll()
|
||||
self.updateMenu()
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder: String? {
|
||||
didSet {
|
||||
self.updateMenu()
|
||||
}
|
||||
}
|
||||
|
||||
var action: ((T) -> Void)?
|
||||
|
||||
var submenuHandler: ((T) -> NSMenu)?
|
||||
private var submenus = [T: NSMenu]()
|
||||
|
||||
init(menu: NSMenu, items: [T])
|
||||
{
|
||||
self.menu = menu
|
||||
self.items = items
|
||||
|
||||
super.init()
|
||||
|
||||
self.menu.delegate = self
|
||||
}
|
||||
|
||||
@objc
|
||||
private func performAction(_ menuItem: NSMenuItem)
|
||||
{
|
||||
guard case let index = self.menu.index(of: menuItem), index != -1 else { return }
|
||||
|
||||
let item = self.items[index]
|
||||
self.action?(item)
|
||||
}
|
||||
|
||||
@objc
|
||||
func numberOfItems(in menu: NSMenu) -> Int
|
||||
{
|
||||
let numberOfItems = (self.items.isEmpty && self.placeholder != nil) ? 1 : self.items.count
|
||||
return numberOfItems
|
||||
}
|
||||
|
||||
@objc
|
||||
func menu(_ menu: NSMenu, update menuItem: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool
|
||||
{
|
||||
if let text = self.placeholder, self.items.isEmpty
|
||||
{
|
||||
menuItem.title = text
|
||||
menuItem.isEnabled = false
|
||||
menuItem.target = nil
|
||||
menuItem.action = nil
|
||||
}
|
||||
else
|
||||
{
|
||||
let item = self.items[index]
|
||||
|
||||
menuItem.title = item.name
|
||||
menuItem.isEnabled = true
|
||||
menuItem.target = self
|
||||
menuItem.action = #selector(MenuController.performAction(_:))
|
||||
menuItem.tag = index
|
||||
|
||||
if let submenu = self.submenus[item] ?? self.submenuHandler?(item)
|
||||
{
|
||||
menuItem.submenu = submenu
|
||||
|
||||
// Cache submenu to prevent duplicate calls to submenuHandler.
|
||||
self.submenus[item] = submenu
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private extension MenuController
|
||||
{
|
||||
func updateMenu()
|
||||
{
|
||||
self.menu.removeAllItems()
|
||||
|
||||
let numberOfItems = self.numberOfItems(in: self.menu)
|
||||
for index in 0 ..< numberOfItems
|
||||
{
|
||||
let menuItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
|
||||
guard self.menu(self.menu, update: menuItem, at: index, shouldCancel: false) else { break }
|
||||
|
||||
self.menu.addItem(menuItem)
|
||||
}
|
||||
|
||||
self.menu.update()
|
||||
}
|
||||
}
|
||||
419
AltServer/Plugin/PluginManager.swift
Normal file
@@ -0,0 +1,419 @@
|
||||
//
|
||||
// PluginManager.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut on 9/16/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import CryptoKit
|
||||
|
||||
import STPrivilegedTask
|
||||
|
||||
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
|
||||
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
|
||||
|
||||
extension PluginError
|
||||
{
|
||||
enum Code: Int, ALTErrorCode
|
||||
{
|
||||
typealias Error = PluginError
|
||||
|
||||
case cancelled
|
||||
case unknown
|
||||
case notFound
|
||||
case mismatchedHash
|
||||
case taskError
|
||||
case taskErrorCode
|
||||
}
|
||||
|
||||
static let cancelled = PluginError(code: .cancelled)
|
||||
static let notFound = PluginError(code: .notFound)
|
||||
|
||||
static func unknown(file: String = #fileID, line: UInt = #line) -> PluginError { PluginError(code: .unknown, sourceFile: file, sourceLine: line) }
|
||||
static func mismatchedHash(hash: String, expectedHash: String) -> PluginError { PluginError(code: .mismatchedHash, hash: hash, expectedHash: expectedHash) }
|
||||
static func taskError(output: String) -> PluginError { PluginError(code: .taskError, taskErrorOutput: output) }
|
||||
static func taskErrorCode(_ code: Int) -> PluginError { PluginError(code: .taskErrorCode, taskErrorCode: code) }
|
||||
}
|
||||
|
||||
struct PluginError: ALTLocalizedError
|
||||
{
|
||||
let code: Code
|
||||
|
||||
var errorTitle: String?
|
||||
var errorFailure: String?
|
||||
var sourceFile: String?
|
||||
var sourceLine: UInt?
|
||||
|
||||
var hash: String?
|
||||
var expectedHash: String?
|
||||
var taskErrorOutput: String?
|
||||
var taskErrorCode: Int?
|
||||
|
||||
var errorFailureReason: String {
|
||||
switch self.code
|
||||
{
|
||||
case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
|
||||
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "")
|
||||
case .notFound: return NSLocalizedString("The Mail plug-in does not exist at the requested URL.", comment: "")
|
||||
case .mismatchedHash:
|
||||
let baseMessage = NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.", comment: "")
|
||||
guard let hash = self.hash, let expectedHash = self.expectedHash else { return baseMessage }
|
||||
|
||||
let additionalInfo = String(format: NSLocalizedString("Hash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash)
|
||||
return baseMessage + "\n\n" + additionalInfo
|
||||
|
||||
case .taskError:
|
||||
if let output = self.taskErrorOutput
|
||||
{
|
||||
return output
|
||||
}
|
||||
|
||||
// Use .taskErrorCode base message as fallback.
|
||||
fallthrough
|
||||
|
||||
case .taskErrorCode:
|
||||
let baseMessage = NSLocalizedString("There was an error installing the Mail plug-in.", comment: "")
|
||||
guard let errorCode = self.taskErrorCode else { return baseMessage }
|
||||
|
||||
let additionalInfo = String(format: NSLocalizedString("(Error Code: %@)", comment: ""), NSNumber(value: errorCode))
|
||||
return baseMessage + " " + additionalInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension URL
|
||||
{
|
||||
#if STAGING
|
||||
static let altPluginUpdateURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altserver/altplugin/altplugin.json")!
|
||||
#else
|
||||
static let altPluginUpdateURL = URL(string: "https://cdn.altstore.io/file/altstore/altserver/altplugin/altplugin.json")!
|
||||
#endif
|
||||
}
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
private let session = URLSession(configuration: .ephemeral)
|
||||
private var latestPluginVersion: PluginVersion?
|
||||
|
||||
var isMailPluginInstalled: Bool {
|
||||
let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
return isMailPluginInstalled
|
||||
}
|
||||
|
||||
func isUpdateAvailable(completionHandler: @escaping (Result<Bool, Error>) -> Void)
|
||||
{
|
||||
self.isUpdateAvailable(useCache: false, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func isUpdateAvailable(useCache: Bool, completionHandler: @escaping (Result<Bool, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
// If Mail plug-in is not yet installed, then there is no update available.
|
||||
var isDirectory: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: pluginURL.path, isDirectory: &isDirectory), isDirectory.boolValue else { return completionHandler(.success(false)) }
|
||||
|
||||
// Load Info.plist from disk because Bundle.infoDictionary is cached by system.
|
||||
let infoDictionaryURL = pluginURL.appendingPathComponent("Contents/Info.plist")
|
||||
guard let infoDictionary = NSDictionary(contentsOf: infoDictionaryURL) as? [String: Any],
|
||||
let localVersion = infoDictionary["CFBundleShortVersionString"] as? String
|
||||
else { throw CocoaError(.fileReadCorruptFile, userInfo: [NSURLErrorKey: infoDictionaryURL]) }
|
||||
|
||||
if let pluginVersion = self.latestPluginVersion, useCache
|
||||
{
|
||||
let isUpdateAvailable = (localVersion != pluginVersion.version)
|
||||
completionHandler(.success(isUpdateAvailable))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.fetchLatestPluginVersion(useCache: useCache) { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let pluginVersion):
|
||||
let isUpdateAvailable = (localVersion != pluginVersion.version)
|
||||
completionHandler(.success(isUpdateAvailable))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PluginManager
|
||||
{
|
||||
func installMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.isUpdateAvailable(useCache: true) { result in
|
||||
DispatchQueue.main.async {
|
||||
do
|
||||
{
|
||||
let isUpdateAvailable = try result.get()
|
||||
|
||||
let alert = NSAlert()
|
||||
if isUpdateAvailable
|
||||
{
|
||||
alert.messageText = NSLocalizedString("Update Mail Plug-in", comment: "")
|
||||
alert.informativeText = NSLocalizedString("An update is available for AltServer's Mail plug-in. Please update the plug-in now in order to keep using AltStore.", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Update Plug-in", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
}
|
||||
else
|
||||
{
|
||||
alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "")
|
||||
alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
}
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { throw PluginError.cancelled }
|
||||
|
||||
self.downloadPlugin { (result) in
|
||||
do
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
|
||||
// Ensure plug-in directory exists.
|
||||
let authorization = try self.runAndKeepAuthorization("mkdir", arguments: ["-p", pluginDirectoryURL.path])
|
||||
|
||||
// Create temporary directory.
|
||||
let temporaryDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
defer { try? FileManager.default.removeItem(at: temporaryDirectoryURL) }
|
||||
|
||||
// Unzip AltPlugin to temporary directory.
|
||||
try self.runAndKeepAuthorization("unzip", arguments: ["-o", fileURL.path, "-d", temporaryDirectoryURL.path], authorization: authorization)
|
||||
|
||||
if FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
{
|
||||
// Delete existing Mail plug-in.
|
||||
try self.runAndKeepAuthorization("rm", arguments: ["-rf", pluginURL.path], authorization: authorization)
|
||||
}
|
||||
|
||||
// Copy AltPlugin to Mail plug-ins directory.
|
||||
// Must be separate step than unzip to prevent macOS from considering plug-in corrupted.
|
||||
let unzippedPluginURL = temporaryDirectoryURL.appendingPathComponent(pluginURL.lastPathComponent)
|
||||
try self.runAndKeepAuthorization("cp", arguments: ["-R", unzippedPluginURL.path, pluginDirectoryURL.path], authorization: authorization)
|
||||
|
||||
guard self.isMailPluginInstalled else { throw PluginError.unknown() }
|
||||
|
||||
// Enable Mail plug-in preferences.
|
||||
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization)
|
||||
|
||||
print("Finished installing Mail plug-in!")
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
|
||||
alert.informativeText = NSLocalizedString("Are you sure you want to uninstall the AltServer Mail plug-in? You will no longer be able to install or refresh apps with AltStore.", comment: "")
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Uninstall Plug-in", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
|
||||
|
||||
let response = alert.runModal()
|
||||
guard response == .alertFirstButtonReturn else { return completionHandler(.failure(PluginError.cancelled)) }
|
||||
|
||||
DispatchQueue.global().async {
|
||||
do
|
||||
{
|
||||
if FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
{
|
||||
// Delete Mail plug-in from privileged directory.
|
||||
try self.run("rm", arguments: ["-rf", pluginURL.path])
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PluginManager
|
||||
{
|
||||
func fetchLatestPluginVersion(useCache: Bool, completionHandler: @escaping (Result<PluginVersion, Error>) -> Void)
|
||||
{
|
||||
if let pluginVersion = self.latestPluginVersion, useCache
|
||||
{
|
||||
return completionHandler(.success(pluginVersion))
|
||||
}
|
||||
|
||||
guard #available(macOS 11, *) else {
|
||||
// macOS versions prior to 11.0 require Mail plug-ins be *unsigned*,
|
||||
// so we hardcode these versions to use the unsigned AltPlugin v1.0.
|
||||
return completionHandler(.success(.v1_0))
|
||||
}
|
||||
|
||||
let dataTask = self.session.dataTask(with: .altPluginUpdateURL) { (data, response, error) in
|
||||
do
|
||||
{
|
||||
if let response = response as? HTTPURLResponse
|
||||
{
|
||||
guard response.statusCode != 404 else { return completionHandler(.failure(PluginError.notFound)) }
|
||||
}
|
||||
|
||||
guard let data = data else { throw error! }
|
||||
|
||||
let response = try JSONDecoder().decode(PluginVersionResponse.self, from: data)
|
||||
completionHandler(.success(response.pluginVersion))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
dataTask.resume()
|
||||
}
|
||||
|
||||
func downloadPlugin(completion: @escaping (Result<URL, Error>) -> Void)
|
||||
{
|
||||
self.fetchLatestPluginVersion(useCache: true) { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completion(.failure(error))
|
||||
case .success(let pluginVersion):
|
||||
|
||||
func finish(_ result: Result<URL, Error>)
|
||||
{
|
||||
do
|
||||
{
|
||||
let fileURL = try result.get()
|
||||
|
||||
if #available(OSX 10.15, *)
|
||||
{
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let sha256Hash = SHA256.hash(data: data)
|
||||
let hashString = sha256Hash.compactMap { String(format: "%02x", $0) }.joined()
|
||||
|
||||
print("Comparing Mail plug-in hash (\(hashString)) against expected hash (\(pluginVersion.sha256Hash))...")
|
||||
guard hashString == pluginVersion.sha256Hash else { throw PluginError.mismatchedHash(hash: hashString, expectedHash: pluginVersion.sha256Hash) }
|
||||
}
|
||||
|
||||
completion(.success(fileURL))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
if pluginVersion.url.isFileURL
|
||||
{
|
||||
finish(.success(pluginVersion.url))
|
||||
}
|
||||
else
|
||||
{
|
||||
let downloadTask = URLSession.shared.downloadTask(with: pluginVersion.url) { (fileURL, response, error) in
|
||||
if let response = response as? HTTPURLResponse
|
||||
{
|
||||
guard response.statusCode != 404 else { return finish(.failure(PluginError.notFound)) }
|
||||
}
|
||||
|
||||
let result = Result(fileURL, error)
|
||||
finish(result)
|
||||
|
||||
if let fileURL = fileURL
|
||||
{
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws
|
||||
{
|
||||
_ = try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: true)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func runAndKeepAuthorization(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef
|
||||
{
|
||||
return try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: false)
|
||||
}
|
||||
|
||||
func _run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil, freeAuthorization: Bool) throws -> AuthorizationRef
|
||||
{
|
||||
var launchPath = "/usr/bin/" + program
|
||||
if !FileManager.default.fileExists(atPath: launchPath)
|
||||
{
|
||||
launchPath = "/bin/" + program
|
||||
}
|
||||
|
||||
print("Running program:", launchPath)
|
||||
|
||||
let task = STPrivilegedTask()
|
||||
task.launchPath = launchPath
|
||||
task.arguments = arguments
|
||||
task.freeAuthorizationWhenDone = freeAuthorization
|
||||
|
||||
let errorCode: OSStatus
|
||||
|
||||
if let authorization = authorization
|
||||
{
|
||||
errorCode = task.launch(withAuthorization: authorization)
|
||||
}
|
||||
else
|
||||
{
|
||||
errorCode = task.launch()
|
||||
}
|
||||
|
||||
guard errorCode == 0 else { throw PluginError.taskErrorCode(Int(errorCode)) }
|
||||
|
||||
task.waitUntilExit()
|
||||
|
||||
print("Exit code:", task.terminationStatus)
|
||||
|
||||
guard task.terminationStatus == 0 else {
|
||||
let outputData = task.outputFileHandle.readDataToEndOfFile()
|
||||
|
||||
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
|
||||
{
|
||||
throw PluginError.taskError(output: outputString)
|
||||
}
|
||||
|
||||
throw PluginError.taskErrorCode(Int(task.terminationStatus))
|
||||
}
|
||||
|
||||
guard let authorization = task.authorization else { throw PluginError.unknown() }
|
||||
return authorization
|
||||
}
|
||||
}
|
||||
37
AltServer/Plugin/PluginVersion.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// PluginVersion.swift
|
||||
// AltServer
|
||||
//
|
||||
// Created by Riley Testut and Weedles on 2/15/22 <3
|
||||
// Copyright © 2022 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PluginVersion: Decodable
|
||||
{
|
||||
var url: URL
|
||||
var sha256Hash: String
|
||||
var version: String
|
||||
|
||||
static let v1_0 = PluginVersion(url: URL(string: "https://f000.backblazeb2.com/file/altstore/altserver/altplugin/1_0.zip")!,
|
||||
sha256Hash: "070e9b7e1f74e7a6474d36253ab5a3623ff93892acc9e1043c3581f2ded12200",
|
||||
version: "1.0")
|
||||
|
||||
static let v1_9 = PluginVersion(url: Bundle.main.url(forResource: "AltPlugin", withExtension: "zip")!,
|
||||
sha256Hash: "83ead26d8776ef6850e06fe3d1c5c5559aca284718b1cf3cc49785ba6b1e2849",
|
||||
version: "1.9")
|
||||
|
||||
private enum CodingKeys: String, CodingKey
|
||||
{
|
||||
case url
|
||||
case sha256Hash = "sha256"
|
||||
case version
|
||||
}
|
||||
}
|
||||
|
||||
struct PluginVersionResponse: Decodable
|
||||
{
|
||||
var version: Int
|
||||
var pluginVersion: PluginVersion
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
location = "self:AltStore.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
111
AltStore.xcodeproj/xcshareddata/xcschemes/AltDaemon.xcscheme
Normal file
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1150"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A7A6DC28A6D60809855FE404C6A3EA29"
|
||||
BuildableName = "libPods-AltDaemon.a"
|
||||
BlueprintName = "Pods-AltDaemon"
|
||||
ReferencedContainer = "container:Pods/Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||
BuildableName = "libAltKit.a"
|
||||
BlueprintName = "AltKit"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "THEOS"
|
||||
value = "~/theos"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
|
||||
BuildableName = "AltDaemon"
|
||||
BlueprintName = "AltDaemon"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
67
AltStore.xcodeproj/xcshareddata/xcschemes/AltPlugin.xcscheme
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1120"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF5C5FC4237DF5AE00EDD0C6"
|
||||
BuildableName = "AltPlugin.mailbundle"
|
||||
BlueprintName = "AltPlugin"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "1"
|
||||
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 = "BF5C5FC4237DF5AE00EDD0C6"
|
||||
BuildableName = "AltPlugin.mailbundle"
|
||||
BlueprintName = "AltPlugin"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.7">
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -15,9 +14,9 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF58047A246A28F7008AE704"
|
||||
BuildableName = "AltBackup.app"
|
||||
BlueprintName = "AltBackup"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
@@ -27,8 +26,20 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -44,42 +55,14 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF58047A246A28F7008AE704"
|
||||
BuildableName = "AltBackup.app"
|
||||
BlueprintName = "AltBackup"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.MigrationDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "$(DEBUG_ACTIVITY_MODE)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "OBJC_DEBUG_DUPLICATE_CLASSES"
|
||||
value = "$(DEBUG_DUPLICATE_CLASSES)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -91,9 +74,9 @@
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF58047A246A28F7008AE704"
|
||||
BuildableName = "AltBackup.app"
|
||||
BlueprintName = "AltBackup"
|
||||
BlueprintIdentifier = "BF45868C229872EA00BD7491"
|
||||
BuildableName = "AltServer.app"
|
||||
BlueprintName = "AltServer"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.7">
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
@@ -15,22 +15,34 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D586D39728EF58B0000E101F"
|
||||
BuildableName = "AltTests.xctest"
|
||||
BlueprintName = "AltTests"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
@@ -44,41 +56,17 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.MigrationDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLiteIntegrityCheck 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.SQLDebug 1"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "$(DEBUG_ACTIVITY_MODE)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "OBJC_DEBUG_DUPLICATE_CLASSES"
|
||||
value = "$(DEBUG_DUPLICATE_CLASSES)"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -91,14 +79,14 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFD247692284B9A500981D42"
|
||||
BuildableName = "SideStore.app"
|
||||
BlueprintName = "SideStore"
|
||||
BuildableName = "AltStore.app"
|
||||
BlueprintName = "AltStore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Release">
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -1,32 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
buildImplicitDependencies = "YES">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:SideStore/Tests/DataStructureTests.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "YES">
|
||||
<CodeCoverageTargets>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF66EE7D2501AE50007EE018"
|
||||
BuildableName = "AltStoreCore.framework"
|
||||
BlueprintName = "AltStoreCore"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A81A8CC42D68BA610086C96F"
|
||||
BuildableName = "DataStructureTests.xctest"
|
||||
BlueprintName = "DataStructureTests"
|
||||
BlueprintIdentifier = "D586D39728EF58B0000E101F"
|
||||
BuildableName = "AltTests.xctest"
|
||||
BlueprintName = "AltTests"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||