Skip to content

Commit 9eaa0ea

Browse files
obj-pclaude
andcommitted
Add inferredPlatform helper, fix SetupCache for dylib, fix CI
- Add inferredPlatform(for:) and inferredPlatformAsync(for:) to SPMBuildSystem, replacing 4 duplicated string-comparison blocks with single-source-of-truth helpers. - Fix SetupCache to persist and validate dylibPath. The cache entry now includes the dylib path, and load() verifies the dylib exists. - Update validateArtifacts to accept .dylib in addition to .a when checking -l library flags. - Update SetupCacheTests for the new Result shape and dylib artifacts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bfc106b commit 9eaa0ea

7 files changed

Lines changed: 64 additions & 31 deletions

File tree

Sources/PreviewsCLI/MCPServer.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -537,9 +537,7 @@ private func handlePreviewStart(params: CallTool.Parameters, macCompiler: Compil
537537
platformStr = explicit
538538
} else if let configPlatform = config?.platform {
539539
platformStr = configPlatform
540-
} else if let spmPlatforms = await SPMBuildSystem.detectPlatformsAsync(for: fileURL),
541-
spmPlatforms.contains("ios"), !spmPlatforms.contains("macos")
542-
{
540+
} else if await SPMBuildSystem.inferredPlatformAsync(for: fileURL) == .iOS {
543541
platformStr = "ios"
544542
} else {
545543
platformStr = "macos"

Sources/PreviewsCLI/RunCommand.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ struct RunCommand: ParsableCommand {
7979
let resolvedPlatform: CLIPlatform = {
8080
if platform != .macos { return platform }
8181
if let cp = projectConfig?.platform, cp == "ios" { return .ios }
82-
if let spmPlatforms = SPMBuildSystem.detectPlatforms(for: fileURL),
83-
spmPlatforms.contains("ios"), !spmPlatforms.contains("macos")
84-
{
82+
if SPMBuildSystem.inferredPlatform(for: fileURL) == .iOS {
8583
return .ios
8684
}
8785
return platform

Sources/PreviewsCLI/SnapshotCommand.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ struct SnapshotCommand: ParsableCommand {
8181
let resolvedPlatform: CLIPlatform = {
8282
if platform != .macos { return platform }
8383
if let cp = projectConfig?.platform, cp == "ios" { return .ios }
84-
if let spmPlatforms = SPMBuildSystem.detectPlatforms(for: fileURL),
85-
spmPlatforms.contains("ios"), !spmPlatforms.contains("macos")
86-
{
84+
if SPMBuildSystem.inferredPlatform(for: fileURL) == .iOS {
8785
return .ios
8886
}
8987
return platform

Sources/PreviewsCLI/VariantsCommand.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,7 @@ struct VariantsCommand: ParsableCommand {
105105
let resolvedPlatform: CLIPlatform = {
106106
if platform != .macos { return platform }
107107
if let cp = configResult?.config.platform, cp == "ios" { return .ios }
108-
if let spmPlatforms = SPMBuildSystem.detectPlatforms(for: fileURL),
109-
spmPlatforms.contains("ios"), !spmPlatforms.contains("macos")
110-
{
108+
if SPMBuildSystem.inferredPlatform(for: fileURL) == .iOS {
111109
return .ios
112110
}
113111
return platform

Sources/PreviewsCore/SPMBuildSystem.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ public actor SPMBuildSystem: BuildSystem {
7070
return nil
7171
}
7272

73+
/// Returns .iOS if the SPM package declares iOS but not macOS; nil otherwise.
74+
/// Convenience for the common "should we default to iOS?" check at CLI/MCP call sites.
75+
public static func inferredPlatform(for sourceFile: URL) -> PreviewPlatform? {
76+
guard let platforms = detectPlatforms(for: sourceFile) else { return nil }
77+
if platforms.contains("ios"), !platforms.contains("macos") { return .iOS }
78+
return nil
79+
}
80+
81+
/// Async variant of `inferredPlatform` for MCP server contexts.
82+
public static func inferredPlatformAsync(for sourceFile: URL) async -> PreviewPlatform? {
83+
guard let platforms = await detectPlatformsAsync(for: sourceFile) else { return nil }
84+
if platforms.contains("ios"), !platforms.contains("macos") { return .iOS }
85+
return nil
86+
}
87+
7388
private static func decodePlatforms(from data: Data) -> [String]? {
7489
struct PlatformInfo: Decodable {
7590
let platforms: [PlatformEntry]?

Sources/PreviewsCore/SetupCache.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public enum SetupCache {
107107
let moduleName: String
108108
let typeName: String
109109
let compilerFlags: [String]
110+
let dylibPath: String
110111
let sourceHash: String
111112
let swiftVersion: String
112113
let platform: String
@@ -149,10 +150,14 @@ public enum SetupCache {
149150
return nil
150151
}
151152

153+
let dylibURL = URL(fileURLWithPath: entry.dylibPath)
154+
guard FileManager.default.fileExists(atPath: dylibURL.path) else { return nil }
155+
152156
return SetupBuilder.Result(
153157
moduleName: entry.moduleName,
154158
typeName: entry.typeName,
155-
compilerFlags: entry.compilerFlags
159+
compilerFlags: entry.compilerFlags,
160+
dylibPath: dylibURL
156161
)
157162
}
158163

@@ -176,6 +181,7 @@ public enum SetupCache {
176181
moduleName: result.moduleName,
177182
typeName: result.typeName,
178183
compilerFlags: result.compilerFlags,
184+
dylibPath: result.dylibPath.path,
179185
sourceHash: sourceHash,
180186
swiftVersion: swiftVersion,
181187
platform: platform.rawValue
@@ -241,10 +247,13 @@ public enum SetupCache {
241247
guard fm.fileExists(atPath: dir) else { return false }
242248
}
243249

244-
// Validate -l libraries resolve to lib<name>.a under -L dirs
250+
// Validate -l libraries resolve to lib<name>.dylib or lib<name>.a under -L dirs
245251
for lib in libs {
246252
let found = lDirs.contains { dir in
247-
fm.fileExists(atPath: (dir as NSString).appendingPathComponent("lib\(lib).a"))
253+
fm.fileExists(
254+
atPath: (dir as NSString).appendingPathComponent("lib\(lib).dylib"))
255+
|| fm.fileExists(
256+
atPath: (dir as NSString).appendingPathComponent("lib\(lib).a"))
248257
}
249258
guard found else { return false }
250259
}

Tests/PreviewsCoreTests/SetupCacheTests.swift

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ struct SetupCacheTests {
134134
// MARK: - Store / Load
135135

136136
/// Create a fake build artifacts directory that matches expected compiler flags.
137-
private func makeArtifacts(packageDir: URL, moduleName: String) throws -> [String] {
137+
private func makeArtifacts(packageDir: URL, moduleName: String) throws
138+
-> (flags: [String], dylibPath: URL)
139+
{
138140
let buildDir = packageDir.appendingPathComponent(".build/debug")
139141
let modulesDir = buildDir.appendingPathComponent("Modules")
140142
try FileManager.default.createDirectory(
@@ -147,11 +149,14 @@ struct SetupCacheTests {
147149
try Data("fake".utf8).write(
148150
to: swiftmoduleDir.appendingPathComponent("arm64-apple-macos.swiftmodule"))
149151

150-
// Create static library
151-
try Data("fake".utf8).write(
152-
to: buildDir.appendingPathComponent("lib\(moduleName).a"))
152+
// Create setup dylib
153+
let dylibPath = buildDir.appendingPathComponent("libPreviewSetup.dylib")
154+
try Data("fake".utf8).write(to: dylibPath)
153155

154-
return ["-I", modulesDir.path, "-L", buildDir.path, "-l\(moduleName)"]
156+
return (
157+
flags: ["-I", modulesDir.path, "-L", buildDir.path, "-lPreviewSetup"],
158+
dylibPath: dylibPath
159+
)
155160
}
156161

157162
@Test("load returns nil when no cache file exists")
@@ -191,7 +196,8 @@ struct SetupCacheTests {
191196
// Store a valid entry with flags pointing to non-existent paths
192197
let fakeResult = SetupBuilder.Result(
193198
moduleName: "TestSetup", typeName: "Setup",
194-
compilerFlags: ["-I", "/nonexistent/Modules", "-L", "/nonexistent"])
199+
compilerFlags: ["-I", "/nonexistent/Modules", "-L", "/nonexistent"],
200+
dylibPath: URL(fileURLWithPath: "/nonexistent/libPreviewSetup.dylib"))
195201
SetupCache.store(
196202
fakeResult, packageDir: dir, platform: .macOS,
197203
sourceHash: "abc123", swiftVersion: "Swift 6.0")
@@ -216,13 +222,20 @@ struct SetupCacheTests {
216222
try Data("fake".utf8).write(
217223
to: swiftmoduleDir.appendingPathComponent("arm64.swiftmodule"))
218224

225+
let dylibPath = buildDir.appendingPathComponent("libPreviewSetup.dylib")
226+
try Data("fake".utf8).write(to: dylibPath)
227+
219228
let fakeResult = SetupBuilder.Result(
220229
moduleName: "TestSetup", typeName: "Setup",
221-
compilerFlags: ["-I", modulesDir.path, "-L", buildDir.path, "-lTestSetup"])
230+
compilerFlags: ["-I", modulesDir.path, "-L", buildDir.path, "-lPreviewSetup"],
231+
dylibPath: dylibPath)
222232
SetupCache.store(
223233
fakeResult, packageDir: dir, platform: .macOS,
224234
sourceHash: "abc123", swiftVersion: "Swift 6.0")
225235

236+
// Delete the dylib to simulate missing artifact
237+
try FileManager.default.removeItem(at: dylibPath)
238+
226239
let loaded = SetupCache.load(
227240
packageDir: dir, platform: .macOS, sourceHash: "abc123",
228241
swiftVersion: "Swift 6.0")
@@ -234,9 +247,10 @@ struct SetupCacheTests {
234247
let dir = try makePackageDir()
235248
defer { try? FileManager.default.removeItem(at: dir) }
236249

237-
let flags = try makeArtifacts(packageDir: dir, moduleName: "TestSetup")
250+
let (flags, dylibPath) = try makeArtifacts(packageDir: dir, moduleName: "TestSetup")
238251
let original = SetupBuilder.Result(
239-
moduleName: "TestSetup", typeName: "AppSetup", compilerFlags: flags)
252+
moduleName: "TestSetup", typeName: "AppSetup", compilerFlags: flags,
253+
dylibPath: dylibPath)
240254

241255
SetupCache.store(
242256
original, packageDir: dir, platform: .macOS,
@@ -270,7 +284,8 @@ struct SetupCacheTests {
270284
}
271285

272286
let result = SetupBuilder.Result(
273-
moduleName: "Test", typeName: "Setup", compilerFlags: [])
287+
moduleName: "Test", typeName: "Setup", compilerFlags: [],
288+
dylibPath: URL(fileURLWithPath: "/tmp/libPreviewSetup.dylib"))
274289
// Should not throw — errors are swallowed
275290
SetupCache.store(
276291
result, packageDir: dir, platform: .macOS,
@@ -284,9 +299,10 @@ struct SetupCacheTests {
284299
let dir = try makePackageDir()
285300
defer { try? FileManager.default.removeItem(at: dir) }
286301

287-
let macFlags = try makeArtifacts(packageDir: dir, moduleName: "TestSetup")
302+
let (macFlags, macDylibPath) = try makeArtifacts(packageDir: dir, moduleName: "TestSetup")
288303
let macResult = SetupBuilder.Result(
289-
moduleName: "TestSetup", typeName: "Setup", compilerFlags: macFlags)
304+
moduleName: "TestSetup", typeName: "Setup", compilerFlags: macFlags,
305+
dylibPath: macDylibPath)
290306

291307
// Create separate iOS artifacts
292308
let iosBuildDir = dir.appendingPathComponent(".build/ios-debug")
@@ -296,11 +312,12 @@ struct SetupCacheTests {
296312
at: iosSwiftmodule, withIntermediateDirectories: true)
297313
try Data("fake".utf8).write(
298314
to: iosSwiftmodule.appendingPathComponent("arm64.swiftmodule"))
299-
try Data("fake".utf8).write(
300-
to: iosBuildDir.appendingPathComponent("libTestSetup.a"))
301-
let iosFlags = ["-I", iosModulesDir.path, "-L", iosBuildDir.path, "-lTestSetup"]
315+
let iosDylibPath = iosBuildDir.appendingPathComponent("libPreviewSetup.dylib")
316+
try Data("fake".utf8).write(to: iosDylibPath)
317+
let iosFlags = ["-I", iosModulesDir.path, "-L", iosBuildDir.path, "-lPreviewSetup"]
302318
let iosResult = SetupBuilder.Result(
303-
moduleName: "TestSetup", typeName: "Setup", compilerFlags: iosFlags)
319+
moduleName: "TestSetup", typeName: "Setup", compilerFlags: iosFlags,
320+
dylibPath: iosDylibPath)
304321

305322
// Store both
306323
SetupCache.store(

0 commit comments

Comments
 (0)