Skip to content

Assure trait-guarded dependencies are not included in resolution; precompute enabled traits before resolution #8852

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jul 17, 2025

Conversation

bripeticca
Copy link
Contributor

@bripeticca bripeticca commented Jun 19, 2025

Trait-guarded dependencies were still being considered during dependency resolution, when they should be excluded if they aren't being used in any other scenario. Additionally, enabled traits should be pre-computed before entering the resolution stage in order to avoid possible race conditions when navigating through the dependency graph nodes.

Modifications:

Since we have the --experimental-prune-unused-dependencies feature behind an experimental flag, we'll now consider an alternate path that will prune trait-guarded package dependencies from the dependency graph if and only if said dependency is not used in any other unguarded context.

A dictionary wrapper titled EnabledTraitsMap is used to store the enabled traits per package for the package graph and is stored as a property within the Workspace, with some additional behaviour to return a ["default"] set of traits if the package has not yet been stored in the dictionary, rather than returning nil.

Following this behaviour, when passing a set of traits to methods that require them (e.g. for dependency computation, enabled trait computation, etc.) we now require that it is not Optional, since the checks done on a nil set of traits were redundant.

SwiftCommandState now also stores a TraitConfiguration, since we'll want access to this across multiple SwiftCommands, and it is essentially a part of the state anyhow. TraitOptions is now included in the GlobalOptions for SwiftCommands to supplement this, so that when a SwiftCommandState is created we will have access to the user-passed enabled traits. These options, as entitled, are available globally across all the swift package commands, so previous properties that declared TraitOptions in these commands has been removed in favour of using it straight from the GlobalOptions.

Result:

Trait-guarded dependencies are excluded from dependency resolution, and since traits are pre-computed there should no longer be an issue with race conditions for traits in resolution as well.

@bripeticca
Copy link
Contributor Author

@swift-ci please test

- Create a 'precomputeTraits' method that cycles through the graph to
  unify all the traits
- Modify the traits error message to include both the package identity
  and display name for better readability

TODO
- change instances of optional enabledTraits to non-optional, defaulting
  instead to ["default"]
- assure that loaded manifests arent fetching guarded dependencies -
  this is contingent on how we are calculating the unified traits
  afterwards
Since there is a bit of redundancy when checking the conditions
of an optional set of enabled traits, it makes more sense to
simply have the set of enabled traits be non-optional, and to
derive the checks for defaults by asserting whether the set
contains the "default" keyword.
To help with the computation of traits, a wrapper struct
for a dictionary of traits has been created wherein the default
value when a key isn't found is no longer nil but instead ["default"],
since if we haven't defined a set of explicitly enabled traits for
a given package, then it should default to the defaults.
* Whenever this method is called, it will store a previously-
created Workspace object in the SwiftCommandState. Some calls
to this method were missing the traitConfiguration, thus instantiating
the Workspace without the necessary trait config.
@bripeticca
Copy link
Contributor Author

@swift-ci please test

- Previous calls to getActiveWorkspace could have defaulted to using the
default trait configuration (re: PluginCommand -> PluginDelegate.createSymbolGraphForPlugin case),
therefore we should update the existing workspace's trait configuration if a new one is passed.
Since the trait configuration is used for a variety of swift package subcommands,
it makes it a lot easier to add the trait options as a part of the GlobalOptions which
is consumed by SwiftCommandState and persisted across all SwiftCommands.

This also allows us to omit passing a trait configuration to the build system factory
methods, and instead replacing it with a flag that can override the trait configuration
to enable all traits for edge-case scenarios (e.g. fetching symbol graphs)
@bripeticca
Copy link
Contributor Author

@swift-ci please test

@bripeticca
Copy link
Contributor Author

@swift-ci please test

@bripeticca
Copy link
Contributor Author

@swift-ci please test

@bripeticca bripeticca marked this pull request as ready for review July 11, 2025 17:41
@bripeticca bripeticca requested a review from FranzBusch July 11, 2025 17:41
@bripeticca bripeticca changed the title [WIP] Fix for trait-guarded deps being included in resolution Fix for trait-guarded deps being included in resolution Jul 11, 2025
@bripeticca bripeticca changed the title Fix for trait-guarded deps being included in resolution Assure trait-guarded dependencies are not included in resolution; precompute enabled traits before resolution Jul 11, 2025
@bripeticca
Copy link
Contributor Author

@swift-ci please test

Copy link
Member

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing! So great to see unused dependencies getting pruned

//===----------------------------------------------------------------------===//

/// A wrapper for a dictionary that stores the transitively enabled traits for each package.
public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be public?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll double check, it may not need to be

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah this does need to be public since it's used in public initializers in other packages.

Since we'll have others consuming the ModulesGraph load method,
create a version of the method without the enabledTraitsMap
parameter.
@bripeticca
Copy link
Contributor Author

@swift-ci please test

@bripeticca
Copy link
Contributor Author

@swift-ci please test windows

@@ -53,7 +53,7 @@ struct DumpSymbolGraph: AsyncSwiftCommand {
// We turn build manifest caching off because we need the build plan.
let buildSystem = try await swiftCommandState.createBuildSystem(
// We are enabling all traits for dumping the symbol graph.
traitConfiguration: .init(enableAllTraits: true),
enableAllTraits: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what benefit does passing this argument have on the creation of a build system? Should the enableAllTraits option be provided when building instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will affect how the SwiftCommandState fetches and stores the currently active workspace, which is determined by its method SwiftCommandState.getActiveWorkspace. When creating a build system, there are many calls to various SwiftCommandState methods that call getActiveWorkspace, and the workspace that is created in this method is stored in its _workspace property.

The workspace is only ever created once in this method, and every subsequent call to this method checks whether the _workspace property has a value. If so, it will return the workspace stored there.

The reason why we have enableAllTraits propagated to this call is because we have special edge case scenarios where we would initially create a SwiftCommandState with some trait configuration, but if we then make a call that wishes to dump the symbol graph (see PluginDelegate.createSymbolGraphForPlugin), it will return a workspace that has whichever trait configuration we've initialized with the SwiftCommandState, which isn't always what we'd want when requesting a dump of the symbol graph.

@@ -292,6 +292,8 @@ public final class SwiftCommandState {

package var preferredBuildConfiguration = BuildConfiguration.debug

public var traitConfiguration: TraitConfiguration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: can this be a let instead of var?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it can!

@@ -56,7 +56,7 @@ extension Package {
public let kind: Kind

/// The dependencies traits configuration.
@available(_PackageDescription, introduced: 999.0)
@available(_PackageDescription, introduced: 6.1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: nice!

],
buildSystem: buildSystem,
)
// We expect no warnings to be produced. Specifically no unused dependency warnings.
let unusedDependencyRegex = try Regex("warning: '.*': dependency '.*' is not used by any target")
#expect(!stderr.contains(unusedDependencyRegex))
if buildSystem == .swiftbuild {
print(stderr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: out of curiosity, why are we printing stderr here? if it's meant to troubleshoot a failed expectation, we can add a comment to the #expect(...) or #require(...)

See the expect(::sourcelocation:) API docs for details.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this must've slipped by me. I was printing out stderr for debugging purposes. Thanks for catching this!

@bripeticca
Copy link
Contributor Author

@swift-ci please test

@dschaefer2
Copy link
Member

@swift-ci please test windows

@bripeticca bripeticca merged commit 6e3f087 into swiftlang:main Jul 17, 2025
6 checks passed
bripeticca added a commit to bripeticca/swift-package-manager that referenced this pull request Jul 17, 2025
…compute enabled traits before resolution (swiftlang#8852)

Trait-guarded dependencies were still being considered during dependency
resolution, when they should be excluded if they aren't being used in
any other scenario. Additionally, enabled traits should be pre-computed
before entering the resolution stage in order to avoid possible race
conditions when navigating through the dependency graph nodes.

Since we have the `--experimental-prune-unused-dependencies` feature
behind an experimental flag, we'll now consider an alternate path that
will prune trait-guarded package dependencies from the dependency graph
**_if and only if_** said dependency is not used in any other unguarded
context.

A dictionary wrapper titled `EnabledTraitsMap` is used to store the
enabled traits per package for the package graph and is stored as a
property within the `Workspace`, with some additional behaviour to
return a `["default"]` set of traits if the package has not yet been
stored in the dictionary, rather than returning `nil`.

Following this behaviour, when passing a set of traits to methods that
require them (e.g. for dependency computation, enabled trait
computation, etc.) we now require that it is not Optional, since the
checks done on a `nil` set of traits were redundant.

SwiftCommandState now also stores a `TraitConfiguration`, since we'll
want access to this across multiple `SwiftCommand`s, and it is
essentially a part of the state anyhow. `TraitOptions` is now included
in the `GlobalOptions` for `SwiftCommand`s to supplement this, so that
when a `SwiftCommandState` is created we will have access to the
user-passed enabled traits. These options, as entitled, are available
globally across all the swift package commands, so previous properties
that declared `TraitOptions` in these commands has been removed in
favour of using it straight from the `GlobalOptions`.

Trait-guarded dependencies are excluded from dependency resolution, and
since traits are pre-computed there should no longer be an issue with
race conditions for traits in resolution as well.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants