Xcode previews do not work anymore after porting the project to Tuist

Question or problem

I tried to port our project to Tuist to see if the using cache would improve our build times (it does, by 53% :tada: ).

However, Xcode previews do not work anymore.

It’s important to note that our code is split in multiple local small Swift packages (actually they are multiple targets inside a single SPM package) + a big app target. So in order to use previews, we have to first select the scheme corresponding to the target. Then, previews work. The big app target is too big and previews never worked (it loads indefinitely and we never figured out why).

When porting the project to Tuist, I turned each local Swift package into a static library target. However when I select the scheme corresponding to a target, previews no longer work (they build but they don’t start).

Changing the preview driver to the legacy one doesn’t work because it’s unable to preview “plain” static libraries (it worked before because they were Swift packages targets).

The error logs are below.

Expectation

I expect the previews to work as they do in the project, as they do without using Tuist.

Context

  • Tuist version: 4.37.0
  • Xcode version: 16.1.0

Reproduction (mandatory for problems)

== PREVIEW UPDATE ERROR:

    [Remote] JITError: Runtime linking failure
    
    Additional Link Time Errors:
    Symbols not found: [ ___isPlatformVersionAtLeast ]
    Symbols not found: [ ___isPlatformVersionAtLeast ]
    In static-UITools, failed to materialize { _$sSS15SwiftRichStringE3set5style5rangeSo019NSMutableAttributedC0CSgSS_So8_NSRangeVSgtF }, due to unsatisfied dependencies { (static-UITools, { _$s15SwiftRichString13StylesManagerC6sharedACvau }) } (dependencies removed or in error state)
    
    ==================================
    
    |  [Remote] LLVMError
    |  
    |  LLVMError: LLVMError(description: "Failed to materialize symbols: { (static-UITools, { __replacement_tag$429 }) }")

== VERSION INFO:

    Tools: 16B40
    OS:    24B91
    PID:   44580
    Model: MacBook Pro
    Arch:  arm64e

== ENVIRONMENT:

    openFiles = [
        /Redacted.swift
    ]
    wantsNewBuildSystem = true
    newBuildSystemAvailable = true
    activeScheme = UITools
    activeRunDestination = iPhone 16 Pro variant iphonesimulator arm64
    workspaceArena = [x]
    buildArena = [x]
    buildableEntries = [
        libUITools.a
    ]
    runMode = JIT Executor

== SELECTED RUN DESTINATION:

    Simulator - iOS 18.1 | iphonesimulator | arm64 | iPhone 16 Pro | no proxy

== EXECUTION MODE OVERRIDES:

    Workspace JIT mode user setting: true
    Falling back to Dynamic Replacement: false

== JIT LINKAGE:

    Run Destination: B696D6E3-9E09-4373-BF5B-277CA73071F6-iphonesimulator18.1-arm64-iphonesimulator
    JIT Link Description {
        10:libUITools.a [
            4:libAudioTools.a
            6:libModels.a
            5:libReporting.a
            2:libResources.a
            3:libTools.a
        ]
    }
    
== Truncated for brevity, if the rest is necessary I can provide it

UITools is the name of the target I am trying to preview. The different libXXX.a files are local dependencies of UITools (so other local targets in the project).

SwiftRichString in a SPM dependency that’s added to the target. It builds fine outside of previews. It’s missing from JIT LINKAGE, maybe this is the issue?

1 Like

This is amazing :clap:

When porting the project to Tuist, I turned each local Swift package into a static library target.

Previews work better with dynamic frameworks – you can use Tuist’s support for environment variables to switch between dynamic and static frameworks easily by running a command such as TUIST_DEFAULT_FRAMEWORK_TYPE="static" tuist generate and then reading the environment variable in your Project.swift or Package.swift manifest to change the type at generation time. @pepicrft described this approach in more detail here.

In static-UITools, failed to materialize { _$sSS15SwiftRichStringE3set5style5rangeSo019NSMutableAttributedC0CSgSS_So8_NSRangeVSgtF }

This suggests to me that some of the static symbols are stripped. This is because SwiftPM by default passes GENERATE_MASTER_OBJECT_FILE=YES. What this build setting does is that it disables code stripping of static libraries. This does help with some runtime failures but it also unnecessarily increases your binary size. We don’t recommend setting that value across the board, but some libraries end up working better with it. We considered making that a default to align with SwiftPM or at least adding an easy opt-in but we haven’t got around doing that, yet. There’s an issue for this, however: Include a generation option to enable the generation of the master object file · Issue #6777 · tuist/tuist · GitHub

What you can do and what I would recommend trying is setting that flag for the particular dependency in your Package.swift using Tuist’s PackageSettings:

// Tuist/Package.swift
import PackageDescription

#if TUIST
import ProjectDescription

let packageSettings = PackageSettings(
  targetSettings: ["SwiftRichString": ["GENERATE_MASTER_OBJECT_FILE": true]]
)

#endif

let package = Package(...)

I am quite sure either switching your libraries to dynamic or adding the GENERATE_MASTER_OBJECT_FILE setting should fix your issue – but preview failures are sometimes hard to pin down, so let me know if neither works for you.

Thank you for your reply!

Here is the summary of what I’ve tried:

Static libraires with GENERATE_MASTER_OBJECT_FILE

Previews do not start when the main scheme is selected, there is an infinite spinner (as usual).

When the right target scheme is selected, there is the same “Runtime linking failure” with the following error:

== PREVIEW UPDATE ERROR:

    [Remote] JITError: Runtime linking failure
    
    Additional Link Time Errors:
    Symbols not found: [ ___isPlatformVersionAtLeast ]
    Symbols not found: [ ___isPlatformVersionAtLeast ]
    Symbols not found: [ ___isPlatformVersionAtLeast ]
    
    ==================================
    
    |  [Remote] LLVMError
    |  
    |  LLVMError: LLVMError(description: "Failed to materialize symbols: { (static-UITools, { __replacement_tag$429 }) }")

The SwiftRichString errors are gone but it’s not enough to make previews work.

Switching to dynamic frameworks

It works, I can use previews from both the target scheme and the main app scheme (which was never possible under regular Xcode) :tada:

However when running tuist generate I have the following warnings:

 · Xcframework 'GoogleAppMeasurement.xcframework' has been linked from target 'Redacted' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Xcframework 'SwiftCompilerPluginMessageHandling.xcframework' has been linked from target 'CommonScenes', target 'Models', target 'Tools', and target 'UITools', it is a static product so may introduce unwanted side effects.
 · Xcframework 'SwiftDiagnostics.xcframework' has been linked from target 'CommonScenes', target 'Models', target 'Tools', and target 'UITools', it is a static product so may introduce unwanted side effects.
 · Xcframework 'SwiftSyntax509.xcframework' has been linked from target 'Models', target 'Tools', and target 'UITools', it is a static product so may introduce unwanted side effects.
 · Xcframework 'GoogleAppMeasurementIdentitySupport.xcframework' has been linked from target 'AlbusAir' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Xcframework 'SwiftCompilerPlugin.xcframework' has been linked from target 'Models', target 'Tools', and target 'UITools', it is a static product so may introduce unwanted side effects.

And so on, there is one line per external dependency per target.

I assume this is because my cache was generated before I switched to dynamic frameworks.

I tried running tuist cache again after making the change to dynamic frameworks to see if it would make the warning disappear, and I noticed that Tuist now creates a cache for all internal targets. Which is not something that I want.

So I ran tuist cache --external-only and it says All cacheable targets are already cached which seems correct.

So can I safely ignore that warning? Or are there additional steps I need to take to ensure the cache has the right format for frameworks and static libraries (because I want the CI/CD to keep using static libraries)?

It’s not clear to me where those symbols exactly are coming from, so not sure what else you could do. But dynamic frameworks are either way preferred for local development.

This should have been fixed in Do not output warnings for cache xcframeworks linked mulitple times by fortmarek · Pull Request #7158 · tuist/tuist · GitHub. Try upgrading to tuist 4.38.1 – those warnings should hopefully be gone. Yes, they should be false positives and can be safely ignored.

After updating to Tuist 4.38.1 and rebuilding the cache, most of the warnings are gone but they remain for some dependencies (mainly Google / Firebase ones):

The following warnings need attention:
 · Target 'GoogleUtilities-UserDefaults' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'FirebaseCoreInternal' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'GoogleAppMeasurementTarget' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'GoogleUtilities-Logger' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'Firebase' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'GoogleUtilities-Environment' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'GoogleUtilities-Network' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Xcframework 'GoogleAppMeasurementIdentitySupport.xcframework' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'FirebaseCore' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'GoogleUtilities-NSData' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'FirebaseInstallations' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'GoogleUtilities-Reachability' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'GoogleUtilities-AppDelegateSwizzler' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'GoogleUtilities-MethodSwizzler' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Xcframework 'GoogleAppMeasurement.xcframework' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'nanopb' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'FirebaseAnalyticsTarget' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'third-party-IsAppEncrypted' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'FBLPromises' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Xcframework 'FirebaseAnalytics.xcframework' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'FirebaseAnalyticsWrapper' has been linked from target 'MainAppTarget' and target 'Reporting', it is a static product so may introduce unwanted side effects.
 · Target 'SwiftRichString' has been linked from target 'MainAppTarget' and target 'UITools', it is a static product so may introduce unwanted side effects.
 · Target 'BetterSegmentedControl' has been linked from target 'MainAppTarget' and target 'SofiaBetterSegmentedControl', it is a static product so may introduce unwanted side effects.

Notice how they are worded differently. Are they not false positives?

I believe those are not false positives. You should see those warnings even when you generate a project with tuist generate --no-binary-cache and with your internal targets being dynamic.

You can change the product types of most of those dependencies to dynamic using the PackageSettings.productTypes: PackageSettings · Project Description · References · Tuist. However, you can’t change the product type for some of the prebuilt frameworks like FirebaseAnalytics.xcframework. You can ignore those warnings as long as you use static frameworks when building for release.

It looks like I have to specify the product type for transitive dependencies too, which is annoying.

Is there a way to specify the default product type for all products, without specifying their name?

Also how does this affect the cache? I noticed the cache needed to be rebuilt when I added the product type. Does that mean that caching is dependent on the product type? Which would make sense.

Yes, I undestand that’s annoying. There’s an issue, so that transitive dependencies respect the product type of their parent, however, we haven’t had the chance to implement it, yet: When using cached xcframeworks for SPM packages, nested packages don't respect product type of their parent. · Issue #6638 · tuist/tuist · GitHub

Currently, no. But it’s something we could add and we would accept a contribution for that. There’s an issue for that as well: SPM Targets with `automatic` product type is always fixed to `static` product type · Issue #6616 · tuist/tuist · GitHub

Yes, that is correct.

Okay thanks, I think I’ll leave it at that and ignore the warnings locally.

Last question, is there a way to default to --external-only when running tuist cache so that I don’t have to specify the option every time? That would also prevent from running tuist cache without --external-only, to guarantee that our internal frameworks are never cached by mistake.

Not right now. We should add support for making that configurable through environment variables like we do for most of our other commands. Would you mind raising an issue?

Once implemented, you should be able to do the following:

export TUIST_CACHE_EXTERNAL_ONLY=true

tuist cache # by default uses external only as long as the above variable is defined

Sure I will raise an issue.

I would personally prefer an option inside Tuist.swift over an environment variable because that way I’m sure the setting is always set. New contributors to my project can’t forget to set it, and it doesn’t add an additional step to the project setup.

We try to keep the options in Tuist.swift limited. The environment variables for tuist flags and options are preferred as they scale easily without bloating the interface of our configuration files.

I understand it’s an extra thing you have to configure, but if you end up using Mise, that’s really easy to do: Environments | mise-en-place

If mise can specify the environment then it’s all good !