Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Align resource bundle accessor generation with SPM #6146

Merged
235 changes: 126 additions & 109 deletions Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this
}

if target.supportsSources,
target.sources.contains(where: { $0.path.extension == "swift" }),
!target.sources.contains(where: { $0.path.basename == "\(target.name)Resources.swift" })
target.sources.containsSwiftFiles
{
let (filePath, data) = synthesizedSwiftFile(bundleName: bundleName, target: target, project: project)

Expand All @@ -78,10 +77,9 @@ public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this
sideEffects.append(sideEffect)
}

if project.isExternal,
danibachar marked this conversation as resolved.
Show resolved Hide resolved
target.supportsSources,
target.sources.contains(where: { $0.path.extension == "m" || $0.path.extension == "mm" }),
!target.resources.resources.filter({ $0.path.extension != "xcprivacy" }).isEmpty
if target.supportsSources,
target.sources.containsObjcFiles,
target.resources.containsBundleAccessedResources
{
let (headerFilePath, headerData) = synthesizedObjcHeaderFile(bundleName: bundleName, target: target, project: project)

Expand Down Expand Up @@ -129,19 +127,16 @@ public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this
let content: String = ResourcesProjectMapper.fileContent(
targetName: target.name,
bundleName: bundleName.replacingOccurrences(of: "-", with: "_"),
target: target
target: target,
in: project
)
return (filePath, content.data(using: .utf8))
}

private func synthesizedObjcHeaderFile(bundleName: String, target: Target, project: Project) -> (AbsolutePath, Data?) {
private func synthesizedObjcHeaderFile(bundleName _: String, target: Target, project: Project) -> (AbsolutePath, Data?) {
let filePath = synthesizedFilePath(target: target, project: project, fileExtension: "h")

let content: String = ResourcesProjectMapper.objcHeaderFileContent(
targetName: target.name,
bundleName: bundleName.replacingOccurrences(of: "-", with: "_"),
target: target
)
let content: String = ResourcesProjectMapper.objcHeaderFileContent(targetName: target.name)
return (filePath, content.data(using: .utf8))
}

Expand All @@ -165,107 +160,34 @@ public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this
}

// swiftlint:disable:next function_body_length
static func fileContent(targetName: String, bundleName: String, target: Target) -> String {
static func fileContent(targetName _: String, bundleName: String, target: Target, in project: Project) -> String {
var content = """
// swiftlint:disable all
// swift-format-ignore-file
// swiftformat:disable all
import Foundation
"""
if !target.supportsResources {
return """
// swiftlint:disable all
// swift-format-ignore-file
// swiftformat:disable all
import Foundation

// MARK: - Swift Bundle Accessor

private class BundleFinder {}

extension Foundation.Bundle {
/// Since \(targetName) is a \(
target
.product
), the bundle containing the resources is copied into the final product.
static let module: Bundle = {
let bundleName = "\(bundleName)"

var candidates = [
Bundle.main.resourceURL,
Bundle(for: BundleFinder.self).resourceURL,
Bundle.main.bundleURL,
]

// This is a fix to make Previews work with bundled resources.
// Logic here is taken from SPM's generated `resource_bundle_accessors.swift` file,
// which is located under the derived data directory after building the project.
if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_PATH"] {
candidates.append(URL(fileURLWithPath: override))

// Deleting derived data and not rebuilding the frameworks containing resources may result in a state
// where the bundles are only available in the framework's directory that is actively being previewed.
// Since we don't know which framework this is, we also need to look in all the framework subpaths.
if let subpaths = try? FileManager.default.contentsOfDirectory(atPath: override) {
for subpath in subpaths {
if subpath.hasSuffix(".framework") {
candidates.append(URL(fileURLWithPath: override + "/" + subpath))
}
}
}
}

for candidate in candidates {
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
return bundle
}
}
fatalError("unable to find bundle named \(bundleName)")
}()
}

// MARK: - Objective-C Bundle Accessor

@objc
public class \(target.productName.toValidSwiftIdentifier())Resources: NSObject {
@objc public class var bundle: Bundle {
return .module
}
}
// swiftlint:enable all
// swiftformat:enable all

"""
content += swiftSPMBundleAccessorString(for: target, and: bundleName)
} else {
return """
// swiftlint:disable all
// swift-format-ignore-file
// swiftformat:disable all
import Foundation

// MARK: - Swift Bundle Accessor

private class BundleFinder {}

extension Foundation.Bundle {
/// Since \(targetName) is a \(
target
.product
), the bundle for classes within this module can be used directly.
static let module = Bundle(for: BundleFinder.self)
}

// MARK: - Objective-C Bundle Accessor

@objc
public class \(target.productName.toValidSwiftIdentifier())Resources: NSObject {
@objc public class var bundle: Bundle {
return .module
}
}
// swiftlint:enable all
// swiftformat:enable all
content += swiftFrameworkBundleAccessorString(for: target)
}

"""
// Add public accessors only for non external projects
if !project.isExternal, !target.sourcesContainsPublicResourceClassName {
content += publicBundleAccessorString(for: target)
}

content += """
// swiftlint:enable all
// swiftformat:enable all
"""
return content
}

static func objcHeaderFileContent(targetName: String, bundleName _: String, target _: Target) -> String {
static func objcHeaderFileContent(
targetName: String
) -> String {
return """
#import <Foundation/Foundation.h>

Expand All @@ -283,7 +205,10 @@ public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this
"""
}

static func objcImplementationFileContent(targetName: String, bundleName: String) -> String {
static func objcImplementationFileContent(
targetName: String,
bundleName: String
) -> String {
return """
#import <Foundation/Foundation.h>
#import "TuistBundle+\(targetName).h"
Expand All @@ -297,4 +222,96 @@ public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this
}
"""
}

private static func publicBundleAccessorString(for target: Target) -> String {
"""
// MARK: - Objective-C Bundle Accessor
@objc
public class \(target.productName.toValidSwiftIdentifier())Resources: NSObject {
@objc public class var bundle: Bundle {
return .module
}
}
"""
}

private static func swiftSPMBundleAccessorString(for target: Target, and bundleName: String) -> String {
"""
// MARK: - Swift Bundle Accessor - for SPM
private class BundleFinder {}
extension Foundation.Bundle {
/// Since \(target.name) is a \(
target
.product
), the bundle containing the resources is copied into the final product.
Comment on lines +243 to +246
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatting is slightly odd. Is it actually treated as one comment? πŸ€”

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't touch this formatting, mise lint didn't change it, but indeed its considered as one comment - i.e one line

Example from the app_with_spm_dependencies, the derived file Derived/Sources/TuistBundle+Styles

...
/// Since Styles is a staticFramework, the bundle containing the resources is copied into the final product.
...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can change it if you wish

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, it's fine as long as it's treated as a single line of comment πŸ‘

static let module: Bundle = {
let bundleName = "\(bundleName)"
var candidates = [
Bundle.main.resourceURL,
Bundle(for: BundleFinder.self).resourceURL,
Bundle.main.bundleURL,
]
// This is a fix to make Previews work with bundled resources.
// Logic here is taken from SPM's generated `resource_bundle_accessors.swift` file,
// which is located under the derived data directory after building the project.
if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_PATH"] {
candidates.append(URL(fileURLWithPath: override))
// Deleting derived data and not rebuilding the frameworks containing resources may result in a state
// where the bundles are only available in the framework's directory that is actively being previewed.
// Since we don't know which framework this is, we also need to look in all the framework subpaths.
if let subpaths = try? FileManager.default.contentsOfDirectory(atPath: override) {
for subpath in subpaths {
if subpath.hasSuffix(".framework") {
candidates.append(URL(fileURLWithPath: override + "/" + subpath))
}
}
}
}
for candidate in candidates {
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
return bundle
}
}
fatalError("unable to find bundle named \(bundleName)")
}()
}
"""
}

private static func swiftFrameworkBundleAccessorString(for target: Target) -> String {
"""
// MARK: - Swift Bundle Accessor for Frameworks
private class BundleFinder {}
extension Foundation.Bundle {
/// Since \(target.name) is a \(
target
.product
), the bundle for classes within this module can be used directly.
static let module = Bundle(for: BundleFinder.self)
}
"""
}
}

extension [SourceFile] {
fileprivate var containsObjcFiles: Bool {
contains(where: { $0.path.extension == "m" || $0.path.extension == "mm" })
}

fileprivate var containsSwiftFiles: Bool {
contains(where: { $0.path.extension == "swift" })
}
}

extension ResourceFileElements {
fileprivate var containsBundleAccessedResources: Bool {
!resources.filter { $0.path.extension != "xcprivacy" }.isEmpty
}
}

extension Target {
fileprivate var sourcesContainsPublicResourceClassName: Bool {
sources.contains(where: { $0.path.basename == "\(name)Resources.swift" })
}
}
18 changes: 9 additions & 9 deletions Sources/TuistGenerator/Templates/AssetsTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,15 @@ extension SynthesizedResourceInterfaceTemplates {
@available(iOS 11.3, *)
{{accessModifier}} extension ARReferenceImage {
static func referenceImages(in asset: {{arResourceGroupType}}) -> Set<ARReferenceImage> {
let bundle = {{bundleToken}}.bundle
let bundle = Bundle.module
return referenceImages(inGroupNamed: asset.name, bundle: bundle) ?? Set()
}
}

@available(iOS 12.0, *)
{{accessModifier}} extension ARReferenceObject {
static func referenceObjects(in asset: {{arResourceGroupType}}) -> Set<ARReferenceObject> {
let bundle = {{bundleToken}}.bundle
let bundle = Bundle.module
return referenceObjects(inGroupNamed: asset.name, bundle: bundle) ?? Set()
}
}
Expand Down Expand Up @@ -189,7 +189,7 @@ extension SynthesizedResourceInterfaceTemplates {
{{accessModifier}} extension {{colorType}}.Color {
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, visionOS 1.0, *)
convenience init?(asset: {{colorType}}) {
let bundle = {{bundleToken}}.bundle
let bundle = Bundle.module
#if os(iOS) || os(tvOS) || os(visionOS)
self.init(named: asset.name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
Expand All @@ -204,7 +204,7 @@ extension SynthesizedResourceInterfaceTemplates {
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, visionOS 1.0, *)
{{accessModifier}} extension SwiftUI.Color {
init(asset: {{colorType}}) {
let bundle = {{bundleToken}}.bundle
let bundle = Bundle.module
self.init(asset.name, bundle: bundle)
}
}
Expand All @@ -230,7 +230,7 @@ extension SynthesizedResourceInterfaceTemplates {
@available(iOS 9.0, macOS 10.11, visionOS 1.0, *)
{{accessModifier}} extension NSDataAsset {
convenience init?(asset: {{dataType}}) {
let bundle = {{bundleToken}}.bundle
let bundle = Bundle.module
#if os(iOS) || os(tvOS) || os(visionOS)
self.init(name: asset.name, bundle: bundle)
#elseif os(macOS)
Expand All @@ -252,7 +252,7 @@ extension SynthesizedResourceInterfaceTemplates {
#endif

{{accessModifier}} var image: Image {
let bundle = {{bundleToken}}.bundle
let bundle = Bundle.module
#if os(iOS) || os(tvOS) || os(visionOS)
let image = Image(named: name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
Expand All @@ -278,17 +278,17 @@ extension SynthesizedResourceInterfaceTemplates {
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, visionOS 1.0, *)
{{accessModifier}} extension SwiftUI.Image {
init(asset: {{imageType}}) {
let bundle = {{bundleToken}}.bundle
let bundle = Bundle.module
self.init(asset.name, bundle: bundle)
}

init(asset: {{imageType}}, label: Text) {
let bundle = {{bundleToken}}.bundle
let bundle = Bundle.module
self.init(asset.name, bundle: bundle, label: label)
}

init(decorative asset: {{imageType}}) {
let bundle = {{bundleToken}}.bundle
let bundle = Bundle.module
self.init(decorative: asset.name, bundle: bundle)
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/TuistGenerator/Templates/StringsTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ extension SynthesizedResourceInterfaceTemplates {
{% if param.lookupFunction %}
let format = {{ param.lookupFunction }}(key, table)
{% else %}
let format = {{bundleToken}}.bundle.localizedString(forKey: key, value: nil, table: table)
let format = Bundle.module.localizedString(forKey: key, value: nil, table: table)
{% endif %}
return String(format: format, locale: Locale.current, arguments: args)
}
Expand Down