Skip to content

Commit 0f5bf84

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 0f5bf84

File tree

13 files changed

+395
-1
lines changed

13 files changed

+395
-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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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)?] = []
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
protocol P {}
2+
3+
func test1(_: P) {}
4+
5+
func test2(_: P.Protocol) {}
6+
7+
func test3() {
8+
let _: [P?] = []
9+
}

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

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)