Skip to content

Commit 7f0fd5e

Browse files
p-linnaneclaude
andauthored
feat: improve development kernel handling and chip display (#4)
* feat: improve development kernel handling and chip display - Resolve device mappings for development kernelcaches by normalizing filenames to release equivalents for board codename lookup - Set chip to "Multiple" when a kernel spans multiple chip families instead of showing "Unknown" - Show resolved chip names in kernel cards (sorted by generation/tier) with "Development" badge - Deduplicate devices in Hardware Support when they appear in both release and development kernels - Add opaque background to sidebar "Show pre-releases" toggle - Rescan macOS 12.3 and 12.3.1 with updated scanner Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve swiftlint violations and CI scheme discovery - Extract KernelCard view and sortedChipNames computed property to reduce closure/function body lengths in ReleaseDetailView - Condense IPSWScanner comment to stay within type body length limit - Add -workspace . to CI xcodebuild commands so macOSdb-Package scheme is discoverable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: remove unused SPM cache steps xcodebuild with -workspace uses DerivedData, not .build, so the SPM cache was never populated and produced path validation warnings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5b20b25 commit 7f0fd5e

10 files changed

Lines changed: 237 additions & 53 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,10 @@ jobs:
3030
with:
3131
persist-credentials: false
3232

33-
- name: Cache SPM packages
34-
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
35-
with:
36-
path: .build
37-
key: spm-${{ runner.os }}-${{ hashFiles('Package.resolved') }}
38-
restore-keys: |
39-
spm-${{ runner.os }}-
40-
4133
- name: Test with Thread Sanitizer
4234
run: |
4335
xcodebuild test \
36+
-workspace . \
4437
-scheme macOSdb-Package \
4538
-destination 'platform=macOS' \
4639
-enableCodeCoverage YES \
@@ -73,17 +66,10 @@ jobs:
7366
with:
7467
persist-credentials: false
7568

76-
- name: Cache SPM packages
77-
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
78-
with:
79-
path: .build
80-
key: spm-${{ runner.os }}-${{ hashFiles('Package.resolved') }}
81-
restore-keys: |
82-
spm-${{ runner.os }}-
83-
8469
- name: Test with Address Sanitizer
8570
run: |
8671
xcodebuild test \
72+
-workspace . \
8773
-scheme macOSdb-Package \
8874
-destination 'platform=macOS' \
8975
-enableAddressSanitizer YES \

Sources/macOSdbKit/Models/KernelInfo.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,30 @@ public struct KernelInfo: Codable, Identifiable, Hashable, Sendable {
4949
self.deviceChips = deviceChips
5050
}
5151

52+
public var isDevelopment: Bool {
53+
file.contains("kernelcache.development.")
54+
}
55+
56+
/// Unique chip names from `deviceChips`, sorted by generation then tier
57+
/// (generation 0 sorts last). Falls back to the kernel-level `chip`.
58+
public var sortedChipNames: [String] {
59+
guard let deviceChips, !deviceChips.isEmpty else { return [chip] }
60+
return deviceChips
61+
.reduce(into: [String]()) { result, dc in
62+
if !result.contains(dc.chip) { result.append(dc.chip) }
63+
}
64+
.sorted { lhs, rhs in
65+
let lhsChip = ChipFamily.from(chipName: lhs)
66+
let rhsChip = ChipFamily.from(chipName: rhs)
67+
let lhsGen = lhsChip?.generation ?? 0
68+
let rhsGen = rhsChip?.generation ?? 0
69+
let lhsSortGen = lhsGen == 0 ? Int.max : lhsGen
70+
let rhsSortGen = rhsGen == 0 ? Int.max : rhsGen
71+
if lhsSortGen != rhsSortGen { return lhsSortGen < rhsSortGen }
72+
return (lhsChip?.tier ?? .base) < (rhsChip?.tier ?? .base)
73+
}
74+
}
75+
5276
public var chipFamily: ChipFamily? {
5377
ChipFamily.from(chipName: chip)
5478
}

Sources/macOSdbKit/Scanner/IPSWScanner.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@ public actor IPSWScanner {
105105
for path in kernelcaches {
106106
guard var kernel = KernelParser.parse(kernelcachePath: path) else { continue }
107107
if kernel.devices.isEmpty {
108-
// Try BuildManifest device map first, then fall back to hardcoded board codename map
108+
// Try BuildManifest device map, then board codename map (normalizing dev → release)
109+
let releaseKey = kernel.file.replacingOccurrences(of: ".development.", with: ".release.")
109110
let devices = deviceMap[kernel.file]
110-
?? boardCodeNameDevices[kernel.file]
111+
?? boardCodeNameDevices[kernel.file] ?? boardCodeNameDevices[releaseKey]
111112
if let devices {
112113
kernel = KernelInfo(
113114
file: kernel.file,
@@ -130,12 +131,19 @@ public actor IPSWScanner {
130131
return DeviceChip(device: device, chip: chip.displayName)
131132
}
132133
guard !deviceChips.isEmpty else { return kernel }
134+
let resolvedChip: String
135+
if kernel.chip == "Unknown" {
136+
let uniqueChips = Set(deviceChips.map(\.chip))
137+
resolvedChip = uniqueChips.count == 1 ? uniqueChips.first! : "Multiple"
138+
} else {
139+
resolvedChip = kernel.chip
140+
}
133141
return KernelInfo(
134142
file: kernel.file,
135143
darwinVersion: kernel.darwinVersion,
136144
xnuVersion: kernel.xnuVersion,
137145
arch: kernel.arch,
138-
chip: kernel.chip,
146+
chip: resolvedChip,
139147
devices: kernel.devices,
140148
deviceChips: deviceChips
141149
)

Sources/macOSdbKit/Scanner/KernelParser.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,15 @@ public enum KernelParser {
114114
/// - `kernelcache.release.Mac16,1_2_3_10_12_13` → `["Mac16,1", "Mac16,2", ...]`
115115
/// - `kernelcache.release.MacBookAir10,1_MacBookPro17,1` → `["MacBookAir10,1", "MacBookPro17,1"]`
116116
/// - `kernelcache.release.VirtualMac2,1` → `["VirtualMac2,1"]`
117+
/// - `kernelcache.development.Mac16,1_2_3` → `["Mac16,1", "Mac16,2", "Mac16,3"]`
117118
/// - `kernelcache.release.mac13g` → `[]` (board codename, not device model IDs)
118119
public static func parseDevicesFromFilename(_ filename: String) -> [String] {
119120
var suffix = filename
120-
if let range = suffix.range(of: "kernelcache.release.") {
121-
suffix = String(suffix[range.upperBound...])
121+
for prefix in ["kernelcache.release.", "kernelcache.development."] {
122+
if let range = suffix.range(of: prefix) {
123+
suffix = String(suffix[range.upperBound...])
124+
break
125+
}
122126
}
123127

124128
// Handle VirtualMac specially

Tests/macOSdbKitTests/ScannerTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,22 @@ struct KernelParserTests {
170170
)
171171
#expect(devices.isEmpty)
172172
}
173+
174+
@Test("Parse device models from development kernelcache")
175+
func parseDevelopmentKernelcache() {
176+
let devices = KernelParser.parseDevicesFromFilename(
177+
"kernelcache.development.Mac16,1_2_3"
178+
)
179+
#expect(devices == ["Mac16,1", "Mac16,2", "Mac16,3"])
180+
}
181+
182+
@Test("Development board codename returns empty devices")
183+
func parseDevelopmentBoardCodename() {
184+
let devices = KernelParser.parseDevicesFromFilename(
185+
"kernelcache.development.mac13g"
186+
)
187+
#expect(devices.isEmpty)
188+
}
173189
}
174190

175191
@Suite("Scanner config tests")

data/releases/12/macOS-12.3-21E230.json

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,71 @@
110110
"kernels" : [
111111
{
112112
"arch" : "ARM64",
113-
"chip" : "Unknown",
113+
"chip" : "Multiple",
114114
"darwinVersion" : "21.4.0",
115+
"deviceChips" : [
116+
{
117+
"chip" : "M1",
118+
"device" : "MacBookAir10,1"
119+
},
120+
{
121+
"chip" : "M1",
122+
"device" : "MacBookPro17,1"
123+
},
124+
{
125+
"chip" : "M1",
126+
"device" : "Macmini9,1"
127+
},
128+
{
129+
"chip" : "M1",
130+
"device" : "iMac21,1"
131+
},
132+
{
133+
"chip" : "M1",
134+
"device" : "iMac21,2"
135+
},
136+
{
137+
"chip" : "M1 Max",
138+
"device" : "Mac13,1"
139+
},
140+
{
141+
"chip" : "M1 Ultra",
142+
"device" : "Mac13,2"
143+
},
144+
{
145+
"chip" : "M1 Pro",
146+
"device" : "MacBookPro18,1"
147+
},
148+
{
149+
"chip" : "M1 Max",
150+
"device" : "MacBookPro18,2"
151+
},
152+
{
153+
"chip" : "M1 Pro",
154+
"device" : "MacBookPro18,3"
155+
},
156+
{
157+
"chip" : "M1 Max",
158+
"device" : "MacBookPro18,4"
159+
},
160+
{
161+
"chip" : "Virtual Mac",
162+
"device" : "VirtualMac2,1"
163+
}
164+
],
115165
"devices" : [
116-
166+
"MacBookAir10,1",
167+
"MacBookPro17,1",
168+
"Macmini9,1",
169+
"iMac21,1",
170+
"iMac21,2",
171+
"Mac13,1",
172+
"Mac13,2",
173+
"MacBookPro18,1",
174+
"MacBookPro18,2",
175+
"MacBookPro18,3",
176+
"MacBookPro18,4",
177+
"VirtualMac2,1"
117178
],
118179
"file" : "kernelcache.development.mac13g",
119180
"xnuVersion" : "8020.101.4"

data/releases/12/macOS-12.3.1-21E258.json

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,71 @@
110110
"kernels" : [
111111
{
112112
"arch" : "ARM64",
113-
"chip" : "Unknown",
113+
"chip" : "Multiple",
114114
"darwinVersion" : "21.4.0",
115+
"deviceChips" : [
116+
{
117+
"chip" : "M1",
118+
"device" : "MacBookAir10,1"
119+
},
120+
{
121+
"chip" : "M1",
122+
"device" : "MacBookPro17,1"
123+
},
124+
{
125+
"chip" : "M1",
126+
"device" : "Macmini9,1"
127+
},
128+
{
129+
"chip" : "M1",
130+
"device" : "iMac21,1"
131+
},
132+
{
133+
"chip" : "M1",
134+
"device" : "iMac21,2"
135+
},
136+
{
137+
"chip" : "M1 Max",
138+
"device" : "Mac13,1"
139+
},
140+
{
141+
"chip" : "M1 Ultra",
142+
"device" : "Mac13,2"
143+
},
144+
{
145+
"chip" : "M1 Pro",
146+
"device" : "MacBookPro18,1"
147+
},
148+
{
149+
"chip" : "M1 Max",
150+
"device" : "MacBookPro18,2"
151+
},
152+
{
153+
"chip" : "M1 Pro",
154+
"device" : "MacBookPro18,3"
155+
},
156+
{
157+
"chip" : "M1 Max",
158+
"device" : "MacBookPro18,4"
159+
},
160+
{
161+
"chip" : "Virtual Mac",
162+
"device" : "VirtualMac2,1"
163+
}
164+
],
115165
"devices" : [
116-
166+
"MacBookAir10,1",
167+
"MacBookPro17,1",
168+
"Macmini9,1",
169+
"iMac21,1",
170+
"iMac21,2",
171+
"Mac13,1",
172+
"Mac13,2",
173+
"MacBookPro18,1",
174+
"MacBookPro18,2",
175+
"MacBookPro18,3",
176+
"MacBookPro18,4",
177+
"VirtualMac2,1"
117178
],
118179
"file" : "kernelcache.development.mac13g",
119180
"xnuVersion" : "8020.101.4"

macOSdbApp/Views/ChipSupportView.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ struct ChipSupportView: View {
7878
for dc in deviceChips {
7979
guard let chip = ChipFamily.from(chipName: dc.chip) else { continue }
8080
if let index = groups.firstIndex(where: { $0.chip == chip }) {
81-
groups[index].devices.append(dc.device)
81+
if !groups[index].devices.contains(dc.device) {
82+
groups[index].devices.append(dc.device)
83+
}
8284
} else {
8385
groups.append(ChipGroup(chip: chip, devices: [dc.device], arch: kernel.arch))
8486
}
@@ -89,7 +91,9 @@ struct ChipSupportView: View {
8991
let chip = DeviceRegistry.chip(for: device) ?? kernelChip
9092
guard let chip else { continue }
9193
if let index = groups.firstIndex(where: { $0.chip == chip }) {
92-
groups[index].devices.append(device)
94+
if !groups[index].devices.contains(device) {
95+
groups[index].devices.append(device)
96+
}
9397
} else {
9498
groups.append(ChipGroup(chip: chip, devices: [device], arch: kernel.arch))
9599
}

macOSdbApp/Views/ReleaseDetailView.swift

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -100,32 +100,7 @@ struct ReleaseDetailView: View {
100100
.fontWeight(.semibold)
101101

102102
ForEach(release.kernels) { kernel in
103-
HStack(alignment: .top, spacing: 16) {
104-
VStack(alignment: .leading, spacing: 4) {
105-
Text(kernel.chip)
106-
.font(.headline)
107-
Text("Darwin \(kernel.darwinVersion)")
108-
.font(.callout)
109-
if let xnuVersion = kernel.xnuVersion {
110-
Text("XNU \(xnuVersion)")
111-
.font(.callout)
112-
.foregroundStyle(.secondary)
113-
}
114-
}
115-
116-
Spacer()
117-
118-
VStack(alignment: .trailing, spacing: 4) {
119-
Text(kernel.arch)
120-
.font(.caption)
121-
.foregroundStyle(.secondary)
122-
Text(kernel.devices.joined(separator: ", "))
123-
.font(.caption2)
124-
.foregroundStyle(.tertiary)
125-
}
126-
}
127-
.padding(12)
128-
.background(.quaternary.opacity(0.3), in: RoundedRectangle(cornerRadius: 8))
103+
KernelCard(kernel: kernel)
129104
}
130105
}
131106
}
@@ -152,3 +127,46 @@ struct ReleaseDetailView: View {
152127
}
153128
}
154129
}
130+
131+
// MARK: - Kernel Card
132+
133+
private struct KernelCard: View {
134+
let kernel: KernelInfo
135+
136+
var body: some View {
137+
HStack(alignment: .top, spacing: 16) {
138+
VStack(alignment: .leading, spacing: 4) {
139+
HStack(spacing: 6) {
140+
Text(kernel.sortedChipNames.joined(separator: ", "))
141+
.font(.headline)
142+
if kernel.isDevelopment {
143+
Text("Development")
144+
.font(.caption2)
145+
.fontWeight(.medium)
146+
.foregroundStyle(.orange)
147+
}
148+
}
149+
Text("Darwin \(kernel.darwinVersion)")
150+
.font(.callout)
151+
if let xnuVersion = kernel.xnuVersion {
152+
Text("XNU \(xnuVersion)")
153+
.font(.callout)
154+
.foregroundStyle(.secondary)
155+
}
156+
}
157+
158+
Spacer()
159+
160+
VStack(alignment: .trailing, spacing: 4) {
161+
Text(kernel.arch)
162+
.font(.caption)
163+
.foregroundStyle(.secondary)
164+
Text(kernel.devices.joined(separator: ", "))
165+
.font(.caption2)
166+
.foregroundStyle(.tertiary)
167+
}
168+
}
169+
.padding(12)
170+
.background(.quaternary.opacity(0.3), in: RoundedRectangle(cornerRadius: 8))
171+
}
172+
}

0 commit comments

Comments
 (0)