Skip to content

[Commands] swift package migrate command #8613

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 8 commits into from
May 7, 2025
10 changes: 10 additions & 0 deletions Fixtures/SwiftMigrate/ExistentialAnyMigration/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// swift-tools-version:5.8

import PackageDescription

let package = Package(
name: "ExistentialAnyMigration",
targets: [
.target(name: "Diagnostics", path: "Sources", exclude: ["Fixed"]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
protocol P {
}

func test1(_: any P) {
}

func test2(_: (any P).Protocol) {
}

func test3() {
let _: [(any P)?] = []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
protocol P {
}

func test1(_: P) {
}

func test2(_: P.Protocol) {
}

func test3() {
let _: [P?] = []
}
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ let package = Package(
type: .dynamic,
targets: ["AppleProductTypes"]
),

.library(
name: "PackagePlugin",
type: .dynamic,
Expand Down Expand Up @@ -588,6 +588,7 @@ let package = Package(
"Workspace",
"XCBuildSupport",
"SwiftBuildSupport",
"SwiftFixIt",
] + swiftSyntaxDependencies(["SwiftIDEUtils"]),
exclude: ["CMakeLists.txt", "README.md"],
swiftSettings: swift6CompatibleExperimentalFeatures + [
Expand Down
8 changes: 8 additions & 0 deletions Sources/Build/BuildDescription/ModuleBuildDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ public enum ModuleBuildDescription: SPMBuildCore.ModuleBuildDescription {
}
}

public var diagnosticFiles: [AbsolutePath] {
switch self {
case .swift(let buildDescription):
buildDescription.diagnosticFiles
case .clang(_):
[]
}
}
/// Determines the arguments needed to run `swift-symbolgraph-extract` for
/// this module.
public func symbolGraphExtractArguments() throws -> [String] {
Expand Down
1 change: 1 addition & 0 deletions Sources/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ add_subdirectory(SourceKitLSPAPI)
add_subdirectory(SPMBuildCore)
add_subdirectory(SPMLLBuild)
add_subdirectory(SPMSQLite3)
add_subdirectory(SwiftFixIt)
add_subdirectory(swift-bootstrap)
add_subdirectory(swift-build)
add_subdirectory(swift-experimental-sdk)
Expand Down
2 changes: 2 additions & 0 deletions Sources/Commands/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ add_library(Commands
PackageCommands/Init.swift
PackageCommands/Install.swift
PackageCommands/Learn.swift
PackageCommands/Migrate.swift
PackageCommands/PluginCommand.swift
PackageCommands/ResetCommands.swift
PackageCommands/Resolve.swift
Expand Down Expand Up @@ -64,6 +65,7 @@ target_link_libraries(Commands PUBLIC
PackageGraph
PackageModelSyntax
SourceControl
SwiftFixIt
TSCBasic
TSCUtility
Workspace
Expand Down
42 changes: 33 additions & 9 deletions Sources/Commands/PackageCommands/AddSetting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,47 @@ extension SwiftPackageCommand {
}

func run(_ swiftCommandState: SwiftCommandState) throws {
if !self._swiftSettings.isEmpty {
try Self.editSwiftSettings(
of: self.target,
using: swiftCommandState,
self.swiftSettings,
verbose: !self.globalOptions.logging.quiet
)
}
}

package static func editSwiftSettings(
of target: String,
using swiftCommandState: SwiftCommandState,
_ settings: [(SwiftSetting, String)],
verbose: Bool = false
) throws {
let workspace = try swiftCommandState.getActiveWorkspace()
guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else {
throw StringError("unknown package")
}

try self.applyEdits(packagePath: packagePath, workspace: workspace)
try self.applyEdits(
packagePath: packagePath,
workspace: workspace,
target: target,
swiftSettings: settings
)
}

private func applyEdits(
private static func applyEdits(
packagePath: Basics.AbsolutePath,
workspace: Workspace
workspace: Workspace,
target: String,
swiftSettings: [(SwiftSetting, String)],
verbose: Bool = false
) throws {
// Load the manifest file
let fileSystem = workspace.fileSystem
let manifestPath = packagePath.appending(component: Manifest.filename)

for (setting, value) in try self.swiftSettings {
for (setting, value) in swiftSettings {
let manifestContents: ByteString
do {
manifestContents = try fileSystem.readFileContents(manifestPath)
Expand All @@ -105,13 +129,13 @@ extension SwiftPackageCommand {
switch setting {
case .experimentalFeature:
editResult = try AddSwiftSetting.experimentalFeature(
to: self.target,
to: target,
name: value,
manifest: manifestSyntax
)
case .upcomingFeature:
editResult = try AddSwiftSetting.upcomingFeature(
to: self.target,
to: target,
name: value,
manifest: manifestSyntax
)
Expand All @@ -121,7 +145,7 @@ extension SwiftPackageCommand {
}

editResult = try AddSwiftSetting.languageMode(
to: self.target,
to: target,
mode: mode,
manifest: manifestSyntax
)
Expand All @@ -131,7 +155,7 @@ extension SwiftPackageCommand {
}

editResult = try AddSwiftSetting.strictMemorySafety(
to: self.target,
to: target,
manifest: manifestSyntax
)
}
Expand All @@ -140,7 +164,7 @@ extension SwiftPackageCommand {
to: fileSystem,
manifest: manifestSyntax,
manifestPath: manifestPath,
verbose: !self.globalOptions.logging.quiet
verbose: verbose
)
}
}
Expand Down
208 changes: 208 additions & 0 deletions Sources/Commands/PackageCommands/Migrate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import ArgumentParser

import Basics

@_spi(SwiftPMInternal)
import CoreCommands

import Foundation

import PackageGraph
import PackageModel

import SPMBuildCore
import SwiftFixIt

import var TSCBasic.stdoutStream

struct MigrateOptions: ParsableArguments {
@Option(
name: .customLong("targets"),
help: "The targets to migrate to specified set of features."
)
var _targets: String?

var targets: Set<String>? {
self._targets.flatMap { Set($0.components(separatedBy: ",")) }
}

@Option(
name: .customLong("to-feature"),
parsing: .unconditionalSingleValue,
help: "The Swift language upcoming/experimental feature to migrate to."
)
var features: [String]
}

extension SwiftPackageCommand {
struct Migrate: AsyncSwiftCommand {
package static let configuration = CommandConfiguration(
abstract: "Migrate a package or its individual targets to use the given set of features."
)

@OptionGroup()
public var globalOptions: GlobalOptions

@OptionGroup()
var options: MigrateOptions

public func run(_ swiftCommandState: SwiftCommandState) async throws {
let toolchain = try swiftCommandState.productsBuildParameters.toolchain

let supportedFeatures = try Dictionary(
uniqueKeysWithValues: toolchain.swiftCompilerSupportedFeatures
.map { ($0.name, $0) }
)

// First, let's validate that all of the features are supported
// by the compiler and are migratable.

var features: [SwiftCompilerFeature] = []
for name in self.options.features {
guard let feature = supportedFeatures[name] else {
let migratableFeatures = supportedFeatures.map(\.value).filter(\.migratable).map(\.name)
throw ValidationError(
"Unsupported feature: \(name). Available features: \(migratableFeatures.joined(separator: ", "))"
)
}

guard feature.migratable else {
throw ValidationError("Feature '\(name)' is not migratable")
}

features.append(feature)
}

let buildSystem = try await createBuildSystem(
swiftCommandState,
features: features
)

// Next, let's build all of the individual targets or the
// whole project to get diagnostic files.

print("> Starting the build.")
if let targets = self.options.targets {
for target in targets {
try await buildSystem.build(subset: .target(target))
}
} else {
try await buildSystem.build(subset: .allIncludingTests)
}

// Determine all of the targets we need up update.
let buildPlan = try buildSystem.buildPlan

var modules: [any ModuleBuildDescription] = []
if let targets = self.options.targets {
for buildDescription in buildPlan.buildModules where targets.contains(buildDescription.module.name) {
modules.append(buildDescription)
}
} else {
let graph = try await buildSystem.getPackageGraph()
for buildDescription in buildPlan.buildModules
where graph.isRootPackage(buildDescription.package) && buildDescription.module.type != .plugin
{
modules.append(buildDescription)
}
}

// If the build suceeded, let's extract all of the diagnostic
// files from build plan and feed them to the fix-it tool.

print("> Applying fix-its.")
for module in modules {
let fixit = try SwiftFixIt(
diagnosticFiles: module.diagnosticFiles,
fileSystem: swiftCommandState.fileSystem
)
try fixit.applyFixIts()
}

// Once the fix-its were applied, it's time to update the
// manifest with newly adopted feature settings.

print("> Updating manifest.")
for module in modules.map(\.module) {
print("> Adding feature(s) to '\(module.name)'.")
for feature in features {
self.updateManifest(
for: module.name,
add: feature,
using: swiftCommandState
)
}
}
}

private func createBuildSystem(
_ swiftCommandState: SwiftCommandState,
features: [SwiftCompilerFeature]
) async throws -> BuildSystem {
let toolsBuildParameters = try swiftCommandState.toolsBuildParameters
var destinationBuildParameters = try swiftCommandState.productsBuildParameters

// Inject feature settings as flags. This is safe and not as invasive
// as trying to update manifest because in adoption mode the features
// can only produce warnings.
for feature in features {
destinationBuildParameters.flags.swiftCompilerFlags.append(contentsOf: [
"-Xfrontend",
"-enable-\(feature.upcoming ? "upcoming" : "experimental")-feature",
"-Xfrontend",
"\(feature.name):migrate",
])
}

return try await swiftCommandState.createBuildSystem(
traitConfiguration: .init(),
productsBuildParameters: destinationBuildParameters,
toolsBuildParameters: toolsBuildParameters,
// command result output goes on stdout
// ie "swift build" should output to stdout
outputStream: TSCBasic.stdoutStream
)
}

private func updateManifest(
for target: String,
add feature: SwiftCompilerFeature,
using swiftCommandState: SwiftCommandState
) {
typealias SwiftSetting = SwiftPackageCommand.AddSetting.SwiftSetting

let setting: (SwiftSetting, String) = switch feature {
case .upcoming(name: let name, migratable: _, enabledIn: _):
(.upcomingFeature, "\(name)")
case .experimental(name: let name, migratable: _):
(.experimentalFeature, "\(name)")
}

do {
try SwiftPackageCommand.AddSetting.editSwiftSettings(
of: target,
using: swiftCommandState,
[setting]
)
} catch {
print(
"! Couldn't update manifest due to - \(error); Please add '.enable\(feature.upcoming ? "Upcoming" : "Experimental")Feature(\"\(feature.name)\")' to target '\(target)' settings manually."
)
}
}

public init() {}
}
}
Loading