Skip to content

Commit b725865

Browse files
committed
feat(tools): add utility to generate swift toolchain dictionaries
Generating the toolchain dictionary currently requires downloading toolchains one by one and calculating their checksum, which is tedious and time consuming. We're adding a utility here that will do that automatically. The tool supports caching downloaded archives and can fetch release information from swift.org's API to generate the checksums dictionary format used by the build system. While we're at it, we're running that utility for the 2 latest releases (6.2.2 and 6.2.3) and adding them to the dict. Because that dictionary is probably gonna keep growing over time, we're also moving it in its own standalone file (swift_releases.bzl).
1 parent 8e1fb2c commit b725865

File tree

4 files changed

+343
-22
lines changed

4 files changed

+343
-22
lines changed

swift/extensions.bzl

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
load("@bazel_features//:features.bzl", "bazel_features")
1818
load("//swift:repositories.bzl", "swift_rules_dependencies")
1919
load("//swift/internal:extensions/standalone_toolchain.bzl", _standalone_toolchain = "standalone_toolchain")
20+
load("//swift/internal:extensions/swift_releases.bzl", "SWIFT_RELEASES")
2021
load(
2122
"//swift/internal:extensions/toolchains.bzl",
2223
_toolchains_for_platform = "toolchains_for_platform",
@@ -36,25 +37,6 @@ def _non_module_deps_impl(module_ctx):
3637

3738
non_module_deps = module_extension(implementation = _non_module_deps_impl)
3839

39-
# This mapping is intended to map each version to its supported platforms and checksums
40-
_SWIFT_RELEASES = {
41-
"6.2.1": {
42-
"xcode": "4ca13d0abd364664d19facd75e23630c0884898bbcaf1920b45df288bdb86cb2",
43-
"amazonlinux2": "218fc55ba7224626fd25f8ca285b083fda020e3737146e2fe10b8ae9aaf2ae97",
44-
"amazonlinux2-aarch64": "00999039a82a81b1e9f3915eb2c78b63552fe727bcbfe9a2611628ac350287f2",
45-
"debian12": "d6405e4fb7f092cbb9973a892ce8410837b4335f67d95bf8607baef1f69939e4",
46-
"debian12-aarch64": "522d231bb332fe5da9648ca7811e8054721f05eccd1eefae491cf4a86eab4155",
47-
"fedora39": "ec78360dfa7817d7637f207b1ffb3a22164deb946c9a9f8c40ab8871856668e8",
48-
"fedora39-aarch64": "d8bc04e7e283e314d1b96adc55e1803dd01a0106dc0d0263e784a5c9f2a46d3b",
49-
"ubi9": "9a082c3efdeda2e65cbc7038d0c295b75fa48f360369b2538449fc665192da3e",
50-
"ubi9-aarch64": "47f109f1f63fa24df3659676bb1afac2fdd05c0954d4f00977da6a868dd31e66",
51-
"ubuntu22.04": "5ec23d4004f760fafdbb76c21e380d3bacef1824300427a458dc88c1c0bef381",
52-
"ubuntu22.04-aarch64": "ab5f3eb0349c575c38b96ed10e9a7ffa2741b0038285c12d56251a38749cadf0",
53-
"ubuntu24.04": "4022cb64faf7e2681c19f9b62a22fb7d9055db6194d9e4a4bef9107b6ce10946",
54-
"ubuntu24.04-aarch64": "3b70a3b23b9435c37112d96ee29aa70061e23059ef9c4d3cfa4951f49c4dfedb",
55-
},
56-
}
57-
5840
def _standalone_toolchain_impl(module_ctx):
5941
root_module = None
6042
for mod in module_ctx.modules:
@@ -77,13 +59,13 @@ def _standalone_toolchain_impl(module_ctx):
7759
if toolchain.swift_version_file:
7860
swift_version = module_ctx.read(toolchain.swift_version_file).strip()
7961

80-
if swift_version not in _SWIFT_RELEASES:
62+
if swift_version not in SWIFT_RELEASES:
8163
fail("Version `{}` is not supported by this version of rules_swift. Please choose one of: {}".format(
8264
swift_version,
83-
_SWIFT_RELEASES.keys(),
65+
SWIFT_RELEASES.keys(),
8466
))
8567

86-
for platform, sha256 in _SWIFT_RELEASES[swift_version].items():
68+
for platform, sha256 in SWIFT_RELEASES[swift_version].items():
8769
repository_name = toolchain.name + "_{}".format(platform)
8870
_standalone_toolchain(
8971
name = repository_name,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# This mapping is intended to map each version to its supported platforms and checksums
2+
SWIFT_RELEASES = {
3+
"6.2.1": {
4+
"amazonlinux2": "218fc55ba7224626fd25f8ca285b083fda020e3737146e2fe10b8ae9aaf2ae97",
5+
"amazonlinux2-aarch64": "00999039a82a81b1e9f3915eb2c78b63552fe727bcbfe9a2611628ac350287f2",
6+
"debian12": "d6405e4fb7f092cbb9973a892ce8410837b4335f67d95bf8607baef1f69939e4",
7+
"debian12-aarch64": "522d231bb332fe5da9648ca7811e8054721f05eccd1eefae491cf4a86eab4155",
8+
"fedora39": "ec78360dfa7817d7637f207b1ffb3a22164deb946c9a9f8c40ab8871856668e8",
9+
"fedora39-aarch64": "d8bc04e7e283e314d1b96adc55e1803dd01a0106dc0d0263e784a5c9f2a46d3b",
10+
"ubi9": "9a082c3efdeda2e65cbc7038d0c295b75fa48f360369b2538449fc665192da3e",
11+
"ubi9-aarch64": "47f109f1f63fa24df3659676bb1afac2fdd05c0954d4f00977da6a868dd31e66",
12+
"ubuntu22.04": "5ec23d4004f760fafdbb76c21e380d3bacef1824300427a458dc88c1c0bef381",
13+
"ubuntu22.04-aarch64": "ab5f3eb0349c575c38b96ed10e9a7ffa2741b0038285c12d56251a38749cadf0",
14+
"ubuntu24.04": "4022cb64faf7e2681c19f9b62a22fb7d9055db6194d9e4a4bef9107b6ce10946",
15+
"ubuntu24.04-aarch64": "3b70a3b23b9435c37112d96ee29aa70061e23059ef9c4d3cfa4951f49c4dfedb",
16+
"xcode": "4ca13d0abd364664d19facd75e23630c0884898bbcaf1920b45df288bdb86cb2",
17+
},
18+
"6.2.2": {
19+
"amazonlinux2": "2de884b0ccf1012750fd93c710506c3d216e34676b488ba318fefe711a136125",
20+
"amazonlinux2-aarch64": "4bb5714a683d8ddf78bc69027cb2acc9854ae51e91e55badba2e5c231b923a42",
21+
"debian12": "d4817caaf70e95639702b69be24730057f4220f76796573397cdc067a4360041",
22+
"debian12-aarch64": "1e225d1f9a78de78d5f4d0cdc4e58531b125788a7c5f904db68a3f6f21f639d9",
23+
"fedora39": "c68971618737c66e76e39e7304a59f6af332c68dca64f0a97ff2393bfd09e136",
24+
"fedora39-aarch64": "aaec949e278427fc8ba095a4edf67b80d1a8230a5c7c43ef9383d6860407dd75",
25+
"ubi9": "a90b616b97616fdc4906babced4961982ab36a1e3ce44cf07d4a036298529abb",
26+
"ubi9-aarch64": "a90b616b97616fdc4906babced4961982ab36a1e3ce44cf07d4a036298529abb",
27+
"ubuntu22.04": "b3cafe1ca87ba0bf253639aec53052b545c9fcccd810da8cf15ac9ad62561f7e",
28+
"ubuntu22.04-aarch64": "6f3bff4c2a69163e56d2bacfa8ede2535ae52f5a29824f3c13d9e4c3ad1ac155",
29+
"ubuntu24.04": "2e226607d419f7b6197a6a0a9b317ee1cdb4125c21c72b0b24adfb82d4274fa9",
30+
"ubuntu24.04-aarch64": "53152dfed20e971f4cdbb40a205e9b4a8d8d34a84e1d0fefbdfce7af87072db1",
31+
"xcode": "1173886e2084a6705a774875e4b1b2fceeb890d79ced54ee824cfd10bdc26328"
32+
},
33+
"6.2.3": {
34+
"amazonlinux2": "fe1513e441ab653a134f9fd35855fe5dddac5fa716c0b0fe119eb76757525f05",
35+
"amazonlinux2-aarch64": "0753ec4fb786c626a681803c25ea3c681df583f0f576a6e326a25bd92294b4c6",
36+
"debian12": "d47b7416f68e75b3b8ed538c939dc6e5a9e9a8de2d605389661d2ef31e75b772",
37+
"debian12-aarch64": "6d9703968ef399b953e67229c5feb0781ceca12d089208ecef8157b59e22582b",
38+
"fedora39": "34314fab3f8e975980bcddf6b372b10e6430fb5c469e7232b95e06ae2762f449",
39+
"fedora39-aarch64": "802154a68eade7051ddaa290cf30d51a801a6b291edfc34643398acde9dffde9",
40+
"ubi9": "a43399aad9d5b19f7d7d6f88ed19129ca6afaf34bb6b455ca01e61a98ec425f2",
41+
"ubi9-aarch64": "a43399aad9d5b19f7d7d6f88ed19129ca6afaf34bb6b455ca01e61a98ec425f2",
42+
"ubuntu22.04": "23653abba4b153aa6625f73e63e3f119bdaf18363b00e3770a306fbd9b192aef",
43+
"ubuntu22.04-aarch64": "fbb4282ec60107cc844700aac6c7a8115534defb1c9b36867bd77c0829e5b163",
44+
"ubuntu24.04": "3e0b8eaf9210131a1756e6a1a9e9103bac83609a0ae604d6f2e791053f98f115",
45+
"ubuntu24.04-aarch64": "48dc99bcabc54feadd2942f4830be854ca2396e2db4ca4ec6b6c926a25c87d55",
46+
"xcode": "c1ed84cf543286c549caaccc47e0b47d8c61c3c8fedbce1205dedcbebe7601a8"
47+
},
48+
}

tools/swift-releases/BUILD

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
load(
2+
"//swift:swift_binary.bzl",
3+
"swift_binary",
4+
)
5+
6+
package(default_visibility = ["//visibility:public"])
7+
8+
licenses(["notice"])
9+
10+
swift_binary(
11+
name = "swift-releases",
12+
srcs = ["SwiftReleases.swift"],
13+
visibility = ["//visibility:public"],
14+
deps = [
15+
"@com_github_apple_swift_argument_parser//:ArgumentParser",
16+
],
17+
)
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import ArgumentParser
2+
import CryptoKit
3+
import Foundation
4+
5+
#if canImport(FoundationNetworking)
6+
import FoundationNetworking
7+
#endif
8+
9+
// Helper to write to stderr
10+
var standardError = FileHandle.standardError
11+
12+
extension FileHandle: @retroactive TextOutputStream {
13+
public func write(_ string: String) {
14+
guard let data = string.data(using: .utf8) else { return }
15+
self.write(data)
16+
}
17+
}
18+
19+
struct Release: Codable {
20+
let name: String
21+
let tag: String
22+
let xcode: String?
23+
let xcode_release: Bool?
24+
let date: String?
25+
let platforms: [Platform]
26+
27+
struct Platform: Codable {
28+
let name: String
29+
let platform: String
30+
let docker: String?
31+
let dir: String?
32+
let checksum: String?
33+
let archs: [String]
34+
}
35+
}
36+
37+
typealias ReleasesResponse = [Release]
38+
39+
func fetchReleases() throws -> ReleasesResponse {
40+
let url = URL(string: "https://www.swift.org/api/v1/install/releases.json")!
41+
let semaphore = DispatchSemaphore(value: 0)
42+
var result: Result<Data, Error>?
43+
44+
let task = URLSession.shared.dataTask(with: url) { data, response, error in
45+
if let error = error {
46+
result = .failure(error)
47+
} else if let httpResponse = response as? HTTPURLResponse {
48+
if httpResponse.statusCode != 200 {
49+
let error = NSError(
50+
domain: "SwiftReleaseDownloader",
51+
code: httpResponse.statusCode,
52+
userInfo: [NSLocalizedDescriptionKey: "HTTP error \(httpResponse.statusCode)"]
53+
)
54+
result = .failure(error)
55+
} else if let data = data {
56+
result = .success(data)
57+
} else {
58+
result = .failure(
59+
NSError(
60+
domain: "SwiftReleaseDownloader", code: -1,
61+
userInfo: [NSLocalizedDescriptionKey: "No data received"]))
62+
}
63+
} else if let data = data {
64+
result = .success(data)
65+
} else {
66+
result = .failure(
67+
NSError(
68+
domain: "SwiftReleaseDownloader", code: -1,
69+
userInfo: [NSLocalizedDescriptionKey: "No data received"]))
70+
}
71+
semaphore.signal()
72+
}
73+
task.resume()
74+
semaphore.wait()
75+
76+
let data = try result!.get()
77+
let decoder = JSONDecoder()
78+
return try decoder.decode(ReleasesResponse.self, from: data)
79+
}
80+
81+
func downloadFile(url: URL, cacheDir: String?) throws -> Data {
82+
// Check cache first if cache directory is provided
83+
if let cacheDir = cacheDir {
84+
let filename = url.lastPathComponent
85+
let cachePath = (cacheDir as NSString).appendingPathComponent(filename)
86+
87+
if FileManager.default.fileExists(atPath: cachePath) {
88+
print("Found \(filename) in \(cacheDir). Skipping", to: &standardError)
89+
return try Data(contentsOf: URL(fileURLWithPath: cachePath))
90+
}
91+
}
92+
print("Downloading \(url)...", to: &standardError)
93+
94+
let semaphore = DispatchSemaphore(value: 0)
95+
var result: Result<Data, Error>?
96+
97+
let task = URLSession.shared.dataTask(with: url) { data, response, error in
98+
if let error = error {
99+
result = .failure(error)
100+
} else if let httpResponse = response as? HTTPURLResponse {
101+
if httpResponse.statusCode != 200 {
102+
let error = NSError(
103+
domain: "SwiftReleaseDownloader",
104+
code: httpResponse.statusCode,
105+
userInfo: [
106+
NSLocalizedDescriptionKey:
107+
"HTTP error \(httpResponse.statusCode) for \(url.absoluteString)"
108+
]
109+
)
110+
result = .failure(error)
111+
} else if let data = data {
112+
result = .success(data)
113+
} else {
114+
result = .failure(
115+
NSError(
116+
domain: "SwiftReleaseDownloader", code: -1,
117+
userInfo: [NSLocalizedDescriptionKey: "No data received"]))
118+
}
119+
} else if let data = data {
120+
result = .success(data)
121+
} else {
122+
result = .failure(
123+
NSError(
124+
domain: "SwiftReleaseDownloader", code: -1,
125+
userInfo: [NSLocalizedDescriptionKey: "No data received"]))
126+
}
127+
semaphore.signal()
128+
}
129+
task.resume()
130+
semaphore.wait()
131+
132+
let data = try result!.get()
133+
134+
// Save to cache if cache directory is provided
135+
if let cacheDir = cacheDir {
136+
let filename = url.lastPathComponent
137+
let cachePath = (cacheDir as NSString).appendingPathComponent(filename)
138+
139+
// Create cache directory if it doesn't exist
140+
try FileManager.default.createDirectory(
141+
atPath: cacheDir, withIntermediateDirectories: true, attributes: nil)
142+
143+
// Write data to cache
144+
try data.write(to: URL(fileURLWithPath: cachePath))
145+
print(" Cached to: \(cachePath)", to: &standardError)
146+
}
147+
148+
return data
149+
}
150+
151+
func sha256(data: Data) -> String {
152+
let hash = SHA256.hash(data: data)
153+
return hash.compactMap { String(format: "%02x", $0) }.joined()
154+
}
155+
156+
func getDownloadURL(tag: String, platformName: String) -> String {
157+
let version = tag
158+
let category = tag.lowercased()
159+
let filename: String
160+
161+
let platformDir = platformName.replacingOccurrences(of: ".", with: "")
162+
if platformName == "xcode" {
163+
filename = "\(version)-osx.pkg"
164+
} else {
165+
filename = "\(version)-\(platformName).tar.gz"
166+
}
167+
168+
return "https://download.swift.org/\(category)/\(platformDir)/\(version)/\(filename)"
169+
}
170+
171+
@main
172+
struct SwiftReleases: ParsableCommand {
173+
static let configuration = CommandConfiguration(
174+
commandName: "swift-releases",
175+
abstract: "A utility for working with Swift releases",
176+
subcommands: [List.self],
177+
defaultSubcommand: List.self
178+
)
179+
}
180+
181+
extension SwiftReleases {
182+
struct List: ParsableCommand {
183+
static let configuration = CommandConfiguration(
184+
abstract: "Download Swift release archives and print their SHA256 hashes"
185+
)
186+
187+
@Argument(help: "The Swift version to download (e.g., 6.0.3)")
188+
var version: String
189+
190+
@Option(help: "Directory to cache downloaded archives")
191+
var cache: String?
192+
193+
func run() throws {
194+
let releases = try fetchReleases()
195+
196+
guard let release = releases.first(where: { $0.name == version }) else {
197+
print("Error: Version '\(version)' not found", to: &standardError)
198+
print("Available versions:", to: &standardError)
199+
for rel in releases.reversed().prefix(20) {
200+
print(" - \(rel.name)", to: &standardError)
201+
}
202+
if releases.count > 20 {
203+
print(" ... and \(releases.count - 20) more", to: &standardError)
204+
}
205+
throw ExitCode.failure
206+
}
207+
208+
// Dictionary to store platform -> sha256 mappings
209+
var checksums: [String: String] = [:]
210+
211+
// Only Linux & MacOS toolchains are supported for now.
212+
// Supported Linux toolchains for a given version are in the response,
213+
// but MacOS toolchains aren't. They're just assumed to be there. So we
214+
// must add them manually.
215+
let platforms =
216+
release.platforms.filter {
217+
$0.platform == "Linux"
218+
} + [
219+
Release.Platform(
220+
name: "osx",
221+
platform: "osx",
222+
docker: nil,
223+
dir: "xcode",
224+
checksum: nil,
225+
archs: ["aarch64"],
226+
)
227+
]
228+
229+
for platform in platforms {
230+
// Handle platforms with checksums (like static-sdk, wasm-sdk)
231+
if let checksum = platform.checksum {
232+
// For platforms with checksums, use the platform name directly
233+
let platformKey = platform.platform
234+
checksums[platformKey] = checksum
235+
continue
236+
}
237+
238+
// Download for each architecture
239+
for arch in platform.archs {
240+
let platformName =
241+
platform.dir ?? platform.name.lowercased().replacingOccurrences(of: " ", with: "")
242+
let platformKey: String
243+
if platformName == "xcode" || arch == "x86_64" {
244+
platformKey = platformName
245+
} else {
246+
platformKey = "\(platformName)-\(arch)"
247+
}
248+
249+
let downloadURL = getDownloadURL(
250+
tag: release.tag, platformName: platform.dir ?? platformKey)
251+
252+
do {
253+
let url = URL(string: downloadURL)!
254+
let data = try downloadFile(url: url, cacheDir: cache)
255+
let hash = sha256(data: data)
256+
checksums[platformKey] = hash
257+
} catch {
258+
print("Error: \(error)", to: &standardError)
259+
throw error
260+
}
261+
}
262+
}
263+
264+
// Print in the requested format
265+
print(" \"\(version)\": {")
266+
let sortedKeys = checksums.keys.sorted()
267+
for (index, key) in sortedKeys.enumerated() {
268+
let comma = index < sortedKeys.count - 1 ? "," : ""
269+
print(" \"\(key)\": \"\(checksums[key]!)\"\(comma)")
270+
}
271+
print(" },")
272+
}
273+
}
274+
}

0 commit comments

Comments
 (0)