liamnichols / xcstrings-tool

A plugin to generate Swift constants for your Strings Catalogs.
https://swiftpackageindex.com/liamnichols/xcstrings-tool/documentation/documentation
MIT License
177 stars 26 forks source link

Local Package Previews Bundle issue: Fatal Error in resource_bundle_accessor.swift #111

Open LucasVanDongen opened 2 months ago

LucasVanDongen commented 2 months ago

Set Up

I have a Local Swift Package I use for separating my features and have a more quick iteration over features using local Previews that load fast and reliable because I don't need to build the whole project, nor include heavy 3rd party dependencies for my UI target.

Problem

One big issue is that Previews ran within a package rather than the main app have issues accessing values from a bundle like icons, fonts and translations. I used SwiftGen for everything, that works with some hacking using a custom bundle .myPackage (see below), but it doesn't support xcassets yet. XCStrings Tool does, and works really well, but I cannot control the bundle.

Attempted Solution

For SwiftGen I can, and with the help of a custom bundle .myPackage it works everywhere. I don't see a way to do this in XCStrings Tool. I cloned the repo and tried to hard code .myPackage but I wasn't able to make it work inside my local package, perhaps because the binary version of the plug-in just works better in that scenario? So I'm not sure if that direction of solution would actually work for me.

Bundle.swift and Crash Log

Bundle.swift:

//
//  Bundle.swift
//  
//
//  Created by Lucas van Dongen on 23/07/2024.
//

import Foundation

extension Bundle {
    private class BundleFinder {}
}

extension Bundle {
    static let myPackageBundleName = "HardwareIntegrationUI_HardwareIntegrationUI"

    public static let myPackage: Bundle = {
        let bundleNameIOS = myPackageBundleName
        let candidates = [
            // Bundle should be here when the package is linked into an App.
            Bundle.main.resourceURL,
            // Bundle should be here when the package is linked into a framework.
            Bundle(for: BundleFinder.self).resourceURL,
            // For command-line tools.
            Bundle.main.bundleURL,
            // Bundle should be here when running previews from a different package
            // (this is the path to "…/Debug-iphonesimulator/").
            Bundle(for: BundleFinder.self)
                .resourceURL?
                .deletingLastPathComponent()
                .deletingLastPathComponent()
                .deletingLastPathComponent(),
            Bundle(for: BundleFinder.self)
                .resourceURL?
                .deletingLastPathComponent()
                .deletingLastPathComponent(),
        ]

        for candidate in candidates {
            let bundlePathiOS = candidate?.appendingPathComponent(bundleNameIOS + ".bundle")
            if let bundle = bundlePathiOS.flatMap(Bundle.init(url:)) {
                return bundle
            }
        }
        fatalError("Can't find myPackage custom bundle.")
    }()
}

The pruned error report:

== PREVIEW UPDATE ERROR:

    CrashReportError: Fatal Error in resource_bundle_accessor.swift

    XCPreviewAgent crashed due to fatalError in resource_bundle_accessor.swift at line 44.

    unable to find bundle named HardwareIntegration_HardwareIntegrationUI

    Frames: [
        Frame {
            imageIndex: 9
            imageOffset: 189728
            symbol: _assertionFailure(_:_:file:line:flags:)
            symbolLocation: 244
        }
        Frame {
            imageIndex: 2
            imageOffset: 3753152
            symbol: closure #1 in variable initialization expression of static NSBundle.module
            symbolLocation: 3372
            sourceFile: resource_bundle_accessor.swift
            sourceLine: 44
        }
        Frame {
            imageIndex: 2
            imageOffset: 3749764
            symbol: one-time initialization function for module
            symbolLocation: 12
            sourceFile: resource_bundle_accessor.swift
            sourceLine: 9
        }
        Frame {
            imageIndex: 10
            imageOffset: 15848
            symbol: _dispatch_client_callout
            symbolLocation: 16
        }
        Frame {
            imageIndex: 10
            imageOffset: 21980
            symbol: _dispatch_once_callout
            symbolLocation: 28
        }
        Frame {
            imageIndex: 2
            imageOffset: 3753608
            symbol: NSBundle.module.unsafeMutableAddressor
            symbolLocation: 80
            sourceFile: resource_bundle_accessor.swift
            sourceLine: 9
        }
        Frame {
            imageIndex: 2
            imageOffset: 3809132
            symbol: static String.Localizable.BundleDescription.current.getter
            symbolLocation: 144
            sourceFile: Localizable.swift
            sourceLine: 63
        }
liamnichols commented 2 months ago

Thanks for raising this issue!

Previews ran within a package rather than the main app have issues

Ah, this is something that I've not tried myself. I've only tested using previews in the main app target, which appeared to work just fine.

I'll try to reproduce this in the Dog Tracker sample app and get back to you.

liamnichols commented 2 months ago

So I had a bit of a look into this, but so far I haven't been able to reproduce the issue myself.

I came across this post - https://forums.swift.org/t/xcode-previews-swiftpm-resources-xcpreviewagent-crashed/51680 - is it the same issue?

I tried the sample project that the post author linked to using Xcode 15.2 but I can't reproduce the original issue on my side (am I doing something different?)

Would it be possible for you to provide me with a reproducer for this issue? You can use the xcstrings-tool-demo repo as a starting point if you would like.


Regardless of the bug with Xcode/SPM/Previews, I guess you might alternatively just be after a way to customise the Bundle yourself.. If this is common issue, I would like to address it with the generated code (i.e by using your extension for resolving Bundle). I'd prefer this to providing ways to override the bundle used by the generator.

The reason for this is because the idea is that the generated code should already be sufficient.

That said, if you want to customise how the localizations are looked up, I am looking into adding a way to opt-out of generating the helper/accessor code so that you can just write it yourself instead - #104. Perhaps this might also work for you as you would just be able to substitute for your own bundle.

Anyway, let me know about the reproducer and if not, I can try to prioritise the helper opt-out change.

LucasVanDongen commented 2 months ago

Repro

This zip file has a self-contained Module as a Swift Package, tested in Xcode 15.4

Localization.zip

Your Suggestions

I think with a bit of perseverance, I can get a fork to work for my own purposes. But it would be great if this would be somehow supported automatically, so nobody has to deal with this anymore.

This is how SwiftGen solves it:

    params:
      bundle: Bundle.myPackage

When I add an image to the working view, it keeps working because of that:

        VStack {
            Button(action: test.test, label: {
                Text(String.good)
            })
            Asset.amsterdamsepoortLogoColored.swiftUIImage
        }
image

I noticed there's a flag XCSTRINGS_TOOL_ACCESS_LEVEL_PUBLIC, perhaps we could also have something like XCSTRINGS_TOOL_BUNDLE_NAME?

I think there is a significant subset of developers that cannot run Previews in their Xcode project anymore because of a combination of project size, Previews on big targets being brittle and Swift compile times. So that would either mean using something like Tuist to generate custom per-feature projects, or working inside of a Swift Package and use Previews from there.

I prefer to work in the Swift Module because it will keep everything associated with the Module inside of there, the Previews will be available to everybody. And I use Snapshots on Previews as a form of testing my UI layer, which gives me a huge kick in code coverage basically for free!

liamnichols commented 2 months ago

Thanks for the reproducer and the explanation! I agree that this is something we should certainly resolve to allow developers to use previews in separate modules. I don't think the Apple suggested workaround (of using an App Xcode project) is a very good one.

I was able to reproduce the issue, but only when the run destination was set to a physical device and not a simulator. Is that the case for you as well?

perhaps we could also have something like XCSTRINGS_TOOL_BUNDLE_NAME?

This would be a quick fix, but unfortunately it isn't possible to pass key: value configs in via swiftSettings - only flags, so you couldn't get something like a custom bundle name through.

That said, it also goes a bit against the motivation behind the build tool plugin. By tightly wrapping everything up into a build tool plugin, I don't want users to have to worry about getting the right bundle because the generated code should use the right bundle automatically.

For us to use the right bundle automatically, it's going to mean that we need to generate some custom code that will do the correct bundle lookup for us. I'm more than happy to do that based on the example that you shared when you created the issue.. I'll have a look at how I can integrate it so that it's isolated to just previews.

liamnichols commented 1 month ago

I had a further look into this tonight, and if your issue is the same as my issue (that only crashes when the destination is set to a physical device and not a simulator, even if the preview renders inside Xcode) then it seems that it has been fixed in Xcode 16 (beta 4).

I'm happy to still try and patch it, but if you could confirm to me that this is the same issue that you are seeing then it would be very helpful. Thanks!

LucasVanDongen commented 1 month ago

I’m on holiday, just my phone and spotty connections, my apology for not being super responsive.

Currently seeing it everywhere:

I’m not sure what you mean by device. If it’s the one that looks like a device then yes, but I don’t remember the other ones working, but that might just be because I never tried.

thanks again for putting effort into this 🙏