Skip to content

Commit 97fad74

Browse files
authored
[Commands] swift package migrate command (#8613)
### Motivation: `swift package migrate` could be used to migrate whole package or its individual targets to use the given feature(s) that support migration mode. This is paired with Swift compiler changes to add `:migration` mode to upcoming/experimental features. ### Modifications: - `UserToolchain` gained an ability to list features supported by the Swift compiler. - Implementation of the `add-setting` package command has been slightly refactored to allow use of manifest modification logic without having to call the command itself. - `ModuleBuildDescription` protocol can now list diagnostic files - Added SwiftMigratedCommand that accepts targets (optional) and features, performs a build and uses the newly added `SwiftFixIt` API to apply fix-its and `AddSwiftSetting` API to enable features. ### Result: SwiftPM gained a new command `swift package migrate --targets <target,...> --to-feature <feature> [--to-feature <...>]`
1 parent 7571d6a commit 97fad74

File tree

16 files changed

+468
-15
lines changed

16 files changed

+468
-15
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// swift-tools-version:5.8
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "ExistentialAnyMigration",
7+
targets: [
8+
.target(name: "Diagnostics", path: "Sources", exclude: ["Fixed"]),
9+
]
10+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
protocol P {
2+
}
3+
4+
func test1(_: any P) {
5+
}
6+
7+
func test2(_: (any P).Protocol) {
8+
}
9+
10+
func test3() {
11+
let _: [(any P)?] = []
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
protocol P {
2+
}
3+
4+
func test1(_: P) {
5+
}
6+
7+
func test2(_: P.Protocol) {
8+
}
9+
10+
func test3() {
11+
let _: [P?] = []
12+
}

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ let package = Package(
138138
type: .dynamic,
139139
targets: ["AppleProductTypes"]
140140
),
141-
141+
142142
.library(
143143
name: "PackagePlugin",
144144
type: .dynamic,
@@ -588,6 +588,7 @@ let package = Package(
588588
"Workspace",
589589
"XCBuildSupport",
590590
"SwiftBuildSupport",
591+
"SwiftFixIt",
591592
] + swiftSyntaxDependencies(["SwiftIDEUtils"]),
592593
exclude: ["CMakeLists.txt", "README.md"],
593594
swiftSettings: swift6CompatibleExperimentalFeatures + [

Sources/Build/BuildDescription/ModuleBuildDescription.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ public enum ModuleBuildDescription: SPMBuildCore.ModuleBuildDescription {
140140
}
141141
}
142142

143+
public var diagnosticFiles: [AbsolutePath] {
144+
switch self {
145+
case .swift(let buildDescription):
146+
buildDescription.diagnosticFiles
147+
case .clang(_):
148+
[]
149+
}
150+
}
143151
/// Determines the arguments needed to run `swift-symbolgraph-extract` for
144152
/// this module.
145153
public func symbolGraphExtractArguments() throws -> [String] {

Sources/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ add_subdirectory(SourceKitLSPAPI)
3030
add_subdirectory(SPMBuildCore)
3131
add_subdirectory(SPMLLBuild)
3232
add_subdirectory(SPMSQLite3)
33+
add_subdirectory(SwiftFixIt)
3334
add_subdirectory(swift-bootstrap)
3435
add_subdirectory(swift-build)
3536
add_subdirectory(swift-experimental-sdk)

Sources/Commands/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ add_library(Commands
2424
PackageCommands/Init.swift
2525
PackageCommands/Install.swift
2626
PackageCommands/Learn.swift
27+
PackageCommands/Migrate.swift
2728
PackageCommands/PluginCommand.swift
2829
PackageCommands/ResetCommands.swift
2930
PackageCommands/Resolve.swift
@@ -64,6 +65,7 @@ target_link_libraries(Commands PUBLIC
6465
PackageGraph
6566
PackageModelSyntax
6667
SourceControl
68+
SwiftFixIt
6769
TSCBasic
6870
TSCUtility
6971
Workspace

Sources/Commands/PackageCommands/AddSetting.swift

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,47 @@ extension SwiftPackageCommand {
6767
}
6868

6969
func run(_ swiftCommandState: SwiftCommandState) throws {
70+
if !self._swiftSettings.isEmpty {
71+
try Self.editSwiftSettings(
72+
of: self.target,
73+
using: swiftCommandState,
74+
self.swiftSettings,
75+
verbose: !self.globalOptions.logging.quiet
76+
)
77+
}
78+
}
79+
80+
package static func editSwiftSettings(
81+
of target: String,
82+
using swiftCommandState: SwiftCommandState,
83+
_ settings: [(SwiftSetting, String)],
84+
verbose: Bool = false
85+
) throws {
7086
let workspace = try swiftCommandState.getActiveWorkspace()
7187
guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else {
7288
throw StringError("unknown package")
7389
}
7490

75-
try self.applyEdits(packagePath: packagePath, workspace: workspace)
91+
try self.applyEdits(
92+
packagePath: packagePath,
93+
workspace: workspace,
94+
target: target,
95+
swiftSettings: settings
96+
)
7697
}
7798

78-
private func applyEdits(
99+
private static func applyEdits(
79100
packagePath: Basics.AbsolutePath,
80-
workspace: Workspace
101+
workspace: Workspace,
102+
target: String,
103+
swiftSettings: [(SwiftSetting, String)],
104+
verbose: Bool = false
81105
) throws {
82106
// Load the manifest file
83107
let fileSystem = workspace.fileSystem
84108
let manifestPath = packagePath.appending(component: Manifest.filename)
85109

86-
for (setting, value) in try self.swiftSettings {
110+
for (setting, value) in swiftSettings {
87111
let manifestContents: ByteString
88112
do {
89113
manifestContents = try fileSystem.readFileContents(manifestPath)
@@ -105,13 +129,13 @@ extension SwiftPackageCommand {
105129
switch setting {
106130
case .experimentalFeature:
107131
editResult = try AddSwiftSetting.experimentalFeature(
108-
to: self.target,
132+
to: target,
109133
name: value,
110134
manifest: manifestSyntax
111135
)
112136
case .upcomingFeature:
113137
editResult = try AddSwiftSetting.upcomingFeature(
114-
to: self.target,
138+
to: target,
115139
name: value,
116140
manifest: manifestSyntax
117141
)
@@ -121,7 +145,7 @@ extension SwiftPackageCommand {
121145
}
122146

123147
editResult = try AddSwiftSetting.languageMode(
124-
to: self.target,
148+
to: target,
125149
mode: mode,
126150
manifest: manifestSyntax
127151
)
@@ -131,7 +155,7 @@ extension SwiftPackageCommand {
131155
}
132156

133157
editResult = try AddSwiftSetting.strictMemorySafety(
134-
to: self.target,
158+
to: target,
135159
manifest: manifestSyntax
136160
)
137161
}
@@ -140,7 +164,7 @@ extension SwiftPackageCommand {
140164
to: fileSystem,
141165
manifest: manifestSyntax,
142166
manifestPath: manifestPath,
143-
verbose: !self.globalOptions.logging.quiet
167+
verbose: verbose
144168
)
145169
}
146170
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
15+
import Basics
16+
17+
@_spi(SwiftPMInternal)
18+
import CoreCommands
19+
20+
import Foundation
21+
22+
import PackageGraph
23+
import PackageModel
24+
25+
import SPMBuildCore
26+
import SwiftFixIt
27+
28+
import var TSCBasic.stdoutStream
29+
30+
struct MigrateOptions: ParsableArguments {
31+
@Option(
32+
name: .customLong("targets"),
33+
help: "The targets to migrate to specified set of features."
34+
)
35+
var _targets: String?
36+
37+
var targets: Set<String>? {
38+
self._targets.flatMap { Set($0.components(separatedBy: ",")) }
39+
}
40+
41+
@Option(
42+
name: .customLong("to-feature"),
43+
parsing: .unconditionalSingleValue,
44+
help: "The Swift language upcoming/experimental feature to migrate to."
45+
)
46+
var features: [String]
47+
}
48+
49+
extension SwiftPackageCommand {
50+
struct Migrate: AsyncSwiftCommand {
51+
package static let configuration = CommandConfiguration(
52+
abstract: "Migrate a package or its individual targets to use the given set of features."
53+
)
54+
55+
@OptionGroup()
56+
public var globalOptions: GlobalOptions
57+
58+
@OptionGroup()
59+
var options: MigrateOptions
60+
61+
public func run(_ swiftCommandState: SwiftCommandState) async throws {
62+
let toolchain = try swiftCommandState.productsBuildParameters.toolchain
63+
64+
let supportedFeatures = try Dictionary(
65+
uniqueKeysWithValues: toolchain.swiftCompilerSupportedFeatures
66+
.map { ($0.name, $0) }
67+
)
68+
69+
// First, let's validate that all of the features are supported
70+
// by the compiler and are migratable.
71+
72+
var features: [SwiftCompilerFeature] = []
73+
for name in self.options.features {
74+
guard let feature = supportedFeatures[name] else {
75+
let migratableFeatures = supportedFeatures.map(\.value).filter(\.migratable).map(\.name)
76+
throw ValidationError(
77+
"Unsupported feature: \(name). Available features: \(migratableFeatures.joined(separator: ", "))"
78+
)
79+
}
80+
81+
guard feature.migratable else {
82+
throw ValidationError("Feature '\(name)' is not migratable")
83+
}
84+
85+
features.append(feature)
86+
}
87+
88+
let buildSystem = try await createBuildSystem(
89+
swiftCommandState,
90+
features: features
91+
)
92+
93+
// Next, let's build all of the individual targets or the
94+
// whole project to get diagnostic files.
95+
96+
print("> Starting the build.")
97+
if let targets = self.options.targets {
98+
for target in targets {
99+
try await buildSystem.build(subset: .target(target))
100+
}
101+
} else {
102+
try await buildSystem.build(subset: .allIncludingTests)
103+
}
104+
105+
// Determine all of the targets we need up update.
106+
let buildPlan = try buildSystem.buildPlan
107+
108+
var modules: [any ModuleBuildDescription] = []
109+
if let targets = self.options.targets {
110+
for buildDescription in buildPlan.buildModules where targets.contains(buildDescription.module.name) {
111+
modules.append(buildDescription)
112+
}
113+
} else {
114+
let graph = try await buildSystem.getPackageGraph()
115+
for buildDescription in buildPlan.buildModules
116+
where graph.isRootPackage(buildDescription.package) && buildDescription.module.type != .plugin
117+
{
118+
modules.append(buildDescription)
119+
}
120+
}
121+
122+
// If the build suceeded, let's extract all of the diagnostic
123+
// files from build plan and feed them to the fix-it tool.
124+
125+
print("> Applying fix-its.")
126+
for module in modules {
127+
let fixit = try SwiftFixIt(
128+
diagnosticFiles: module.diagnosticFiles,
129+
fileSystem: swiftCommandState.fileSystem
130+
)
131+
try fixit.applyFixIts()
132+
}
133+
134+
// Once the fix-its were applied, it's time to update the
135+
// manifest with newly adopted feature settings.
136+
137+
print("> Updating manifest.")
138+
for module in modules.map(\.module) {
139+
print("> Adding feature(s) to '\(module.name)'.")
140+
for feature in features {
141+
self.updateManifest(
142+
for: module.name,
143+
add: feature,
144+
using: swiftCommandState
145+
)
146+
}
147+
}
148+
}
149+
150+
private func createBuildSystem(
151+
_ swiftCommandState: SwiftCommandState,
152+
features: [SwiftCompilerFeature]
153+
) async throws -> BuildSystem {
154+
let toolsBuildParameters = try swiftCommandState.toolsBuildParameters
155+
var destinationBuildParameters = try swiftCommandState.productsBuildParameters
156+
157+
// Inject feature settings as flags. This is safe and not as invasive
158+
// as trying to update manifest because in adoption mode the features
159+
// can only produce warnings.
160+
for feature in features {
161+
destinationBuildParameters.flags.swiftCompilerFlags.append(contentsOf: [
162+
"-Xfrontend",
163+
"-enable-\(feature.upcoming ? "upcoming" : "experimental")-feature",
164+
"-Xfrontend",
165+
"\(feature.name):migrate",
166+
])
167+
}
168+
169+
return try await swiftCommandState.createBuildSystem(
170+
traitConfiguration: .init(),
171+
productsBuildParameters: destinationBuildParameters,
172+
toolsBuildParameters: toolsBuildParameters,
173+
// command result output goes on stdout
174+
// ie "swift build" should output to stdout
175+
outputStream: TSCBasic.stdoutStream
176+
)
177+
}
178+
179+
private func updateManifest(
180+
for target: String,
181+
add feature: SwiftCompilerFeature,
182+
using swiftCommandState: SwiftCommandState
183+
) {
184+
typealias SwiftSetting = SwiftPackageCommand.AddSetting.SwiftSetting
185+
186+
let setting: (SwiftSetting, String) = switch feature {
187+
case .upcoming(name: let name, migratable: _, enabledIn: _):
188+
(.upcomingFeature, "\(name)")
189+
case .experimental(name: let name, migratable: _):
190+
(.experimentalFeature, "\(name)")
191+
}
192+
193+
do {
194+
try SwiftPackageCommand.AddSetting.editSwiftSettings(
195+
of: target,
196+
using: swiftCommandState,
197+
[setting]
198+
)
199+
} catch {
200+
print(
201+
"! Couldn't update manifest due to - \(error); Please add '.enable\(feature.upcoming ? "Upcoming" : "Experimental")Feature(\"\(feature.name)\")' to target '\(target)' settings manually."
202+
)
203+
}
204+
}
205+
206+
public init() {}
207+
}
208+
}

0 commit comments

Comments
 (0)