Skip to content

Commit 53681e0

Browse files
committed
[Commands] Implement swift migrate command
`swift migrate` could be used to migrate whole package or its individual targets to use the given feature(s) that support migration mode.
1 parent e9e5710 commit 53681e0

File tree

13 files changed

+398
-1
lines changed

13 files changed

+398
-1
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: 8 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 + [
@@ -745,6 +746,12 @@ let package = Package(
745746
"Workspace",
746747
]
747748
),
749+
.executableTarget(
750+
/** Builds packages */
751+
name: "swift-migrate",
752+
dependencies: ["Commands"],
753+
exclude: ["CMakeLists.txt"]
754+
),
748755

749756
// MARK: Support for Swift macros, should eventually move to a plugin-based solution
750757

Sources/Commands/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ add_library(Commands
4040
Snippets/Card.swift
4141
Snippets/Colorful.swift
4242
SwiftBuildCommand.swift
43+
SwiftMigrateCommand.swift
4344
SwiftRunCommand.swift
4445
SwiftTestCommand.swift
4546
CommandWorkspaceDelegate.swift
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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 MigrateCommandOptions: 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+
public struct SwiftMigrateCommand: AsyncSwiftCommand {
50+
public static var configuration = CommandConfiguration(
51+
commandName: "migrate",
52+
_superCommandName: "swift",
53+
abstract: "Migrate a package or its individual targets to use the given set of features.",
54+
version: SwiftVersion.current.completeDisplayString,
55+
helpNames: [.short, .long, .customLong("help", withSingleDash: true)]
56+
)
57+
58+
@OptionGroup()
59+
public var globalOptions: GlobalOptions
60+
61+
@OptionGroup()
62+
var options: MigrateCommandOptions
63+
64+
public func run(_ swiftCommandState: SwiftCommandState) async throws {
65+
let toolchain = try swiftCommandState.productsBuildParameters.toolchain
66+
67+
let supportedFeatures = try Dictionary(
68+
uniqueKeysWithValues: toolchain.swiftCompilerSupportedFeatures
69+
.map { ($0.name, $0) }
70+
)
71+
72+
// First, let's validate that all of the features are supported
73+
// by the compiler and are migratable.
74+
75+
var features: [SwiftCompilerFeature] = []
76+
for name in self.options.features {
77+
guard let feature = supportedFeatures[name] else {
78+
let migratableFeatures = supportedFeatures.map(\.value).filter(\.migratable).map(\.name)
79+
throw ValidationError(
80+
"Unsupported feature: \(name). Available features: \(migratableFeatures.joined(separator: ", "))"
81+
)
82+
}
83+
84+
guard feature.migratable else {
85+
throw ValidationError("Feature '\(name)' is not migratable")
86+
}
87+
88+
features.append(feature)
89+
}
90+
91+
let buildSystem = try await createBuildSystem(
92+
swiftCommandState,
93+
features: features
94+
)
95+
96+
// Next, let's build all of the individual targets or the
97+
// whole project to get diagnostic files.
98+
99+
print("> Starting the build.")
100+
if let targets = self.options.targets {
101+
for target in targets {
102+
try await buildSystem.build(subset: .target(target))
103+
}
104+
} else {
105+
try await buildSystem.build(subset: .allIncludingTests)
106+
}
107+
108+
// Determine all of the targets we need up update.
109+
let buildPlan = try buildSystem.buildPlan
110+
111+
var modules: [any ModuleBuildDescription] = []
112+
if let targets = self.options.targets {
113+
for buildDescription in buildPlan.buildModules where targets.contains(buildDescription.module.name) {
114+
modules.append(buildDescription)
115+
}
116+
} else {
117+
let graph = try await buildSystem.getPackageGraph()
118+
for buildDescription in buildPlan.buildModules
119+
where graph.isRootPackage(buildDescription.package) && buildDescription.module.type != .plugin
120+
{
121+
modules.append(buildDescription)
122+
}
123+
}
124+
125+
// If the build suceeded, let's extract all of the diagnostic
126+
// files from build plan and feed them to the fix-it tool.
127+
128+
print("> Applying fix-its.")
129+
for module in modules {
130+
let fixit = try SwiftFixIt(
131+
diagnosticFiles: module.diagnosticFiles,
132+
fileSystem: swiftCommandState.fileSystem
133+
)
134+
try fixit.applyFixIts()
135+
}
136+
137+
// Once the fix-its were applied, it's time to update the
138+
// manifest with newly adopted feature settings.
139+
140+
print("> Updating manifest.")
141+
for module in modules.map(\.module) {
142+
print("> Adding feature(s) to '\(module.name)'.")
143+
for feature in features {
144+
self.updateManifest(
145+
for: module.name,
146+
add: feature,
147+
using: swiftCommandState
148+
)
149+
}
150+
}
151+
}
152+
153+
private func createBuildSystem(
154+
_ swiftCommandState: SwiftCommandState,
155+
features: [SwiftCompilerFeature]
156+
) async throws -> BuildSystem {
157+
let toolsBuildParameters = try swiftCommandState.toolsBuildParameters
158+
var destinationBuildParameters = try swiftCommandState.productsBuildParameters
159+
160+
// Inject feature settings as flags. This is safe and not as invasive
161+
// as trying to update manifest because in adoption mode the features
162+
// can only produce warnings.
163+
for feature in features {
164+
destinationBuildParameters.flags.swiftCompilerFlags.append(contentsOf: [
165+
"-Xfrontend",
166+
"-enable-\(feature.upcoming ? "upcoming" : "experimental")-feature",
167+
"-Xfrontend",
168+
"\(feature.name):migrate",
169+
])
170+
}
171+
172+
return try await swiftCommandState.createBuildSystem(
173+
traitConfiguration: .init(),
174+
productsBuildParameters: destinationBuildParameters,
175+
toolsBuildParameters: toolsBuildParameters,
176+
// command result output goes on stdout
177+
// ie "swift build" should output to stdout
178+
outputStream: TSCBasic.stdoutStream
179+
)
180+
}
181+
182+
private func updateManifest(
183+
for target: String,
184+
add feature: SwiftCompilerFeature,
185+
using swiftCommandState: SwiftCommandState
186+
) {
187+
typealias SwiftSetting = SwiftPackageCommand.AddSetting.SwiftSetting
188+
189+
let setting: (SwiftSetting, String) = switch feature {
190+
case .upcoming(name: let name, migratable: _, enabledIn: _):
191+
(.upcomingFeature, "\(name)")
192+
case .experimental(name: let name, migratable: _):
193+
(.experimentalFeature, "\(name)")
194+
}
195+
196+
do {
197+
try SwiftPackageCommand.AddSetting.editSwiftSettings(
198+
of: target,
199+
using: swiftCommandState,
200+
[setting]
201+
)
202+
} catch {
203+
print(
204+
"! Couldn't update manifest due to - \(error); Please add '.enable\(feature.upcoming ? "Upcoming" : "Experimental")Feature(\"\(feature.name)\")' to target '\(target)' settings manually."
205+
)
206+
}
207+
}
208+
209+
public init() {}
210+
}

Sources/PackageModel/Toolchain+SupportedFeatures.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ public enum SwiftCompilerFeature {
5353
}
5454

5555
extension Toolchain {
56+
public var supportesSupportedFeatures: Bool {
57+
guard let features = try? swiftCompilerSupportedFeatures else {
58+
return false
59+
}
60+
return !features.isEmpty
61+
}
62+
5663
public var swiftCompilerSupportedFeatures: [SwiftCompilerFeature] {
5764
get throws {
5865
let compilerOutput: String

Sources/_InternalTestSupport/SwiftPMProduct.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import enum TSCBasic.ProcessEnv
2323
/// helper method to execute them.
2424
public enum SwiftPM {
2525
case Build
26+
case Migrate
2627
case Package
2728
case Registry
2829
case Test
@@ -37,6 +38,8 @@ extension SwiftPM {
3738
switch self {
3839
case .Build:
3940
return "swift-build"
41+
case .Migrate:
42+
return "swift-migrate"
4043
case .Package:
4144
return "swift-package"
4245
case .Registry:

Sources/_InternalTestSupport/misc.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,28 @@ public func executeSwiftTest(
382382
return try await SwiftPM.Test.execute(args, packagePath: packagePath, env: env, throwIfCommandFails: throwIfCommandFails)
383383
}
384384

385+
@discardableResult
386+
public func executeSwiftMigrate(
387+
_ packagePath: AbsolutePath?,
388+
configuration: Configuration = .Debug,
389+
extraArgs: [String] = [],
390+
Xcc: [String] = [],
391+
Xld: [String] = [],
392+
Xswiftc: [String] = [],
393+
env: Environment? = nil,
394+
buildSystem: BuildSystemProvider.Kind = .native
395+
) async throws -> (stdout: String, stderr: String) {
396+
let args = swiftArgs(
397+
configuration: configuration,
398+
extraArgs: extraArgs,
399+
Xcc: Xcc,
400+
Xld: Xld,
401+
Xswiftc: Xswiftc,
402+
buildSystem: buildSystem
403+
)
404+
return try await SwiftPM.Migrate.execute(args, packagePath: packagePath, env: env)
405+
}
406+
385407
private func swiftArgs(
386408
configuration: Configuration,
387409
extraArgs: [String],

Sources/swift-migrate/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# This source file is part of the Swift open source project
2+
#
3+
# Copyright (c) 2025 Apple Inc. and the Swift project authors
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See http://swift.org/LICENSE.txt for license information
7+
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
8+
9+
add_executable(swift-migrate
10+
Entrypoint.swift)
11+
target_link_libraries(swift-build PRIVATE
12+
Commands)
13+
14+
target_compile_options(swift-migrate PRIVATE
15+
-parse-as-library)
16+
17+
install(TARGETS swift-migrate
18+
DESTINATION bin)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 Commands
14+
15+
@main
16+
struct Entrypoint {
17+
static func main() async {
18+
await SwiftMigrateCommand.main()
19+
}
20+
}

Sources/swift-package-manager/SwiftPM.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ struct SwiftPM {
4141
await SwiftPackageCommand.main()
4242
case "swift-build":
4343
await SwiftBuildCommand.main()
44+
case "swift-migrate":
45+
await SwiftMigrateCommand.main()
4446
case "swift-experimental-sdk":
4547
fputs("warning: `swift experimental-sdk` command is deprecated and will be removed in a future version of SwiftPM. Use `swift sdk` instead.\n", stderr)
4648
fallthrough

0 commit comments

Comments
 (0)