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

Flights

Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.

New Flights are paused Configure model inference, GitHub, and a sandbox provider to start another Flight. Existing results remain available below.
No Flights yet

Start a Flight and preserve its objective, outcome, and session here.

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