Skip to content

Commit d33814b

Browse files
Fix incremental builds for embedInCode resources (#7616)
SwiftPM's incremental build did not detect changes in embedded resources. ### Motivation: SwiftPM's `embedInCode` feature did not detect changes in the resource files and it led to using old resource contents even though they were modified. You can reproduce it with the fixture in this repository like below: ```console $ rm -rf Fixtures/Resources/EmbedInCodeSimple/.build $ swift run --package-path Fixtures/Resources/EmbedInCodeSimple hello world $ echo "update 1" > Fixtures/Resources/EmbedInCodeSimple/Sources/EmbedInCodeSimple/best.txt $ swift run --package-path Fixtures/Resources/EmbedInCodeSimple update 1 $ echo "update 2" > Fixtures/Resources/EmbedInCodeSimple/Sources/EmbedInCodeSimple/best.txt $ swift run --package-path Fixtures/Resources/EmbedInCodeSimple update 1 ``` The first update ("update 1") is reflected in the final executable output because: - The `embedded_resources.swift` is generated while computing build descriptions - Build descriptions are always re-computed in the 2nd build iteration to build `PackageStructure` build cache However, the second update ("update 2") is not updated because we don't re-compute build descriptions in this stage, and the llbuild build system does not track embedding resource files. ### Modifications: Generate `embedded_resources.swift` during llbuild build step instead of during computing build descriptions. ### Result: In this way, llbuild build system can correctly detect resource file modifications in any build iterations.
1 parent 5749d86 commit d33814b

File tree

4 files changed

+74
-30
lines changed

4 files changed

+74
-30
lines changed

Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,13 @@ public final class SwiftTargetBuildDescription {
7474
return resources.filter { $0.rule != .embedInCode }.isEmpty == false
7575
}
7676

77-
private var needsResourceEmbedding: Bool {
78-
return resources.filter { $0.rule == .embedInCode }.isEmpty == false
77+
var resourceFilesToEmbed: [AbsolutePath] {
78+
return resources.filter { $0.rule == .embedInCode }.map { $0.path }
7979
}
8080

81+
/// The path to Swift source file embedding resource contents if needed.
82+
private(set) var resourcesEmbeddingSource: AbsolutePath?
83+
8184
/// The list of all source files in the target, including the derived ones.
8285
public var sources: [AbsolutePath] {
8386
self.target.sources.paths + self.derivedSources.paths + self.pluginDerivedSources.paths
@@ -312,7 +315,10 @@ public final class SwiftTargetBuildDescription {
312315
}
313316
}
314317

315-
try self.generateResourceEmbeddingCode()
318+
if !resourceFilesToEmbed.isEmpty {
319+
resourcesEmbeddingSource = try addResourceEmbeddingSource()
320+
}
321+
316322
try self.generateTestObservation()
317323
}
318324

@@ -343,32 +349,10 @@ public final class SwiftTargetBuildDescription {
343349
try self.fileSystem.writeIfChanged(path: path, string: content)
344350
}
345351

346-
// FIXME: This will not work well for large files, as we will store the entire contents, plus its byte array
347-
// representation in memory and also `writeIfChanged()` will read the entire generated file again.
348-
private func generateResourceEmbeddingCode() throws {
349-
guard needsResourceEmbedding else { return }
350-
351-
var content =
352-
"""
353-
struct PackageResources {
354-
355-
"""
356-
357-
try resources.forEach {
358-
guard $0.rule == .embedInCode else { return }
359-
360-
let variableName = $0.path.basename.spm_mangledToC99ExtendedIdentifier()
361-
let fileContent = try Data(contentsOf: URL(fileURLWithPath: $0.path.pathString)).map { String($0) }.joined(separator: ",")
362-
363-
content += "static let \(variableName): [UInt8] = [\(fileContent)]\n"
364-
}
365-
366-
content += "}"
367-
352+
private func addResourceEmbeddingSource() throws -> AbsolutePath {
368353
let subpath = try RelativePath(validating: "embedded_resources.swift")
369354
self.derivedSources.relativePaths.append(subpath)
370-
let path = self.derivedSources.root.appending(subpath)
371-
try self.fileSystem.writeIfChanged(path: path, string: content)
355+
return self.derivedSources.root.appending(subpath)
372356
}
373357

374358
/// Generate the resource bundle accessor, if appropriate.

Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,11 @@ extension LLBuildManifestBuilder {
414414
inputs.append(resourcesNode)
415415
}
416416

417+
if let resourcesEmbeddingSource = target.resourcesEmbeddingSource {
418+
let resourceFilesToEmbed = target.resourceFilesToEmbed
419+
self.manifest.addWriteEmbeddedResourcesCommand(resources: resourceFilesToEmbed, outputPath: resourcesEmbeddingSource)
420+
}
421+
417422
func addStaticTargetInputs(_ target: ResolvedModule) throws {
418423
// Ignore C Modules.
419424
if target.underlying is SystemLibraryTarget { return }

Sources/LLBuildManifest/LLBuildManifest.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public enum WriteAuxiliary {
2727
LinkFileList.self,
2828
SourcesFileList.self,
2929
SwiftGetVersion.self,
30-
XCTestInfoPlist.self
30+
XCTestInfoPlist.self,
31+
EmbeddedResources.self,
3132
]
3233

3334
public struct EntitlementPlist: AuxiliaryFileType {
@@ -159,6 +160,35 @@ public enum WriteAuxiliary {
159160
case undefinedPrincipalClass
160161
}
161162
}
163+
164+
public struct EmbeddedResources: AuxiliaryFileType {
165+
public static let name = "embedded-resources"
166+
167+
public static func computeInputs(resources: [AbsolutePath]) -> [Node] {
168+
return [.virtual(Self.name)] + resources.map { Node.file($0) }
169+
}
170+
171+
// FIXME: This will not work well for large files, as we will store the entire contents, plus its byte array
172+
// representation in memory.
173+
public static func getFileContents(inputs: [Node]) throws -> String {
174+
var content =
175+
"""
176+
struct PackageResources {
177+
178+
"""
179+
180+
for input in inputs where input.kind == .file {
181+
let resourcePath = try AbsolutePath(validating: input.name)
182+
let variableName = resourcePath.basename.spm_mangledToC99ExtendedIdentifier()
183+
let fileContent = try Data(contentsOf: URL(fileURLWithPath: resourcePath.pathString)).map { String($0) }.joined(separator: ",")
184+
185+
content += "static let \(variableName): [UInt8] = [\(fileContent)]\n"
186+
}
187+
188+
content += "}"
189+
return content
190+
}
191+
}
162192
}
163193

164194
public struct LLBuildManifest {
@@ -280,6 +310,16 @@ public struct LLBuildManifest {
280310
commands[name] = Command(name: name, tool: tool)
281311
}
282312

313+
public mutating func addWriteEmbeddedResourcesCommand(
314+
resources: [AbsolutePath],
315+
outputPath: AbsolutePath
316+
) {
317+
let inputs = WriteAuxiliary.EmbeddedResources.computeInputs(resources: resources)
318+
let tool = WriteAuxiliaryFile(inputs: inputs, outputFilePath: outputPath)
319+
let name = outputPath.pathString
320+
commands[name] = Command(name: name, tool: tool)
321+
}
322+
283323
public mutating func addPkgStructureCmd(
284324
name: String,
285325
inputs: [Node],

Tests/FunctionalTests/ResourcesTests.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,23 @@ class ResourcesTests: XCTestCase {
122122

123123
func testResourcesEmbeddedInCode() throws {
124124
try fixture(name: "Resources/EmbedInCodeSimple") { fixturePath in
125-
let result = try executeSwiftRun(fixturePath, "EmbedInCodeSimple")
126-
XCTAssertEqual(result.stdout, "hello world\n\n")
125+
let execPath = fixturePath.appending(components: ".build", "debug", "EmbedInCodeSimple")
126+
try executeSwiftBuild(fixturePath)
127+
let result = try Process.checkNonZeroExit(args: execPath.pathString)
128+
XCTAssertEqual(result, "hello world\n\n")
129+
let resourcePath = fixturePath.appending(
130+
components: "Sources", "EmbedInCodeSimple", "best.txt")
131+
132+
// Check incremental builds
133+
for i in 0..<2 {
134+
let content = "Hi there \(i)!"
135+
// Update the resource file.
136+
try localFileSystem.writeFileContents(resourcePath, string: content)
137+
try executeSwiftBuild(fixturePath)
138+
// Run the executable again.
139+
let result2 = try Process.checkNonZeroExit(args: execPath.pathString)
140+
XCTAssertEqual(result2, "\(content)\n")
141+
}
127142
}
128143
}
129144

0 commit comments

Comments
 (0)