Showing preview only (1,474K chars total). Download the full file or copy to clipboard to get everything.
Repository: XcodesOrg/XcodesApp
Branch: main
Commit: 1a0d3353b9a1
Files: 213
Total size: 1.4 MB
Directory structure:
gitextract_lwe5ux5j/
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── dependabot.yml
│ ├── release-drafter.yml
│ └── workflows/
│ ├── appcast.yml
│ ├── ci.yml
│ ├── release-drafter.yml
│ └── xcstrings.yml
├── .gitignore
├── AppCast/
│ ├── .gitignore
│ ├── Gemfile
│ ├── _config.yml
│ ├── _includes/
│ │ └── appcast.inc
│ ├── _plugins/
│ │ └── signature_filter.rb
│ ├── appcast.xml
│ └── appcast_pre.xml
├── CONTRIBUTING.md
├── DECISIONS.md
├── HelperXPCShared/
│ └── HelperXPCShared.swift
├── LICENSE
├── README.md
├── Scripts/
│ ├── export_options.plist
│ ├── fix_libfido2_framework.sh
│ ├── increment_build_number.sh
│ ├── notarize.sh
│ ├── package_release.sh
│ ├── sign_update
│ └── uninstall_privileged_helper.sh
├── Xcodes/
│ ├── AcknowledgementsGenerator/
│ │ ├── .gitignore
│ │ ├── .swiftpm/
│ │ │ └── xcode/
│ │ │ └── package.xcworkspace/
│ │ │ └── contents.xcworkspacedata
│ │ ├── Package.swift
│ │ ├── README.md
│ │ ├── Sources/
│ │ │ └── AcknowledgementsGenerator/
│ │ │ ├── Extensions/
│ │ │ │ ├── CollectionExtensions.swift
│ │ │ │ └── StringExtensions.swift
│ │ │ ├── Tools/
│ │ │ │ └── Xcode.swift
│ │ │ └── main.swift
│ │ └── spm-licenses.LICENSE
│ ├── AppleAPI/
│ │ ├── .gitignore
│ │ ├── Package.swift
│ │ ├── README.md
│ │ ├── Sources/
│ │ │ └── AppleAPI/
│ │ │ ├── Client.swift
│ │ │ ├── Environment.swift
│ │ │ ├── Hashcash.swift
│ │ │ └── URLRequest+Apple.swift
│ │ └── Tests/
│ │ ├── AppleAPITests/
│ │ │ ├── AppleAPITests.swift
│ │ │ └── XCTestManifests.swift
│ │ └── LinuxMain.swift
│ ├── Backend/
│ │ ├── AppState+Install.swift
│ │ ├── AppState+Runtimes.swift
│ │ ├── AppState+Update.swift
│ │ ├── AppState.swift
│ │ ├── Aria2CError.swift
│ │ ├── AvailableXcode.swift
│ │ ├── Bundle+InfoPlistValues.swift
│ │ ├── Collection+.swift
│ │ ├── Configure.swift
│ │ ├── DataSource.swift
│ │ ├── DateFormatter+.swift
│ │ ├── Downloader.swift
│ │ ├── Downloads.swift
│ │ ├── Entry+.swift
│ │ ├── Environment.swift
│ │ ├── FileError.swift
│ │ ├── FileManager+.swift
│ │ ├── FocusedValues.swift
│ │ ├── Foundation.swift
│ │ ├── Hardware.swift
│ │ ├── HelperClient.swift
│ │ ├── HelperInstallState.swift
│ │ ├── InstalledXcode.swift
│ │ ├── IsTesting.swift
│ │ ├── NotificationManager.swift
│ │ ├── Optional+IsNotNil.swift
│ │ ├── Path+.swift
│ │ ├── Process.swift
│ │ ├── Progress+.swift
│ │ ├── Publisher+Resumable.swift
│ │ ├── SDKs+Xcode.swift
│ │ ├── SelectedActionType.swift
│ │ ├── SelectedXcode.swift
│ │ ├── URLRequest+Apple.swift
│ │ ├── URLSession+DownloadTaskPublisher.swift
│ │ ├── Version+.swift
│ │ ├── Version+Xcode.swift
│ │ ├── Version+XcodeReleases.swift
│ │ ├── Xcode.swift
│ │ ├── XcodeCommands.swift
│ │ └── XcodeInstallState.swift
│ ├── Frontend/
│ │ ├── About/
│ │ │ ├── AboutView.swift
│ │ │ ├── AcknowledgementsView.swift
│ │ │ └── ScrollingTextView.swift
│ │ ├── Common/
│ │ │ ├── NavigationSplitViewWrapper.swift
│ │ │ ├── ObservingProgressIndicator.swift
│ │ │ ├── ProgressButton.swift
│ │ │ ├── ProgressIndicator.swift
│ │ │ ├── TagView.swift
│ │ │ ├── TrailingIconLabelStyle.swift
│ │ │ ├── XcodesAlert.swift
│ │ │ └── XcodesSheet.swift
│ │ ├── InfoPane/
│ │ │ ├── CompatibilityView.swift
│ │ │ ├── CompilersView.swift
│ │ │ ├── CornerRadiusModifier.swift
│ │ │ ├── IconView.swift
│ │ │ ├── IdenticalBuildView.swift
│ │ │ ├── InfoPane.swift
│ │ │ ├── InfoPaneControls.swift
│ │ │ ├── InstallationStepDetailView.swift
│ │ │ ├── InstalledStateButtons.swift
│ │ │ ├── NotInstalledStateButtons.swift
│ │ │ ├── PlatformsView.swift
│ │ │ ├── ReleaseDateView.swift
│ │ │ ├── ReleaseNotesView.swift
│ │ │ ├── RuntimeInstallationStepDetailView.swift
│ │ │ ├── SDKsView.swift
│ │ │ └── UnselectedView.swift
│ │ ├── MainWindow.swift
│ │ ├── Preferences/
│ │ │ ├── AdvancedPreferencePane.swift
│ │ │ ├── DownloadPreferencePane.swift
│ │ │ ├── ExperiementsPreferencePane.swift
│ │ │ ├── GeneralPreferencePane.swift
│ │ │ ├── NotificationsView.swift
│ │ │ ├── PlatformsListView.swift
│ │ │ ├── PreferencesView.swift
│ │ │ └── UpdatesPreferencePane.swift
│ │ ├── SignIn/
│ │ │ ├── AttributedText.swift
│ │ │ ├── NSAttributedString+.swift
│ │ │ ├── PinCodeTextView.swift
│ │ │ ├── SignIn2FAView.swift
│ │ │ ├── SignInCredentialsView.swift
│ │ │ ├── SignInPhoneListView.swift
│ │ │ ├── SignInSMSView.swift
│ │ │ ├── SignInSecurityKeyPinView.swift
│ │ │ ├── SignInSecurityKeyTouchView.swift
│ │ │ └── SignedInView.swift
│ │ ├── View+Conditional.swift
│ │ ├── View+IsHidden.swift
│ │ └── XcodeList/
│ │ ├── AppStoreButtonStyle.swift
│ │ ├── BottomStatusBar.swift
│ │ ├── InstallationStepRowView.swift
│ │ ├── MainToolbar.swift
│ │ ├── Tag.swift
│ │ ├── XcodeListCategory.swift
│ │ ├── XcodeListView.swift
│ │ └── XcodeListViewRow.swift
│ ├── Preview Content/
│ │ └── Preview Assets.xcassets/
│ │ └── Contents.json
│ ├── Resources/
│ │ ├── Assets.xcassets/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── Icons/
│ │ │ │ └── Contents.json
│ │ │ ├── install.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── xcode-beta.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── xcode.imageset/
│ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ ├── Licenses.rtf
│ │ ├── Localizable.xcstrings
│ │ ├── Xcodes.entitlements
│ │ ├── XcodesIcon.icon/
│ │ │ └── icon.json
│ │ ├── XcodesTest.entitlements
│ │ ├── aria2c
│ │ ├── aria2c.LICENSE
│ │ └── unxip
│ ├── XcodesApp.swift
│ └── XcodesKit/
│ ├── .gitignore
│ ├── Package.swift
│ ├── README.md
│ ├── Sources/
│ │ └── XcodesKit/
│ │ ├── Extensions/
│ │ │ ├── Foundation.swift
│ │ │ └── Logger.swift
│ │ ├── Models/
│ │ │ ├── Runtimes/
│ │ │ │ ├── CoreSimulatorImage.swift
│ │ │ │ ├── RuntimeInstallState.swift
│ │ │ │ ├── RuntimeInstallationStep.swift
│ │ │ │ └── Runtimes.swift
│ │ │ ├── XcodeInstallState.swift
│ │ │ ├── XcodeInstallationStep.swift
│ │ │ └── XcodeReleases/
│ │ │ ├── Architecture.swift
│ │ │ ├── Checksums.swift
│ │ │ ├── Compilers.swift
│ │ │ ├── Link.swift
│ │ │ ├── Release.swift
│ │ │ ├── SDKs.swift
│ │ │ ├── XcodeRelease.swift
│ │ │ ├── XcodeVersion.swift
│ │ │ └── YMD.swift
│ │ ├── Services/
│ │ │ └── RuntimeService.swift
│ │ ├── Shell/
│ │ │ ├── Process.swift
│ │ │ └── XcodesShell.swift
│ │ └── XcodesKitEnvironment.swift
│ └── Tests/
│ └── XcodesKitTests/
│ └── XcodesKitTests.swift
├── Xcodes.xcodeproj/
│ ├── ._project.xcworkspace
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm/
│ │ └── Package.resolved
│ └── xcshareddata/
│ └── xcschemes/
│ ├── Xcodes.xcscheme
│ └── com.robotsandpencils.XcodesApp.Helper.xcscheme
├── XcodesTests/
│ ├── AppStateTests.swift
│ ├── AppStateUpdateTests.swift
│ ├── Bundle+XcodesTests.swift
│ ├── Environment+Mock.swift
│ ├── Fixtures/
│ │ ├── Stub-0.0.0.Info.plist
│ │ └── Stub-version.plist
│ └── Info.plist
├── com.xcodesorg.xcodesapp.Helper/
│ ├── AuditTokenHack.h
│ ├── AuditTokenHack.m
│ ├── ConnectionVerifier.swift
│ ├── Info.plist
│ ├── Logger.swift
│ ├── SimpleXPCApp.LICENSE
│ ├── XPCDelegate.swift
│ ├── com.xcodesorg.xcodesapp.Helper-Bridging-Header.h
│ ├── com.xcodesorg.xcodesapp.HelperTest.entitlements
│ ├── launchd.plist
│ └── main.swift
└── nextstep.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODEOWNERS
================================================
* @RobotsAndPencils/xcodes
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Version**
- OS:
- Xcodes:
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Tell us how we can improve Xcodes**
**Is your feature request related to a problem? Please describe.**
**What would you like to see? How would you like it to work?**
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
================================================
FILE: .github/release-drafter.yml
================================================
categories:
- title: '🚀 Enhancements'
labels:
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'bugfix'
- title: '🌎 Localization'
labels:
- 'localization'
- title: '🧰 Maintenance'
label:
- 'chore'
- 'documentation'
- 'dependencies'
template: |
Install Xcodes using one of the methods listed [here](https://github.com/RobotsAndPencils/XcodesApp#installation).
Update Xcodes by selecting Check for Updates... in the Xcodes menu in the menu bar.
## Changes
$CHANGES
================================================
FILE: .github/workflows/appcast.yml
================================================
name: Build and publish a new appcast file
on:
workflow_dispatch:
release:
jobs:
jekyll:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@v4
with:
# 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.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.197.0
with:
ruby-version: '3.0'
- name: Bundler 💎
working-directory: AppCast
env:
BUNDLE_PATH: vendor/bundle
run: |
gem install bundler
bundle install
- name: Build 🛠
working-directory: AppCast
env:
BUNDLE_PATH: vendor/bundle
JEKYLL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bundle exec jekyll build
- name: Publish 🚀
uses: JamesIves/github-pages-deploy-action@releases/v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages
folder: AppCast/_site
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Run tests
env:
DEVELOPER_DIR: /Applications/Xcode_16.4.app
run: xcodebuild test -scheme Xcodes
================================================
FILE: .github/workflows/release-drafter.yml
================================================
name: Release Drafter
on:
# Allow running it manually in case we forget to label a PR before merging
workflow_dispatch:
push:
branches:
- main
jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/xcstrings.yml
================================================
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
================================================
FILE: .gitignore
================================================
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
Archive/*
Product/*
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Jetbrains IDEA
.idea
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# Accio dependency management
Dependencies/
.accio/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
.DS_Store
================================================
FILE: AppCast/.gitignore
================================================
_site
.sass-cache
.jekyll-metadata
================================================
FILE: AppCast/Gemfile
================================================
source "https://rubygems.org"
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
# bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> 4.4.1"
gem "jekyll-github-metadata", group: :jekyll_plugins
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
gem "tzinfo", "~> 1.2"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform?
# kramdown v2 ships without the gfm parser by default. If you're using
# kramdown v1, comment out this line.
gem "kramdown-parser-gfm"
================================================
FILE: AppCast/_config.yml
================================================
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely edit after that. If you find
# yourself editing this file very often, consider using Jekyll's data files
# feature for the data you need to update frequently.
#
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
# Site settings
# These are used to personalize your new site. If you look in the HTML files,
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
# You can create any custom variable you would like, and they will be accessible
# in the templates via {{ site.myvariable }}.
title: Xcodes.app
description: >- # this means to ignore newlines until "baseurl:"
baseurl: "" # the subpath of your site, e.g. /blog
url: "" # the base hostname & protocol for your site, e.g. http://example.com
# Build settings
markdown: kramdown
plugins:
- "jekyll-github-metadata"
# Exclude from processing.
# The following items will not be processed, by default. Create a custom list
# to override the default setting.
# exclude:
# - Gemfile
# - Gemfile.lock
# - node_modules
# - vendor/bundle/
# - vendor/cache/
# - vendor/gems/
# - vendor/ruby/
================================================
FILE: AppCast/_includes/appcast.inc
================================================
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>{{ site.github.project_title }}</title>
<description>Most recent changes with links to updates.</description>
<language>en</language>
{% for release in site.github.releases %}
{% unless release.draft %}
{% unless release.prerelease and page.release_only %}
<item>
<title>{{ release.name }}</title>
<description><![CDATA[{{ release.body | markdownify }}]]></description>
<pubDate>{{ release.published_at | date_to_rfc822 }}</pubDate>
{% for asset in release.assets limit:1 %}
{% assign signature = release.body | sparkle_signature %}
{% assign build_nums = release.tag_name | replace_first:'v','' | replace_first:'b',',' | split:',' %}
{% if build_nums.size == 2 %}
{% assign version_number = build_nums[0] %}
{% assign build_number = build_nums[1] %}
<enclosure
url="{{ asset.browser_download_url }}"
sparkle:version="{{ build_number }}"
sparkle:shortVersionString="{{ version_number }}"
sparkle:edSignature="{{ signature }}"
length="{{ asset.size }}"
type="application/octet-stream" />
{% else %}
{% assign version = release.tag_name | remove_first:'v' %}
<enclosure
url="{{ asset.browser_download_url }}"
sparkle:version="{{ version }}"
sparkle:edSignature="{{ signature }}"
length="{{ asset.size }}"
type="application/octet-stream" />
{% endif %}
{% endfor %}
</item>
{% endunless %}
{% endunless %}
{% endfor %}
</channel>
</rss>
================================================
FILE: AppCast/_plugins/signature_filter.rb
================================================
module Jekyll
module SignatureFilter
def sparkle_signature(release_body)
regex = /<!-- sparkle:edSignature=(?<signature>.*) -->/m
signature = release_body.match(regex).named_captures["signature"]
raise "Didn't find a signature in the release body." if signature.empty?
signature
end
end
end
Liquid::Template.register_filter(Jekyll::SignatureFilter)
================================================
FILE: AppCast/appcast.xml
================================================
---
release_only: true
---
{%include appcast.inc %}
================================================
FILE: AppCast/appcast_pre.xml
================================================
---
release_only: false
---
{%include appcast.inc %}
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Xcodes
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- 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.
## 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:
1. Fork the repo and create your branch from `main`.
2. If you've added code that should be tested, add tests.
3. If you've added new functionality, add documentation
4. Ensure the test suite passes.
5. Make sure your code lints.
6. Issue that pull request!
## 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 [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
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
People *love* thorough bug reports.
## License
By contributing, you agree that your contributions will be licensed under its MIT License.
================================================
FILE: DECISIONS.md
================================================
# Decisions
This file exists to provide a historical record of the motivation for important technical decisions in the project. It's inspired by Architectural Decision Records, but the implementation is intentionally simpler than usual. When a new decision is made, append it to the end of the file with a header. Decisions can be changed later. This is a reflection of real life, not a contract that has to be followed.
## Why Make Xcodes.app?
[xcodes](https://github.com/RobotsAndPencils/xcodes) has been well-received within and outside of Robots and Pencils as an easy way to manage Xcode versions. A command line tool can have a familiar interface for developers, and is also easier to automate than most GUI apps.
Not everyone wants to use a command line tool though, and there's an opportunity to create an even better developer experience with an app. This is also an opportunity for contributors to get more familiar with SwiftUI and Combine on macOS.
## Code Organization
To begin, we will intentionally not attempt to share code between xcodes and Xcodes.app. In the future, once we have a better idea of the two tools' functionality, we can revisit this decision. An example of code that could be shared are the two AppleAPI libraries which will likely be very similar.
While the intent of xcodes' XcodesKit library was to potentially reuse it in a GUI context, it still makes a lot of assumptions about how the UI works that would prevent that happening immediately. As we reuse that code (by copying and pasting) and tweak it to work in Xcodes.app, we may end up with something that can work in both contexts.
## Asynchrony
Xcodes.app uses Combine to model asynchronous work. This is different than xcodes, which uses PromiseKit because it began prior to Combine's existence. This means that there is a migration of the existing code that has to happen, but the result is easier to use with a SwiftUI app.
## Dependency Injection
xcodes used Point Free's Environment type, and I'm happy with how that turned out. It looks a lot simpler to implement and grow with a codebase, but still allows setting up test double for tests.
- https://www.pointfree.co/episodes/ep16-dependency-injection-made-easy
- https://www.pointfree.co/episodes/ep18-dependency-injection-made-comfortable
- https://vimeo.com/291588126
## State Management
While I'm curious and eager to try Point Free's [Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture), I'm going to avoid it at first in favour of a simpler AppState ObservableObject. My motivation for this is to try to have something more familiar to a contributor that was also new to SwiftUI, so that the codebase doesn't have too many new or unfamiliar things. If we run into performance or correctness issues in the future I think TCA should be a candidate to reconsider.
## Privilege Escalation
Unlike [xcodes](https://github.com/RobotsAndPencils/xcodes/blob/master/DECISIONS.md#privilege-escalation), there is a better option than running sudo in a Process when we need to escalate privileges in Xcodes.app, namely a privileged helper.
A separate, bundle executable is installed as a privileged helper using SMJobBless and communicates with the main app (the client) over XPC. This helper performs the post-install and xcode-select tasks that would require sudo from the command line. The helper and main app validate each other's bundle ID, version and code signing certificate chain. Validation of the connection is done using the private audit token API. An alternative is to validate the code signature of the client based on the PID from a first "handshake" message. DTS [seems to say](https://developer.apple.com/forums/thread/72881#420409022) that this would also be safe against an attacker PID-wrapping. Because the SMJobBless + XPC examples I found online all use the audit token instead, I decided to go with that. The tradeoff is that this is private API.
Uninstallation is not provided yet. I had this partially implemented (one attempt was based on [DoNotDisturb's approach](https://github.com/objective-see/DoNotDisturb/blob/237b19800fa356f830d1c02715a9a75be08b8924/configure/Helper/HelperInterface.m#L123)) but an issue that I kept hitting was that despite the helper not being installed or running I was able to get a remote object proxy over the connection. Adding a timeout to getVersion might be sufficient as a workaround, as it should return the string immediately.
- [Apple Developer: Creating XPC Services](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html)
- [Objective Development: The Story Behind CVE-2019-13013](https://blog.obdev.at/what-we-have-learned-from-a-vulnerability/)
- [Apple Developer Forums: How to and When to uninstall a privileged helper](https://developer.apple.com/forums/thread/66821)
- [Apple Developer Forums: XPC restricted to processes with the same code signing?](https://developer.apple.com/forums/thread/72881#419817)
- [Wojciech Reguła: Learn XPC exploitation - Part 1: Broken cryptography](https://wojciechregula.blog/post/learn-xpc-exploitation-part-1-broken-cryptography/)
- [Wojciech Reguła: Learn XPC exploitation - Part 2: Say no to the PID!](https://wojciechregula.blog/post/learn-xpc-exploitation-part-2-say-no-to-the-pid/)
- [Wojciech Reguła: Learn XPC exploitation - Part 3: Code injections](https://wojciechregula.blog/post/learn-xpc-exploitation-part-3-code-injections/)
- [Apple Developer: EvenBetterAuthorizationSample](https://developer.apple.com/library/archive/samplecode/EvenBetterAuthorizationSample/Introduction/Intro.html)
- [erikberglund/SwiftPrivilegedHelper](https://github.com/erikberglund/SwiftPrivilegedHelper)
- [aronskaya/smjobbless](https://github.com/aronskaya/smjobbless)
- [securing/SimpleXPCApp](https://github.com/securing/SimpleXPCApp)
## Selecting the active version of Xcode
This isn't a technical decision, but we spent enough time talking about this that it's probably worth sharing. When a user has more than one version of Xcode installed, a specific version of the developer tools can be selected with the `xcode-select` tool. The selected version of tools like xcodebuild or xcrun will be used unless the DEVELOPER_DIR environment variable has been set to a different path. You can read more about this in the `xcode-select` man pages. Notably, the man pages and [some notarization documentation](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution) use the term "active" to indicate the Xcode version that's been selected. [This](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_DO_I_SELECT_THE_DEFAULT_VERSION_OF_XCODE_TO_USE_FOR_MY_COMMAND_LINE_TOOLS_) older tech note uses the term "default". And of course, the `xcode-select` tool has the term "select" in its name. xcodes used the terms "select" and "selected" for this functionality, intending to match the xcode-select tool.
Here are the descriptions of these terms from [Apple's Style Guide](https://books.apple.com/ca/book/apple-style-guide/id1161855204):
> active: Use to refer to the app or window currently being used. Preferred to in front.
> default: OK to use to describe the state of settings before the user changes them. See also preset.
> preset: Use to refer to a group of customized settings an app provides or the user saves for reuse.
> select: Use select, not choose, to refer to the action users perform when they select among multiple objects.
Xcodes.app has this same functionality as xcodes, which still uses `xcode-select` under the hood, but because the main UI is a list of selectable rows, there _may_ be some ambiguity about the meaning of "selected". "Default" has a less clear connection to `xcode-select`'s name, but does accurately describe the behaviour that results. In Xcode 11 Launch Services also uses the selected Xcode version when opening a (GUI) developer tool bundled with Xcode, like Instruments. We could also try to follow Apple's lead by using the term "active" from the `xcode-select` man pages and notarization documentation. According to the style guide "active" already has a clear meaning in a GUI context.
Ultimately, we've decided to align with Apple's usage of "active" and "make active" in this specific context, despite possible confusion with the definition in the style guide.
## Software Updates
We're familiar with using GitHub releases to distribute pre-built, code signed and notarized versions of `xcodes` via direct download and Homebrew. Ideally we could use GitHub releases here too with an update mechanism more suitable for an app bundle. For distribution outside the Mac App Store, the most popular choice for updates is [Sparkle](https://sparkle-project.org). The v2 branch has been in beta for a long time, but since Xcodes.app isn't (currently) sandboxed, we can use the production-ready v1 releases.
Based on [this blog post](https://yiqiu.me/2015/11/19/sparkle-update-on-github/), we can use GitHub Pages to generate the appcast for Sparkle to point at releases in our repo. We've made a few changes, like putting the source for the Jekyll site on the main branch, and including the EdDSA signature in the appcast. Generating the appcast file manually would be more straightforward, but we can always edit the files on the gh_pages branch manually if we need to, and it's one less step for a release manager to perform when they're already creating the release in the repo.
We're deliberately not capturing system profile data with Sparkle right now, because we don't want it and because it would require additional infrastructure.
We also considered https://github.com/mxcl/AppUpdater, but decided against it because it seemed less battle-tested than Sparkle and currently lacks an open source license.
================================================
FILE: HelperXPCShared/HelperXPCShared.swift
================================================
import Foundation
let machServiceName = "com.xcodesorg.xcodesapp.Helper"
let clientBundleID = "com.xcodesorg.xcodesapp"
let subjectOrganizationalUnit = Bundle.main.infoDictionary!["CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT"] as! String
@objc(HelperXPCProtocol)
protocol HelperXPCProtocol {
func getVersion(completion: @escaping (String) -> Void)
func xcodeSelect(absolutePath: String, completion: @escaping (Error?) -> Void)
func devToolsSecurityEnable(completion: @escaping (Error?) -> Void)
func addStaffToDevelopersGroup(completion: @escaping (Error?) -> Void)
func acceptXcodeLicense(absoluteXcodePath: String, completion: @escaping (Error?) -> Void)
func runFirstLaunch(absoluteXcodePath: String, completion: @escaping (Error?) -> Void)
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019-2021 Robots and Pencils
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.
================================================
FILE: README.md
================================================
<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.
_If you're looking for a command-line version of Xcodes.app, try [`xcodes`](https://github.com/XcodesOrg/xcodes)._



### :tada: Announcement
XcodesApp is now part of the `XcodesOrg` - [read more here](nextstep.md)
## Features
- List all available Xcode versions from [Xcode Releases'](https://xcodereleases.com) data or the Apple Developer website.
- Install any Xcode version, **fully automated** from start to finish. Xcodes uses [`aria2`](https://aria2.github.io), which uses up to 16 connections to download 3-5x faster than URLSession.
- Automatically resumes installs if network errors.
- Apple ID required to download Xcode versions.
- 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.


## Localization
Xcodes supports localization in several languages.
The following languages are supported because of the following community users!
|||||
|-|-|-|-|
|French 🇫🇷 |[@dompepin](https://github.com/dompepin)|Italian 🇮🇹 |[gualtierofrigerio](https://github.com/gualtierofrigerio)|
|Spanish 🇪🇸🇲 |[@cesartru88](https://github.com/cesartru88)|Korean 🇰🇷 |[@ryan-son](https://github.com/ryan-son)|
|Russian 🇷🇺 |[@alexmazlov](https://github.com/alexmazlov)|Turkish 🇹🇷 |[@egesucu](https://github.com/egesucu)|
|Hindi 🇮🇳 |[@KGurpreet](https://github.com/KGurpreet)|Chinese-Simplified 🇨🇳|[@megabitsenmzq](https://github.com/megabitsenmzq)|
|Finnish 🇫🇮 |[@marcusziade](https://github.com/marcusziade)|Chinese-Traditional 🇹🇼|[@itszero](https://github.com/itszero)|
|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)|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
v3.X - requires macOS 13 - architecture variants and updated icon.
### Install with Homebrew
Developer ID-signed and notarized release builds are available on Homebrew. These don't require Xcode to already be installed in order to use.
```sh
brew install --cask xcodes
```
### Manually install
1. Download the latest version [here](https://github.com/XcodesOrg/XcodesApp/releases/latest) using the **Xcodes.zip** asset. These are Developer ID-signed and notarized release builds and don't require Xcode to already be installed in order to use.
2. Move the unzipped `Xcodes.app` to your `/Applications` directory
## Support
Xcodes.app and CLI is updated, maintained with contributors like yourself. Even open source libraries and tools come with expenses. If you would like to support Xcodes or donate to the development and maintenance of the tool, it would be greatly appreciated. There is absolutely no obligation!
<a href="https://opencollective.com/xcodesapp" target="_blank">
<img src="https://opencollective.com/xcodesapp/donate/button@2x.png?color=blue" class="buymeacoffee" width=200 />
</a>
## Development
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
```
# compile for Intel
swiftc -parse-as-library -O -target x86_64-apple-macos11 unxip.swift
# compile for M1
swiftc -parse-as-library -O -target arm64-apple-macos11 unxip.swift
# combine for universal binary
lipo -create -output unxip unxip_intel unxip_m1
# check it
lipo -archs unxip
```
[`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.
<details>
<summary>Releasing a new version</summary>
Follow the steps below to build and release a new version of Xcodes.app. For any of the git steps, you can use your preferred tool, but please sign the tag.
```sh
# Update the version number in Xcode and commit the change, if necessary
# Question: Did anything in XPCHelper change?
# - com.xcodesorg.xcodesapp.Helper folder and HelperXPCShared
# - if so, bump the version number in com.xcodesorg.xcodesapp.Helper target.
# Note: you do not have to bump the version number if nothing has changed.
# Note2: If you do bump the version, the end user, must re-install the XPCHelper and give permission again.
# Increment the build number
scripts/increment_build_number.sh
# Commit the change
git add Xcodes/Resources/Info.plist
git commit -asm "Increment build number"
# Tag the latest commit
# Replace $VERSION and $BUILD below with the latest real values
git tag -asm "v$VERSIONb$BUILD" "v$VERSIONb$BUILD"
# Push to origin
git push --follow-tags
# Build the app
# Make sure you have the Xcode Selected you want to build with
scripts/package_release.sh
# Notarize the app
# Do this from the Product directory so the app is zipped without being nested inside Product
# Create a app specific password on appleid.apple.com if you haven't already
# xcrun notarytool store-credentials "AC_PASSWORD" \
# --apple-id "test@example.com" \
# --team-id "teamid" \
# --password "app specific password"
pushd Product
../scripts/notarize.sh Xcodes.zip <MYORG>
# Sign the .zip for Sparkle, note the signature in the output for later
# If you're warned about the signing key not being found, see the Xcodes 1Password vault for the key and installation instructions.
../scripts/sign_update Xcodes.zip
popd
# Go to https://github.com/XcodesOrg/XcodesApp/releases
# If there are uncategorized PRs, add the appropriate label and run the Release Drafter action manually
# Edit the latest draft release
# Set its tag to the tag you just pushed
# Set its title to a string with the format "$VERSION ($BUILD)"
# Polish the draft release notes, if necessary
# Add the signature to the bottom of the release notes in a comment, like:
<!-- sparkle:edSignature=$SIGNATURE -->
# Attach the zip that was created in the Product directory to the release
# Publish the release
shasum -a 256 xcodes.zip
# Update the [Homebrew Cask](https://github.com/XcodesOrg/homebrew-cask/blob/master/Casks/x/xcodes.rb).
```
</details>
## Maintainers
[Matt Kiazyk](https://github.com/mattkiazyk) - [Twitter](https://www.twitter.com/mattkiazyk)
[Twitter](https://twitter.com/xcodesApp) | [GitHub](https://github.com/xcodesOrg) | [Mastadon](https://iosdev.space/@XcodesApp) |
================================================
FILE: Scripts/export_options.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
</dict>
</plist>
================================================
FILE: Scripts/fix_libfido2_framework.sh
================================================
#!/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
================================================
FILE: Scripts/increment_build_number.sh
================================================
#!/bin/sh
#
# Increment build number
#
# This will get the latest build number from git tags, add 1, then set it in the Info.plist.
# Assumes that build numbers are monotonically increasing positive integers, across version numbers.
# Tags must be named v$version_numberb$build_number, e.g. v1.2.3b456
infoplist_file="$(pwd)/Xcodes/Resources/Info.plist"
# Get latest tag hash matching pattern
hash=$(git rev-list --tags="v*" --max-count=1)
# Get latest tag at hash that matches the same pattern as a prefix in order to support commits with multiple tags
last_tag=$(git describe --tags --match "v*" "$hash")
# Get build number from last component of tag name
last_build_number=$(echo "$last_tag" | grep -o "b.*" | cut -c 2-)
build_number=$(($last_build_number + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $build_number" "${infoplist_file}"
================================================
FILE: Scripts/notarize.sh
================================================
#!/bin/sh
#
# Notarize
#
# Uploads to Apple's notarization service, polls until it completes, staples the ticket to the built app, then creates a new zip.
#
# Requires four arguments:
# - Apple ID username
# - Apple ID app-specific password (store this in your Keychain and use the @keychain:$NAME syntax to prevent your password from being added to your shell history)
# - App Store Connect provider name
# - Path to .app to upload
#
# Assumes that there's a .app beside the .zip with the same name so it can be stapled and re-zipped.
#
# E.g. notarize.sh "test@example.com" "@keychain:altool" MyOrg Xcodes.zip
#
# https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow
# Adapted from https://github.com/keybase/client/blob/46f5df0aa64ff19198ba7b044bbb7cd907c0be9f/packaging/desktop/package_darwin.sh
file="$1"
team_id="$2"
echo "Uploading to notarization service"
result=$(xcrun notarytool submit "$file" \
--keychain-profile "AC_PASSWORD" \
--team-id "$team_id" \
--wait)
# echo "done1"
echo $result
# My grep/awk is bad and I can't figure out how to get the UUID out properly
# uuid=$("$result" | \
# grep 'id:' | tail -n1 | \
# cut -d":" -f2-)
echo "Successfully uploaded to notarization service, polling for result: $uuid"
# we should check here using the info (or notarytool log) to check the results and log
#
# fullstatus=$(xcrun notarytool info "$uuid" \
# --keychain-profile "AC_PASSWORD" 2>&1)
# status=$(echo "$fullstatus" | grep 'status\:' | awk '{ print $2 }')
# if [ "$status" = "Accepted" ]; then
# echo "Notarization success"
# exit 0
# else
# echo "Notarization failed, full status below"
# echo "$fullstatus"
# exit 1
# fi
# Remove .zip
rm $file
# Staple ticket to .app
app_path="$(basename -s ".zip" "$file").app"
xcrun stapler staple "$app_path"
# Zip the stapled app for distribution
ditto -c -k --sequesterRsrc --keepParent "$app_path" "$file"
================================================
FILE: Scripts/package_release.sh
================================================
#!/bin/bash
#
# Package release
#
# This will build and archive the app and then compress it in a .zip file at Product/Xcodes.zip
# You must already have all required code signing assets installed on your computer
PROJECT_NAME=Xcodes
PROJECT_DIR=$(pwd)/$PROJECT_NAME/Resources
SCRIPTS_DIR=$(pwd)/Scripts
INFOPLIST_FILE="Info.plist"
# If needed ensure that the unxip binary is signed with a hardened runtime so we can notarize
# codesign --force --options runtime --sign "Developer ID Application: Robots and Pencils Inc." $PROJECT_DIR/unxip
# Ensure a clean build
rm -rf Archive/*
rm -rf Product/*
xcodebuild clean -project $PROJECT_NAME.xcodeproj -configuration Release -alltargets
# Archive the app and export for release distribution
xcodebuild archive -project $PROJECT_NAME.xcodeproj -scheme $PROJECT_NAME -archivePath Archive/$PROJECT_NAME.xcarchive
xcodebuild -archivePath Archive/$PROJECT_NAME.xcarchive -exportArchive -exportPath Product/$PROJECT_NAME -exportOptionsPlist "${SCRIPTS_DIR}/export_options.plist"
cp -a "Product/$PROJECT_NAME/$PROJECT_NAME.app" "Product/$PROJECT_NAME.app"
# Create a ZIP archive suitable for altool.
/usr/bin/ditto -c -k --keepParent "Product/$PROJECT_NAME.app" "Product/$PROJECT_NAME.zip"
================================================
FILE: Scripts/uninstall_privileged_helper.sh
================================================
#!/bin/bash
PRIVILEGED_HELPER_LABEL=com.xcodesorg.xcodesapp.Helper
sudo rm /Library/PrivilegedHelperTools/$PRIVILEGED_HELPER_LABEL
sudo rm /Library/LaunchDaemons/$PRIVILEGED_HELPER_LABEL.plist
sudo launchctl bootout system/$PRIVILEGED_HELPER_LABEL #'Boot-out failed: 36: Operation now in progress' is OK output
echo "Querying launchd..."
LAUNCHD_OUTPUT=$(sudo launchctl list | grep $PRIVILEGED_HELPER_LABEL)
if [ -z "$LAUNCHD_OUTPUT" ]
then
echo "Finished successfully."
else
echo "WARNING: $PRIVILEGED_HELPER_LABEL is not removed"
fi
================================================
FILE: Xcodes/AcknowledgementsGenerator/.gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
================================================
FILE: Xcodes/AcknowledgementsGenerator/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
================================================
FILE: Xcodes/AcknowledgementsGenerator/Package.swift
================================================
// swift-tools-version:5.4
import PackageDescription
let package = Package(
name: "AcknowledgementsGenerator",
platforms: [.macOS(.v11)],
products: [
.executable(
name: "AcknowledgementsGenerator",
targets: ["AcknowledgementsGenerator"]
),
],
dependencies: [
],
targets: [
.executableTarget(
name: "AcknowledgementsGenerator",
dependencies: []
),
]
)
================================================
FILE: Xcodes/AcknowledgementsGenerator/README.md
================================================
# AcknowledgementsGenerator
Scans an Xcode project's checked-out SPM packages for license files, then combines them into a single RTF file.
Based on https://github.com/MacPaw/spm-licenses.
================================================
FILE: Xcodes/AcknowledgementsGenerator/Sources/AcknowledgementsGenerator/Extensions/CollectionExtensions.swift
================================================
//
// CollectionExtensions.swift
// spm-licenses
//
// Created by Sergii Kryvoblotskyi on 11/11/19.
// Copyright © 2019 MacPaw. All rights reserved.
//
import Foundation
public extension Collection {
/// Returns the element at the specified index iff it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
================================================
FILE: Xcodes/AcknowledgementsGenerator/Sources/AcknowledgementsGenerator/Extensions/StringExtensions.swift
================================================
//
// StringExtensions.swift
// spm-licenses
//
// Created by Sergii Kryvoblotskyi on 11/11/19.
// Copyright © 2019 MacPaw. All rights reserved.
//
import Foundation
public extension String {
var nsString: NSString {
(self as NSString)
}
var pathExtension: String {
return nsString.pathExtension
}
var lastPathComponent: String {
return nsString.lastPathComponent
}
var deletingLastPathComponent: String {
return nsString.deletingLastPathComponent
}
var stringByDeletingPathExtension: String {
return nsString.deletingPathExtension
}
var expandingTildeInPath: String {
return nsString.expandingTildeInPath
}
func appendingPathComponent(_ component: String) -> String {
return nsString.appendingPathComponent(component)
}
}
================================================
FILE: Xcodes/AcknowledgementsGenerator/Sources/AcknowledgementsGenerator/Tools/Xcode.swift
================================================
//
// Xcode.swift
// spm-licenses
//
// Created by Sergii Kryvoblotskyi on 11/11/19.
// Copyright © 2019 MacPaw. All rights reserved.
//
import Foundation
struct Xcode {
static var derivedDataURL: URL {
if let overridenPath = readOverridenDerivedDataPath() {
return URL(fileURLWithPath: overridenPath.expandingTildeInPath)
}
let defaultPath = "~/Library/Developer/Xcode/DerivedData/".expandingTildeInPath
return URL(fileURLWithPath: defaultPath)
}
}
//defaults read com.apple.dt.Xcode.plist IDECustomDerivedDataLocation
//If the line returns
//
//The domain/default pair of (com.apple.dt.Xcode.plist, IDECustomDerivedDataLocation) does not exist
//it's the default path ~/Library/Developer/Xcode/DerivedData/ otherwise the custom path.
private extension Xcode {
static func readOverridenDerivedDataPath() -> String? {
let task = Process()
let pipe = Pipe()
task.executableURL = URL(fileURLWithPath: "/usr/bin/defaults")
task.arguments = ["read","com.apple.dt.Xcode.plist", "IDECustomDerivedDataLocation"]
task.standardOutput = pipe
try? task.run()
let handle = pipe.fileHandleForReading
let data = handle.readDataToEndOfFile()
let path = String(data: data, encoding: String.Encoding.utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
return (path?.isEmpty ?? true) ? nil : path
}
}
extension Xcode {
struct Project {
let url: URL
let info: [String: Any]
var workspacePath: String? {
return info["WorkspacePath"] as? String
}
}
}
extension Xcode.Project {
struct License {
let url: URL
let name: String
}
}
extension Xcode.Project.License {
func makeRepresentation() throws -> [String: String] {
let data = try Data(contentsOf: url)
let text = String(data: data, encoding: .utf8) ?? ""
return [
"Title": name,
"Type": "PSGroupSpecifier",
"FooterText": text
]
}
}
================================================
FILE: Xcodes/AcknowledgementsGenerator/Sources/AcknowledgementsGenerator/main.swift
================================================
//
// main.swift
// spm-licenses
//
// Created by Sergii Kryvoblotskyi on 11/11/19.
// Copyright © 2019 MacPaw. All rights reserved.
//
import Foundation
import AppKit
let arguments = CommandLine.arguments
guard let projectIndex = arguments.firstIndex(of: "-p"), let projectPath = arguments[safe: projectIndex + 1] else {
print("Project path is missing. Specify -p.")
exit(EXIT_FAILURE)
}
guard let outputIndex = arguments.firstIndex(of: "-o"), let outputPath = arguments[safe: outputIndex + 1] else {
print("Output path is missing. Specify -o.")
exit(EXIT_FAILURE)
}
let fileManager = FileManager.default
let projectURL = URL(fileURLWithPath: projectPath.expandingTildeInPath)
if !fileManager.fileExists(atPath: projectURL.path) {
print("xcodeproj not found at \(projectURL)")
exit(EXIT_FAILURE)
}
let packageURL = projectURL.appendingPathComponent("project.xcworkspace/xcshareddata/swiftpm/Package.resolved")
if !fileManager.fileExists(atPath: packageURL.path) {
print("Package.resolved not found at \(packageURL)")
exit(EXIT_FAILURE)
}
let packageData = try Data(contentsOf: packageURL)
let packageInfo = try JSONSerialization.jsonObject(with: packageData, options: .allowFragments)
guard let package = packageInfo as? [String: Any] else {
print("Invalid package format")
exit(EXIT_FAILURE)
}
guard let object = package["object"] as? [String: Any] else {
print("Invalid obejct format")
exit(EXIT_FAILURE)
}
guard let pins = object["pins"] as? [[String: Any]] else {
print("Invalid pins format")
exit(EXIT_FAILURE)
}
let projectsURL = Xcode.derivedDataURL
func projectsInfo(at url: URL) throws -> [Xcode.Project] {
try fileManager
.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
.map { $0.appendingPathComponent("info.plist") }
.compactMap {
guard let info = NSDictionary(contentsOf: $0) as? [String: Any] else { return nil }
return Xcode.Project(url: $0, info: info)
}
}
let projects = try projectsInfo(at: projectsURL)
// Despite the naming, if the project only has an xcodeproj and not an xcworkspace, the WorkspacePath value will be the path to the xcodeproj
guard let currentProject = projects.first(where: ({ $0.workspacePath == projectPath.expandingTildeInPath })) else {
print("Derived data missing for project")
exit(EXIT_FAILURE)
}
let checkouts = currentProject.url.deletingLastPathComponent().appendingPathComponent("SourcePackages/checkouts")
let checkedDependencies = try fileManager.contentsOfDirectory(at: checkouts, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
let spmLicences: [Xcode.Project.License] = checkedDependencies.compactMap {
let supportedFilenames = ["LICENSE", "LICENSE.txt", "LICENSE.md"]
for filename in supportedFilenames {
let licenseURL = $0.appendingPathComponent(filename)
if fileManager.fileExists(atPath: licenseURL.path) {
return Xcode.Project.License(url: licenseURL, name: $0.lastPathComponent)
}
}
return nil
}
var manualLicenses: [Xcode.Project.License] = []
let enumerator = fileManager.enumerator(at: projectURL.deletingLastPathComponent(), includingPropertiesForKeys: [URLResourceKey.nameKey], options: .skipsHiddenFiles)!
for case let url as URL in enumerator where url.lastPathComponent.hasSuffix(".LICENSE") {
manualLicenses.append(
Xcode.Project.License(
url: url,
name: url.lastPathComponent.replacingOccurrences(of: ".LICENSE", with: "")
)
)
}
let licences = spmLicences + manualLicenses
let acknowledgementsAttributedString = NSMutableAttributedString()
for licence in licences {
acknowledgementsAttributedString.append(NSAttributedString(string: licence.name + "\n\n", attributes: [.font: NSFont.preferredFont(forTextStyle: .title2)]))
let licenseContents = try String(contentsOf: licence.url)
acknowledgementsAttributedString.append(NSAttributedString(string: licenseContents + "\n\n", attributes: [.font: NSFont.preferredFont(forTextStyle: .body)]))
}
guard let data = acknowledgementsAttributedString.rtf(from: NSRange(location: 0, length: acknowledgementsAttributedString.length), documentAttributes: [:]) else {
print("Failed to create RTF data")
exit(EXIT_FAILURE)
}
try data.write(to: URL(fileURLWithPath: outputPath.expandingTildeInPath))
print("Licenses have been saved to \(outputPath)")
================================================
FILE: Xcodes/AcknowledgementsGenerator/spm-licenses.LICENSE
================================================
MIT License
Copyright (c) 2019 MacPaw
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.
================================================
FILE: Xcodes/AppleAPI/.gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
================================================
FILE: Xcodes/AppleAPI/Package.swift
================================================
// 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(.v11)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "AppleAPI",
targets: ["AppleAPI"]),
],
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: [.product(name: "SRP", package: "swift-srp")]),
.testTarget(
name: "AppleAPITests",
dependencies: ["AppleAPI"]),
]
)
================================================
FILE: Xcodes/AppleAPI/README.md
================================================
# AppleAPI
A description of this package.
================================================
FILE: Xcodes/AppleAPI/Sources/AppleAPI/Client.swift
================================================
import Foundation
import Combine
import SRP
import Crypto
import CommonCrypto
public class Client {
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
public init() {}
// MARK: - Login
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)
.decode(type: ServiceKeyResponse.self, decoder: JSONDecoder())
.flatMap { serviceKeyResponse -> AnyPublisher<(String, String), Swift.Error> in
serviceKey = serviceKeyResponse.authServiceKey
// Fixes issue https://github.com/RobotsAndPencils/XcodesApp/issues/360
// On 2023-02-23, Apple added a custom implementation of hashcash to their auth flow
// Without this addition, Apple ID's would get set to locked
return self.loadHashcash(accountName: accountName, serviceKey: serviceKey)
.map { return (serviceKey, $0)}
.eraseToAnyPublisher()
}
.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()
}
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
return Just(data)
.decode(type: SignInResponse.self, decoder: JSONDecoder())
.flatMap { responseBody -> AnyPublisher<AuthenticationState, Swift.Error> in
let httpResponse = response as! HTTPURLResponse
switch httpResponse.statusCode {
case 200:
return Current.network.dataTask(with: URLRequest.olympusSession)
.map { _ in AuthenticationState.authenticated }
.mapError { $0 as Swift.Error }
.eraseToAnyPublisher()
case 401:
return Fail(error: AuthenticationError.invalidUsernameOrPassword(username: accountName))
.eraseToAnyPublisher()
case 403:
let errorMessage = responseBody.serviceErrors?.first?.description.replacingOccurrences(of: "-20209: ", with: "") ?? ""
return Fail(error: AuthenticationError.accountLocked(errorMessage))
.eraseToAnyPublisher()
case 409:
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
return Fail(error: AuthenticationError.appleIDAndPrivacyAcknowledgementRequired)
.eraseToAnyPublisher()
default:
return Fail(error: AuthenticationError.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", ")))
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
.mapError { $0 as Swift.Error }
.eraseToAnyPublisher()
}
func loadHashcash(accountName: String, serviceKey: String) -> AnyPublisher<String, Swift.Error> {
Result {
try URLRequest.federate(account: accountName, serviceKey: serviceKey)
}
.publisher
.flatMap { request in
Current.network.dataTask(with: request)
.mapError { $0 as Error }
.tryMap { (data, response) throws -> (String) in
guard let urlResponse = response as? HTTPURLResponse else {
throw AuthenticationError.invalidSession
}
switch urlResponse.statusCode {
case 200..<300:
let httpResponse = response as! HTTPURLResponse
guard let bitsString = httpResponse.allHeaderFields["X-Apple-HC-Bits"] as? String, let bits = UInt(bitsString) else {
throw AuthenticationError.invalidHashcash
}
guard let challenge = httpResponse.allHeaderFields["X-Apple-HC-Challenge"] as? String else {
throw AuthenticationError.invalidHashcash
}
guard let hashcash = Hashcash().mint(resource: challenge, bits: bits) else {
throw AuthenticationError.invalidHashcash
}
return (hashcash)
case 400, 401:
throw AuthenticationError.invalidHashcash
case let code:
throw AuthenticationError.badStatusCode(statusCode: code, data: data, response: urlResponse)
}
}
}
.eraseToAnyPublisher()
}
func handleTwoStepOrFactor(data: Data, response: URLResponse, serviceKey: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
let httpResponse = response as! HTTPURLResponse
let sessionID = (httpResponse.allHeaderFields["X-Apple-ID-Session-Id"] as! String)
let scnt = (httpResponse.allHeaderFields["scnt"] as! String)
return Current.network.dataTask(with: URLRequest.authOptions(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt))
.map(\.data)
.decode(type: AuthOptionsResponse.self, decoder: JSONDecoder())
.flatMap { authOptions -> AnyPublisher<AuthenticationState, Error> in
switch authOptions.kind {
case .twoStep:
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
.eraseToAnyPublisher()
case .twoFactor, .securityKey:
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
.eraseToAnyPublisher()
case .unknown:
let possibleResponseString = String(data: data, encoding: .utf8)
return Fail(error: AuthenticationError.accountUsesUnknownAuthenticationKind(possibleResponseString))
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
func handleTwoFactor(serviceKey: String, sessionID: String, scnt: String, authOptions: AuthOptionsResponse) -> AnyPublisher<AuthenticationState, Error> {
let option: TwoFactorOption
// SMS was sent automatically
if authOptions.smsAutomaticallySent {
option = .smsSent(authOptions.trustedPhoneNumbers!.first!)
// 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
} else if authOptions.fsaChallenge != nil {
option = .securityKey
// User needs to use a physical security key to respond to the challenge
} else {
option = .codeSent
}
let sessionData = AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
return Just(AuthenticationState.waitingForSecondFactor(option, authOptions, sessionData))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
// MARK: - Continue 2FA
public func requestSMSSecurityCode(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) -> AnyPublisher<AuthenticationState, Error> {
Result {
try URLRequest.requestSecurityCode(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, trustedPhoneID: trustedPhoneNumber.id)
}
.publisher
.flatMap { request in
Current.network.dataTask(with: request)
.mapError { $0 as Error }
}
.map { _ in AuthenticationState.waitingForSecondFactor(.smsSent(trustedPhoneNumber), authOptions, sessionData) }
.eraseToAnyPublisher()
}
public func submitSecurityCode(_ code: SecurityCode, sessionData: AppleSessionData) -> AnyPublisher<AuthenticationState, Error> {
Result {
try URLRequest.submitSecurityCode(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, code: code)
}
.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()
}
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
public func validateSession() -> AnyPublisher<Void, Error> {
return Current.network.dataTask(with: URLRequest.olympusSession)
.tryMap { result -> Data in
let httpResponse = result.response as! HTTPURLResponse
if httpResponse.statusCode == 401 {
throw AuthenticationError.notAuthorized
}
return result.data
}
.decode(type: AppleSession.self, decoder: JSONDecoder())
.tryMap { session in
// A user that is a non-paid Apple Developer will have a provider == nil
// Those users can still download Xcode.
// Non Apple Developers will get caught in the download as invalid
// if session.provider == nil {
// throw AuthenticationError.notDeveloperAppleId
// }
}
.eraseToAnyPublisher()
}
func updateSession(serviceKey: String, sessionID: String, scnt: String) -> AnyPublisher<AuthenticationState, Error> {
return Current.network.dataTask(with: URLRequest.trust(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt))
.flatMap { (data, response) in
Current.network.dataTask(with: URLRequest.olympusSession)
.map { _ in AuthenticationState.authenticated }
}
.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
public enum AuthenticationState: Equatable {
case unauthenticated
case waitingForSecondFactor(TwoFactorOption, AuthOptionsResponse, AppleSessionData)
case authenticated
case notAppleDeveloper
}
public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
case invalidSession
case invalidHashcash
case invalidUsernameOrPassword(username: String)
case incorrectSecurityCode
case unexpectedSignInResponse(statusCode: Int, message: String?)
case appleIDAndPrivacyAcknowledgementRequired
case accountUsesTwoStepAuthentication
case accountUsesUnknownAuthenticationKind(String?)
case accountLocked(String)
case badStatusCode(statusCode: Int, data: Data, response: HTTPURLResponse)
case notDeveloperAppleId
case notAuthorized
case invalidResult(resultString: String?)
case srpInvalidPublicKey
public var errorDescription: String? {
switch self {
case .invalidSession:
return "Your authentication session is invalid. Try signing in again."
case .invalidHashcash:
return "Could not create a hashcash for the session."
case .invalidUsernameOrPassword:
return "Invalid username and password combination."
case .incorrectSecurityCode:
return "The code that was entered is incorrect."
case let .unexpectedSignInResponse(statusCode, message):
return """
Received an unexpected sign in response. If you continue to have problems, please submit a bug report in the Help menu and include the following information:
Status code: \(statusCode)
\(message != nil ? ("Message: " + message!) : "")
"""
case .appleIDAndPrivacyAcknowledgementRequired:
return "You must sign in to https://appstoreconnect.apple.com and acknowledge the Apple ID & Privacy agreement."
case .accountUsesTwoStepAuthentication:
return "Received a response from Apple that indicates this account has two-step authentication enabled. xcodes currently only supports the newer two-factor authentication, though. Please consider upgrading to two-factor authentication, or explain why this isn't an option for you by making a new feature request in the Help menu."
case .accountUsesUnknownAuthenticationKind:
return "Received a response from Apple that indicates this account has two-step or two-factor authentication enabled, but xcodes is unsure how to handle this response. If you continue to have problems, please submit a bug report in the Help menu."
case let .accountLocked(message):
return message
case let .badStatusCode(statusCode, _, _):
return "Received an unexpected status code: \(statusCode). If you continue to have problems, please submit a bug report in the Help menu."
case .notDeveloperAppleId:
return "You are not registered as an Apple Developer. Please visit Apple Developer Registration. https://developer.apple.com/register/"
case .notAuthorized:
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"
}
}
}
public struct AppleSessionData: Equatable, Identifiable {
public let serviceKey: String
public let sessionID: String
public let scnt: String
public var id: String { sessionID }
public init(serviceKey: String, sessionID: String, scnt: String) {
self.serviceKey = serviceKey
self.sessionID = sessionID
self.scnt = scnt
}
}
struct ServiceKeyResponse: Decodable {
let authServiceKey: String
}
struct SignInResponse: Decodable {
let authType: String?
let serviceErrors: [ServiceError]?
struct ServiceError: Decodable, CustomStringConvertible {
let code: String
let message: String
var description: String {
return "\(code): \(message)"
}
}
}
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 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,
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 {
if trustedDevices != nil {
return .twoStep
} else if trustedPhoneNumbers != nil {
return .twoFactor
} else if fsaChallenge != nil {
return .securityKey
} else {
return .unknown
}
}
// One time with a new testing account I had a response where noTrustedDevices was nil, but the account didn't have any trusted devices.
// This should have been a situation where an SMS security code was sent automatically.
// This resolved itself either after some time passed, or by signing into appleid.apple.com with the account.
// Not sure if it's worth explicitly handling this case or if it'll be really rare.
public var canFallBackToSMS: Bool {
noTrustedDevices == true
}
public var smsAutomaticallySent: Bool {
trustedPhoneNumbers?.count == 1 && canFallBackToSMS
}
public struct TrustedPhoneNumber: Equatable, Decodable, Identifiable {
public let id: Int
public let numberWithDialCode: String
public init(id: Int, numberWithDialCode: String) {
self.id = id
self.numberWithDialCode = numberWithDialCode
}
}
public struct TrustedDevice: Equatable, Decodable {
public let id: String
public let name: String
public let modelName: String
public init(id: String, name: String, modelName: String) {
self.id = id
self.name = name
self.modelName = modelName
}
}
public struct SecurityCodeInfo: Equatable, Decodable {
public let length: Int
public let tooManyCodesSent: Bool
public let tooManyCodesValidated: Bool
public let securityCodeLocked: Bool
public let securityCodeCooldown: Bool
public init(
length: Int,
tooManyCodesSent: Bool = false,
tooManyCodesValidated: Bool = false,
securityCodeLocked: Bool = false,
securityCodeCooldown: Bool = false
) {
self.length = length
self.tooManyCodesSent = tooManyCodesSent
self.tooManyCodesValidated = tooManyCodesValidated
self.securityCodeLocked = securityCodeLocked
self.securityCodeCooldown = securityCodeCooldown
}
}
public enum Kind: Equatable {
case twoStep, twoFactor, securityKey, unknown
}
}
public struct ServiceError: Decodable, Equatable {
let code: String
let message: String
}
public enum SecurityCode {
case device(code: String)
case sms(code: String, phoneNumberId: Int)
var urlPathComponent: String {
switch self {
case .device: return "trusteddevice"
case .sms: return "phone"
}
}
}
/// Object returned from olympus/v1/session
/// Used to check Provider, and show name
/// If Provider is nil, we can assume the Apple User is NOT an Apple Developer and can't download Xcode.
public struct AppleSession: Decodable, Equatable {
public let user: AppleUser
public let provider: AppleProvider?
}
public struct AppleProvider: Decodable, Equatable {
public let providerId: Int
public let name: String
}
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()
}
}
================================================
FILE: Xcodes/AppleAPI/Sources/AppleAPI/Environment.swift
================================================
import Foundation
import Combine
/**
Lightweight dependency injection using global mutable state :P
- SeeAlso: https://www.pointfree.co/episodes/ep16-dependency-injection-made-easy
- SeeAlso: https://www.pointfree.co/episodes/ep18-dependency-injection-made-comfortable
- SeeAlso: https://vimeo.com/291588126
*/
public struct Environment {
public var network = Network()
}
public var Current = Environment()
public struct Network {
public var session = URLSession.shared
public var dataTask: (URLRequest) -> URLSession.DataTaskPublisher = { Current.network.session.dataTaskPublisher(for: $0) }
public func dataTask(with request: URLRequest) -> URLSession.DataTaskPublisher {
dataTask(request)
}
}
================================================
FILE: Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift
================================================
//
// Hashcash.swift
//
//
// Created by Matt Kiazyk on 2023-02-23.
//
import Foundation
import CryptoKit
import CommonCrypto
/*
# This App Store Connect hashcash spec was generously donated by...
#
# __ _
# __ _ _ __ _ __ / _|(_) __ _ _ _ _ __ ___ ___
# / _` || '_ \ | '_ \ | |_ | | / _` || | | || '__|/ _ \/ __|
# | (_| || |_) || |_) || _|| || (_| || |_| || | | __/\__ \
# \__,_|| .__/ | .__/ |_| |_| \__, | \__,_||_| \___||___/
# |_| |_| |___/
#
#
*/
public struct Hashcash {
/// A function to returned a minted hash, using a bit and resource string
///
/**
X-APPLE-HC: 1:11:20230223170600:4d74fb15eb23f465f1f6fcbf534e5877::6373
^ ^ ^ ^ ^
| | | | +-- Counter
| | | +-- Resource
| | +-- Date YYMMDD[hhmm[ss]]
| +-- Bits (number of leading zeros)
+-- Version
We can't use an off-the-shelf Hashcash because Apple's implementation is not quite the same as the spec/convention.
1. The spec calls for a nonce called "Rand" to be inserted between the Ext and Counter. They don't do that at all.
2. The Counter conventionally encoded as base-64 but Apple just uses the decimal number's string representation.
Iterate from Counter=0 to Counter=N finding an N that makes the SHA1(X-APPLE-HC) lead with Bits leading zero bits
We get the "Resource" from the X-Apple-HC-Challenge header and Bits from X-Apple-HC-Bits
*/
/// - Parameters:
/// - resource: a string to be used for minting
/// - bits: grabbed from `X-Apple-HC-Bits` header
/// - date: Default uses Date() otherwise used for testing to check.
/// - Returns: A String hash to use in `X-Apple-HC` header on /signin
public func mint(resource: String,
bits: UInt = 10,
date: String? = nil) -> String? {
let ver = "1"
var ts: String
if let date = date {
ts = date
} else {
let formatter = DateFormatter()
formatter.dateFormat = "yyMMddHHmmss"
ts = formatter.string(from: Date())
}
let challenge = "\(ver):\(bits):\(ts):\(resource):"
var counter = 0
while true {
guard let digest = ("\(challenge):\(counter)").sha1 else {
print("ERROR: Can't generate SHA1 digest")
return nil
}
if digest == bits {
return "\(challenge):\(counter)"
}
counter += 1
}
}
}
extension String {
var sha1: Int? {
let data = Data(self.utf8)
var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
}
let bigEndianValue = digest.withUnsafeBufferPointer {
($0.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: 1) { $0 })
}.pointee
let value = UInt32(bigEndian: bigEndianValue)
return value.leadingZeroBitCount
}
}
================================================
FILE: Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift
================================================
import Foundation
public extension URL {
static let itcServiceKey = URL(string: "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com")!
static let signIn = URL(string: "https://idmsa.apple.com/appleauth/auth/signin")!
static let authOptions = URL(string: "https://idmsa.apple.com/appleauth/auth")!
static let requestSecurityCode = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/phone")!
static func submitSecurityCode(_ code: SecurityCode) -> URL { URL(string: "https://idmsa.apple.com/appleauth/auth/verify/\(code.urlPathComponent)/securitycode")! }
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 {
static var itcServiceKey: URLRequest {
return URLRequest(url: .itcServiceKey)
}
static func signIn(serviceKey: String, accountName: String, password: String, hashcash: String) -> URLRequest {
struct Body: Encodable {
let accountName: String
let password: String
let rememberMe = true
}
var request = URLRequest(url: .signIn)
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
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.allHTTPHeaderFields?["Accept"] = "application/json, text/javascript"
request.httpMethod = "POST"
request.httpBody = try! JSONEncoder().encode(Body(accountName: accountName, password: password))
return request
}
static func authOptions(serviceKey: String, sessionID: String, scnt: String) -> URLRequest {
var request = URLRequest(url: .authOptions)
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"
return request
}
static func requestSecurityCode(serviceKey: String, sessionID: String, scnt: String, trustedPhoneID: Int) throws -> URLRequest {
struct Body: Encodable {
let phoneNumber: PhoneNumber
let mode = "sms"
struct PhoneNumber: Encodable {
let id: Int
}
}
var request = URLRequest(url: .requestSecurityCode)
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
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.httpMethod = "PUT"
request.httpBody = try JSONEncoder().encode(Body(phoneNumber: .init(id: trustedPhoneID)))
return request
}
static func submitSecurityCode(serviceKey: String, sessionID: String, scnt: String, code: SecurityCode) throws -> URLRequest {
struct DeviceSecurityCodeRequest: Encodable {
let securityCode: SecurityCode
struct SecurityCode: Encodable {
let code: String
}
}
struct SMSSecurityCodeRequest: Encodable {
let securityCode: SecurityCode
let phoneNumber: PhoneNumber
let mode = "sms"
struct SecurityCode: Encodable {
let code: String
}
struct PhoneNumber: Encodable {
let id: Int
}
}
var request = URLRequest(url: .submitSecurityCode(code))
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"
switch code {
case .device(let code):
request.httpBody = try JSONEncoder().encode(DeviceSecurityCodeRequest(securityCode: .init(code: code)))
case .sms(let code, let phoneNumberId):
request.httpBody = try JSONEncoder().encode(SMSSecurityCodeRequest(securityCode: .init(code: code), phoneNumber: .init(id: phoneNumberId)))
}
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)
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"
return request
}
static var olympusSession: URLRequest {
return URLRequest(url: .olympusSession)
}
static func federate(account: String, serviceKey: String) throws -> URLRequest {
struct FederateRequest: Encodable {
let accountName: String
let rememberMe: Bool
}
var request = URLRequest(url: .signIn)
request.allHTTPHeaderFields?["Accept"] = "application/json"
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
request.httpMethod = "GET"
// let encoder = JSONEncoder()
// encoder.outputFormatting = .withoutEscapingSlashes
// request.httpBody = try encoder.encode(FederateRequest(accountName: account, rememberMe: true))
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
}
================================================
FILE: Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift
================================================
import XCTest
@testable import AppleAPI
final class AppleAPITests: XCTestCase {
func testValidHashCashMint() {
let bits: UInt = 11
let resource = "4d74fb15eb23f465f1f6fcbf534e5877"
let testDate = "20230223170600"
let stamp = Hashcash().mint(resource: resource, bits: bits, date: testDate)
XCTAssertEqual(stamp, "1:11:20230223170600:4d74fb15eb23f465f1f6fcbf534e5877::6373")
}
func testValidHashCashMint2() {
let bits: UInt = 10
let resource = "bb63edf88d2f9c39f23eb4d6f0281158"
let testDate = "20230224001754"
let stamp = Hashcash().mint(resource: resource, bits: bits, date: testDate)
XCTAssertEqual(stamp, "1:10:20230224001754:bb63edf88d2f9c39f23eb4d6f0281158::866")
}
static var allTests = [
("testValidHashCashMint", testValidHashCashMint),
("testValidHashCashMint2", testValidHashCashMint2),
]
}
================================================
FILE: Xcodes/AppleAPI/Tests/AppleAPITests/XCTestManifests.swift
================================================
import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(AppleAPITests.allTests),
]
}
#endif
================================================
FILE: Xcodes/AppleAPI/Tests/LinuxMain.swift
================================================
import XCTest
import AppleAPITests
var tests = [XCTestCaseEntry]()
tests += AppleAPITests.allTests()
XCTMain(tests)
================================================
FILE: Xcodes/Backend/AppState+Install.swift
================================================
import Combine
import Foundation
import Path
import AppleAPI
import Version
import LegibleError
import os.log
import DockProgress
import XcodesKit
/// Downloads and installs Xcodes
extension AppState {
// check to see if we should auto install for the user
public func autoInstallIfNeeded() {
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
guard let newestXcode = allXcodes.first, newestXcode.installState == .notInstalled else {
Logger.appState.info("User has latest Xcode already installed")
return
}
if autoInstallType == .newestBeta {
Logger.appState.info("Auto installing newest Xcode Beta")
// install it, as user doesn't have it installed and it's either latest beta or latest release
checkMinVersionAndInstall(id: newestXcode.id)
} else if autoInstallType == .newestVersion && newestXcode.version.isNotPrerelease {
Logger.appState.info("Auto installing newest Xcode")
checkMinVersionAndInstall(id: newestXcode.id)
} else {
Logger.appState.info("No new Xcodes version found to auto install")
}
}
public func install(_ installationType: InstallationType, downloader: Downloader) -> AnyPublisher<Void, Error> {
install(installationType, downloader: downloader, attemptNumber: 0)
.map { _ in Void() }
.eraseToAnyPublisher()
}
private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher<InstalledXcode, Error> {
Logger.appState.info("Using \(downloader) downloader")
setupDockProgress()
return validateSession()
.flatMap { _ in
self.getXcodeArchive(installationType, downloader: downloader)
}
.flatMap { xcode, url -> AnyPublisher<InstalledXcode, Swift.Error> in
self.installArchivedXcode(xcode, at: url)
}
.catch { error -> AnyPublisher<InstalledXcode, Swift.Error> in
self.resetDockProgressTracking()
switch error {
case InstallationError.damagedXIP(let damagedXIPURL):
guard attemptNumber < 1 else { return Fail(error: error).eraseToAnyPublisher() }
switch installationType {
case .version:
// If the XIP was just downloaded, remove it and try to recover.
do {
Logger.appState.error("\(error.legibleLocalizedDescription)")
Logger.appState.info("Removing damaged XIP and re-attempting installation.")
try Current.files.removeItem(at: damagedXIPURL)
return self.install(installationType, downloader: downloader, attemptNumber: attemptNumber + 1)
.eraseToAnyPublisher()
} catch {
return Fail(error: error)
.eraseToAnyPublisher()
}
}
default:
return Fail(error: error)
.eraseToAnyPublisher()
}
}
.handleEvents(receiveOutput: { installedXcode in
DispatchQueue.main.async {
guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: installedXcode.version) }) else { return }
self.allXcodes[index].installState = .installed(installedXcode.path)
}
})
.eraseToAnyPublisher()
}
private func getXcodeArchive(_ installationType: InstallationType, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> {
switch installationType {
case .version(let availableXcode):
if let installedXcode = Current.files.installedXcodes(Path.installDirectory).first(where: { $0.version.isEquivalent(to: availableXcode.version) }) {
return Fail(error: InstallationError.versionAlreadyInstalled(installedXcode))
.eraseToAnyPublisher()
}
return downloadXcode(availableXcode: availableXcode, downloader: downloader)
}
}
private func downloadXcode(availableXcode: AvailableXcode, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> {
self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
DispatchQueue.main.async {
self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress))
self.overallProgress.addChild(progress, withPendingUnitCount: AppState.totalProgressUnits - AppState.unxipProgressWeight)
}
})
.map { return (availableXcode, $0) }
.eraseToAnyPublisher()
}
public func downloadOrUseExistingArchive(for availableXcode: AvailableXcode, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher<URL, Error> {
// Check to see if the archive is in the expected path in case it was downloaded but failed to install
let expectedArchivePath = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).\(availableXcode.filename.suffix(fromLast: "."))"
// 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 = expectedArchivePath.parent/(expectedArchivePath.basename() + ".aria2")
var aria2DownloadIsIncomplete = false
if case .aria2 = downloader, aria2DownloadMetadataPath.exists {
aria2DownloadIsIncomplete = true
}
if Current.files.fileExistsAtPath(expectedArchivePath.string), aria2DownloadIsIncomplete == false {
Logger.appState.info("Found existing archive that will be used for installation at \(expectedArchivePath).")
return Just(expectedArchivePath.url)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
else {
let destination = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).\(availableXcode.filename.suffix(fromLast: "."))"
switch downloader {
case .aria2:
let aria2Path = Path(url: Bundle.main.url(forAuxiliaryExecutable: "aria2c")!)!
return downloadXcodeWithAria2(
availableXcode,
to: destination,
aria2Path: aria2Path,
progressChanged: progressChanged
)
case .urlSession:
return downloadXcodeWithURLSession(
availableXcode,
to: destination,
progressChanged: progressChanged
)
}
}
}
public func downloadXcodeWithAria2(_ availableXcode: AvailableXcode, to destination: Path, aria2Path: Path, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher<URL, Error> {
let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: availableXcode.url) ?? []
let (progress, publisher) = Current.shell.downloadWithAria2(
aria2Path,
availableXcode.url,
destination,
cookies
)
progressChanged(progress)
return publisher
.map { _ in destination.url }
.eraseToAnyPublisher()
}
public func downloadXcodeWithURLSession(_ availableXcode: AvailableXcode, to destination: Path, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher<URL, Error> {
let resumeDataPath = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).resumedata"
let persistedResumeData = Current.files.contents(atPath: resumeDataPath.string)
return attemptResumableTask(maximumRetryCount: 3) { resumeData -> AnyPublisher<URL, Error> in
let (progress, publisher) = Current.network.downloadTask(with: availableXcode.url,
to: destination.url,
resumingWith: resumeData ?? persistedResumeData)
progressChanged(progress)
return publisher
.map { $0.saveLocation }
.eraseToAnyPublisher()
}
.handleEvents(receiveCompletion: { completion in
self.persistOrCleanUpResumeData(at: resumeDataPath, for: completion)
})
.eraseToAnyPublisher()
}
public func installArchivedXcode(_ availableXcode: AvailableXcode, at archiveURL: URL) -> AnyPublisher<InstalledXcode, Error> {
unxipProgress.completedUnitCount = 0
overallProgress.addChild(unxipProgress, withPendingUnitCount: AppState.unxipProgressWeight)
do {
let destinationURL = Path.installDirectory.join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url
switch archiveURL.pathExtension {
case "xip":
return unarchiveAndMoveXIP(availableXcode: availableXcode, at: archiveURL, to: destinationURL)
.tryMap { xcodeURL throws -> InstalledXcode in
guard
let path = Path(url: xcodeURL),
Current.files.fileExists(atPath: path.string),
let installedXcode = InstalledXcode(path: path)
else { throw InstallationError.failedToMoveXcodeToApplications }
return installedXcode
}
.flatMap { installedXcode -> AnyPublisher<InstalledXcode, Error> in
do {
self.setInstallationStep(of: availableXcode.version, to: .trashingArchive)
try Current.files.trashItem(at: archiveURL)
self.setInstallationStep(of: availableXcode.version, to: .checkingSecurity)
return self.verifySecurityAssessment(of: installedXcode)
.combineLatest(self.verifySigningCertificate(of: installedXcode.path.url))
.map { _ in installedXcode }
.eraseToAnyPublisher()
} catch {
return Fail(error: error)
.eraseToAnyPublisher()
}
}
.flatMap { installedXcode -> AnyPublisher<InstalledXcode, Error> in
self.setInstallationStep(of: availableXcode.version, to: .finishing)
return self.performPostInstallSteps(for: installedXcode)
.map { installedXcode }
// Show post-install errors but don't fail because of them
.handleEvents(receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.InstallArchive.Error.Title"), message: error.legibleLocalizedDescription)
}
resetDockProgressTracking()
})
.catch { _ in
Just(installedXcode)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
case "dmg":
throw InstallationError.unsupportedFileFormat(extension: "dmg")
default:
throw InstallationError.unsupportedFileFormat(extension: archiveURL.pathExtension)
}
} catch {
return Fail(error: error)
.eraseToAnyPublisher()
}
}
func unarchiveAndMoveXIP(availableXcode: AvailableXcode, at source: URL, to destination: URL) -> AnyPublisher<URL, Swift.Error> {
self.setInstallationStep(of: availableXcode.version, to: .unarchiving)
return unxipOrUnxipExperiment(source)
.catch { error -> AnyPublisher<ProcessOutput, Swift.Error> in
if let executionError = error as? ProcessExecutionError {
if executionError.standardError.contains("damaged and can’t be expanded") {
return Fail(error: InstallationError.damagedXIP(url: source))
.eraseToAnyPublisher()
} else if executionError.standardError.contains("can’t be expanded because the selected volume doesn’t have enough free space.") {
return Fail(error: InstallationError.notEnoughFreeSpaceToExpandArchive(archivePath: Path(url: source)!,
version: availableXcode.version))
.eraseToAnyPublisher()
}
}
return Fail(error: error)
.eraseToAnyPublisher()
}
.tryMap { output -> URL in
self.setInstallationStep(of: availableXcode.version, to: .moving(destination: destination.path))
let xcodeURL = source.deletingLastPathComponent().appendingPathComponent("Xcode.app")
let xcodeBetaURL = source.deletingLastPathComponent().appendingPathComponent("Xcode-beta.app")
if Current.files.fileExists(atPath: xcodeURL.path) {
try Current.files.moveItem(at: xcodeURL, to: destination)
}
else if Current.files.fileExists(atPath: xcodeBetaURL.path) {
try Current.files.moveItem(at: xcodeBetaURL, to: destination)
}
return destination
}
.handleEvents(receiveCancel: {
if Current.files.fileExists(atPath: source.path) {
try? Current.files.removeItem(source)
}
if Current.files.fileExists(atPath: destination.path) {
try? Current.files.removeItem(destination)
}
})
.eraseToAnyPublisher()
}
func unxipOrUnxipExperiment(_ source: URL) -> AnyPublisher<ProcessOutput, Error> {
if unxipExperiment {
// All hard work done by https://github.com/saagarjha/unxip
// Compiled to binary with `swiftc -parse-as-library -O unxip.swift`
return Current.shell.unxipExperiment(source)
} else {
return Current.shell.unxip(source)
}
}
public func verifySecurityAssessment(of xcode: InstalledXcode) -> AnyPublisher<Void, Error> {
return Current.shell.spctlAssess(xcode.path.url)
.catch { (error: Swift.Error) -> AnyPublisher<ProcessOutput, Error> in
var output = ""
if let executionError = error as? ProcessExecutionError {
output = [executionError.standardOutput, executionError.standardError].joined(separator: "\n")
}
return Fail(error: InstallationError.failedSecurityAssessment(xcode: xcode, output: output))
.eraseToAnyPublisher()
}
.map { _ in Void() }
.eraseToAnyPublisher()
}
func verifySigningCertificate(of url: URL) -> AnyPublisher<Void, Error> {
return Current.shell.codesignVerify(url)
.catch { error -> AnyPublisher<ProcessOutput, Error> in
var output = ""
if let executionError = error as? ProcessExecutionError {
output = [executionError.standardOutput, executionError.standardError].joined(separator: "\n")
}
return Fail(error: InstallationError.codesignVerifyFailed(output: output))
.eraseToAnyPublisher()
}
.map { output -> CertificateInfo in
// codesign prints to stderr
return self.parseCertificateInfo(output.err)
}
.tryMap { cert in
guard
cert.teamIdentifier == XcodeTeamIdentifier,
cert.authority == XcodeCertificateAuthority
else { throw InstallationError.unexpectedCodeSigningIdentity(identifier: cert.teamIdentifier, certificateAuthority: cert.authority) }
return Void()
}
.eraseToAnyPublisher()
}
public struct CertificateInfo {
public var authority: [String]
public var teamIdentifier: String
public var bundleIdentifier: String
}
public func parseCertificateInfo(_ rawInfo: String) -> CertificateInfo {
var info = CertificateInfo(authority: [], teamIdentifier: "", bundleIdentifier: "")
for part in rawInfo.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: .newlines) {
if part.hasPrefix("Authority") {
info.authority.append(part.components(separatedBy: "=")[1])
}
if part.hasPrefix("TeamIdentifier") {
info.teamIdentifier = part.components(separatedBy: "=")[1]
}
if part.hasPrefix("Identifier") {
info.bundleIdentifier = part.components(separatedBy: "=")[1]
}
}
return info
}
// MARK: - Post-Install
/// Attemps to install the helper once, then performs all post-install steps
public func performPostInstallSteps(for xcode: InstalledXcode) {
performPostInstallSteps(for: xcode)
.sink(
receiveCompletion: { completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.PostInstall.Title"), message: error.legibleLocalizedDescription)
}
},
receiveValue: {}
)
.store(in: &cancellables)
}
/// Attemps to install the helper once, then performs all post-install steps
public func performPostInstallSteps(for xcode: InstalledXcode) -> AnyPublisher<Void, Error> {
let postInstallPublisher: AnyPublisher<Void, Error> =
Deferred { [unowned self] in
self.installHelperIfNecessary()
}
.flatMap { [unowned self] in
self.enableDeveloperMode()
}
.flatMap { [unowned self] in
self.approveLicense(for: xcode)
}
.flatMap { [unowned self] in
self.installComponents(for: xcode)
}
.mapError { [unowned self] error in
Logger.appState.error("Performing post-install steps failed: \(error.legibleLocalizedDescription)")
return InstallationError.postInstallStepsNotPerformed(version: xcode.version, helperInstallState: self.helperInstallState)
}
.eraseToAnyPublisher()
guard helperInstallState == .installed else {
// If the helper isn't installed yet then we need to prepare the user for the install prompt,
// and then actually perform the installation,
// and the post-install steps need to wait until that is complete.
// This subject, which completes upon isPreparingUserForActionRequiringHelper being invoked, is used to achieve that.
// This is not the most straightforward code I've ever written...
let helperInstallConsentSubject = PassthroughSubject<Void, Error>()
// Need to dispatch this to avoid duplicate alerts,
// the second of which will crash when force-unwrapping isPreparingUserForActionRequiringHelper
DispatchQueue.main.async {
self.isPreparingUserForActionRequiringHelper = { [unowned self] userConsented in
if userConsented {
helperInstallConsentSubject.send()
} else {
Logger.appState.info("User did not consent to installing helper during post-install steps.")
helperInstallConsentSubject.send(
completion: .failure(
InstallationError.postInstallStepsNotPerformed(version: xcode.version, helperInstallState: self.helperInstallState)
)
)
}
}
self.presentedAlert = .privilegedHelper
}
unxipProgress.completedUnitCount = AppState.totalProgressUnits
resetDockProgressTracking()
return helperInstallConsentSubject
.flatMap {
postInstallPublisher
}
.eraseToAnyPublisher()
}
return postInstallPublisher
}
private func enableDeveloperMode() -> AnyPublisher<Void, Error> {
Current.helper.devToolsSecurityEnable()
.flatMap {
Current.helper.addStaffToDevelopersGroup()
}
.eraseToAnyPublisher()
}
private func approveLicense(for xcode: InstalledXcode) -> AnyPublisher<Void, Error> {
Current.helper.acceptXcodeLicense(xcode.path.string)
.eraseToAnyPublisher()
}
private func installComponents(for xcode: InstalledXcode) -> AnyPublisher<Void, Swift.Error> {
Current.helper.runFirstLaunch(xcode.path.string)
.flatMap {
Current.shell.getUserCacheDir().map { $0.out }
.combineLatest(
Current.shell.buildVersion().map { $0.out },
Current.shell.xcodeBuildVersion(xcode).map { $0.out }
)
}
.flatMap { cacheDirectory, macOSBuildVersion, toolsVersion in
Current.shell.touchInstallCheck(cacheDirectory, macOSBuildVersion, toolsVersion)
}
.map { _ in Void() }
.eraseToAnyPublisher()
}
// MARK: - Dock Progress Tracking
private func setupDockProgress() {
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
}
}
func resetDockProgressTracking() {
Task { @MainActor in
DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete
}
}
// MARK: -
func setInstallationStep(of version: Version, to step: XcodeInstallationStep) {
DispatchQueue.main.async {
guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return }
self.allXcodes[index].installState = .installing(step)
let xcode = self.allXcodes[index]
Current.notificationManager.scheduleNotification(title: xcode.version.major.description + "." + xcode.version.appleDescription, body: step.description, category: .normal)
}
}
func setInstallationStep(of runtime: DownloadableRuntime, to step: RuntimeInstallationStep, postNotification: Bool = true) {
DispatchQueue.main.async {
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
self.downloadableRuntimes[index].installState = .installing(step)
if postNotification {
Current.notificationManager.scheduleNotification(title: runtime.name, body: step.description, category: .normal)
}
}
}
}
extension AppState {
func persistOrCleanUpResumeData<T>(at path: Path, for completion: Subscribers.Completion<T>) {
switch completion {
case .finished:
try? Current.files.removeItem(at: path.url)
case .failure(let error):
guard let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data else { return }
Current.files.createFile(atPath: path.string, contents: resumeData)
}
}
}
public enum InstallationError: LocalizedError, Equatable {
case damagedXIP(url: URL)
case notEnoughFreeSpaceToExpandArchive(archivePath: Path, version: Version)
case failedToMoveXcodeToApplications
case failedSecurityAssessment(xcode: InstalledXcode, output: String)
case codesignVerifyFailed(output: String)
case unexpectedCodeSigningIdentity(identifier: String, certificateAuthority: [String])
case unsupportedFileFormat(extension: String)
case missingSudoerPassword
case unavailableVersion(Version)
case noNonPrereleaseVersionAvailable
case noPrereleaseVersionAvailable
case missingUsernameOrPassword
case versionAlreadyInstalled(InstalledXcode)
case invalidVersion(String)
case versionNotInstalled(Version)
case postInstallStepsNotPerformed(version: Version, helperInstallState: HelperInstallState)
public var errorDescription: String? {
switch self {
case .damagedXIP(let url):
return String(format: localizeString("InstallationError.DamagedXIP"), url.lastPathComponent)
case let .notEnoughFreeSpaceToExpandArchive(archivePath, version):
return String(format: localizeString("InstallationError.NotEnoughFreeSpaceToExpandArchive"), archivePath.basename(), version.appleDescription)
case .failedToMoveXcodeToApplications:
return String(format: localizeString("InstallationError.FailedToMoveXcodeToApplications"), Path.installDirectory.string)
case .failedSecurityAssessment(let xcode, let output):
return String(format: localizeString("InstallationError.FailedSecurityAssessment"), String(xcode.version), output, xcode.path.string)
case .codesignVerifyFailed(let output):
return String(format: localizeString("InstallationError.CodesignVerifyFailed"), output)
case .unexpectedCodeSigningIdentity(let identity, let certificateAuthority):
return String(format: localizeString("InstallationError.UnexpectedCodeSigningIdentity"), identity, certificateAuthority, XcodeTeamIdentifier, XcodeCertificateAuthority)
case .unsupportedFileFormat(let fileExtension):
return String(format: localizeString("InstallationError.UnsupportedFileFormat"), fileExtension)
case .missingSudoerPassword:
return localizeString("InstallationError.MissingSudoerPassword")
case let .unavailableVersion(version):
return String(format: localizeString("InstallationError.UnavailableVersion"), version.appleDescription)
case .noNonPrereleaseVersionAvailable:
return localizeString("InstallationError.NoNonPrereleaseVersionAvailable")
case .noPrereleaseVersionAvailable:
return localizeString("InstallationError.NoPrereleaseVersionAvailable")
case .missingUsernameOrPassword:
return localizeString("InstallationError.MissingUsernameOrPassword")
case let .versionAlreadyInstalled(installedXcode):
return String(format: localizeString("InstallationError.VersionAlreadyInstalled"), installedXcode.version.appleDescription, installedXcode.path.string)
case let .invalidVersion(version):
return String(format: localizeString("InstallationError.InvalidVersion"), version)
case let .versionNotInstalled(version):
return String(format: localizeString("InstallationError.VersionNotInstalled"), version.appleDescription)
case let .postInstallStepsNotPerformed(version, helperInstallState):
switch helperInstallState {
case .installed:
return String(format: localizeString("InstallationError.PostInstallStepsNotPerformed.Installed"), version.appleDescription)
case .notInstalled, .unknown:
return String(format: localizeString("InstallationError.PostInstallStepsNotPerformed.NotInstalled"), version.appleDescription)
}
}
}
}
public enum InstallationType {
case version(AvailableXcode)
}
public enum AutoInstallationType: Int, Identifiable {
case none = 0
case newestVersion
case newestBeta
public var id: Self { self }
public var isAutoInstalling: Bool {
get {
return self != .none
}
set {
self = newValue ? .newestVersion : .none
}
}
public var isAutoInstallingBeta: Bool {
get {
return self == .newestBeta
}
set {
self = newValue ? .newestBeta : (isAutoInstalling ? .newestVersion : .none)
}
}
}
let XcodeTeamIdentifier = "59GAB85EFG"
let XcodeCertificateAuthority = ["Software Signing", "Apple Code Signing Certification Authority", "Apple Root CA"]
================================================
FILE: Xcodes/Backend/AppState+Runtimes.swift
================================================
import Foundation
import XcodesKit
import OSLog
import Combine
import Path
import AppleAPI
import Version
extension AppState {
func updateDownloadableRuntimes() {
Task {
do {
let downloadableRuntimes = try await self.runtimeService.downloadableRuntimes()
let runtimes = downloadableRuntimes.downloadables.map { runtime in
var updatedRuntime = runtime
// This loops through and matches up the simulatorVersion to the mappings
let simulatorBuildUpdate = downloadableRuntimes.sdkToSimulatorMappings.filter { SDKToSimulatorMapping in
SDKToSimulatorMapping.simulatorBuildUpdate == runtime.simulatorVersion.buildUpdate
}
updatedRuntime.sdkBuildUpdate = simulatorBuildUpdate.map { $0.sdkBuildUpdate }
return updatedRuntime
}
DispatchQueue.main.async {
self.downloadableRuntimes = runtimes
}
try? cacheDownloadableRuntimes(runtimes)
} catch {
Logger.appState.error("Error downloading runtimes: \(error.localizedDescription)")
}
}
}
func updateInstalledRuntimes() {
Task {
do {
Logger.appState.info("Loading Installed runtimes")
let runtimes = try await self.runtimeService.localInstalledRuntimes()
DispatchQueue.main.async {
self.installedRuntimes = runtimes
}
} catch {
Logger.appState.error("Error loading installed runtimes: \(error.localizedDescription)")
}
}
}
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)
if !Task.isCancelled {
Logger.appState.debug("Installing runtime: \(runtime.name)")
DispatchQueue.main.async {
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)"
case .diskImage:
try await self.installFromImage(dmgURL: downloadedURL)
DispatchQueue.main.async {
self.setInstallationStep(of: runtime, to: .trashingArchive)
}
try Current.files.removeItem(at: downloadedURL)
}
DispatchQueue.main.async {
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
self.downloadableRuntimes[index].installState = .installed
}
updateInstalledRuntimes()
}
}
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 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: downloadPath)
let downloader = Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2
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")
var aria2DownloadIsIncomplete = false
if case .aria2 = downloader, aria2DownloadMetadataPath.exists {
aria2DownloadIsIncomplete = true
}
if Current.files.fileExistsAtPath(expectedRuntimePath.string), aria2DownloadIsIncomplete == false {
Logger.appState.info("Found existing runtime that will be used for installation at \(expectedRuntimePath).")
return expectedRuntimePath.url
}
Logger.appState.info("Downloading \(runtime.visibleIdentifier) with \(downloader)")
switch downloader {
case .aria2:
let aria2Path = Path(url: Bundle.main.url(forAuxiliaryExecutable: "aria2c")!)!
for try await progress in downloadRuntimeWithAria2(runtime, to: expectedRuntimePath, aria2Path: aria2Path) {
DispatchQueue.main.async {
self.setInstallationStep(of: runtime, to: .downloading(progress: progress), postNotification: false)
}
}
Logger.appState.debug("Done downloading runtime")
case .urlSession:
throw "Downloading runtimes with URLSession is not supported. Please use aria2"
}
return expectedRuntimePath.url
}
public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path) -> AsyncThrowingStream<Progress, Error> {
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, url, destination, cookies)
}
public func installFromImage(dmgURL: URL) async throws {
try await self.runtimeService.installRuntimeImage(dmgURL: dmgURL)
}
func cancelRuntimeInstall(runtime: DownloadableRuntime) {
// Cancel the publisher
runtimePublishers[runtime.identifier]?.cancel()
runtimePublishers[runtime.identifier] = nil
// If the download is cancelled by the user, clean up the download files that aria2 creates.
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")
try? Current.files.removeItem(at: expectedRuntimePath.url)
try? Current.files.removeItem(at: aria2DownloadMetadataPath.url)
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
self.downloadableRuntimes[index].installState = .notInstalled
updateInstalledRuntimes()
}
func runtimeInstallPath(xcode: Xcode, runtime: DownloadableRuntime) -> Path? {
if let coreSimulatorInfo = coreSimulatorInfo(runtime: runtime) {
let urlString = coreSimulatorInfo.path["relative"]!
// app was not allowed to open up file:// url's so remove
let fileRemovedString = urlString.replacingOccurrences(of: "file://", with: "")
let url = URL(fileURLWithPath: fileRemovedString)
return Path(url: url)!
}
return nil
}
func coreSimulatorInfo(runtime: DownloadableRuntime) -> CoreSimulatorImage? {
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 {
if let info = coreSimulatorInfo(runtime: runtime) {
try await runtimeService.deleteRuntime(identifier: info.uuid)
// give it some time to actually finish deleting before updating
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.updateInstalledRuntimes()
}
} else {
throw "No simulator found with \(runtime.identifier)"
}
}
}
extension AnyPublisher {
func async() async throws -> Output {
try await withCheckedThrowingContinuation { continuation in
var cancellable: AnyCancellable?
cancellable = first()
.sink { result in
switch result {
case .finished:
break
case let .failure(error):
continuation.resume(throwing: error)
}
cancellable?.cancel()
} receiveValue: { value in
continuation.resume(with: .success(value))
}
}
}
}
extension AnyPublisher where Failure: Error {
struct Subscriber {
fileprivate let send: (Output) -> Void
fileprivate let complete: (Subscribers.Completion<Failure>) -> Void
func send(_ value: Output) { self.send(value) }
func send(completion: Subscribers.Completion<Failure>) { self.complete(completion) }
}
init(_ closure: (Subscriber) -> AnyCancellable) {
let subject = PassthroughSubject<Output, Failure>()
let subscriber = Subscriber(
send: subject.send,
complete: subject.send(completion:)
)
let cancel = closure(subscriber)
self = subject
.handleEvents(receiveCancel: cancel.cancel)
.eraseToAnyPublisher()
}
}
extension AnyPublisher where Failure == Error {
init(taskPriority: TaskPriority? = nil, asyncFunc: @escaping () async throws -> Output) {
self.init { subscriber in
let task = Task(priority: taskPriority) {
do {
subscriber.send(try await asyncFunc())
subscriber.send(completion: .finished)
} catch {
subscriber.send(completion: .failure(error))
}
}
return AnyCancellable { task.cancel() }
}
}
}
================================================
FILE: Xcodes/Backend/AppState+Update.swift
================================================
import Combine
import Foundation
import Path
import Version
import SwiftSoup
import AppleAPI
import XcodesKit
extension AppState {
var isReadyForUpdate: Bool {
guard let lastUpdated = Current.defaults.date(forKey: "lastUpdated"),
// This is bad date math but for this use case it doesn't need to be exact
lastUpdated < Current.date().addingTimeInterval(-60 * 60 * 5)
else {
return false
}
return true
}
func updateIfNeeded() {
guard
isReadyForUpdate
else {
updatePublisher = updateSelectedXcodePath()
.sink(
receiveCompletion: { _ in
self.updatePublisher = nil
},
receiveValue: { _ in }
)
return
}
update() as Void
}
func update() {
guard !isUpdating else { return }
updateDownloadableRuntimes()
updateInstalledRuntimes()
updatePublisher = updateSelectedXcodePath()
.flatMap { _ in
self.updateAvailableXcodes(from: self.dataSource)
}
.sink(
receiveCompletion: { [unowned self] completion in
switch completion {
case let .failure(error):
// 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 {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.Update.Error.Title"), message: error.legibleLocalizedDescription)
}
case .finished:
Current.defaults.setDate(Current.date(), forKey: "lastUpdated")
}
self.updatePublisher = nil
},
receiveValue: { _ in }
)
}
func updateSelectedXcodePath() -> AnyPublisher<Void, Never> {
Current.shell.xcodeSelectPrintPath()
.handleEvents(receiveOutput: { output in self.selectedXcodePath = output.out })
// Ignore xcode-select failures
.map { _ in Void() }
.catch { _ in Just(()) }
.eraseToAnyPublisher()
}
private func updateAvailableXcodes(from dataSource: DataSource) -> AnyPublisher<[AvailableXcode], Error> {
switch dataSource {
case .apple:
return signInIfNeeded()
.flatMap { [unowned self] in
// this will check to see if the Apple ID is a valid Apple Developer or not.
// If it's not, we can't use the Apple source to get xcode info.
self.validateSession()
}
.flatMap { [unowned self] in self.releasedXcodes().combineLatest(self.prereleaseXcodes()) }
.receive(on: DispatchQueue.main)
.map { releasedXcodes, prereleaseXcodes in
// Starting with Xcode 11 beta 6, developer.apple.com/download and developer.apple.com/download/more both list some pre-release versions of Xcode.
// Previously pre-release versions only appeared on developer.apple.com/download.
// /download/more doesn't include build numbers, so we trust that if the version number and prerelease identifiers are the same that they're the same build.
// If an Xcode version is listed on both sites then prefer the one on /download because the build metadata is used to compare against installed Xcodes.
let xcodes = releasedXcodes.filter { releasedXcode in
prereleaseXcodes.contains { $0.version.isEquivalent(to: releasedXcode.version) } == false
} + prereleaseXcodes
return xcodes
}
.handleEvents(
receiveOutput: { xcodes in
self.availableXcodes = xcodes
try? self.cacheAvailableXcodes(xcodes)
}
)
.eraseToAnyPublisher()
case .xcodeReleases:
return xcodeReleases()
.receive(on: DispatchQueue.main)
.handleEvents(
receiveOutput: { xcodes in
self.availableXcodes = xcodes
try? self.cacheAvailableXcodes(xcodes)
}
)
.eraseToAnyPublisher()
}
}
}
extension AppState {
// MARK: - Available Xcode Cache
func loadCachedAvailableXcodes() throws {
guard let data = Current.files.contents(atPath: Path.cacheFile.string) else { return }
let xcodes = try JSONDecoder().decode([AvailableXcode].self, from: data)
self.availableXcodes = xcodes
}
func cacheAvailableXcodes(_ xcodes: [AvailableXcode]) throws {
let data = try JSONEncoder().encode(xcodes)
try FileManager.default.createDirectory(at: Path.cacheFile.url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try data.write(to: Path.cacheFile.url)
}
// MARK: Runtime Cache
func loadCacheDownloadableRuntimes() throws {
guard let data = Current.files.contents(atPath: Path.runtimeCacheFile.string) else { return }
let runtimes = try JSONDecoder().decode([DownloadableRuntime].self, from: data)
self.downloadableRuntimes = runtimes
}
func cacheDownloadableRuntimes(_ runtimes: [DownloadableRuntime]) throws {
let data = try JSONEncoder().encode(runtimes)
try FileManager.default.createDirectory(at: Path.runtimeCacheFile.url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try data.write(to: Path.runtimeCacheFile.url)
}
}
extension AppState {
// MARK: - Apple
private func releasedXcodes() -> AnyPublisher<[AvailableXcode], Swift.Error> {
Current.network.dataTask(with: URLRequest.downloads)
.map(\.data)
.decode(type: Downloads.self, decoder: configure(JSONDecoder()) {
$0.dateDecodingStrategy = .formatted(.downloadsDateModified)
})
.tryMap { downloads -> [AvailableXcode] in
if downloads.hasError {
throw AuthenticationError.invalidResult(resultString: downloads.resultsString)
}
guard let downloadList = downloads.downloads else {
throw AuthenticationError.invalidResult(resultString: localizeString("DownloadingError"))
}
let xcodes = downloadList
.filter { $0.name.range(of: "^Xcode [0-9]", options: .regularExpression) != nil }
.compactMap { download -> AvailableXcode? in
let urlPrefix = URL(string: "https://download.developer.apple.com/")!
guard
let xcodeFile = download.files.first(where: { $0.remotePath.hasSuffix("dmg") || $0.remotePath.hasSuffix("xip") }),
let version = Version(xcodeVersion: download.name)
else { return nil }
let url = urlPrefix.appendingPathComponent(xcodeFile.remotePath)
return AvailableXcode(version: version, url: url, filename: String(xcodeFile.remotePath.suffix(fromLast: "/")), releaseDate: download.dateModified, fileSize: xcodeFile.fileSize)
}
return xcodes
}
.eraseToAnyPublisher()
}
private func prereleaseXcodes() -> AnyPublisher<[AvailableXcode], Error> {
Current.network.dataTask(with: URLRequest.download)
.tryMap { (data, _) -> [AvailableXcode] in
try self.parsePrereleaseXcodes(from: data)
}
.eraseToAnyPublisher()
}
private func parsePrereleaseXcodes(from data: Data) throws -> [AvailableXcode] {
let body = String(data: data, encoding: .utf8)!
let document = try SwiftSoup.parse(body)
guard
let xcodeHeader = try document.select("h2:containsOwn(Xcode)").first(),
let productBuildVersion = try xcodeHeader.parent()?.select("li:contains(Build)").text().replacingOccurrences(of: "Build", with: ""),
let releaseDateString = try xcodeHeader.parent()?.select("li:contains(Released)").text().replacingOccurrences(of: "Released", with: ""),
let version = Version(xcodeVersion: try xcodeHeader.text(), buildMetadataIdentifier: productBuildVersion),
let path = try document.select(".direct-download[href*=xip]").first()?.attr("href"),
let url = URL(string: "https://developer.apple.com" + path)
else { return [] }
let filename = String(path.suffix(fromLast: "/"))
return [AvailableXcode(version: version, url: url, filename: filename, releaseDate: DateFormatter.downloadsReleaseDate.date(from: releaseDateString))]
}
}
extension AppState {
// MARK: - XcodeReleases
private func xcodeReleases() -> AnyPublisher<[AvailableXcode], Error> {
Current.network.dataTask(with: URLRequest(url: URL(string: "https://xcodereleases.com/data.json")!))
.map(\.data)
.decode(type: [XcodeRelease].self, decoder: JSONDecoder())
.map { xcReleasesXcodes in
let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> AvailableXcode? in
guard
let downloadURL = xcReleasesXcode.links?.download?.url,
let version = Version(xcReleasesXcode: xcReleasesXcode)
else { return nil }
let releaseDate = Calendar(identifier: .gregorian).date(from: DateComponents(
year: xcReleasesXcode.date.year,
month: xcReleasesXcode.date.month,
day: xcReleasesXcode.date.day
))
return AvailableXcode(
version: version,
url: downloadURL,
filename: String(downloadURL.path.suffix(fromLast: "/")),
releaseDate: releaseDate,
requiredMacOSVersion: xcReleasesXcode.requires,
releaseNotesURL: xcReleasesXcode.links?.notes?.url,
sdks: xcReleasesXcode.sdks,
compilers: xcReleasesXcode.compilers,
architectures: xcReleasesXcode.architectures
)
}
return xcodes
}
.eraseToAnyPublisher()
}
}
================================================
FILE: Xcodes/Backend/AppState.swift
================================================
import AppKit
import AppleAPI
import Combine
import Path
import LegibleError
import KeychainAccess
import Path
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()
internal let runtimeService = RuntimeService()
// MARK: - Published Properties
@Published var authenticationState: AuthenticationState = .unauthenticated
@Published var availableXcodes: [AvailableXcode] = [] {
willSet {
if newValue.count > availableXcodes.count && availableXcodes.count != 0 {
Current.notificationManager.scheduleNotification(title: localizeString("Notification.NewXcodeVersion.Title"), body: localizeString("Notification.NewXcodeVersion.Body"), category: .normal)
}
updateAllXcodes(
availableXcodes: newValue,
installedXcodes: Current.files.installedXcodes(Path.installDirectory),
selectedXcodePath: selectedXcodePath
)
}
didSet {
autoInstallIfNeeded()
}
}
@Published var allXcodes: [Xcode] = []
@Published var selectedXcodePath: String? {
willSet {
updateAllXcodes(
availableXcodes: availableXcodes,
installedXcodes: Current.files.installedXcodes(Path.installDirectory),
selectedXcodePath: newValue
)
}
}
@Published var updatePublisher: AnyCancellable?
var isUpdating: Bool { updatePublisher != nil }
@Published var presentedSheet: XcodesSheet? = nil
@Published var isProcessingAuthRequest = false
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
@Published var presentedAlert: XcodesAlert?
@Published var presentedPreferenceAlert: XcodesPreferencesAlert?
@Published var helperInstallState: HelperInstallState = .notInstalled
/// Whether the user is being prepared for the helper installation alert with an explanation.
/// This closure will be performed after the user chooses whether or not to proceed.
@Published var isPreparingUserForActionRequiringHelper: ((Bool) -> Void)?
// MARK: - Errors
@Published var error: Error?
@Published var authError: Error?
// MARK: Advanced Preferences
@Published var localPath = "" {
didSet {
Current.defaults.set(localPath, forKey: "localPath")
}
}
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")
}
}
var createSymLinkOnSelectDisabled: Bool {
return onSelectActionType == .rename || PreferenceKey.createSymLinkOnSelect.isManaged()
}
@Published var onSelectActionType = SelectedActionType.none {
didSet {
Current.defaults.set(onSelectActionType.rawValue, forKey: "onSelectActionType")
if onSelectActionType == .rename {
createSymLinkOnSelect = false
}
}
}
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] = []
@Published var installedRuntimes: [CoreSimulatorImage] = []
// MARK: - Publisher Cancellables
var cancellables = Set<AnyCancellable>()
private var installationPublishers: [XcodeID: AnyCancellable] = [:]
internal var runtimePublishers: [String: Task<(), any Error>] = [:]
private var selectPublisher: AnyCancellable?
private var uninstallPublisher: AnyCancellable?
private var autoInstallTimer: Timer?
// MARK: - Dock Progress Tracking
public static let totalProgressUnits = Int64(10)
public static let unxipProgressWeight = Int64(1)
var overallProgress = Progress()
var unxipProgress = {
let progress = Progress(totalUnitCount: totalProgressUnits)
progress.kind = .file
progress.fileOperationKind = .copying
return progress
}()
// MARK: -
var dataSource: DataSource {
Current.defaults.string(forKey: "dataSource").flatMap(DataSource.init(rawValue:)) ?? .default
}
var savedUsername: String? {
Current.defaults.string(forKey: "username")
}
var hasSavedUsername: Bool {
savedUsername != nil
}
var bottomStatusBarMessage: String {
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
let finishDate = formatter.date(from: "11/06/2022")
if Date().compare(finishDate!) == .orderedAscending {
return String(format: localizeString("WWDC.Message"), "2022")
}
return ""
}
// MARK: - Init
init() {
guard !isTesting else { return }
try? loadCachedAvailableXcodes()
try? loadCacheDownloadableRuntimes()
checkIfHelperIsInstalled()
setupAutoInstallTimer()
setupDefaults()
}
func setupDefaults() {
localPath = Current.defaults.string(forKey: "localPath") ?? Path.defaultXcodesApplicationSupport.string
unxipExperiment = Current.defaults.bool(forKey: "unxipExperiment") ?? false
createSymLinkOnSelect = Current.defaults.bool(forKey: "createSymLinkOnSelect") ?? false
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 = 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
self?.updateIfNeeded()
}
}
// MARK: - Authentication
func validateADCSession(path: String) -> AnyPublisher<Void, Error> {
return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path))
.receive(on: DispatchQueue.main)
.tryMap { result -> Void in
let httpResponse = result.response as! HTTPURLResponse
if httpResponse.statusCode == 401 {
throw AuthenticationError.notAuthorized
}
}
.eraseToAnyPublisher()
}
func validateADCSession(path: String) async throws {
let result = try await Current.network.dataTaskAsync(with: URLRequest.downloadADCAuth(path: path))
let httpResponse = result.1 as! HTTPURLResponse
if httpResponse.statusCode == 401 {
throw AuthenticationError.notAuthorized
}
}
func validateSession() -> AnyPublisher<Void, Error> {
return Current.network.validateSession()
.receive(on: DispatchQueue.main)
.handleEvents(receiveCompletion: { completion in
if case .failure = completion {
// this is causing some awkwardness with showing an alert with the error and also popping up the sign in view
// self.authenticationState = .unauthenticated
// self.presentedSheet = .signIn
}
})
.eraseToAnyPublisher()
}
func signInIfNeeded() -> AnyPublisher<Void, Error> {
validateSession()
.catch { (error) -> AnyPublisher<Void, Error> in
guard
let username = self.savedUsername,
let password = try? Current.keychain.getString(username)
else {
return Fail(error: error)
.eraseToAnyPublisher()
}
return self.signIn(username: username, password: password)
.map { _ in Void() }
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
func signIn(username: String, password: String) {
authError = nil
signIn(username: username.lowercased(), password: password)
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in }
)
.store(in: &cancellables)
}
func signIn(username: String, password: String) -> AnyPublisher<AuthenticationState, Error> {
try? Current.keychain.set(password, key: username)
Current.defaults.set(username, forKey: "username")
isProcessingAuthRequest = true
return client.srpLogin(accountName: username, password: password)
.receive(on: DispatchQueue.main)
.handleEvents(
receiveOutput: { authenticationState in
self.authenticationState = authenticationState
},
receiveCompletion: { completion in
self.handleAuthenticationFlowCompletion(completion)
self.isProcessingAuthRequest = false
}
)
.eraseToAnyPublisher()
}
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
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) {
isProcessingAuthRequest = true
client.requestSMSSecurityCode(to: trustedPhoneNumber, authOptions: authOptions, sessionData: sessionData)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
self.handleAuthenticationFlowCompletion(completion)
self.isProcessingAuthRequest = false
},
receiveValue: { authenticationState in
self.authenticationState = authenticationState
if case let AuthenticationState.waitingForSecondFactor(option, authOptions, sessionData) = authenticationState {
self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
}
}
)
.store(in: &cancellables)
}
func choosePhoneNumberForSMS(authOptions: AuthOptionsResponse, sessionData: AppleSessionData) {
self.presentedSheet = .twoFactor(.init(
option: .smsPendingChoice,
authOptions: authOptions,
sessionData: sessionData
))
}
func submitSecurityCode(_ code: SecurityCode, sessionData: AppleSessionData) {
isProcessingAuthRequest = true
client.submitSecurityCode(code, sessionData: sessionData)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
self.handleAuthenticationFlowCompletion(completion)
self.isProcessingAuthRequest = false
},
receiveValue: { authenticationState in
self.authenticationState = authenticationState
}
)
.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):
// remove saved username and any stored keychain password if authentication fails so it doesn't try again.
clearLoginCredentials()
Logger.appState.error("Authentication error: \(error.legibleDescription)")
self.authError = error
case .finished:
switch self.authenticationState {
case .authenticated, .unauthenticated, .notAppleDeveloper:
self.presentedSheet = nil
case let .waitingForSecondFactor(option, authOptions, sessionData):
self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
}
}
}
func signOut() {
clearLoginCredentials()
AppleAPI.Current.network.session.configuration.httpCookieStorage?.removeCookies(since: .distantPast)
authenticationState = .unauthenticated
}
// MARK: - Helper
/// Install the privileged helper if it isn't already installed.
///
/// The way this is done is a little roundabout, because it requires user interaction in an alert before installation should be attempted.
/// The first time this method is invoked should be with `shouldPrepareUserForHelperInstallation` set to true.
/// If the helper is already installed, then nothing will happen.
/// If the helper is not already installed, the user will be prepared for installation and this method will return early.
/// If they consent to installing the helper then this method will be invoked again with `shouldPrepareUserForHelperInstallation` set to false.
/// This will install the helper.
///
/// - Parameter shouldPrepareUserForHelperInstallation: Whether the user should be presented with an alert preparing them for helper installation.
func installHelperIfNecessary(shouldPrepareUserForHelperInstallation: Bool = true) {
guard helperInstallState == .installed || shouldPrepareUserForHelperInstallation == false else {
isPreparingUserForActionRequiringHelper = { [unowned self] userConsented in
guard userConsented else { return }
self.installHelperIfNecessary(shouldPrepareUserForHelperInstallation: false)
}
presentedAlert = .privilegedHelper
return
}
installHelperIfNecessary()
.sink(
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.PrivilegedHelper.Error.Title"), message: error.legibleLocalizedDescription)
}
},
receiveValue: {}
)
.store(in: &cancellables)
}
func installHelperIfNecessary() -> AnyPublisher<Void, Error> {
Result {
if helperInstallState == .notInstalled {
try Current.helper.install()
checkIfHelperIsInstalled()
}
}
.publisher
.subscribe(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
private func checkIfHelperIsInstalled() {
helperInstallState = .unknown
Current.helper.checkIfLatestHelperIsInstalled()
.receive(on: DispatchQueue.main)
.sink(
receiveValue: { installed in
self.helperInstallState = installed ? .installed : .notInstalled
}
)
.store(in: &cancellables)
}
// MARK: - Install
func checkMinVersionAndInstall(id: XcodeID) {
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
// Check to see if users macOS is supported
if let requiredMacOSVersion = availableXcode.requiredMacOSVersion {
if hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) {
// prompt
self.presentedAlert = .checkMinSupportedVersion(xcode: availableXcode, macOS: ProcessInfo.processInfo.operatingSystemVersion.versionString())
return
}
}
switch self.dataSource {
case .apple:
install(id: id)
case .xcodeReleases:
install(id: id)
}
}
func hasMinSupportedOS(requiredMacOSVersion: String) -> Bool {
let split = requiredMacOSVersion.components(separatedBy: ".").compactMap { Int($0) }
let xcodeMinimumMacOSVersion = OperatingSystemVersion(majorVersion: split[safe: 0] ?? 0, minorVersion: split[safe: 1] ?? 0, patchVersion: split[safe: 2] ?? 0)
return !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion)
}
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,
// indicating that the user finished what they were doing in the UI.
self.$authenticationState
.filter { state in
switch state {
case .authenticated, .unauthenticated, .notAppleDeveloper: return true
case .waitingForSecondFactor: return false
}
}
.prefix(1)
.tryMap { state in
if state == .unauthenticated {
throw AuthenticationError.invalidSession
}
if state == .notAppleDeveloper {
throw AuthenticationError.notDeveloperAppleId
}
return Void()
}
}
.flatMap {
// This request would've already been made if the Apple data source were being used.
// That's not the case for the Xcode Releases data source.
// We need the cookies from its response in order to download Xcodes though,
// so perform it here first just to be sure.
Current.network.dataTask(with: URLRequest.downloads)
.map(\.data)
.decode(type: Downloads.self, decoder: configure(JSONDecoder()) {
$0.dateDecodingStrategy = .formatted(.downloadsDateModified)
})
.tryMap { downloads -> Void in
if downloads.hasError {
throw AuthenticationError.invalidResult(resultString: downloads.resultsString)
}
if downloads.downloads == nil {
throw AuthenticationError.invalidResult(resultString: localizeString("DownloadingError"))
}
}
.mapError { $0 as Error }
}
.flatMap { [unowned self] in
self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
}
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [unowned self] completion in
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 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)
}
if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
self.allXcodes[index].installState = .notInstalled
}
}
},
receiveValue: { _ in }
)
}
/// 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.xcodeID == id }) else { return }
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
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 {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
}
if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
self.allXcodes[index].installState = .notInstalled
}
}
},
receiveValue: { _ in }
)
}
func cancelInstall(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
// Cancel the publisher
installationPublishers[id] = nil
resetDockProgressTracking()
// If the download is cancelled by the user, clean up the download files that aria2 creates.
// This isn't done as part of the publisher with handleEvents(receiveCancel:) because it shouldn't happen when e.g. the app quits.
let expectedArchivePath = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).\(availableXcode.filename.suffix(fromLast: "."))"
let aria2DownloadMetadataPath = expectedArchivePath.parent/(expectedArchivePath.basename() + ".aria2")
try? Current.files.removeItem(at: expectedArchivePath.url)
try? Current.files.removeItem(at: aria2DownloadMetadataPath.url)
if let index = allXcodes.firstIndex(where: { $0.id == id }) {
allXcodes[index].installState = .notInstalled
}
}
// MARK: - Uninstall
func uninstall(xcode: Xcode) {
guard
let installedXcodePath = xcode.installedPath,
uninstallPublisher == nil
else { return }
uninstallPublisher = uninstallXcode(path: installedXcodePath)
.flatMap { [unowned self] _ in
self.updateSelectedXcodePath()
}
.sink(
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.Uninstall.Error.Title"), message: error.legibleLocalizedDescription)
}
self.uninstallPublisher = nil
},
receiveValue: { _ in }
)
}
func reveal(_ path: Path?) {
// TODO: show error if not
guard let path = path else { return }
NSWorkspace.shared.activateFileViewerSelecting([path.url])
}
func reveal(path: String) {
let url = URL(fileURLWithPath: path)
NSWorkspace.shared.activateFileViewerSelecting([url])
}
/// Make an Xcode active, a.k.a select it, in the `xcode-select` sense.
///
/// The underlying work is done by the privileged helper, so we need to make sure that it's installed first.
/// The way this is done is a little roundabout, because it requires user interaction in an alert before the `selectPublisher` is subscribed to.
/// The first time this method is invoked should be with `shouldPrepareUserForHelperInstallation` set to true.
/// If the helper is already installed, the Xcode will be made active immediately.
/// If the helper is not already installed, the user will be prepared for installation and this method will return early.
/// If they consent to installing the helper then this method will be invoked again with `shouldPrepareUserForHelperInstallation` set to false.
/// This will install the helper and make the Xcode active.
///
/// - Parameter xcode: The Xcode to make active.
/// - Parameter shouldPrepareUserForHelperInstallation: Whether the user should be presented with an alert preparing them for helper installation before making the Xcode version active.
func select(xcode: Xcode, shouldPrepareUserForHelperInstallation: Bool = true) {
guard helperInstallState == .installed || shouldPrepareUserForHelperInstallation == false else {
isPreparingUserForActionRequiringHelper = { [unowned self] userConsented in
guard userConsented else { return }
self.select(xcode: xcode, shouldPrepareUserForHelperInstallation: false)
}
presentedAlert = .privilegedHelper
return
}
guard
var installedXcodePath = xcode.installedPath,
selectPublisher == nil
else { return }
if onSelectActionType == .rename {
guard let newDestinationXcodePath = renameToXcode(xcode: xcode) else { return }
installedXcodePath = newDestinationXcodePath
}
selectPublisher = installHelperIfNecessary()
.flatMap {
Current.helper.switchXcodePath(installedXcodePath.string)
}
.flatMap { [unowned self] _ in
self.updateSelectedXcodePath()
}
.sink(
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.Select.Error.Title"), message: error.legibleLocalizedDescription)
} else {
if self.createSymLinkOnSelect {
createSymbolicLink(xcode: xcode)
}
}
self.selectPublisher = nil
},
receiveValue: { _ in }
)
}
func open(xcode: Xcode, openInRosetta: Bool? = false) {
switch xcode.installState {
case let .installed(path):
let config = NSWorkspace.OpenConfiguration.init()
if (openInRosetta ?? false) {
config.architecture = CPU_TYPE_X86_64
}
config.allowsRunningApplicationSubstitution = false
NSWorkspace.shared.openApplication(at: path.url, configuration: config)
default:
Logger.appState.error("\(xcode.id.version) is not installed")
return
}
}
func copyPath(xcode: Xcode) {
guard let installedXcodePath = xcode.installedPath else { return }
NSPasteboard.general.declareTypes([.URL, .string], owner: nil)
NSPasteboard.general.writeObjects([installedXcodePath.url as NSURL])
NSPasteboard.general.setString(installedXcodePath.string, forType: .string)
}
func copyReleaseNote(from url: URL?) {
guard let url = url else { return }
NSPasteboard.general.declareTypes([.URL, .string], owner: nil)
NSPasteboard.general.writeObjects([url as NSURL])
NSPasteboard.general.setString(url.absoluteString, forType: .string)
}
func createSymbolicLink(xcode: Xcode, isBeta: Bool = false) {
guard let installedXcodePath = xcode.installedPath else { return }
let destinationPath: Path = Path.installDirectory/"Xcode\(isBeta ? "-Beta" : "").app"
// does an Xcode.app file exist?
if FileManager.default.fileExists(atPath: destinationPath.string) {
do {
// if it's not a symlink, error because we don't want to delete an actual xcode.app file
let attributes: [FileAttributeKey : Any]? = try? FileManager.default.attributesOfItem(atPath: destinationPath.string)
if attributes?[.type] as? FileAttributeType == FileAttributeType.typeSymbolicLink {
try FileManager.default.removeItem(atPath: destinationPath.string)
Logger.appState.info("Successfully deleted old symlink")
} else {
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: localizeString("Alert.SymLink.Message"))
return
}
} catch {
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.localizedDescription)
}
}
do {
try FileManager.default.createSymbolicLink(atPath: destinationPath.string, withDestinationPath: installedXcodePath.string)
Logger.appState.info("Successfully created symbolic link with Xcode\(isBeta ? "-Beta": "").app")
} catch {
Logger.appState.error("Unable to create symbolic Link")
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
}
}
func renameToXcode(xcode: Xcode) -> Path? {
guard let installedXcodePath = xcode.installedPath else { return nil }
let destinationPath: Path = Path.installDirectory/"Xcode.app"
// rename any old named `Xcode.app` to the Xcodes versioned named files
if FileManager.default.fileExists(atPath: destinationPath.string) {
if let originalXcode = Current.files.installedXcode(destination: destinationPath) {
let newName = "Xcode-\(originalXcode.version.descriptionWithoutBuildMetadata).app"
Logger.appState.debug("Found Xcode.app - renaming back to \(newName)")
do {
try destinationPath.rename(to: newName)
} catch {
Logger.appState.error("Unable to create rename Xcode.app back to original")
self.error = error
// TODO UPDATE MY ERROR STRING
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
}
}
}
// rename passed in xcode to xcode.app
Logger.appState.debug("Found Xcode.app - renaming back to Xcode.app")
do {
return try installedXcodePath.rename(to: "Xcode.app")
} catch {
Logger.appState.error("Unable to create rename Xcode.app back to original")
self.error = error
// TODO UPDATE MY ERROR STRING
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
}
return nil
}
func updateAllXcodes(availableXcodes: [AvailableXcode], installedXcodes: [InstalledXcode], selectedXcodePath: String?) {
var adjustedAvailableXcodes = availableXcodes
// First, adjust all of the available Xcodes so that available and installed versions line up and the second part of this function works properly.
if dataSource == .apple {
for installedXcode in installedXcodes {
// We can trust that build metadata identifiers are unique for each version of Xcode, so if we have it then it's all we need.
// 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].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
else if let index = adjustedAvailableXcodes.firstIndex(where: { availableXcode in
availableXcode.version.isEquivalent(to: installedXcode.version) &&
availableXcode.version.buildMetadataIdentifiers.isEmpty
}) {
adjustedAvailableXcodes[index].xcodeID = installedXcode.xcodeID
}
}
}
// Map all of the available versions into Xcode values that join available and installed Xcode data for display.
var newAllXcodes = adjustedAvailableXcodes
.filter { availableXcode in
// If we don't have the build identifier, don't attempt to filter prerelease versions with identical build identifiers
guard !availableXcode.version.buildMetadataIdentifiers.isEmpty else { return true }
let availableXcodesWithIdenticalBuildIdentifiers = availableXcodes
.filter({ $0.version.buildMetadataIdentifiers == availableXcode.version.buildMetadataIdentifiers })
// 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 || availableXcode.architectures?.count ?? 0 != 0)
}
.map { availableXcode -> Xcode in
let installedXcode = installedXcodes.first(where: { installedXcode in
// 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: [XcodeID]
let prereleaseAvailableXcodesWithIdenticalBuildIdentifiers = availableXcodes
.filter {
return $0.version.buildMetadataIdentifiers == availableXcode.version.buildMetadataIdentifiers &&
!$0.version.prereleaseIdentifiers.isEmpty &&
// If we don't have the build identifier, don't consider this as a potential identical build
!$0.version.buildMetadataIdentifiers.isEmpty
}
// If this is the release version, add the identical builds to it
if !prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.isEmpty, availableXcode.version.prereleaseIdentifiers.isEmpty {
identicalBuilds = [availableXcode.xcodeID] + prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.map(\.xcodeID)
} else {
identicalBuilds = []
}
// If the existing install state is "installing", keep it
let existingXcodeInstallState = allXcodes.first { $0.id == availableXcode.xcodeID && $0.installState.installing }?.installState
// Otherwise, determine it from whether there's an installed Xcode
gitextract_lwe5ux5j/ ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ ├── release-drafter.yml │ └── workflows/ │ ├── appcast.yml │ ├── ci.yml │ ├── release-drafter.yml │ └── xcstrings.yml ├── .gitignore ├── AppCast/ │ ├── .gitignore │ ├── Gemfile │ ├── _config.yml │ ├── _includes/ │ │ └── appcast.inc │ ├── _plugins/ │ │ └── signature_filter.rb │ ├── appcast.xml │ └── appcast_pre.xml ├── CONTRIBUTING.md ├── DECISIONS.md ├── HelperXPCShared/ │ └── HelperXPCShared.swift ├── LICENSE ├── README.md ├── Scripts/ │ ├── export_options.plist │ ├── fix_libfido2_framework.sh │ ├── increment_build_number.sh │ ├── notarize.sh │ ├── package_release.sh │ ├── sign_update │ └── uninstall_privileged_helper.sh ├── Xcodes/ │ ├── AcknowledgementsGenerator/ │ │ ├── .gitignore │ │ ├── .swiftpm/ │ │ │ └── xcode/ │ │ │ └── package.xcworkspace/ │ │ │ └── contents.xcworkspacedata │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources/ │ │ │ └── AcknowledgementsGenerator/ │ │ │ ├── Extensions/ │ │ │ │ ├── CollectionExtensions.swift │ │ │ │ └── StringExtensions.swift │ │ │ ├── Tools/ │ │ │ │ └── Xcode.swift │ │ │ └── main.swift │ │ └── spm-licenses.LICENSE │ ├── AppleAPI/ │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources/ │ │ │ └── AppleAPI/ │ │ │ ├── Client.swift │ │ │ ├── Environment.swift │ │ │ ├── Hashcash.swift │ │ │ └── URLRequest+Apple.swift │ │ └── Tests/ │ │ ├── AppleAPITests/ │ │ │ ├── AppleAPITests.swift │ │ │ └── XCTestManifests.swift │ │ └── LinuxMain.swift │ ├── Backend/ │ │ ├── AppState+Install.swift │ │ ├── AppState+Runtimes.swift │ │ ├── AppState+Update.swift │ │ ├── AppState.swift │ │ ├── Aria2CError.swift │ │ ├── AvailableXcode.swift │ │ ├── Bundle+InfoPlistValues.swift │ │ ├── Collection+.swift │ │ ├── Configure.swift │ │ ├── DataSource.swift │ │ ├── DateFormatter+.swift │ │ ├── Downloader.swift │ │ ├── Downloads.swift │ │ ├── Entry+.swift │ │ ├── Environment.swift │ │ ├── FileError.swift │ │ ├── FileManager+.swift │ │ ├── FocusedValues.swift │ │ ├── Foundation.swift │ │ ├── Hardware.swift │ │ ├── HelperClient.swift │ │ ├── HelperInstallState.swift │ │ ├── InstalledXcode.swift │ │ ├── IsTesting.swift │ │ ├── NotificationManager.swift │ │ ├── Optional+IsNotNil.swift │ │ ├── Path+.swift │ │ ├── Process.swift │ │ ├── Progress+.swift │ │ ├── Publisher+Resumable.swift │ │ ├── SDKs+Xcode.swift │ │ ├── SelectedActionType.swift │ │ ├── SelectedXcode.swift │ │ ├── URLRequest+Apple.swift │ │ ├── URLSession+DownloadTaskPublisher.swift │ │ ├── Version+.swift │ │ ├── Version+Xcode.swift │ │ ├── Version+XcodeReleases.swift │ │ ├── Xcode.swift │ │ ├── XcodeCommands.swift │ │ └── XcodeInstallState.swift │ ├── Frontend/ │ │ ├── About/ │ │ │ ├── AboutView.swift │ │ │ ├── AcknowledgementsView.swift │ │ │ └── ScrollingTextView.swift │ │ ├── Common/ │ │ │ ├── NavigationSplitViewWrapper.swift │ │ │ ├── ObservingProgressIndicator.swift │ │ │ ├── ProgressButton.swift │ │ │ ├── ProgressIndicator.swift │ │ │ ├── TagView.swift │ │ │ ├── TrailingIconLabelStyle.swift │ │ │ ├── XcodesAlert.swift │ │ │ └── XcodesSheet.swift │ │ ├── InfoPane/ │ │ │ ├── CompatibilityView.swift │ │ │ ├── CompilersView.swift │ │ │ ├── CornerRadiusModifier.swift │ │ │ ├── IconView.swift │ │ │ ├── IdenticalBuildView.swift │ │ │ ├── InfoPane.swift │ │ │ ├── InfoPaneControls.swift │ │ │ ├── InstallationStepDetailView.swift │ │ │ ├── InstalledStateButtons.swift │ │ │ ├── NotInstalledStateButtons.swift │ │ │ ├── PlatformsView.swift │ │ │ ├── ReleaseDateView.swift │ │ │ ├── ReleaseNotesView.swift │ │ │ ├── RuntimeInstallationStepDetailView.swift │ │ │ ├── SDKsView.swift │ │ │ └── UnselectedView.swift │ │ ├── MainWindow.swift │ │ ├── Preferences/ │ │ │ ├── AdvancedPreferencePane.swift │ │ │ ├── DownloadPreferencePane.swift │ │ │ ├── ExperiementsPreferencePane.swift │ │ │ ├── GeneralPreferencePane.swift │ │ │ ├── NotificationsView.swift │ │ │ ├── PlatformsListView.swift │ │ │ ├── PreferencesView.swift │ │ │ └── UpdatesPreferencePane.swift │ │ ├── SignIn/ │ │ │ ├── AttributedText.swift │ │ │ ├── NSAttributedString+.swift │ │ │ ├── PinCodeTextView.swift │ │ │ ├── SignIn2FAView.swift │ │ │ ├── SignInCredentialsView.swift │ │ │ ├── SignInPhoneListView.swift │ │ │ ├── SignInSMSView.swift │ │ │ ├── SignInSecurityKeyPinView.swift │ │ │ ├── SignInSecurityKeyTouchView.swift │ │ │ └── SignedInView.swift │ │ ├── View+Conditional.swift │ │ ├── View+IsHidden.swift │ │ └── XcodeList/ │ │ ├── AppStoreButtonStyle.swift │ │ ├── BottomStatusBar.swift │ │ ├── InstallationStepRowView.swift │ │ ├── MainToolbar.swift │ │ ├── Tag.swift │ │ ├── XcodeListCategory.swift │ │ ├── XcodeListView.swift │ │ └── XcodeListViewRow.swift │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ ├── Resources/ │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Icons/ │ │ │ │ └── Contents.json │ │ │ ├── install.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── xcode-beta.imageset/ │ │ │ │ └── Contents.json │ │ │ └── xcode.imageset/ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── Licenses.rtf │ │ ├── Localizable.xcstrings │ │ ├── Xcodes.entitlements │ │ ├── XcodesIcon.icon/ │ │ │ └── icon.json │ │ ├── XcodesTest.entitlements │ │ ├── aria2c │ │ ├── aria2c.LICENSE │ │ └── unxip │ ├── XcodesApp.swift │ └── XcodesKit/ │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ ├── Sources/ │ │ └── XcodesKit/ │ │ ├── Extensions/ │ │ │ ├── Foundation.swift │ │ │ └── Logger.swift │ │ ├── Models/ │ │ │ ├── Runtimes/ │ │ │ │ ├── CoreSimulatorImage.swift │ │ │ │ ├── RuntimeInstallState.swift │ │ │ │ ├── RuntimeInstallationStep.swift │ │ │ │ └── Runtimes.swift │ │ │ ├── XcodeInstallState.swift │ │ │ ├── XcodeInstallationStep.swift │ │ │ └── XcodeReleases/ │ │ │ ├── Architecture.swift │ │ │ ├── Checksums.swift │ │ │ ├── Compilers.swift │ │ │ ├── Link.swift │ │ │ ├── Release.swift │ │ │ ├── SDKs.swift │ │ │ ├── XcodeRelease.swift │ │ │ ├── XcodeVersion.swift │ │ │ └── YMD.swift │ │ ├── Services/ │ │ │ └── RuntimeService.swift │ │ ├── Shell/ │ │ │ ├── Process.swift │ │ │ └── XcodesShell.swift │ │ └── XcodesKitEnvironment.swift │ └── Tests/ │ └── XcodesKitTests/ │ └── XcodesKitTests.swift ├── Xcodes.xcodeproj/ │ ├── ._project.xcworkspace │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ ├── Xcodes.xcscheme │ └── com.robotsandpencils.XcodesApp.Helper.xcscheme ├── XcodesTests/ │ ├── AppStateTests.swift │ ├── AppStateUpdateTests.swift │ ├── Bundle+XcodesTests.swift │ ├── Environment+Mock.swift │ ├── Fixtures/ │ │ ├── Stub-0.0.0.Info.plist │ │ └── Stub-version.plist │ └── Info.plist ├── com.xcodesorg.xcodesapp.Helper/ │ ├── AuditTokenHack.h │ ├── AuditTokenHack.m │ ├── ConnectionVerifier.swift │ ├── Info.plist │ ├── Logger.swift │ ├── SimpleXPCApp.LICENSE │ ├── XPCDelegate.swift │ ├── com.xcodesorg.xcodesapp.Helper-Bridging-Header.h │ ├── com.xcodesorg.xcodesapp.HelperTest.entitlements │ ├── launchd.plist │ └── main.swift └── nextstep.md
SYMBOL INDEX (3 symbols across 1 files)
FILE: AppCast/_plugins/signature_filter.rb
type Jekyll (line 1) | module Jekyll
type SignatureFilter (line 2) | module SignatureFilter
function sparkle_signature (line 3) | def sparkle_signature(release_body)
Condensed preview — 213 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,554K chars).
[
{
"path": ".github/CODEOWNERS",
"chars": 29,
"preview": "* @RobotsAndPencils/xcodes\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 426,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 286,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Te"
},
{
"path": ".github/dependabot.yml",
"chars": 162,
"preview": "version: 2\nupdates:\n # Maintain dependencies for GitHub Actions\n - package-ecosystem: \"github-actions\"\n directory: "
},
{
"path": ".github/release-drafter.yml",
"chars": 533,
"preview": "categories:\n - title: '🚀 Enhancements'\n labels:\n - 'enhancement'\n - title: '🐛 Bug Fixes'\n labels:\n - '"
},
{
"path": ".github/workflows/appcast.yml",
"chars": 1312,
"preview": "name: Build and publish a new appcast file\n\non:\n workflow_dispatch:\n release:\n\njobs:\n jekyll:\n runs-on: ubuntu-lat"
},
{
"path": ".github/workflows/ci.yml",
"chars": 272,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n pull_request:\n\njobs:\n test:\n runs-on: macos-15\n steps:\n - u"
},
{
"path": ".github/workflows/release-drafter.yml",
"chars": 343,
"preview": "name: Release Drafter\n\non:\n # Allow running it manually in case we forget to label a PR before merging\n workflow_dispa"
},
{
"path": ".github/workflows/xcstrings.yml",
"chars": 604,
"preview": "name: XCStrings Validation\n\non:\n workflow_dispatch:\n push:\n branches:\n - main\n pull_request:\n\njobs:\n test:\n "
},
{
"path": ".gitignore",
"chars": 2226,
"preview": "# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n"
},
{
"path": "AppCast/.gitignore",
"chars": 35,
"preview": "_site\n.sass-cache\n.jekyll-metadata\n"
},
{
"path": "AppCast/Gemfile",
"chars": 900,
"preview": "source \"https://rubygems.org\"\n\n# Hello! This is where you manage which Jekyll version is used to run.\n# When you want to"
},
{
"path": "AppCast/_config.yml",
"chars": 1354,
"preview": "# Welcome to Jekyll!\n#\n# This config file is meant for settings that affect your whole blog, values\n# which you are expe"
},
{
"path": "AppCast/_includes/appcast.inc",
"chars": 2485,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<rss version=\"2.0\" xmlns:sparkle=\"http://www.andymatuschak.org/xml-namespaces/spa"
},
{
"path": "AppCast/_plugins/signature_filter.rb",
"chars": 385,
"preview": "module Jekyll\n module SignatureFilter\n def sparkle_signature(release_body)\n regex = /<!-- sparkle:edSignature=("
},
{
"path": "AppCast/appcast.xml",
"chars": 52,
"preview": "---\nrelease_only: true\n---\n{%include appcast.inc %}\n"
},
{
"path": "AppCast/appcast_pre.xml",
"chars": 53,
"preview": "---\nrelease_only: false\n---\n{%include appcast.inc %}\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 1783,
"preview": "# Contributing to Xcodes\nWe love your input! We want to make contributing to this project as easy and transparent as pos"
},
{
"path": "DECISIONS.md",
"chars": 9939,
"preview": "# Decisions\n\nThis file exists to provide a historical record of the motivation for important technical decisions in the "
},
{
"path": "HelperXPCShared/HelperXPCShared.swift",
"chars": 769,
"preview": "import Foundation\n\nlet machServiceName = \"com.xcodesorg.xcodesapp.Helper\"\nlet clientBundleID = \"com.xcodesorg.xcodesapp\""
},
{
"path": "LICENSE",
"chars": 1080,
"preview": "MIT License\n\nCopyright (c) 2019-2021 Robots and Pencils\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "README.md",
"chars": 8261,
"preview": "<h1><img src=\"icon.png\" align=\"center\" width=50 height=50 /> <img src=\"IconDark.png\" align=\"center\" width=50 height=50 /"
},
{
"path": "Scripts/export_options.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Scripts/fix_libfido2_framework.sh",
"chars": 1097,
"preview": "#!/bin/sh\n\n# Fix libfido2.framework structure for macOS validation\n# If this script is not run, the build will fail beca"
},
{
"path": "Scripts/increment_build_number.sh",
"chars": 853,
"preview": "#!/bin/sh\n#\n# Increment build number\n#\n# This will get the latest build number from git tags, add 1, then set it in the "
},
{
"path": "Scripts/notarize.sh",
"chars": 2043,
"preview": "#!/bin/sh\n#\n# Notarize\n#\n# Uploads to Apple's notarization service, polls until it completes, staples the ticket to the "
},
{
"path": "Scripts/package_release.sh",
"chars": 1235,
"preview": "#!/bin/bash\n#\n# Package release\n#\n# This will build and archive the app and then compress it in a .zip file at Product/X"
},
{
"path": "Scripts/uninstall_privileged_helper.sh",
"chars": 557,
"preview": "#!/bin/bash\n\nPRIVILEGED_HELPER_LABEL=com.xcodesorg.xcodesapp.Helper\n\nsudo rm /Library/PrivilegedHelperTools/$PRIVILEGED_"
},
{
"path": "Xcodes/AcknowledgementsGenerator/.gitignore",
"chars": 53,
"preview": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\n"
},
{
"path": "Xcodes/AcknowledgementsGenerator/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata",
"chars": 135,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:\">\n </FileRef"
},
{
"path": "Xcodes/AcknowledgementsGenerator/Package.swift",
"chars": 465,
"preview": "// swift-tools-version:5.4\n\nimport PackageDescription\n\nlet package = Package(\n name: \"AcknowledgementsGenerator\",\n "
},
{
"path": "Xcodes/AcknowledgementsGenerator/README.md",
"chars": 191,
"preview": "# AcknowledgementsGenerator\n\nScans an Xcode project's checked-out SPM packages for license files, then combines them int"
},
{
"path": "Xcodes/AcknowledgementsGenerator/Sources/AcknowledgementsGenerator/Extensions/CollectionExtensions.swift",
"chars": 413,
"preview": "//\n// CollectionExtensions.swift\n// spm-licenses\n//\n// Created by Sergii Kryvoblotskyi on 11/11/19.\n// Copyright © 2"
},
{
"path": "Xcodes/AcknowledgementsGenerator/Sources/AcknowledgementsGenerator/Extensions/StringExtensions.swift",
"chars": 856,
"preview": "//\n// StringExtensions.swift\n// spm-licenses\n//\n// Created by Sergii Kryvoblotskyi on 11/11/19.\n// Copyright © 2019 "
},
{
"path": "Xcodes/AcknowledgementsGenerator/Sources/AcknowledgementsGenerator/Tools/Xcode.swift",
"chars": 2102,
"preview": "//\n// Xcode.swift\n// spm-licenses\n//\n// Created by Sergii Kryvoblotskyi on 11/11/19.\n// Copyright © 2019 MacPaw. All"
},
{
"path": "Xcodes/AcknowledgementsGenerator/Sources/AcknowledgementsGenerator/main.swift",
"chars": 4478,
"preview": "//\n// main.swift\n// spm-licenses\n//\n// Created by Sergii Kryvoblotskyi on 11/11/19.\n// Copyright © 2019 MacPaw. All "
},
{
"path": "Xcodes/AcknowledgementsGenerator/spm-licenses.LICENSE",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2019 MacPaw\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "Xcodes/AppleAPI/.gitignore",
"chars": 53,
"preview": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\n"
},
{
"path": "Xcodes/AppleAPI/Package.swift",
"chars": 1027,
"preview": "// swift-tools-version:5.7\n// The swift-tools-version declares the minimum version of Swift required to build this packa"
},
{
"path": "Xcodes/AppleAPI/README.md",
"chars": 43,
"preview": "# AppleAPI\n\nA description of this package.\n"
},
{
"path": "Xcodes/AppleAPI/Sources/AppleAPI/Client.swift",
"chars": 27614,
"preview": "import Foundation\nimport Combine\nimport SRP\nimport Crypto\nimport CommonCrypto\n\n\npublic class Client {\n private static"
},
{
"path": "Xcodes/AppleAPI/Sources/AppleAPI/Environment.swift",
"chars": 734,
"preview": "import Foundation\nimport Combine\n\n/**\n Lightweight dependency injection using global mutable state :P\n\n - SeeAlso: https"
},
{
"path": "Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift",
"chars": 3399,
"preview": "//\n// Hashcash.swift\n// \n//\n// Created by Matt Kiazyk on 2023-02-23.\n//\n\nimport Foundation\nimport CryptoKit\nimport Co"
},
{
"path": "Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift",
"chars": 9447,
"preview": "import Foundation\n\npublic extension URL {\n static let itcServiceKey = URL(string: \"https://appstoreconnect.apple.com/"
},
{
"path": "Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift",
"chars": 931,
"preview": "import XCTest\n@testable import AppleAPI\n\nfinal class AppleAPITests: XCTestCase {\n \n func testValidHashCashMint() {"
},
{
"path": "Xcodes/AppleAPI/Tests/AppleAPITests/XCTestManifests.swift",
"chars": 158,
"preview": "import XCTest\n\n#if !canImport(ObjectiveC)\npublic func allTests() -> [XCTestCaseEntry] {\n return [\n testCase(Ap"
},
{
"path": "Xcodes/AppleAPI/Tests/LinuxMain.swift",
"chars": 118,
"preview": "import XCTest\n\nimport AppleAPITests\n\nvar tests = [XCTestCaseEntry]()\ntests += AppleAPITests.allTests()\nXCTMain(tests)\n"
},
{
"path": "Xcodes/Backend/AppState+Install.swift",
"chars": 30260,
"preview": "import Combine\nimport Foundation\nimport Path\nimport AppleAPI\nimport Version\nimport LegibleError\nimport os.log\nimport Doc"
},
{
"path": "Xcodes/Backend/AppState+Runtimes.swift",
"chars": 16091,
"preview": "import Foundation\nimport XcodesKit\nimport OSLog\nimport Combine\nimport Path\nimport AppleAPI\nimport Version\n\nextension App"
},
{
"path": "Xcodes/Backend/AppState+Update.swift",
"chars": 11094,
"preview": "import Combine\nimport Foundation\nimport Path\nimport Version\nimport SwiftSoup\nimport AppleAPI\nimport XcodesKit\n\nextension"
},
{
"path": "Xcodes/Backend/AppState.swift",
"chars": 45686,
"preview": "import AppKit\nimport AppleAPI\nimport Combine\nimport Path\nimport LegibleError\nimport KeychainAccess\nimport Path\nimport Ve"
},
{
"path": "Xcodes/Backend/Aria2CError.swift",
"chars": 4655,
"preview": "import Foundation\n\n/// A LocalizedError that represents a non-zero exit code from running aria2c.\nstruct Aria2CError: Lo"
},
{
"path": "Xcodes/Backend/AvailableXcode.swift",
"chars": 1402,
"preview": "import Foundation\nimport Version\nimport XcodesKit\n\n/// A version of Xcode that's available for installation\npublic struc"
},
{
"path": "Xcodes/Backend/Bundle+InfoPlistValues.swift",
"chars": 443,
"preview": "import Foundation\n\nextension Bundle {\n var bundleName: String? {\n infoDictionary?[\"CFBundleName\"] as? String\n "
},
{
"path": "Xcodes/Backend/Collection+.swift",
"chars": 403,
"preview": "//\n// Collection+.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2022-04-11.\n// Copyright © 2022 Robots and Pencils"
},
{
"path": "Xcodes/Backend/Configure.swift",
"chars": 170,
"preview": "public func configure<Subject>(_ subject: Subject, configuration: (inout Subject) -> Void) -> Subject {\n var copy = s"
},
{
"path": "Xcodes/Backend/DataSource.swift",
"chars": 482,
"preview": "import Foundation\n\npublic enum DataSource: String, CaseIterable, Identifiable, CustomStringConvertible {\n case apple\n"
},
{
"path": "Xcodes/Backend/DateFormatter+.swift",
"chars": 642,
"preview": "import Foundation\n\nextension DateFormatter {\n /// Date format used in JSON returned from `URL.downloads`\n static l"
},
{
"path": "Xcodes/Backend/Downloader.swift",
"chars": 420,
"preview": "import Foundation\nimport Path\n\npublic enum Downloader: String, CaseIterable, Identifiable, CustomStringConvertible {\n "
},
{
"path": "Xcodes/Backend/Downloads.swift",
"chars": 558,
"preview": "import Foundation\nimport Path\nimport Version\n\nstruct Downloads: Codable {\n let resultCode: Int\n let resultsString:"
},
{
"path": "Xcodes/Backend/Entry+.swift",
"chars": 713,
"preview": "import Foundation\nimport Path\n\nextension Path {\n static func isAppBundle(path: Path) -> Bool {\n path.isDirecto"
},
{
"path": "Xcodes/Backend/Environment.swift",
"chars": 19908,
"preview": "import Combine\nimport Foundation\nimport Path\nimport AppleAPI\nimport KeychainAccess\nimport XcodesKit\n/**\n Lightweight dep"
},
{
"path": "Xcodes/Backend/FileError.swift",
"chars": 503,
"preview": "//\n// FileError.swift\n// Xcodes\n//\n// Created by Leon Wolf on 06.10.22.\n// Copyright © 2022 Robots and Pencils. All "
},
{
"path": "Xcodes/Backend/FileManager+.swift",
"chars": 711,
"preview": "import Foundation\n\nextension FileManager {\n /**\n Moves an item to the trash.\n \n This implementation exist"
},
{
"path": "Xcodes/Backend/FocusedValues.swift",
"chars": 331,
"preview": "import SwiftUI\n\n// MARK: - FocusedXcodeKey\n\nstruct FocusedXcodeKey : FocusedValueKey {\n typealias Value = SelectedXco"
},
{
"path": "Xcodes/Backend/Foundation.swift",
"chars": 824,
"preview": "import Foundation\n\npublic extension BidirectionalCollection where Element: Equatable {\n func suffix(fromLast delimite"
},
{
"path": "Xcodes/Backend/Hardware.swift",
"chars": 946,
"preview": "import Foundation\n\n\nstruct Hardware {\n \n ///\n /// Determines the architecture of the Mac on which we're runnin"
},
{
"path": "Xcodes/Backend/HelperClient.swift",
"chars": 15222,
"preview": "import Combine\nimport Foundation\nimport os.log\nimport ServiceManagement\n\nfinal class HelperClient {\n private var conn"
},
{
"path": "Xcodes/Backend/HelperInstallState.swift",
"chars": 123,
"preview": "import Foundation\n\npublic enum HelperInstallState: Equatable {\n case unknown\n case notInstalled\n case installed"
},
{
"path": "Xcodes/Backend/InstalledXcode.swift",
"chars": 3103,
"preview": "import Foundation\nimport Version\nimport Path\nimport XcodesKit\n\n/// A version of Xcode that's already installed\npublic st"
},
{
"path": "Xcodes/Backend/IsTesting.swift",
"chars": 74,
"preview": "import Foundation\n\nlet isTesting = NSClassFromString(\"XCTestCase\") != nil\n"
},
{
"path": "Xcodes/Backend/NotificationManager.swift",
"chars": 3852,
"preview": "import Foundation\nimport os.log\nimport UserNotifications\n\n/// Representation of the 3 states of the Notifications permis"
},
{
"path": "Xcodes/Backend/Optional+IsNotNil.swift",
"chars": 328,
"preview": "import Foundation\n\nextension Optional {\n /// Note that this is lossy when setting, so you can really only set it to n"
},
{
"path": "Xcodes/Backend/Path+.swift",
"chars": 1514,
"preview": "import Path\nimport Foundation\n\nextension Path {\n static let defaultXcodesApplicationSupport = Path.applicationSupport"
},
{
"path": "Xcodes/Backend/Process.swift",
"chars": 3235,
"preview": "import Combine\nimport Foundation\nimport os.log\nimport Path\nimport XcodesKit\n\npublic typealias ProcessOutput = (status: I"
},
{
"path": "Xcodes/Backend/Progress+.swift",
"chars": 4702,
"preview": "import os.log\nimport Foundation\n\nextension Progress {\n var xcodesLocalizedDescription: String {\n return locali"
},
{
"path": "Xcodes/Backend/Publisher+Resumable.swift",
"chars": 1656,
"preview": "import Combine\nimport Foundation\n\n/// Attempt and retry a task that fails with resume data up to `maximumRetryCount` tim"
},
{
"path": "Xcodes/Backend/SDKs+Xcode.swift",
"chars": 1580,
"preview": "//\n// SDKs+Xcode.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2023-06-05.\n// Copyright © 2023 Robots and Pencils."
},
{
"path": "Xcodes/Backend/SelectedActionType.swift",
"chars": 849,
"preview": "//\n// SelectedActionType.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2022-07-24.\n// Copyright © 2022 Robots and "
},
{
"path": "Xcodes/Backend/SelectedXcode.swift",
"chars": 1576,
"preview": "import Foundation\n\n/// As part of the unexpected way we have to use focusedValue in XcodesApp, we need to provide an `Op"
},
{
"path": "Xcodes/Backend/URLRequest+Apple.swift",
"chars": 1603,
"preview": "import Foundation\n\nextension URL {\n static let download = URL(string: \"https://developer.apple.com/download\")!\n st"
},
{
"path": "Xcodes/Backend/URLSession+DownloadTaskPublisher.swift",
"chars": 2749,
"preview": "import Combine\nimport Foundation\n\nextension URLSession {\n /**\n - Parameter convertible: A URL or URLRequest.\n "
},
{
"path": "Xcodes/Backend/Version+.swift",
"chars": 1703,
"preview": "import Version\n\npublic extension Version {\n /// Determines if two Xcode versions should be treated equivalently. This"
},
{
"path": "Xcodes/Backend/Version+Xcode.swift",
"chars": 2868,
"preview": "import Foundation\nimport Version\n\npublic extension Version {\n /**\n E.g.:\n Xcode 10.2 Beta 4\n Xcode 10.2 G"
},
{
"path": "Xcodes/Backend/Version+XcodeReleases.swift",
"chars": 1835,
"preview": "import Version\nimport XcodesKit\n\nextension Version {\n /// Initialize a Version from an XcodeReleases' XCModel.Xcode\n "
},
{
"path": "Xcodes/Backend/Xcode.swift",
"chars": 2573,
"preview": "import AppKit\nimport Foundation\nimport Version\nimport Path\nimport XcodesKit\n\npublic struct XcodeID: Codable, Hashable, I"
},
{
"path": "Xcodes/Backend/XcodeCommands.swift",
"chars": 9406,
"preview": "import SwiftUI\nimport XcodesKit\n\n// MARK: - CommandMenu\n\nstruct XcodeCommands: Commands {\n // CommandMenus don't part"
},
{
"path": "Xcodes/Backend/XcodeInstallState.swift",
"chars": 584,
"preview": "import Foundation\nimport Path\nimport XcodesKit\n\nenum XcodeInstallState: Equatable {\n case notInstalled\n case insta"
},
{
"path": "Xcodes/Frontend/About/AboutView.swift",
"chars": 2621,
"preview": "import SwiftUI\n\nstruct AboutView: View {\n let showAcknowledgementsWindow: () -> Void\n @SwiftUI.Environment(\\.openU"
},
{
"path": "Xcodes/Frontend/About/AcknowledgementsView.swift",
"chars": 595,
"preview": "import SwiftUI\n\nstruct AcknowledgmentsView: View {\n \n var body: some View {\n ScrollingTextView(\n "
},
{
"path": "Xcodes/Frontend/About/ScrollingTextView.swift",
"chars": 744,
"preview": "import SwiftUI\n\nstruct ScrollingTextView: NSViewRepresentable {\n typealias NSViewType = NSScrollView\n\n let attribu"
},
{
"path": "Xcodes/Frontend/Common/NavigationSplitViewWrapper.swift",
"chars": 815,
"preview": "//\n// NavigationSplitViewWrapper.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2023-12-12.\n//\n\nimport SwiftUI\n\nstru"
},
{
"path": "Xcodes/Frontend/Common/ObservingProgressIndicator.swift",
"chars": 3809,
"preview": "import Combine\nimport SwiftUI\n\n/// A ProgressIndicator that reflects the state of a Progress object.\n/// This functional"
},
{
"path": "Xcodes/Frontend/Common/ProgressButton.swift",
"chars": 1250,
"preview": "//\n// ProgressButton.swift\n// Xcodes\n//\n// Created by Chad Sykes on 2020-12-27.\n// Copyright © 2020 Robots and Penci"
},
{
"path": "Xcodes/Frontend/Common/ProgressIndicator.swift",
"chars": 1228,
"preview": "import SwiftUI\nimport AppKit\n\n/// You probably want ProgressView unless you need more of NSProgressIndicator's API, whic"
},
{
"path": "Xcodes/Frontend/Common/TagView.swift",
"chars": 447,
"preview": "//\n// TagView.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2025-06-25.//\n\n\nimport SwiftUI\n\nstruct TagView: View {\n"
},
{
"path": "Xcodes/Frontend/Common/TrailingIconLabelStyle.swift",
"chars": 485,
"preview": "//\n// TrailingIconLabelStyle.swift\n// Xcodes\n//\n// Created by Daniel Chick on 3/11/24.\n// Copyright © 2024 Robots an"
},
{
"path": "Xcodes/Frontend/Common/XcodesAlert.swift",
"chars": 1115,
"preview": "import Foundation\nimport XcodesKit\n\nenum XcodesAlert: Identifiable {\n case cancelInstall(xcode: Xcode)\n case cance"
},
{
"path": "Xcodes/Frontend/Common/XcodesSheet.swift",
"chars": 1231,
"preview": "import Foundation\nimport AppleAPI\n\nenum XcodesSheet: Identifiable {\n case signIn\n case twoFactor(SecondFactorData)"
},
{
"path": "Xcodes/Frontend/InfoPane/CompatibilityView.swift",
"chars": 1307,
"preview": "//\n// CompatibilityView.swift\n// Xcodes\n//\n// Created by Duong Thai on 13/10/2023.\n// Copyright © 2023 Robots and Pe"
},
{
"path": "Xcodes/Frontend/InfoPane/CompilersView.swift",
"chars": 1671,
"preview": "//\n// CompilersView.swift\n// Xcodes\n//\n// Created by Duong Thai on 13/10/2023.\n// Copyright © 2023 Robots and Pencil"
},
{
"path": "Xcodes/Frontend/InfoPane/CornerRadiusModifier.swift",
"chars": 772,
"preview": "//\n// CornerRadiusModifier.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2023-12-19.\n//\n\nimport Foundation\nimport S"
},
{
"path": "Xcodes/Frontend/InfoPane/IconView.swift",
"chars": 1261,
"preview": "//\n// IconView.swift\n// Xcodes\n//\n// Created by Duong Thai on 11/10/2023.\n// Copyright © 2023 Robots and Pencils. Al"
},
{
"path": "Xcodes/Frontend/InfoPane/IdenticalBuildView.swift",
"chars": 1758,
"preview": "//\n// IdenticalBuildView.swift\n// Xcodes\n//\n// Created by Duong Thai on 11/10/2023.\n// Copyright © 2023 Robots and P"
},
{
"path": "Xcodes/Frontend/InfoPane/InfoPane.swift",
"chars": 7661,
"preview": "import AppKit\nimport XcodesKit\nimport Path\nimport SwiftUI\nimport Version\n\nstruct InfoPane: View {\n let xcode: Xcode\n "
},
{
"path": "Xcodes/Frontend/InfoPane/InfoPaneControls.swift",
"chars": 1818,
"preview": "//\n// InfoPaneControls.swift\n// Xcodes\n//\n// Created by Duong Thai on 14/10/2023.\n// Copyright © 2023 Robots and Pen"
},
{
"path": "Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift",
"chars": 1426,
"preview": "import SwiftUI\nimport XcodesKit\n\nstruct InstallationStepDetailView: View {\n let installationStep: XcodeInstallationSt"
},
{
"path": "Xcodes/Frontend/InfoPane/InstalledStateButtons.swift",
"chars": 2028,
"preview": "//\n// InstallingStateButtons.swift\n// Xcodes\n//\n// Created by Duong Thai on 13/10/2023.\n// Copyright © 2023 Robots a"
},
{
"path": "Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift",
"chars": 1226,
"preview": "//\n// NotInstalledStateButtonsView.swift\n// Xcodes\n//\n// Created by Duong Thai on 13/10/2023.\n// Copyright © 2023 Ro"
},
{
"path": "Xcodes/Frontend/InfoPane/PlatformsView.swift",
"chars": 4932,
"preview": "//\n// PlatformsView.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2023-12-18.\n//\n\nimport Foundation\nimport SwiftUI\n"
},
{
"path": "Xcodes/Frontend/InfoPane/ReleaseDateView.swift",
"chars": 1184,
"preview": "//\n// ReleaseDateView.swift\n// Xcodes\n//\n// Created by Duong Thai on 11/10/2023.\n// Copyright © 2023 Robots and Penc"
},
{
"path": "Xcodes/Frontend/InfoPane/ReleaseNotesView.swift",
"chars": 898,
"preview": "//\n// ReleaseNotesView.swift\n// Xcodes\n//\n// Created by Duong Thai on 13/10/2023.\n// Copyright © 2023 Robots and Pen"
},
{
"path": "Xcodes/Frontend/InfoPane/RuntimeInstallationStepDetailView.swift",
"chars": 1710,
"preview": "//\n// RuntimeInstallationStepDetailView.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2023-11-23.\n// Copyright © 2"
},
{
"path": "Xcodes/Frontend/InfoPane/SDKsView.swift",
"chars": 1816,
"preview": "//\n// SDKsView.swift\n// Xcodes\n//\n// Created by Duong Thai on 13/10/2023.\n// Copyright © 2023 Robots and Pencils. Al"
},
{
"path": "Xcodes/Frontend/InfoPane/UnselectedView.swift",
"chars": 457,
"preview": "//\n// UnselectedView.swift\n// Xcodes\n//\n// Created by Duong Thai on 13/10/2023.\n// Copyright © 2023 Robots and Penci"
},
{
"path": "Xcodes/Frontend/MainWindow.swift",
"chars": 12708,
"preview": "import ErrorHandling\nimport SwiftUI\nimport XcodesKit\nimport Path\nimport Version\n\nstruct MainWindow: View {\n @Environm"
},
{
"path": "Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift",
"chars": 8059,
"preview": "import AppleAPI\nimport SwiftUI\nimport Path\n\nstruct AdvancedPreferencePane: View {\n @EnvironmentObject var appState: A"
},
{
"path": "Xcodes/Frontend/Preferences/DownloadPreferencePane.swift",
"chars": 2242,
"preview": "import AppleAPI\nimport SwiftUI\n\nstruct DownloadPreferencePane: View {\n @EnvironmentObject var appState: AppState\n "
},
{
"path": "Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift",
"chars": 1165,
"preview": "import AppleAPI\nimport Path\nimport SwiftUI\n\nstruct ExperimentsPreferencePane: View {\n @EnvironmentObject var appState"
},
{
"path": "Xcodes/Frontend/Preferences/GeneralPreferencePane.swift",
"chars": 1276,
"preview": "import AppleAPI\nimport SwiftUI\n\nstruct GeneralPreferencePane: View {\n @EnvironmentObject var appState: AppState\n \n "
},
{
"path": "Xcodes/Frontend/Preferences/NotificationsView.swift",
"chars": 1200,
"preview": "import SwiftUI\n\nstruct NotificationsView: View {\n @EnvironmentObject var appState: AppState\n \n var body: some V"
},
{
"path": "Xcodes/Frontend/Preferences/PlatformsListView.swift",
"chars": 2805,
"preview": "//\n// PlatformsListView.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2023-12-20.\n//\n\nimport Foundation\nimport Swif"
},
{
"path": "Xcodes/Frontend/Preferences/PreferencesView.swift",
"chars": 1449,
"preview": "import SwiftUI\n\nstruct PreferencesView: View {\n private enum Tabs: Hashable {\n case general, updates, advanced"
},
{
"path": "Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift",
"chars": 5911,
"preview": "import AppleAPI\nimport Sparkle\nimport SwiftUI\n\nstruct UpdatesPreferencePane: View {\n @EnvironmentObject var updater: "
},
{
"path": "Xcodes/Frontend/SignIn/AttributedText.swift",
"chars": 2886,
"preview": "import SwiftUI\n\n/// A text view that supports NSAttributedStrings, based on NSTextView.\npublic struct AttributedText: Vi"
},
{
"path": "Xcodes/Frontend/SignIn/NSAttributedString+.swift",
"chars": 1233,
"preview": "import Foundation\n\npublic extension NSAttributedString {\n func addingAttribute(_ attribute: NSAttributedString.Key, v"
},
{
"path": "Xcodes/Frontend/SignIn/PinCodeTextView.swift",
"chars": 7476,
"preview": "import Cocoa\nimport SwiftUI\n\nstruct PinCodeTextField: NSViewRepresentable {\n typealias NSViewType = PinCodeTextView\n\n"
},
{
"path": "Xcodes/Frontend/SignIn/SignIn2FAView.swift",
"chars": 2155,
"preview": "import SwiftUI\nimport AppleAPI\n\nstruct SignIn2FAView: View {\n @EnvironmentObject var appState: AppState\n @Binding "
},
{
"path": "Xcodes/Frontend/SignIn/SignInCredentialsView.swift",
"chars": 2390,
"preview": "import SwiftUI\n\nstruct SignInCredentialsView: View {\n private enum FocusedField {\n case username, password\n "
},
{
"path": "Xcodes/Frontend/SignIn/SignInPhoneListView.swift",
"chars": 2893,
"preview": "import AppleAPI\nimport SwiftUI\n\nstruct SignInPhoneListView: View {\n @EnvironmentObject var appState: AppState\n @Bi"
},
{
"path": "Xcodes/Frontend/SignIn/SignInSMSView.swift",
"chars": 2219,
"preview": "import SwiftUI\nimport AppleAPI\n\nstruct SignInSMSView: View {\n @EnvironmentObject var appState: AppState\n @Binding "
},
{
"path": "Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift",
"chars": 2406,
"preview": "//\n// SignInSecurityKeyPin.swift\n// Xcodes\n//\n// Created by Kino on 2024-09-26.\n// Copyright © 2024 Robots and Penci"
},
{
"path": "Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift",
"chars": 1573,
"preview": "//\n// SignInSecurityKeyPin.swift\n// Xcodes\n//\n// Created by Kino on 2024-09-26.\n// Copyright © 2024 Robots and Penci"
},
{
"path": "Xcodes/Frontend/SignIn/SignedInView.swift",
"chars": 559,
"preview": "import SwiftUI\n\nstruct SignedInView: View {\n @EnvironmentObject var appState: AppState\n\n private var username: Str"
},
{
"path": "Xcodes/Frontend/View+Conditional.swift",
"chars": 232,
"preview": "import SwiftUI\n\nextension View {\n @ViewBuilder\n func `if`<Other: View>(_ predicate: Bool, then: (Self) -> Other) -"
},
{
"path": "Xcodes/Frontend/View+IsHidden.swift",
"chars": 478,
"preview": "import SwiftUI\n\nextension View {\n @ViewBuilder\n func isHidden(_ isHidden: Bool) -> some View {\n if isHidden"
},
{
"path": "Xcodes/Frontend/XcodeList/AppStoreButtonStyle.swift",
"chars": 5734,
"preview": "import SwiftUI\n\nstruct AppStoreButtonStyle: ButtonStyle {\n var primary: Bool\n var highlighted: Bool\n \n priva"
},
{
"path": "Xcodes/Frontend/XcodeList/BottomStatusBar.swift",
"chars": 2724,
"preview": "//\n// BottomStatusBar.swift\n// Xcodes\n//\n// Created by Matt Kiazyk on 2022-06-03.\n// Copyright © 2022 Robots and Pen"
},
{
"path": "Xcodes/Frontend/XcodeList/InstallationStepRowView.swift",
"chars": 4083,
"preview": "import SwiftUI\nimport XcodesKit\n\nstruct InstallationStepRowView: View {\n let installationStep: XcodeInstallationStep\n"
},
{
"path": "Xcodes/Frontend/XcodeList/MainToolbar.swift",
"chars": 3214,
"preview": "import SwiftUI\n\nstruct MainToolbarModifier: ViewModifier {\n @EnvironmentObject var appState: AppState\n @Binding va"
},
{
"path": "Xcodes/Frontend/XcodeList/Tag.swift",
"chars": 415,
"preview": "import SwiftUI\n\nstruct Tag: View {\n var text: String\n var body: some View {\n Text(text)\n .foregr"
},
{
"path": "Xcodes/Frontend/XcodeList/XcodeListCategory.swift",
"chars": 961,
"preview": "import Foundation\nimport XcodesKit\n\nenum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConvertible "
},
{
"path": "Xcodes/Frontend/XcodeList/XcodeListView.swift",
"chars": 5254,
"preview": "import Path\nimport SwiftUI\nimport Version\n\nstruct XcodeListView: View {\n @EnvironmentObject var appState: AppState\n "
},
{
"path": "Xcodes/Frontend/XcodeList/XcodeListViewRow.swift",
"chars": 6529,
"preview": "import Path\nimport SwiftUI\nimport Version\n\nstruct XcodeListViewRow: View {\n let xcode: Xcode\n let selected: Bool\n "
},
{
"path": "Xcodes/Preview Content/Preview Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Xcodes/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1357,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"xcodes.app-icon16.png\",\n \"idiom\" : \"mac\",\n \"scale\" : \"1x\",\n \"s"
},
{
"path": "Xcodes/Resources/Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Xcodes/Resources/Assets.xcassets/Icons/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Xcodes/Resources/Assets.xcassets/install.imageset/Contents.json",
"chars": 226,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"install.pdf\",\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n \"author"
},
{
"path": "Xcodes/Resources/Assets.xcassets/xcode-beta.imageset/Contents.json",
"chars": 155,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Image.png\",\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n \"author\" "
},
{
"path": "Xcodes/Resources/Assets.xcassets/xcode.imageset/Contents.json",
"chars": 155,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"xcode.png\",\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n \"author\" "
},
{
"path": "Xcodes/Resources/Info.plist",
"chars": 1769,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Xcodes/Resources/Licenses.rtf",
"chars": 87268,
"preview": "{\\rtf1\\ansi\\ansicpg1252\\cocoartf2865\n\\cocoatextscaling0\\cocoaplatform0{\\fonttbl\\f0\\fnil\\fcharset0 .SFNS-Regular;}\n{\\colo"
},
{
"path": "Xcodes/Resources/Localizable.xcstrings",
"chars": 688072,
"preview": "{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"\" : {\n \"localizations\" : {\n \"ar\" : {\n \"stringUnit"
},
{
"path": "Xcodes/Resources/Xcodes.entitlements",
"chars": 181,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Xcodes/Resources/XcodesIcon.icon/icon.json",
"chars": 989,
"preview": "{\n \"fill\" : {\n \"automatic-gradient\" : \"extended-srgb:0.00000,0.47843,1.00000,1.00000\"\n },\n \"groups\" : [\n {\n "
},
{
"path": "Xcodes/Resources/XcodesTest.entitlements",
"chars": 181,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Xcodes/Resources/aria2c.LICENSE",
"chars": 18002,
"preview": "\t\t GNU GENERAL PUBLIC LICENSE\n\t\t Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc."
},
{
"path": "Xcodes/XcodesApp.swift",
"chars": 7122,
"preview": "import AppKit\nimport Sparkle\nimport SwiftUI\n\n@main\nstruct XcodesApp: App {\n @NSApplicationDelegateAdaptor(AppDelegate"
},
{
"path": "Xcodes/XcodesKit/.gitignore",
"chars": 165,
"preview": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\nDerivedData/\n.swiftpm/config/registries.json\n.swiftpm/xcode/package"
},
{
"path": "Xcodes/XcodesKit/Package.swift",
"chars": 1332,
"preview": "// swift-tools-version: 5.7\n// The swift-tools-version declares the minimum version of Swift required to build this pack"
},
{
"path": "Xcodes/XcodesKit/README.md",
"chars": 44,
"preview": "# XcodesKit\n\nA description of this package.\n"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Foundation.swift",
"chars": 456,
"preview": "import Foundation\n\nextension NSRegularExpression {\n func firstString(in string: String, options: NSRegularExpression."
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Extensions/Logger.swift",
"chars": 384,
"preview": "import Foundation\nimport os.log\n\nextension Logger {\n private static var subsystem = Bundle.main.bundleIdentifier!\n\n "
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift",
"chars": 1174,
"preview": "//\n// CoreSimulatorImage.swift\n// \n//\n// Created by Matt Kiazyk on 2023-01-08.\n//\n\nimport Foundation\n\npublic struct C"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift",
"chars": 661,
"preview": "//\n// RuntimeInstallState.swift\n//\n//\n// Created by Matt Kiazyk on 2023-11-23.\n//\n\nimport Foundation\nimport Path\n\npubl"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift",
"chars": 914,
"preview": "//\n// RuntimeInstallationStep.swift\n// \n//\n// Created by Matt Kiazyk on 2023-11-23.\n//\n\nimport Foundation\n\npublic enu"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift",
"chars": 5922,
"preview": "import Foundation\n\npublic struct DownloadableRuntimesResponse: Codable {\n public let sdkToSimulatorMappings: [SDKToSi"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallState.swift",
"chars": 648,
"preview": "//\n// InstallState.swift\n// \n//\n// Created by Matt Kiazyk on 2023-06-06.\n//\n\nimport Foundation\nimport Path\n\npublic en"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeInstallationStep.swift",
"chars": 1784,
"preview": "//\n// InstallationStep.swift\n// \n//\n// Created by Matt Kiazyk on 2023-06-06.\n//\n\nimport Foundation\n\n// A numbered ste"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift",
"chars": 1624,
"preview": "//\n// Architecture.swift\n// XcodesKit\n//\n// Created by Matt Kiazyk on 2025-08-23.\n//\n\nimport Foundation\n\n/// The name"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Checksums.swift",
"chars": 319,
"preview": "//\n// Checksums.swift\n// xcodereleases\n//\n// Created by Xcode Releases on 9/17/20.\n// Copyright © 2020 Xcode Release"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Compilers.swift",
"chars": 1250,
"preview": "//\n// Compiler.swift\n// xcodereleases\n//\n// Created by Xcode Releases on 4/4/18.\n// Copyright © 2018 Xcode Releases."
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Link.swift",
"chars": 800,
"preview": "//\n// Link.swift\n// xcodereleases\n//\n// Created by Xcode Releases on 4/5/18.\n// Copyright © 2018 Xcode Releases. All"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Release.swift",
"chars": 1921,
"preview": "//\n// Release.swift\n// xcodereleases\n//\n// Created by Xcode Releases on 4/4/18.\n// Copyright © 2018 Xcode Releases. "
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/SDKs.swift",
"chars": 2494,
"preview": "//\n// SDKs.swift\n// xcodereleases\n//\n// Created by Xcode Releases on 4/4/18.\n// Copyright © 2018 Xcode Releases. All"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeRelease.swift",
"chars": 1033,
"preview": "//\n// Xcode.swift\n// xcodereleases\n//\n// Created by Xcode Releases on 4/3/18.\n// Copyright © 2018 Xcode Releases. Al"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeVersion.swift",
"chars": 679,
"preview": "//\n// Version.swift\n// xcodereleases\n//\n// Created by Xcode Releases on 4/4/18.\n// Copyright © 2018 Xcode Releases. "
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/YMD.swift",
"chars": 512,
"preview": "//\n// YMD.swift\n// xcodereleases\n//\n// Created by Xcode Releases on 4/4/18.\n// Copyright © 2018 Xcode Releases. All "
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift",
"chars": 4382,
"preview": "import Foundation\nimport AsyncNetworkService\nimport Path\n\nextension URL {\n static let downloadableRuntimes = URL(stri"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift",
"chars": 2816,
"preview": "import Foundation\nimport Path\nimport os.log\n\npublic typealias ProcessOutput = (status: Int32, out: String, err: String)\n"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift",
"chars": 1612,
"preview": "import Foundation\nimport Path\n\npublic struct XcodesShell {\n public var installedRuntimes: () async throws -> ProcessO"
},
{
"path": "Xcodes/XcodesKit/Sources/XcodesKit/XcodesKitEnvironment.swift",
"chars": 140,
"preview": "import Foundation\n\npublic struct XcodesKitEnvironment {\n public var shell = XcodesShell()\n}\n\npublic var Current = Xco"
},
{
"path": "Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift",
"chars": 346,
"preview": "import XCTest\n@testable import XcodesKit\n\nfinal class XcodesKitTests: XCTestCase {\n func testExample() throws {\n "
},
{
"path": "Xcodes.xcodeproj/project.pbxproj",
"chars": 101602,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "Xcodes.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 135,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:\">\n </FileRef"
},
{
"path": "Xcodes.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
"chars": 4143,
"preview": "{\n \"object\": {\n \"pins\": [\n {\n \"package\": \"AsyncNetworkService\",\n \"repositoryURL\": \"https://github"
},
{
"path": "Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme",
"chars": 4463,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1220\"\n version = \"1.7\">\n <BuildAction\n "
},
{
"path": "Xcodes.xcodeproj/xcshareddata/xcschemes/com.robotsandpencils.XcodesApp.Helper.xcscheme",
"chars": 2999,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1500\"\n version = \"1.7\">\n <BuildAction\n "
},
{
"path": "XcodesTests/AppStateTests.swift",
"chars": 15184,
"preview": "import AppleAPI\nimport Combine\nimport CombineExpectations\nimport Path\nimport Version\nimport XCTest\nimport XcodesKit\n\n@te"
},
{
"path": "XcodesTests/AppStateUpdateTests.swift",
"chars": 18418,
"preview": "import Path\nimport CryptoKit\nimport Version\n@testable import Xcodes\nimport XCTest\nimport CommonCrypto\nimport BigNum\nimpo"
},
{
"path": "XcodesTests/Bundle+XcodesTests.swift",
"chars": 153,
"preview": "import Foundation\n\nextension Bundle {\n static var xcodesTests: Bundle {\n Bundle(for: BundleMember.self)\n }\n"
},
{
"path": "XcodesTests/Environment+Mock.swift",
"chars": 4335,
"preview": "import Combine\nimport Foundation\n@testable import Xcodes\n\nextension Xcodes.Environment {\n static var mock = Xcodes.En"
},
{
"path": "XcodesTests/Fixtures/Stub-0.0.0.Info.plist",
"chars": 319,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "XcodesTests/Fixtures/Stub-version.plist",
"chars": 245,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "XcodesTests/Info.plist",
"chars": 727,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "com.xcodesorg.xcodesapp.Helper/AuditTokenHack.h",
"chars": 322,
"preview": "// From https://github.com/securing/SimpleXPCApp/\n\n#import <Foundation/Foundation.h>\n\n@interface NSXPCConnection(Private"
},
{
"path": "com.xcodesorg.xcodesapp.Helper/AuditTokenHack.m",
"chars": 330,
"preview": "// From https://github.com/securing/SimpleXPCApp/\n\n#import \"AuditTokenHack.h\"\n\n@implementation AuditTokenHack\n\n+ (NSData"
},
{
"path": "com.xcodesorg.xcodesapp.Helper/ConnectionVerifier.swift",
"chars": 5255,
"preview": "// From https://github.com/securing/SimpleXPCApp/\n\nimport Foundation\nimport os.log\n\nclass ConnectionVerifier {\n \n "
}
]
// ... and 13 more files (download for full content)
About this extraction
This page contains the full source code of the XcodesOrg/XcodesApp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 213 files (1.4 MB), approximately 362.4k tokens, and a symbol index with 3 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.