Hive Hive
Sign in

tuist generate crashes with “Could not locate project at path” when an external SPM package transitively depends on a build-tool-plugin product

GitHub issue · Closed

Metadata
Source
tuist/tuist #11453
Updated
Jun 26, 2026
Domains
Generated projects
Details

What happened?

Summary

When an external SPM dependency declares a build-tool plugin product in one of its target dependencies: (rather than plugins:), tuist generate fails with:

Could not locate project at path: {project-path}/Tuist/.build/checkouts/<PluginPackage>

Tuist already ignores build-tool plugins during project generation (a plugin-only package yields no project), but it still emits an external-dependency edge pointing at that non-existent project. The two paths are asymmetric, so a plugin pulled in transitively (by a package we consume via .external, not by us directly) crashes generation. This is distinct from #11358 (directly consuming a plugin via .external): here we never reference the plugin - a dependency does - and we cannot change that dependency’s manifest.

Note: If LibPkg instead attaches the plugin the canonical way - plugins: [.plugin(name: "MyBuildToolPlugin", package: "PluginPkg")] - generation succeeds. Only the plugin-product-in-dependencies: form crashes.

Environment

  • Tuist 4.201.0-rc.1 (also reproduced on 4.200.5)
  • macOS 26.5.1
  • Xcode 26.5.0
  • SPM integration via Tuist/Package.swift + .external(name:)

Root cause

cli/Sources/TuistLoader/SwiftPackageManager/PackageInfoMapper.swift (source @ tag 4.201.0-rc.1, commit 2f2ee30):

  • Project generation filters plugins: map(target:) switch (~L715-732) sends .plugin (and .binary) target types to default → return nil; map(packageInfo:) then hits guard !targets.isEmpty else { return nil } (L420), so a plugin-only package produces no project (correctly absent from externalProjects).
  • Dependency-edge generation does NOT filter plugins: resolveExternalDependencies (L240-276) iterates the plugin product‘s targets; ResolvedDependency.fromTarget (L1438) returns .target(name:) (a plugin target is not a known framework), rewritten to .project(target:, path: <checkout>) - an edge to the project that was never generated. The transitive consumer’s dependencies entry resolves via mapDependency (L1127 → else L1194-1200) to .external(name:), which links to that broken edge.
  • TuistCore.GraphLoader (GraphLoader.swift:77/:99) then throws GraphLoadingError.missingProject (message defined in GraphLoadingError.swift:25).

Asymmetry: plugins are filtered out of project generation but not out of external-dependency-edge generation.

Possible fix (suggestion; needs verification)

In resolveExternalDependencies / ResolvedDependency.fromTarget, skip products whose only targets are .plugin (mirror the per-target plugin filter already used in map(target:)), and/or tolerate a missing project when an .external resolves to a plugin-only product. This matches Tuist’s documented behavior of ignoring SPM build-tool plugins.

How do we reproduce it?

  1. Use the attached sample project (minimal, self-contained).
  2. Run:
    mise install
    mise x -- tuist install
    mise x -- tuist generate --no-open
  3. Expected result:
    • Generation succeeds.
    • The build-tool plugin is ignored for an externally-integrated package (consistent with how Tuist already drops plugin targets during project generation).
  4. Actual result:
    • Tuist fails to generate the project with error “Could not locate project at path: {project-path}/PluginPkg”

Example project: TUIST-buildtool-plugin-bug-report.zip

Error log

$ mise x -- tuist generate --no-open
Loading and constructing the graph
It might take a while if the cache is empty
Error
Could not locate project at path: {project-path}/PluginPkg
Sorry this didn’t work. Here’s what to try next:
If the error is actionable, address it
If the error is not actionable, let's discuss it in the Troubleshooting & how to
▸ If you are very certain it's a bug, file an issue
Check out the logs at {user-home}/.local/state/tuist/sessions/8F6505BD-2251-403D-B0C2-8309E0D55938/logs.txt

{user-home}/.local/state/tuist/sessions/8F6505BD-2251-403D-B0C2-8309E0D55938/logs.txt:

[2026-06-23T14:47:43Z] [debug] [TuistCacheEE] Using the concurrency limit of 100 for the cache's HTTP connections.
[2026-06-23T14:47:43Z] [debug] [TuistKit] Using cache profile onlyExternal
[2026-06-23T14:47:43Z] [notice] [TuistKit] Loading and constructing the graph ["is": section]
[2026-06-23T14:47:43Z] [notice] [TuistKit] It might take a while if the cache is empty
[2026-06-23T14:47:44Z] [debug] [TuistSupport] Waiting for Swift Package Manager lock at [project-path]/Tuist/.build
[2026-06-23T14:47:44Z] [debug] [TuistSupport] Acquired Swift Package Manager lock at [project-path]/Tuist/.build
[2026-06-23T14:47:44Z] [debug] [TuistLoader] Target MyBuildToolPlugin of type plugin ignored
[2026-06-23T14:47:44Z] [debug] [Noora] Error alert: ✖ Error
- Message: Could not locate project at path: <project-path>/PluginPkg
- Takeaways:
- If the error is actionable, address it
- If the error is not actionable, let's discuss it in the (Troubleshooting & how to)
- If you are very certain it's a bug, (file an issue)
- Check out the logs at {user-home}/.local/state/tuist/sessions/8F6505BD-2251-403D-B0C2-8309E0D55938/logs.txt

macOS version

26.5.1

Tuist version

4.201.0-rc.1

Xcode version

26.5.0

Comments
D
dosubot[bot] Jun 23, 2026

Thanks for the detailed bug report and root-cause analysis — I can confirm your diagnosis looks correct based on the code.

The asymmetry is in PackageInfoMapper.swift: resolveExternalDependencies iterates all products without checking product.type, so it creates dependency edges for .plugin products [1]. Meanwhile, map(target:) correctly filters plugin targets via its default → return nil case [2], meaning the plugin package never produces a project — hence the crash when the graph loader tries to follow that dangling edge.

A recent documentation PR (merged 2026-06-18) also explicitly acknowledges that build-tool plugins cannot be mapped via Tuist’s XcodeProj-based system [3], which further supports that resolveExternalDependencies should skip plugin products entirely.

Your suggested fix direction — filtering out products whose type is .plugin (or whose only targets are .plugin) inside resolveExternalDependencies — aligns with how the rest of the mapper already handles plugins. There’s also an open PR exploring native plugin target modeling [4], but that’s a broader effort.

If you’d like to try tackling a fix yourself, the key spot is around lines 240–276 in PackageInfoMapper.swift — adding a guard product.type != .plugin else { continue } (or equivalent check) before the product.targets.flatMap call should prevent the broken edge from being emitted.

To reply, just mention @dosu.


Share context across your team and agents. Try Dosu.

Leave Feedback Learn about tuist with Dosu Add Dosu to your team