Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ If applicable, add screenshots to help explain your problem.
- Xcodes fork:
- Xcode involved:

Remove Apple ID details, cookies, signing keys, and other secrets from logs and screenshots.
Remove Apple Account details, cookies, signing keys, and other secrets from logs and screenshots.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Include:
- expected and actual behaviour
- relevant redacted logs or screenshots

Never include Apple ID credentials, cookies, Keychain contents, signing keys, or other secrets.
Never include Apple Account credentials, cookies, Keychain contents, signing keys, or other secrets.

## Licence

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ If you need a command-line tool, see the upstream [`xcodes`](https://github.com/
- Shows release notes, OS compatibility, SDKs, and compilers.
- Installs supported simulator platforms and runtimes.
- Supports Apple silicon and universal Xcode variants.
- Supports Apple ID and security-key authentication where Apple requires sign-in.
- Supports Apple Account and security-key authentication where Apple requires sign-in.
- Supports stable and prerelease update channels through Sparkle.

The optional unxip experiment builds on [saagarjha/unxip](https://github.com/saagarjha/unxip). It can reduce extraction time, at the cost of higher memory use on some systems.
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.5b47`"), "Release guide must document tag-ref manual dispatch")
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?("gh workflow run appcast.yml --ref v4.0.5b47 -f tag=v4.0.5b47"),
releasing_docs.include?("gh workflow run appcast.yml --ref v4.0.5b48 -f tag=v4.0.5b48"),
"Release guide must document exact tag-bound appcast dispatch"
)
check.call(
Expand Down
4 changes: 2 additions & 2 deletions Scripts/check_fork_identity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ 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="47"
readonly build_number="48"
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 @@ -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.5b47" "$release_documentation"
require_literal "v4.0.5b48" "$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
69 changes: 68 additions & 1 deletion Scripts/check_localizations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"Open Browser",
"Paste redirected URL",
"Signing out...",
"This Apple ID uses federated authentication via %@.",
].freeze
allowed_missing = (
baseline_keys.product(established_languages) +
Expand Down Expand Up @@ -55,11 +54,79 @@
end
end.sort

legacy_terms = /Apple(?:\s+|-)?ID\b|\bID Apple\b|\bID de Apple\b|\bIdentifiant Apple\b/i
legacy_terminology = catalog.fetch("strings").flat_map do |key, value|
value.fetch("localizations", {}).filter_map do |language, localization|
values = []
visit = lambda do |node|
case node
when Hash
values << node["value"] if node["value"].is_a?(String)
node.each_value { |child| visit.call(child) }
when Array
node.each { |child| visit.call(child) }
end
end
visit.call(localization)
"#{key}:#{language}" if values.any? { |text| text.match?(legacy_terms) }
end
end.sort

forbidden_keys = catalog.fetch("strings").keys.grep(legacy_terms)

required_keys = [
"AppBehaviour",
"AppleAccount",
"AuthError.NotAuthorized",
"AuthError.PasswordRequired",
"AuthError.PrivacyAcknowledgementRequired",
"CheckingAppleAccount",
"CheckingNotificationSettings",
"ManageAppleAccount",
"SignInToAppleAccount",
"SignedIn",
"This Apple Account uses federated authentication via %@.",
].reject { |key| catalog.fetch("strings").key?(key) }

required_grouping_values = {
"ar" => "تجميع إصدارات Xcode في القائمة",
"ca" => "Agrupa les versions d’Xcode a la llista",
"de" => "Xcode-Versionen in der Liste gruppieren",
"el" => "Ομαδοποίηση εκδόσεων Xcode στη λίστα",
"en" => "Group Xcode versions in the list",
"es" => "Agrupar las versiones de Xcode en la lista",
"fi" => "Ryhmittele Xcode-versiot luettelossa",
"fr" => "Regrouper les versions de Xcode dans la liste",
"hi" => "सूची में Xcode संस्करणों को समूहित करें",
"it" => "Raggruppa le versioni di Xcode nell’elenco",
"ja" => "リスト内のXcodeバージョンをグループ化",
"ko" => "목록에서 Xcode 버전 그룹화",
"nl" => "Xcode-versies in de lijst groeperen",
"pl" => "Grupuj wersje Xcode na liście",
"pt-BR" => "Agrupar versões do Xcode na lista",
"ru" => "Группировать версии Xcode в списке",
"th" => "จัดกลุ่มเวอร์ชัน Xcode ในรายการ",
"tr" => "Listedeki Xcode sürümlerini grupla",
"uk" => "Групувати версії Xcode у списку",
"zh-Hans" => "在列表中将 Xcode 版本分组",
"zh-Hant" => "將列表中的 Xcode 版本分組",
}.freeze

grouping_localizations = catalog.fetch("strings").fetch("GroupXcodeVersionsInList").fetch("localizations")
incorrect_grouping_values = required_grouping_values.filter_map do |language, expected_value|
actual_value = grouping_localizations.dig(language, "stringUnit", "value")
"GroupXcodeVersionsInList:#{language} expected #{expected_value.inspect}, got #{actual_value.inspect}" unless actual_value == expected_value
end

unexpected = missing - allowed_missing
resolved = allowed_missing - missing
errors = []
errors << "New missing or unreviewed translations:\n #{unexpected.join("\n ")}" unless unexpected.empty?
errors << "Localization baseline is stale; remove resolved entries:\n #{resolved.join("\n ")}" unless resolved.empty?
errors << "Legacy Apple ID terminology remains:\n #{legacy_terminology.join("\n ")}" unless legacy_terminology.empty?
errors << "Legacy localization keys remain:\n #{forbidden_keys.join("\n ")}" unless forbidden_keys.empty?
errors << "Required localization keys are missing:\n #{required_keys.join("\n ")}" unless required_keys.empty?
errors << "Grouped-version translations are mapped to the wrong locales:\n #{incorrect_grouping_values.join("\n ")}" unless incorrect_grouping_values.empty?

unless errors.empty?
warn errors.join("\n")
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.5b47 -f tag=v4.0.5b47", "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.0.5b48 -f tag=v4.0.5b48", "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
4 changes: 4 additions & 0 deletions Scripts/test_localization_contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ expect_failure new_thai_gap \
'path = ARGV.fetch(0); data = JSON.parse(File.read(path)); data["strings"]["AutomaticallyCreateSymbolicLink"]["localizations"].delete("th"); File.write(path, JSON.pretty_generate(data))'
expect_failure stale_baseline \
'path = ARGV.fetch(0); data = JSON.parse(File.read(path)); data["strings"]["AutomaticallyCreateBetaSymbolicLink"]["localizations"]["de"] = { "stringUnit" => { "state" => "translated", "value" => "Beta-Link" } }; File.write(path, JSON.pretty_generate(data))'
expect_failure legacy_apple_id_copy \
'path = ARGV.fetch(0); data = JSON.parse(File.read(path)); data["strings"]["AccessGranted"]["localizations"]["en"]["stringUnit"]["value"] = "Signed in with your Apple ID."; File.write(path, JSON.pretty_generate(data))'
expect_failure shifted_grouping_translation \
'path = ARGV.fetch(0); data = JSON.parse(File.read(path)); data["strings"]["GroupXcodeVersionsInList"]["localizations"]["en"]["stringUnit"]["value"] = data["strings"]["GroupXcodeVersionsInList"]["localizations"]["es"]["stringUnit"]["value"]; File.write(path, JSON.pretty_generate(data))'

checker_fixture="$test_root/omitted-language-checker.rb"
cp "$checker" "$checker_fixture"
Expand Down
6 changes: 3 additions & 3 deletions Xcodes.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 47;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = K2648T24P4;
ENABLE_HARDENED_RUNTIME = NO;
Expand Down Expand Up @@ -1325,7 +1325,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 47;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = K2648T24P4;
ENABLE_HARDENED_RUNTIME = YES;
Expand Down Expand Up @@ -1354,7 +1354,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 47;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = K2648T24P4;
ENABLE_HARDENED_RUNTIME = YES;
Expand Down
10 changes: 5 additions & 5 deletions Xcodes/Backend/AppState+Update.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ extension AppState {
} catch {
// 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)
let presentedError = Self.userFacingAuthenticationError(error)
self.error = presentedError
self.presentedAlert = .generic(title: localizeString("Alert.Update.Error.Title"), message: presentedError.legibleLocalizedDescription)
}
}
}
Expand All @@ -79,9 +80,8 @@ extension AppState {

private func updateAvailableXcodes(from dataSource: DataSource) async throws -> [AvailableXcode] {
if dataSource == .apple {
try await signInIfNeededAsync()
// This checks whether the Apple ID is a valid Apple Developer account.
try await validateSessionAsync()
_ = try await signInIfNeededAsync()
try await waitForAuthenticationTerminalState()
}

let service = XcodeListService(urlSession: Current.network.session)
Expand Down
Loading