From 9beb1a296daed2fa9a4180e5ede3a0f51060bb8e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:25:07 -0400 Subject: [PATCH 01/15] Generate CompanyIdentifier name lookup as Swift source Emits a switch statement mapping raw company identifier values to their SIG-assigned names, mirroring the existing numeric-constant generator instead of loading Resources/CompanyIdentifier.json at runtime. --- .../CompanyIdentifierMetadata.swift | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Sources/GenerateBluetooth/CompanyIdentifierMetadata.swift diff --git a/Sources/GenerateBluetooth/CompanyIdentifierMetadata.swift b/Sources/GenerateBluetooth/CompanyIdentifierMetadata.swift new file mode 100644 index 000000000..f29df5545 --- /dev/null +++ b/Sources/GenerateBluetooth/CompanyIdentifierMetadata.swift @@ -0,0 +1,57 @@ +// +// CompanyIdentifierMetadata.swift +// +// +// Created by Alsey Coleman Miller on 1/12/25. +// + +import Foundation +import BluetoothMetadata + +extension GenerateTool { + + static func generateCompanyIdentifierMetadata(output: URL) throws { + let data = try parseCompanyIdentifiersFile() + try generateCompanyIdentifierMetadataSwitch(data, output: output) + } + + static func generateCompanyIdentifierMetadataSwitch(_ data: [UInt16: String], output: URL) throws { + + var generatedCode = "" + // sorted, but not filtered — unlike the numeric constant accessors, + // this backs the lossless `name` lookup, so every entry (including + // the internal-use 0xFFFF identifier) is kept. + let companies = data.sorted(by: { $0.key < $1.key }) + + func 🖨(_ text: String) { + generatedCode += text + "\n" + } + + 🖨("//") + 🖨("// \(output.lastPathComponent)") + 🖨("// Bluetooth") + 🖨("//") + 🖨("// Generated from Resources/CompanyIdentifier.json — do not edit.") + 🖨("//") + 🖨("") + 🖨("#if Metadata") + 🖨("extension CompanyIdentifier {") + 🖨("") + 🖨(" // swiftlint:disable:next cyclomatic_complexity") + 🖨(" static func generatedName(for rawValue: UInt16) -> String? {") + 🖨(" switch rawValue {") + + for (id, name) in companies { + 🖨(" case \(id): return #\"\(name)\"#") + } + + 🖨(" default: return nil") + 🖨(" }") + 🖨(" }") + 🖨("}") + 🖨("#endif") + + try generatedCode.write(toFile: output.path, atomically: true, encoding: .utf8) + print("Generated \(output.path)") + } +} From 551fd1c5b4fa5db6e352a2d4a25a7fe4f8dddb7b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:25:16 -0400 Subject: [PATCH 02/15] Generate BluetoothUUID metadata lookup as Swift source Merges the six UUID category files (service, characteristic, declaration, descriptor, member, unit) into a single switch keyed by raw value, returning name and type, in a fixed category precedence order. Collisions across categories are reported at build time and resolved by keeping the first category in that order. --- .../BluetoothUUIDMetadata.swift | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Sources/GenerateBluetooth/BluetoothUUIDMetadata.swift diff --git a/Sources/GenerateBluetooth/BluetoothUUIDMetadata.swift b/Sources/GenerateBluetooth/BluetoothUUIDMetadata.swift new file mode 100644 index 000000000..5669c9b8d --- /dev/null +++ b/Sources/GenerateBluetooth/BluetoothUUIDMetadata.swift @@ -0,0 +1,79 @@ +// +// BluetoothUUIDMetadata.swift +// +// +// Created by Alsey Coleman Miller on 1/12/25. +// + +import Foundation +import BluetoothMetadata + +extension GenerateTool { + + /// Categories merged in this fixed order when the same 16-bit value + /// appears in more than one — matches `BluetoothUUIDMetadata.swift`'s + /// prior runtime behavior of returning the first match found, but + /// deterministically instead of in unspecified dictionary order. + static var uuidMetadataCategoryOrder: [BluetoothMetadata.BluetoothUUID.Category] { + [.service, .characteristic, .declaration, .descriptor, .member, .unit] + } + + static func generateBluetoothUUIDMetadata(output: URL) throws { + var merged = [UInt16: BluetoothMetadata.BluetoothUUID]() + for category in uuidMetadataCategoryOrder { + let file = try BluetoothMetadata.BluetoothUUID.File(category) + for entry in file.uuids { + if let existing = merged[entry.uuid] { + print( + "warning: BluetoothUUID 0x\(entry.uuid.toHexadecimal()) is defined in more than one category" + + " (keeping \"\(existing.name)\", ignoring \"\(entry.name)\" from \(category))" + ) + continue + } + merged[entry.uuid] = entry + } + } + try generateBluetoothUUIDMetadataSwitch(merged, output: output) + } + + static func generateBluetoothUUIDMetadataSwitch( + _ data: [UInt16: BluetoothMetadata.BluetoothUUID], + output: URL + ) throws { + + var generatedCode = "" + let entries = data.values.sorted(by: { $0.uuid < $1.uuid }) + + func 🖨(_ text: String) { + generatedCode += text + "\n" + } + + 🖨("//") + 🖨("// \(output.lastPathComponent)") + 🖨("// Bluetooth") + 🖨("//") + 🖨("// Generated from Resources/{Service,Characteristic,Declaration,Descriptor,Member,Unit}UUID.json — do not edit.") + 🖨("//") + 🖨("") + 🖨("#if Metadata") + 🖨("extension BluetoothUUID {") + 🖨("") + 🖨(" // swiftlint:disable:next cyclomatic_complexity") + 🖨(" static func generatedMetadata(for rawValue: UInt16) -> (name: String, type: String?)? {") + 🖨(" switch rawValue {") + + for entry in entries { + let typeLiteral = entry.type.map { "#\"\($0)\"#" } ?? "nil" + 🖨(" case \(entry.uuid): return (#\"\(entry.name)\"#, \(typeLiteral))") + } + + 🖨(" default: return nil") + 🖨(" }") + 🖨(" }") + 🖨("}") + 🖨("#endif") + + try generatedCode.write(toFile: output.path, atomically: true, encoding: .utf8) + print("Generated \(output.path)") + } +} From 9bcfbfcbb873cef75b481119d2364ca698f4a3a7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:25:21 -0400 Subject: [PATCH 03/15] Wire up companyIdentifierMetadata and uuidMetadata commands Also fixes a pre-existing arguments[3]/arguments[2] typo in the .unitIdentifierTests case that crashed with an out-of-range index whenever this command path was actually invoked. --- Sources/GenerateBluetooth/Generate.swift | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Sources/GenerateBluetooth/Generate.swift b/Sources/GenerateBluetooth/Generate.swift index 1dfc86350..925b68a7e 100644 --- a/Sources/GenerateBluetooth/Generate.swift +++ b/Sources/GenerateBluetooth/Generate.swift @@ -30,6 +30,16 @@ struct GenerateTool { try generateCompanyIdentifiers( output: outputFile ) + case .companyIdentifierMetadata: + // parse arguments + guard arguments.count == 3 else { + throw CommandError.invalidArguments(arguments) + } + let outputFile = URL(fileURLWithPath: arguments[2]) + // generate files + try generateCompanyIdentifierMetadata( + output: outputFile + ) case .companyIdentifierTests: // parse arguments guard arguments.count == 3 else { @@ -55,7 +65,7 @@ struct GenerateTool { guard arguments.count == 3 else { throw CommandError.invalidArguments(arguments) } - let outputFile = URL(fileURLWithPath: arguments[3]) + let outputFile = URL(fileURLWithPath: arguments[2]) // generate files try generateUnitIdentifierTests( output: outputFile @@ -72,6 +82,16 @@ struct GenerateTool { type: uuidType, output: outputFile ) + case .uuidMetadata: + // parse arguments + guard arguments.count == 3 else { + throw CommandError.invalidArguments(arguments) + } + let outputFile = URL(fileURLWithPath: arguments[2]) + // generate files + try generateBluetoothUUIDMetadata( + output: outputFile + ) } } } @@ -79,10 +99,12 @@ struct GenerateTool { enum CommandType: String { case companyIdentifier + case companyIdentifierMetadata case companyIdentifierTests case unitIdentifier case unitIdentifierTests case uuid + case uuidMetadata } enum CommandError: Error { From 5078d68a42507f04887aeefdcc7f32eb152fd6c4 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:25:28 -0400 Subject: [PATCH 04/15] Generate CompanyIdentifierTests as a parameterized test Replaces thousands of individually unrolled #expect() calls with a single @Test(arguments:) call site over a generated entries array, built with sequential append() calls rather than one large array literal. Both unrolled #expect() chains and large array literals are pathologically slow for the type checker at this data's scale (~4000 entries); a single parameterized test site with straight-line data construction compiles in seconds instead of minutes. --- .../GenerateBluetooth/CompanyIdentifier.swift | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/Sources/GenerateBluetooth/CompanyIdentifier.swift b/Sources/GenerateBluetooth/CompanyIdentifier.swift index 37c96e08d..2395150c2 100644 --- a/Sources/GenerateBluetooth/CompanyIdentifier.swift +++ b/Sources/GenerateBluetooth/CompanyIdentifier.swift @@ -88,13 +88,23 @@ extension GenerateTool { let data = try parseCompanyIdentifiersFile() var generatedCode = "" - let companies = companyIdentifiers(from: data) + // sorted, but not filtered — this backs a lossless correctness + // check, so every entry (including the internal-use 0xFFFF + // identifier) is covered. + let companies = data.sorted(by: { $0.key < $1.key }) func 🖨(_ text: String) { generatedCode += text + "\n" } - // generate unit test for extensions + // One #expect() call *site*, not one per entry: swift-testing's + // macro expansion cost scales with the number of source-level + // #expect occurrences, not with how many times each runs, so a + // few thousand individually-unrolled #expect calls (whether in + // one function or split across many) is many minutes of type + // checking. A single parameterized @Test(arguments:) — data + // description, runtime loop — keeps compile time bounded + // regardless of entry count. generatedCode = """ // // CompanyIdentifierTests.swift @@ -105,37 +115,44 @@ extension GenerateTool { import Testing @testable import Bluetooth - // swiftlint:disable type_body_length - #if !canImport(Darwin) + #if !canImport(Darwin) && Metadata @Suite struct CompanyIdentifierTests { - @Test func companies() { + struct Entry: Sendable { + let id: UInt16 + let name: String + } + // A single array-literal expression with thousands of + // elements is pathologically slow for the type checker, + // same as an unrolled #expect() chain. Sequential + // append() calls keep each statement's type-checking + // cost independent of the others. + static var entries: [Entry] { + var entries: [Entry] = [] + entries.reserveCapacity(\(companies.count)) """ - // generate test methods - - for (id, name, memberName) in companies { - - 🖨( - """ - // \(name) - #expect(CompanyIdentifier.\(memberName).rawValue == \(id)) - #expect(CompanyIdentifier.\(memberName).name == #\"\(name)\"#) - #expect(CompanyIdentifier.\(memberName).description == #\"\(name)\"#) - - """) + for (id, name) in companies { + 🖨(" entries.append(Entry(id: \(id), name: #\"\(name)\"#))") } 🖨( """ + return entries } + @Test(arguments: entries) + func company(_ entry: Entry) { + let identifier = CompanyIdentifier(rawValue: entry.id) + #expect(identifier.rawValue == entry.id) + #expect(identifier.name == entry.name) + #expect(identifier.description == entry.name) + } } #endif - // swiftlint:enable type_body_length """) try generatedCode.write(toFile: output.path, atomically: true, encoding: .utf8) From 2abdd418602a90c3c6907f5a0288973034397cc8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:25:34 -0400 Subject: [PATCH 05/15] Generate UnitIdentifierTests as a parameterized test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same rewrite as CompanyIdentifierTests. Also fixes the generated description expectation, which composed a "0x... (name)" string that never matched UnitIdentifier.description's actual output (just the name, consistent with CompanyIdentifier.description) — previously undetected because this generated test was never actually invoked due to the arguments[3] bug and the Package.swift target-index bug. --- .../GenerateBluetooth/UnitIdentifier.swift | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/Sources/GenerateBluetooth/UnitIdentifier.swift b/Sources/GenerateBluetooth/UnitIdentifier.swift index db9a5181f..010f5158f 100644 --- a/Sources/GenerateBluetooth/UnitIdentifier.swift +++ b/Sources/GenerateBluetooth/UnitIdentifier.swift @@ -90,13 +90,17 @@ extension GenerateTool { let data = try parseUnitIdentifiersFile() var generatedCode = "" - let units = unitIdentifiers(from: data) + // sorted, but not filtered — every entry (including the + // internal-use identifier) is covered. + let units = data.sorted(by: { $0.key < $1.key }) func 🖨(_ text: String) { generatedCode += text + "\n" } - // generate unit test for extensions + // See generateCompanyIdentifierTests: one #expect() call site, + // driven by a parameterized @Test(arguments:), instead of one + // unrolled per entry. generatedCode = """ // // UnitIdentifierTests.swift @@ -107,37 +111,43 @@ extension GenerateTool { import Testing @testable import Bluetooth - // swiftlint:disable type_body_length + #if Metadata @Suite struct UnitIdentifierTests { - @Test func units() { + struct Entry: Sendable { + let id: UInt16 + let type: String + let name: String + } + // See generateCompanyIdentifierTests for why this is + // built with append() calls instead of an array literal. + static var entries: [Entry] { + var entries: [Entry] = [] + entries.reserveCapacity(\(units.count)) """ - // generate test methods - - for (id, name, type, memberName) in units { - let hexLiteral = "0x" + id.toHexadecimal() - let description = hexLiteral + " " + "(" + name + ")" - 🖨( - """ - // \(name) - #expect(UnitIdentifier.\(memberName).rawValue == \(hexLiteral)) - #expect(UnitIdentifier.\(memberName).type == #\"\(type)\"#) - #expect(UnitIdentifier.\(memberName).name == #\"\(name)\"#) - #expect(UnitIdentifier.\(memberName).description == #\"\(description)\"#) - - """) + for (id, metadata) in units { + 🖨(" entries.append(Entry(id: \(id), type: #\"\(metadata.id)\"#, name: #\"\(metadata.name)\"#))") } 🖨( """ + return entries } + @Test(arguments: entries) + func unit(_ entry: Entry) { + let identifier = UnitIdentifier(rawValue: entry.id) + #expect(identifier.rawValue == entry.id) + #expect(identifier.type == entry.type) + #expect(identifier.name == entry.name) + #expect(identifier.description == entry.name) + } } - // swiftlint:enable type_body_length + #endif """) try generatedCode.write(toFile: output.path, atomically: true, encoding: .utf8) From 0775b0258e19588db5b1500a150089a125db1bdb Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:25:39 -0400 Subject: [PATCH 06/15] Add build command for CompanyIdentifier metadata generation Invokes GenerateBluetooth companyIdentifierMetadata to emit CompanyIdentifierNames.swift into the Bluetooth target. --- .../CompanyIdentifierMetadata.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Plugins/GenerateBluetoothDefinitions/CompanyIdentifierMetadata.swift diff --git a/Plugins/GenerateBluetoothDefinitions/CompanyIdentifierMetadata.swift b/Plugins/GenerateBluetoothDefinitions/CompanyIdentifierMetadata.swift new file mode 100644 index 000000000..f1bdfcfc9 --- /dev/null +++ b/Plugins/GenerateBluetoothDefinitions/CompanyIdentifierMetadata.swift @@ -0,0 +1,37 @@ +// +// CompanyIdentifierMetadata.swift +// Bluetooth +// +// Created by Alsey Coleman Miller on 1/12/25. +// + +import Foundation +import PackagePlugin + +extension GenerateBluetoothDefinitionsPlugin { + + func companyIdentifierMetadataBuildCommands( + for context: PluginContext, + target: SwiftSourceModuleTarget, + commands: inout [Command] + ) throws { + guard target.name == "Bluetooth" else { + return + } + // Generate Bluetooth Company Identifier Metadata (name lookup) + let outputDirectory = context.pluginWorkDirectoryURL + let outputURLs = [ + outputDirectory.appending(component: "CompanyIdentifierNames.swift") + ] + let arguments = ["companyIdentifierMetadata"] + outputURLs.map { $0.path() } + let command = Command.buildCommand( + displayName: "Generate Bluetooth Company Identifier Metadata", + executable: try context.tool(named: "GenerateBluetooth").url, + arguments: arguments, + inputFiles: [], + outputFiles: outputURLs + ) + assert(arguments.count == 2) + commands.append(command) + } +} From 2bde7180b558e61b870209600b5d803484af67f0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:26:15 -0400 Subject: [PATCH 07/15] Add build command for BluetoothUUID metadata generation Invokes GenerateBluetooth uuidMetadata to emit BluetoothUUIDMetadataNames.swift into the Bluetooth target. --- .../BluetoothUUIDMetadata.swift | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Plugins/GenerateBluetoothDefinitions/BluetoothUUIDMetadata.swift diff --git a/Plugins/GenerateBluetoothDefinitions/BluetoothUUIDMetadata.swift b/Plugins/GenerateBluetoothDefinitions/BluetoothUUIDMetadata.swift new file mode 100644 index 000000000..933a0d9fb --- /dev/null +++ b/Plugins/GenerateBluetoothDefinitions/BluetoothUUIDMetadata.swift @@ -0,0 +1,38 @@ +// +// BluetoothUUIDMetadata.swift +// Bluetooth +// +// Created by Alsey Coleman Miller on 1/12/25. +// + +import Foundation +import PackagePlugin + +extension GenerateBluetoothDefinitionsPlugin { + + func bluetoothUUIDMetadataBuildCommands( + for context: PluginContext, + target: SwiftSourceModuleTarget, + commands: inout [Command] + ) throws { + guard target.name == "Bluetooth" else { + return + } + // Generate Bluetooth UUID Metadata (name/type lookup, merged + // across all categories) + let outputDirectory = context.pluginWorkDirectoryURL + let outputURLs = [ + outputDirectory.appending(component: "BluetoothUUIDMetadataNames.swift") + ] + let arguments = ["uuidMetadata"] + outputURLs.map { $0.path() } + let command = Command.buildCommand( + displayName: "Generate Bluetooth UUID Metadata", + executable: try context.tool(named: "GenerateBluetooth").url, + arguments: arguments, + inputFiles: [], + outputFiles: outputURLs + ) + assert(arguments.count == 2) + commands.append(command) + } +} From dc34889033f4e9286e48ace6586a2252e1bbec8a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:27:24 -0400 Subject: [PATCH 08/15] Wire the metadata generation commands into the build plugin --- Plugins/GenerateBluetoothDefinitions/Plugin.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Plugins/GenerateBluetoothDefinitions/Plugin.swift b/Plugins/GenerateBluetoothDefinitions/Plugin.swift index 73c86e30b..6aae4bbac 100644 --- a/Plugins/GenerateBluetoothDefinitions/Plugin.swift +++ b/Plugins/GenerateBluetoothDefinitions/Plugin.swift @@ -21,6 +21,8 @@ struct GenerateBluetoothDefinitionsPlugin: BuildToolPlugin { // Generate Bluetooth Company Identifier Definitions try companyIdentifiersBuildCommands(for: context, target: target, commands: &commands) + // Generate Bluetooth Company Identifier Metadata (name lookup) + try companyIdentifierMetadataBuildCommands(for: context, target: target, commands: &commands) // Generate Bluetooth Company Identifier Unit Tests try companyIdentifierTestsBuildCommands(for: context, target: target, commands: &commands) // Generate Bluetooth Unit Identifier Definitions @@ -44,6 +46,8 @@ struct GenerateBluetoothDefinitionsPlugin: BuildToolPlugin { commands: &commands ) } + // Generate Bluetooth UUID Metadata (name/type lookup) + try bluetoothUUIDMetadataBuildCommands(for: context, target: target, commands: &commands) return commands } From c438c53bf214845fdb9ad963d6371b46bb48f25e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:27:35 -0400 Subject: [PATCH 09/15] Replace runtime JSON loading with generated metadata lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BluetoothUUID.metadata now reads from the build-time-generated generatedMetadata(for:) switch instead of loading and merging all six UUID category JSON files (and their BluetoothMetadata module) at runtime. Introduces a Foundation-free BluetoothUUID.Metadata struct in place of the old BluetoothMetadata.BluetoothUUID return type — this is an intentional breaking API change, since the old type's home module pulls in Foundation unconditionally. --- Sources/Bluetooth/BluetoothUUIDMetadata.swift | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/Sources/Bluetooth/BluetoothUUIDMetadata.swift b/Sources/Bluetooth/BluetoothUUIDMetadata.swift index 01b81186e..8851f633e 100644 --- a/Sources/Bluetooth/BluetoothUUIDMetadata.swift +++ b/Sources/Bluetooth/BluetoothUUIDMetadata.swift @@ -5,37 +5,36 @@ // Created by Alsey Coleman Miller on 1/12/25. // -#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded) -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation +#if Metadata +public extension BluetoothUUID { + + /// Bluetooth SIG assigned-numbers metadata for a 16-bit UUID. + struct Metadata: Equatable, Hashable, Sendable { + + /// The name of the defined UUID. + public let name: String + + /// The Bluetooth type namespace of the UUID. + public let type: String? + } +} + +#if !hasFeature(Embedded) +extension BluetoothUUID.Metadata: Codable {} #endif -import BluetoothMetadata public extension BluetoothUUID { /// Fetch the metadata for the UUID. - var metadata: BluetoothMetadata.BluetoothUUID? { - guard case let .bit16(rawValue) = self else { + /// + /// - SeeAlso: [Assigned Numbers](https://www.bluetooth.com/specifications/assigned-numbers/) + var metadata: Metadata? { + guard case let .bit16(rawValue) = self, + let generated = Self.generatedMetadata(for: rawValue) + else { return nil } - for file in files.values { - if let metadata = file[rawValue] { - return metadata - } - } - return nil + return Metadata(name: generated.name, type: generated.type) } } - -internal let files: [BluetoothMetadata.BluetoothUUID.Category: BluetoothMetadata.BluetoothUUID.File] = { - do { - return try BluetoothMetadata.BluetoothUUID.File.load() - } catch { - assertionFailure("Unable to load metadata: \(error)") - return [:] - } -}() - #endif From 58847acc0020186899dc15bf1223175d317c0d32 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:27:47 -0400 Subject: [PATCH 10/15] Replace runtime JSON loading with generated name lookup CompanyIdentifier.name now reads from the build-time-generated generatedName(for:) switch instead of loading and parsing CompanyIdentifier.json (via the BluetoothMetadata module) at runtime. --- .../Bluetooth/CompanyIdentifierMetadata.swift | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/Sources/Bluetooth/CompanyIdentifierMetadata.swift b/Sources/Bluetooth/CompanyIdentifierMetadata.swift index d71f0ce91..81f06c492 100644 --- a/Sources/Bluetooth/CompanyIdentifierMetadata.swift +++ b/Sources/Bluetooth/CompanyIdentifierMetadata.swift @@ -5,36 +5,14 @@ // Created by Alsey Coleman Miller on 1/12/25. // -#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded) -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif -import BluetoothMetadata - +#if Metadata public extension CompanyIdentifier { /// Bluetooth Company name. /// /// - SeeAlso: [Company Identifiers](https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers) var name: String? { - companyIdentifiers[rawValue] + Self.generatedName(for: rawValue) } } - -internal let companyIdentifiers: [UInt16: String] = { - do { - let file = try BluetoothMetadata.CompanyIdentifier.File.load() - var companyIdentifiers = [UInt16: String]() - for element in file.companyIdentifiers { - companyIdentifiers[element.id] = element.name - } - assert(companyIdentifiers.count == file.companyIdentifiers.count) - return companyIdentifiers - } catch { - assertionFailure("Unable to load metadata: \(error)") - return [:] - } -}() #endif From c75b77806f9df1706433b2241dfd1bbea22c2028 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:27:47 -0400 Subject: [PATCH 11/15] Drop now-unneeded Foundation/BluetoothMetadata imports UnitIdentifier.name/type already delegate to BluetoothUUID.metadata, which no longer needs Foundation or the BluetoothMetadata module now that it reads from generated Swift code. --- Sources/Bluetooth/UnitIdentifierMetadata.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Sources/Bluetooth/UnitIdentifierMetadata.swift b/Sources/Bluetooth/UnitIdentifierMetadata.swift index 1c0cd3c10..9a592de68 100644 --- a/Sources/Bluetooth/UnitIdentifierMetadata.swift +++ b/Sources/Bluetooth/UnitIdentifierMetadata.swift @@ -5,14 +5,7 @@ // Created by Alsey Coleman Miller on 1/12/25. // -#if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded) -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif -import BluetoothMetadata - +#if Metadata public extension UnitIdentifier { /// The name of the unit. From 5aece84b71c8b2c660f221c9e62be0d41bf16dae Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:27:57 -0400 Subject: [PATCH 12/15] Simplify Metadata #if conditions now that lookup is Foundation-free description and init?(_:) only reach through self.metadata, which no longer touches Foundation, WASI, or Embedded Swift concerns now that it's backed by generated Swift code rather than a JSON-loading runtime dependency. --- Sources/Bluetooth/BluetoothUUID.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Bluetooth/BluetoothUUID.swift b/Sources/Bluetooth/BluetoothUUID.swift index d62dc9238..64979002e 100644 --- a/Sources/Bluetooth/BluetoothUUID.swift +++ b/Sources/Bluetooth/BluetoothUUID.swift @@ -56,7 +56,7 @@ extension BluetoothUUID: Equatable { extension BluetoothUUID: CustomStringConvertible { public var description: String { - #if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded) + #if Metadata if let name = self.metadata?.name { return "\(rawValue) (\(name))" } else { @@ -73,7 +73,7 @@ extension BluetoothUUID: CustomStringConvertible { extension BluetoothUUID: LosslessStringConvertible { public init?(_ string: String) { - #if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded) + #if Metadata var rawValue = string var name: String? // Find UUID name From bf350226dd3932b54583116f028e66e29175141c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:27:57 -0400 Subject: [PATCH 13/15] Simplify Metadata #if condition now that name is Foundation-free --- Sources/Bluetooth/CompanyIdentifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Bluetooth/CompanyIdentifier.swift b/Sources/Bluetooth/CompanyIdentifier.swift index 7e5d0dd61..9e4a7d658 100644 --- a/Sources/Bluetooth/CompanyIdentifier.swift +++ b/Sources/Bluetooth/CompanyIdentifier.swift @@ -50,7 +50,7 @@ extension CompanyIdentifier: ExpressibleByIntegerLiteral { extension CompanyIdentifier: CustomStringConvertible { public var description: String { - #if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded) + #if Metadata return name ?? rawValue.description #else return rawValue.description From fbfc46556a1e5c956af42b55407b1e8bf588b577 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:27:57 -0400 Subject: [PATCH 14/15] Simplify Metadata #if condition now that name is Foundation-free --- Sources/Bluetooth/UnitIdentifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Bluetooth/UnitIdentifier.swift b/Sources/Bluetooth/UnitIdentifier.swift index a15bb2978..9e635e023 100644 --- a/Sources/Bluetooth/UnitIdentifier.swift +++ b/Sources/Bluetooth/UnitIdentifier.swift @@ -30,7 +30,7 @@ extension UnitIdentifier: ExpressibleByIntegerLiteral { extension UnitIdentifier: CustomStringConvertible { public var description: String { - #if Metadata && canImport(Foundation) && !os(WASI) && !hasFeature(Embedded) + #if Metadata return self.name ?? rawValueDescription #else return rawValueDescription From 306c0fe3e0868c9503a83220ff5455cf0104529f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 3 Aug 2026 00:28:06 -0400 Subject: [PATCH 15/15] Fix target-index drift that silently disabled generated tests package.targets[4] no longer pointed at BluetoothTests once BluetoothSDP was inserted ahead of it, so GenerateBluetoothDefinitions was never actually attached to the test target and its generated test files were never compiled or run. Look targets up by name instead of a hardcoded index. --- Package.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index 345b9e297..f8a466e5d 100644 --- a/Package.swift +++ b/Package.swift @@ -206,12 +206,18 @@ if generateCode { ] ) ] - package.targets[0].plugins = [ - "GenerateBluetoothDefinitions" - ] - package.targets[4].plugins = [ - "GenerateBluetoothDefinitions" - ] + // Name-based lookup, not a hardcoded index — the target array's + // order has changed before (silently detaching this plugin from + // BluetoothTests when a new target was inserted ahead of it) and + // will again. + for name in ["Bluetooth", "BluetoothTests"] { + guard let index = package.targets.firstIndex(where: { $0.name == name }) else { + fatalError("Missing target \(name)") + } + package.targets[index].plugins = [ + "GenerateBluetoothDefinitions" + ] + } } if enableMacros {