Skip to content

Commit c3fc702

Browse files
committed
WIP: Support for embedding resources in an executable
Basic support for a new `.embed` resource rule which will allow embedding the contents of the resource into the executable code by generating a byte array, e.g. ``` @_implementationOnly import struct Foundation.Data struct PackageResources { static let best_txt = Data([104,101,108,108,111,32,119,111,114,108,100,10]) } ``` Note that the current naïve implementaton will not work well for larger resources as it is pretty memory inefficient.
1 parent a227ff0 commit c3fc702

File tree

8 files changed

+73
-8
lines changed

8 files changed

+73
-8
lines changed

Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,21 @@ public final class SwiftTargetBuildDescription {
5050

5151
/// Path to the bundle generated for this module (if any).
5252
var bundlePath: AbsolutePath? {
53-
if let bundleName = target.underlyingTarget.potentialBundleName, !resources.isEmpty {
53+
if let bundleName = target.underlyingTarget.potentialBundleName, needsResourceBundle {
5454
return self.buildParameters.bundlePath(named: bundleName)
5555
} else {
5656
return .none
5757
}
5858
}
5959

60+
private var needsResourceBundle: Bool {
61+
return resources.filter { $0.rule != .embed }.isEmpty == false
62+
}
63+
64+
private var needsResourceEmbedding: Bool {
65+
return resources.filter { $0.rule == .embed }.isEmpty == false
66+
}
67+
6068
/// The list of all source files in the target, including the derived ones.
6169
public var sources: [AbsolutePath] {
6270
self.target.sources.paths + self.derivedSources.paths + self.pluginDerivedSources.paths
@@ -284,6 +292,39 @@ public final class SwiftTargetBuildDescription {
284292
self.resourceBundleInfoPlistPath = infoPlistPath
285293
}
286294
}
295+
296+
try self.generateResourceEmbeddingCode()
297+
}
298+
299+
// FIXME: This will not work well for large files, as we will store the entire contents, plus its byte array representation in memory and also `writeIfChanged()` will read the entire generated file again.
300+
private func generateResourceEmbeddingCode() throws {
301+
guard needsResourceEmbedding else { return }
302+
303+
let stream = BufferedOutputByteStream()
304+
stream <<< """
305+
\(self.toolsVersion < .vNext ? "import" : "@_implementationOnly import") struct Foundation.Data
306+
307+
struct PackageResources {
308+
309+
"""
310+
311+
try resources.forEach {
312+
guard $0.rule == .embed else { return }
313+
314+
let variableName = $0.path.basename.spm_mangledToC99ExtendedIdentifier()
315+
let fileContent = try Data(contentsOf: URL(fileURLWithPath: $0.path.pathString)).map { String($0) }.joined(separator: ",")
316+
317+
stream <<< "static let \(variableName) = Data([\(fileContent)])\n"
318+
}
319+
320+
stream <<< """
321+
}
322+
"""
323+
324+
let subpath = RelativePath("embedded_resources.swift")
325+
self.derivedSources.relativePaths.append(subpath)
326+
let path = self.derivedSources.root.appending(subpath)
327+
try self.fileSystem.writeIfChanged(path: path, bytes: stream.bytes)
287328
}
288329

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

Sources/Build/LLBuildManifestBuilder.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,14 @@ extension LLBuildManifestBuilder {
174174

175175
// Create a copy command for each resource file.
176176
for resource in target.resources {
177-
let destination = bundlePath.appending(resource.destination)
178-
let (_, output) = addCopyCommand(from: resource.path, to: destination)
179-
outputs.append(output)
177+
switch resource.rule {
178+
case .copy, .process:
179+
let destination = bundlePath.appending(resource.destination)
180+
let (_, output) = addCopyCommand(from: resource.path, to: destination)
181+
outputs.append(output)
182+
case .embed:
183+
break
184+
}
180185
}
181186

182187
// Create a copy command for the Info.plist if a resource with the same name doesn't exist yet.

Sources/PackageDescription/Resource.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,10 @@ public struct Resource: Encodable {
9494
public static func copy(_ path: String) -> Resource {
9595
return Resource(rule: "copy", path: path, localization: nil)
9696
}
97+
98+
/// Applies the embed rule to a resource at the given path.
99+
@available(_PackageDescription, introduced: 999.0)
100+
public static func embed(_ path: String) -> Resource {
101+
return Resource(rule: "embed", path: path, localization: nil)
102+
}
97103
}

Sources/PackageLoading/ManifestJSONParser.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ enum ManifestJSONParser {
362362
return .init(rule: .process(localization: localization), path: path.pathString)
363363
case "copy":
364364
return .init(rule: .copy, path: path.pathString)
365+
case "embed":
366+
return .init(rule: .embed, path: path.pathString)
365367
default:
366368
throw InternalError("invalid resource rule \(rule)")
367369
}

Sources/PackageLoading/TargetSourcesBuilder.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,10 @@ public struct TargetSourcesBuilder {
298298
}
299299

300300
return Resource(rule: .process(localization: implicitLocalization ?? explicitLocalization), path: path)
301-
case .copy:
301+
case .copyResource:
302302
return Resource(rule: .copy, path: path)
303+
case .embedResource:
304+
return Resource(rule: .embed, path: path)
303305
}
304306
}
305307

@@ -504,7 +506,7 @@ public struct TargetSourcesBuilder {
504506
} else {
505507
observabilityScope.emit(warning: "Only Swift is supported for generated plugin source files at this time: \(absPath)")
506508
}
507-
case .copy, .processResource:
509+
case .copyResource, .processResource, .embedResource:
508510
if let resource = Self.resource(for: absPath, with: rule, defaultLocalization: defaultLocalization, targetName: targetName, targetPath: targetPath, observabilityScope: observabilityScope) {
509511
resources.append(resource)
510512
} else {
@@ -537,8 +539,11 @@ public struct FileRuleDescription {
537539
/// This defaults to copy if there's no specialized behavior.
538540
case processResource(localization: TargetDescription.Resource.Localization?)
539541

542+
/// The embed rule.
543+
case embedResource
544+
540545
/// The copy rule.
541-
case copy
546+
case copyResource
542547

543548
/// The modulemap rule.
544549
case modulemap
@@ -709,7 +714,9 @@ extension FileRuleDescription.Rule {
709714
case .process(let localization):
710715
self = .processResource(localization: localization)
711716
case .copy:
712-
self = .copy
717+
self = .copyResource
718+
case .embed:
719+
self = .embedResource
713720
}
714721
}
715722
}

Sources/PackageModel/Manifest/TargetDescription.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public struct TargetDescription: Equatable, Encodable {
4242
public enum Rule: Encodable, Equatable {
4343
case process(localization: Localization?)
4444
case copy
45+
case embed
4546
}
4647

4748
public enum Localization: String, Encodable {

Sources/PackageModel/ManifestSourceGeneration.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,8 @@ fileprivate extension SourceCodeFragment {
422422
self.init(enum: "process", subnodes: params)
423423
case .copy:
424424
self.init(enum: "copy", subnodes: params)
425+
case .embed:
426+
self.init(enum: "embed", subnodes: params)
425427
}
426428
}
427429

Sources/PackageModel/Resource.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ public struct Resource: Codable, Equatable {
4444
public enum Rule: Codable, Equatable {
4545
case process(localization: String?)
4646
case copy
47+
case embed
4748
}
4849
}

0 commit comments

Comments
 (0)