Skip to content

Commit

Permalink
WIP: Support for embedding resources in an executable
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
neonichu committed Jan 20, 2023
1 parent a227ff0 commit c3fc702
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 8 deletions.
43 changes: 42 additions & 1 deletion Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,21 @@ public final class SwiftTargetBuildDescription {

/// Path to the bundle generated for this module (if any).
var bundlePath: AbsolutePath? {
if let bundleName = target.underlyingTarget.potentialBundleName, !resources.isEmpty {
if let bundleName = target.underlyingTarget.potentialBundleName, needsResourceBundle {
return self.buildParameters.bundlePath(named: bundleName)
} else {
return .none
}
}

private var needsResourceBundle: Bool {
return resources.filter { $0.rule != .embed }.isEmpty == false
}

private var needsResourceEmbedding: Bool {
return resources.filter { $0.rule == .embed }.isEmpty == false
}

/// The list of all source files in the target, including the derived ones.
public var sources: [AbsolutePath] {
self.target.sources.paths + self.derivedSources.paths + self.pluginDerivedSources.paths
Expand Down Expand Up @@ -284,6 +292,39 @@ public final class SwiftTargetBuildDescription {
self.resourceBundleInfoPlistPath = infoPlistPath
}
}

try self.generateResourceEmbeddingCode()
}

// 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.
private func generateResourceEmbeddingCode() throws {
guard needsResourceEmbedding else { return }

let stream = BufferedOutputByteStream()
stream <<< """
\(self.toolsVersion < .vNext ? "import" : "@_implementationOnly import") struct Foundation.Data
struct PackageResources {
"""

try resources.forEach {
guard $0.rule == .embed else { return }

let variableName = $0.path.basename.spm_mangledToC99ExtendedIdentifier()
let fileContent = try Data(contentsOf: URL(fileURLWithPath: $0.path.pathString)).map { String($0) }.joined(separator: ",")

stream <<< "static let \(variableName) = Data([\(fileContent)])\n"
}

stream <<< """
}
"""

let subpath = RelativePath("embedded_resources.swift")
self.derivedSources.relativePaths.append(subpath)
let path = self.derivedSources.root.appending(subpath)
try self.fileSystem.writeIfChanged(path: path, bytes: stream.bytes)
}

/// Generate the resource bundle accessor, if appropriate.
Expand Down
11 changes: 8 additions & 3 deletions Sources/Build/LLBuildManifestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,14 @@ extension LLBuildManifestBuilder {

// Create a copy command for each resource file.
for resource in target.resources {
let destination = bundlePath.appending(resource.destination)
let (_, output) = addCopyCommand(from: resource.path, to: destination)
outputs.append(output)
switch resource.rule {
case .copy, .process:
let destination = bundlePath.appending(resource.destination)
let (_, output) = addCopyCommand(from: resource.path, to: destination)
outputs.append(output)
case .embed:
break
}
}

// Create a copy command for the Info.plist if a resource with the same name doesn't exist yet.
Expand Down
6 changes: 6 additions & 0 deletions Sources/PackageDescription/Resource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,10 @@ public struct Resource: Encodable {
public static func copy(_ path: String) -> Resource {
return Resource(rule: "copy", path: path, localization: nil)
}

/// Applies the embed rule to a resource at the given path.
@available(_PackageDescription, introduced: 999.0)
public static func embed(_ path: String) -> Resource {
return Resource(rule: "embed", path: path, localization: nil)
}
}
2 changes: 2 additions & 0 deletions Sources/PackageLoading/ManifestJSONParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ enum ManifestJSONParser {
return .init(rule: .process(localization: localization), path: path.pathString)
case "copy":
return .init(rule: .copy, path: path.pathString)
case "embed":
return .init(rule: .embed, path: path.pathString)
default:
throw InternalError("invalid resource rule \(rule)")
}
Expand Down
15 changes: 11 additions & 4 deletions Sources/PackageLoading/TargetSourcesBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,10 @@ public struct TargetSourcesBuilder {
}

return Resource(rule: .process(localization: implicitLocalization ?? explicitLocalization), path: path)
case .copy:
case .copyResource:
return Resource(rule: .copy, path: path)
case .embedResource:
return Resource(rule: .embed, path: path)
}
}

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

/// The embed rule.
case embedResource

/// The copy rule.
case copy
case copyResource

/// The modulemap rule.
case modulemap
Expand Down Expand Up @@ -709,7 +714,9 @@ extension FileRuleDescription.Rule {
case .process(let localization):
self = .processResource(localization: localization)
case .copy:
self = .copy
self = .copyResource
case .embed:
self = .embedResource
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Sources/PackageModel/Manifest/TargetDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public struct TargetDescription: Equatable, Encodable {
public enum Rule: Encodable, Equatable {
case process(localization: Localization?)
case copy
case embed
}

public enum Localization: String, Encodable {
Expand Down
2 changes: 2 additions & 0 deletions Sources/PackageModel/ManifestSourceGeneration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ fileprivate extension SourceCodeFragment {
self.init(enum: "process", subnodes: params)
case .copy:
self.init(enum: "copy", subnodes: params)
case .embed:
self.init(enum: "embed", subnodes: params)
}
}

Expand Down
1 change: 1 addition & 0 deletions Sources/PackageModel/Resource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ public struct Resource: Codable, Equatable {
public enum Rule: Codable, Equatable {
case process(localization: String?)
case copy
case embed
}
}

0 comments on commit c3fc702

Please sign in to comment.