Skip to content

Commit f231059

Browse files
committed
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. ``` struct PackageResources { static let best_txt: [UInt8] = [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 f231059

File tree

13 files changed

+101
-8
lines changed

13 files changed

+101
-8
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// swift-tools-version: 999.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "EmbedInCodeSimple",
7+
targets: [
8+
.executableTarget(name: "EmbedInCodeSimple", resources: [.embedInCode("best.txt")]),
9+
]
10+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello world
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Foundation
2+
3+
print("\(String(decoding: Data(PackageResources.best_txt), as: UTF8.self))")

Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift

Lines changed: 40 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 != .embedInCode }.isEmpty == false
62+
}
63+
64+
private var needsResourceEmbedding: Bool {
65+
return resources.filter { $0.rule == .embedInCode }.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,37 @@ 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+
struct PackageResources {
306+
307+
"""
308+
309+
try resources.forEach {
310+
guard $0.rule == .embedInCode else { return }
311+
312+
let variableName = $0.path.basename.spm_mangledToC99ExtendedIdentifier()
313+
let fileContent = try Data(contentsOf: URL(fileURLWithPath: $0.path.pathString)).map { String($0) }.joined(separator: ",")
314+
315+
stream <<< "static let \(variableName): [UInt8] = [\(fileContent)]\n"
316+
}
317+
318+
stream <<< """
319+
}
320+
"""
321+
322+
let subpath = RelativePath("embedded_resources.swift")
323+
self.derivedSources.relativePaths.append(subpath)
324+
let path = self.derivedSources.root.appending(subpath)
325+
try self.fileSystem.writeIfChanged(path: path, bytes: stream.bytes)
287326
}
288327

289328
/// 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 .embedInCode:
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 embedInCode(_ path: String) -> Resource {
101+
return Resource(rule: "embedInCode", 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 "embedInCode":
366+
return .init(rule: .embedInCode, path: path.pathString)
365367
default:
366368
throw InternalError("invalid resource rule \(rule)")
367369
}

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ public enum ModuleError: Swift.Error {
7676

7777
/// A plugin target didn't declare a capability.
7878
case pluginCapabilityNotDeclared(target: String)
79+
80+
/// A C target has declared an embedded resource
81+
case embedInCodeNotSupported(target: String)
7982
}
8083

8184
extension ModuleError: CustomStringConvertible {
@@ -124,6 +127,8 @@ extension ModuleError: CustomStringConvertible {
124127
return "manifest property 'defaultLocalization' not set; it is required in the presence of localized resources"
125128
case .pluginCapabilityNotDeclared(let target):
126129
return "plugin target '\(target)' doesn't have a 'capability' property"
130+
case .embedInCodeNotSupported(let target):
131+
return "embedding resources in code not supported for C-family language target \(target)"
127132
}
128133
}
129134
}
@@ -886,6 +891,10 @@ public final class PackageBuilder {
886891
moduleMapType = .none
887892
}
888893

894+
if resources.contains(where: { $0.rule == .embedInCode }) {
895+
throw ModuleError.embedInCodeNotSupported(target: potentialModule.name)
896+
}
897+
889898
return try ClangTarget(
890899
name: potentialModule.name,
891900
potentialBundleName: potentialBundleName,

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 .embedResourceInCode:
304+
return Resource(rule: .embedInCode, 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, .embedResourceInCode:
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 embedResourceInCode
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 .embedInCode:
719+
self = .embedResourceInCode
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 embedInCode
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 .embedInCode:
426+
self.init(enum: "embedInCode", 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 embedInCode
4748
}
4849
}

Tests/FunctionalTests/ResourcesTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,11 @@ class ResourcesTests: XCTestCase {
124124
XCTAssertSwiftTest(fixturePath, extraArgs: ["--filter", "ClangResourceTests"])
125125
}
126126
}
127+
128+
func testResourcesEmbeddedInCode() throws {
129+
try fixture(name: "Resources/EmbedInCodeSimple") { fixturePath in
130+
let result = try executeSwiftRun(fixturePath, "EmbedInCodeSimple")
131+
XCTAssertEqual(result.stdout, "hello world\n\n")
132+
}
133+
}
127134
}

0 commit comments

Comments
 (0)