Skip to content

Commit 9f3ba67

Browse files
committed
[Commands] Make migrate a sub-command of swift package
1 parent 53681e0 commit 9f3ba67

File tree

12 files changed

+261
-355
lines changed

12 files changed

+261
-355
lines changed

Package.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -746,12 +746,6 @@ let package = Package(
746746
"Workspace",
747747
]
748748
),
749-
.executableTarget(
750-
/** Builds packages */
751-
name: "swift-migrate",
752-
dependencies: ["Commands"],
753-
exclude: ["CMakeLists.txt"]
754-
),
755749

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

Sources/Commands/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
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
@@ -40,7 +41,6 @@ add_library(Commands
4041
Snippets/Card.swift
4142
Snippets/Colorful.swift
4243
SwiftBuildCommand.swift
43-
SwiftMigrateCommand.swift
4444
SwiftRunCommand.swift
4545
SwiftTestCommand.swift
4646
CommandWorkspaceDelegate.swift
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+
}

Sources/Commands/PackageCommands/SwiftPackageCommand.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand {
4545
Describe.self,
4646
Init.self,
4747
Format.self,
48+
Migrate.self,
4849

4950
Install.self,
5051
Uninstall.self,

0 commit comments

Comments
 (0)