How to setup binaries for a Tuist project with 1 project, 1 target, multiple local packages with 3rd party dependencies

Question or problem

Hi folks,

Im new user of the tuist project generation.
In my current setup I generate a single xcworkspace that contains 1 xcode project with a target that depends on local Swift packages. The project itself depends on few 3rd party Swift Pacckages.
Additionally the local Swift packages do also depend on 3rd party Swift Packages.

Is there a guideline/tutorial where I could find how to ensure that all the 3rd party dependencies are cached & linked as xcframeworks?

Resolving the 3rd party dependencies takes a while :slight_smile:

Im happy to share my setup if that helps.

The dependency tree in my Project.swift look moreless like this:

let commonDependencies: [TargetDependency] = [
    .package(product: "Networking", type: .runtime),
    .package(product: "Assets", type: .runtime),
    .package(product: "SharedUI", type: .runtime),
    .package(product: "Analytics", type: .runtime),
    .package(product: "FirebaseCrashlytics", type: .runtime),
    .package(product: "FirebaseAnalytics", type: .runtime),
    .package(product: "ComposableArchitecture", type: .runtime),
]

let project = Project(
    name: "my_project",
    packages: [
        .local(path: .relativeToManifest("./Networking")),
        .local(path: .relativeToManifest("./Assets")),
        .local(path: .relativeToManifest("./SharedUI")),
        .local(path: .relativeToManifest("./Analytics")),
        .remote(
            url: "https://github.com/firebase/firebase-ios-sdk",
            requirement: .upToNextMajor(from: "11.8.1")
        ),
        .remote(
            url: "https://github.com/pointfreeco/swift-composable-architecture",
            requirement: .upToNextMajor(from: "1.17.0")
        ),
        .remote(
            url: "https://github.com/ProxymanApp/atlantis",
            requirement: .upToNextMajor(from: "1.26.0")
        ),
    ],
    targets: [
      .target(..., dependencies: commonDependencies)
    ]
    ...

Worth to note, my tuist configuration skills are pretty lame, so far I only configured the Project.swift :slight_smile:

Thanks for the help!

Hi @bielikb and welcome to the forum :wave:

Welcome to project generation too :slight_smile: . What motivated you to adopt it?

Is there a guideline/tutorial where I could find how to ensure that all the 3rd party dependencies are cached & linked as xcframeworks?

Note have two solutions that address:

  • Slow dependency resolution (solved by our registry)
  • Slow clean builds (solved by caching)

With the later you can turn your targets and external dependencies into XCFrameworks and link against them. Note though that your dependencies should be integrated using XcodeProj-based integration such that they are part of your project’s graph.

I’d recommend playing with the GitHub - tuist/tuist: Tuist's CLI repository itself:

git clone https://github.com/tuist/tuist.git
tuist install
tuist cache
tuist generate

Let me know if you have questions.

1 Like

Hey @pepicrft,

What motivated you to adopt it?

In the team we’re leveraging the xcodegen & I was playing around to see if I could replicate the basic setup with our dependency slightly complex dependency graph :). The main motivator’s to figure out if we can leverage tuist for feature (focus) driven development, the caching is a big part of it.


Thanks a lot for these resources! It’s really useful to see integration examples!

I followed the samples and think I got to a decent point, where my project with local Swift packages now follows the XCodeProj-based integration.


Topic: Resources + SwiftGen

I got stumbled upon handling the resources, esp. leveraging the resourceSynthesizers in the dedicated Project.swift for the DesignSystem package.

Currently we have DesignSystem package that is composed of several targets (Assets, Colors, Typography). On top of each target we leverage SwiftGen with default stencils (eg Assets).

The folder structure per target looks like the following:
DesignSystem/Assets/Resources/Assets.xcassets

I tried to describe a single target in Project.swift located under DesignSystem folder as follows:

let project = Project(
    name: "DesignSystem",
    targets: [
        .target(
            name: "Assets",
            destinations: [.iPhone, .iPad],
            product: .framework,
            bundleId: "...",
            sources: ["Sources/Assets/*.swift"],
            resources: ["Sources/Assets/Resources/**"],
        ),
        ...
    ],
    ...,
    resourceSynthesizers: [
        .assets(),
        ...
    ]
)

However, this doesn’t work, since I don’t get any swift files generated for my .xcassets.
I wonder if this could be also due to our specific SwiftGen configuration.

The contents of swiftgen,yml:

input_dir: Resources
output_dir: ${DERIVED_SOURCES_DIR}

xcassets:
    inputs: Assets.xcassets
    outputs:
        params:
            bundle: Bundle.module
            forceProvidesNamespaces: true
            publicAccess: true
        templatePath: ./Assets.stencil
        output: Assets.swift

At this point, any hints would be super useful :slight_smile:
I believe there’s a way to achieve this, so far I enjoyed configuring the tuist - it’s really well done!

Best,
Boris

Thanks @bielikb for sharing your motivations :slight_smile:

XcodeGen is amazing too. They just made different design decisions and trade-offs. We decided to go the path of using Swift as a description language and put a strong focus on conceptually compressing the complexities of modular Xcode projects, and it turned out people like it a lot. It’s a lot of fun editing your projects in Xcode, isn’t it?

Synthesized interfaces for resources

I recommend checking out these docs. If I’m not mistaken, you’ll have to move your stencil template to:

Tuist/ResourceSynthesizers/Assets.stencil

Then you can use the .assets() options in your project, and Tuist will read the template from that directory. Note that you can pass the options there directly.

With that, you don’t need the swiftgen.yml file :slight_smile:

1 Like

Great, I was able to implement the swiftgen.

Thanks for the suggestion, the reason the swiftgen didn’t work for me is that I forgot to remove the dependency to local Swift Package in my Tuist/Package.swift :slight_smile:

I was able to provide Project.swift for all our respective Swift Packages.

Topic: Build tool plugins lead to duplicated dependencies

However Im hitting another issue. In my new XcodeProj-based integration I’ve got a Networking package that is driven by swift-openapi-generator.

I followed the documentation to integrate dependencies and stumbled upon this callout:

SPM Build Tool Plugins
SPM build tool plugins must be declared using Xcode’s default integration mechanism, even when using Tuist’s XcodeProj-based integration for your project dependencies.

Since my Networking module leverages swift-openai plugin to generate the API layer at the build time, I integrated the plugin to my Networking module as follows:

# Networking/Project.swift
import ProjectDescription

let project = Project(
    name: "Framework",
    packages: [
        .remote(url: "https://github.com/apple/swift-openapi-generator.git", requirement: .exact("1.7.0")),
    ],
    targets: [
        .target(
            name: "Networking",
            dependencies: [
                .external(name: "OpenAPIRuntime"),
                .external(name: "OpenAPIURLSession"),
                .package(product: "OpenAPIGenerator", type: .plugin),
            ]
        ),
    ]
)

This means, I left the world of the XcodeProj-based integration and implemented the plugin via typical project dependencies.

This integration however brings an issue in my current project setup,

App
– Networking
– DesignSystem
– Analytics

Since we’re leveraging ComposableArchitecture and other macros that as well depend on the swift-syntax package in our respective Swift Packages (modules), Im hitting the issue with the dependency resolutions.

Multiple commands produce '/var/folders/jg/4jcydlld5fgcg12jsxlf1ldm0000gn/T/TemporaryDirectory.Jddp5d/derived-data/Build/Intermediates.noindex/swift-syntax.build/Debug/SwiftBasicFormat.build/Objects-normal/arm64/SwiftBasicFormat Swift Compilation Requirements Finished'

I tried to find a way how to ensure:

  • I can still leverage the XcodeProj based integration, since it brings lot of benefits
  • I need to run the OpenAPIGenerator plugin to ensure the latest version of our openapi spec is always respected.

but I couldn’t find a one. And thus the resolution will always result in duplicated frameworks/symbols being produced (Swift Packages x Projects).

Any hints how to overcome this blocker, would be really appreciated. :pray:

Additionally here’s my version of Tuist/Package.swift, which is pretty straightforward.

// swift-tools-version: 6.0
@preconcurrency import PackageDescription

#if TUIST
import ProjectDescription

let packageSettings = PackageSettings()
#endif

let package = Package(
  name: "TestIntegration",
  platforms: [
      .iOS(.v17),
  ],
  dependencies: [
        .package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.18.0"),
        .package(url: "https://github.com/firebase/firebase-ios-sdk", exact: "11.9.0"),
        .package(url: "https://github.com/ProxymanApp/atlantis", exact: "1.26.0"),

        .package(url: "https://github.com/apple/swift-openapi-runtime.git", exact: "1.8.0"),
        .package(url: "https://github.com/apple/swift-openapi-urlsession.git", exact: "1.0.2"),

        .package(url: "https://github.com/apple/swift-http-types", exact: "1.3.1"),
        .package(url: "https://github.com/luppoalberto111/swift-macros", branch: "main"),
        .package(url: "https://github.com/aws-amplify/amplify-swift.git", exact: "2.46.0"),

        .package(url: "https://github.com/lokalise/lokalise-ios-framework.git", exact: "0.10.2"),
        .package(url: "https://github.com/SDWebImage/SDWebImageSwiftUI.git", exact: "3.1.3"),
        .package(url: "https://github.com/SDWebImage/SDWebImageSVGCoder.git", exact: "1.8.0")
    ]
)

Thanks!

Best,
Boris

Unfortunately, build tool plugins are not currently supported by the XcodeProj-based integration. There’s an issue for that here. I’d recommend either:

  • Moving away from the XcodeProj-based integration for now if the build plugin is a requirement
  • Consider using a build phase to generate the OpenAPI definition. It would be very similar to running this command.

I’d suggest exploring if a build tool plugin is actually a requirement. FWIW, the vast majority of build tool plugins should be possible to run from build phases instead, including the open api generation.