Compare commits
163 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a0d3353b9 | ||
|
|
da8d22c6d3 | ||
|
|
4f9b7f8f6a | ||
|
|
f34e291433 | ||
|
|
789eedcdb7 | ||
|
|
f9262d9a05 | ||
|
|
4776af75ee | ||
|
|
cab5533b49 | ||
|
|
fc50db1cee | ||
|
|
576585b7de | ||
|
|
6cf9647840 | ||
|
|
90dccd8180 | ||
|
|
372d5b1f4e | ||
|
|
4063796833 | ||
|
|
be2eea0532 | ||
|
|
a6dec75603 | ||
|
|
9527fb2961 | ||
|
|
084bd8f4a1 | ||
|
|
f93da1c502 | ||
|
|
79ef494504 | ||
|
|
cdf3675a5e | ||
|
|
5974a85800 | ||
|
|
4ccb6e7f06 | ||
|
|
22e4991f9a | ||
|
|
059c51b5dd | ||
|
|
00dcf3816e | ||
|
|
08738d6912 | ||
|
|
472e36ed0f | ||
|
|
bfb8c2cbb8 | ||
|
|
a7b3f78813 | ||
|
|
2e2b16e759 | ||
|
|
0bc8e42a9b | ||
|
|
ceae881d9a | ||
|
|
debc41f688 | ||
|
|
4b9d86b22e | ||
|
|
bbec397613 | ||
|
|
a434d26921 | ||
|
|
78615418b5 | ||
|
|
54cca28dbe | ||
|
|
10ce2b8511 | ||
|
|
14b2b3da10 | ||
|
|
f3f3bf0e4f | ||
|
|
3621601658 | ||
|
|
9630a68224 | ||
|
|
227f42af37 | ||
|
|
8fbf9eef8a | ||
|
|
b302365454 | ||
|
|
a61338804c | ||
|
|
259ad0789a | ||
|
|
cc03660576 | ||
|
|
a43bf63aab | ||
|
|
cfef2879b5 | ||
|
|
36424a78e0 | ||
|
|
3d9cf73fc1 | ||
|
|
2dc1bcdcbb | ||
|
|
afa6dff0b9 | ||
|
|
17f3d365b8 | ||
|
|
7b154501b6 | ||
|
|
8654756d67 | ||
|
|
85c9bdba55 | ||
|
|
0a02b29a4b | ||
|
|
3a8722c07d | ||
|
|
29bf77007a | ||
|
|
a75c54f2f6 | ||
|
|
42c2c6bfc0 | ||
|
|
69e667cf87 | ||
|
|
dc22b913f3 | ||
|
|
9b107ec98c | ||
|
|
2ed84ef792 | ||
|
|
e04ed029de | ||
|
|
e3f996da6e | ||
|
|
aca4e0ac89 | ||
|
|
dd9a348298 | ||
|
|
d6efd8fa74 | ||
|
|
b81eab0cc4 | ||
|
|
dc31fe884c | ||
|
|
283c1a4739 | ||
|
|
cac0ebfab7 | ||
|
|
59331bcb38 | ||
|
|
3d6908f526 | ||
|
|
b91a7e53ad | ||
|
|
f8970f44d5 | ||
|
|
6050c78613 | ||
|
|
cf85e2fc5a | ||
|
|
15630109d5 | ||
|
|
7171b90c88 | ||
|
|
3d5fdd528b | ||
|
|
06558610e9 | ||
|
|
5e5fdf0d3a | ||
|
|
8277554048 | ||
|
|
ba11d4129e | ||
|
|
389d8d2a63 | ||
|
|
f4567bdf1e | ||
|
|
27bbf6f2a3 | ||
|
|
c1670b250a | ||
|
|
8e78c1c8c5 | ||
|
|
c31a1ef6fa | ||
|
|
37a86643b9 | ||
|
|
c245a1e69f | ||
|
|
0f5e42b88e | ||
|
|
dd46112769 | ||
|
|
c3e568e234 | ||
|
|
f269dcbc98 | ||
|
|
604358bf6c | ||
|
|
1a3ca60f46 | ||
|
|
27d5b03321 | ||
|
|
20b9833b67 | ||
|
|
e855a1fb62 | ||
|
|
908e7ba3ea | ||
|
|
7daa3e600e | ||
|
|
a936bf84b2 | ||
|
|
897f449b7c | ||
|
|
5ebe24060f | ||
|
|
13496d8a7d | ||
|
|
4b6616fdea | ||
|
|
74237f8fc1 | ||
|
|
c94c2c1979 | ||
|
|
b36165100b | ||
|
|
8a6f5a7deb | ||
|
|
600d4cbafa | ||
|
|
8811a6d0d8 | ||
|
|
f973e7d744 | ||
|
|
6e64db26fb | ||
|
|
97bf958301 | ||
|
|
dc5f637886 | ||
|
|
a587d53798 | ||
|
|
a433bc4f72 | ||
|
|
66deeb0d57 | ||
|
|
4a4b469e3f | ||
|
|
7628f85ba8 | ||
|
|
f64e97f7a2 | ||
|
|
1152bbfb28 | ||
|
|
33dc6347f2 | ||
|
|
7599178042 | ||
|
|
6fda52ed37 | ||
|
|
8f1f6e9363 | ||
|
|
848ce1d362 | ||
|
|
38ae157be5 | ||
|
|
7f3e8125c0 | ||
|
|
7f14a4b8b5 | ||
|
|
cd80053be6 | ||
|
|
021d0aa745 | ||
|
|
180dd52eee | ||
|
|
348c89871e | ||
|
|
21c26c9793 | ||
|
|
4e177b5fbf | ||
|
|
411f76788c | ||
|
|
88b15b989a | ||
|
|
45626e2789 | ||
|
|
1411bd30b3 | ||
|
|
f3472d8cc6 | ||
|
|
b9d37054ea | ||
|
|
48df320aa8 | ||
|
|
feac373350 | ||
|
|
9e1eff06a3 | ||
|
|
35314ce36f | ||
|
|
9d73cd2091 | ||
|
|
91b24fc162 | ||
|
|
b3fa08b43e | ||
|
|
c417978976 | ||
|
|
aa34f3b10d | ||
|
|
c595eba5df | ||
|
|
9a5e127387 |
16
.github/workflows/appcast.yml
vendored
|
|
@ -14,16 +14,16 @@ jobs:
|
||||||
# If you're using actions/checkout@v4 you must set persist-credentials to false in most cases for the deployment to work correctly.
|
# If you're using actions/checkout@v4 you must set persist-credentials to false in most cases for the deployment to work correctly.
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Cache 📦
|
# - name: Cache 📦
|
||||||
uses: actions/cache@v4.0.1
|
# uses: actions/cache@v4.1.1
|
||||||
with:
|
# with:
|
||||||
path: AppCast/vendor/bundle
|
# path: AppCast/vendor/bundle
|
||||||
key: ${{ runner.os }}-gems-v1.0-${{ hashFiles('AppCast/Gemfile') }}
|
# key: ${{ runner.os }}-gems-v1.0-${{ hashFiles('AppCast/Gemfile') }}
|
||||||
restore-keys: |
|
# restore-keys: |
|
||||||
${{ runner.os }}-gems-
|
# ${{ runner.os }}-gems-
|
||||||
|
|
||||||
- name: Setup Ruby, JRuby and TruffleRuby
|
- name: Setup Ruby, JRuby and TruffleRuby
|
||||||
uses: ruby/setup-ruby@v1.172.0
|
uses: ruby/setup-ruby@v1.197.0
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.0'
|
ruby-version: '3.0'
|
||||||
|
|
||||||
|
|
|
||||||
4
.github/workflows/ci.yml
vendored
|
|
@ -8,10 +8,10 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: macos-13
|
runs-on: macos-15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
env:
|
env:
|
||||||
DEVELOPER_DIR: /Applications/Xcode_15.0.1.app
|
DEVELOPER_DIR: /Applications/Xcode_16.4.app
|
||||||
run: xcodebuild test -scheme Xcodes
|
run: xcodebuild test -scheme Xcodes
|
||||||
|
|
|
||||||
22
.github/workflows/xcstrings.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: XCStrings Validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: macos-13
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Clone SwiftPolyglot
|
||||||
|
run: git clone --branch 0.3.1 -- https://github.com/appdecostudio/SwiftPolyglot.git ../SwiftPolyglot
|
||||||
|
|
||||||
|
- name: validate translations
|
||||||
|
run: |
|
||||||
|
swift build --package-path ../SwiftPolyglot --configuration release
|
||||||
|
swift run --package-path ../SwiftPolyglot swiftpolyglot "ca,de,el,es,fi,fr,hi,it,ja,ko,nl,pl,pt-BR,ru,tr,uk,zh-Hans,zh-Hant" --errorOnMissing
|
||||||
|
|
@ -8,7 +8,7 @@ source "https://rubygems.org"
|
||||||
#
|
#
|
||||||
# This will help ensure the proper Jekyll version is running.
|
# This will help ensure the proper Jekyll version is running.
|
||||||
# Happy Jekylling!
|
# Happy Jekylling!
|
||||||
gem "jekyll", "~> 3.9.0"
|
gem "jekyll", "~> 4.4.1"
|
||||||
|
|
||||||
gem "jekyll-github-metadata", group: :jekyll_plugins
|
gem "jekyll-github-metadata", group: :jekyll_plugins
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,110 @@
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
addressable (2.8.0)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
|
base64 (0.3.0)
|
||||||
|
bigdecimal (3.2.2)
|
||||||
colorator (1.1.0)
|
colorator (1.1.0)
|
||||||
concurrent-ruby (1.1.7)
|
concurrent-ruby (1.3.5)
|
||||||
em-websocket (0.5.2)
|
csv (3.3.5)
|
||||||
|
em-websocket (0.5.3)
|
||||||
eventmachine (>= 0.12.9)
|
eventmachine (>= 0.12.9)
|
||||||
http_parser.rb (~> 0.6.0)
|
http_parser.rb (~> 0)
|
||||||
eventmachine (1.2.7)
|
eventmachine (1.2.7)
|
||||||
faraday (1.3.0)
|
faraday (1.3.0)
|
||||||
faraday-net_http (~> 1.0)
|
faraday-net_http (~> 1.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.1)
|
||||||
ffi (1.14.2)
|
ffi (1.17.2)
|
||||||
|
ffi (1.17.2-x86_64-darwin)
|
||||||
forwardable-extended (2.6.0)
|
forwardable-extended (2.6.0)
|
||||||
http_parser.rb (0.6.0)
|
google-protobuf (4.31.1)
|
||||||
i18n (0.9.5)
|
bigdecimal
|
||||||
|
rake (>= 13)
|
||||||
|
google-protobuf (4.31.1-x86_64-darwin)
|
||||||
|
bigdecimal
|
||||||
|
rake (>= 13)
|
||||||
|
http_parser.rb (0.8.0)
|
||||||
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jekyll (3.9.0)
|
jekyll (4.4.1)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
|
base64 (~> 0.2)
|
||||||
colorator (~> 1.0)
|
colorator (~> 1.0)
|
||||||
|
csv (~> 3.0)
|
||||||
em-websocket (~> 0.5)
|
em-websocket (~> 0.5)
|
||||||
i18n (~> 0.7)
|
i18n (~> 1.0)
|
||||||
jekyll-sass-converter (~> 1.0)
|
jekyll-sass-converter (>= 2.0, < 4.0)
|
||||||
jekyll-watch (~> 2.0)
|
jekyll-watch (~> 2.0)
|
||||||
kramdown (>= 1.17, < 3)
|
json (~> 2.6)
|
||||||
|
kramdown (~> 2.3, >= 2.3.1)
|
||||||
|
kramdown-parser-gfm (~> 1.0)
|
||||||
liquid (~> 4.0)
|
liquid (~> 4.0)
|
||||||
mercenary (~> 0.3.3)
|
mercenary (~> 0.3, >= 0.3.6)
|
||||||
pathutil (~> 0.9)
|
pathutil (~> 0.9)
|
||||||
rouge (>= 1.7, < 4)
|
rouge (>= 3.0, < 5.0)
|
||||||
safe_yaml (~> 1.0)
|
safe_yaml (~> 1.0)
|
||||||
|
terminal-table (>= 1.8, < 4.0)
|
||||||
|
webrick (~> 1.7)
|
||||||
jekyll-github-metadata (2.13.0)
|
jekyll-github-metadata (2.13.0)
|
||||||
jekyll (>= 3.4, < 5.0)
|
jekyll (>= 3.4, < 5.0)
|
||||||
octokit (~> 4.0, != 4.4.0)
|
octokit (~> 4.0, != 4.4.0)
|
||||||
jekyll-sass-converter (1.5.2)
|
jekyll-sass-converter (3.1.0)
|
||||||
sass (~> 3.4)
|
sass-embedded (~> 1.75)
|
||||||
jekyll-watch (2.2.1)
|
jekyll-watch (2.2.1)
|
||||||
listen (~> 3.0)
|
listen (~> 3.0)
|
||||||
kramdown (2.3.1)
|
json (2.12.2)
|
||||||
rexml
|
kramdown (2.5.1)
|
||||||
|
rexml (>= 3.3.9)
|
||||||
kramdown-parser-gfm (1.1.0)
|
kramdown-parser-gfm (1.1.0)
|
||||||
kramdown (~> 2.0)
|
kramdown (~> 2.0)
|
||||||
liquid (4.0.3)
|
liquid (4.0.4)
|
||||||
listen (3.4.1)
|
listen (3.9.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
mercenary (0.3.6)
|
mercenary (0.4.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
octokit (4.20.0)
|
octokit (4.20.0)
|
||||||
faraday (>= 0.9)
|
faraday (>= 0.9)
|
||||||
sawyer (~> 0.8.0, >= 0.5.3)
|
sawyer (~> 0.8.0, >= 0.5.3)
|
||||||
pathutil (0.16.2)
|
pathutil (0.16.2)
|
||||||
forwardable-extended (~> 2.6)
|
forwardable-extended (~> 2.6)
|
||||||
public_suffix (4.0.6)
|
public_suffix (6.0.2)
|
||||||
rb-fsevent (0.10.4)
|
rake (13.3.0)
|
||||||
rb-inotify (0.10.1)
|
rb-fsevent (0.11.2)
|
||||||
|
rb-inotify (0.11.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
rexml (3.2.5)
|
rexml (3.4.1)
|
||||||
rouge (3.26.0)
|
rouge (4.5.2)
|
||||||
ruby2_keywords (0.0.2)
|
ruby2_keywords (0.0.2)
|
||||||
safe_yaml (1.0.5)
|
safe_yaml (1.0.5)
|
||||||
sass (3.7.4)
|
sass-embedded (1.89.2)
|
||||||
sass-listen (~> 4.0.0)
|
google-protobuf (~> 4.31)
|
||||||
sass-listen (4.0.0)
|
rake (>= 13)
|
||||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
sass-embedded (1.89.2-x86_64-darwin)
|
||||||
rb-inotify (~> 0.9, >= 0.9.7)
|
google-protobuf (~> 4.31)
|
||||||
sawyer (0.8.2)
|
sawyer (0.8.2)
|
||||||
addressable (>= 2.3.5)
|
addressable (>= 2.3.5)
|
||||||
faraday (> 0.8, < 2.0)
|
faraday (> 0.8, < 2.0)
|
||||||
|
terminal-table (3.0.2)
|
||||||
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tzinfo (1.2.10)
|
tzinfo (1.2.10)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
tzinfo-data (1.2020.6)
|
tzinfo-data (1.2020.6)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
|
unicode-display_width (2.6.0)
|
||||||
wdm (0.1.1)
|
wdm (0.1.1)
|
||||||
|
webrick (1.9.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
x86_64-darwin-20
|
x86_64-darwin-20
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
jekyll (~> 3.9.0)
|
jekyll (~> 4.4.1)
|
||||||
jekyll-github-metadata
|
jekyll-github-metadata
|
||||||
kramdown-parser-gfm
|
kramdown-parser-gfm
|
||||||
tzinfo (~> 1.2)
|
tzinfo (~> 1.2)
|
||||||
|
|
@ -90,4 +112,4 @@ DEPENDENCIES
|
||||||
wdm (~> 0.1.0)
|
wdm (~> 0.1.0)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.2.5
|
2.6.9
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ We love your input! We want to make contributing to this project as easy and tra
|
||||||
- Proposing new features
|
- Proposing new features
|
||||||
- Becoming a maintainer
|
- Becoming a maintainer
|
||||||
|
|
||||||
## We Develop with Github
|
## We Develop with GitHub
|
||||||
We use github to host code, to track issues and feature requests, as well as accept pull requests.
|
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
|
||||||
|
|
||||||
## All Code Changes Happen Through Pull Requests
|
## All Code Changes Happen Through Pull Requests
|
||||||
Pull requests are the best way to propose changes to the codebase We actively welcome your pull requests:
|
Pull requests are the best way to propose changes to the codebase We actively welcome your pull requests:
|
||||||
|
|
@ -23,7 +23,7 @@ Pull requests are the best way to propose changes to the codebase We actively w
|
||||||
## Any contributions you make will be under the MIT Software License
|
## Any contributions you make will be under the MIT Software License
|
||||||
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||||
|
|
||||||
## Report bugs using Github's [issues](https://github.com/robotsandpencils/xcodesapp/issues)
|
## Report bugs using GitHub [issues](https://github.com/robotsandpencils/xcodesapp/issues)
|
||||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
|
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
|
||||||
|
|
||||||
## Write bug reports with detail, background, and sample code
|
## Write bug reports with detail, background, and sample code
|
||||||
|
|
|
||||||
BIN
IconDark.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
IconMono.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
25
README.md
|
|
@ -1,4 +1,4 @@
|
||||||
<h1><img src="icon.png" align="center" width=50 height=50 /> Xcodes.app</h1>
|
<h1><img src="icon.png" align="center" width=50 height=50 /> <img src="IconDark.png" align="center" width=50 height=50 /> <img src="IconMono.png" align="center" width=50 height=50 /> Xcodes.app</h1>
|
||||||
|
|
||||||
The easiest way to install and switch between multiple versions of Xcode.
|
The easiest way to install and switch between multiple versions of Xcode.
|
||||||
|
|
||||||
|
|
@ -22,11 +22,20 @@ XcodesApp is now part of the `XcodesOrg` - [read more here](nextstep.md)
|
||||||
- Just click a button to make a version active with `xcode-select`.
|
- Just click a button to make a version active with `xcode-select`.
|
||||||
- View release notes, OS compatibility, included SDKs and compilers from [Xcode Releases](https://xcodereleases.com).
|
- View release notes, OS compatibility, included SDKs and compilers from [Xcode Releases](https://xcodereleases.com).
|
||||||
- Dark/Light Mode supported
|
- Dark/Light Mode supported
|
||||||
|
- Security Key Authentication supported
|
||||||
|
- Support installing Platforms/Runtimes
|
||||||
|
- Support installing Apple Silicon variants
|
||||||
|
|
||||||
## Platforms/Runtimes
|
## Platforms/Runtimes
|
||||||
|
|
||||||
- Xcodes supports downloading the Apple runtimes via the app. Simply click on the Platform, and Xcodes will install automatically for you.
|
- Xcodes supports downloading the Apple runtimes via the app. Simply click on the Platform, and Xcodes will install automatically for you.
|
||||||
|
|
||||||
|
**Note: iOS 18+, tvOS 18+, watchOS 11+, visionOS 2+ requires that Xcode 16.1 Beta 3+ be installed and active.**
|
||||||
|
|
||||||
|
## Apple Silicon Variants
|
||||||
|
|
||||||
|
As of Xcode 26, Apple provides Apple Silicon as well as Universal variants for Xcode versions as well as each runtime. Simply tap on which variant you want installed. To install the Apple Silicon runtime variant Xcode 26 is required to be active.
|
||||||
|
|
||||||
## Experiments
|
## Experiments
|
||||||
|
|
||||||
- Thanks to the wonderful work of [https://github.com/saagarjha/unxip](https://github.com/saagarjha/unxip), turn on the experiment to increase your unxipping time by up to 70%! More can be found on his repo, but bugs, high memory may occur if used.
|
- Thanks to the wonderful work of [https://github.com/saagarjha/unxip](https://github.com/saagarjha/unxip), turn on the experiment to increase your unxipping time by up to 70%! More can be found on his repo, but bugs, high memory may occur if used.
|
||||||
|
|
@ -50,13 +59,15 @@ The following languages are supported because of the following community users!
|
||||||
|Ukranian 🇺🇦 |[@gelosi](https://github.com/gelosi)|Japanese 🇯🇵|[@tatsuz0u](https://github.com/tatsuz0u)|
|
|Ukranian 🇺🇦 |[@gelosi](https://github.com/gelosi)|Japanese 🇯🇵|[@tatsuz0u](https://github.com/tatsuz0u)|
|
||||||
|German 🇩🇪|[@drct](https://github.com/drct)|Dutch 🇳🇱|[@jfversluis](https://github/com/jfversluis)|
|
|German 🇩🇪|[@drct](https://github.com/drct)|Dutch 🇳🇱|[@jfversluis](https://github/com/jfversluis)|
|
||||||
|Brazilian Portuguese 🇧🇷|[@brunomunizaf](https://github.com/brunomunizaf)|Polish 🇵🇱|[@jakex7](https://github.com/jakex7)|
|
|Brazilian Portuguese 🇧🇷|[@brunomunizaf](https://github.com/brunomunizaf)|Polish 🇵🇱|[@jakex7](https://github.com/jakex7)|
|
||||||
|Catalan|[@ferranabello](https://github.com/ferranabello)|
|
|Catalan|[@ferranabello](https://github.com/ferranabello)|Greek 🇬🇷|[@alladinian](https://github.com/alladinian)
|
||||||
|
|Thai 🇹🇭|[@neetrath](https://github.com/neetrath)|
|
||||||
|
|
||||||
Want to add more languages? Simply create a PR with the updated strings file.
|
Want to add more languages? Simply create a PR with the updated strings file.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
v1.X - requires MacOS 11 or newer
|
v1.X - requires macOS 11 or newer
|
||||||
v2.X - requires MacOS 13
|
v2.X - requires macOS 13
|
||||||
|
v3.X - requires macOS 13 - architecture variants and updated icon.
|
||||||
|
|
||||||
### Install with Homebrew
|
### Install with Homebrew
|
||||||
|
|
||||||
|
|
@ -81,7 +92,7 @@ Xcodes.app and CLI is updated, maintained with contributors like yourself. Even
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
You'll need macOS 13.5 Ventura and Xcode 15 in order to build and run Xcodes.app.
|
You'll need macOS 15.6 Ventura and Xcode 26 in order to build and run Xcodes.app.
|
||||||
|
|
||||||
`Unxip` and `aria2` must be compiled as a universal binary
|
`Unxip` and `aria2` must be compiled as a universal binary
|
||||||
```
|
```
|
||||||
|
|
@ -96,7 +107,6 @@ You'll need macOS 13.5 Ventura and Xcode 15 in order to build and run Xcodes.app
|
||||||
lipo -archs unxip
|
lipo -archs unxip
|
||||||
```
|
```
|
||||||
|
|
||||||
Notable design decisions are recorded in [DECISIONS.md](./DECISIONS.md). The Apple authentication flow is described in [Apple.paw](./Apple.paw), which will allow you to play with the API endpoints that are involved using the [Paw](https://paw.cloud) app.
|
|
||||||
|
|
||||||
[`xcode-install`](https://github.com/xcpretty/xcode-install) and [fastlane/spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) both deserve credit for figuring out the hard parts of what makes this possible.
|
[`xcode-install`](https://github.com/xcpretty/xcode-install) and [fastlane/spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) both deserve credit for figuring out the hard parts of what makes this possible.
|
||||||
|
|
||||||
|
|
@ -160,7 +170,8 @@ popd
|
||||||
# Attach the zip that was created in the Product directory to the release
|
# Attach the zip that was created in the Product directory to the release
|
||||||
# Publish the release
|
# Publish the release
|
||||||
|
|
||||||
# Update the [Homebrew Cask](https://github.com/RobotsAndPencils/homebrew-cask/blob/master/Casks/xcodes.rb).
|
shasum -a 256 xcodes.zip
|
||||||
|
# Update the [Homebrew Cask](https://github.com/XcodesOrg/homebrew-cask/blob/master/Casks/x/xcodes.rb).
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
|
||||||
26
Scripts/fix_libfido2_framework.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Fix libfido2.framework structure for macOS validation
|
||||||
|
# If this script is not run, the build will fail because xcodebuild is expecting the library in a specific structure
|
||||||
|
FRAMEWORK_PATH="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Contents/Frameworks/libfido2.framework"
|
||||||
|
|
||||||
|
if [ -d "$FRAMEWORK_PATH" ] && [ -f "$FRAMEWORK_PATH/Info.plist" ] && [ ! -d "$FRAMEWORK_PATH/Versions" ]; then
|
||||||
|
echo "Fixing libfido2.framework bundle structure..."
|
||||||
|
|
||||||
|
# Create proper bundle structure
|
||||||
|
mkdir -p "$FRAMEWORK_PATH/Versions/A/Resources"
|
||||||
|
|
||||||
|
# Move files to proper locations
|
||||||
|
mv "$FRAMEWORK_PATH/Info.plist" "$FRAMEWORK_PATH/Versions/A/Resources/"
|
||||||
|
mv "$FRAMEWORK_PATH/libfido2" "$FRAMEWORK_PATH/Versions/A/"
|
||||||
|
if [ -f "$FRAMEWORK_PATH/LICENSE" ]; then
|
||||||
|
mv "$FRAMEWORK_PATH/LICENSE" "$FRAMEWORK_PATH/Versions/A/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create symbolic links
|
||||||
|
ln -sf A "$FRAMEWORK_PATH/Versions/Current"
|
||||||
|
ln -sf Versions/Current/libfido2 "$FRAMEWORK_PATH/libfido2"
|
||||||
|
ln -sf Versions/Current/Resources "$FRAMEWORK_PATH/Resources"
|
||||||
|
|
||||||
|
echo "libfido2.framework structure fixed"
|
||||||
|
fi
|
||||||
|
|
@ -7,6 +7,10 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */; };
|
||||||
|
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; };
|
||||||
|
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; };
|
||||||
|
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */; };
|
||||||
36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; };
|
36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; };
|
||||||
36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; };
|
36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; };
|
||||||
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
|
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
|
||||||
|
|
@ -44,7 +48,6 @@
|
||||||
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF84D2595079F00E47BAF /* ScrollingTextView.swift */; };
|
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF84D2595079F00E47BAF /* ScrollingTextView.swift */; };
|
||||||
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8512595080100E47BAF /* AcknowledgementsView.swift */; };
|
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8512595080100E47BAF /* AcknowledgementsView.swift */; };
|
||||||
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8652595130600E47BAF /* View+IsHidden.swift */; };
|
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8652595130600E47BAF /* View+IsHidden.swift */; };
|
||||||
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */ = {isa = PBXBuildFile; productRef = CA9FF86C25951C6E00E47BAF /* XCModel */; };
|
|
||||||
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */; };
|
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */; };
|
||||||
CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF87A2595293E00E47BAF /* DataSource.swift */; };
|
CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF87A2595293E00E47BAF /* DataSource.swift */; };
|
||||||
CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF88025955C7000E47BAF /* AvailableXcode.swift */; };
|
CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF88025955C7000E47BAF /* AvailableXcode.swift */; };
|
||||||
|
|
@ -111,17 +114,19 @@
|
||||||
CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4AB325B7D3AF0064FE51 /* AdvancedPreferencePane.swift */; };
|
CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4AB325B7D3AF0064FE51 /* AdvancedPreferencePane.swift */; };
|
||||||
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
|
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
|
||||||
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
|
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
|
||||||
|
D93F95C12E0C8C1A00238FB5 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93F95C02E0C8C1A00238FB5 /* TagView.swift */; };
|
||||||
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; };
|
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; };
|
||||||
E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; };
|
E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; };
|
||||||
E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; };
|
E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; };
|
||||||
|
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E83FDC432CBB649100679C6B /* Sparkle */; };
|
||||||
E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */; };
|
E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */; };
|
||||||
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */; };
|
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */; };
|
||||||
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F532B333864003F3959 /* PlatformsListView.swift */; };
|
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F532B333864003F3959 /* PlatformsListView.swift */; };
|
||||||
E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E84E4F562B335094003F3959 /* OrderedCollections */; };
|
E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E84E4F562B335094003F3959 /* OrderedCollections */; };
|
||||||
|
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */ = {isa = PBXBuildFile; productRef = E862D43A2CC8B26F00BAA376 /* SRP */; };
|
||||||
E86671272B309D2F0048559A /* PlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86671262B309D2F0048559A /* PlatformsView.swift */; };
|
E86671272B309D2F0048559A /* PlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86671262B309D2F0048559A /* PlatformsView.swift */; };
|
||||||
E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; };
|
E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; };
|
||||||
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
|
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
|
||||||
E891A1C42B43ACF900A1B9D1 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E891A1C32B43ACF900A1B9D1 /* Sparkle */; };
|
|
||||||
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
|
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
|
||||||
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
|
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
|
||||||
E8B20CBF2A2EDEC20057D816 /* SDKs+Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */; };
|
E8B20CBF2A2EDEC20057D816 /* SDKs+Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */; };
|
||||||
|
|
@ -134,6 +139,7 @@
|
||||||
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; };
|
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; };
|
||||||
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
|
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
|
||||||
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
|
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
|
||||||
|
E8EEAD1D2E79174F00BE67E8 /* XcodesIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = E8EEAD1C2E79174F00BE67E8 /* XcodesIcon.icon */; };
|
||||||
E8F44A1E296B4CD7002D6592 /* Path in Frameworks */ = {isa = PBXBuildFile; productRef = E8F44A1D296B4CD7002D6592 /* Path */; };
|
E8F44A1E296B4CD7002D6592 /* Path in Frameworks */ = {isa = PBXBuildFile; productRef = E8F44A1D296B4CD7002D6592 /* Path */; };
|
||||||
E8FA00542B5B109800769CE0 /* com.xcodesorg.xcodesapp.Helper in Copy Helper */ = {isa = PBXBuildFile; fileRef = CA9FF8AE2595967A00E47BAF /* com.xcodesorg.xcodesapp.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
E8FA00542B5B109800769CE0 /* com.xcodesorg.xcodesapp.Helper in Copy Helper */ = {isa = PBXBuildFile; fileRef = CA9FF8AE2595967A00E47BAF /* com.xcodesorg.xcodesapp.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */ = {isa = PBXBuildFile; productRef = E8FD5726291EE4AC001E004C /* AsyncNetworkService */; };
|
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */ = {isa = PBXBuildFile; productRef = E8FD5726291EE4AC001E004C /* AsyncNetworkService */; };
|
||||||
|
|
@ -192,6 +198,9 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
|
||||||
|
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = "<group>"; };
|
||||||
|
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = "<group>"; };
|
||||||
36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = "<group>"; };
|
36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = "<group>"; };
|
||||||
36741BFE291E50F500A85AAE /* FileError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = "<group>"; };
|
36741BFE291E50F500A85AAE /* FileError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = "<group>"; };
|
||||||
536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = "<group>"; };
|
536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -313,6 +322,7 @@
|
||||||
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatesPreferencePane.swift; sourceTree = "<group>"; };
|
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatesPreferencePane.swift; sourceTree = "<group>"; };
|
||||||
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
|
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
|
||||||
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
|
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
|
||||||
|
D93F95C02E0C8C1A00238FB5 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
||||||
E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = "<group>"; };
|
E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = "<group>"; };
|
||||||
E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = "<group>"; };
|
E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = "<group>"; };
|
||||||
E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitViewWrapper.swift; sourceTree = "<group>"; };
|
E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitViewWrapper.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -332,6 +342,7 @@
|
||||||
E8D655BF288DD04700A139C2 /* SelectedActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedActionType.swift; sourceTree = "<group>"; };
|
E8D655BF288DD04700A139C2 /* SelectedActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedActionType.swift; sourceTree = "<group>"; };
|
||||||
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||||
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = "<group>"; };
|
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
E8EEAD1C2E79174F00BE67E8 /* XcodesIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = XcodesIcon.icon; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -346,12 +357,14 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */,
|
||||||
|
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */,
|
||||||
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
|
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
|
||||||
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
|
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
|
||||||
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
|
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
|
||||||
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
|
|
||||||
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
|
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
|
||||||
E891A1C42B43ACF900A1B9D1 /* Sparkle in Frameworks */,
|
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */,
|
||||||
|
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */,
|
||||||
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */,
|
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */,
|
||||||
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */,
|
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */,
|
||||||
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */,
|
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */,
|
||||||
|
|
@ -376,6 +389,7 @@
|
||||||
63EAA4E9259944340046AB8F /* Common */ = {
|
63EAA4E9259944340046AB8F /* Common */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D93F95C02E0C8C1A00238FB5 /* TagView.swift */,
|
||||||
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */,
|
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */,
|
||||||
63EAA4EA259944450046AB8F /* ProgressButton.swift */,
|
63EAA4EA259944450046AB8F /* ProgressButton.swift */,
|
||||||
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */,
|
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */,
|
||||||
|
|
@ -408,6 +422,7 @@
|
||||||
CA538A12255A4F7C00E64DD7 /* Frameworks */ = {
|
CA538A12255A4F7C00E64DD7 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -454,6 +469,8 @@
|
||||||
CA735108257BF96D00EA9CF8 /* AttributedText.swift */,
|
CA735108257BF96D00EA9CF8 /* AttributedText.swift */,
|
||||||
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */,
|
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */,
|
||||||
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */,
|
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */,
|
||||||
|
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */,
|
||||||
|
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */,
|
||||||
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */,
|
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */,
|
||||||
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
|
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
|
||||||
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
|
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
|
||||||
|
|
@ -545,6 +562,7 @@
|
||||||
CABFAA1D2592F7F200380FEE /* Resources */ = {
|
CABFAA1D2592F7F200380FEE /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E8EEAD1C2E79174F00BE67E8 /* XcodesIcon.icon */,
|
||||||
CAA858DA25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz */,
|
CAA858DA25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz */,
|
||||||
CAA8588025A2B63A00ACF8C0 /* aria2c */,
|
CAA8588025A2B63A00ACF8C0 /* aria2c */,
|
||||||
CAA8588A25A2B69300ACF8C0 /* aria2c.LICENSE */,
|
CAA8588A25A2B69300ACF8C0 /* aria2c.LICENSE */,
|
||||||
|
|
@ -706,14 +724,15 @@
|
||||||
CABFA9ED2592F0CC00380FEE /* SwiftSoup */,
|
CABFA9ED2592F0CC00380FEE /* SwiftSoup */,
|
||||||
CABFA9F72592F0F900380FEE /* KeychainAccess */,
|
CABFA9F72592F0F900380FEE /* KeychainAccess */,
|
||||||
CABFA9FC2592F13300380FEE /* LegibleError */,
|
CABFA9FC2592F13300380FEE /* LegibleError */,
|
||||||
CA9FF86C25951C6E00E47BAF /* XCModel */,
|
|
||||||
CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */,
|
CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */,
|
||||||
E689540225BE8C64000EBCEA /* DockProgress */,
|
E689540225BE8C64000EBCEA /* DockProgress */,
|
||||||
E8FD5726291EE4AC001E004C /* AsyncNetworkService */,
|
E8FD5726291EE4AC001E004C /* AsyncNetworkService */,
|
||||||
E8C0EB19291EF43E0081528A /* XcodesKit */,
|
E8C0EB19291EF43E0081528A /* XcodesKit */,
|
||||||
E8F44A1D296B4CD7002D6592 /* Path */,
|
E8F44A1D296B4CD7002D6592 /* Path */,
|
||||||
E84E4F562B335094003F3959 /* OrderedCollections */,
|
E84E4F562B335094003F3959 /* OrderedCollections */,
|
||||||
E891A1C32B43ACF900A1B9D1 /* Sparkle */,
|
E83FDC432CBB649100679C6B /* Sparkle */,
|
||||||
|
334A932B2CA885A400A5E079 /* LibFido2Swift */,
|
||||||
|
E862D43A2CC8B26F00BAA376 /* SRP */,
|
||||||
);
|
);
|
||||||
productName = XcodesMac;
|
productName = XcodesMac;
|
||||||
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
|
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
|
||||||
|
|
@ -786,6 +805,8 @@
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
nl,
|
nl,
|
||||||
pl,
|
pl,
|
||||||
|
ar,
|
||||||
|
th,
|
||||||
);
|
);
|
||||||
mainGroup = CAD2E7952449574E00113D76;
|
mainGroup = CAD2E7952449574E00113D76;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
|
|
@ -793,14 +814,14 @@
|
||||||
CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||||
CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||||
CABFA9FB2592F13300380FEE /* XCRemoteSwiftPackageReference "LegibleError" */,
|
CABFA9FB2592F13300380FEE /* XCRemoteSwiftPackageReference "LegibleError" */,
|
||||||
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */,
|
|
||||||
CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */,
|
CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */,
|
||||||
CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */,
|
CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */,
|
||||||
E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */,
|
E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */,
|
||||||
E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */,
|
E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */,
|
||||||
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
|
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
|
||||||
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
|
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||||
E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||||
|
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */,
|
||||||
);
|
);
|
||||||
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
|
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
|
@ -820,6 +841,7 @@
|
||||||
files = (
|
files = (
|
||||||
CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */,
|
CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */,
|
||||||
9DD4FFCB2B13EC1800C974F1 /* Localizable.xcstrings in Resources */,
|
9DD4FFCB2B13EC1800C974F1 /* Localizable.xcstrings in Resources */,
|
||||||
|
E8EEAD1D2E79174F00BE67E8 /* XcodesIcon.icon in Resources */,
|
||||||
CA9FF83F2594FBC000E47BAF /* Licenses.rtf in Resources */,
|
CA9FF83F2594FBC000E47BAF /* Licenses.rtf in Resources */,
|
||||||
CAA858DB25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz in Resources */,
|
CAA858DB25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz in Resources */,
|
||||||
CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */,
|
CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */,
|
||||||
|
|
@ -888,6 +910,7 @@
|
||||||
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
|
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
|
||||||
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
|
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
|
||||||
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
|
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
|
||||||
|
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */,
|
||||||
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
|
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
|
||||||
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
|
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
|
||||||
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
|
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
|
||||||
|
|
@ -913,7 +936,9 @@
|
||||||
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */,
|
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */,
|
||||||
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */,
|
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */,
|
||||||
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */,
|
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */,
|
||||||
|
D93F95C12E0C8C1A00238FB5 /* TagView.swift in Sources */,
|
||||||
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
|
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
|
||||||
|
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */,
|
||||||
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
|
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
|
||||||
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
|
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
|
||||||
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
|
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
|
||||||
|
|
@ -1072,13 +1097,14 @@
|
||||||
CA8FB636256E154800469DA5 /* Test */ = {
|
CA8FB636256E154800469DA5 /* Test */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = XcodesIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||||
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/XcodesTest.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/XcodesTest.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 25;
|
CURRENT_PROJECT_VERSION = 34;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = NO;
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
|
|
@ -1090,7 +1116,7 @@
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 2.1.1;
|
MARKETING_VERSION = 3.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
|
||||||
PRODUCT_NAME = Xcodes;
|
PRODUCT_NAME = Xcodes;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
@ -1324,13 +1350,14 @@
|
||||||
CAD2E7BD2449575100113D76 /* Debug */ = {
|
CAD2E7BD2449575100113D76 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = XcodesIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||||
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 25;
|
CURRENT_PROJECT_VERSION = 34;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
|
@ -1342,7 +1369,7 @@
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 2.1.1;
|
MARKETING_VERSION = 3.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
|
||||||
PRODUCT_NAME = Xcodes;
|
PRODUCT_NAME = Xcodes;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -1352,13 +1379,14 @@
|
||||||
CAD2E7BE2449575100113D76 /* Release */ = {
|
CAD2E7BE2449575100113D76 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = XcodesIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT = ZU6GR6B2FY;
|
||||||
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 25;
|
CURRENT_PROJECT_VERSION = 34;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
|
@ -1370,7 +1398,7 @@
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 2.1.1;
|
MARKETING_VERSION = 3.0.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
|
||||||
PRODUCT_NAME = Xcodes;
|
PRODUCT_NAME = Xcodes;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -1468,9 +1496,9 @@
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */ = {
|
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/xcodereleases/data";
|
repositoryURL = "https://github.com/kinoroy/LibFido2Swift";
|
||||||
requirement = {
|
requirement = {
|
||||||
branch = main;
|
branch = main;
|
||||||
kind = branch;
|
kind = branch;
|
||||||
|
|
@ -1529,7 +1557,15 @@
|
||||||
repositoryURL = "https://github.com/sindresorhus/DockProgress";
|
repositoryURL = "https://github.com/sindresorhus/DockProgress";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMinorVersion;
|
kind = upToNextMinorVersion;
|
||||||
minimumVersion = 3.2.0;
|
minimumVersion = 4.3.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.6.4;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||||
|
|
@ -1540,14 +1576,6 @@
|
||||||
minimumVersion = 1.0.5;
|
minimumVersion = 1.0.5;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 2.5.2;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */ = {
|
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/mxcl/Path.swift";
|
repositoryURL = "https://github.com/mxcl/Path.swift";
|
||||||
|
|
@ -1567,10 +1595,9 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
CA9FF86C25951C6E00E47BAF /* XCModel */ = {
|
334A932B2CA885A400A5E079 /* LibFido2Swift */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */;
|
productName = LibFido2Swift;
|
||||||
productName = XCModel;
|
|
||||||
};
|
};
|
||||||
CAA1CB2C255A5262003FD669 /* AppleAPI */ = {
|
CAA1CB2C255A5262003FD669 /* AppleAPI */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
|
@ -1611,15 +1638,19 @@
|
||||||
package = E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */;
|
package = E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */;
|
||||||
productName = DockProgress;
|
productName = DockProgress;
|
||||||
};
|
};
|
||||||
|
E83FDC432CBB649100679C6B /* Sparkle */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||||
|
productName = Sparkle;
|
||||||
|
};
|
||||||
E84E4F562B335094003F3959 /* OrderedCollections */ = {
|
E84E4F562B335094003F3959 /* OrderedCollections */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
package = E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
productName = OrderedCollections;
|
productName = OrderedCollections;
|
||||||
};
|
};
|
||||||
E891A1C32B43ACF900A1B9D1 /* Sparkle */ = {
|
E862D43A2CC8B26F00BAA376 /* SRP */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
productName = SRP;
|
||||||
productName = Sparkle;
|
|
||||||
};
|
};
|
||||||
E8C0EB19291EF43E0081528A /* XcodesKit */ = {
|
E8C0EB19291EF43E0081528A /* XcodesKit */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,15 @@
|
||||||
"version": null
|
"version": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "big-num",
|
||||||
|
"repositoryURL": "https://github.com/adam-fowler/big-num",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "5c5511ad06aeb2b97d0868f7394e14a624bfb1c7",
|
||||||
|
"version": "2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "CombineExpectations",
|
"package": "CombineExpectations",
|
||||||
"repositoryURL": "https://github.com/groue/CombineExpectations",
|
"repositoryURL": "https://github.com/groue/CombineExpectations",
|
||||||
|
|
@ -19,22 +28,13 @@
|
||||||
"version": "0.6.0"
|
"version": "0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"package": "XcodeReleases",
|
|
||||||
"repositoryURL": "https://github.com/xcodereleases/data",
|
|
||||||
"state": {
|
|
||||||
"branch": "main",
|
|
||||||
"revision": "a43ad89e536d7a3da525fcc23fb182c37b756ecc",
|
|
||||||
"version": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"package": "DockProgress",
|
"package": "DockProgress",
|
||||||
"repositoryURL": "https://github.com/sindresorhus/DockProgress",
|
"repositoryURL": "https://github.com/sindresorhus/DockProgress",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "7100b68571e2dafe3a06ad5905b80fc3b0107b4b",
|
"revision": "d4f23b5a8f5ca0fac393eb7ba78c2fe3e32e52da",
|
||||||
"version": "3.2.0"
|
"version": "4.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -64,6 +64,15 @@
|
||||||
"version": "1.0.4"
|
"version": "1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "LibFido2Swift",
|
||||||
|
"repositoryURL": "https://github.com/kinoroy/LibFido2Swift",
|
||||||
|
"state": {
|
||||||
|
"branch": "main",
|
||||||
|
"revision": "b87a93300c5b35307c9f26ae490963196bd927f1",
|
||||||
|
"version": null
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "Path.swift",
|
"package": "Path.swift",
|
||||||
"repositoryURL": "https://github.com/mxcl/Path.swift",
|
"repositoryURL": "https://github.com/mxcl/Path.swift",
|
||||||
|
|
@ -78,8 +87,8 @@
|
||||||
"repositoryURL": "https://github.com/sparkle-project/Sparkle/",
|
"repositoryURL": "https://github.com/sparkle-project/Sparkle/",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "47d3d90aee3c52b6f61d04ceae426e607df62347",
|
"revision": "0ef1ee0220239b3776f433314515fd849025673f",
|
||||||
"version": "2.5.2"
|
"version": "2.6.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -91,6 +100,24 @@
|
||||||
"version": "1.0.5"
|
"version": "1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "swift-crypto",
|
||||||
|
"repositoryURL": "https://github.com/apple/swift-crypto",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "ddb07e896a2a8af79512543b1c7eb9797f8898a5",
|
||||||
|
"version": "1.1.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "swift-srp",
|
||||||
|
"repositoryURL": "https://github.com/xcodesOrg/swift-srp",
|
||||||
|
"state": {
|
||||||
|
"branch": "main",
|
||||||
|
"revision": "543aa0122a0257b992f6c7d62d18a26e3dffb8fe",
|
||||||
|
"version": null
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "SwiftSoup",
|
"package": "SwiftSoup",
|
||||||
"repositoryURL": "https://github.com/scinfu/SwiftSoup",
|
"repositoryURL": "https://github.com/scinfu/SwiftSoup",
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
// swift-tools-version:5.3
|
// swift-tools-version:5.7
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "AppleAPI",
|
name: "AppleAPI",
|
||||||
platforms: [.macOS(.v10_15)],
|
platforms: [.macOS(.v11)],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
.library(
|
.library(
|
||||||
name: "AppleAPI",
|
name: "AppleAPI",
|
||||||
targets: ["AppleAPI"]),
|
targets: ["AppleAPI"]),
|
||||||
],
|
],
|
||||||
dependencies: [],
|
dependencies: [
|
||||||
|
.package(url: "https://github.com/xcodesOrg/swift-srp", branch: "main")
|
||||||
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "AppleAPI",
|
name: "AppleAPI",
|
||||||
dependencies: []),
|
dependencies: [.product(name: "SRP", package: "swift-srp")]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "AppleAPITests",
|
name: "AppleAPITests",
|
||||||
dependencies: ["AppleAPI"]),
|
dependencies: ["AppleAPI"]),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
import SRP
|
||||||
|
import Crypto
|
||||||
|
import CommonCrypto
|
||||||
|
|
||||||
|
|
||||||
public class Client {
|
public class Client {
|
||||||
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
|
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
|
||||||
|
|
@ -8,9 +12,13 @@ public class Client {
|
||||||
|
|
||||||
// MARK: - Login
|
// MARK: - Login
|
||||||
|
|
||||||
public func login(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
|
public func srpLogin(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
|
||||||
var serviceKey: String!
|
var serviceKey: String!
|
||||||
|
|
||||||
|
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
|
||||||
|
let clientKeys = client.generateKeys()
|
||||||
|
let a = clientKeys.public
|
||||||
|
|
||||||
return Current.network.dataTask(with: URLRequest.itcServiceKey)
|
return Current.network.dataTask(with: URLRequest.itcServiceKey)
|
||||||
.map(\.data)
|
.map(\.data)
|
||||||
.decode(type: ServiceKeyResponse.self, decoder: JSONDecoder())
|
.decode(type: ServiceKeyResponse.self, decoder: JSONDecoder())
|
||||||
|
|
@ -24,11 +32,45 @@ public class Client {
|
||||||
.map { return (serviceKey, $0)}
|
.map { return (serviceKey, $0)}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
.flatMap { (serviceKey, hashcash) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
|
.flatMap { (serviceKey, hashcash) -> AnyPublisher<(String, String, ServerSRPInitResponse), Swift.Error> in
|
||||||
|
|
||||||
return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password, hashcash: hashcash))
|
return Current.network.dataTask(with: URLRequest.SRPInit(serviceKey: serviceKey, a: Data(a.bytes).base64EncodedString(), accountName: accountName))
|
||||||
|
.map(\.data)
|
||||||
|
.decode(type: ServerSRPInitResponse.self, decoder: JSONDecoder())
|
||||||
|
.map { return (serviceKey, hashcash, $0) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.flatMap { (serviceKey, hashcash, srpInit) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
|
||||||
|
guard let decodedB = Data(base64Encoded: srpInit.b) else {
|
||||||
|
return Fail(error: AuthenticationError.srpInvalidPublicKey)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let decodedSalt = Data(base64Encoded: srpInit.salt) else {
|
||||||
|
return Fail(error: AuthenticationError.srpInvalidPublicKey)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
let iterations = srpInit.iteration
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard let encryptedPassword = self.pbkdf2(password: password, saltData: decodedSalt, keyByteCount: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), rounds: iterations, protocol: srpInit.protocol) else {
|
||||||
|
return Fail(error: AuthenticationError.srpInvalidPublicKey)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
let sharedSecret = try client.calculateSharedSecret(password: encryptedPassword, salt: [UInt8](decodedSalt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))
|
||||||
|
|
||||||
|
let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](decodedSalt), clientPublicKey: a, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
|
||||||
|
let m2 = client.calculateServerProof(clientPublicKey: a, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))
|
||||||
|
|
||||||
|
return Current.network.dataTask(with: URLRequest.SRPComplete(serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit.c, m1: Data(m1).base64EncodedString(), m2: Data(m2).base64EncodedString()))
|
||||||
.mapError { $0 as Swift.Error }
|
.mapError { $0 as Swift.Error }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
} catch {
|
||||||
|
return Fail(error: AuthenticationError.srpInvalidPublicKey)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.flatMap { result -> AnyPublisher<AuthenticationState, Swift.Error> in
|
.flatMap { result -> AnyPublisher<AuthenticationState, Swift.Error> in
|
||||||
let (data, response) = result
|
let (data, response) = result
|
||||||
|
|
@ -118,7 +160,7 @@ public class Client {
|
||||||
case .twoStep:
|
case .twoStep:
|
||||||
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
|
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
case .twoFactor:
|
case .twoFactor, .securityKey:
|
||||||
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
|
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
case .unknown:
|
case .unknown:
|
||||||
|
|
@ -139,7 +181,10 @@ public class Client {
|
||||||
// SMS wasn't sent automatically because user needs to choose a phone to send to
|
// SMS wasn't sent automatically because user needs to choose a phone to send to
|
||||||
} else if authOptions.canFallBackToSMS {
|
} else if authOptions.canFallBackToSMS {
|
||||||
option = .smsPendingChoice
|
option = .smsPendingChoice
|
||||||
// Code is shown on trusted devices
|
// Code is shown on trusted devices
|
||||||
|
} else if authOptions.fsaChallenge != nil {
|
||||||
|
option = .securityKey
|
||||||
|
// User needs to use a physical security key to respond to the challenge
|
||||||
} else {
|
} else {
|
||||||
option = .codeSent
|
option = .codeSent
|
||||||
}
|
}
|
||||||
|
|
@ -193,6 +238,33 @@ public class Client {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func submitChallenge(response: Data, sessionData: AppleSessionData) -> AnyPublisher<AuthenticationState, Error> {
|
||||||
|
Result {
|
||||||
|
URLRequest.respondToChallenge(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, response: response)
|
||||||
|
}
|
||||||
|
.publisher
|
||||||
|
.flatMap { request in
|
||||||
|
Current.network.dataTask(with: request)
|
||||||
|
.mapError { $0 as Error }
|
||||||
|
.tryMap { (data, response) throws -> (Data, URLResponse) in
|
||||||
|
guard let urlResponse = response as? HTTPURLResponse else { return (data, response) }
|
||||||
|
switch urlResponse.statusCode {
|
||||||
|
case 200..<300:
|
||||||
|
return (data, urlResponse)
|
||||||
|
case 400, 401:
|
||||||
|
throw AuthenticationError.incorrectSecurityCode
|
||||||
|
case 412:
|
||||||
|
throw AuthenticationError.appleIDAndPrivacyAcknowledgementRequired
|
||||||
|
case let code:
|
||||||
|
throw AuthenticationError.badStatusCode(statusCode: code, data: data, response: urlResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap { (data, response) -> AnyPublisher<AuthenticationState, Error> in
|
||||||
|
self.updateSession(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
|
||||||
|
}
|
||||||
|
}.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Session
|
// MARK: - Session
|
||||||
|
|
||||||
/// Use the olympus session endpoint to see if the existing session is still valid
|
/// Use the olympus session endpoint to see if the existing session is still valid
|
||||||
|
|
@ -227,6 +299,49 @@ public class Client {
|
||||||
.mapError { $0 as Error }
|
.mapError { $0 as Error }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sha256(data : Data) -> Data {
|
||||||
|
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||||
|
data.withUnsafeBytes {
|
||||||
|
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
|
||||||
|
}
|
||||||
|
return Data(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int, protocol srpProtocol: SRPProtocol) -> Data? {
|
||||||
|
guard let passwordData = password.data(using: .utf8) else { return nil }
|
||||||
|
let hashedPasswordDataRaw = sha256(data: passwordData)
|
||||||
|
let hashedPasswordData = switch srpProtocol {
|
||||||
|
case .s2k: hashedPasswordDataRaw
|
||||||
|
// the legacy s2k_fo protocol requires hex-encoding the digest before performing PBKDF2.
|
||||||
|
case .s2k_fo: Data(hashedPasswordDataRaw.hexEncodedString().lowercased().utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
|
||||||
|
let derivedCount = derivedKeyData.count
|
||||||
|
let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
|
||||||
|
let keyBuffer: UnsafeMutablePointer<UInt8> =
|
||||||
|
derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||||
|
return saltData.withUnsafeBytes { saltBytes -> Int32 in
|
||||||
|
let saltBuffer: UnsafePointer<UInt8> = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||||
|
return hashedPasswordData.withUnsafeBytes { hashedPasswordBytes -> Int32 in
|
||||||
|
let passwordBuffer: UnsafePointer<UInt8> = hashedPasswordBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||||
|
return CCKeyDerivationPBKDF(
|
||||||
|
CCPBKDFAlgorithm(kCCPBKDF2),
|
||||||
|
passwordBuffer,
|
||||||
|
hashedPasswordData.count,
|
||||||
|
saltBuffer,
|
||||||
|
saltData.count,
|
||||||
|
prf,
|
||||||
|
UInt32(rounds),
|
||||||
|
keyBuffer,
|
||||||
|
derivedCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return derivationStatus == kCCSuccess ? derivedKeyData : nil
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Types
|
// MARK: - Types
|
||||||
|
|
@ -252,6 +367,7 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
|
||||||
case notDeveloperAppleId
|
case notDeveloperAppleId
|
||||||
case notAuthorized
|
case notAuthorized
|
||||||
case invalidResult(resultString: String?)
|
case invalidResult(resultString: String?)
|
||||||
|
case srpInvalidPublicKey
|
||||||
|
|
||||||
public var errorDescription: String? {
|
public var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
|
@ -286,6 +402,8 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
|
||||||
return "You are not authorized. Please Sign in with your Apple ID first."
|
return "You are not authorized. Please Sign in with your Apple ID first."
|
||||||
case let .invalidResult(resultString):
|
case let .invalidResult(resultString):
|
||||||
return resultString ?? "If you continue to have problems, please submit a bug report in the Help menu."
|
return resultString ?? "If you continue to have problems, please submit a bug report in the Help menu."
|
||||||
|
case .srpInvalidPublicKey:
|
||||||
|
return "Invalid Key"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -326,27 +444,37 @@ public enum TwoFactorOption: Equatable {
|
||||||
case smsSent(AuthOptionsResponse.TrustedPhoneNumber)
|
case smsSent(AuthOptionsResponse.TrustedPhoneNumber)
|
||||||
case codeSent
|
case codeSent
|
||||||
case smsPendingChoice
|
case smsPendingChoice
|
||||||
|
case securityKey
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FSAChallenge: Equatable, Decodable {
|
||||||
|
public let challenge: String
|
||||||
|
public let keyHandles: [String]
|
||||||
|
public let allowedCredentials: String
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct AuthOptionsResponse: Equatable, Decodable {
|
public struct AuthOptionsResponse: Equatable, Decodable {
|
||||||
public let trustedPhoneNumbers: [TrustedPhoneNumber]?
|
public let trustedPhoneNumbers: [TrustedPhoneNumber]?
|
||||||
public let trustedDevices: [TrustedDevice]?
|
public let trustedDevices: [TrustedDevice]?
|
||||||
public let securityCode: SecurityCodeInfo
|
public let securityCode: SecurityCodeInfo?
|
||||||
public let noTrustedDevices: Bool?
|
public let noTrustedDevices: Bool?
|
||||||
public let serviceErrors: [ServiceError]?
|
public let serviceErrors: [ServiceError]?
|
||||||
|
public let fsaChallenge: FSAChallenge?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?,
|
trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?,
|
||||||
trustedDevices: [AuthOptionsResponse.TrustedDevice]?,
|
trustedDevices: [AuthOptionsResponse.TrustedDevice]?,
|
||||||
securityCode: AuthOptionsResponse.SecurityCodeInfo,
|
securityCode: AuthOptionsResponse.SecurityCodeInfo,
|
||||||
noTrustedDevices: Bool? = nil,
|
noTrustedDevices: Bool? = nil,
|
||||||
serviceErrors: [ServiceError]? = nil
|
serviceErrors: [ServiceError]? = nil,
|
||||||
|
fsaChallenge: FSAChallenge? = nil
|
||||||
) {
|
) {
|
||||||
self.trustedPhoneNumbers = trustedPhoneNumbers
|
self.trustedPhoneNumbers = trustedPhoneNumbers
|
||||||
self.trustedDevices = trustedDevices
|
self.trustedDevices = trustedDevices
|
||||||
self.securityCode = securityCode
|
self.securityCode = securityCode
|
||||||
self.noTrustedDevices = noTrustedDevices
|
self.noTrustedDevices = noTrustedDevices
|
||||||
self.serviceErrors = serviceErrors
|
self.serviceErrors = serviceErrors
|
||||||
|
self.fsaChallenge = fsaChallenge
|
||||||
}
|
}
|
||||||
|
|
||||||
public var kind: Kind {
|
public var kind: Kind {
|
||||||
|
|
@ -354,6 +482,8 @@ public struct AuthOptionsResponse: Equatable, Decodable {
|
||||||
return .twoStep
|
return .twoStep
|
||||||
} else if trustedPhoneNumbers != nil {
|
} else if trustedPhoneNumbers != nil {
|
||||||
return .twoFactor
|
return .twoFactor
|
||||||
|
} else if fsaChallenge != nil {
|
||||||
|
return .securityKey
|
||||||
} else {
|
} else {
|
||||||
return .unknown
|
return .unknown
|
||||||
}
|
}
|
||||||
|
|
@ -416,7 +546,7 @@ public struct AuthOptionsResponse: Equatable, Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Kind: Equatable {
|
public enum Kind: Equatable {
|
||||||
case twoStep, twoFactor, unknown
|
case twoStep, twoFactor, securityKey, unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -453,3 +583,24 @@ public struct AppleProvider: Decodable, Equatable {
|
||||||
public struct AppleUser: Decodable, Equatable {
|
public struct AppleUser: Decodable, Equatable {
|
||||||
public let fullName: String
|
public let fullName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct ServerSRPInitResponse: Decodable {
|
||||||
|
let iteration: Int
|
||||||
|
let salt: String
|
||||||
|
let b: String
|
||||||
|
let c: String
|
||||||
|
let `protocol`: SRPProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
func base64ToU8Array() -> Data {
|
||||||
|
return Data(base64Encoded: self) ?? Data()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extension Data {
|
||||||
|
func hexEncodedString() -> String {
|
||||||
|
return map { String(format: "%02hhx", $0) }.joined()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ public extension URL {
|
||||||
static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")!
|
static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")!
|
||||||
static let federate = URL(string: "https://idmsa.apple.com/appleauth/auth/federate")!
|
static let federate = URL(string: "https://idmsa.apple.com/appleauth/auth/federate")!
|
||||||
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!
|
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!
|
||||||
|
static let keyAuth = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/security/key")!
|
||||||
|
|
||||||
|
static let srpInit = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/init")!
|
||||||
|
static let srpComplete = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/complete?isRememberMeEnabled=false")!
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension URLRequest {
|
public extension URLRequest {
|
||||||
|
|
@ -106,6 +111,19 @@ public extension URLRequest {
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func respondToChallenge(serviceKey: String, sessionID: String, scnt: String, response: Data) -> URLRequest {
|
||||||
|
var request = URLRequest(url: .keyAuth)
|
||||||
|
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
|
||||||
|
request.allHTTPHeaderFields?["X-Apple-ID-Session-Id"] = sessionID
|
||||||
|
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
|
||||||
|
request.allHTTPHeaderFields?["scnt"] = scnt
|
||||||
|
request.allHTTPHeaderFields?["Accept"] = "application/json"
|
||||||
|
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.httpBody = response
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
static func trust(serviceKey: String, sessionID: String, scnt: String) -> URLRequest {
|
static func trust(serviceKey: String, sessionID: String, scnt: String) -> URLRequest {
|
||||||
var request = URLRequest(url: .trust)
|
var request = URLRequest(url: .trust)
|
||||||
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
|
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
|
||||||
|
|
@ -136,4 +154,51 @@ public extension URLRequest {
|
||||||
|
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func SRPInit(serviceKey: String, a: String, accountName: String) -> URLRequest {
|
||||||
|
struct ServerSRPInitRequest: Encodable {
|
||||||
|
public let a: String
|
||||||
|
public let accountName: String
|
||||||
|
public let protocols: [SRPProtocol]
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: .srpInit)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
|
||||||
|
request.allHTTPHeaderFields?["Accept"] = "application/json"
|
||||||
|
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
|
||||||
|
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
|
||||||
|
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
|
||||||
|
|
||||||
|
request.httpBody = try? JSONEncoder().encode(ServerSRPInitRequest(a: a, accountName: accountName, protocols: [.s2k, .s2k_fo]))
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
static func SRPComplete(serviceKey: String, hashcash: String, accountName: String, c: String, m1: String, m2: String) -> URLRequest {
|
||||||
|
struct ServerSRPCompleteRequest: Encodable {
|
||||||
|
let accountName: String
|
||||||
|
let c: String
|
||||||
|
let m1: String
|
||||||
|
let m2: String
|
||||||
|
let rememberMe: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: .srpComplete)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
|
||||||
|
request.allHTTPHeaderFields?["Accept"] = "application/json"
|
||||||
|
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
|
||||||
|
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
|
||||||
|
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
|
||||||
|
request.allHTTPHeaderFields?["X-Apple-HC"] = hashcash
|
||||||
|
|
||||||
|
request.httpBody = try? JSONEncoder().encode(ServerSRPCompleteRequest(accountName: accountName, c: c, m1: m1, m2: m2, rememberMe: false))
|
||||||
|
return request
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum SRPProtocol: String, Codable {
|
||||||
|
case s2k, s2k_fo
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ extension AppState {
|
||||||
|
|
||||||
// check to see if we should auto install for the user
|
// check to see if we should auto install for the user
|
||||||
public func autoInstallIfNeeded() {
|
public func autoInstallIfNeeded() {
|
||||||
guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
|
guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
|
||||||
|
|
||||||
if autoInstallType == .none { return }
|
if autoInstallType == .none { return }
|
||||||
|
|
||||||
|
|
@ -227,6 +227,7 @@ extension AppState {
|
||||||
self.error = error
|
self.error = error
|
||||||
self.presentedAlert = .generic(title: localizeString("Alert.InstallArchive.Error.Title"), message: error.legibleLocalizedDescription)
|
self.presentedAlert = .generic(title: localizeString("Alert.InstallArchive.Error.Title"), message: error.legibleLocalizedDescription)
|
||||||
}
|
}
|
||||||
|
resetDockProgressTracking()
|
||||||
})
|
})
|
||||||
.catch { _ in
|
.catch { _ in
|
||||||
Just(installedXcode)
|
Just(installedXcode)
|
||||||
|
|
@ -473,19 +474,24 @@ extension AppState {
|
||||||
// MARK: - Dock Progress Tracking
|
// MARK: - Dock Progress Tracking
|
||||||
|
|
||||||
private func setupDockProgress() {
|
private func setupDockProgress() {
|
||||||
DockProgress.progressInstance = nil
|
Task { @MainActor in
|
||||||
DockProgress.style = .bar
|
DockProgress.progressInstance = nil
|
||||||
|
DockProgress.style = .bar
|
||||||
|
|
||||||
let progress = Progress(totalUnitCount: AppState.totalProgressUnits)
|
let progress = Progress(totalUnitCount: AppState.totalProgressUnits)
|
||||||
progress.kind = .file
|
progress.kind = .file
|
||||||
progress.fileOperationKind = .downloading
|
progress.fileOperationKind = .downloading
|
||||||
overallProgress = progress
|
overallProgress = progress
|
||||||
|
|
||||||
|
DockProgress.progressInstance = overallProgress
|
||||||
|
}
|
||||||
|
|
||||||
DockProgress.progressInstance = overallProgress
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetDockProgressTracking() {
|
func resetDockProgressTracking() {
|
||||||
DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete
|
Task { @MainActor in
|
||||||
|
DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
@ -496,7 +502,7 @@ extension AppState {
|
||||||
self.allXcodes[index].installState = .installing(step)
|
self.allXcodes[index].installState = .installing(step)
|
||||||
|
|
||||||
let xcode = self.allXcodes[index]
|
let xcode = self.allXcodes[index]
|
||||||
Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal)
|
Current.notificationManager.scheduleNotification(title: xcode.version.major.description + "." + xcode.version.appleDescription, body: step.description, category: .normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import OSLog
|
||||||
import Combine
|
import Combine
|
||||||
import Path
|
import Path
|
||||||
import AppleAPI
|
import AppleAPI
|
||||||
|
import Version
|
||||||
|
|
||||||
extension AppState {
|
extension AppState {
|
||||||
func updateDownloadableRuntimes() {
|
func updateDownloadableRuntimes() {
|
||||||
|
|
@ -48,6 +49,90 @@ extension AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadRuntime(runtime: DownloadableRuntime) {
|
func downloadRuntime(runtime: DownloadableRuntime) {
|
||||||
|
guard let selectedXcode = self.allXcodes.first(where: { $0.selected }) else {
|
||||||
|
Logger.appState.error("No selected Xcode")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: "No selected Xcode. Please make an Xcode active")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// new runtimes
|
||||||
|
if runtime.contentType == .cryptexDiskImage {
|
||||||
|
// only selected xcodes > 16.1 beta 3 can download runtimes via a xcodebuild -downloadPlatform version
|
||||||
|
// only Runtimes coming from cryptexDiskImage can be downloaded via xcodebuild
|
||||||
|
if selectedXcode.version > Version(major: 16, minor: 0, patch: 0) {
|
||||||
|
|
||||||
|
if runtime.architectures?.isAppleSilicon ?? false {
|
||||||
|
// Need Xcode 26 but with some RC/Beta's its simpler to just to greater > 25
|
||||||
|
if selectedXcode.version > Version(major: 25, minor: 0, patch: 0) {
|
||||||
|
downloadRuntimeViaXcodeBuild(runtime: runtime)
|
||||||
|
} else {
|
||||||
|
// not supported
|
||||||
|
Logger.appState.error("Trying to download a runtime we can't download")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: localizeString("Alert.Install.Error.Need.Xcode26"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
downloadRuntimeViaXcodeBuild(runtime: runtime)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// not supported
|
||||||
|
Logger.appState.error("Trying to download a runtime we can't download")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: localizeString("Alert.Install.Error.Need.Xcode16.1"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
downloadRuntimeObseleteWay(runtime: runtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadRuntimeViaXcodeBuild(runtime: DownloadableRuntime) {
|
||||||
|
|
||||||
|
let downloadRuntimeTask = Current.shell.downloadRuntime(runtime.platform.shortName, runtime.simulatorVersion.buildUpdate, runtime.architectures?.isAppleSilicon ?? false ? Architecture.arm64.rawValue : nil)
|
||||||
|
|
||||||
|
runtimePublishers[runtime.identifier] = Task { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
do {
|
||||||
|
for try await progress in downloadRuntimeTask {
|
||||||
|
if progress.isIndeterminate {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setInstallationStep(of: runtime, to: .installing, postNotification: false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setInstallationStep(of: runtime, to: .downloading(progress: progress), postNotification: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Logger.appState.debug("Done downloading runtime - \(runtime.name)")
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
|
||||||
|
self.downloadableRuntimes[index].installState = .installed
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.error = error
|
||||||
|
if let error = error as? String {
|
||||||
|
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error)
|
||||||
|
} else {
|
||||||
|
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadRuntimeObseleteWay(runtime: DownloadableRuntime) {
|
||||||
runtimePublishers[runtime.identifier] = Task {
|
runtimePublishers[runtime.identifier] = Task {
|
||||||
do {
|
do {
|
||||||
let downloadedURL = try await downloadRunTimeFull(runtime: runtime)
|
let downloadedURL = try await downloadRunTimeFull(runtime: runtime)
|
||||||
|
|
@ -57,6 +142,9 @@ extension AppState {
|
||||||
self.setInstallationStep(of: runtime, to: .installing)
|
self.setInstallationStep(of: runtime, to: .installing)
|
||||||
}
|
}
|
||||||
switch runtime.contentType {
|
switch runtime.contentType {
|
||||||
|
case .cryptexDiskImage:
|
||||||
|
// not supported yet (do we need to for old packages?)
|
||||||
|
throw "Installing via cryptexDiskImage not support - please install manually from \(downloadedURL.description)"
|
||||||
case .package:
|
case .package:
|
||||||
// not supported yet (do we need to for old packages?)
|
// not supported yet (do we need to for old packages?)
|
||||||
throw "Installing via package not support - please install manually from \(downloadedURL.description)"
|
throw "Installing via package not support - please install manually from \(downloadedURL.description)"
|
||||||
|
|
@ -80,19 +168,31 @@ extension AppState {
|
||||||
Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
|
Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.error = error
|
self.error = error
|
||||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
|
if let error = error as? String {
|
||||||
|
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error)
|
||||||
|
} else {
|
||||||
|
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadRunTimeFull(runtime: DownloadableRuntime) async throws -> URL {
|
func downloadRunTimeFull(runtime: DownloadableRuntime) async throws -> URL {
|
||||||
|
guard let source = runtime.source else {
|
||||||
|
throw "Invalid runtime source"
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let downloadPath = runtime.downloadPath else {
|
||||||
|
throw "Invalid runtime downloadPath"
|
||||||
|
}
|
||||||
|
|
||||||
// sets a proper cookie for runtimes
|
// sets a proper cookie for runtimes
|
||||||
try await validateADCSession(path: runtime.downloadPath)
|
try await validateADCSession(path: downloadPath)
|
||||||
|
|
||||||
let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2
|
let downloader = Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2
|
||||||
|
|
||||||
let url = URL(string: runtime.source)!
|
let url = URL(string: source)!
|
||||||
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"
|
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"
|
||||||
// aria2 downloads directly to the destination (instead of into /tmp first) so we need to make sure that the download isn't incomplete
|
// aria2 downloads directly to the destination (instead of into /tmp first) so we need to make sure that the download isn't incomplete
|
||||||
let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")
|
let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")
|
||||||
|
|
@ -123,9 +223,15 @@ extension AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path) -> AsyncThrowingStream<Progress, Error> {
|
public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path) -> AsyncThrowingStream<Progress, Error> {
|
||||||
let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: runtime.url) ?? []
|
guard let url = runtime.url else {
|
||||||
|
return AsyncThrowingStream<Progress, Error> { continuation in
|
||||||
|
continuation.finish(throwing: "Invalid or non existant runtime url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Current.shell.downloadWithAria2Async(aria2Path, runtime.url, destination, cookies)
|
let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: url) ?? []
|
||||||
|
|
||||||
|
return Current.shell.downloadWithAria2Async(aria2Path, url, destination, cookies)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -140,7 +246,10 @@ extension AppState {
|
||||||
runtimePublishers[runtime.identifier] = nil
|
runtimePublishers[runtime.identifier] = nil
|
||||||
|
|
||||||
// If the download is cancelled by the user, clean up the download files that aria2 creates.
|
// If the download is cancelled by the user, clean up the download files that aria2 creates.
|
||||||
let url = URL(string: runtime.source)!
|
guard let source = runtime.source else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let url = URL(string: source)!
|
||||||
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"
|
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"
|
||||||
let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")
|
let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")
|
||||||
|
|
||||||
|
|
@ -166,7 +275,10 @@ extension AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
func coreSimulatorInfo(runtime: DownloadableRuntime) -> CoreSimulatorImage? {
|
func coreSimulatorInfo(runtime: DownloadableRuntime) -> CoreSimulatorImage? {
|
||||||
return installedRuntimes.filter({ $0.runtimeInfo.build == runtime.simulatorVersion.buildUpdate }).first
|
return installedRuntimes.filter({
|
||||||
|
$0.runtimeInfo.build == runtime.simulatorVersion.buildUpdate &&
|
||||||
|
((runtime.architectures ?? []).isEmpty ? true :
|
||||||
|
$0.runtimeInfo.supportedArchitectures == runtime.architectures )}).first
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteRuntime(runtime: DownloadableRuntime) async throws {
|
func deleteRuntime(runtime: DownloadableRuntime) async throws {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import Foundation
|
||||||
import Path
|
import Path
|
||||||
import Version
|
import Version
|
||||||
import SwiftSoup
|
import SwiftSoup
|
||||||
import struct XCModel.Xcode
|
|
||||||
import AppleAPI
|
import AppleAPI
|
||||||
import XcodesKit
|
import XcodesKit
|
||||||
|
|
||||||
|
|
@ -211,7 +210,7 @@ extension AppState {
|
||||||
private func xcodeReleases() -> AnyPublisher<[AvailableXcode], Error> {
|
private func xcodeReleases() -> AnyPublisher<[AvailableXcode], Error> {
|
||||||
Current.network.dataTask(with: URLRequest(url: URL(string: "https://xcodereleases.com/data.json")!))
|
Current.network.dataTask(with: URLRequest(url: URL(string: "https://xcodereleases.com/data.json")!))
|
||||||
.map(\.data)
|
.map(\.data)
|
||||||
.decode(type: [XCModel.Xcode].self, decoder: JSONDecoder())
|
.decode(type: [XcodeRelease].self, decoder: JSONDecoder())
|
||||||
.map { xcReleasesXcodes in
|
.map { xcReleasesXcodes in
|
||||||
let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> AvailableXcode? in
|
let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> AvailableXcode? in
|
||||||
guard
|
guard
|
||||||
|
|
@ -233,7 +232,8 @@ extension AppState {
|
||||||
requiredMacOSVersion: xcReleasesXcode.requires,
|
requiredMacOSVersion: xcReleasesXcode.requires,
|
||||||
releaseNotesURL: xcReleasesXcode.links?.notes?.url,
|
releaseNotesURL: xcReleasesXcode.links?.notes?.url,
|
||||||
sdks: xcReleasesXcode.sdks,
|
sdks: xcReleasesXcode.sdks,
|
||||||
compilers: xcReleasesXcode.compilers
|
compilers: xcReleasesXcode.compilers,
|
||||||
|
architectures: xcReleasesXcode.architectures
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return xcodes
|
return xcodes
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,27 @@ import Version
|
||||||
import os.log
|
import os.log
|
||||||
import DockProgress
|
import DockProgress
|
||||||
import XcodesKit
|
import XcodesKit
|
||||||
|
import LibFido2Swift
|
||||||
|
|
||||||
|
enum PreferenceKey: String {
|
||||||
|
case installPath
|
||||||
|
case localPath
|
||||||
|
case unxipExperiment
|
||||||
|
case createSymLinkOnSelect
|
||||||
|
case onSelectActionType
|
||||||
|
case showOpenInRosettaOption
|
||||||
|
case autoInstallation
|
||||||
|
case SUEnableAutomaticChecks
|
||||||
|
case includePrereleaseVersions
|
||||||
|
case downloader
|
||||||
|
case dataSource
|
||||||
|
case xcodeListCategory
|
||||||
|
case allowedMajorVersions
|
||||||
|
case hideSupportXcodes
|
||||||
|
case xcodeListArchitectures
|
||||||
|
|
||||||
|
func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) }
|
||||||
|
}
|
||||||
|
|
||||||
class AppState: ObservableObject {
|
class AppState: ObservableObject {
|
||||||
private let client = AppleAPI.Client()
|
private let client = AppleAPI.Client()
|
||||||
|
|
@ -66,18 +87,24 @@ class AppState: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var disableLocalPathChange: Bool { PreferenceKey.localPath.isManaged() }
|
||||||
|
|
||||||
@Published var installPath = "" {
|
@Published var installPath = "" {
|
||||||
didSet {
|
didSet {
|
||||||
Current.defaults.set(installPath, forKey: "installPath")
|
Current.defaults.set(installPath, forKey: "installPath")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var disableInstallPathChange: Bool { PreferenceKey.installPath.isManaged() }
|
||||||
|
|
||||||
@Published var unxipExperiment = false {
|
@Published var unxipExperiment = false {
|
||||||
didSet {
|
didSet {
|
||||||
Current.defaults.set(unxipExperiment, forKey: "unxipExperiment")
|
Current.defaults.set(unxipExperiment, forKey: "unxipExperiment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var disableUnxipExperiment: Bool { PreferenceKey.unxipExperiment.isManaged() }
|
||||||
|
|
||||||
@Published var createSymLinkOnSelect = false {
|
@Published var createSymLinkOnSelect = false {
|
||||||
didSet {
|
didSet {
|
||||||
Current.defaults.set(createSymLinkOnSelect, forKey: "createSymLinkOnSelect")
|
Current.defaults.set(createSymLinkOnSelect, forKey: "createSymLinkOnSelect")
|
||||||
|
|
@ -85,7 +112,7 @@ class AppState: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
var createSymLinkOnSelectDisabled: Bool {
|
var createSymLinkOnSelectDisabled: Bool {
|
||||||
return onSelectActionType == .rename
|
return onSelectActionType == .rename || PreferenceKey.createSymLinkOnSelect.isManaged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var onSelectActionType = SelectedActionType.none {
|
@Published var onSelectActionType = SelectedActionType.none {
|
||||||
|
|
@ -98,12 +125,20 @@ class AppState: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var onSelectActionTypeDisabled: Bool { PreferenceKey.onSelectActionType.isManaged() }
|
||||||
|
|
||||||
@Published var showOpenInRosettaOption = false {
|
@Published var showOpenInRosettaOption = false {
|
||||||
didSet {
|
didSet {
|
||||||
Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption")
|
Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var terminateAfterLastWindowClosed = false {
|
||||||
|
didSet {
|
||||||
|
Current.defaults.set(terminateAfterLastWindowClosed, forKey: "terminateAfterLastWindowClosed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Runtimes
|
// MARK: - Runtimes
|
||||||
|
|
||||||
@Published var downloadableRuntimes: [DownloadableRuntime] = []
|
@Published var downloadableRuntimes: [DownloadableRuntime] = []
|
||||||
|
|
@ -112,7 +147,7 @@ class AppState: ObservableObject {
|
||||||
// MARK: - Publisher Cancellables
|
// MARK: - Publisher Cancellables
|
||||||
|
|
||||||
var cancellables = Set<AnyCancellable>()
|
var cancellables = Set<AnyCancellable>()
|
||||||
private var installationPublishers: [Version: AnyCancellable] = [:]
|
private var installationPublishers: [XcodeID: AnyCancellable] = [:]
|
||||||
internal var runtimePublishers: [String: Task<(), any Error>] = [:]
|
internal var runtimePublishers: [String: Task<(), any Error>] = [:]
|
||||||
private var selectPublisher: AnyCancellable?
|
private var selectPublisher: AnyCancellable?
|
||||||
private var uninstallPublisher: AnyCancellable?
|
private var uninstallPublisher: AnyCancellable?
|
||||||
|
|
@ -173,12 +208,13 @@ class AppState: ObservableObject {
|
||||||
onSelectActionType = SelectedActionType(rawValue: Current.defaults.string(forKey: "onSelectActionType") ?? "none") ?? .none
|
onSelectActionType = SelectedActionType(rawValue: Current.defaults.string(forKey: "onSelectActionType") ?? "none") ?? .none
|
||||||
installPath = Current.defaults.string(forKey: "installPath") ?? Path.defaultInstallDirectory.string
|
installPath = Current.defaults.string(forKey: "installPath") ?? Path.defaultInstallDirectory.string
|
||||||
showOpenInRosettaOption = Current.defaults.bool(forKey: "showOpenInRosettaOption") ?? false
|
showOpenInRosettaOption = Current.defaults.bool(forKey: "showOpenInRosettaOption") ?? false
|
||||||
|
terminateAfterLastWindowClosed = Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Timer
|
// MARK: Timer
|
||||||
/// Runs a timer every 6 hours when app is open to check if it needs to auto install any xcodes
|
/// Runs a timer every 6 hours when app is open to check if it needs to auto install any xcodes
|
||||||
func setupAutoInstallTimer() {
|
func setupAutoInstallTimer() {
|
||||||
guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
|
guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
|
||||||
|
|
||||||
if autoInstallType == .none { return }
|
if autoInstallType == .none { return }
|
||||||
|
|
||||||
|
|
@ -242,7 +278,7 @@ class AppState: ObservableObject {
|
||||||
|
|
||||||
func signIn(username: String, password: String) {
|
func signIn(username: String, password: String) {
|
||||||
authError = nil
|
authError = nil
|
||||||
signIn(username: username, password: password)
|
signIn(username: username.lowercased(), password: password)
|
||||||
.sink(
|
.sink(
|
||||||
receiveCompletion: { _ in },
|
receiveCompletion: { _ in },
|
||||||
receiveValue: { _ in }
|
receiveValue: { _ in }
|
||||||
|
|
@ -255,7 +291,7 @@ class AppState: ObservableObject {
|
||||||
Current.defaults.set(username, forKey: "username")
|
Current.defaults.set(username, forKey: "username")
|
||||||
|
|
||||||
isProcessingAuthRequest = true
|
isProcessingAuthRequest = true
|
||||||
return client.login(accountName: username, password: password)
|
return client.srpLogin(accountName: username, password: password)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
receiveOutput: { authenticationState in
|
receiveOutput: { authenticationState in
|
||||||
|
|
@ -270,11 +306,17 @@ class AppState: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
|
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
|
||||||
self.presentedSheet = .twoFactor(.init(
|
let sessionData = AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||||
option: option,
|
|
||||||
authOptions: authOptions,
|
if option == .securityKey, fido2DeviceIsPresent() && !fido2DeviceNeedsPin() {
|
||||||
sessionData: AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
createAndSubmitSecurityKeyAssertationWithPinCode(nil, sessionData: sessionData, authOptions: authOptions)
|
||||||
))
|
} else {
|
||||||
|
self.presentedSheet = .twoFactor(.init(
|
||||||
|
option: option,
|
||||||
|
authOptions: authOptions,
|
||||||
|
sessionData: sessionData
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestSMS(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) {
|
func requestSMS(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) {
|
||||||
|
|
@ -320,6 +362,83 @@ class AppState: ObservableObject {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lazy var fido2 = FIDO2()
|
||||||
|
|
||||||
|
func createAndSubmitSecurityKeyAssertationWithPinCode(_ pinCode: String?, sessionData: AppleSessionData, authOptions: AuthOptionsResponse) {
|
||||||
|
self.presentedSheet = .securityKeyTouchToConfirm
|
||||||
|
|
||||||
|
guard let fsaChallenge = authOptions.fsaChallenge else {
|
||||||
|
// This shouldn't happen
|
||||||
|
// we shouldn't have called this method without setting the fsaChallenge
|
||||||
|
// so this is an assertionFailure
|
||||||
|
assertionFailure()
|
||||||
|
self.authError = "Something went wrong. Please file a bug report"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The challenge is encoded in Base64URL encoding
|
||||||
|
let challengeUrl = fsaChallenge.challenge
|
||||||
|
let challenge = FIDO2.base64urlToBase64(base64url: challengeUrl)
|
||||||
|
let origin = "https://idmsa.apple.com"
|
||||||
|
let rpId = "apple.com"
|
||||||
|
// Allowed creds is sent as a comma separated string
|
||||||
|
let validCreds = fsaChallenge.allowedCredentials.split(separator: ",").map(String.init)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let response = try fido2.respondToChallenge(args: ChallengeArgs(rpId: rpId, validCredentials: validCreds, devPin: pinCode, challenge: challenge, origin: origin))
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
self.isProcessingAuthRequest = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let respData = try JSONEncoder().encode(response)
|
||||||
|
client.submitChallenge(response: respData, sessionData: AppleSessionData(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt))
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.handleEvents(
|
||||||
|
receiveOutput: { authenticationState in
|
||||||
|
self.authenticationState = authenticationState
|
||||||
|
},
|
||||||
|
receiveCompletion: { completion in
|
||||||
|
self.handleAuthenticationFlowCompletion(completion)
|
||||||
|
self.isProcessingAuthRequest = false
|
||||||
|
}
|
||||||
|
).sink(
|
||||||
|
receiveCompletion: { _ in },
|
||||||
|
receiveValue: { _ in }
|
||||||
|
).store(in: &cancellables)
|
||||||
|
} catch FIDO2Error.canceledByUser {
|
||||||
|
// User cancelled the auth flow
|
||||||
|
// we don't have to show an error
|
||||||
|
// because the sheet will already be dismissed
|
||||||
|
} catch {
|
||||||
|
Task { @MainActor in
|
||||||
|
authError = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fido2DeviceIsPresent() -> Bool {
|
||||||
|
fido2.hasDeviceAttached()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fido2DeviceNeedsPin() -> Bool {
|
||||||
|
do {
|
||||||
|
return try fido2.deviceHasPin()
|
||||||
|
} catch {
|
||||||
|
Task { @MainActor in
|
||||||
|
authError = error
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelSecurityKeyAssertationRequest() {
|
||||||
|
self.fido2.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
private func handleAuthenticationFlowCompletion(_ completion: Subscribers.Completion<Error>) {
|
private func handleAuthenticationFlowCompletion(_ completion: Subscribers.Completion<Error>) {
|
||||||
switch completion {
|
switch completion {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
|
|
@ -405,10 +524,10 @@ class AppState: ObservableObject {
|
||||||
|
|
||||||
// MARK: - Install
|
// MARK: - Install
|
||||||
|
|
||||||
func checkMinVersionAndInstall(id: Xcode.ID) {
|
func checkMinVersionAndInstall(id: XcodeID) {
|
||||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
|
||||||
|
|
||||||
// Check to see if users MacOS is supported
|
// Check to see if users macOS is supported
|
||||||
if let requiredMacOSVersion = availableXcode.requiredMacOSVersion {
|
if let requiredMacOSVersion = availableXcode.requiredMacOSVersion {
|
||||||
if hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) {
|
if hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) {
|
||||||
// prompt
|
// prompt
|
||||||
|
|
@ -432,10 +551,15 @@ class AppState: ObservableObject {
|
||||||
return !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion)
|
return !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func install(id: Xcode.ID) {
|
func install(id: XcodeID) {
|
||||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
|
||||||
|
|
||||||
installationPublishers[id] = signInIfNeeded()
|
installationPublishers[id] = signInIfNeeded()
|
||||||
|
.handleEvents(
|
||||||
|
receiveSubscription: { [unowned self] _ in
|
||||||
|
self.setInstallationStep(of: availableXcode.version, to: .authenticating)
|
||||||
|
}
|
||||||
|
)
|
||||||
.flatMap { [unowned self] in
|
.flatMap { [unowned self] in
|
||||||
// signInIfNeeded might finish before the user actually authenticates if UI is involved.
|
// signInIfNeeded might finish before the user actually authenticates if UI is involved.
|
||||||
// This publisher will wait for the @Published authentication state to change to authenticated or unauthenticated before finishing,
|
// This publisher will wait for the @Published authentication state to change to authenticated or unauthenticated before finishing,
|
||||||
|
|
@ -479,7 +603,7 @@ class AppState: ObservableObject {
|
||||||
.mapError { $0 as Error }
|
.mapError { $0 as Error }
|
||||||
}
|
}
|
||||||
.flatMap { [unowned self] in
|
.flatMap { [unowned self] in
|
||||||
self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
|
self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
|
||||||
}
|
}
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(
|
.sink(
|
||||||
|
|
@ -487,7 +611,11 @@ class AppState: ObservableObject {
|
||||||
self.installationPublishers[id] = nil
|
self.installationPublishers[id] = nil
|
||||||
if case let .failure(error) = completion {
|
if case let .failure(error) = completion {
|
||||||
// Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
|
// Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
|
||||||
if error as? AuthenticationError != .invalidSession {
|
if let error = error as? AuthenticationError, case .notAuthorized = error {
|
||||||
|
self.error = error
|
||||||
|
self.presentedAlert = .unauthenticated
|
||||||
|
|
||||||
|
} else if error as? AuthenticationError != .invalidSession {
|
||||||
self.error = error
|
self.error = error
|
||||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
|
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
|
||||||
}
|
}
|
||||||
|
|
@ -503,9 +631,9 @@ class AppState: ObservableObject {
|
||||||
/// Skips using the username/password to log in to Apple, and simply gets a Auth Cookie used in downloading
|
/// Skips using the username/password to log in to Apple, and simply gets a Auth Cookie used in downloading
|
||||||
/// As of Nov 2022 this was returning a 403 forbidden
|
/// As of Nov 2022 this was returning a 403 forbidden
|
||||||
func installWithoutLogin(id: Xcode.ID) {
|
func installWithoutLogin(id: Xcode.ID) {
|
||||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
|
||||||
|
|
||||||
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
|
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(
|
.sink(
|
||||||
receiveCompletion: { [unowned self] completion in
|
receiveCompletion: { [unowned self] completion in
|
||||||
|
|
@ -526,7 +654,7 @@ class AppState: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelInstall(id: Xcode.ID) {
|
func cancelInstall(id: Xcode.ID) {
|
||||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
|
||||||
|
|
||||||
// Cancel the publisher
|
// Cancel the publisher
|
||||||
installationPublishers[id] = nil
|
installationPublishers[id] = nil
|
||||||
|
|
@ -644,7 +772,7 @@ class AppState: ObservableObject {
|
||||||
config.allowsRunningApplicationSubstitution = false
|
config.allowsRunningApplicationSubstitution = false
|
||||||
NSWorkspace.shared.openApplication(at: path.url, configuration: config)
|
NSWorkspace.shared.openApplication(at: path.url, configuration: config)
|
||||||
default:
|
default:
|
||||||
Logger.appState.error("\(xcode.id) is not installed")
|
Logger.appState.error("\(xcode.id.version) is not installed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -740,7 +868,7 @@ class AppState: ObservableObject {
|
||||||
// If build metadata matches exactly, replace the available version with the installed version.
|
// If build metadata matches exactly, replace the available version with the installed version.
|
||||||
// This should handle Apple versions from /downloads/more which don't have build metadata identifiers.
|
// This should handle Apple versions from /downloads/more which don't have build metadata identifiers.
|
||||||
if let index = adjustedAvailableXcodes.map(\.version).firstIndex(where: { $0.buildMetadataIdentifiers == installedXcode.version.buildMetadataIdentifiers }) {
|
if let index = adjustedAvailableXcodes.map(\.version).firstIndex(where: { $0.buildMetadataIdentifiers == installedXcode.version.buildMetadataIdentifiers }) {
|
||||||
adjustedAvailableXcodes[index].version = installedXcode.version
|
adjustedAvailableXcodes[index].xcodeID = installedXcode.xcodeID
|
||||||
}
|
}
|
||||||
// If an installed version is the same as one that's listed online which doesn't have build metadata, replace it with the installed version
|
// If an installed version is the same as one that's listed online which doesn't have build metadata, replace it with the installed version
|
||||||
// Not all prerelease Apple versions available online include build metadata
|
// Not all prerelease Apple versions available online include build metadata
|
||||||
|
|
@ -748,7 +876,7 @@ class AppState: ObservableObject {
|
||||||
availableXcode.version.isEquivalent(to: installedXcode.version) &&
|
availableXcode.version.isEquivalent(to: installedXcode.version) &&
|
||||||
availableXcode.version.buildMetadataIdentifiers.isEmpty
|
availableXcode.version.buildMetadataIdentifiers.isEmpty
|
||||||
}) {
|
}) {
|
||||||
adjustedAvailableXcodes[index].version = installedXcode.version
|
adjustedAvailableXcodes[index].xcodeID = installedXcode.xcodeID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -765,14 +893,21 @@ class AppState: ObservableObject {
|
||||||
// Include this version if there's only one with this build identifier
|
// Include this version if there's only one with this build identifier
|
||||||
return availableXcodesWithIdenticalBuildIdentifiers.count == 1 ||
|
return availableXcodesWithIdenticalBuildIdentifiers.count == 1 ||
|
||||||
// Or if there's more than one with this build identifier and this is the release version
|
// Or if there's more than one with this build identifier and this is the release version
|
||||||
availableXcodesWithIdenticalBuildIdentifiers.count > 1 && availableXcode.version.prereleaseIdentifiers.isEmpty
|
|
||||||
|
availableXcodesWithIdenticalBuildIdentifiers.count > 1 && (availableXcode.version.prereleaseIdentifiers.isEmpty || availableXcode.architectures?.count ?? 0 != 0)
|
||||||
}
|
}
|
||||||
.map { availableXcode -> Xcode in
|
.map { availableXcode -> Xcode in
|
||||||
let installedXcode = installedXcodes.first(where: { installedXcode in
|
let installedXcode = installedXcodes.first(where: { installedXcode in
|
||||||
availableXcode.version.isEquivalent(to: installedXcode.version)
|
// if we want to have only specific Xcodes as selected instead of the Architecture Equivalent.
|
||||||
|
// if availableXcode.architectures == nil {
|
||||||
|
// return availableXcode.version.isEquivalent(to: installedXcode.version)
|
||||||
|
// } else {
|
||||||
|
// return availableXcode.xcodeID == installedXcode.xcodeID
|
||||||
|
// }
|
||||||
|
return availableXcode.version.isEquivalent(to: installedXcode.version)
|
||||||
})
|
})
|
||||||
|
|
||||||
let identicalBuilds: [Version]
|
let identicalBuilds: [XcodeID]
|
||||||
let prereleaseAvailableXcodesWithIdenticalBuildIdentifiers = availableXcodes
|
let prereleaseAvailableXcodesWithIdenticalBuildIdentifiers = availableXcodes
|
||||||
.filter {
|
.filter {
|
||||||
return $0.version.buildMetadataIdentifiers == availableXcode.version.buildMetadataIdentifiers &&
|
return $0.version.buildMetadataIdentifiers == availableXcode.version.buildMetadataIdentifiers &&
|
||||||
|
|
@ -782,13 +917,13 @@ class AppState: ObservableObject {
|
||||||
}
|
}
|
||||||
// If this is the release version, add the identical builds to it
|
// If this is the release version, add the identical builds to it
|
||||||
if !prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.isEmpty, availableXcode.version.prereleaseIdentifiers.isEmpty {
|
if !prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.isEmpty, availableXcode.version.prereleaseIdentifiers.isEmpty {
|
||||||
identicalBuilds = [availableXcode.version] + prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.map(\.version)
|
identicalBuilds = [availableXcode.xcodeID] + prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.map(\.xcodeID)
|
||||||
} else {
|
} else {
|
||||||
identicalBuilds = []
|
identicalBuilds = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the existing install state is "installing", keep it
|
// If the existing install state is "installing", keep it
|
||||||
let existingXcodeInstallState = allXcodes.first { $0.version == availableXcode.version && $0.installState.installing }?.installState
|
let existingXcodeInstallState = allXcodes.first { $0.id == availableXcode.xcodeID && $0.installState.installing }?.installState
|
||||||
// Otherwise, determine it from whether there's an installed Xcode
|
// Otherwise, determine it from whether there's an installed Xcode
|
||||||
let defaultXcodeInstallState: XcodeInstallState = installedXcode.map { .installed($0.path) } ?? .notInstalled
|
let defaultXcodeInstallState: XcodeInstallState = installedXcode.map { .installed($0.path) } ?? .notInstalled
|
||||||
|
|
||||||
|
|
@ -803,7 +938,8 @@ class AppState: ObservableObject {
|
||||||
releaseDate: availableXcode.releaseDate,
|
releaseDate: availableXcode.releaseDate,
|
||||||
sdks: availableXcode.sdks,
|
sdks: availableXcode.sdks,
|
||||||
compilers: availableXcode.compilers,
|
compilers: availableXcode.compilers,
|
||||||
downloadFileSize: availableXcode.fileSize
|
downloadFileSize: availableXcode.fileSize,
|
||||||
|
architectures: availableXcode.architectures
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Version
|
import Version
|
||||||
import struct XCModel.SDKs
|
import XcodesKit
|
||||||
import struct XCModel.Compilers
|
|
||||||
|
|
||||||
/// A version of Xcode that's available for installation
|
/// A version of Xcode that's available for installation
|
||||||
public struct AvailableXcode: Codable {
|
public struct AvailableXcode: Codable {
|
||||||
public var version: Version
|
public var version: Version {
|
||||||
|
return xcodeID.version
|
||||||
|
}
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let filename: String
|
public let filename: String
|
||||||
public let releaseDate: Date?
|
public let releaseDate: Date?
|
||||||
|
|
@ -14,9 +15,11 @@ public struct AvailableXcode: Codable {
|
||||||
public let sdks: SDKs?
|
public let sdks: SDKs?
|
||||||
public let compilers: Compilers?
|
public let compilers: Compilers?
|
||||||
public let fileSize: Int64?
|
public let fileSize: Int64?
|
||||||
|
public let architectures: [Architecture]?
|
||||||
public var downloadPath: String {
|
public var downloadPath: String {
|
||||||
return url.path
|
return url.path
|
||||||
}
|
}
|
||||||
|
public var xcodeID: XcodeID
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
version: Version,
|
version: Version,
|
||||||
|
|
@ -27,9 +30,9 @@ public struct AvailableXcode: Codable {
|
||||||
releaseNotesURL: URL? = nil,
|
releaseNotesURL: URL? = nil,
|
||||||
sdks: SDKs? = nil,
|
sdks: SDKs? = nil,
|
||||||
compilers: Compilers? = nil,
|
compilers: Compilers? = nil,
|
||||||
fileSize: Int64? = nil
|
fileSize: Int64? = nil,
|
||||||
|
architectures: [Architecture]? = nil
|
||||||
) {
|
) {
|
||||||
self.version = version
|
|
||||||
self.url = url
|
self.url = url
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.releaseDate = releaseDate
|
self.releaseDate = releaseDate
|
||||||
|
|
@ -38,5 +41,7 @@ public struct AvailableXcode: Codable {
|
||||||
self.sdks = sdks
|
self.sdks = sdks
|
||||||
self.compilers = compilers
|
self.compilers = compilers
|
||||||
self.fileSize = fileSize
|
self.fileSize = fileSize
|
||||||
|
self.architectures = architectures
|
||||||
|
self.xcodeID = XcodeID(version: version, architectures: architectures)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,6 @@ public enum DataSource: String, CaseIterable, Identifiable, CustomStringConverti
|
||||||
case .xcodeReleases: return "Xcode Releases"
|
case .xcodeReleases: return "Xcode Releases"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isManaged: Bool { PreferenceKey.dataSource.isManaged() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,6 @@ public enum Downloader: String, CaseIterable, Identifiable, CustomStringConverti
|
||||||
case .aria2: return "aria2"
|
case .aria2: return "aria2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isManaged: Bool { PreferenceKey.downloader.isManaged() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,8 @@ public struct Shell {
|
||||||
return AsyncThrowingStream<Progress, Error> { continuation in
|
return AsyncThrowingStream<Progress, Error> { continuation in
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
var progress = Progress()
|
// Assume progress will not have data races, so we manually opt-out isolation checks.
|
||||||
|
nonisolated(unsafe) var progress = Progress()
|
||||||
progress.kind = .file
|
progress.kind = .file
|
||||||
progress.fileOperationKind = .downloading
|
progress.fileOperationKind = .downloading
|
||||||
|
|
||||||
|
|
@ -195,6 +196,84 @@ public struct Shell {
|
||||||
return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"])
|
return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var downloadRuntime: (String, String, String?) -> AsyncThrowingStream<Progress, Error> = { platform, version, architecture in
|
||||||
|
return AsyncThrowingStream<Progress, Error> { continuation in
|
||||||
|
Task {
|
||||||
|
// Assume progress will not have data races, so we manually opt-out isolation checks.
|
||||||
|
nonisolated(unsafe) var progress = Progress()
|
||||||
|
progress.kind = .file
|
||||||
|
progress.fileOperationKind = .downloading
|
||||||
|
|
||||||
|
var process = Process()
|
||||||
|
let xcodeBuildPath = Path.root.usr.bin.join("xcodebuild").url
|
||||||
|
|
||||||
|
process.executableURL = xcodeBuildPath
|
||||||
|
process.arguments = [
|
||||||
|
"-downloadPlatform",
|
||||||
|
"\(platform)",
|
||||||
|
"-buildVersion",
|
||||||
|
"\(version)"
|
||||||
|
]
|
||||||
|
|
||||||
|
if let architecture {
|
||||||
|
process.arguments?.append(contentsOf: [
|
||||||
|
"-architectureVariant",
|
||||||
|
"\(architecture)"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdOutPipe = Pipe()
|
||||||
|
process.standardOutput = stdOutPipe
|
||||||
|
let stdErrPipe = Pipe()
|
||||||
|
process.standardError = stdErrPipe
|
||||||
|
|
||||||
|
let observer = NotificationCenter.default.addObserver(
|
||||||
|
forName: .NSFileHandleDataAvailable,
|
||||||
|
object: nil,
|
||||||
|
queue: OperationQueue.main
|
||||||
|
) { note in
|
||||||
|
guard
|
||||||
|
// This should always be the case for Notification.Name.NSFileHandleDataAvailable
|
||||||
|
let handle = note.object as? FileHandle,
|
||||||
|
handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
defer { handle.waitForDataInBackgroundAndNotify() }
|
||||||
|
|
||||||
|
let string = String(decoding: handle.availableData, as: UTF8.self)
|
||||||
|
|
||||||
|
// TODO: fix warning. ObservingProgressView is currently tied to an updating progress
|
||||||
|
progress.updateFromXcodebuild(text: string)
|
||||||
|
|
||||||
|
continuation.yield(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||||
|
stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||||
|
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
process.terminate()
|
||||||
|
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try process.run()
|
||||||
|
} catch {
|
||||||
|
continuation.finish(throwing: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.waitUntilExit()
|
||||||
|
|
||||||
|
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
|
||||||
|
|
||||||
|
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
|
||||||
|
continuation.finish(throwing: ProcessExecutionError(process: process, standardOutput: "", standardError: ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continuation.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Files {
|
public struct Files {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Version
|
import Version
|
||||||
import Path
|
import Path
|
||||||
|
import XcodesKit
|
||||||
|
|
||||||
/// A version of Xcode that's already installed
|
/// A version of Xcode that's already installed
|
||||||
public struct InstalledXcode: Equatable {
|
public struct InstalledXcode: Equatable {
|
||||||
public let path: Path
|
public let path: Path
|
||||||
|
public let xcodeID: XcodeID
|
||||||
|
|
||||||
/// Composed of the bundle short version from Info.plist and the product build version from version.plist
|
/// Composed of the bundle short version from Info.plist and the product build version from version.plist
|
||||||
public let version: Version
|
public var version: Version {
|
||||||
|
return xcodeID.version
|
||||||
|
}
|
||||||
|
|
||||||
public init?(path: Path) {
|
public init?(path: Path) {
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|
@ -32,11 +37,20 @@ public struct InstalledXcode: Equatable {
|
||||||
prereleaseIdentifiers = ["beta"]
|
prereleaseIdentifiers = ["beta"]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.version = Version(major: bundleVersion.major,
|
let archsString = try? XcodesKit.Current.shell.archs(path.url.appending(path: "Contents/MacOS/Xcode")).out
|
||||||
|
|
||||||
|
let architectures = archsString?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.split(separator: " ")
|
||||||
|
.compactMap { Architecture(rawValue: String($0)) }
|
||||||
|
|
||||||
|
let version = Version(major: bundleVersion.major,
|
||||||
minor: bundleVersion.minor,
|
minor: bundleVersion.minor,
|
||||||
patch: bundleVersion.patch,
|
patch: bundleVersion.patch,
|
||||||
prereleaseIdentifiers: prereleaseIdentifiers,
|
prereleaseIdentifiers: prereleaseIdentifiers,
|
||||||
buildMetadataIdentifiers: [versionPlist.productBuildVersion].compactMap { $0 })
|
buildMetadataIdentifiers: [versionPlist.productBuildVersion].compactMap { $0 })
|
||||||
|
|
||||||
|
self.xcodeID = XcodeID(version: version, architectures: architectures)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,5 +70,38 @@ extension Progress {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateFromXcodebuild(text: String) {
|
||||||
|
self.totalUnitCount = 100
|
||||||
|
self.completedUnitCount = 0
|
||||||
|
self.localizedAdditionalDescription = "" // to not show the addtional
|
||||||
|
|
||||||
|
do {
|
||||||
|
|
||||||
|
let downloadPattern = #"(\d+\.\d+)% \(([\d.]+ (?:MB|GB)) of ([\d.]+ GB)\)"#
|
||||||
|
let downloadRegex = try NSRegularExpression(pattern: downloadPattern)
|
||||||
|
|
||||||
|
// Search for matches in the text
|
||||||
|
if let match = downloadRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)) {
|
||||||
|
// Extract the percentage - simpler then trying to extract size MB/GB and convert to bytes.
|
||||||
|
if let percentRange = Range(match.range(at: 1), in: text), let percentDouble = Double(text[percentRange]) {
|
||||||
|
let percent = Int64(percentDouble.rounded())
|
||||||
|
self.completedUnitCount = percent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Downloading tvOS 18.1 Simulator (22J5567a): Installing..." or
|
||||||
|
// "Downloading tvOS 18.1 Simulator (22J5567a): Installing (registering download)..."
|
||||||
|
if text.range(of: "Installing") != nil {
|
||||||
|
// sets the progress to indeterminite to show animating progress
|
||||||
|
self.totalUnitCount = 0
|
||||||
|
self.completedUnitCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Logger.appState.error("Invalid regular expression")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import struct XCModel.SDKs
|
|
||||||
import XcodesKit
|
import XcodesKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import Version
|
import Version
|
||||||
import struct XCModel.Xcode
|
import XcodesKit
|
||||||
|
|
||||||
extension Version {
|
extension Version {
|
||||||
/// Initialize a Version from an XcodeReleases' XCModel.Xcode
|
/// Initialize a Version from an XcodeReleases' XCModel.Xcode
|
||||||
///
|
///
|
||||||
/// This is kinda quick-and-dirty, and it would probably be better for us to adopt something closer to XCModel.Xcode under the hood and map the scraped data to it instead.
|
/// This is kinda quick-and-dirty, and it would probably be better for us to adopt something closer to XCModel.Xcode under the hood and map the scraped data to it instead.
|
||||||
init?(xcReleasesXcode: XCModel.Xcode) {
|
init?(xcReleasesXcode: XcodeRelease) {
|
||||||
var versionString = xcReleasesXcode.version.number ?? ""
|
var versionString = xcReleasesXcode.version.number ?? ""
|
||||||
|
|
||||||
// Append trailing ".0" in order to get a fully-specified version string
|
// Append trailing ".0" in order to get a fully-specified version string
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,30 @@
|
||||||
import AppKit
|
import AppKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import Version
|
import Version
|
||||||
import struct XCModel.SDKs
|
|
||||||
import struct XCModel.Compilers
|
|
||||||
import Path
|
import Path
|
||||||
|
import XcodesKit
|
||||||
|
|
||||||
|
public struct XcodeID: Codable, Hashable, Identifiable {
|
||||||
|
public let version: Version
|
||||||
|
public let architectures: [Architecture]?
|
||||||
|
|
||||||
|
public var id: String {
|
||||||
|
let architectures = architectures?.map { $0.rawValue}.joined() ?? ""
|
||||||
|
return version.description + architectures
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(version: Version, architectures: [Architecture]? = nil) {
|
||||||
|
self.version = version
|
||||||
|
self.architectures = architectures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Xcode: Identifiable, CustomStringConvertible {
|
struct Xcode: Identifiable, CustomStringConvertible {
|
||||||
let version: Version
|
var version: Version {
|
||||||
|
return id.version
|
||||||
|
}
|
||||||
/// Other Xcode versions that have the same build identifier
|
/// Other Xcode versions that have the same build identifier
|
||||||
let identicalBuilds: [Version]
|
let identicalBuilds: [XcodeID]
|
||||||
var installState: XcodeInstallState
|
var installState: XcodeInstallState
|
||||||
let selected: Bool
|
let selected: Bool
|
||||||
let icon: NSImage?
|
let icon: NSImage?
|
||||||
|
|
@ -18,10 +34,12 @@ struct Xcode: Identifiable, CustomStringConvertible {
|
||||||
let sdks: SDKs?
|
let sdks: SDKs?
|
||||||
let compilers: Compilers?
|
let compilers: Compilers?
|
||||||
let downloadFileSize: Int64?
|
let downloadFileSize: Int64?
|
||||||
|
let architectures: [Architecture]?
|
||||||
|
let id: XcodeID
|
||||||
|
|
||||||
init(
|
init(
|
||||||
version: Version,
|
version: Version,
|
||||||
identicalBuilds: [Version] = [],
|
identicalBuilds: [XcodeID] = [],
|
||||||
installState: XcodeInstallState,
|
installState: XcodeInstallState,
|
||||||
selected: Bool,
|
selected: Bool,
|
||||||
icon: NSImage?,
|
icon: NSImage?,
|
||||||
|
|
@ -30,9 +48,9 @@ struct Xcode: Identifiable, CustomStringConvertible {
|
||||||
releaseDate: Date? = nil,
|
releaseDate: Date? = nil,
|
||||||
sdks: SDKs? = nil,
|
sdks: SDKs? = nil,
|
||||||
compilers: Compilers? = nil,
|
compilers: Compilers? = nil,
|
||||||
downloadFileSize: Int64? = nil
|
downloadFileSize: Int64? = nil,
|
||||||
|
architectures: [Architecture]? = nil
|
||||||
) {
|
) {
|
||||||
self.version = version
|
|
||||||
self.identicalBuilds = identicalBuilds
|
self.identicalBuilds = identicalBuilds
|
||||||
self.installState = installState
|
self.installState = installState
|
||||||
self.selected = selected
|
self.selected = selected
|
||||||
|
|
@ -43,10 +61,10 @@ struct Xcode: Identifiable, CustomStringConvertible {
|
||||||
self.sdks = sdks
|
self.sdks = sdks
|
||||||
self.compilers = compilers
|
self.compilers = compilers
|
||||||
self.downloadFileSize = downloadFileSize
|
self.downloadFileSize = downloadFileSize
|
||||||
|
self.architectures = architectures
|
||||||
|
self.id = XcodeID(version: version, architectures: architectures)
|
||||||
}
|
}
|
||||||
|
|
||||||
var id: Version { version }
|
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
version.appleDescription
|
version.appleDescription
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,11 @@ struct XcodeCommands: Commands {
|
||||||
|
|
||||||
struct InstallButton: View {
|
struct InstallButton: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@State private var isLoading = false
|
|
||||||
|
|
||||||
let xcode: Xcode?
|
let xcode: Xcode?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ProgressButton(isInProgress: isLoading) {
|
Button {
|
||||||
install()
|
install()
|
||||||
} label: {
|
} label: {
|
||||||
Text("Install")
|
Text("Install")
|
||||||
|
|
@ -49,7 +48,6 @@ struct InstallButton: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func install() {
|
private func install() {
|
||||||
isLoading = true
|
|
||||||
guard let xcode = xcode else { return }
|
guard let xcode = xcode else { return }
|
||||||
appState.checkMinVersionAndInstall(id: xcode.id)
|
appState.checkMinVersionAndInstall(id: xcode.id)
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +59,7 @@ struct CancelInstallButton: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: cancelInstall) {
|
Button(action: cancelInstall) {
|
||||||
Image(systemName: "xmark.circle.fill")
|
Label("Cancel", systemImage: "xmark")
|
||||||
}
|
}
|
||||||
.help(localizeString("StopInstallation"))
|
.help(localizeString("StopInstallation"))
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ public struct ObservingProgressIndicator: View {
|
||||||
self.progress = progress
|
self.progress = progress
|
||||||
cancellable = progress.publisher(for: \.fractionCompleted)
|
cancellable = progress.publisher(for: \.fractionCompleted)
|
||||||
.combineLatest(progress.publisher(for: \.localizedAdditionalDescription))
|
.combineLatest(progress.publisher(for: \.localizedAdditionalDescription))
|
||||||
|
.combineLatest(progress.publisher(for: \.isIndeterminate))
|
||||||
.throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true)
|
.throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true)
|
||||||
.sink { [weak self] _ in self?.objectWillChange.send() }
|
.sink { [weak self] _ in self?.objectWillChange.send() }
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +83,18 @@ struct ObservingProgressBar_Previews: PreviewProvider {
|
||||||
style: .bar,
|
style: .bar,
|
||||||
showsAdditionalDescription: true
|
showsAdditionalDescription: true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ObservingProgressIndicator(
|
||||||
|
configure(Progress()) {
|
||||||
|
$0.kind = .file
|
||||||
|
$0.fileOperationKind = .downloading
|
||||||
|
$0.totalUnitCount = 0
|
||||||
|
$0.completedUnitCount = 0
|
||||||
|
},
|
||||||
|
controlSize: .regular,
|
||||||
|
style: .bar,
|
||||||
|
showsAdditionalDescription: true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,10 @@ struct ProgressIndicator: NSViewRepresentable {
|
||||||
nsView.doubleValue = doubleValue
|
nsView.doubleValue = doubleValue
|
||||||
nsView.controlSize = controlSize
|
nsView.controlSize = controlSize
|
||||||
nsView.isIndeterminate = isIndeterminate
|
nsView.isIndeterminate = isIndeterminate
|
||||||
|
nsView.usesThreadedAnimation = true
|
||||||
|
|
||||||
nsView.style = style
|
nsView.style = style
|
||||||
|
nsView.startAnimation(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
24
Xcodes/Frontend/Common/TagView.swift
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// TagView.swift
|
||||||
|
// Xcodes
|
||||||
|
//
|
||||||
|
// Created by Matt Kiazyk on 2025-06-25.//
|
||||||
|
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TagView: View {
|
||||||
|
let text: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(text)
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.padding(.horizontal, 5)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(
|
||||||
|
Capsule()
|
||||||
|
.fill(.quaternary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ enum XcodesAlert: Identifiable {
|
||||||
case privilegedHelper
|
case privilegedHelper
|
||||||
case generic(title: String, message: String)
|
case generic(title: String, message: String)
|
||||||
case checkMinSupportedVersion(xcode: AvailableXcode, macOS: String)
|
case checkMinSupportedVersion(xcode: AvailableXcode, macOS: String)
|
||||||
|
case unauthenticated
|
||||||
|
|
||||||
var id: Int {
|
var id: Int {
|
||||||
switch self {
|
switch self {
|
||||||
|
|
@ -15,6 +16,7 @@ enum XcodesAlert: Identifiable {
|
||||||
case .generic: return 3
|
case .generic: return 3
|
||||||
case .checkMinSupportedVersion: return 4
|
case .checkMinSupportedVersion: return 4
|
||||||
case .cancelRuntimeInstall: return 5
|
case .cancelRuntimeInstall: return 5
|
||||||
|
case .unauthenticated: return 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import AppleAPI
|
||||||
enum XcodesSheet: Identifiable {
|
enum XcodesSheet: Identifiable {
|
||||||
case signIn
|
case signIn
|
||||||
case twoFactor(SecondFactorData)
|
case twoFactor(SecondFactorData)
|
||||||
|
case securityKeyTouchToConfirm
|
||||||
|
|
||||||
var id: Int { Kind(self).hashValue }
|
var id: Int { Kind(self).hashValue }
|
||||||
|
|
||||||
|
|
@ -16,12 +17,13 @@ enum XcodesSheet: Identifiable {
|
||||||
|
|
||||||
extension XcodesSheet {
|
extension XcodesSheet {
|
||||||
private enum Kind: Hashable {
|
private enum Kind: Hashable {
|
||||||
case signIn, twoFactor(TwoFactorOption)
|
case signIn, twoFactor(TwoFactorOption), securityKeyTouchToConfirm
|
||||||
|
|
||||||
enum TwoFactorOption {
|
enum TwoFactorOption {
|
||||||
case smsSent
|
case smsSent
|
||||||
case codeSent
|
case codeSent
|
||||||
case smsPendingChoice
|
case smsPendingChoice
|
||||||
|
case securityKeyPin
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ sheet: XcodesSheet) {
|
init(_ sheet: XcodesSheet) {
|
||||||
|
|
@ -32,7 +34,9 @@ extension XcodesSheet {
|
||||||
case .smsSent: self = .twoFactor(.smsSent)
|
case .smsSent: self = .twoFactor(.smsSent)
|
||||||
case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
|
case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
|
||||||
case .codeSent: self = .twoFactor(.codeSent)
|
case .codeSent: self = .twoFactor(.codeSent)
|
||||||
|
case .securityKey: self = .twoFactor(.securityKeyPin)
|
||||||
}
|
}
|
||||||
|
case .securityKeyTouchToConfirm: self = .securityKeyTouchToConfirm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import struct XCModel.Compilers
|
import XcodesKit
|
||||||
|
|
||||||
struct CompilersView: View {
|
struct CompilersView: View {
|
||||||
let compilers: Compilers?
|
let compilers: Compilers?
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,20 @@ import XcodesKit
|
||||||
import Path
|
import Path
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Version
|
import Version
|
||||||
import struct XCModel.Compilers
|
|
||||||
import struct XCModel.SDKs
|
|
||||||
|
|
||||||
struct InfoPane: View {
|
struct InfoPane: View {
|
||||||
let xcode: Xcode
|
let xcode: Xcode
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
if #available(macOS 14.0, *) {
|
||||||
|
mainContent
|
||||||
|
.contentMargins(10, for: .scrollContent)
|
||||||
|
} else {
|
||||||
|
mainContent
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mainContent: some View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
@ -25,20 +33,14 @@ struct InfoPane: View {
|
||||||
}
|
}
|
||||||
.xcodesBackground()
|
.xcodesBackground()
|
||||||
|
|
||||||
|
PlatformsView(xcode: xcode)
|
||||||
VStack {
|
|
||||||
Text("Platforms")
|
|
||||||
.font(.title3)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
PlatformsView(xcode: xcode)
|
|
||||||
}
|
|
||||||
.xcodesBackground()
|
|
||||||
}
|
}
|
||||||
|
.frame(minWidth: 380)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL)
|
ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL)
|
||||||
CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion)
|
CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion)
|
||||||
IdenticalBuildsView(builds: xcode.identicalBuilds)
|
IdenticalBuildsView(builds: xcode.identicalBuilds.map { $0.version })
|
||||||
SDKandCompilers
|
SDKandCompilers
|
||||||
}
|
}
|
||||||
.frame(width: 200)
|
.frame(width: 200)
|
||||||
|
|
@ -65,15 +67,16 @@ struct InfoPane: View {
|
||||||
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
|
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
|
||||||
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
|
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
|
||||||
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
|
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
|
||||||
|
#Preview(XcodePreviewName.allCases[5].rawValue) { makePreviewContent(for: 5) }
|
||||||
|
|
||||||
private func makePreviewContent(for index: Int) -> some View {
|
private func makePreviewContent(for index: Int) -> some View {
|
||||||
let name = XcodePreviewName.allCases[index]
|
let name = XcodePreviewName.allCases[index]
|
||||||
return InfoPane(xcode: xcodeDict[name]!)
|
return InfoPane(xcode: xcodeDict[name]!)
|
||||||
.environmentObject(configure(AppState()) {
|
.environmentObject(configure(AppState()) {
|
||||||
$0.allXcodes = [xcodeDict[name]!]
|
$0.allXcodes = [xcodeDict[name]!]
|
||||||
})
|
})
|
||||||
.frame(width: 300, height: 400)
|
.frame(width: 600, height: 400)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum XcodePreviewName: String, CaseIterable, Identifiable {
|
enum XcodePreviewName: String, CaseIterable, Identifiable {
|
||||||
|
|
@ -82,6 +85,7 @@ enum XcodePreviewName: String, CaseIterable, Identifiable {
|
||||||
case Populated_Uninstalled
|
case Populated_Uninstalled
|
||||||
case Basic_Installed
|
case Basic_Installed
|
||||||
case Basic_Installing
|
case Basic_Installing
|
||||||
|
case Basic_Unarchiving
|
||||||
|
|
||||||
var id: XcodePreviewName { self }
|
var id: XcodePreviewName { self }
|
||||||
}
|
}
|
||||||
|
|
@ -142,6 +146,14 @@ var xcodeDict: [XcodePreviewName: Xcode] = [
|
||||||
sdks: nil,
|
sdks: nil,
|
||||||
compilers: nil
|
compilers: nil
|
||||||
),
|
),
|
||||||
|
.Basic_Unarchiving: .init(
|
||||||
|
version: _versionWithMeta,
|
||||||
|
installState: .installing(.unarchiving),
|
||||||
|
selected: false,
|
||||||
|
icon: nil,
|
||||||
|
sdks: nil,
|
||||||
|
compilers: nil
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
var downloadableRuntimes: [DownloadableRuntime] = {
|
var downloadableRuntimes: [DownloadableRuntime] = {
|
||||||
|
|
@ -164,7 +176,7 @@ var downloadableRuntimes: [DownloadableRuntime] = {
|
||||||
$0.completedUnitCount = 848_444_920
|
$0.completedUnitCount = 848_444_920
|
||||||
$0.throughput = 9_211_681
|
$0.throughput = 9_211_681
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
runtimes[watchOSIndex] = runtime
|
runtimes[watchOSIndex] = runtime
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ struct InfoPaneControls: View {
|
||||||
case .installing(let installationStep):
|
case .installing(let installationStep):
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
InstallationStepDetailView(installationStep: installationStep)
|
InstallationStepDetailView(installationStep: installationStep)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
CancelInstallButton(xcode: xcode)
|
CancelInstallButton(xcode: xcode)
|
||||||
}
|
}
|
||||||
case .installed(_):
|
case .installed(_):
|
||||||
|
|
@ -39,6 +40,7 @@ struct InfoPaneControls: View {
|
||||||
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
|
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
|
||||||
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
|
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
|
||||||
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
|
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
|
||||||
|
#Preview(XcodePreviewName.allCases[5].rawValue) { makePreviewContent(for: 5) }
|
||||||
|
|
||||||
private func makePreviewContent(for index: Int) -> some View {
|
private func makePreviewContent(for index: Int) -> some View {
|
||||||
let name = XcodePreviewName.allCases[index]
|
let name = XcodePreviewName.allCases[index]
|
||||||
|
|
@ -47,6 +49,6 @@ private func makePreviewContent(for index: Int) -> some View {
|
||||||
.environmentObject(configure(AppState()) {
|
.environmentObject(configure(AppState()) {
|
||||||
$0.allXcodes = [xcodeDict[name]!]
|
$0.allXcodes = [xcodeDict[name]!]
|
||||||
})
|
})
|
||||||
.frame(width: 300)
|
.frame(width: 500)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ struct InstallationStepDetailView: View {
|
||||||
showsAdditionalDescription: true
|
showsAdditionalDescription: true
|
||||||
)
|
)
|
||||||
|
|
||||||
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
|
case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.scaleEffect(0.5)
|
.scaleEffect(0.5)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Version
|
import Version
|
||||||
import XCModel
|
import XcodesKit
|
||||||
import Path
|
import Path
|
||||||
|
|
||||||
struct InstalledStateButtons: View {
|
struct InstalledStateButtons: View {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import Version
|
||||||
|
|
||||||
struct NotInstalledStateButtons: View {
|
struct NotInstalledStateButtons: View {
|
||||||
let downloadFileSizeString: String?
|
let downloadFileSizeString: String?
|
||||||
let id: Version
|
let id: XcodeID
|
||||||
|
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
|
|
||||||
|
|
@ -20,7 +20,11 @@ struct NotInstalledStateButtons: View {
|
||||||
Button {
|
Button {
|
||||||
appState.checkMinVersionAndInstall(id: id)
|
appState.checkMinVersionAndInstall(id: id)
|
||||||
} label: {
|
} label: {
|
||||||
Text("Install") .help("Install")
|
if id.architectures?.isAppleSilicon ?? false {
|
||||||
|
Text("Install Apple Silicon").help("Install")
|
||||||
|
} else {
|
||||||
|
Text("Install Universal").help("Install")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let size = downloadFileSizeString {
|
if let size = downloadFileSizeString {
|
||||||
|
|
@ -38,7 +42,7 @@ struct NotInstalledStateButtons: View {
|
||||||
#Preview {
|
#Preview {
|
||||||
NotInstalledStateButtons(
|
NotInstalledStateButtons(
|
||||||
downloadFileSizeString: "1,19 GB",
|
downloadFileSizeString: "1,19 GB",
|
||||||
id: Version(major: 12, minor: 3, patch: 0)
|
id: XcodeID(version: Version(major: 12, minor: 3, patch: 0), architectures: nil)
|
||||||
)
|
)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import XcodesKit
|
||||||
|
|
||||||
struct PlatformsView: View {
|
struct PlatformsView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
|
@AppStorage("selectedRuntimeArchitecture") private var selectedVariant: ArchitectureVariant = .universal
|
||||||
|
|
||||||
let xcode: Xcode
|
let xcode: Xcode
|
||||||
|
|
||||||
|
|
@ -19,17 +20,49 @@ struct PlatformsView: View {
|
||||||
let builds = xcode.sdks?.allBuilds()
|
let builds = xcode.sdks?.allBuilds()
|
||||||
let runtimes = builds?.flatMap { sdkBuild in
|
let runtimes = builds?.flatMap { sdkBuild in
|
||||||
appState.downloadableRuntimes.filter {
|
appState.downloadableRuntimes.filter {
|
||||||
$0.sdkBuildUpdate?.contains(sdkBuild) ?? false
|
$0.sdkBuildUpdate?.contains(sdkBuild) ?? false &&
|
||||||
|
($0.architectures?.isEmpty ?? true ||
|
||||||
|
($0.architectures?.isUniversal ?? false && selectedVariant == .universal) ||
|
||||||
|
($0.architectures?.isAppleSilicon ?? false && selectedVariant == .appleSilicon)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(runtimes ?? [], id: \.simulatorVersion.buildUpdate) { runtime in
|
let architectures = Set((runtimes ?? []).flatMap { $0.architectures ?? [] })
|
||||||
runtimeView(runtime: runtime)
|
|
||||||
.frame(minWidth: 200)
|
VStack {
|
||||||
.padding()
|
HStack {
|
||||||
.background(.quinary)
|
Text("Platforms")
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
|
.font(.title3)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
if !architectures.isEmpty {
|
||||||
|
Spacer()
|
||||||
|
Picker("Architecture", selection: $selectedVariant) {
|
||||||
|
ForEach(ArchitectureVariant.allCases, id: \.self) { arch in
|
||||||
|
Label(arch.displayString, systemImage: arch.iconName)
|
||||||
|
.tag(arch)
|
||||||
|
}
|
||||||
|
.labelStyle(.trailingIcon)
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.menuStyle(.button)
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.fixedSize()
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(runtimes ?? [], id: \.identifier) { runtime in
|
||||||
|
runtimeView(runtime: runtime)
|
||||||
|
.frame(minWidth: 200)
|
||||||
|
.padding()
|
||||||
|
.background(.quinary)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.xcodesBackground()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
@ -39,34 +72,38 @@ struct PlatformsView: View {
|
||||||
runtime.icon()
|
runtime.icon()
|
||||||
Text("\(runtime.visibleIdentifier)")
|
Text("\(runtime.visibleIdentifier)")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
ForEach(runtime.architectures ?? [], id: \.self) { architecture in
|
||||||
|
TagView(text: architecture.displayString)
|
||||||
|
}
|
||||||
|
|
||||||
pathIfAvailable(xcode: xcode, runtime: runtime)
|
pathIfAvailable(xcode: xcode, runtime: runtime)
|
||||||
Spacer()
|
|
||||||
Text(runtime.downloadFileSizeString)
|
if runtime.installState == .notInstalled {
|
||||||
.font(.subheadline)
|
// TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly
|
||||||
}
|
if appState.runtimeInstallPath(xcode: xcode, runtime: runtime) != nil {
|
||||||
switch runtime.installState {
|
EmptyView()
|
||||||
case .installed:
|
} else {
|
||||||
EmptyView()
|
HStack {
|
||||||
case .notInstalled:
|
Spacer()
|
||||||
// TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly
|
DownloadRuntimeButton(runtime: runtime)
|
||||||
if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) {
|
}
|
||||||
EmptyView()
|
|
||||||
} else {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
DownloadRuntimeButton(runtime: runtime)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case .installing(let installationStep):
|
Spacer()
|
||||||
HStack(alignment: .top, spacing: 5){
|
Text(runtime.downloadFileSizeString)
|
||||||
RuntimeInstallationStepDetailView(installationStep: installationStep)
|
.font(.subheadline)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.frame(width: 70, alignment: .trailing)
|
||||||
Spacer()
|
|
||||||
CancelRuntimeInstallButton(runtime: runtime)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if case let .installing(installationStep) = runtime.installState {
|
||||||
|
HStack(alignment: .top, spacing: 5){
|
||||||
|
RuntimeInstallationStepDetailView(installationStep: installationStep)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
Spacer()
|
||||||
|
CancelRuntimeInstallButton(runtime: runtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,12 @@ struct RuntimeInstallationStepDetailView: View {
|
||||||
)
|
)
|
||||||
|
|
||||||
case .installing, .trashingArchive:
|
case .installing, .trashingArchive:
|
||||||
ProgressView()
|
ObservingProgressIndicator(
|
||||||
.scaleEffect(0.5)
|
Progress(),
|
||||||
|
controlSize: .regular,
|
||||||
|
style: .bar,
|
||||||
|
showsAdditionalDescription: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import struct XCModel.SDKs
|
import XcodesKit
|
||||||
|
|
||||||
struct SDKsView: View {
|
struct SDKsView: View {
|
||||||
let content: String
|
let content: String
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,12 @@ struct MainWindow: View {
|
||||||
// FB8979533 SceneStorage doesn't restore value after app is quit by user
|
// FB8979533 SceneStorage doesn't restore value after app is quit by user
|
||||||
@AppStorage("isShowingInfoPane") private var isShowingInfoPane = false
|
@AppStorage("isShowingInfoPane") private var isShowingInfoPane = false
|
||||||
@AppStorage("xcodeListCategory") private var category: XcodeListCategory = .all
|
@AppStorage("xcodeListCategory") private var category: XcodeListCategory = .all
|
||||||
|
@AppStorage("xcodeListArchitecture") private var architecture: XcodeListArchitecture = .universal
|
||||||
@AppStorage("isInstalledOnly") private var isInstalledOnly = false
|
@AppStorage("isInstalledOnly") private var isInstalledOnly = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationSplitViewWrapper {
|
NavigationSplitViewWrapper {
|
||||||
XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly)
|
XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly, architecture: architecture)
|
||||||
.layoutPriority(1)
|
.layoutPriority(1)
|
||||||
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
|
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
|
||||||
Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)),
|
Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)),
|
||||||
|
|
@ -31,7 +32,8 @@ struct MainWindow: View {
|
||||||
.mainToolbar(
|
.mainToolbar(
|
||||||
category: $category,
|
category: $category,
|
||||||
isInstalledOnly: $isInstalledOnly,
|
isInstalledOnly: $isInstalledOnly,
|
||||||
isShowingInfoPane: $isShowingInfoPane
|
isShowingInfoPane: $isShowingInfoPane,
|
||||||
|
architecture: $architecture
|
||||||
)
|
)
|
||||||
} detail: {
|
} detail: {
|
||||||
Group {
|
Group {
|
||||||
|
|
@ -41,7 +43,6 @@ struct MainWindow: View {
|
||||||
UnselectedView()
|
UnselectedView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup {
|
ToolbarItemGroup {
|
||||||
Button(action: { appState.presentedSheet = .signIn }, label: {
|
Button(action: { appState.presentedSheet = .signIn }, label: {
|
||||||
|
|
@ -77,6 +78,9 @@ struct MainWindow: View {
|
||||||
case .twoFactor(let secondFactorData):
|
case .twoFactor(let secondFactorData):
|
||||||
secondFactorView(secondFactorData)
|
secondFactorView(secondFactorData)
|
||||||
.environmentObject(appState)
|
.environmentObject(appState)
|
||||||
|
case .securityKeyTouchToConfirm:
|
||||||
|
SignInSecurityKeyTouchView(isPresented: $appState.presentedSheet.isNotNil)
|
||||||
|
.environmentObject(appState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(item: $appState.presentedAlert, content: { presentedAlert in
|
.alert(item: $appState.presentedAlert, content: { presentedAlert in
|
||||||
|
|
@ -108,6 +112,8 @@ struct MainWindow: View {
|
||||||
SignInSMSView(isPresented: $appState.presentedSheet.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
SignInSMSView(isPresented: $appState.presentedSheet.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
||||||
case .smsPendingChoice:
|
case .smsPendingChoice:
|
||||||
SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
||||||
|
case .securityKey:
|
||||||
|
SignInSecurityKeyPinView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,14 +190,28 @@ struct MainWindow: View {
|
||||||
action: { appState.presentedAlert = nil }
|
action: { appState.presentedAlert = nil }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
case .unauthenticated:
|
||||||
|
return Alert(
|
||||||
|
title: Text("Alert.Install.Error.Title"),
|
||||||
|
message: Text("Alert.Install.AuthError.Message"),
|
||||||
|
primaryButton: .default(
|
||||||
|
Text("OK"),
|
||||||
|
action: {
|
||||||
|
appState.presentedSheet = .signIn
|
||||||
|
}
|
||||||
|
),
|
||||||
|
secondaryButton: .cancel(
|
||||||
|
Text("Cancel")
|
||||||
|
)
|
||||||
|
)
|
||||||
case let .checkMinSupportedVersion(xcode, deviceVersion):
|
case let .checkMinSupportedVersion(xcode, deviceVersion):
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text("Alert.MinSupported.Title"),
|
title: Text("Alert.MinSupported.Title"),
|
||||||
message: Text(String(format: localizeString("Alert.MinSupported.Message"), xcode.version.descriptionWithoutBuildMetadata, xcode.requiredMacOSVersion ?? "", deviceVersion)),
|
message: Text(String(format: localizeString("Alert.MinSupported.Message"), xcode.xcodeID.version.descriptionWithoutBuildMetadata, xcode.requiredMacOSVersion ?? "", deviceVersion)),
|
||||||
primaryButton: .default(
|
primaryButton: .default(
|
||||||
Text("Install"),
|
Text("Install"),
|
||||||
action: {
|
action: {
|
||||||
self.appState.install(id: xcode.version)
|
self.appState.install(id: xcode.xcodeID)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
secondaryButton: .cancel(Text("Cancel"))
|
secondaryButton: .cancel(Text("Cancel"))
|
||||||
|
|
@ -219,7 +239,7 @@ struct MainWindow_Previews: PreviewProvider {
|
||||||
MainWindow().environmentObject({ () -> AppState in
|
MainWindow().environmentObject({ () -> AppState in
|
||||||
let a = AppState()
|
let a = AppState()
|
||||||
a.allXcodes = [
|
a.allXcodes = [
|
||||||
Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0+1234A")!, Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [XcodeID(version: Version("12.0.0+1234A")!), XcodeID(version: Version("12.0.0-RC+1234A")!)], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||||
Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil),
|
Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil),
|
||||||
Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil),
|
Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil),
|
||||||
Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil),
|
Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil),
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ struct AdvancedPreferencePane: View {
|
||||||
self.appState.installPath = path.string
|
self.appState.installPath = path.string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabled(appState.disableInstallPathChange)
|
||||||
Text("InstallPathDescription")
|
Text("InstallPathDescription")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
@ -72,6 +73,7 @@ struct AdvancedPreferencePane: View {
|
||||||
self.appState.localPath = path.string
|
self.appState.localPath = path.string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabled(appState.disableLocalPathChange)
|
||||||
Text("LocalCachePathDescription")
|
Text("LocalCachePathDescription")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
@ -93,6 +95,7 @@ struct AdvancedPreferencePane: View {
|
||||||
}
|
}
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.pickerStyle(.inline)
|
.pickerStyle(.inline)
|
||||||
|
.disabled(appState.onSelectActionTypeDisabled)
|
||||||
|
|
||||||
Text(appState.onSelectActionType.detailedDescription)
|
Text(appState.onSelectActionType.detailedDescription)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ struct DownloadPreferencePane: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||||
|
.disabled(dataSource.isManaged)
|
||||||
|
|
||||||
GroupBox(label: Text("Downloader")) {
|
GroupBox(label: Text("Downloader")) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
@ -46,6 +47,7 @@ struct DownloadPreferencePane: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||||
|
.disabled(downloader.isManaged)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ struct ExperimentsPreferencePane: View {
|
||||||
"UseUnxipExperiment",
|
"UseUnxipExperiment",
|
||||||
isOn: $appState.unxipExperiment
|
isOn: $appState.unxipExperiment
|
||||||
)
|
)
|
||||||
|
.disabled(appState.disableUnxipExperiment)
|
||||||
Text("FasterUnxipDescription")
|
Text("FasterUnxipDescription")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,12 @@ struct GeneralPreferencePane: View {
|
||||||
NotificationsView().environmentObject(appState)
|
NotificationsView().environmentObject(appState)
|
||||||
}
|
}
|
||||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
GroupBox(label: Text("Misc")) {
|
||||||
|
Toggle("TerminateAfterLastWindowClosed", isOn: $appState.terminateAfterLastWindowClosed)
|
||||||
|
}
|
||||||
|
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ struct UpdatesPreferencePane: View {
|
||||||
"AutomaticInstallNewVersion",
|
"AutomaticInstallNewVersion",
|
||||||
isOn: $autoInstallationType.isAutoInstalling
|
isOn: $autoInstallationType.isAutoInstalling
|
||||||
)
|
)
|
||||||
|
.disabled(updater.disableAutoInstallNewVersions)
|
||||||
|
|
||||||
Toggle(
|
Toggle(
|
||||||
"IncludePreRelease",
|
"IncludePreRelease",
|
||||||
isOn: $autoInstallationType.isAutoInstallingBeta
|
isOn: $autoInstallationType.isAutoInstallingBeta
|
||||||
)
|
)
|
||||||
|
.disabled(updater.disableIncludePrereleaseVersions)
|
||||||
}
|
}
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
|
|
@ -34,16 +36,19 @@ struct UpdatesPreferencePane: View {
|
||||||
isOn: $updater.automaticallyChecksForUpdates
|
isOn: $updater.automaticallyChecksForUpdates
|
||||||
)
|
)
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
|
.disabled(updater.disableAutoUpdateXcodesApp)
|
||||||
|
|
||||||
Toggle(
|
Toggle(
|
||||||
"IncludePreRelease",
|
"IncludePreRelease",
|
||||||
isOn: $updater.includePrereleaseVersions
|
isOn: $updater.includePrereleaseVersions
|
||||||
)
|
)
|
||||||
|
.disabled(updater.disableAutoUpdateXcodesAppPrereleaseVersions)
|
||||||
|
|
||||||
Button("CheckNow") {
|
Button("CheckNow") {
|
||||||
updater.checkForUpdates()
|
updater.checkForUpdates()
|
||||||
}
|
}
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
|
.disabled(updater.disableAutoUpdateXcodesApp)
|
||||||
|
|
||||||
Text(String(format: localizeString("LastChecked"), lastUpdatedString))
|
Text(String(format: localizeString("LastChecked"), lastUpdatedString))
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
|
|
@ -83,12 +88,18 @@ class ObservableUpdater: ObservableObject {
|
||||||
private var lastUpdateCheckDateObservation: NSKeyValueObservation?
|
private var lastUpdateCheckDateObservation: NSKeyValueObservation?
|
||||||
@Published var includePrereleaseVersions = false {
|
@Published var includePrereleaseVersions = false {
|
||||||
didSet {
|
didSet {
|
||||||
UserDefaults.standard.setValue(includePrereleaseVersions, forKey: "includePrereleaseVersions")
|
Current.defaults.set(includePrereleaseVersions, forKey: "includePrereleaseVersions")
|
||||||
|
|
||||||
updaterDelegate.includePrereleaseVersions = includePrereleaseVersions
|
updaterDelegate.includePrereleaseVersions = includePrereleaseVersions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var disableAutoInstallNewVersions: Bool { PreferenceKey.autoInstallation.isManaged() }
|
||||||
|
var disableIncludePrereleaseVersions: Bool { PreferenceKey.autoInstallation.isManaged() }
|
||||||
|
|
||||||
|
var disableAutoUpdateXcodesApp: Bool { PreferenceKey.SUEnableAutomaticChecks.isManaged() }
|
||||||
|
var disableAutoUpdateXcodesAppPrereleaseVersions: Bool { PreferenceKey.includePrereleaseVersions.isManaged() }
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
updater = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterDelegate, userDriverDelegate: nil).updater
|
updater = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterDelegate, userDriverDelegate: nil).updater
|
||||||
|
|
||||||
|
|
@ -111,7 +122,7 @@ class ObservableUpdater: ObservableObject {
|
||||||
self.lastUpdateCheckDate = updater.lastUpdateCheckDate
|
self.lastUpdateCheckDate = updater.lastUpdateCheckDate
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
includePrereleaseVersions = UserDefaults.standard.bool(forKey: "includePrereleaseVersions")
|
includePrereleaseVersions = Current.defaults.bool(forKey: "includePrereleaseVersions") ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkForUpdates() {
|
func checkForUpdates() {
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ struct SignIn2FAView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode.length))
|
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode!.length))
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) {
|
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) {
|
||||||
appState.submitSecurityCode(.device(code: $0), sessionData: sessionData)
|
appState.submitSecurityCode(.device(code: $0), sessionData: sessionData)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
@ -32,7 +32,7 @@ struct SignIn2FAView: View {
|
||||||
Text("Continue")
|
Text("Continue")
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
.disabled(code.count != authOptions.securityCode.length)
|
.disabled(code.count != authOptions.securityCode!.length)
|
||||||
}
|
}
|
||||||
.frame(height: 25)
|
.frame(height: 25)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ struct SignInPhoneListView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty {
|
if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty {
|
||||||
Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode.length))
|
Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode!.length))
|
||||||
|
|
||||||
List(phoneNumbers, selection: $selectedPhoneNumberID) {
|
List(phoneNumbers, selection: $selectedPhoneNumberID) {
|
||||||
Text($0.numberWithDialCode)
|
Text($0.numberWithDialCode)
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ struct SignInSMSView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode.length, trustedPhoneNumber.numberWithDialCode))
|
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode!.length, trustedPhoneNumber.numberWithDialCode))
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) {
|
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) {
|
||||||
appState.submitSecurityCode(.sms(code: $0, phoneNumberId: trustedPhoneNumber.id), sessionData: sessionData)
|
appState.submitSecurityCode(.sms(code: $0, phoneNumberId: trustedPhoneNumber.id), sessionData: sessionData)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
@ -31,7 +31,7 @@ struct SignInSMSView: View {
|
||||||
Text("Continue")
|
Text("Continue")
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.defaultAction)
|
.keyboardShortcut(.defaultAction)
|
||||||
.disabled(code.count != authOptions.securityCode.length)
|
.disabled(code.count != authOptions.securityCode!.length)
|
||||||
}
|
}
|
||||||
.frame(height: 25)
|
.frame(height: 25)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
//
|
||||||
|
// SignInSecurityKeyPin.swift
|
||||||
|
// Xcodes
|
||||||
|
//
|
||||||
|
// Created by Kino on 2024-09-26.
|
||||||
|
// Copyright © 2024 Robots and Pencils. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AppleAPI
|
||||||
|
|
||||||
|
struct SignInSecurityKeyPinView: View {
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@Binding var isPresented: Bool
|
||||||
|
@State private var pin: String = ""
|
||||||
|
let authOptions: AuthOptionsResponse
|
||||||
|
let sessionData: AppleSessionData
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(localizeString("SecurityKeyPinDescription"))
|
||||||
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
SecureField("PIN", text: $pin)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button("Cancel", action: { isPresented = false })
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button("PIN not set", action: submitWithoutPinCode)
|
||||||
|
|
||||||
|
ProgressButton(isInProgress: appState.isProcessingAuthRequest,
|
||||||
|
action: submitPinCode) {
|
||||||
|
Text("Continue")
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
// FIDO2 device pin codes must be at least 4 code points
|
||||||
|
// https://docs.yubico.com/yesdk/users-manual/application-fido2/fido2-pin.html
|
||||||
|
.disabled(pin.count < 4)
|
||||||
|
}
|
||||||
|
.frame(height: 25)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.emittingError($appState.authError, recoveryHandler: { _ in })
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitPinCode() {
|
||||||
|
appState.createAndSubmitSecurityKeyAssertationWithPinCode(pin, sessionData: sessionData, authOptions: authOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitWithoutPinCode() {
|
||||||
|
appState.createAndSubmitSecurityKeyAssertationWithPinCode(nil, sessionData: sessionData, authOptions: authOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SignInSecurityKeyPinView(isPresented: .constant(true),
|
||||||
|
authOptions: AuthOptionsResponse(
|
||||||
|
trustedPhoneNumbers: nil,
|
||||||
|
trustedDevices: nil,
|
||||||
|
securityCode: .init(length: 6)
|
||||||
|
), sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: ""))
|
||||||
|
.environmentObject(AppState())
|
||||||
|
}
|
||||||
54
Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
//
|
||||||
|
// SignInSecurityKeyPin.swift
|
||||||
|
// Xcodes
|
||||||
|
//
|
||||||
|
// Created by Kino on 2024-09-26.
|
||||||
|
// Copyright © 2024 Robots and Pencils. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import AppleAPI
|
||||||
|
|
||||||
|
struct SignInSecurityKeyTouchView: View {
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
@Binding var isPresented: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
Image(systemName: "key.radiowaves.forward")
|
||||||
|
.font(.system(size: 32)).bold()
|
||||||
|
.padding(.bottom)
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(localizeString("SecurityKeyTouchDescription"))
|
||||||
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Button("Cancel", action: self.cancel)
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle())
|
||||||
|
.scaleEffect(x: 0.5, y: 0.5, anchor: .center)
|
||||||
|
.isHidden(!appState.isProcessingAuthRequest)
|
||||||
|
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
}
|
||||||
|
.frame(height: 25)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.emittingError($appState.authError, recoveryHandler: { _ in })
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
appState.cancelSecurityKeyAssertationRequest()
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SignInSecurityKeyTouchView(isPresented: .constant(true))
|
||||||
|
.environmentObject(AppState())
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,8 @@ import SwiftUI
|
||||||
|
|
||||||
struct BottomStatusModifier: ViewModifier {
|
struct BottomStatusModifier: ViewModifier {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
|
@AppStorage(PreferenceKey.hideSupportXcodes.rawValue) var hideSupportXcodes = false
|
||||||
|
|
||||||
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
|
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
|
|
@ -20,17 +22,19 @@ struct BottomStatusModifier: ViewModifier {
|
||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
Text(appState.bottomStatusBarMessage)
|
Text(appState.bottomStatusBarMessage)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: {
|
if !hideSupportXcodes {
|
||||||
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
|
Button(action: {
|
||||||
}) {
|
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
|
||||||
HStack {
|
}) {
|
||||||
Image(systemName: "heart.circle")
|
HStack {
|
||||||
Text("Support.Xcodes")
|
Image(systemName: "heart.circle")
|
||||||
|
Text("Support.Xcodes")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(Bundle.main.shortVersion!)
|
Text(verbatim: "\(Bundle.main.shortVersion!) (\(Bundle.main.version!))")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
|
.frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
|
||||||
|
|
@ -51,8 +55,34 @@ extension View {
|
||||||
|
|
||||||
struct Previews_BottomStatusBar_Previews: PreviewProvider {
|
struct Previews_BottomStatusBar_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
HStack {
|
Group {
|
||||||
|
HStack {
|
||||||
|
|
||||||
}.bottomStatusBar()
|
}
|
||||||
|
.bottomStatusBar()
|
||||||
|
.environmentObject({ () -> AppState in
|
||||||
|
let a = AppState()
|
||||||
|
return a }()
|
||||||
|
)
|
||||||
|
.defaultAppStorage({ () -> UserDefaults in
|
||||||
|
let d = UserDefaults(suiteName: "hide_support")!
|
||||||
|
d.set(true, forKey: PreferenceKey.hideSupportXcodes.rawValue)
|
||||||
|
return d
|
||||||
|
}())
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
|
||||||
|
}
|
||||||
|
.bottomStatusBar()
|
||||||
|
.environmentObject({ () -> AppState in
|
||||||
|
let a = AppState()
|
||||||
|
return a }()
|
||||||
|
)
|
||||||
|
.defaultAppStorage({ () -> UserDefaults in
|
||||||
|
let d = UserDefaults(suiteName: "show_support")!
|
||||||
|
d.set(false, forKey: PreferenceKey.hideSupportXcodes.rawValue)
|
||||||
|
return d
|
||||||
|
}())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ struct InstallationStepRowView: View {
|
||||||
controlSize: .small,
|
controlSize: .small,
|
||||||
style: .spinning
|
style: .spinning
|
||||||
)
|
)
|
||||||
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
|
case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.scaleEffect(0.5)
|
.scaleEffect(0.5)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ struct MainToolbarModifier: ViewModifier {
|
||||||
@Binding var category: XcodeListCategory
|
@Binding var category: XcodeListCategory
|
||||||
@Binding var isInstalledOnly: Bool
|
@Binding var isInstalledOnly: Bool
|
||||||
@Binding var isShowingInfoPane: Bool
|
@Binding var isShowingInfoPane: Bool
|
||||||
|
@Binding var architectures: XcodeListArchitecture
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
|
|
@ -21,41 +22,44 @@ struct MainToolbarModifier: ViewModifier {
|
||||||
}
|
}
|
||||||
.keyboardShortcut(KeyEquivalent("r"))
|
.keyboardShortcut(KeyEquivalent("r"))
|
||||||
.help("RefreshDescription")
|
.help("RefreshDescription")
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button(action: {
|
let isFiltering = isInstalledOnly || category != .all || architectures != .universal
|
||||||
switch category {
|
Menu("Filter", systemImage: "line.horizontal.3.decrease.circle") {
|
||||||
case .all: category = .release
|
Section {
|
||||||
case .release: category = .beta
|
Toggle("Installed Only", systemImage: "arrow.down.app", isOn: $isInstalledOnly) .labelStyle(.titleAndIcon)
|
||||||
case .beta: category = .all
|
|
||||||
}
|
}
|
||||||
}) {
|
.help("FilterInstalledDescription")
|
||||||
switch category {
|
|
||||||
case .all:
|
|
||||||
Label("All", systemImage: "line.horizontal.3.decrease.circle")
|
|
||||||
case .release:
|
|
||||||
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
|
||||||
.labelStyle(.trailingIcon)
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
case .beta:
|
|
||||||
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
|
||||||
.labelStyle(.trailingIcon)
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.help("FilterAvailableDescription")
|
|
||||||
|
|
||||||
Button(action: {
|
Section {
|
||||||
isInstalledOnly.toggle()
|
Picker("Category", selection: $category) {
|
||||||
}) {
|
Label("All", systemImage: "line.horizontal.3.decrease.circle")
|
||||||
if isInstalledOnly {
|
.tag(XcodeListCategory.all)
|
||||||
Label("Filter", systemImage: "arrow.down.app.fill")
|
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||||
.foregroundColor(.accentColor)
|
.tag(XcodeListCategory.release)
|
||||||
} else {
|
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||||
Label("Filter", systemImage: "arrow.down.app")
|
.tag(XcodeListCategory.beta)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.help("FilterAvailableDescription")
|
||||||
|
.disabled(category.isManaged)
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Picker("Architecture", selection: $architectures) {
|
||||||
|
Label("Universal", systemImage: "cpu.fill")
|
||||||
|
.tag(XcodeListArchitecture.universal)
|
||||||
|
Label("Apple Silicon", systemImage: "m4.button.horizontal")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.tag(XcodeListArchitecture.appleSilicon)
|
||||||
|
}
|
||||||
|
.help("FilterArchitecturesDescription")
|
||||||
|
.disabled(architectures.isManaged)
|
||||||
|
}
|
||||||
|
.labelStyle(.titleAndIcon)
|
||||||
}
|
}
|
||||||
.help("FilterInstalledDescription")
|
.pickerStyle(.inline)
|
||||||
|
.symbolVariant(isFiltering ? .fill : .none)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,13 +68,15 @@ extension View {
|
||||||
func mainToolbar(
|
func mainToolbar(
|
||||||
category: Binding<XcodeListCategory>,
|
category: Binding<XcodeListCategory>,
|
||||||
isInstalledOnly: Binding<Bool>,
|
isInstalledOnly: Binding<Bool>,
|
||||||
isShowingInfoPane: Binding<Bool>
|
isShowingInfoPane: Binding<Bool>,
|
||||||
|
architecture: Binding<XcodeListArchitecture>
|
||||||
) -> some View {
|
) -> some View {
|
||||||
modifier(
|
modifier(
|
||||||
MainToolbarModifier(
|
MainToolbarModifier(
|
||||||
category: category,
|
category: category,
|
||||||
isInstalledOnly: isInstalledOnly,
|
isInstalledOnly: isInstalledOnly,
|
||||||
isShowingInfoPane: isShowingInfoPane
|
isShowingInfoPane: isShowingInfoPane,
|
||||||
|
architectures: architecture
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import XcodesKit
|
||||||
|
|
||||||
enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConvertible {
|
enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConvertible {
|
||||||
case all
|
case all
|
||||||
|
|
@ -14,4 +15,22 @@ enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConverti
|
||||||
case .beta: return localizeString("Beta")
|
case .beta: return localizeString("Beta")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isManaged: Bool { PreferenceKey.xcodeListCategory.isManaged() }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum XcodeListArchitecture: String, CaseIterable, Identifiable, CustomStringConvertible {
|
||||||
|
case universal
|
||||||
|
case appleSilicon
|
||||||
|
|
||||||
|
var id: Self { self }
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .universal: return localizeString("Universal")
|
||||||
|
case .appleSilicon: return localizeString("Apple Silicon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isManaged: Bool { PreferenceKey.xcodeListCategory.isManaged() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,16 @@ struct XcodeListView: View {
|
||||||
@Binding var selectedXcodeID: Xcode.ID?
|
@Binding var selectedXcodeID: Xcode.ID?
|
||||||
private let searchText: String
|
private let searchText: String
|
||||||
private let category: XcodeListCategory
|
private let category: XcodeListCategory
|
||||||
|
private let architecture: XcodeListArchitecture
|
||||||
private let isInstalledOnly: Bool
|
private let isInstalledOnly: Bool
|
||||||
|
@AppStorage(PreferenceKey.allowedMajorVersions.rawValue) private var allowedMajorVersions = Int.max
|
||||||
|
|
||||||
init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool) {
|
init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool, architecture: XcodeListArchitecture) {
|
||||||
self._selectedXcodeID = selectedXcodeID
|
self._selectedXcodeID = selectedXcodeID
|
||||||
self.searchText = searchText
|
self.searchText = searchText
|
||||||
self.category = category
|
self.category = category
|
||||||
self.isInstalledOnly = isInstalledOnly
|
self.isInstalledOnly = isInstalledOnly
|
||||||
|
self.architecture = architecture
|
||||||
}
|
}
|
||||||
|
|
||||||
var visibleXcodes: [Xcode] {
|
var visibleXcodes: [Xcode] {
|
||||||
|
|
@ -27,6 +30,27 @@ struct XcodeListView: View {
|
||||||
xcodes = appState.allXcodes.filter { $0.version.isPrerelease }
|
xcodes = appState.allXcodes.filter { $0.version.isPrerelease }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if architecture == .appleSilicon {
|
||||||
|
xcodes = xcodes.filter { $0.architectures == [.arm64] }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let latestMajor = xcodes.sorted(\.version)
|
||||||
|
.filter { $0.version.isNotPrerelease }
|
||||||
|
.last?
|
||||||
|
.version
|
||||||
|
.major
|
||||||
|
|
||||||
|
xcodes = xcodes.filter {
|
||||||
|
if $0.installState.notInstalled,
|
||||||
|
let latestMajor = latestMajor,
|
||||||
|
$0.version.major < (latestMajor - min(latestMajor,allowedMajorVersions)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if !searchText.isEmpty {
|
if !searchText.isEmpty {
|
||||||
xcodes = xcodes.filter { $0.description.contains(searchText) }
|
xcodes = xcodes.filter { $0.description.contains(searchText) }
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +71,7 @@ struct XcodeListView: View {
|
||||||
PlatformsPocket()
|
PlatformsPocket()
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,34 +84,47 @@ struct PlatformsPocket: View {
|
||||||
openWindow(id: "platforms")
|
openWindow(id: "platforms")
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
HStack(spacing: 5) {
|
if #available(macOS 26.0, *) {
|
||||||
Image(systemName: "square.3.layers.3d")
|
platformsLabel
|
||||||
.font(.title3.weight(.medium))
|
.glassEffect(in: .rect(cornerRadius: 8, style: .continuous))
|
||||||
Text("PlatformsDescription")
|
} else {
|
||||||
|
platformsLabel
|
||||||
|
.background(.quaternary.opacity(0.75))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||||
}
|
}
|
||||||
.font(.body.weight(.medium))
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.background(.quaternary.opacity(0.75))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var platformsLabel: some View {
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
Image(systemName: "square.3.layers.3d")
|
||||||
|
.font(.title3.weight(.medium))
|
||||||
|
Text("PlatformsDescription")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.font(.body.weight(.medium))
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct XcodeListView_Previews: PreviewProvider {
|
struct XcodeListView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all, isInstalledOnly: false)
|
XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all, isInstalledOnly: false, architecture: .appleSilicon)
|
||||||
.environmentObject({ () -> AppState in
|
.environmentObject({ () -> AppState in
|
||||||
let a = AppState()
|
let a = AppState()
|
||||||
a.allXcodes = [
|
a.allXcodes = [
|
||||||
Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0+1234A")!, Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [XcodeID(version: Version("12.0.0+1234A")!), XcodeID(version: Version("12.0.0-RC+1234A")!)], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||||
Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil),
|
Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil),
|
||||||
Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil),
|
Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil),
|
||||||
Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil),
|
Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil),
|
||||||
Xcode(version: Version("12.0.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
Xcode(version: Version("12.0.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||||
|
Xcode(version: Version("10.1.0")!, installState: .notInstalled, selected: false, icon: nil),
|
||||||
|
Xcode(version: Version("10.0.0")!, installState: .installed(Path("/Applications/Xcode-10.0.0.app")!), selected: false, icon: nil),
|
||||||
|
Xcode(version: Version("9.0.0")!, installState: .notInstalled, selected: false, icon: nil),
|
||||||
]
|
]
|
||||||
return a
|
return a
|
||||||
}())
|
}())
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,17 @@ struct XcodeListViewRow: View {
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.accessibility(label: Text("IdenticalBuilds"))
|
.accessibility(label: Text("IdenticalBuilds"))
|
||||||
.accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", ")))
|
.accessibility(value: Text(xcode.identicalBuilds.map(\.version.appleDescription).joined(separator: ", ")))
|
||||||
.help("IdenticalBuilds.help")
|
.help("IdenticalBuilds.help")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if xcode.architectures?.isAppleSilicon ?? false {
|
||||||
|
Image(systemName: "m4.button.horizontal")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.accessibility(label: Text("Apple Silicon"))
|
||||||
|
.help("Apple Silicon")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if case let .installed(path) = xcode.installState {
|
if case let .installed(path) = xcode.installState {
|
||||||
|
|
@ -156,7 +164,7 @@ struct XcodeListViewRow_Previews: PreviewProvider {
|
||||||
)
|
)
|
||||||
|
|
||||||
XcodeListViewRow(
|
XcodeListViewRow(
|
||||||
xcode: Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
xcode: Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [XcodeID(version: Version("12.0.0-RC+1234A")!)], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||||
selected: false,
|
selected: false,
|
||||||
appState: AppState()
|
appState: AppState()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
6
Xcodes/Resources/Assets.xcassets/Icons/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
{\rtf1\ansi\ansicpg1252\cocoartf2759
|
{\rtf1\ansi\ansicpg1252\cocoartf2865
|
||||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
|
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
|
||||||
{\colortbl;\red255\green255\blue255;}
|
{\colortbl;\red255\green255\blue255;}
|
||||||
{\*\expandedcolortbl;;}
|
{\*\expandedcolortbl;;}
|
||||||
|
|
@ -58,6 +58,33 @@ SOFTWARE.\
|
||||||
\
|
\
|
||||||
\
|
\
|
||||||
|
|
||||||
|
\fs34 LibFido2Swift\
|
||||||
|
\
|
||||||
|
|
||||||
|
\fs26 MIT License\
|
||||||
|
\
|
||||||
|
Copyright (c) 2024 Kino Roy\
|
||||||
|
\
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy\
|
||||||
|
of this software and associated documentation files (the "Software"), to deal\
|
||||||
|
in the Software without restriction, including without limitation the rights\
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
|
||||||
|
copies of the Software, and to permit persons to whom the Software is\
|
||||||
|
furnished to do so, subject to the following conditions:\
|
||||||
|
\
|
||||||
|
The above copyright notice and this permission notice shall be included in all\
|
||||||
|
copies or substantial portions of the Software.\
|
||||||
|
\
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
|
||||||
|
SOFTWARE.\
|
||||||
|
\
|
||||||
|
\
|
||||||
|
|
||||||
\fs34 ErrorHandling\
|
\fs34 ErrorHandling\
|
||||||
\
|
\
|
||||||
|
|
||||||
|
|
@ -86,6 +113,241 @@ SOFTWARE.\
|
||||||
\
|
\
|
||||||
\
|
\
|
||||||
|
|
||||||
|
\fs34 big-num\
|
||||||
|
\
|
||||||
|
|
||||||
|
\fs26 MIT License\
|
||||||
|
\
|
||||||
|
Copyright (c) 2019 Adam Fowler\
|
||||||
|
\
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy\
|
||||||
|
of this software and associated documentation files (the "Software"), to deal\
|
||||||
|
in the Software without restriction, including without limitation the rights\
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
|
||||||
|
copies of the Software, and to permit persons to whom the Software is\
|
||||||
|
furnished to do so, subject to the following conditions:\
|
||||||
|
\
|
||||||
|
The above copyright notice and this permission notice shall be included in all\
|
||||||
|
copies or substantial portions of the Software.\
|
||||||
|
\
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
|
||||||
|
SOFTWARE.\
|
||||||
|
\
|
||||||
|
\
|
||||||
|
|
||||||
|
\fs34 swift-crypto\
|
||||||
|
\
|
||||||
|
|
||||||
|
\fs26 \
|
||||||
|
Apache License\
|
||||||
|
Version 2.0, January 2004\
|
||||||
|
http://www.apache.org/licenses/\
|
||||||
|
\
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\
|
||||||
|
\
|
||||||
|
1. Definitions.\
|
||||||
|
\
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,\
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.\
|
||||||
|
\
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by\
|
||||||
|
the copyright owner that is granting the License.\
|
||||||
|
\
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all\
|
||||||
|
other entities that control, are controlled by, or are under common\
|
||||||
|
control with that entity. For the purposes of this definition,\
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the\
|
||||||
|
direction or management of such entity, whether by contract or\
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the\
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.\
|
||||||
|
\
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity\
|
||||||
|
exercising permissions granted by this License.\
|
||||||
|
\
|
||||||
|
"Source" form shall mean the preferred form for making modifications,\
|
||||||
|
including but not limited to software source code, documentation\
|
||||||
|
source, and configuration files.\
|
||||||
|
\
|
||||||
|
"Object" form shall mean any form resulting from mechanical\
|
||||||
|
transformation or translation of a Source form, including but\
|
||||||
|
not limited to compiled object code, generated documentation,\
|
||||||
|
and conversions to other media types.\
|
||||||
|
\
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or\
|
||||||
|
Object form, made available under the License, as indicated by a\
|
||||||
|
copyright notice that is included in or attached to the work\
|
||||||
|
(an example is provided in the Appendix below).\
|
||||||
|
\
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object\
|
||||||
|
form, that is based on (or derived from) the Work and for which the\
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications\
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes\
|
||||||
|
of this License, Derivative Works shall not include works that remain\
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,\
|
||||||
|
the Work and Derivative Works thereof.\
|
||||||
|
\
|
||||||
|
"Contribution" shall mean any work of authorship, including\
|
||||||
|
the original version of the Work and any modifications or additions\
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally\
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner\
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of\
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"\
|
||||||
|
means any form of electronic, verbal, or written communication sent\
|
||||||
|
to the Licensor or its representatives, including but not limited to\
|
||||||
|
communication on electronic mailing lists, source code control systems,\
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the\
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but\
|
||||||
|
excluding communication that is conspicuously marked or otherwise\
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."\
|
||||||
|
\
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity\
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and\
|
||||||
|
subsequently incorporated within the Work.\
|
||||||
|
\
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of\
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,\
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,\
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the\
|
||||||
|
Work and such Derivative Works in Source or Object form.\
|
||||||
|
\
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of\
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,\
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
|
||||||
|
(except as stated in this section) patent license to make, have made,\
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,\
|
||||||
|
where such license applies only to those patent claims licensable\
|
||||||
|
by such Contributor that are necessarily infringed by their\
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)\
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You\
|
||||||
|
institute patent litigation against any entity (including a\
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work\
|
||||||
|
or a Contribution incorporated within the Work constitutes direct\
|
||||||
|
or contributory patent infringement, then any patent licenses\
|
||||||
|
granted to You under this License for that Work shall terminate\
|
||||||
|
as of the date such litigation is filed.\
|
||||||
|
\
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the\
|
||||||
|
Work or Derivative Works thereof in any medium, with or without\
|
||||||
|
modifications, and in Source or Object form, provided that You\
|
||||||
|
meet the following conditions:\
|
||||||
|
\
|
||||||
|
(a) You must give any other recipients of the Work or\
|
||||||
|
Derivative Works a copy of this License; and\
|
||||||
|
\
|
||||||
|
(b) You must cause any modified files to carry prominent notices\
|
||||||
|
stating that You changed the files; and\
|
||||||
|
\
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works\
|
||||||
|
that You distribute, all copyright, patent, trademark, and\
|
||||||
|
attribution notices from the Source form of the Work,\
|
||||||
|
excluding those notices that do not pertain to any part of\
|
||||||
|
the Derivative Works; and\
|
||||||
|
\
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its\
|
||||||
|
distribution, then any Derivative Works that You distribute must\
|
||||||
|
include a readable copy of the attribution notices contained\
|
||||||
|
within such NOTICE file, excluding those notices that do not\
|
||||||
|
pertain to any part of the Derivative Works, in at least one\
|
||||||
|
of the following places: within a NOTICE text file distributed\
|
||||||
|
as part of the Derivative Works; within the Source form or\
|
||||||
|
documentation, if provided along with the Derivative Works; or,\
|
||||||
|
within a display generated by the Derivative Works, if and\
|
||||||
|
wherever such third-party notices normally appear. The contents\
|
||||||
|
of the NOTICE file are for informational purposes only and\
|
||||||
|
do not modify the License. You may add Your own attribution\
|
||||||
|
notices within Derivative Works that You distribute, alongside\
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided\
|
||||||
|
that such additional attribution notices cannot be construed\
|
||||||
|
as modifying the License.\
|
||||||
|
\
|
||||||
|
You may add Your own copyright statement to Your modifications and\
|
||||||
|
may provide additional or different license terms and conditions\
|
||||||
|
for use, reproduction, or distribution of Your modifications, or\
|
||||||
|
for any such Derivative Works as a whole, provided Your use,\
|
||||||
|
reproduction, and distribution of the Work otherwise complies with\
|
||||||
|
the conditions stated in this License.\
|
||||||
|
\
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,\
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work\
|
||||||
|
by You to the Licensor shall be under the terms and conditions of\
|
||||||
|
this License, without any additional terms or conditions.\
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify\
|
||||||
|
the terms of any separate license agreement you may have executed\
|
||||||
|
with Licensor regarding such Contributions.\
|
||||||
|
\
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade\
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,\
|
||||||
|
except as required for reasonable and customary use in describing the\
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.\
|
||||||
|
\
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or\
|
||||||
|
agreed to in writing, Licensor provides the Work (and each\
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,\
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\
|
||||||
|
implied, including, without limitation, any warranties or conditions\
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the\
|
||||||
|
appropriateness of using or redistributing the Work and assume any\
|
||||||
|
risks associated with Your exercise of permissions under this License.\
|
||||||
|
\
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,\
|
||||||
|
whether in tort (including negligence), contract, or otherwise,\
|
||||||
|
unless required by applicable law (such as deliberate and grossly\
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be\
|
||||||
|
liable to You for damages, including any direct, indirect, special,\
|
||||||
|
incidental, or consequential damages of any character arising as a\
|
||||||
|
result of this License or out of the use or inability to use the\
|
||||||
|
Work (including but not limited to damages for loss of goodwill,\
|
||||||
|
work stoppage, computer failure or malfunction, or any and all\
|
||||||
|
other commercial damages or losses), even if such Contributor\
|
||||||
|
has been advised of the possibility of such damages.\
|
||||||
|
\
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing\
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,\
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,\
|
||||||
|
or other liability obligations and/or rights consistent with this\
|
||||||
|
License. However, in accepting such obligations, You may act only\
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf\
|
||||||
|
of any other Contributor, and only if You agree to indemnify,\
|
||||||
|
defend, and hold each Contributor harmless for any liability\
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason\
|
||||||
|
of your accepting any such warranty or additional liability.\
|
||||||
|
\
|
||||||
|
END OF TERMS AND CONDITIONS\
|
||||||
|
\
|
||||||
|
APPENDIX: How to apply the Apache License to your work.\
|
||||||
|
\
|
||||||
|
To apply the Apache License to your work, attach the following\
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"\
|
||||||
|
replaced with your own identifying information. (Don't include\
|
||||||
|
the brackets!) The text should be enclosed in the appropriate\
|
||||||
|
comment syntax for the file format. We also recommend that a\
|
||||||
|
file or class name and description of purpose be included on the\
|
||||||
|
same "printed page" as the copyright notice for easier\
|
||||||
|
identification within third-party archives.\
|
||||||
|
\
|
||||||
|
Copyright [yyyy] [name of copyright owner]\
|
||||||
|
\
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");\
|
||||||
|
you may not use this file except in compliance with the License.\
|
||||||
|
You may obtain a copy of the License at\
|
||||||
|
\
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0\
|
||||||
|
\
|
||||||
|
Unless required by applicable law or agreed to in writing, software\
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,\
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\
|
||||||
|
See the License for the specific language governing permissions and\
|
||||||
|
limitations under the License.\
|
||||||
|
\
|
||||||
|
\
|
||||||
|
|
||||||
\fs34 Path.swift\
|
\fs34 Path.swift\
|
||||||
\
|
\
|
||||||
|
|
||||||
|
|
@ -552,12 +814,219 @@ For more information, please refer to <<http://unlicense.org/>>\
|
||||||
\
|
\
|
||||||
\
|
\
|
||||||
|
|
||||||
|
\fs34 swift-srp\
|
||||||
|
\
|
||||||
|
|
||||||
|
\fs26 Apache License\
|
||||||
|
Version 2.0, January 2004\
|
||||||
|
http://www.apache.org/licenses/\
|
||||||
|
\
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\
|
||||||
|
\
|
||||||
|
1. Definitions.\
|
||||||
|
\
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,\
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.\
|
||||||
|
\
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by\
|
||||||
|
the copyright owner that is granting the License.\
|
||||||
|
\
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all\
|
||||||
|
other entities that control, are controlled by, or are under common\
|
||||||
|
control with that entity. For the purposes of this definition,\
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the\
|
||||||
|
direction or management of such entity, whether by contract or\
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the\
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.\
|
||||||
|
\
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity\
|
||||||
|
exercising permissions granted by this License.\
|
||||||
|
\
|
||||||
|
"Source" form shall mean the preferred form for making modifications,\
|
||||||
|
including but not limited to software source code, documentation\
|
||||||
|
source, and configuration files.\
|
||||||
|
\
|
||||||
|
"Object" form shall mean any form resulting from mechanical\
|
||||||
|
transformation or translation of a Source form, including but\
|
||||||
|
not limited to compiled object code, generated documentation,\
|
||||||
|
and conversions to other media types.\
|
||||||
|
\
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or\
|
||||||
|
Object form, made available under the License, as indicated by a\
|
||||||
|
copyright notice that is included in or attached to the work\
|
||||||
|
(an example is provided in the Appendix below).\
|
||||||
|
\
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object\
|
||||||
|
form, that is based on (or derived from) the Work and for which the\
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications\
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes\
|
||||||
|
of this License, Derivative Works shall not include works that remain\
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,\
|
||||||
|
the Work and Derivative Works thereof.\
|
||||||
|
\
|
||||||
|
"Contribution" shall mean any work of authorship, including\
|
||||||
|
the original version of the Work and any modifications or additions\
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally\
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner\
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of\
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"\
|
||||||
|
means any form of electronic, verbal, or written communication sent\
|
||||||
|
to the Licensor or its representatives, including but not limited to\
|
||||||
|
communication on electronic mailing lists, source code control systems,\
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the\
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but\
|
||||||
|
excluding communication that is conspicuously marked or otherwise\
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."\
|
||||||
|
\
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity\
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and\
|
||||||
|
subsequently incorporated within the Work.\
|
||||||
|
\
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of\
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,\
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,\
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the\
|
||||||
|
Work and such Derivative Works in Source or Object form.\
|
||||||
|
\
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of\
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,\
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
|
||||||
|
(except as stated in this section) patent license to make, have made,\
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,\
|
||||||
|
where such license applies only to those patent claims licensable\
|
||||||
|
by such Contributor that are necessarily infringed by their\
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)\
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You\
|
||||||
|
institute patent litigation against any entity (including a\
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work\
|
||||||
|
or a Contribution incorporated within the Work constitutes direct\
|
||||||
|
or contributory patent infringement, then any patent licenses\
|
||||||
|
granted to You under this License for that Work shall terminate\
|
||||||
|
as of the date such litigation is filed.\
|
||||||
|
\
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the\
|
||||||
|
Work or Derivative Works thereof in any medium, with or without\
|
||||||
|
modifications, and in Source or Object form, provided that You\
|
||||||
|
meet the following conditions:\
|
||||||
|
\
|
||||||
|
(a) You must give any other recipients of the Work or\
|
||||||
|
Derivative Works a copy of this License; and\
|
||||||
|
\
|
||||||
|
(b) You must cause any modified files to carry prominent notices\
|
||||||
|
stating that You changed the files; and\
|
||||||
|
\
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works\
|
||||||
|
that You distribute, all copyright, patent, trademark, and\
|
||||||
|
attribution notices from the Source form of the Work,\
|
||||||
|
excluding those notices that do not pertain to any part of\
|
||||||
|
the Derivative Works; and\
|
||||||
|
\
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its\
|
||||||
|
distribution, then any Derivative Works that You distribute must\
|
||||||
|
include a readable copy of the attribution notices contained\
|
||||||
|
within such NOTICE file, excluding those notices that do not\
|
||||||
|
pertain to any part of the Derivative Works, in at least one\
|
||||||
|
of the following places: within a NOTICE text file distributed\
|
||||||
|
as part of the Derivative Works; within the Source form or\
|
||||||
|
documentation, if provided along with the Derivative Works; or,\
|
||||||
|
within a display generated by the Derivative Works, if and\
|
||||||
|
wherever such third-party notices normally appear. The contents\
|
||||||
|
of the NOTICE file are for informational purposes only and\
|
||||||
|
do not modify the License. You may add Your own attribution\
|
||||||
|
notices within Derivative Works that You distribute, alongside\
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided\
|
||||||
|
that such additional attribution notices cannot be construed\
|
||||||
|
as modifying the License.\
|
||||||
|
\
|
||||||
|
You may add Your own copyright statement to Your modifications and\
|
||||||
|
may provide additional or different license terms and conditions\
|
||||||
|
for use, reproduction, or distribution of Your modifications, or\
|
||||||
|
for any such Derivative Works as a whole, provided Your use,\
|
||||||
|
reproduction, and distribution of the Work otherwise complies with\
|
||||||
|
the conditions stated in this License.\
|
||||||
|
\
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,\
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work\
|
||||||
|
by You to the Licensor shall be under the terms and conditions of\
|
||||||
|
this License, without any additional terms or conditions.\
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify\
|
||||||
|
the terms of any separate license agreement you may have executed\
|
||||||
|
with Licensor regarding such Contributions.\
|
||||||
|
\
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade\
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,\
|
||||||
|
except as required for reasonable and customary use in describing the\
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.\
|
||||||
|
\
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or\
|
||||||
|
agreed to in writing, Licensor provides the Work (and each\
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,\
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\
|
||||||
|
implied, including, without limitation, any warranties or conditions\
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the\
|
||||||
|
appropriateness of using or redistributing the Work and assume any\
|
||||||
|
risks associated with Your exercise of permissions under this License.\
|
||||||
|
\
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,\
|
||||||
|
whether in tort (including negligence), contract, or otherwise,\
|
||||||
|
unless required by applicable law (such as deliberate and grossly\
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be\
|
||||||
|
liable to You for damages, including any direct, indirect, special,\
|
||||||
|
incidental, or consequential damages of any character arising as a\
|
||||||
|
result of this License or out of the use or inability to use the\
|
||||||
|
Work (including but not limited to damages for loss of goodwill,\
|
||||||
|
work stoppage, computer failure or malfunction, or any and all\
|
||||||
|
other commercial damages or losses), even if such Contributor\
|
||||||
|
has been advised of the possibility of such damages.\
|
||||||
|
\
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing\
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,\
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,\
|
||||||
|
or other liability obligations and/or rights consistent with this\
|
||||||
|
License. However, in accepting such obligations, You may act only\
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf\
|
||||||
|
of any other Contributor, and only if You agree to indemnify,\
|
||||||
|
defend, and hold each Contributor harmless for any liability\
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason\
|
||||||
|
of your accepting any such warranty or additional liability.\
|
||||||
|
\
|
||||||
|
END OF TERMS AND CONDITIONS\
|
||||||
|
\
|
||||||
|
APPENDIX: How to apply the Apache License to your work.\
|
||||||
|
\
|
||||||
|
To apply the Apache License to your work, attach the following\
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"\
|
||||||
|
replaced with your own identifying information. (Don't include\
|
||||||
|
the brackets!) The text should be enclosed in the appropriate\
|
||||||
|
comment syntax for the file format. We also recommend that a\
|
||||||
|
file or class name and description of purpose be included on the\
|
||||||
|
same "printed page" as the copyright notice for easier\
|
||||||
|
identification within third-party archives.\
|
||||||
|
\
|
||||||
|
Copyright [yyyy] [name of copyright owner]\
|
||||||
|
\
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");\
|
||||||
|
you may not use this file except in compliance with the License.\
|
||||||
|
You may obtain a copy of the License at\
|
||||||
|
\
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0\
|
||||||
|
\
|
||||||
|
Unless required by applicable law or agreed to in writing, software\
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,\
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\
|
||||||
|
See the License for the specific language governing permissions and\
|
||||||
|
limitations under the License.\
|
||||||
|
\
|
||||||
|
\
|
||||||
|
|
||||||
\fs34 DockProgress\
|
\fs34 DockProgress\
|
||||||
\
|
\
|
||||||
|
|
||||||
\fs26 MIT License\
|
\fs26 MIT License\
|
||||||
\
|
\
|
||||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\
|
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\
|
||||||
\
|
\
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
|
||||||
\
|
\
|
||||||
|
|
|
||||||
BIN
Xcodes/Resources/XcodesIcon.icon/Assets/Dark.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Xcodes/Resources/XcodesIcon.icon/Assets/Light.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
Xcodes/Resources/XcodesIcon.icon/Assets/Mono.png
Normal file
|
After Width: | Height: | Size: 716 KiB |
50
Xcodes/Resources/XcodesIcon.icon/icon.json
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"fill" : {
|
||||||
|
"automatic-gradient" : "extended-srgb:0.00000,0.47843,1.00000,1.00000"
|
||||||
|
},
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"glass" : false,
|
||||||
|
"hidden" : false,
|
||||||
|
"image-name-specializations" : [
|
||||||
|
{
|
||||||
|
"value" : "Light.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "dark",
|
||||||
|
"value" : "Dark.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "tinted",
|
||||||
|
"value" : "Mono.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "Dark",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 1,
|
||||||
|
"translation-in-points" : [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"specular" : false,
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : false,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"squares" : [
|
||||||
|
"macOS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ struct XcodesApp: App {
|
||||||
@StateObject private var updater = ObservableUpdater()
|
@StateObject private var updater = ObservableUpdater()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup("Xcodes") {
|
Window("Xcodes", id: "main") {
|
||||||
MainWindow()
|
MainWindow()
|
||||||
.environmentObject(appState)
|
.environmentObject(appState)
|
||||||
.environmentObject(updater)
|
.environmentObject(updater)
|
||||||
|
|
@ -51,19 +51,19 @@ struct XcodesApp: App {
|
||||||
|
|
||||||
CommandGroup(replacing: CommandGroupPlacement.help) {
|
CommandGroup(replacing: CommandGroupPlacement.help) {
|
||||||
Button("Menu.GitHubRepo") {
|
Button("Menu.GitHubRepo") {
|
||||||
let xcodesRepoURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!
|
let xcodesRepoURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/")!
|
||||||
openURL(xcodesRepoURL)
|
openURL(xcodesRepoURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
Button("Menu.ReportABug") {
|
Button("Menu.ReportABug") {
|
||||||
let bugReportURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")!
|
let bugReportURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")!
|
||||||
openURL(bugReportURL)
|
openURL(bugReportURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Menu.RequestNewFeature") {
|
Button("Menu.RequestNewFeature") {
|
||||||
let featureRequestURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")!
|
let featureRequestURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")!
|
||||||
openURL(featureRequestURL)
|
openURL(featureRequestURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +166,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_: Notification) {}
|
func applicationDidFinishLaunching(_: Notification) {}
|
||||||
|
|
||||||
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func localizeString(_ key: String, comment: String = "") -> String {
|
func localizeString(_ key: String, comment: String = "") -> String {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "XcodesKit",
|
name: "XcodesKit",
|
||||||
platforms: [.macOS(.v11)],
|
platforms: [.macOS(.v13)],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
.library(
|
.library(
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,10 @@ public struct CoreSimulatorImage: Decodable, Identifiable, Equatable {
|
||||||
|
|
||||||
public struct CoreSimulatorRuntimeInfo: Decodable {
|
public struct CoreSimulatorRuntimeInfo: Decodable {
|
||||||
public let build: String
|
public let build: String
|
||||||
|
public let supportedArchitectures: [Architecture]?
|
||||||
|
|
||||||
public init(build: String) {
|
public init(build: String, supportedArchitectures: [Architecture]? = nil) {
|
||||||
self.build = build
|
self.build = build
|
||||||
|
self.supportedArchitectures = supportedArchitectures
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ public struct DownloadableRuntimesResponse: Codable {
|
||||||
public struct DownloadableRuntime: Codable, Identifiable, Hashable {
|
public struct DownloadableRuntime: Codable, Identifiable, Hashable {
|
||||||
public let category: Category
|
public let category: Category
|
||||||
public let simulatorVersion: SimulatorVersion
|
public let simulatorVersion: SimulatorVersion
|
||||||
public let source: String
|
public let source: String?
|
||||||
|
public let architectures: [Architecture]?
|
||||||
public let dictionaryVersion: Int
|
public let dictionaryVersion: Int
|
||||||
public let contentType: ContentType
|
public let contentType: ContentType
|
||||||
public let platform: Platform
|
public let platform: Platform
|
||||||
|
|
@ -21,11 +22,14 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable {
|
||||||
public let hostRequirements: HostRequirements?
|
public let hostRequirements: HostRequirements?
|
||||||
public let name: String
|
public let name: String
|
||||||
public let authentication: Authentication?
|
public let authentication: Authentication?
|
||||||
public var url: URL {
|
public var url: URL? {
|
||||||
return URL(string: source)!
|
if let source {
|
||||||
|
return URL(string: source)!
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
public var downloadPath: String {
|
public var downloadPath: String? {
|
||||||
url.path
|
url?.path
|
||||||
}
|
}
|
||||||
|
|
||||||
// dynamically updated - not decoded
|
// dynamically updated - not decoded
|
||||||
|
|
@ -46,10 +50,11 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable {
|
||||||
case name
|
case name
|
||||||
case authentication
|
case authentication
|
||||||
case sdkBuildUpdate
|
case sdkBuildUpdate
|
||||||
|
case architectures
|
||||||
}
|
}
|
||||||
|
|
||||||
var betaNumber: Int? {
|
var betaNumber: Int? {
|
||||||
enum Regex { static let shared = try! NSRegularExpression(pattern: "b[0-9]+$") }
|
enum Regex { static let shared = try! NSRegularExpression(pattern: "b[0-9]+") }
|
||||||
guard var foundString = Regex.shared.firstString(in: identifier) else { return nil }
|
guard var foundString = Regex.shared.firstString(in: identifier) else { return nil }
|
||||||
foundString.removeFirst()
|
foundString.removeFirst()
|
||||||
return Int(foundString)!
|
return Int(foundString)!
|
||||||
|
|
@ -91,6 +96,7 @@ public struct SDKToSimulatorMapping: Codable {
|
||||||
public let sdkBuildUpdate: String
|
public let sdkBuildUpdate: String
|
||||||
public let simulatorBuildUpdate: String
|
public let simulatorBuildUpdate: String
|
||||||
public let sdkIdentifier: String
|
public let sdkIdentifier: String
|
||||||
|
public let downloadableIdentifiers: [String]?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DownloadableRuntime {
|
extension DownloadableRuntime {
|
||||||
|
|
@ -117,6 +123,7 @@ extension DownloadableRuntime {
|
||||||
public enum ContentType: String, Codable {
|
public enum ContentType: String, Codable {
|
||||||
case diskImage = "diskImage"
|
case diskImage = "diskImage"
|
||||||
case package = "package"
|
case package = "package"
|
||||||
|
case cryptexDiskImage = "cryptexDiskImage"
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Platform: String, Codable {
|
public enum Platform: String, Codable {
|
||||||
|
|
@ -163,6 +170,7 @@ public struct InstalledRuntime: Decodable {
|
||||||
let state: String
|
let state: String
|
||||||
let version: String
|
let version: String
|
||||||
let sizeBytes: Int?
|
let sizeBytes: Int?
|
||||||
|
let supportedArchitectures: [Architecture]?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InstalledRuntime {
|
extension InstalledRuntime {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import Foundation
|
||||||
|
|
||||||
// A numbered step
|
// A numbered step
|
||||||
public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
|
public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
|
||||||
|
case authenticating
|
||||||
case downloading(progress: Progress)
|
case downloading(progress: Progress)
|
||||||
case unarchiving
|
case unarchiving
|
||||||
case moving(destination: String)
|
case moving(destination: String)
|
||||||
|
|
@ -22,6 +23,8 @@ public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
|
||||||
|
|
||||||
public var message: String {
|
public var message: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .authenticating:
|
||||||
|
return localizeString("Authenticating")
|
||||||
case .downloading:
|
case .downloading:
|
||||||
return localizeString("Downloading")
|
return localizeString("Downloading")
|
||||||
case .unarchiving:
|
case .unarchiving:
|
||||||
|
|
@ -39,16 +42,17 @@ public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
|
||||||
|
|
||||||
public var stepNumber: Int {
|
public var stepNumber: Int {
|
||||||
switch self {
|
switch self {
|
||||||
case .downloading: return 1
|
case .authenticating: return 1
|
||||||
case .unarchiving: return 2
|
case .downloading: return 2
|
||||||
case .moving: return 3
|
case .unarchiving: return 3
|
||||||
case .trashingArchive: return 4
|
case .moving: return 4
|
||||||
case .checkingSecurity: return 5
|
case .trashingArchive: return 5
|
||||||
case .finishing: return 6
|
case .checkingSecurity: return 6
|
||||||
|
case .finishing: return 7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var stepCount: Int { 6 }
|
public var stepCount: Int { 7 }
|
||||||
}
|
}
|
||||||
|
|
||||||
func localizeString(_ key: String, comment: String = "") -> String {
|
func localizeString(_ key: String, comment: String = "") -> String {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
//
|
||||||
|
// Architecture.swift
|
||||||
|
// XcodesKit
|
||||||
|
//
|
||||||
|
// Created by Matt Kiazyk on 2025-08-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// The name of an Architecture.
|
||||||
|
public enum Architecture: String, Codable, Equatable, Hashable, Identifiable, CaseIterable {
|
||||||
|
public var id: Self { self }
|
||||||
|
|
||||||
|
/// The Arm64 architecture (Apple Silicon)
|
||||||
|
case arm64 = "arm64"
|
||||||
|
/// The X86\_64 architecture (64-bit Intel)
|
||||||
|
case x86_64 = "x86_64"
|
||||||
|
|
||||||
|
public var displayString: String {
|
||||||
|
switch self {
|
||||||
|
case .arm64:
|
||||||
|
return "Apple Silicon"
|
||||||
|
case .x86_64:
|
||||||
|
return "Intel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var iconName: String {
|
||||||
|
switch self {
|
||||||
|
case .arm64:
|
||||||
|
return "m4.button.horizontal"
|
||||||
|
case .x86_64:
|
||||||
|
return "cpu.fill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ArchitectureVariant: String, Codable, Equatable, Hashable, Identifiable, CaseIterable {
|
||||||
|
public var id: Self { self }
|
||||||
|
|
||||||
|
case universal
|
||||||
|
case appleSilicon
|
||||||
|
|
||||||
|
public var displayString: String {
|
||||||
|
switch self {
|
||||||
|
case .appleSilicon:
|
||||||
|
return "Apple Silicon"
|
||||||
|
case .universal:
|
||||||
|
return "Universal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var iconName: String {
|
||||||
|
switch self {
|
||||||
|
case .appleSilicon:
|
||||||
|
return "m4.button.horizontal"
|
||||||
|
case .universal:
|
||||||
|
return "cpu.fill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Array where Element == Architecture {
|
||||||
|
public var isAppleSilicon: Bool {
|
||||||
|
self == [.arm64]
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isUniversal: Bool {
|
||||||
|
self.contains([.arm64, .x86_64])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// Checksums.swift
|
||||||
|
// xcodereleases
|
||||||
|
//
|
||||||
|
// Created by Xcode Releases on 9/17/20.
|
||||||
|
// Copyright © 2020 Xcode Releases. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct Checksums: Codable {
|
||||||
|
|
||||||
|
public let sha1: String?
|
||||||
|
|
||||||
|
public init(sha1: String? = nil) {
|
||||||
|
self.sha1 = sha1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// Compiler.swift
|
||||||
|
// xcodereleases
|
||||||
|
//
|
||||||
|
// Created by Xcode Releases on 4/4/18.
|
||||||
|
// Copyright © 2018 Xcode Releases. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct Compilers: Codable {
|
||||||
|
public let gcc: Array<XcodeVersion>?
|
||||||
|
public let llvm_gcc: Array<XcodeVersion>?
|
||||||
|
public let llvm: Array<XcodeVersion>?
|
||||||
|
public let clang: Array<XcodeVersion>?
|
||||||
|
public let swift: Array<XcodeVersion>?
|
||||||
|
|
||||||
|
public init(gcc: XcodeVersion? = nil, llvm_gcc: XcodeVersion? = nil, llvm: XcodeVersion? = nil, clang: XcodeVersion? = nil, swift: XcodeVersion? = nil) {
|
||||||
|
self.gcc = gcc.map { [$0] }
|
||||||
|
self.llvm_gcc = llvm_gcc.map { [$0] }
|
||||||
|
self.llvm = llvm.map { [$0] }
|
||||||
|
self.clang = clang.map { [$0] }
|
||||||
|
self.swift = swift.map { [$0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(gcc: Array<XcodeVersion>?, llvm_gcc: Array<XcodeVersion>?, llvm: Array<XcodeVersion>?, clang: Array<XcodeVersion>?, swift: Array<XcodeVersion>?) {
|
||||||
|
self.gcc = gcc?.isEmpty == true ? nil : gcc
|
||||||
|
self.llvm_gcc = llvm_gcc?.isEmpty == true ? nil : llvm_gcc
|
||||||
|
self.llvm = llvm?.isEmpty == true ? nil : llvm
|
||||||
|
self.clang = clang?.isEmpty == true ? nil : clang
|
||||||
|
self.swift = swift?.isEmpty == true ? nil : swift
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
//
|
||||||
|
// Link.swift
|
||||||
|
// xcodereleases
|
||||||
|
//
|
||||||
|
// Created by Xcode Releases on 4/5/18.
|
||||||
|
// Copyright © 2018 Xcode Releases. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct Link: Codable {
|
||||||
|
public let url: URL
|
||||||
|
public let sizeMB: Int?
|
||||||
|
/// The platforms supported by this link, if applicable.
|
||||||
|
public var architectures: [Architecture]?
|
||||||
|
|
||||||
|
// public init(_ string: String, _ size: Int? = nil, _ architectures: [Architecture]? = nil) {
|
||||||
|
// self.url = URL(string: string)!
|
||||||
|
// self.sizeMB = size
|
||||||
|
// self.architectures = architectures
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Links: Codable {
|
||||||
|
public let download: Link?
|
||||||
|
public let notes: Link?
|
||||||
|
|
||||||
|
public init(download: Link? = nil, notes: Link? = nil) {
|
||||||
|
self.download = download
|
||||||
|
self.notes = notes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
//
|
||||||
|
// Release.swift
|
||||||
|
// xcodereleases
|
||||||
|
//
|
||||||
|
// Created by Xcode Releases on 4/4/18.
|
||||||
|
// Copyright © 2018 Xcode Releases. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum Release: Codable {
|
||||||
|
|
||||||
|
public enum CodingKeys: String, CodingKey {
|
||||||
|
case gm, gmSeed, rc, beta, dp, release
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isGM: Bool {
|
||||||
|
guard case .gm = self else { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case gm
|
||||||
|
case gmSeed(Int)
|
||||||
|
case rc(Int)
|
||||||
|
case beta(Int)
|
||||||
|
case dp(Int)
|
||||||
|
case release
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
if let _ = try container.decodeIfPresent(Bool.self, forKey: .gm) {
|
||||||
|
self = .gm
|
||||||
|
} else if let v = try container.decodeIfPresent(Int.self, forKey: .gmSeed) {
|
||||||
|
self = .gmSeed(v)
|
||||||
|
} else if let v = try container.decodeIfPresent(Int.self, forKey: .rc) {
|
||||||
|
self = .rc(v)
|
||||||
|
} else if let v = try container.decodeIfPresent(Int.self, forKey: .beta) {
|
||||||
|
self = .beta(v)
|
||||||
|
} else if let v = try container.decodeIfPresent(Int.self, forKey: .dp) {
|
||||||
|
self = .dp(v)
|
||||||
|
} else if let _ = try container.decodeIfPresent(Bool.self, forKey: .release) {
|
||||||
|
self = .release
|
||||||
|
} else {
|
||||||
|
fatalError("Unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
switch self {
|
||||||
|
case .gm: try container.encode(true, forKey: .gm)
|
||||||
|
case .gmSeed(let v): try container.encode(v, forKey: .gmSeed)
|
||||||
|
case .rc(let v): try container.encode(v, forKey: .rc)
|
||||||
|
case .beta(let v): try container.encode(v, forKey: .beta)
|
||||||
|
case .dp(let v): try container.encode(v, forKey: .dp)
|
||||||
|
case .release: try container.encode(true, forKey: .release)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
//
|
||||||
|
// SDKs.swift
|
||||||
|
// xcodereleases
|
||||||
|
//
|
||||||
|
// Created by Xcode Releases on 4/4/18.
|
||||||
|
// Copyright © 2018 Xcode Releases. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct SDKs: Codable {
|
||||||
|
public let macOS: Array<XcodeVersion>?
|
||||||
|
public let iOS: Array<XcodeVersion>?
|
||||||
|
public let watchOS: Array<XcodeVersion>?
|
||||||
|
public let tvOS: Array<XcodeVersion>?
|
||||||
|
public let visionOS: Array<XcodeVersion>?
|
||||||
|
|
||||||
|
public init(macOS: XcodeVersion? = nil, iOS: XcodeVersion? = nil, watchOS: XcodeVersion? = nil, tvOS: XcodeVersion? = nil, visionOS: XcodeVersion? = nil) {
|
||||||
|
self.macOS = macOS.map { [$0] }
|
||||||
|
self.iOS = iOS.map { [$0] }
|
||||||
|
self.watchOS = watchOS.map { [$0] }
|
||||||
|
self.tvOS = tvOS.map { [$0] }
|
||||||
|
self.visionOS = visionOS.map { [$0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(macOS: Array<XcodeVersion>?, iOS: XcodeVersion? = nil, watchOS: XcodeVersion? = nil, tvOS: XcodeVersion? = nil, visionOS: XcodeVersion? = nil) {
|
||||||
|
self.macOS = macOS?.isEmpty == true ? nil : macOS
|
||||||
|
self.iOS = iOS.map { [$0] }
|
||||||
|
self.watchOS = watchOS.map { [$0] }
|
||||||
|
self.tvOS = tvOS.map { [$0] }
|
||||||
|
self.visionOS = visionOS.map { [$0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(macOS: Array<XcodeVersion>?, iOS: Array<XcodeVersion>?, watchOS: XcodeVersion? = nil, tvOS: XcodeVersion? = nil, visionOS: XcodeVersion? = nil) {
|
||||||
|
self.macOS = macOS?.isEmpty == true ? nil : macOS
|
||||||
|
self.iOS = iOS?.isEmpty == true ? nil : iOS
|
||||||
|
self.watchOS = watchOS.map { [$0] }
|
||||||
|
self.tvOS = tvOS.map { [$0] }
|
||||||
|
self.visionOS = visionOS.map { [$0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(macOS: Array<XcodeVersion>?, iOS: Array<XcodeVersion>?, watchOS: Array<XcodeVersion>?, tvOS: XcodeVersion? = nil, visionOS: XcodeVersion? = nil) {
|
||||||
|
self.macOS = macOS?.isEmpty == true ? nil : macOS
|
||||||
|
self.iOS = iOS?.isEmpty == true ? nil : iOS
|
||||||
|
self.watchOS = watchOS?.isEmpty == true ? nil : watchOS
|
||||||
|
self.tvOS = tvOS.map { [$0] }
|
||||||
|
self.visionOS = visionOS.map { [$0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(macOS: Array<XcodeVersion>?, iOS: Array<XcodeVersion>?, watchOS: Array<XcodeVersion>?, tvOS: Array<XcodeVersion>?, visionOS: Array<XcodeVersion>?) {
|
||||||
|
self.macOS = macOS?.isEmpty == true ? nil : macOS
|
||||||
|
self.iOS = iOS?.isEmpty == true ? nil : iOS
|
||||||
|
self.watchOS = watchOS?.isEmpty == true ? nil : watchOS
|
||||||
|
self.tvOS = tvOS?.isEmpty == true ? nil : tvOS
|
||||||
|
self.visionOS = visionOS?.isEmpty == true ? nil : visionOS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// Xcode.swift
|
||||||
|
// xcodereleases
|
||||||
|
//
|
||||||
|
// Created by Xcode Releases on 4/3/18.
|
||||||
|
// Copyright © 2018 Xcode Releases. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct XcodeRelease: Codable {
|
||||||
|
public let name: String
|
||||||
|
public let version: XcodeVersion
|
||||||
|
public let date: YMD
|
||||||
|
public let requires: String
|
||||||
|
public let sdks: SDKs?
|
||||||
|
public let compilers: Compilers?
|
||||||
|
public let links: Links?
|
||||||
|
public let checksums: Checksums?
|
||||||
|
|
||||||
|
public var architectures: [Architecture]? {
|
||||||
|
return links.flatMap { $0.download?.architectures }
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(name: String = "Xcode", version: XcodeVersion, date: (Int, Int, Int), requires: String, sdks: SDKs? = nil, compilers: Compilers? = nil, links: Links? = nil, checksums: Checksums? = nil) {
|
||||||
|
self.name = name
|
||||||
|
self.version = version;
|
||||||
|
self.date = YMD(date);
|
||||||
|
self.requires = requires;
|
||||||
|
self.sdks = sdks;
|
||||||
|
self.compilers = compilers
|
||||||
|
self.links = links
|
||||||
|
self.checksums = checksums
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// Version.swift
|
||||||
|
// xcodereleases
|
||||||
|
//
|
||||||
|
// Created by Xcode Releases on 4/4/18.
|
||||||
|
// Copyright © 2018 Xcode Releases. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public typealias V = XcodeVersion
|
||||||
|
public struct XcodeVersion: Codable {
|
||||||
|
public let number: String?
|
||||||
|
public let build: String?
|
||||||
|
public let release: Release
|
||||||
|
|
||||||
|
public init(_ build: String, _ number: String? = nil, _ release: Release = .release) {
|
||||||
|
self.number = number; self.build = build; self.release = release
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(number: String, _ build: String? = nil, _ release: Release = .release) {
|
||||||
|
self.number = number; self.build = build; self.release = release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
//
|
||||||
|
// YMD.swift
|
||||||
|
// xcodereleases
|
||||||
|
//
|
||||||
|
// Created by Xcode Releases on 4/4/18.
|
||||||
|
// Copyright © 2018 Xcode Releases. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct YMD: Codable {
|
||||||
|
public let year: Int
|
||||||
|
public let month: Int
|
||||||
|
public let day: Int
|
||||||
|
|
||||||
|
public init(_ ymd: (Int, Int, Int)) {
|
||||||
|
self.year = ymd.0; self.month = ymd.1; self.day = ymd.2
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(_ year: Int, _ month: Int, _ day: Int) {
|
||||||
|
self.year = year; self.month = month; self.day = day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,9 +22,14 @@ public struct RuntimeService {
|
||||||
|
|
||||||
// Apple gives a plist for download
|
// Apple gives a plist for download
|
||||||
let (data, _) = try await networkService.requestData(urlRequest, validators: [])
|
let (data, _) = try await networkService.requestData(urlRequest, validators: [])
|
||||||
let decodedResponse = try PropertyListDecoder().decode(DownloadableRuntimesResponse.self, from: data)
|
do {
|
||||||
|
let decodedResponse = try PropertyListDecoder().decode(DownloadableRuntimesResponse.self, from: data)
|
||||||
|
return decodedResponse
|
||||||
|
} catch {
|
||||||
|
print("error: \(error)")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
return decodedResponse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func installedRuntimes() async throws -> [InstalledRuntime] {
|
public func installedRuntimes() async throws -> [InstalledRuntime] {
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,14 @@ public typealias ProcessOutput = (status: Int32, out: String, err: String)
|
||||||
|
|
||||||
extension Process {
|
extension Process {
|
||||||
static func run(_ executable: Path, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) async throws -> ProcessOutput {
|
static func run(_ executable: Path, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) async throws -> ProcessOutput {
|
||||||
return try await run(executable.url, workingDirectory: workingDirectory, input: input, arguments)
|
return try run(executable.url, workingDirectory: workingDirectory, input: input, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func run(_ executable: URL, workingDirectory: URL? = nil, input: String? = nil, _ arguments: [String]) async throws -> ProcessOutput {
|
static func run(_ executable: Path, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) throws -> ProcessOutput {
|
||||||
|
return try run(executable.url, workingDirectory: workingDirectory, input: input, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func run(_ executable: URL, workingDirectory: URL? = nil, input: String? = nil, _ arguments: [String]) throws -> ProcessOutput {
|
||||||
|
|
||||||
let process = Process()
|
let process = Process()
|
||||||
process.currentDirectoryURL = workingDirectory ?? executable.deletingLastPathComponent()
|
process.currentDirectoryURL = workingDirectory ?? executable.deletingLastPathComponent()
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,8 @@ public struct XcodesShell {
|
||||||
public var deleteRuntime: (String) async throws -> ProcessOutput = {
|
public var deleteRuntime: (String) async throws -> ProcessOutput = {
|
||||||
try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "delete", $0)
|
try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "delete", $0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var archs: (URL) throws -> ProcessOutput = {
|
||||||
|
try Process.run(Path.root.usr.bin.join("lipo"), "-archs", $0.path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import Path
|
import Path
|
||||||
|
import CryptoKit
|
||||||
import Version
|
import Version
|
||||||
@testable import Xcodes
|
@testable import Xcodes
|
||||||
import XCTest
|
import XCTest
|
||||||
|
import CommonCrypto
|
||||||
|
import BigNum
|
||||||
|
import SRP
|
||||||
|
|
||||||
class AppStateUpdateTests: XCTestCase {
|
class AppStateUpdateTests: XCTestCase {
|
||||||
var subject: AppState!
|
var subject: AppState!
|
||||||
|
|
@ -145,7 +149,7 @@ class AppStateUpdateTests: XCTestCase {
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0+12D4e")!])
|
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0+12D4e")!])
|
||||||
XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[Version("12.4.0+12D4e")!, Version("12.4.0-RC+12D4e")!]])
|
XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[XcodeID(version: Version("12.4.0+12D4e")!), XcodeID(version: Version("12.4.0-RC+12D4e")!)]])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIdenticalBuilds_DoNotMergeReleaseVersions() {
|
func testIdenticalBuilds_DoNotMergeReleaseVersions() {
|
||||||
|
|
@ -230,7 +234,7 @@ class AppStateUpdateTests: XCTestCase {
|
||||||
)
|
)
|
||||||
|
|
||||||
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0+12D4e")!])
|
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0+12D4e")!])
|
||||||
XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[Version("12.4.0+12D4e")!, Version("12.4.0-RC+12D4e")!]])
|
XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[XcodeID(version: Version("12.4.0+12D4e")!), XcodeID(version: Version("12.4.0-RC+12D4e")!)]])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIdenticalBuilds_AppleDataSource_DoNotMergeVersionsWithoutBuildIdentifiers() {
|
func testIdenticalBuilds_AppleDataSource_DoNotMergeVersionsWithoutBuildIdentifiers() {
|
||||||
|
|
@ -258,4 +262,81 @@ class AppStateUpdateTests: XCTestCase {
|
||||||
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0")!, Version("12.3.0-RC")!])
|
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0")!, Version("12.3.0-RC")!])
|
||||||
XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[], []])
|
XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[], []])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sha256(data : Data) -> Data {
|
||||||
|
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||||
|
data.withUnsafeBytes {
|
||||||
|
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
|
||||||
|
}
|
||||||
|
return Data(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSIRP() throws {
|
||||||
|
/*
|
||||||
|
Obtaind by running fastlane spaceauth --verbose -u anand.appleseed@example.com with some custom logging
|
||||||
|
|
||||||
|
Starting SIRP Apple ID login
|
||||||
|
a: {"a":"VLEKLa+n2cyeYNWbECm45CuS4kCdCxodlTDGlW1FKaUyOrv/RbtN2sM0pVE12zI7k3VkocPC3rN5DZBIkahR6I8JHj/J97dtTvzsR+aNRWTYCT2HGP1PBI0QArp3eitAbFqTWI4+Zw+oOnV8+AYdH/wjbq7gOK4C4dvIHE+FzRwIlmguPb5qu2r47R9W3y1msVdoUGlFBOMOMb7Gyq7F0MaEIFH63lNzGomwq74mfss/cFqurd6fxU+Y7tdVTPZw1GWyBEPiXWpk8sNm2zE+S6zWo5tOsICprU75IC9galh1igfzN7VNe0SUFLNFTbFK+Bb1SFAOrAbBZOmyOG5uSQ==","accountName":"anand.appleseed@example.com","protocols":["s2k","s2k_fo"]}
|
||||||
|
Received SIRP signin init response: {"iteration"=>20309, "salt"=>"fIjNflgqSJXACWwwvhDU+w==", "protocol"=>"s2k", "b"=>"PMbU75wwG6rDTySXn2ASWyfQuPoW5ham15SzIscpInwOPE2uk7sePsW4ra0dHcLDUMFQn/LgBggIKOo7YZ9hf1VReiAzXwSKSHdJHjHUURTC2eNpANGUPO1qzuXYgc/MP3MR+GipKHsz+KTLT+8wLjNaiCIHsL/7evJBMw9QqiwhyXlAIm5mGZfhdTVbGpLz2/QzrFmI6pUTrHpio6m1Q74DH3FBxxIeiIcuEdGdeVt9iUweowBRyf2woasTvSV1fbMQbl+lsWPwzt/a73+J30eOGFdSubqSVYh2pV0OLqRz7zPzJars12teCWUV+0WUIaxb14Mp7tlmqcTPuqZe9w==", "c"=>"d-533-eccbc4e9-9564-11ef-84a6-018111c8cc60:PRN"}
|
||||||
|
encrypted_password: 40532b4de9353fc537dc62ee84eacebd7ecb5ec26efca98bd01b0380e302100f 32
|
||||||
|
bb: 7672345903537871991962715758896796468138571329014139041563495295907370682045347022183702983061785424983278857706335295545994877883818377653653442007828499221881058994644619578239367613808278802931379172730746665773282250642455690715139985911303055104847971308813151718669484181874342088801251592138154023949370621963319928723678385968989085032385411532317797659749008135679504901238396934480214258070495365760319076978872181485178648397361564241555189629889320567561713407566532187413091018319494367244540399242523126294027225387432028960726767445027313453858210115810946641002311734776929442587065438110307439763191
|
||||||
|
x: 726436461883978586175291668001486484510457416477927591386889224605776454162
|
||||||
|
u: 49415306980415573732801389514223606278850554555635359953422678270536095422201
|
||||||
|
private 23161374166158551996079451276150657702422963034121842124445818241826569345033578345120284496449280736328513302994568402583647660370960353252836732307301957364261384324957527103960720408713825427474127658415917826326829664923997096839970397226662116904369925262192683131695683487505523842260218490007066160096482662715246662018133837725691586205535995663334471723776536640973591229093933458552240634178864509015968350855952558520147559154646484379002445961375384929682566525908284011230815908584648931495968206840416022306138033496705677078512266958633477047047323620540878121579549170392075029336954975132431417099801
|
||||||
|
S: 4f75b6ea99c2d7d121357cce80c75c8e1bf74a65e8f66f75f8f66a481301afb8bebf0e54a3fac4f8bfdd60c77d6e670c87968b341f62175e25eb1d4f496e4e6596e1a387f2840688a35002419b70115b7902a46544cc7b31eb4c909c0acaeb752835d1562a687c431421510ebc7535c007a2bd12a4f7696c8c96a75a491b1eb9189ade2bef23dd5b0bb962b4f03e7fba7f6ba6fe67ba34cc18647daf3e474876f85dac5a15eb51c99d1ed78783579ffd6c8b6911f72564d87dc8f76519c8fc1535b83743ed5f7d6b8461d7154ce2a874cbb45bf63018352b9b997fbafbd6b15eac2a544a801c0152470796f3b69a84a4a653e5186b30efeeb148ff3c32ab8e08
|
||||||
|
K: c5207f707186a52f1adee41bf0a7bc41e51e6dffc25cdaeca8acb7de2710b20a
|
||||||
|
hN: 65908899099613711898698321155848703477601840791750658211391687862255842366922
|
||||||
|
hG: 23094592799618609623465742609366149076596436609130823198107630312273622653270
|
||||||
|
hxor 73599884097654065452785151481733181870375477364472235597514429707329935690908
|
||||||
|
response: {"accountName":"anand.appleseed@example.com","c":"d-533-eccbc4e9-9564-11ef-84a6-018111c8cc60:PRN","m1":"f/Bkq8gBTYxl7SyiRd4SXTyE/jM/g6E0mVyZIQDIsPg=","m2":"R2rgqC9cMAtWiXUImOrvs4oF+ccibf8KaFsZQ22WokM=","rememberMe":false}
|
||||||
|
*/
|
||||||
|
|
||||||
|
let publicKey = Data(base64Encoded: "VLEKLa+n2cyeYNWbECm45CuS4kCdCxodlTDGlW1FKaUyOrv/RbtN2sM0pVE12zI7k3VkocPC3rN5DZBIkahR6I8JHj/J97dtTvzsR+aNRWTYCT2HGP1PBI0QArp3eitAbFqTWI4+Zw+oOnV8+AYdH/wjbq7gOK4C4dvIHE+FzRwIlmguPb5qu2r47R9W3y1msVdoUGlFBOMOMb7Gyq7F0MaEIFH63lNzGomwq74mfss/cFqurd6fxU+Y7tdVTPZw1GWyBEPiXWpk8sNm2zE+S6zWo5tOsICprU75IC9galh1igfzN7VNe0SUFLNFTbFK+Bb1SFAOrAbBZOmyOG5uSQ==".data(using: .utf8)!)
|
||||||
|
|
||||||
|
let clientKeys = SRPKeyPair(public: .init([UInt8](publicKey!)),
|
||||||
|
private: .init(BigNum("23161374166158551996079451276150657702422963034121842124445818241826569345033578345120284496449280736328513302994568402583647660370960353252836732307301957364261384324957527103960720408713825427474127658415917826326829664923997096839970397226662116904369925262192683131695683487505523842260218490007066160096482662715246662018133837725691586205535995663334471723776536640973591229093933458552240634178864509015968350855952558520147559154646484379002445961375384929682566525908284011230815908584648931495968206840416022306138033496705677078512266958633477047047323620540878121579549170392075029336954975132431417099801")!))
|
||||||
|
|
||||||
|
let password = sha256(data: "example".data(using: .utf8)!)
|
||||||
|
let salt = Data(base64Encoded: "fIjNflgqSJXACWwwvhDU+w==".data(using: .utf8)!)!
|
||||||
|
let iterations: Int = 20309
|
||||||
|
let derivedKeyLength: Int = 32
|
||||||
|
var keyArray = Array<UInt8>(repeating: 0, count: derivedKeyLength)
|
||||||
|
|
||||||
|
let result:Int32 = keyArray.withUnsafeMutableBytes { keyBytes -> Int32 in
|
||||||
|
let keyBuffer = UnsafeMutablePointer<UInt8>(keyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self))
|
||||||
|
return password.withUnsafeBytes { passwordDigestBytes -> Int32 in
|
||||||
|
let passwordBuffer = UnsafePointer<UInt8>(passwordDigestBytes.baseAddress!.assumingMemoryBound(to: UInt8.self))
|
||||||
|
return salt.withUnsafeBytes { saltBytes -> Int32 in
|
||||||
|
let saltBuffer = UnsafePointer<UInt8>(saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self))
|
||||||
|
return CCKeyDerivationPBKDF(
|
||||||
|
CCPBKDFAlgorithm(kCCPBKDF2),
|
||||||
|
passwordBuffer,
|
||||||
|
password.count,
|
||||||
|
saltBuffer,
|
||||||
|
salt.count,
|
||||||
|
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
|
||||||
|
UInt32(iterations),
|
||||||
|
keyBuffer,
|
||||||
|
derivedKeyLength)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expectedKey: [UInt8] = [0x40, 0x53, 0x2b, 0x4d, 0xe9, 0x35, 0x3f, 0xc5, 0x37, 0xdc, 0x62, 0xee, 0x84, 0xea, 0xce, 0xbd, 0x7e, 0xcb, 0x5e, 0xc2, 0x6e, 0xfc, 0xa9, 0x8b, 0xd0, 0x1b, 0x03, 0x80, 0xe3, 0x02, 0x10, 0x0f]
|
||||||
|
|
||||||
|
XCTAssertEqual(expectedKey, keyArray)
|
||||||
|
|
||||||
|
let decodedB = Data(base64Encoded: "PMbU75wwG6rDTySXn2ASWyfQuPoW5ham15SzIscpInwOPE2uk7sePsW4ra0dHcLDUMFQn/LgBggIKOo7YZ9hf1VReiAzXwSKSHdJHjHUURTC2eNpANGUPO1qzuXYgc/MP3MR+GipKHsz+KTLT+8wLjNaiCIHsL/7evJBMw9QqiwhyXlAIm5mGZfhdTVbGpLz2/QzrFmI6pUTrHpio6m1Q74DH3FBxxIeiIcuEdGdeVt9iUweowBRyf2woasTvSV1fbMQbl+lsWPwzt/a73+J30eOGFdSubqSVYh2pV0OLqRz7zPzJars12teCWUV+0WUIaxb14Mp7tlmqcTPuqZe9w==".data(using: .utf8)!)!
|
||||||
|
|
||||||
|
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
|
||||||
|
let sharedSecret = try client.calculateSharedSecret(password: Data(keyArray), salt: [UInt8](salt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))
|
||||||
|
|
||||||
|
let accountName = "anand.appleseed@example.com"
|
||||||
|
let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](salt), clientPublicKey: clientKeys.public, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
|
||||||
|
let m2 = client.calculateServerProof(clientPublicKey: clientKeys.public, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))
|
||||||
|
|
||||||
|
XCTAssertEqual(Data(m1).base64EncodedString(), "f/Bkq8gBTYxl7SyiRd4SXTyE/jM/g6E0mVyZIQDIsPg=")
|
||||||
|
XCTAssertEqual(Data(m2).base64EncodedString(), "R2rgqC9cMAtWiXUImOrvs4oF+ccibf8KaFsZQ22WokM=")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 137 KiB |
BIN
icon.png
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 22 KiB |
BIN
screenshot.png
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 193 KiB |