Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e1cc775
Support password manager autofill during Apple ID sign-in
Aug 5, 2026
c890879
fix: duplicated major version in install notification title
YuriNachos Aug 7, 2026
5232086
fix: notify on new Xcode version identity, not array-count growth
YuriNachos Aug 8, 2026
a973b23
Fix platform architecture picker visibility
MattKiazyk Sep 8, 2026
ce7f93a
Merge pull request #842 from YuriNachos/YuriNachos/w9-xcodes
MattKiazyk Sep 8, 2026
5de4c0f
Merge pull request #840 from YuriNachos/fix/install-notification-title
MattKiazyk Sep 8, 2026
8f71a79
Merge pull request #849 from XcodesOrg/fix/platform-architecture-picker
MattKiazyk Sep 12, 2026
b6d68e7
chore: update xcodesKit and LoginKit to latest. minor fixes
MattKiazyk Sep 12, 2026
8c31527
Merge pull request #851 from XcodesOrg/updateXcodesKit+Login
MattKiazyk Sep 12, 2026
498efad
Add privileged file-operation XPC methods to the helper
abiligiri Aug 29, 2026
841dea6
Route Xcode select/rename/symlink/uninstall file ops through the priv…
abiligiri Aug 29, 2026
b010e58
Add localized label for privileged-helper file operations toggle
abiligiri Aug 29, 2026
d3059fa
Mark createSymbolicLink test async to match new async signature
abiligiri Aug 29, 2026
b090197
Route install-time Xcode.app move through privileged helper
abiligiri Aug 29, 2026
6539d8c
Show a spinner while an Xcode uninstall is in progress
abiligiri Aug 29, 2026
9335041
Add missing translations for new Uninstalling/UsePrivilegedHelperForF…
abiligiri Aug 30, 2026
1f72ae8
Merge pull request #847 from abiligiri/feature/privileged-helper-file…
MattKiazyk Sep 14, 2026
014b29e
Merge branch 'main' into signin-autofill
MattKiazyk Sep 14, 2026
1f45dfb
Merge pull request #839 from zeromhz/signin-autofill
MattKiazyk Sep 14, 2026
aaf917f
better 401 error messages
MattKiazyk Sep 14, 2026
7f79cc9
v4.1 - v2.1 Helper tool
MattKiazyk Sep 14, 2026
f9d50b9
v 4.1.1b41
MattKiazyk Sep 14, 2026
0bc7c5b
chore: merged upstream 4.1.1
jacobcxdev Sep 18, 2026
54a7a33
fix: handled wrapped authentication failures
jacobcxdev Sep 18, 2026
2b74b28
fix: improved compact window layouts
jacobcxdev Sep 18, 2026
a8cd778
fix: kept grouped sidebar labels readable
jacobcxdev Sep 18, 2026
1e7d7f8
fix: clarified retry status inference
jacobcxdev Sep 18, 2026
068e458
test: used real uninstall bundle fixtures
jacobcxdev Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions HelperXPCShared/FileOperations.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Foundation
import os.log

enum FileOperations {
private static let subsystem = Bundle.main.bundleIdentifier!
static let fileOperations = Logger(subsystem: subsystem, category: "fileOperations")

static func moveApp(at source: String, to destination: String, completion: @escaping ((any Error)?) -> Void) {
do {
guard (source as NSString).isAbsolutePath,
URL(fileURLWithPath: source).hasDirectoryPath else { throw XPCDelegateError(.invalidSourcePath)}

guard (destination as NSString).isAbsolutePath,
URL(fileURLWithPath: destination).deletingLastPathComponent().hasDirectoryPath else { throw
XPCDelegateError(.invalidDestinationPath)}

try FileManager.default.moveItem(at: URL(fileURLWithPath: source), to: URL(fileURLWithPath: destination))
completion(nil)
} catch {
completion(error)
}
}

// does an Xcode.app file exist?
static func createSymbolicLink(source: String, destination: String, completion: @escaping ((any Error)?) -> Void) {
do {
guard (source as NSString).isAbsolutePath else { throw XPCDelegateError(.invalidSourcePath) }
guard (destination as NSString).isAbsolutePath else { throw XPCDelegateError(.invalidDestinationPath) }

let attributes: [FileAttributeKey: Any]?
do {
// Read the link itself, including links whose target no longer exists.
attributes = try FileManager.default.attributesOfItem(atPath: destination)
} catch CocoaError.fileReadNoSuchFile {
attributes = nil
}

if let attributes {
if attributes[.type] as? FileAttributeType == FileAttributeType.typeSymbolicLink {
try FileManager.default.removeItem(atPath: destination)
Self.fileOperations.info("Successfully deleted old symlink")
} else {
throw XPCDelegateError(.destinationIsNotASymbolicLink)
}
}

try FileManager.default.createSymbolicLink(atPath: destination, withDestinationPath: source)
Self.fileOperations.info("Successfully created symbolic link with \(destination)")
completion(nil)
} catch {
completion(error)
}
}

static func rename(source: String, destination: String, completion: @escaping ((any Error)?) -> Void) {
do {
guard (source as NSString).isAbsolutePath else { throw XPCDelegateError(.invalidSourcePath) }
guard (destination as NSString).isAbsolutePath else { throw XPCDelegateError(.invalidDestinationPath) }
try FileManager.default.moveItem(at: URL(fileURLWithPath: source), to: URL(fileURLWithPath: destination))
completion(nil)
} catch {
completion(error)
}
}

static func remove(path: String, completion: @escaping ((any Error)?) -> Void) {
do {
guard (path as NSString).isAbsolutePath,
URL(fileURLWithPath: path).standardizedFileURL.path != "/" else { throw XPCDelegateError(.invalidSourcePath) }
try FileManager.default.removeItem(atPath: path)
completion(nil)
} catch {
completion(error)
}
}
}
50 changes: 50 additions & 0 deletions HelperXPCShared/HelperXPCShared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,54 @@ protocol HelperXPCProtocol: Sendable {
func addStaffToDevelopersGroup(completion: @escaping (Error?) -> Void)
func acceptXcodeLicense(absoluteXcodePath: String, completion: @escaping (Error?) -> Void)
func runFirstLaunch(absoluteXcodePath: String, completion: @escaping (Error?) -> Void)
func moveApp(at source: String, to destination: String, completion: @escaping (Error?) -> Void)
func createSymbolicLink(source: String, destination: String, completion: @escaping (Error?) -> Void)
func rename(source: String, destination: String, completion: @escaping (Error?) -> Void)
func remove(path: String, completion: @escaping (Error?) -> Void)
}

struct XPCDelegateError: CustomNSError {
enum Code: Int {
case invalidXcodePath
case invalidSourcePath
case invalidDestinationPath
case destinationIsNotASymbolicLink
}

let code: Code

init(_ code: Code) {
self.code = code
}

// MARK: - CustomNSError

static var errorDomain: String { "XPCDelegateError" }

var errorCode: Int { code.rawValue }

var errorUserInfo: [String : Any] {
switch code {
case .invalidXcodePath:
return [
NSLocalizedDescriptionKey: "Invalid Xcode path.",
NSLocalizedFailureReasonErrorKey: "Xcode path must be absolute."
]
case .invalidSourcePath:
return [
NSLocalizedDescriptionKey: "Invalid source path.",
NSLocalizedFailureReasonErrorKey: "Source path must be absolute and must be a directory."
]
case .invalidDestinationPath:
return [
NSLocalizedDescriptionKey: "Invalid destination path.",
NSLocalizedFailureReasonErrorKey: "Destination path must be absolute and must be a directory."
]
case .destinationIsNotASymbolicLink:
return [
NSLocalizedDescriptionKey: "Invalid destination path.",
NSLocalizedFailureReasonErrorKey: "Destination path must be a symbolic link."
]
}
}
}
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The easiest way to install and switch between multiple versions of Xcode.

This repository is a maintained fork of [XcodesOrg/XcodesApp](https://github.com/XcodesOrg/XcodesApp). Fork history includes upstream release `v4.0.5b40`. It keeps the visible `Xcodes.app` name and workflow as a drop-in replacement while using fork-owned bundle, helper, update, signing, and release identities.
This repository is a maintained fork of [XcodesOrg/XcodesApp](https://github.com/XcodesOrg/XcodesApp). Fork history includes upstream release `v4.1.1b41`. It keeps the visible `Xcodes.app` name and workflow as a drop-in replacement while using fork-owned bundle, helper, update, signing, and release identities.

If you need a command-line tool, see the upstream [`xcodes`](https://github.com/XcodesOrg/xcodes) project.

Expand Down Expand Up @@ -34,6 +34,8 @@ Fork releases use the same `Xcodes.app` application name as upstream. The fork h

The release process distributes fork assets only after Developer ID signing and notarization. GitHub Releases is the supported binary distribution path. This fork does not publish a Homebrew cask.

If you installed upstream through Homebrew, run `brew uninstall --cask xcodes-app` before installing this fork. Otherwise, `brew upgrade` can replace the fork with upstream because both use `/Applications/Xcodes.app`. Do not use `--zap`; keep your existing settings. After installation, use **Xcodes → Check for Updates…** for fork updates.

On first launch, non-secret preferences can migrate from upstream Xcodes. Apple credentials, cookies, Keychain items, and the privileged helper do not migrate. Sign in again. When Xcodes prompts you, approve helper installation.

## Build from source
Expand Down
4 changes: 2 additions & 2 deletions Scripts/check_ci_release_workflows.rb
Original file line number Diff line number Diff line change
Expand Up @@ -475,9 +475,9 @@

releasing_docs = File.file?(releasing_docs_path) ? File.read(releasing_docs_path) : ""
check.call(releasing_docs.include?("environment protection rule must allow only protected tags matching `v*`"), "Release guide must require exact environment tag restrictions")
check.call(releasing_docs.include?("`workflow_dispatch` reruns must use `--ref v4.0.5b48`"), "Release guide must document tag-ref manual dispatch")
check.call(releasing_docs.include?("`workflow_dispatch` reruns must use `--ref v4.1.1b49`"), "Release guide must document tag-ref manual dispatch")
check.call(
releasing_docs.include?("gh workflow run appcast.yml --ref v4.0.5b48 -f tag=v4.0.5b48"),
releasing_docs.include?("gh workflow run appcast.yml --ref v4.1.1b49 -f tag=v4.1.1b49"),
"Release guide must document exact tag-bound appcast dispatch"
)
check.call(
Expand Down
8 changes: 4 additions & 4 deletions Scripts/check_fork_identity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ readonly app_id="dev.jacobcx.Xcodes"
readonly tests_id="dev.jacobcx.Xcodes.Tests"
readonly helper_id="dev.jacobcx.Xcodes.Helper"
readonly team_id="K2648T24P4"
readonly marketing_version="4.0.5"
readonly build_number="48"
readonly marketing_version="4.1.1"
readonly build_number="49"
readonly app_copyright="Fork contributions © 2026 JacobCXDev. Upstream contributors retain their copyrights."
# shellcheck disable=SC2016 # Xcode expands this build-setting literal, not the shell.
readonly app_requirement='identifier "dev.jacobcx.Xcodes" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)"'
Expand Down Expand Up @@ -154,7 +154,7 @@ done
require_literal "maintained fork" "$readme"
require_literal "[JacobCXDev](https://github.com/jacobcxdev) maintains this fork" "$readme"
# shellcheck disable=SC2016 # Markdown backticks are literal README content.
require_literal 'upstream release `v4.0.5b40`' "$readme"
require_literal 'upstream release `v4.1.1b41`' "$readme"
require_literal "https://github.com/XcodesOrg/XcodesApp" "$readme"
require_literal "git clone https://github.com/jacobcxdev/XcodesApp.git" "$readme"
require_literal "https://github.com/jacobcxdev/XcodesApp/releases/latest" "$readme"
Expand Down Expand Up @@ -183,7 +183,7 @@ if [[ -e "$repo_root/Xcodes/Frontend/XcodeList/BottomStatusBar.swift" ]] \
fail "Obsolete main-window footer remains"
fi
require_literal "DEVELOPER_ID_APPLICATION_P12_BASE64" "$release_documentation"
require_literal "v4.0.5b48" "$release_documentation"
require_literal "v4.1.1b49" "$release_documentation"

if grep -n -F -- 'Jacob Clayden' "$readme" "$about_source" "$app_info_plist"; then
fail "Legal name leaked into public-facing fork branding"
Expand Down
2 changes: 1 addition & 1 deletion Scripts/test_ci_release_workflows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ mutate_and_reject publish_unexpected_control \
mutate_and_reject missing_release_drafter_timeout \
'path = File.join(ARGV.fetch(0), ".github/workflows/release-drafter.yml"); data = YAML.safe_load_file(path, aliases: false); data["jobs"]["update_release_draft"].delete("timeout-minutes"); File.write(path, YAML.dump(data) + "# bounded timeout\n")'
mutate_and_reject missing_appcast_dispatch_docs \
'path = File.join(ARGV.fetch(0), "docs/RELEASING.md"); text = File.read(path).sub("gh workflow run appcast.yml --ref v4.0.5b48 -f tag=v4.0.5b48", "gh workflow run appcast.yml --ref main -f tag=latest"); File.write(path, text)'
'path = File.join(ARGV.fetch(0), "docs/RELEASING.md"); text = File.read(path).sub("gh workflow run appcast.yml --ref v4.1.1b49 -f tag=v4.1.1b49", "gh workflow run appcast.yml --ref main -f tag=latest"); File.write(path, text)'
# shellcheck disable=SC2016 # Documentation code spans must remain literal in the mutation.
mutate_and_reject missing_reusable_ref_docs \
'path = File.join(ARGV.fetch(0), "docs/RELEASING.md"); text = File.read(path).sub("Reusable workflows receive the caller'\''s `github.ref`; the appcast build requires that ref to equal `refs/tags/<tag>`", "Reusable workflows are called after release publication"); File.write(path, text)'
Expand Down
Loading