Compare commits

...

160 commits

Author SHA1 Message Date
Matt Kiazyk
1a0d3353b9 update readme with v3 changes 2025-09-19 23:53:54 -05:00
Matt Kiazyk
da8d22c6d3 bad package.resolved 2025-09-19 19:56:41 -05:00
Matt Kiazyk
4f9b7f8f6a remove old libfido2swift fix 2025-09-19 19:31:16 -05:00
Matt Kiazyk
f34e291433 3.0.2 2025-09-19 19:20:34 -05:00
Matt Kiazyk
789eedcdb7 add some glass effect... why not? 2025-09-19 19:19:58 -05:00
Matt Kiazyk
f9262d9a05 fix for codesign issues with libfido2swift 2025-09-19 19:19:48 -05:00
Matt Kiazyk
4776af75ee 3.0.1 2025-09-17 23:01:03 -05:00
Matt Kiazyk
cab5533b49
Merge pull request #756 from neetrath/main
Add Thai translations
2025-09-17 22:55:26 -05:00
Matt Kiazyk
fc50db1cee fix how platforms are architecture filtered 2025-09-17 22:37:19 -05:00
Ruttanachai Auitragool
576585b7de feat: Add Thai translations 2025-09-16 23:22:44 +07:00
Matt Kiazyk
6cf9647840
Merge pull request #755 from XcodesOrg/newIcon
New Icon - who dis?
2025-09-16 10:43:43 -05:00
Matt Kiazyk
90dccd8180 new fancy app icon! 2025-09-15 23:08:51 -05:00
Matt Kiazyk
372d5b1f4e
Merge pull request #753 from XcodesOrg/matt/FixXcoded26Check
fix: xcode 26 check for Apple Silicon installs
2025-09-15 22:59:56 -05:00
Matt Kiazyk
4063796833
Merge pull request #727 from megabitsenmzq/main
Update zh_Hans translations.
2025-09-15 22:42:13 -05:00
Matt Kiazyk
be2eea0532
Merge pull request #746 from LiYanan2004/filter-ux
Improve filtering UI/UX on Xcode List
2025-09-15 22:41:32 -05:00
Matt Kiazyk
a6dec75603 fix libfido2 errors on Xcode26 thanks @anciltech 2025-09-15 22:34:19 -05:00
Matt Kiazyk
9527fb2961 fix: xcode 26 check for Apple Silicon installs 2025-09-15 22:08:49 -05:00
Matt Kiazyk
084bd8f4a1
Merge pull request #744 from leschlogl/feature/portuguese-translations
Fixes Brazilian Portuguese translations
2025-09-15 22:06:46 -05:00
Lucas Eduardo Schlögl
f93da1c502 small translation improvements 2025-09-10 14:43:20 +02:00
LiYanan2004
79ef494504 Add icon for "Installed Only" filter 2025-09-02 22:22:55 +08:00
LiYanan2004
cdf3675a5e Improve filtering UI 2025-09-02 14:40:35 +08:00
Jinyu Meng
5974a85800 Update zh_Hans translations.
# Conflicts:
#	Xcodes/Resources/Localizable.xcstrings
2025-08-29 11:45:06 +09:00
Matt Kiazyk
4ccb6e7f06
Merge pull request #741 from r1b2ns/feat/redirect-new-user-to-login
feat: improve user experience with a redirect window to sign-in
2025-08-27 08:37:19 -05:00
Rubens
22e4991f9a
chore: add new localized string in all languages 2025-08-27 10:32:20 -03:00
Lucas Eduardo Schlögl
059c51b5dd Fixes Brazilian Portuguese translations 2025-08-27 13:30:47 +02:00
Matt Kiazyk
00dcf3816e v3.0.0 2025-08-26 22:22:11 -05:00
Matt Kiazyk
08738d6912
Merge pull request #743 from XcodesOrg/XcodeArchitectures
Support Showing and Downloading Multiple Xcode Architectures.
2025-08-26 20:49:45 -05:00
Matt Kiazyk
472e36ed0f adjust names 2025-08-25 22:59:28 -05:00
Matt Kiazyk
bfb8c2cbb8 fix tests 2025-08-25 22:45:21 -05:00
Matt Kiazyk
a7b3f78813 clean up localizable strings 2025-08-25 22:37:51 -05:00
Matt Kiazyk
2e2b16e759 warning if users download silicon runtime without xcode 26 selected 2025-08-25 22:26:41 -05:00
Matt Kiazyk
0bc8e42a9b have both arch versions as the same version on list 2025-08-23 21:51:13 -06:00
Matt Kiazyk
ceae881d9a show installed xcodes specific with architectures 2025-08-23 20:11:57 -06:00
Matt Kiazyk
debc41f688 support downloading individual xcode architecture versions 2025-08-23 14:56:40 -06:00
Matt Kiazyk
4b9d86b22e Support showing multiple architectures 2025-08-23 13:35:49 -06:00
Rubens
bbec397613
feat: add button to redirect user when this try to install a xcode version without signed 2025-08-20 08:38:16 -03:00
Matt Kiazyk
a434d26921
Merge pull request #730 from XcodesOrg/matt/multipleArch
Support Xcode 26 multiple Architectures
2025-07-09 21:46:04 -05:00
Matt Kiazyk
78615418b5 update ci image 2025-07-09 21:40:33 -05:00
Matt Kiazyk
54cca28dbe clean up localizations 2025-07-09 21:36:23 -05:00
Matt Kiazyk
10ce2b8511 support filtering 2025-07-07 23:03:20 -05:00
Matt Kiazyk
14b2b3da10 support displaying multiple architectures in runtimes 2025-06-25 15:31:51 -05:00
Matt Kiazyk
f3f3bf0e4f remove cache 2025-06-09 23:04:33 -05:00
Matt Kiazyk
3621601658 update jekyll gem 2025-06-09 23:02:54 -05:00
Matt Kiazyk
9630a68224 update jekyll 2025-06-09 22:59:06 -05:00
Matt Kiazyk
227f42af37 v2.4.2b31 2025-06-09 22:21:44 -05:00
Matt Kiazyk
8fbf9eef8a
Merge pull request #678 from mahadshahib/patch-1
Update AppState.swift
2025-06-09 22:18:41 -05:00
Matt Kiazyk
b302365454
Merge pull request #655 from MultiColourPixel/main
Support FIDO2 authentication with devices that don’t have a PIN code
2025-06-09 22:10:12 -05:00
Mohammad ShahibZadeh
a61338804c
Update AppState.swift
Fix the issue where entering an email address with capital letters causes the login to fail.
2024-12-24 12:31:56 +03:30
Edgar Story
259ad0789a
Improve the support for PIN-less FIDO2 devices
- We now check if in the handling of two factor option, the option to be used is a SecurityKey. If so, check if a FIDO2 device is attached and if it needs a PIN.
- When a PIN is not required, we can just move straight onto assertation, the code for which will present the touch key UI.
- Otherwise we fallback to the original flow.
2024-11-12 09:20:38 +00:00
Edgar Story
cc03660576
Push the setting of authError to happen on MainActor
- The Xcode “Run Time Issue” breakpoint was being hit whenever an error was being set, complaining about this being set outside of the main thread.
2024-11-12 09:14:51 +00:00
Edgar Story
a43bf63aab
Add function to check if a FIDO2 device is even connected 2024-11-12 09:12:44 +00:00
Edgar Story
cfef2879b5
Add function to check if fido2 device needs a PIN 2024-11-12 09:12:21 +00:00
Edgar Story
36424a78e0
Make fido2 property a lazy var
- This object was being kept around after being created and as we need it in some other functions it made sense to make it lazy and keep it around that way.
- Arguably the FIDO2 instance could be removed after each time it’s been used, but as the FIDO2 class doesn’t have any state stored in it, it seems benign keeping it about for now.
2024-11-12 09:08:26 +00:00
Edgar Story
3d9cf73fc1
Require LibFido2Swift to be 0.1.4 at least
- This has the new functionality for checking if a device is attached.
2024-11-12 09:02:45 +00:00
Edgar Story
2dc1bcdcbb
Update requirement for LibFido2Swift library to be at least 0.1.3 2024-11-10 22:06:14 +00:00
Edgar Story
afa6dff0b9
Support FIDO authentication with devices that don’t have a PIN code set 2024-11-10 12:16:17 +00:00
Matt Kiazyk
17f3d365b8 v2.4.1b30 2024-11-04 21:47:10 -06:00
Matt Kiazyk
7b154501b6
Merge pull request #650 from kabiroberai/kabir/legacy-srp
Fix handling of s2k_fo
2024-11-04 21:41:13 -06:00
Kabir Oberai
8654756d67 comment 2024-11-04 21:21:12 -05:00
Kabir Oberai
85c9bdba55 Fix handling of s2k_fo 2024-11-04 21:14:48 -05:00
Matt Kiazyk
0a02b29a4b v2.4.0b29 2024-10-28 22:28:29 -05:00
Matt Kiazyk
3a8722c07d
Merge pull request #640 from XcodesOrg/matt/SRPLogin
Support SRP Login
2024-10-28 22:22:21 -05:00
Matt Kiazyk
29bf77007a
Merge pull request #638 from abiligiri/SRPLogin_fix
SRP Login works now
2024-10-28 22:02:04 -05:00
Matt Kiazyk
a75c54f2f6
Merge pull request #632 from abiligiri/feature/more_managed_preferences
Restrict allowed versions & hide 'Support Xcodes'
2024-10-28 22:01:37 -05:00
Matt Kiazyk
42c2c6bfc0 move to xcodesOrg/swift-srp fork 2024-10-28 21:45:07 -05:00
Matt Kiazyk
69e667cf87 some cleanup of extra prints and old code 2024-10-28 21:34:41 -05:00
Anand Biligiri
dc22b913f3 Use swift-srp from remote source
- Use from https://github.com/abiligiri/swift-srp, version 1.1.0
  This is based on latest from upstream with changes required
- Remove local copy of swift-srp
2024-10-28 16:52:25 -07:00
Anand Biligiri
9b107ec98c SRP Login works now
- Switch to use https://github.com/adam-fowler/swift-srp with some modifications
  that are local
  - Pad g value to equal size of N while calculating clientProof
- Use SHA256(plain-text-password) while computing key using PBKDF2
- Added a unit test with some sample values
2024-10-28 13:25:17 -07:00
Matt Kiazyk
2ed84ef792 clean up srp client, still not working 2024-10-24 19:34:26 -05:00
Matt Kiazyk
e04ed029de (wip) SRP Login implementation 2024-10-22 23:35:59 -05:00
Anand Biligiri
e3f996da6e Restrict allowed versions & hide 'Support Xcodes'
- Introduced a new preference keys allowedMajorVersions, hideSupportXcodes
- allowedMajorVersions defaults to Int.max (ie allow all versions till date)
- allowedMajorVersions is used to limit the number of major versions to as many as
    value set for this key. Eg: A value of 1 would allow the latest GA version and one major version before
    A value of 0 would allow only the latest GA version
    A value of 2 would allow the latest GA and previous two major versions
- allowedMajorVersions does not have preference UI
    $ defaults write com.xcodesorg.xcodesapp allowedMajorVersions 2 #to limit to current GA and previous major
    $ defaults delete com.xcodesorg.xcodesapp allowedMajorVersions  #to remove limits
- Display buildNumber in bottom status bar
2024-10-20 12:51:14 -07:00
Matt Kiazyk
aca4e0ac89 update readme 2024-10-18 09:55:36 -05:00
Matt Kiazyk
dd9a348298 Bump version 2.3 2024-10-18 08:54:38 -05:00
Matt Kiazyk
d6efd8fa74
Merge pull request #629 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.197.0
Bump ruby/setup-ruby from 1.190.0 to 1.197.0
2024-10-18 08:38:20 -05:00
Matt Kiazyk
b81eab0cc4
Merge pull request #625 from XcodesOrg/dependabot/github_actions/actions/cache-4.1.1
Bump actions/cache from 4.0.2 to 4.1.1
2024-10-18 08:38:09 -05:00
Matt Kiazyk
dc31fe884c
Merge pull request #628 from XcodesOrg/matt/runtimeDownloadCleanup
allow cancelling of runtime downloads, cleanup
2024-10-18 08:37:50 -05:00
Matt Kiazyk
283c1a4739
Merge pull request #621 from Kyle-Ye/bugfix/windows
Terminate Xcodes app after last window closed
2024-10-18 08:37:35 -05:00
dependabot[bot]
cac0ebfab7
Bump ruby/setup-ruby from 1.190.0 to 1.197.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.190.0 to 1.197.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.190.0...v1.197.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-18 10:50:01 +00:00
Kyle
59331bcb38
Adjust TerminateAfterLastWindowClosed toggle location
Move from advanced pane into general
2024-10-17 00:32:56 +08:00
Kyle
3d6908f526
Fix Xcode warning for Localizable string 2024-10-16 21:50:16 +08:00
Kyle
b91a7e53ad
Update the missing Localizable string 2024-10-16 21:46:04 +08:00
Kyle
f8970f44d5
Add TerminateAfterLastWindowClosed toggle support in Misc 2024-10-16 14:02:55 +08:00
Matt Kiazyk
6050c78613 allow cancelling of runtime downloads, cleanup 2024-10-15 22:07:53 -05:00
Matt Kiazyk
cf85e2fc5a
Merge pull request #569 from abiligiri/feature/managed_preferences
Disallow changes to managed preferences
2024-10-15 21:38:49 -05:00
Matt Kiazyk
15630109d5
Merge pull request #598 from duffpod/main
Update Localizable.xcstrings for Russian language (3)
2024-10-15 21:36:41 -05:00
dependabot[bot]
7171b90c88
Bump actions/cache from 4.0.2 to 4.1.1
Bumps [actions/cache](https://github.com/actions/cache) from 4.0.2 to 4.1.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.0.2...v4.1.1)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 10:16:07 +00:00
Kyle
3d5fdd528b
Replace WIndowGroup with Window to fix multi window and terminate issue 2024-10-13 12:11:50 +08:00
Matt Kiazyk
06558610e9
Merge pull request #593 from pan93412/l10n/update-zhtw-localization
Update translation in zh-Hant
2024-10-12 22:53:58 -05:00
Matt Kiazyk
5e5fdf0d3a
Merge pull request #609 from megabitsenmzq/main
Fix one Greek localization. Add one zh_Hans localization.
2024-10-12 22:53:08 -05:00
Matt Kiazyk
8277554048
Merge pull request #617 from kinoroy/security-key-auth
Implement Security Key Auth
2024-10-12 22:50:16 -05:00
Matt Kiazyk
ba11d4129e
Merge pull request #601 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.190.0
Bump ruby/setup-ruby from 1.177.1 to 1.190.0
2024-10-12 22:36:12 -05:00
Matt Kiazyk
389d8d2a63
Merge pull request #603 from LiYanan2004/context-menu-fine-tune
Update label for Cancel Install Button
2024-10-12 22:34:37 -05:00
Kino Roy
f4567bdf1e Merge branch 'main' into security-key-auth
# Conflicts:
#	Xcodes.xcodeproj/project.pbxproj
2024-10-12 20:30:34 -07:00
Kino Roy
27bbf6f2a3 Add missing localizations 2024-10-12 20:26:19 -07:00
Matt Kiazyk
c1670b250a
Merge pull request #624 from XcodesOrg/matt/updateSparkle
chore: update sparkle framework
2024-10-12 22:25:00 -05:00
Matt Kiazyk
8e78c1c8c5
Merge pull request #622 from XcodesOrg/matt/cryptexRuntimeDownloads
feat: support downloading of cryptex (ex iOS 18+) runtimes
2024-10-12 22:24:48 -05:00
Matt Kiazyk
c31a1ef6fa Merge branch 'main' into matt/cryptexRuntimeDownloads 2024-10-12 21:16:32 -05:00
Matt Kiazyk
37a86643b9 chore: update sparkle framework 2024-10-12 21:16:00 -05:00
Matt Kiazyk
c245a1e69f
Merge pull request #613 from chuganzy/ganzy/rollback-loading
Add `authenticating` to the `XcodeInstallationStep`
2024-10-12 21:15:07 -05:00
Matt Kiazyk
0f5e42b88e
Merge pull request #623 from XcodesOrg/matt/Xcode16
chore: set CI to MacOS 14, Xcode 16
2024-10-12 21:03:17 -05:00
Matt Kiazyk
dd46112769 chore: set CI to MacOS 14, Xcode 16 2024-10-12 20:57:39 -05:00
Kino Roy
c3e568e234 Add missing English localization 2024-10-12 14:13:02 -07:00
Kino Roy
f269dcbc98 Fix typo 2024-10-12 14:12:50 -07:00
Kino Roy
604358bf6c Update LibFido2Swift with Xcode 15 support 2024-10-12 14:06:48 -07:00
Takeru Chuganji
1a3ca60f46
Temporarily add all translations 2024-10-12 10:08:59 +09:00
Kyle
27d5b03321
Terminate Xcodes app after last window closed 2024-10-12 00:09:20 +08:00
Matt Kiazyk
20b9833b67 feat: support downloading of cryptex runtimes 2024-10-11 10:57:16 -05:00
Kino Roy
e855a1fb62 Implement security key auth 2024-09-28 16:25:11 -07:00
Takeru Chuganji
908e7ba3ea
Add authenticating step 2024-09-22 18:06:29 +09:00
Jinyu Meng
7daa3e600e Fix one Greek localization. Add one zh_Hans localization. 2024-09-10 18:09:59 +09:00
LiYanan2004
a936bf84b2 Update label of CancelInstallButton to better describe the intent 2024-08-17 11:37:05 +08:00
dependabot[bot]
897f449b7c
Bump ruby/setup-ruby from 1.177.1 to 1.190.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.177.1 to 1.190.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.177.1...v1.190.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-29 10:27:15 +00:00
duffpod
5ebe24060f Update Localizable.strings for Russian language (3) 2024-07-17 21:16:04 +03:00
pan93412
13496d8a7d
Update translation
Co-Authored-By: itszero <itszero@gmail.com>
2024-07-09 09:23:56 +08:00
Yi-Jyun Pan
4b6616fdea
Update translation in zh-Hant 2024-07-08 01:52:38 +08:00
Matt Kiazyk
74237f8fc1 v 2.2.0 2024-06-28 10:38:20 -05:00
Matt Kiazyk
c94c2c1979
Merge pull request #568 from floydkim/korean-localisation
Update Korean localisation
2024-06-28 10:36:12 -05:00
Matt Kiazyk
b36165100b
Merge pull request #585 from XcodesOrg/matt/iOS18Runtimes
Fix runtime downloading
2024-06-28 10:35:34 -05:00
Matt Kiazyk
8a6f5a7deb
Merge pull request #581 from Person2099/patch-1
fix: Update GitHub URLs in help menu
2024-06-28 10:33:32 -05:00
Matt Kiazyk
600d4cbafa
Merge pull request #576 from MohamediosDev/feature/add_arabic_localization_to_app
ADD: Added Arabic Localizations To Xcodes
2024-06-28 10:26:24 -05:00
Matt Kiazyk
8811a6d0d8
Merge pull request #564 from LePips/left-align-unarchiving-pane
Left Align Unarchiving Pane
2024-06-28 10:19:52 -05:00
Matt Kiazyk
f973e7d744
Merge pull request #545 from Kyle-Ye/bugfix/dock
Fix broken DockProgress Bar
2024-06-28 10:19:32 -05:00
Anand Biligiri
6e64db26fb Disallow changes to managed preferences
- Define enumerations for preferences that can be managed in an enterprise environment using MDM
- Add methods in AppState to check for managed preferences
- Update Advanced, Download, Experiments and Update preference panes to disable controls
  to modify any of the managed preferences
- Update Xcode category list button to be disabled if preference is managed
2024-06-21 07:17:18 +05:30
Seb
97bf958301
Update XcodesApp.swift
Tiny fix in help menu to use updated GitHub URLs. Changed "https://github.com/RobotsAndPencils/XcodesApp" to "https://github.com/XcodesOrg/XcodesApp"
2024-06-15 00:28:06 +10:00
MohamediosDev
dc5f637886 ADD: Added Arabic Localizations To Xcodes 2024-06-11 19:44:04 +03:00
Matt Kiazyk
a587d53798 don't fail downloading old runtime images 2024-06-10 16:18:36 -05:00
floydkim
a433bc4f72 Update Korean localisation 2024-06-03 21:16:56 +09:00
Ethan Pippin
66deeb0d57 left align unarchiving pane 2024-05-28 11:47:50 -06:00
Matt Kiazyk
4a4b469e3f
Merge pull request #522 from LiYanan2004/swift-5.10
Resolve concurrency check warning on Swift 5.10
2024-05-24 20:39:28 -05:00
Matt Kiazyk
7628f85ba8
Merge pull request #563 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.177.1
Bump ruby/setup-ruby from 1.173.0 to 1.177.1
2024-05-24 20:36:45 -05:00
Matt Kiazyk
f64e97f7a2
Merge pull request #560 from mikeirvingweb/png-compression
chore: lossless png compression
2024-05-24 20:36:35 -05:00
Matt Kiazyk
1152bbfb28
Merge pull request #548 from alladinian/alladinian-readme-greek
Update README.md
2024-05-24 20:34:42 -05:00
Matt Kiazyk
33dc6347f2 after last step reset dock progress 2024-05-24 20:23:03 -05:00
Matt Kiazyk
7599178042
Merge pull request #559 from mikeirvingweb/product-name-casings
fix: product name casings
2024-05-24 16:14:37 -05:00
Matt Kiazyk
6fda52ed37
Merge pull request #547 from roddymunro/add-swiftpolyglot-version
Set explicit SwiftPolyglot tag and update clone path
2024-05-24 16:13:39 -05:00
dependabot[bot]
8f1f6e9363
Bump ruby/setup-ruby from 1.173.0 to 1.177.1
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.173.0 to 1.177.1.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.173.0...v1.177.1)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-23 10:30:04 +00:00
Mike Irving
848ce1d362 fix: rollback icon.png 2024-05-22 21:58:33 +01:00
Mike Irving
38ae157be5 fix: rollback A&P, App Icon, Beta Icon 2024-05-22 21:57:16 +01:00
Mike Irving
7f3e8125c0 chore: lossless png compression 2024-05-14 22:40:07 +01:00
Mike Irving
7f14a4b8b5
fix: product name casings 2024-05-14 22:18:30 +01:00
Roddy Munro
cd80053be6 Move branch argument before url 2024-05-09 19:03:56 -03:00
Vasilis Akoinoglou
021d0aa745
Update README.md
Added Greek localization to the language support table
2024-04-05 10:26:05 +03:00
Roddy Munro
180dd52eee Set explicit SwiftPolyglot tag and update clone path 2024-04-04 21:27:15 -03:00
Kyle
348c89871e
Fix broken DockProgress Bar
Update DockProgress and adopt the new Concurrency Model
2024-03-31 01:44:34 +08:00
Matt Kiazyk
21c26c9793
Merge pull request #542 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.173.0
Bump ruby/setup-ruby from 1.172.0 to 1.173.0
2024-03-22 10:15:28 -05:00
Matt Kiazyk
4e177b5fbf
Merge pull request #543 from XcodesOrg/dependabot/github_actions/actions/cache-4.0.2
Bump actions/cache from 4.0.1 to 4.0.2
2024-03-22 10:15:18 -05:00
Matt Kiazyk
411f76788c
Merge pull request #533 from XcodesOrg/matt/testLocalizationValidation
adds localization validation on PR's
2024-03-22 10:15:05 -05:00
Matt Kiazyk
88b15b989a Update missing translations. 2024-03-22 10:09:47 -05:00
Matt Kiazyk
45626e2789
Merge pull request #540 from marlonjames71/InfoPaneTweaks
InfoPane UI Tweaks
2024-03-22 09:57:20 -05:00
dependabot[bot]
1411bd30b3
Bump actions/cache from 4.0.1 to 4.0.2
Bumps [actions/cache](https://github.com/actions/cache) from 4.0.1 to 4.0.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.0.1...v4.0.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-20 10:42:25 +00:00
dependabot[bot]
f3472d8cc6
Bump ruby/setup-ruby from 1.172.0 to 1.173.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.172.0 to 1.173.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.172.0...v1.173.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-20 10:42:22 +00:00
Marlon
b9d37054ea
Reduces size of screenshots even more 2024-03-20 02:06:52 -04:00
Marlon
48df320aa8
Reduces size of screenshots 2024-03-20 02:02:49 -04:00
Marlon
feac373350
Updates screenshots for README 2024-03-19 23:35:45 -04:00
Marlon
9e1eff06a3
InfoPane now responsible for its padding
- Uses content margins instead of padding when it can to avoid content clipping during scrolling
- Also fixes a few indentation inconsistencies
- Adds min width for platforms section
2024-03-19 22:51:57 -04:00
Marlon
35314ce36f
Adjusts layout of platform views 2024-03-19 22:45:51 -04:00
Matt Kiazyk
c417978976
Update xcstrings.yml 2024-03-16 21:18:32 -05:00
Matt Kiazyk
aa34f3b10d allow workflow dispatch 2024-03-16 21:16:44 -05:00
Matt Kiazyk
c595eba5df Test localizable.xcstrings Validation action 2024-03-16 21:12:46 -05:00
LiYanan2004
9a5e127387 Resolve warning on Swift 5.10 2024-03-16 10:28:16 +08:00
92 changed files with 7238 additions and 424 deletions

View file

@ -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.
persist-credentials: false
- name: Cache 📦
uses: actions/cache@v4.0.1
with:
path: AppCast/vendor/bundle
key: ${{ runner.os }}-gems-v1.0-${{ hashFiles('AppCast/Gemfile') }}
restore-keys: |
${{ runner.os }}-gems-
# - name: Cache 📦
# uses: actions/cache@v4.1.1
# with:
# path: AppCast/vendor/bundle
# key: ${{ runner.os }}-gems-v1.0-${{ hashFiles('AppCast/Gemfile') }}
# restore-keys: |
# ${{ runner.os }}-gems-
- name: Setup Ruby, JRuby and TruffleRuby
uses: ruby/setup-ruby@v1.172.0
uses: ruby/setup-ruby@v1.197.0
with:
ruby-version: '3.0'

View file

@ -8,10 +8,10 @@ on:
jobs:
test:
runs-on: macos-13
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Run tests
env:
DEVELOPER_DIR: /Applications/Xcode_15.0.1.app
DEVELOPER_DIR: /Applications/Xcode_16.4.app
run: xcodebuild test -scheme Xcodes

22
.github/workflows/xcstrings.yml vendored Normal file
View 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

View file

@ -8,7 +8,7 @@ source "https://rubygems.org"
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> 3.9.0"
gem "jekyll", "~> 4.4.1"
gem "jekyll-github-metadata", group: :jekyll_plugins

View file

@ -1,88 +1,110 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
base64 (0.3.0)
bigdecimal (3.2.2)
colorator (1.1.0)
concurrent-ruby (1.1.7)
em-websocket (0.5.2)
concurrent-ruby (1.3.5)
csv (3.3.5)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
http_parser.rb (~> 0)
eventmachine (1.2.7)
faraday (1.3.0)
faraday-net_http (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
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)
http_parser.rb (0.6.0)
i18n (0.9.5)
google-protobuf (4.31.1)
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)
jekyll (3.9.0)
jekyll (4.4.1)
addressable (~> 2.4)
base64 (~> 0.2)
colorator (~> 1.0)
csv (~> 3.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
i18n (~> 1.0)
jekyll-sass-converter (>= 2.0, < 4.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)
mercenary (~> 0.3.3)
mercenary (~> 0.3, >= 0.3.6)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
rouge (>= 3.0, < 5.0)
safe_yaml (~> 1.0)
terminal-table (>= 1.8, < 4.0)
webrick (~> 1.7)
jekyll-github-metadata (2.13.0)
jekyll (>= 3.4, < 5.0)
octokit (~> 4.0, != 4.4.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-sass-converter (3.1.0)
sass-embedded (~> 1.75)
jekyll-watch (2.2.1)
listen (~> 3.0)
kramdown (2.3.1)
rexml
json (2.12.2)
kramdown (2.5.1)
rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.3)
listen (3.4.1)
liquid (4.0.4)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.3.6)
mercenary (0.4.0)
multipart-post (2.1.1)
octokit (4.20.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (4.0.6)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
public_suffix (6.0.2)
rake (13.3.0)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rexml (3.2.5)
rouge (3.26.0)
rexml (3.4.1)
rouge (4.5.2)
ruby2_keywords (0.0.2)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sass-embedded (1.89.2)
google-protobuf (~> 4.31)
rake (>= 13)
sass-embedded (1.89.2-x86_64-darwin)
google-protobuf (~> 4.31)
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
thread_safe (0.3.6)
tzinfo (1.2.10)
thread_safe (~> 0.1)
tzinfo-data (1.2020.6)
tzinfo (>= 1.0.0)
unicode-display_width (2.6.0)
wdm (0.1.1)
webrick (1.9.1)
PLATFORMS
ruby
x86_64-darwin-20
DEPENDENCIES
jekyll (~> 3.9.0)
jekyll (~> 4.4.1)
jekyll-github-metadata
kramdown-parser-gfm
tzinfo (~> 1.2)
@ -90,4 +112,4 @@ DEPENDENCIES
wdm (~> 0.1.0)
BUNDLED WITH
2.2.5
2.6.9

View file

@ -7,8 +7,8 @@ We love your input! We want to make contributing to this project as easy and tra
- Proposing new features
- Becoming a maintainer
## We Develop with Github
We use github to host code, to track issues and feature requests, as well as accept pull requests.
## We Develop with GitHub
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
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
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!
## Write bug reports with detail, background, and sample code

BIN
IconDark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
IconMono.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -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.
@ -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`.
- View release notes, OS compatibility, included SDKs and compilers from [Xcode Releases](https://xcodereleases.com).
- Dark/Light Mode supported
- Security Key Authentication supported
- Support installing Platforms/Runtimes
- Support installing Apple Silicon variants
## Platforms/Runtimes
- 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
- 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)|
|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)|
|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.
## Installation
v1.X - requires MacOS 11 or newer
v2.X - requires MacOS 13
v1.X - requires macOS 11 or newer
v2.X - requires macOS 13
v3.X - requires macOS 13 - architecture variants and updated icon.
### Install with Homebrew
@ -81,7 +92,7 @@ Xcodes.app and CLI is updated, maintained with contributors like yourself. Even
## 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
```
@ -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
```
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.
@ -160,7 +170,8 @@ popd
# Attach the zip that was created in the Product directory to 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>

View 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

View file

@ -7,6 +7,10 @@
objects = {
/* 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 */; };
36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.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 */; };
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8512595080100E47BAF /* AcknowledgementsView.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 */; };
CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF87A2595293E00E47BAF /* DataSource.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 */; };
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.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 */; };
E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.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 */; };
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */; };
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F532B333864003F3959 /* PlatformsListView.swift */; };
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 */; };
E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.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 */; };
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.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 */; };
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.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 */; };
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 */; };
@ -192,6 +198,9 @@
/* End PBXCopyFilesBuildPhase 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>"; };
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>"; };
@ -313,6 +322,7 @@
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>"; };
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>"; };
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>"; };
@ -332,6 +342,7 @@
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@ -346,12 +357,14 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */,
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */,
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
E891A1C42B43ACF900A1B9D1 /* Sparkle in Frameworks */,
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */,
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */,
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */,
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */,
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */,
@ -376,6 +389,7 @@
63EAA4E9259944340046AB8F /* Common */ = {
isa = PBXGroup;
children = (
D93F95C02E0C8C1A00238FB5 /* TagView.swift */,
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */,
63EAA4EA259944450046AB8F /* ProgressButton.swift */,
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */,
@ -408,6 +422,7 @@
CA538A12255A4F7C00E64DD7 /* Frameworks */ = {
isa = PBXGroup;
children = (
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -454,6 +469,8 @@
CA735108257BF96D00EA9CF8 /* AttributedText.swift */,
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */,
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */,
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */,
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */,
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */,
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
@ -545,6 +562,7 @@
CABFAA1D2592F7F200380FEE /* Resources */ = {
isa = PBXGroup;
children = (
E8EEAD1C2E79174F00BE67E8 /* XcodesIcon.icon */,
CAA858DA25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz */,
CAA8588025A2B63A00ACF8C0 /* aria2c */,
CAA8588A25A2B69300ACF8C0 /* aria2c.LICENSE */,
@ -706,14 +724,15 @@
CABFA9ED2592F0CC00380FEE /* SwiftSoup */,
CABFA9F72592F0F900380FEE /* KeychainAccess */,
CABFA9FC2592F13300380FEE /* LegibleError */,
CA9FF86C25951C6E00E47BAF /* XCModel */,
CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */,
E689540225BE8C64000EBCEA /* DockProgress */,
E8FD5726291EE4AC001E004C /* AsyncNetworkService */,
E8C0EB19291EF43E0081528A /* XcodesKit */,
E8F44A1D296B4CD7002D6592 /* Path */,
E84E4F562B335094003F3959 /* OrderedCollections */,
E891A1C32B43ACF900A1B9D1 /* Sparkle */,
E83FDC432CBB649100679C6B /* Sparkle */,
334A932B2CA885A400A5E079 /* LibFido2Swift */,
E862D43A2CC8B26F00BAA376 /* SRP */,
);
productName = XcodesMac;
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
@ -786,6 +805,8 @@
"pt-BR",
nl,
pl,
ar,
th,
);
mainGroup = CAD2E7952449574E00113D76;
packageReferences = (
@ -793,14 +814,14 @@
CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */,
CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */,
CABFA9FB2592F13300380FEE /* XCRemoteSwiftPackageReference "LegibleError" */,
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */,
CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */,
CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */,
E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */,
E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */,
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */,
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */,
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */,
);
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
projectDirPath = "";
@ -820,6 +841,7 @@
files = (
CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */,
9DD4FFCB2B13EC1800C974F1 /* Localizable.xcstrings in Resources */,
E8EEAD1D2E79174F00BE67E8 /* XcodesIcon.icon in Resources */,
CA9FF83F2594FBC000E47BAF /* Licenses.rtf in Resources */,
CAA858DB25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz in Resources */,
CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */,
@ -888,6 +910,7 @@
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */,
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
@ -913,7 +936,9 @@
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */,
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */,
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */,
D93F95C12E0C8C1A00238FB5 /* TagView.swift in Sources */,
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */,
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
@ -1072,13 +1097,14 @@
CA8FB636256E154800469DA5 /* Test */ = {
isa = XCBuildConfiguration;
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_SIGN_ENTITLEMENTS = Xcodes/Resources/XcodesTest.entitlements;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 26;
CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
@ -1090,7 +1116,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.1.2;
MARKETING_VERSION = 3.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
PRODUCT_NAME = Xcodes;
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1324,13 +1350,14 @@
CAD2E7BD2449575100113D76 /* Debug */ = {
isa = XCBuildConfiguration;
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_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 26;
CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = ZU6GR6B2FY;
ENABLE_HARDENED_RUNTIME = YES;
@ -1342,7 +1369,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.1.2;
MARKETING_VERSION = 3.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
PRODUCT_NAME = Xcodes;
SWIFT_VERSION = 5.0;
@ -1352,13 +1379,14 @@
CAD2E7BE2449575100113D76 /* Release */ = {
isa = XCBuildConfiguration;
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_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 26;
CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = ZU6GR6B2FY;
ENABLE_HARDENED_RUNTIME = YES;
@ -1370,7 +1398,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.1.2;
MARKETING_VERSION = 3.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
PRODUCT_NAME = Xcodes;
SWIFT_VERSION = 5.0;
@ -1468,9 +1496,9 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */ = {
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/xcodereleases/data";
repositoryURL = "https://github.com/kinoroy/LibFido2Swift";
requirement = {
branch = main;
kind = branch;
@ -1529,7 +1557,15 @@
repositoryURL = "https://github.com/sindresorhus/DockProgress";
requirement = {
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" */ = {
@ -1540,14 +1576,6 @@
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" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mxcl/Path.swift";
@ -1567,10 +1595,9 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
CA9FF86C25951C6E00E47BAF /* XCModel */ = {
334A932B2CA885A400A5E079 /* LibFido2Swift */ = {
isa = XCSwiftPackageProductDependency;
package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */;
productName = XCModel;
productName = LibFido2Swift;
};
CAA1CB2C255A5262003FD669 /* AppleAPI */ = {
isa = XCSwiftPackageProductDependency;
@ -1611,15 +1638,19 @@
package = E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */;
productName = DockProgress;
};
E83FDC432CBB649100679C6B /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
E84E4F562B335094003F3959 /* OrderedCollections */ = {
isa = XCSwiftPackageProductDependency;
package = E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */;
productName = OrderedCollections;
};
E891A1C32B43ACF900A1B9D1 /* Sparkle */ = {
E862D43A2CC8B26F00BAA376 /* SRP */ = {
isa = XCSwiftPackageProductDependency;
package = E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
productName = SRP;
};
E8C0EB19291EF43E0081528A /* XcodesKit */ = {
isa = XCSwiftPackageProductDependency;

View file

@ -10,6 +10,15 @@
"version": null
}
},
{
"package": "big-num",
"repositoryURL": "https://github.com/adam-fowler/big-num",
"state": {
"branch": null,
"revision": "5c5511ad06aeb2b97d0868f7394e14a624bfb1c7",
"version": "2.0.2"
}
},
{
"package": "CombineExpectations",
"repositoryURL": "https://github.com/groue/CombineExpectations",
@ -19,22 +28,13 @@
"version": "0.6.0"
}
},
{
"package": "XcodeReleases",
"repositoryURL": "https://github.com/xcodereleases/data",
"state": {
"branch": "main",
"revision": "a43ad89e536d7a3da525fcc23fb182c37b756ecc",
"version": null
}
},
{
"package": "DockProgress",
"repositoryURL": "https://github.com/sindresorhus/DockProgress",
"state": {
"branch": null,
"revision": "7100b68571e2dafe3a06ad5905b80fc3b0107b4b",
"version": "3.2.0"
"revision": "d4f23b5a8f5ca0fac393eb7ba78c2fe3e32e52da",
"version": "4.3.1"
}
},
{
@ -64,6 +64,15 @@
"version": "1.0.4"
}
},
{
"package": "LibFido2Swift",
"repositoryURL": "https://github.com/kinoroy/LibFido2Swift",
"state": {
"branch": "main",
"revision": "b87a93300c5b35307c9f26ae490963196bd927f1",
"version": null
}
},
{
"package": "Path.swift",
"repositoryURL": "https://github.com/mxcl/Path.swift",
@ -78,8 +87,8 @@
"repositoryURL": "https://github.com/sparkle-project/Sparkle/",
"state": {
"branch": null,
"revision": "47d3d90aee3c52b6f61d04ceae426e607df62347",
"version": "2.5.2"
"revision": "0ef1ee0220239b3776f433314515fd849025673f",
"version": "2.6.4"
}
},
{
@ -91,6 +100,24 @@
"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",
"repositoryURL": "https://github.com/scinfu/SwiftSoup",

View file

@ -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.
import PackageDescription
let package = Package(
name: "AppleAPI",
platforms: [.macOS(.v10_15)],
platforms: [.macOS(.v11)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "AppleAPI",
targets: ["AppleAPI"]),
],
dependencies: [],
dependencies: [
.package(url: "https://github.com/xcodesOrg/swift-srp", branch: "main")
],
targets: [
// 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.
.target(
name: "AppleAPI",
dependencies: []),
dependencies: [.product(name: "SRP", package: "swift-srp")]),
.testTarget(
name: "AppleAPITests",
dependencies: ["AppleAPI"]),

View file

@ -1,5 +1,9 @@
import Foundation
import Combine
import SRP
import Crypto
import CommonCrypto
public class Client {
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
@ -8,8 +12,12 @@ public class Client {
// 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!
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
let clientKeys = client.generateKeys()
let a = clientKeys.public
return Current.network.dataTask(with: URLRequest.itcServiceKey)
.map(\.data)
@ -24,11 +32,45 @@ public class Client {
.map { return (serviceKey, $0)}
.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.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()
}
return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password, hashcash: hashcash))
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 }
.eraseToAnyPublisher()
} catch {
return Fail(error: AuthenticationError.srpInvalidPublicKey)
.eraseToAnyPublisher()
}
}
.flatMap { result -> AnyPublisher<AuthenticationState, Swift.Error> in
let (data, response) = result
@ -118,7 +160,7 @@ public class Client {
case .twoStep:
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
.eraseToAnyPublisher()
case .twoFactor:
case .twoFactor, .securityKey:
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
.eraseToAnyPublisher()
case .unknown:
@ -139,7 +181,10 @@ public class Client {
// SMS wasn't sent automatically because user needs to choose a phone to send to
} else if authOptions.canFallBackToSMS {
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 {
option = .codeSent
}
@ -193,6 +238,33 @@ public class Client {
.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
/// 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 }
.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
@ -252,6 +367,7 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
case notDeveloperAppleId
case notAuthorized
case invalidResult(resultString: String?)
case srpInvalidPublicKey
public var errorDescription: String? {
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."
case let .invalidResult(resultString):
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 codeSent
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 let trustedPhoneNumbers: [TrustedPhoneNumber]?
public let trustedDevices: [TrustedDevice]?
public let securityCode: SecurityCodeInfo
public let securityCode: SecurityCodeInfo?
public let noTrustedDevices: Bool?
public let serviceErrors: [ServiceError]?
public let fsaChallenge: FSAChallenge?
public init(
trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?,
trustedDevices: [AuthOptionsResponse.TrustedDevice]?,
securityCode: AuthOptionsResponse.SecurityCodeInfo,
noTrustedDevices: Bool? = nil,
serviceErrors: [ServiceError]? = nil
serviceErrors: [ServiceError]? = nil,
fsaChallenge: FSAChallenge? = nil
) {
self.trustedPhoneNumbers = trustedPhoneNumbers
self.trustedDevices = trustedDevices
self.securityCode = securityCode
self.noTrustedDevices = noTrustedDevices
self.serviceErrors = serviceErrors
self.fsaChallenge = fsaChallenge
}
public var kind: Kind {
@ -354,6 +482,8 @@ public struct AuthOptionsResponse: Equatable, Decodable {
return .twoStep
} else if trustedPhoneNumbers != nil {
return .twoFactor
} else if fsaChallenge != nil {
return .securityKey
} else {
return .unknown
}
@ -416,7 +546,7 @@ public struct AuthOptionsResponse: Equatable, Decodable {
}
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 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()
}
}

View file

@ -9,6 +9,11 @@ public extension URL {
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 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 {
@ -105,6 +110,19 @@ public extension URLRequest {
}
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 {
var request = URLRequest(url: .trust)
@ -136,4 +154,51 @@ public extension URLRequest {
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
}

View file

@ -13,8 +13,8 @@ extension AppState {
// check to see if we should auto install for the user
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 }
// get newest xcode version
@ -227,6 +227,7 @@ extension AppState {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.InstallArchive.Error.Title"), message: error.legibleLocalizedDescription)
}
resetDockProgressTracking()
})
.catch { _ in
Just(installedXcode)
@ -473,19 +474,24 @@ extension AppState {
// MARK: - Dock Progress Tracking
private func setupDockProgress() {
DockProgress.progressInstance = nil
DockProgress.style = .bar
Task { @MainActor in
DockProgress.progressInstance = nil
DockProgress.style = .bar
let progress = Progress(totalUnitCount: AppState.totalProgressUnits)
progress.kind = .file
progress.fileOperationKind = .downloading
overallProgress = progress
DockProgress.progressInstance = overallProgress
}
let progress = Progress(totalUnitCount: AppState.totalProgressUnits)
progress.kind = .file
progress.fileOperationKind = .downloading
overallProgress = progress
DockProgress.progressInstance = overallProgress
}
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: -
@ -496,7 +502,7 @@ extension AppState {
self.allXcodes[index].installState = .installing(step)
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)
}
}

View file

@ -4,6 +4,7 @@ import OSLog
import Combine
import Path
import AppleAPI
import Version
extension AppState {
func updateDownloadableRuntimes() {
@ -48,6 +49,90 @@ extension AppState {
}
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 {
do {
let downloadedURL = try await downloadRunTimeFull(runtime: runtime)
@ -57,6 +142,9 @@ extension AppState {
self.setInstallationStep(of: runtime, to: .installing)
}
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:
// not supported yet (do we need to for old packages?)
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)")
DispatchQueue.main.async {
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 {
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
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)"
// 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")
@ -123,9 +223,15 @@ extension AppState {
}
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")
}
}
let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: url) ?? []
return Current.shell.downloadWithAria2Async(aria2Path, runtime.url, destination, cookies)
return Current.shell.downloadWithAria2Async(aria2Path, url, destination, cookies)
}
@ -140,7 +246,10 @@ extension AppState {
runtimePublishers[runtime.identifier] = nil
// 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 aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")
@ -166,7 +275,10 @@ extension AppState {
}
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 {

View file

@ -3,7 +3,6 @@ import Foundation
import Path
import Version
import SwiftSoup
import struct XCModel.Xcode
import AppleAPI
import XcodesKit
@ -211,8 +210,8 @@ extension AppState {
private func xcodeReleases() -> AnyPublisher<[AvailableXcode], Error> {
Current.network.dataTask(with: URLRequest(url: URL(string: "https://xcodereleases.com/data.json")!))
.map(\.data)
.decode(type: [XCModel.Xcode].self, decoder: JSONDecoder())
.map { xcReleasesXcodes in
.decode(type: [XcodeRelease].self, decoder: JSONDecoder())
.map { xcReleasesXcodes in
let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> AvailableXcode? in
guard
let downloadURL = xcReleasesXcode.links?.download?.url,
@ -233,7 +232,8 @@ extension AppState {
requiredMacOSVersion: xcReleasesXcode.requires,
releaseNotesURL: xcReleasesXcode.links?.notes?.url,
sdks: xcReleasesXcode.sdks,
compilers: xcReleasesXcode.compilers
compilers: xcReleasesXcode.compilers,
architectures: xcReleasesXcode.architectures
)
}
return xcodes

View file

@ -9,6 +9,27 @@ import Version
import os.log
import DockProgress
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 {
private let client = AppleAPI.Client()
@ -66,18 +87,24 @@ class AppState: ObservableObject {
}
}
var disableLocalPathChange: Bool { PreferenceKey.localPath.isManaged() }
@Published var installPath = "" {
didSet {
Current.defaults.set(installPath, forKey: "installPath")
}
}
var disableInstallPathChange: Bool { PreferenceKey.installPath.isManaged() }
@Published var unxipExperiment = false {
didSet {
Current.defaults.set(unxipExperiment, forKey: "unxipExperiment")
}
}
var disableUnxipExperiment: Bool { PreferenceKey.unxipExperiment.isManaged() }
@Published var createSymLinkOnSelect = false {
didSet {
Current.defaults.set(createSymLinkOnSelect, forKey: "createSymLinkOnSelect")
@ -85,7 +112,7 @@ class AppState: ObservableObject {
}
var createSymLinkOnSelectDisabled: Bool {
return onSelectActionType == .rename
return onSelectActionType == .rename || PreferenceKey.createSymLinkOnSelect.isManaged()
}
@Published var onSelectActionType = SelectedActionType.none {
@ -98,12 +125,20 @@ class AppState: ObservableObject {
}
}
var onSelectActionTypeDisabled: Bool { PreferenceKey.onSelectActionType.isManaged() }
@Published var showOpenInRosettaOption = false {
didSet {
Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption")
}
}
@Published var terminateAfterLastWindowClosed = false {
didSet {
Current.defaults.set(terminateAfterLastWindowClosed, forKey: "terminateAfterLastWindowClosed")
}
}
// MARK: - Runtimes
@Published var downloadableRuntimes: [DownloadableRuntime] = []
@ -112,7 +147,7 @@ class AppState: ObservableObject {
// MARK: - Publisher Cancellables
var cancellables = Set<AnyCancellable>()
private var installationPublishers: [Version: AnyCancellable] = [:]
private var installationPublishers: [XcodeID: AnyCancellable] = [:]
internal var runtimePublishers: [String: Task<(), any Error>] = [:]
private var selectPublisher: AnyCancellable?
private var uninstallPublisher: AnyCancellable?
@ -173,13 +208,14 @@ class AppState: ObservableObject {
onSelectActionType = SelectedActionType(rawValue: Current.defaults.string(forKey: "onSelectActionType") ?? "none") ?? .none
installPath = Current.defaults.string(forKey: "installPath") ?? Path.defaultInstallDirectory.string
showOpenInRosettaOption = Current.defaults.bool(forKey: "showOpenInRosettaOption") ?? false
terminateAfterLastWindowClosed = Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
}
// MARK: Timer
/// Runs a timer every 6 hours when app is open to check if it needs to auto install any xcodes
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 }
autoInstallTimer = Timer.scheduledTimer(withTimeInterval: 60*60*6, repeats: true) { [weak self] _ in
@ -242,7 +278,7 @@ class AppState: ObservableObject {
func signIn(username: String, password: String) {
authError = nil
signIn(username: username, password: password)
signIn(username: username.lowercased(), password: password)
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in }
@ -255,7 +291,7 @@ class AppState: ObservableObject {
Current.defaults.set(username, forKey: "username")
isProcessingAuthRequest = true
return client.login(accountName: username, password: password)
return client.srpLogin(accountName: username, password: password)
.receive(on: DispatchQueue.main)
.handleEvents(
receiveOutput: { authenticationState in
@ -270,11 +306,17 @@ class AppState: ObservableObject {
}
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
self.presentedSheet = .twoFactor(.init(
option: option,
authOptions: authOptions,
sessionData: AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
))
let sessionData = AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
if option == .securityKey, fido2DeviceIsPresent() && !fido2DeviceNeedsPin() {
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) {
@ -320,6 +362,83 @@ class AppState: ObservableObject {
.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>) {
switch completion {
case let .failure(error):
@ -405,10 +524,10 @@ class AppState: ObservableObject {
// MARK: - Install
func checkMinVersionAndInstall(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
func checkMinVersionAndInstall(id: XcodeID) {
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 hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) {
// prompt
@ -432,10 +551,15 @@ class AppState: ObservableObject {
return !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion)
}
func install(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
func install(id: XcodeID) {
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
installationPublishers[id] = signInIfNeeded()
.handleEvents(
receiveSubscription: { [unowned self] _ in
self.setInstallationStep(of: availableXcode.version, to: .authenticating)
}
)
.flatMap { [unowned self] in
// 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,
@ -479,7 +603,7 @@ class AppState: ObservableObject {
.mapError { $0 as Error }
}
.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)
.sink(
@ -487,7 +611,11 @@ class AppState: ObservableObject {
self.installationPublishers[id] = nil
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
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.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
/// As of Nov 2022 this was returning a 403 forbidden
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)
.sink(
receiveCompletion: { [unowned self] completion in
@ -526,7 +654,7 @@ class AppState: ObservableObject {
}
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
installationPublishers[id] = nil
@ -644,7 +772,7 @@ class AppState: ObservableObject {
config.allowsRunningApplicationSubstitution = false
NSWorkspace.shared.openApplication(at: path.url, configuration: config)
default:
Logger.appState.error("\(xcode.id) is not installed")
Logger.appState.error("\(xcode.id.version) is not installed")
return
}
}
@ -740,7 +868,7 @@ class AppState: ObservableObject {
// 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.
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
// 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.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
return availableXcodesWithIdenticalBuildIdentifiers.count == 1 ||
// 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
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
.filter {
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 !prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.isEmpty, availableXcode.version.prereleaseIdentifiers.isEmpty {
identicalBuilds = [availableXcode.version] + prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.map(\.version)
identicalBuilds = [availableXcode.xcodeID] + prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.map(\.xcodeID)
} else {
identicalBuilds = []
}
// 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
let defaultXcodeInstallState: XcodeInstallState = installedXcode.map { .installed($0.path) } ?? .notInstalled
@ -803,7 +938,8 @@ class AppState: ObservableObject {
releaseDate: availableXcode.releaseDate,
sdks: availableXcode.sdks,
compilers: availableXcode.compilers,
downloadFileSize: availableXcode.fileSize
downloadFileSize: availableXcode.fileSize,
architectures: availableXcode.architectures
)
}

View file

@ -1,11 +1,12 @@
import Foundation
import Version
import struct XCModel.SDKs
import struct XCModel.Compilers
import XcodesKit
/// A version of Xcode that's available for installation
public struct AvailableXcode: Codable {
public var version: Version
public var version: Version {
return xcodeID.version
}
public let url: URL
public let filename: String
public let releaseDate: Date?
@ -14,9 +15,11 @@ public struct AvailableXcode: Codable {
public let sdks: SDKs?
public let compilers: Compilers?
public let fileSize: Int64?
public let architectures: [Architecture]?
public var downloadPath: String {
return url.path
}
public var xcodeID: XcodeID
public init(
version: Version,
@ -27,9 +30,9 @@ public struct AvailableXcode: Codable {
releaseNotesURL: URL? = nil,
sdks: SDKs? = nil,
compilers: Compilers? = nil,
fileSize: Int64? = nil
fileSize: Int64? = nil,
architectures: [Architecture]? = nil
) {
self.version = version
self.url = url
self.filename = filename
self.releaseDate = releaseDate
@ -38,5 +41,7 @@ public struct AvailableXcode: Codable {
self.sdks = sdks
self.compilers = compilers
self.fileSize = fileSize
self.architectures = architectures
self.xcodeID = XcodeID(version: version, architectures: architectures)
}
}

View file

@ -14,4 +14,6 @@ public enum DataSource: String, CaseIterable, Identifiable, CustomStringConverti
case .xcodeReleases: return "Xcode Releases"
}
}
var isManaged: Bool { PreferenceKey.dataSource.isManaged() }
}

View file

@ -13,4 +13,6 @@ public enum Downloader: String, CaseIterable, Identifiable, CustomStringConverti
case .aria2: return "aria2"
}
}
var isManaged: Bool { PreferenceKey.downloader.isManaged() }
}

View file

@ -116,7 +116,8 @@ public struct Shell {
return AsyncThrowingStream<Progress, Error> { continuation in
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.fileOperationKind = .downloading
@ -195,6 +196,84 @@ public struct Shell {
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 {

View file

@ -1,12 +1,17 @@
import Foundation
import Version
import Path
import XcodesKit
/// A version of Xcode that's already installed
public struct InstalledXcode: Equatable {
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
public let version: Version
public var version: Version {
return xcodeID.version
}
public init?(path: Path) {
self.path = path
@ -31,12 +36,21 @@ public struct InstalledXcode: Equatable {
else if infoPlist.bundleIconName == "XcodeBeta", !prereleaseIdentifiers.contains("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,
patch: bundleVersion.patch,
prereleaseIdentifiers: prereleaseIdentifiers,
buildMetadataIdentifiers: [versionPlist.productBuildVersion].compactMap { $0 })
self.xcodeID = XcodeID(version: version, architectures: architectures)
}
}

View file

@ -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")
}
}
}

View file

@ -7,7 +7,6 @@
//
import Foundation
import struct XCModel.SDKs
import XcodesKit
import SwiftUI

View file

@ -1,11 +1,11 @@
import Version
import struct XCModel.Xcode
import XcodesKit
extension Version {
/// 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.
init?(xcReleasesXcode: XCModel.Xcode) {
init?(xcReleasesXcode: XcodeRelease) {
var versionString = xcReleasesXcode.version.number ?? ""
// Append trailing ".0" in order to get a fully-specified version string

View file

@ -1,14 +1,30 @@
import AppKit
import Foundation
import Version
import struct XCModel.SDKs
import struct XCModel.Compilers
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 {
let version: Version
var version: Version {
return id.version
}
/// Other Xcode versions that have the same build identifier
let identicalBuilds: [Version]
let identicalBuilds: [XcodeID]
var installState: XcodeInstallState
let selected: Bool
let icon: NSImage?
@ -18,10 +34,12 @@ struct Xcode: Identifiable, CustomStringConvertible {
let sdks: SDKs?
let compilers: Compilers?
let downloadFileSize: Int64?
let architectures: [Architecture]?
let id: XcodeID
init(
version: Version,
identicalBuilds: [Version] = [],
identicalBuilds: [XcodeID] = [],
installState: XcodeInstallState,
selected: Bool,
icon: NSImage?,
@ -30,9 +48,9 @@ struct Xcode: Identifiable, CustomStringConvertible {
releaseDate: Date? = nil,
sdks: SDKs? = nil,
compilers: Compilers? = nil,
downloadFileSize: Int64? = nil
downloadFileSize: Int64? = nil,
architectures: [Architecture]? = nil
) {
self.version = version
self.identicalBuilds = identicalBuilds
self.installState = installState
self.selected = selected
@ -43,10 +61,10 @@ struct Xcode: Identifiable, CustomStringConvertible {
self.sdks = sdks
self.compilers = compilers
self.downloadFileSize = downloadFileSize
self.architectures = architectures
self.id = XcodeID(version: version, architectures: architectures)
}
var id: Version { version }
var description: String {
version.appleDescription
}

View file

@ -35,12 +35,11 @@ struct XcodeCommands: Commands {
struct InstallButton: View {
@EnvironmentObject var appState: AppState
@State private var isLoading = false
let xcode: Xcode?
var body: some View {
ProgressButton(isInProgress: isLoading) {
Button {
install()
} label: {
Text("Install")
@ -49,7 +48,6 @@ struct InstallButton: View {
}
private func install() {
isLoading = true
guard let xcode = xcode else { return }
appState.checkMinVersionAndInstall(id: xcode.id)
}
@ -61,7 +59,7 @@ struct CancelInstallButton: View {
var body: some View {
Button(action: cancelInstall) {
Image(systemName: "xmark.circle.fill")
Label("Cancel", systemImage: "xmark")
}
.help(localizeString("StopInstallation"))
.buttonStyle(.plain)

View file

@ -31,6 +31,7 @@ public struct ObservingProgressIndicator: View {
self.progress = progress
cancellable = progress.publisher(for: \.fractionCompleted)
.combineLatest(progress.publisher(for: \.localizedAdditionalDescription))
.combineLatest(progress.publisher(for: \.isIndeterminate))
.throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in self?.objectWillChange.send() }
}
@ -82,6 +83,18 @@ struct ObservingProgressBar_Previews: PreviewProvider {
style: .bar,
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)
}

View file

@ -22,7 +22,10 @@ struct ProgressIndicator: NSViewRepresentable {
nsView.doubleValue = doubleValue
nsView.controlSize = controlSize
nsView.isIndeterminate = isIndeterminate
nsView.usesThreadedAnimation = true
nsView.style = style
nsView.startAnimation(nil)
}
}

View 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)
)
}
}

View file

@ -7,6 +7,7 @@ enum XcodesAlert: Identifiable {
case privilegedHelper
case generic(title: String, message: String)
case checkMinSupportedVersion(xcode: AvailableXcode, macOS: String)
case unauthenticated
var id: Int {
switch self {
@ -15,6 +16,7 @@ enum XcodesAlert: Identifiable {
case .generic: return 3
case .checkMinSupportedVersion: return 4
case .cancelRuntimeInstall: return 5
case .unauthenticated: return 6
}
}
}

View file

@ -4,6 +4,7 @@ import AppleAPI
enum XcodesSheet: Identifiable {
case signIn
case twoFactor(SecondFactorData)
case securityKeyTouchToConfirm
var id: Int { Kind(self).hashValue }
@ -16,12 +17,13 @@ enum XcodesSheet: Identifiable {
extension XcodesSheet {
private enum Kind: Hashable {
case signIn, twoFactor(TwoFactorOption)
case signIn, twoFactor(TwoFactorOption), securityKeyTouchToConfirm
enum TwoFactorOption {
case smsSent
case codeSent
case smsPendingChoice
case securityKeyPin
}
init(_ sheet: XcodesSheet) {
@ -32,7 +34,9 @@ extension XcodesSheet {
case .smsSent: self = .twoFactor(.smsSent)
case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
case .codeSent: self = .twoFactor(.codeSent)
case .securityKey: self = .twoFactor(.securityKeyPin)
}
case .securityKeyTouchToConfirm: self = .securityKeyTouchToConfirm
}
}
}

View file

@ -7,7 +7,7 @@
//
import SwiftUI
import struct XCModel.Compilers
import XcodesKit
struct CompilersView: View {
let compilers: Compilers?

View file

@ -3,12 +3,20 @@ import XcodesKit
import Path
import SwiftUI
import Version
import struct XCModel.Compilers
import struct XCModel.SDKs
struct InfoPane: View {
let xcode: Xcode
var body: some View {
if #available(macOS 14.0, *) {
mainContent
.contentMargins(10, for: .scrollContent)
} else {
mainContent
.padding()
}
}
private var mainContent: some View {
ScrollView(.vertical) {
HStack(alignment: .top) {
VStack {
@ -24,21 +32,15 @@ struct InfoPane: View {
InfoPaneControls(xcode: xcode)
}
.xcodesBackground()
VStack {
Text("Platforms")
.font(.title3)
.frame(maxWidth: .infinity, alignment: .leading)
PlatformsView(xcode: xcode)
}
.xcodesBackground()
PlatformsView(xcode: xcode)
}
.frame(minWidth: 380)
VStack(alignment: .leading) {
ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL)
CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion)
IdenticalBuildsView(builds: xcode.identicalBuilds)
IdenticalBuildsView(builds: xcode.identicalBuilds.map { $0.version })
SDKandCompilers
}
.frame(width: 200)
@ -65,15 +67,16 @@ struct InfoPane: View {
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
#Preview(XcodePreviewName.allCases[5].rawValue) { makePreviewContent(for: 5) }
private func makePreviewContent(for index: Int) -> some View {
let name = XcodePreviewName.allCases[index]
let name = XcodePreviewName.allCases[index]
return InfoPane(xcode: xcodeDict[name]!)
.environmentObject(configure(AppState()) {
$0.allXcodes = [xcodeDict[name]!]
$0.allXcodes = [xcodeDict[name]!]
})
.frame(width: 300, height: 400)
.padding()
.frame(width: 600, height: 400)
.padding()
}
enum XcodePreviewName: String, CaseIterable, Identifiable {
@ -82,7 +85,8 @@ enum XcodePreviewName: String, CaseIterable, Identifiable {
case Populated_Uninstalled
case Basic_Installed
case Basic_Installing
case Basic_Unarchiving
var id: XcodePreviewName { self }
}
@ -142,6 +146,14 @@ var xcodeDict: [XcodePreviewName: Xcode] = [
sdks: nil,
compilers: nil
),
.Basic_Unarchiving: .init(
version: _versionWithMeta,
installState: .installing(.unarchiving),
selected: false,
icon: nil,
sdks: nil,
compilers: nil
),
]
var downloadableRuntimes: [DownloadableRuntime] = {
@ -164,7 +176,7 @@ var downloadableRuntimes: [DownloadableRuntime] = {
$0.completedUnitCount = 848_444_920
$0.throughput = 9_211_681
}
)
)
)
runtimes[watchOSIndex] = runtime

View file

@ -25,6 +25,7 @@ struct InfoPaneControls: View {
case .installing(let installationStep):
HStack(alignment: .top) {
InstallationStepDetailView(installationStep: installationStep)
.frame(maxWidth: .infinity, alignment: .leading)
CancelInstallButton(xcode: xcode)
}
case .installed(_):
@ -39,6 +40,7 @@ struct InfoPaneControls: View {
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
#Preview(XcodePreviewName.allCases[5].rawValue) { makePreviewContent(for: 5) }
private func makePreviewContent(for index: Int) -> some View {
let name = XcodePreviewName.allCases[index]
@ -47,6 +49,6 @@ private func makePreviewContent(for index: Int) -> some View {
.environmentObject(configure(AppState()) {
$0.allXcodes = [xcodeDict[name]!]
})
.frame(width: 300)
.frame(width: 500)
.padding()
}

View file

@ -17,7 +17,7 @@ struct InstallationStepDetailView: View {
showsAdditionalDescription: true
)
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
ProgressView()
.scaleEffect(0.5)
}

View file

@ -8,7 +8,7 @@
import SwiftUI
import Version
import XCModel
import XcodesKit
import Path
struct InstalledStateButtons: View {

View file

@ -11,7 +11,7 @@ import Version
struct NotInstalledStateButtons: View {
let downloadFileSizeString: String?
let id: Version
let id: XcodeID
@EnvironmentObject var appState: AppState
@ -20,7 +20,11 @@ struct NotInstalledStateButtons: View {
Button {
appState.checkMinVersionAndInstall(id: id)
} 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 {
@ -38,7 +42,7 @@ struct NotInstalledStateButtons: View {
#Preview {
NotInstalledStateButtons(
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()
}

View file

@ -11,7 +11,8 @@ import XcodesKit
struct PlatformsView: View {
@EnvironmentObject var appState: AppState
@AppStorage("selectedRuntimeArchitecture") private var selectedVariant: ArchitectureVariant = .universal
let xcode: Xcode
var body: some View {
@ -19,17 +20,49 @@ struct PlatformsView: View {
let builds = xcode.sdks?.allBuilds()
let runtimes = builds?.flatMap { sdkBuild in
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
runtimeView(runtime: runtime)
.frame(minWidth: 200)
.padding()
.background(.quinary)
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
let architectures = Set((runtimes ?? []).flatMap { $0.architectures ?? [] })
VStack {
HStack {
Text("Platforms")
.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
@ -39,34 +72,38 @@ struct PlatformsView: View {
runtime.icon()
Text("\(runtime.visibleIdentifier)")
.font(.headline)
ForEach(runtime.architectures ?? [], id: \.self) { architecture in
TagView(text: architecture.displayString)
}
pathIfAvailable(xcode: xcode, runtime: runtime)
if runtime.installState == .notInstalled {
// 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 {
EmptyView()
} else {
HStack {
Spacer()
DownloadRuntimeButton(runtime: runtime)
}
}
}
Spacer()
Text(runtime.downloadFileSizeString)
.font(.subheadline)
.frame(width: 70, alignment: .trailing)
}
switch runtime.installState {
case .installed:
EmptyView()
case .notInstalled:
// TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly
if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) {
EmptyView()
} else {
HStack {
Spacer()
DownloadRuntimeButton(runtime: runtime)
}
}
case .installing(let installationStep):
HStack(alignment: .top, spacing: 5){
RuntimeInstallationStepDetailView(installationStep: installationStep)
.fixedSize(horizontal: false, vertical: true)
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)
}
}
}
}

View file

@ -26,8 +26,12 @@ struct RuntimeInstallationStepDetailView: View {
)
case .installing, .trashingArchive:
ProgressView()
.scaleEffect(0.5)
ObservingProgressIndicator(
Progress(),
controlSize: .regular,
style: .bar,
showsAdditionalDescription: false
)
}
}
}

View file

@ -7,7 +7,7 @@
//
import SwiftUI
import struct XCModel.SDKs
import XcodesKit
struct SDKsView: View {
let content: String

View file

@ -15,11 +15,12 @@ struct MainWindow: View {
// FB8979533 SceneStorage doesn't restore value after app is quit by user
@AppStorage("isShowingInfoPane") private var isShowingInfoPane = false
@AppStorage("xcodeListCategory") private var category: XcodeListCategory = .all
@AppStorage("xcodeListArchitecture") private var architecture: XcodeListArchitecture = .universal
@AppStorage("isInstalledOnly") private var isInstalledOnly = false
var body: some View {
NavigationSplitViewWrapper {
XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly)
XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly, architecture: architecture)
.layoutPriority(1)
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)),
@ -31,7 +32,8 @@ struct MainWindow: View {
.mainToolbar(
category: $category,
isInstalledOnly: $isInstalledOnly,
isShowingInfoPane: $isShowingInfoPane
isShowingInfoPane: $isShowingInfoPane,
architecture: $architecture
)
} detail: {
Group {
@ -41,7 +43,6 @@ struct MainWindow: View {
UnselectedView()
}
}
.padding()
.toolbar {
ToolbarItemGroup {
Button(action: { appState.presentedSheet = .signIn }, label: {
@ -77,6 +78,9 @@ struct MainWindow: View {
case .twoFactor(let secondFactorData):
secondFactorView(secondFactorData)
.environmentObject(appState)
case .securityKeyTouchToConfirm:
SignInSecurityKeyTouchView(isPresented: $appState.presentedSheet.isNotNil)
.environmentObject(appState)
}
}
.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)
case .smsPendingChoice:
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 }
)
)
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):
return Alert(
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(
Text("Install"),
action: {
self.appState.install(id: xcode.version)
self.appState.install(id: xcode.xcodeID)
}
),
secondaryButton: .cancel(Text("Cancel"))
@ -219,7 +239,7 @@ struct MainWindow_Previews: PreviewProvider {
MainWindow().environmentObject({ () -> AppState in
let a = AppState()
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.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),

View file

@ -36,6 +36,7 @@ struct AdvancedPreferencePane: View {
self.appState.installPath = path.string
}
}
.disabled(appState.disableInstallPathChange)
Text("InstallPathDescription")
.font(.footnote)
.foregroundStyle(.secondary)
@ -72,6 +73,7 @@ struct AdvancedPreferencePane: View {
self.appState.localPath = path.string
}
}
.disabled(appState.disableLocalPathChange)
Text("LocalCachePathDescription")
.font(.footnote)
.foregroundStyle(.secondary)
@ -93,7 +95,8 @@ struct AdvancedPreferencePane: View {
}
.labelsHidden()
.pickerStyle(.inline)
.disabled(appState.onSelectActionTypeDisabled)
Text(appState.onSelectActionType.detailedDescription)
.font(.footnote)
.foregroundStyle(.secondary)

View file

@ -19,7 +19,7 @@ struct DownloadPreferencePane: View {
}
.labelsHidden()
.fixedSize()
Text("DataSourceDescription")
.font(.footnote)
.foregroundStyle(.secondary)
@ -27,7 +27,8 @@ struct DownloadPreferencePane: View {
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
.disabled(dataSource.isManaged)
GroupBox(label: Text("Downloader")) {
VStack(alignment: .leading) {
Picker("Downloader", selection: $downloader) {
@ -38,7 +39,7 @@ struct DownloadPreferencePane: View {
}
.labelsHidden()
.fixedSize()
Text("DownloaderDescription")
.font(.footnote)
.foregroundStyle(.secondary)
@ -46,6 +47,7 @@ struct DownloadPreferencePane: View {
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
.disabled(downloader.isManaged)
}
}
}

View file

@ -13,6 +13,7 @@ struct ExperimentsPreferencePane: View {
"UseUnxipExperiment",
isOn: $appState.unxipExperiment
)
.disabled(appState.disableUnxipExperiment)
Text("FasterUnxipDescription")
.font(.footnote)
.foregroundStyle(.secondary)

View file

@ -20,6 +20,12 @@ struct GeneralPreferencePane: View {
NotificationsView().environmentObject(appState)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
Divider()
GroupBox(label: Text("Misc")) {
Toggle("TerminateAfterLastWindowClosed", isOn: $appState.terminateAfterLastWindowClosed)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
}
}

View file

@ -15,11 +15,13 @@ struct UpdatesPreferencePane: View {
"AutomaticInstallNewVersion",
isOn: $autoInstallationType.isAutoInstalling
)
.disabled(updater.disableAutoInstallNewVersions)
Toggle(
"IncludePreRelease",
isOn: $autoInstallationType.isAutoInstallingBeta
)
.disabled(updater.disableIncludePrereleaseVersions)
}
.fixedSize(horizontal: false, vertical: true)
}
@ -34,17 +36,20 @@ struct UpdatesPreferencePane: View {
isOn: $updater.automaticallyChecksForUpdates
)
.fixedSize(horizontal: true, vertical: false)
.disabled(updater.disableAutoUpdateXcodesApp)
Toggle(
"IncludePreRelease",
isOn: $updater.includePrereleaseVersions
)
.disabled(updater.disableAutoUpdateXcodesAppPrereleaseVersions)
Button("CheckNow") {
updater.checkForUpdates()
}
.padding(.top)
.disabled(updater.disableAutoUpdateXcodesApp)
Text(String(format: localizeString("LastChecked"), lastUpdatedString))
.font(.footnote)
.foregroundStyle(.secondary)
@ -83,12 +88,18 @@ class ObservableUpdater: ObservableObject {
private var lastUpdateCheckDateObservation: NSKeyValueObservation?
@Published var includePrereleaseVersions = false {
didSet {
UserDefaults.standard.setValue(includePrereleaseVersions, forKey: "includePrereleaseVersions")
Current.defaults.set(includePrereleaseVersions, forKey: "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() {
updater = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterDelegate, userDriverDelegate: nil).updater
@ -111,7 +122,7 @@ class ObservableUpdater: ObservableObject {
self.lastUpdateCheckDate = updater.lastUpdateCheckDate
}
)
includePrereleaseVersions = UserDefaults.standard.bool(forKey: "includePrereleaseVersions")
includePrereleaseVersions = Current.defaults.bool(forKey: "includePrereleaseVersions") ?? false
}
func checkForUpdates() {

View file

@ -10,12 +10,12 @@ struct SignIn2FAView: View {
var body: some View {
VStack(alignment: .leading) {
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode.length))
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode!.length))
.fixedSize(horizontal: true, vertical: false)
HStack {
Spacer()
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) {
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) {
appState.submitSecurityCode(.device(code: $0), sessionData: sessionData)
}
Spacer()
@ -32,7 +32,7 @@ struct SignIn2FAView: View {
Text("Continue")
}
.keyboardShortcut(.defaultAction)
.disabled(code.count != authOptions.securityCode.length)
.disabled(code.count != authOptions.securityCode!.length)
}
.frame(height: 25)
}

View file

@ -11,7 +11,7 @@ struct SignInPhoneListView: View {
var body: some View {
VStack(alignment: .leading) {
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) {
Text($0.numberWithDialCode)

View file

@ -11,11 +11,11 @@ struct SignInSMSView: View {
var body: some View {
VStack(alignment: .leading) {
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode.length, trustedPhoneNumber.numberWithDialCode))
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode!.length, trustedPhoneNumber.numberWithDialCode))
HStack {
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)
}
Spacer()
@ -31,7 +31,7 @@ struct SignInSMSView: View {
Text("Continue")
}
.keyboardShortcut(.defaultAction)
.disabled(code.count != authOptions.securityCode.length)
.disabled(code.count != authOptions.securityCode!.length)
}
.frame(height: 25)
}

View 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())
}

View 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())
}

View file

@ -11,6 +11,8 @@ import SwiftUI
struct BottomStatusModifier: ViewModifier {
@EnvironmentObject var appState: AppState
@AppStorage(PreferenceKey.hideSupportXcodes.rawValue) var hideSupportXcodes = false
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
func body(content: Content) -> some View {
@ -20,17 +22,19 @@ struct BottomStatusModifier: ViewModifier {
Divider()
HStack {
Text(appState.bottomStatusBarMessage)
.font(.subheadline)
.font(.subheadline)
Spacer()
Button(action: {
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
}) {
HStack {
Image(systemName: "heart.circle")
Text("Support.Xcodes")
if !hideSupportXcodes {
Button(action: {
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
}) {
HStack {
Image(systemName: "heart.circle")
Text("Support.Xcodes")
}
}
}
Text(Bundle.main.shortVersion!)
Text(verbatim: "\(Bundle.main.shortVersion!) (\(Bundle.main.version!))")
.font(.subheadline)
}
.frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
@ -51,8 +55,34 @@ extension View {
struct Previews_BottomStatusBar_Previews: PreviewProvider {
static var previews: some View {
HStack {
}.bottomStatusBar()
Group {
HStack {
}
.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
}())
}
}
}

View file

@ -18,7 +18,7 @@ struct InstallationStepRowView: View {
controlSize: .small,
style: .spinning
)
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
ProgressView()
.scaleEffect(0.5)
}

View file

@ -5,6 +5,7 @@ struct MainToolbarModifier: ViewModifier {
@Binding var category: XcodeListCategory
@Binding var isInstalledOnly: Bool
@Binding var isShowingInfoPane: Bool
@Binding var architectures: XcodeListArchitecture
func body(content: Content) -> some View {
content
@ -21,41 +22,44 @@ struct MainToolbarModifier: ViewModifier {
}
.keyboardShortcut(KeyEquivalent("r"))
.help("RefreshDescription")
Spacer()
Button(action: {
switch category {
case .all: category = .release
case .release: category = .beta
case .beta: category = .all
let isFiltering = isInstalledOnly || category != .all || architectures != .universal
Menu("Filter", systemImage: "line.horizontal.3.decrease.circle") {
Section {
Toggle("Installed Only", systemImage: "arrow.down.app", isOn: $isInstalledOnly) .labelStyle(.titleAndIcon)
}
}) {
switch category {
case .all:
Label("All", systemImage: "line.horizontal.3.decrease.circle")
case .release:
.help("FilterInstalledDescription")
Section {
Picker("Category", selection: $category) {
Label("All", systemImage: "line.horizontal.3.decrease.circle")
.tag(XcodeListCategory.all)
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(.trailingIcon)
.tag(XcodeListCategory.release)
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.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)
case .beta:
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(.trailingIcon)
.foregroundColor(.accentColor)
.tag(XcodeListArchitecture.appleSilicon)
}
.help("FilterArchitecturesDescription")
.disabled(architectures.isManaged)
}
.labelStyle(.titleAndIcon)
}
.help("FilterAvailableDescription")
Button(action: {
isInstalledOnly.toggle()
}) {
if isInstalledOnly {
Label("Filter", systemImage: "arrow.down.app.fill")
.foregroundColor(.accentColor)
} else {
Label("Filter", systemImage: "arrow.down.app")
}
}
.help("FilterInstalledDescription")
.pickerStyle(.inline)
.symbolVariant(isFiltering ? .fill : .none)
}
}
}
@ -64,13 +68,15 @@ extension View {
func mainToolbar(
category: Binding<XcodeListCategory>,
isInstalledOnly: Binding<Bool>,
isShowingInfoPane: Binding<Bool>
isShowingInfoPane: Binding<Bool>,
architecture: Binding<XcodeListArchitecture>
) -> some View {
modifier(
MainToolbarModifier(
category: category,
isInstalledOnly: isInstalledOnly,
isShowingInfoPane: isShowingInfoPane
isShowingInfoPane: isShowingInfoPane,
architectures: architecture
)
)
}

View file

@ -1,4 +1,5 @@
import Foundation
import XcodesKit
enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConvertible {
case all
@ -14,4 +15,22 @@ enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConverti
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() }
}

View file

@ -7,13 +7,16 @@ struct XcodeListView: View {
@Binding var selectedXcodeID: Xcode.ID?
private let searchText: String
private let category: XcodeListCategory
private let architecture: XcodeListArchitecture
private let isInstalledOnly: Bool
init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool) {
@AppStorage(PreferenceKey.allowedMajorVersions.rawValue) private var allowedMajorVersions = Int.max
init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool, architecture: XcodeListArchitecture) {
self._selectedXcodeID = selectedXcodeID
self.searchText = searchText
self.category = category
self.isInstalledOnly = isInstalledOnly
self.architecture = architecture
}
var visibleXcodes: [Xcode] {
@ -27,6 +30,27 @@ struct XcodeListView: View {
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 {
xcodes = xcodes.filter { $0.description.contains(searchText) }
}
@ -47,6 +71,7 @@ struct XcodeListView: View {
PlatformsPocket()
.padding(.horizontal)
.padding(.vertical, 8)
}
}
}
@ -59,34 +84,47 @@ struct PlatformsPocket: View {
openWindow(id: "platforms")
}
) {
HStack(spacing: 5) {
Image(systemName: "square.3.layers.3d")
.font(.title3.weight(.medium))
Text("PlatformsDescription")
Spacer()
if #available(macOS 26.0, *) {
platformsLabel
.glassEffect(in: .rect(cornerRadius: 8, style: .continuous))
} else {
platformsLabel
.background(.quaternary.opacity(0.75))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
.font(.body.weight(.medium))
.padding(.horizontal)
.padding(.vertical, 12)
.background(.quaternary.opacity(0.75))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
.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 {
static var previews: some View {
Group {
XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all, isInstalledOnly: false)
XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all, isInstalledOnly: false, architecture: .appleSilicon)
.environmentObject({ () -> AppState in
let a = AppState()
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.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.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
}())

View file

@ -21,9 +21,17 @@ struct XcodeListViewRow: View {
.font(.subheadline)
.foregroundColor(.secondary)
.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")
}
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 {
@ -156,7 +164,7 @@ struct XcodeListViewRow_Previews: PreviewProvider {
)
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,
appState: AppState()
)

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -1,4 +1,4 @@
{\rtf1\ansi\ansicpg1252\cocoartf2759
{\rtf1\ansi\ansicpg1252\cocoartf2865
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
{\colortbl;\red255\green255\blue255;}
{\*\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\
\
@ -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\
\
@ -552,12 +814,219 @@ For more information, please refer to &lt;<http://unlicense.org/>&gt;\
\
\
\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\
\
\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:\
\

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 KiB

View 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"
]
}
}

View file

@ -11,7 +11,7 @@ struct XcodesApp: App {
@StateObject private var updater = ObservableUpdater()
var body: some Scene {
WindowGroup("Xcodes") {
Window("Xcodes", id: "main") {
MainWindow()
.environmentObject(appState)
.environmentObject(updater)
@ -51,19 +51,19 @@ struct XcodesApp: App {
CommandGroup(replacing: CommandGroupPlacement.help) {
Button("Menu.GitHubRepo") {
let xcodesRepoURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!
let xcodesRepoURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/")!
openURL(xcodesRepoURL)
}
Divider()
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)
}
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)
}
}
@ -166,6 +166,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
func applicationDidFinishLaunching(_: Notification) {}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
}
}
func localizeString(_ key: String, comment: String = "") -> String {

View file

@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
name: "XcodesKit",
platforms: [.macOS(.v11)],
platforms: [.macOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(

View file

@ -37,8 +37,10 @@ public struct CoreSimulatorImage: Decodable, Identifiable, Equatable {
public struct CoreSimulatorRuntimeInfo: Decodable {
public let build: String
public let supportedArchitectures: [Architecture]?
public init(build: String) {
public init(build: String, supportedArchitectures: [Architecture]? = nil) {
self.build = build
self.supportedArchitectures = supportedArchitectures
}
}

View file

@ -11,7 +11,8 @@ public struct DownloadableRuntimesResponse: Codable {
public struct DownloadableRuntime: Codable, Identifiable, Hashable {
public let category: Category
public let simulatorVersion: SimulatorVersion
public let source: String
public let source: String?
public let architectures: [Architecture]?
public let dictionaryVersion: Int
public let contentType: ContentType
public let platform: Platform
@ -21,11 +22,14 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable {
public let hostRequirements: HostRequirements?
public let name: String
public let authentication: Authentication?
public var url: URL {
return URL(string: source)!
public var url: URL? {
if let source {
return URL(string: source)!
}
return nil
}
public var downloadPath: String {
url.path
public var downloadPath: String? {
url?.path
}
// dynamically updated - not decoded
@ -46,10 +50,11 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable {
case name
case authentication
case sdkBuildUpdate
case architectures
}
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 }
foundString.removeFirst()
return Int(foundString)!
@ -91,6 +96,7 @@ public struct SDKToSimulatorMapping: Codable {
public let sdkBuildUpdate: String
public let simulatorBuildUpdate: String
public let sdkIdentifier: String
public let downloadableIdentifiers: [String]?
}
extension DownloadableRuntime {
@ -117,6 +123,7 @@ extension DownloadableRuntime {
public enum ContentType: String, Codable {
case diskImage = "diskImage"
case package = "package"
case cryptexDiskImage = "cryptexDiskImage"
}
public enum Platform: String, Codable {
@ -163,6 +170,7 @@ public struct InstalledRuntime: Decodable {
let state: String
let version: String
let sizeBytes: Int?
let supportedArchitectures: [Architecture]?
}
extension InstalledRuntime {

View file

@ -9,6 +9,7 @@ import Foundation
// A numbered step
public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
case authenticating
case downloading(progress: Progress)
case unarchiving
case moving(destination: String)
@ -22,6 +23,8 @@ public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
public var message: String {
switch self {
case .authenticating:
return localizeString("Authenticating")
case .downloading:
return localizeString("Downloading")
case .unarchiving:
@ -39,16 +42,17 @@ public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
public var stepNumber: Int {
switch self {
case .downloading: return 1
case .unarchiving: return 2
case .moving: return 3
case .trashingArchive: return 4
case .checkingSecurity: return 5
case .finishing: return 6
case .authenticating: return 1
case .downloading: return 2
case .unarchiving: return 3
case .moving: return 4
case .trashingArchive: return 5
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 {

View file

@ -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])
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -22,9 +22,14 @@ public struct RuntimeService {
// Apple gives a plist for download
let (data, _) = try await networkService.requestData(urlRequest, validators: [])
let decodedResponse = try PropertyListDecoder().decode(DownloadableRuntimesResponse.self, from: data)
return decodedResponse
do {
let decodedResponse = try PropertyListDecoder().decode(DownloadableRuntimesResponse.self, from: data)
return decodedResponse
} catch {
print("error: \(error)")
throw error
}
}
public func installedRuntimes() async throws -> [InstalledRuntime] {

View file

@ -6,10 +6,14 @@ public typealias ProcessOutput = (status: Int32, out: String, err: String)
extension Process {
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()
process.currentDirectoryURL = workingDirectory ?? executable.deletingLastPathComponent()

View file

@ -26,4 +26,8 @@ public struct XcodesShell {
public var deleteRuntime: (String) async throws -> ProcessOutput = {
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)
}
}

View file

@ -1,7 +1,11 @@
import Path
import CryptoKit
import Version
@testable import Xcodes
import XCTest
import CommonCrypto
import BigNum
import SRP
class AppStateUpdateTests: XCTestCase {
var subject: AppState!
@ -145,7 +149,7 @@ class AppStateUpdateTests: XCTestCase {
)
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() {
@ -230,7 +234,7 @@ class AppStateUpdateTests: XCTestCase {
)
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() {
@ -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(\.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=")
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 137 KiB

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 193 KiB