Skip to content

Commit bc074bd

Browse files
authored
[Commands] Initial implementation of swift package add-setting command (#8532)
### Motivation: Adds a way to programmatically insert new settings into a package manifest. Currently only some Swift settings are supported, namely: `enable{Upcoming, Experimental}Feature`, `swiftLanguageMode` and `strictMemorySafety`; but the command could be expanded to support more Swift (C, C++, linker) settings in the future. ### Modifications: - Add a new "package" sub-command named "add-setting" that accepts a target and a list of `--swift Name[=Value]?` pairs for each new swift setting to add. Each setting would make sure that manifest is new enough to support it. - Add new manifest refactoring action - AddSwiftSetting that would add `swiftSettings:` to a target or modify the existing one. - Expands existing way to check whether manifest is too old to support custom versions. This is doe to make sure that users don't add settings that are not supported by the manifest tools version i.e. `swiftLanguageMode` was introduced in 6.0 tools. ### Result: A new `swift package add-setting --target <name> [--swift Name[=Value]?]+` command that allows users to programmatically add new settings to package manifests.
1 parent b021cef commit bc074bd

File tree

8 files changed

+619
-5
lines changed

8 files changed

+619
-5
lines changed

Sources/Commands/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ add_library(Commands
1111
PackageCommands/AddProduct.swift
1212
PackageCommands/AddTarget.swift
1313
PackageCommands/AddTargetDependency.swift
14+
PackageCommands/AddSetting.swift
1415
PackageCommands/APIDiff.swift
1516
PackageCommands/ArchiveSource.swift
1617
PackageCommands/CompletionCommand.swift
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
import Basics
15+
import CoreCommands
16+
import Foundation
17+
import PackageGraph
18+
import PackageModel
19+
import PackageModelSyntax
20+
import SwiftParser
21+
import TSCBasic
22+
import TSCUtility
23+
import Workspace
24+
25+
extension SwiftPackageCommand {
26+
struct AddSetting: SwiftCommand {
27+
/// The Swift language setting that can be specified on the command line.
28+
enum SwiftSetting: String, Codable, ExpressibleByArgument, CaseIterable {
29+
case experimentalFeature
30+
case upcomingFeature
31+
case languageMode
32+
case strictMemorySafety
33+
}
34+
35+
package static let configuration = CommandConfiguration(
36+
abstract: "Add a new setting to the manifest"
37+
)
38+
39+
@OptionGroup(visibility: .hidden)
40+
var globalOptions: GlobalOptions
41+
42+
@Option(help: "The target to add the setting to")
43+
var target: String
44+
45+
@Option(
46+
name: .customLong("swift"),
47+
parsing: .unconditionalSingleValue,
48+
help: "The Swift language setting(s) to add. Supported settings: \(SwiftSetting.allCases.map(\.rawValue).joined(separator: ", "))"
49+
)
50+
var _swiftSettings: [String]
51+
52+
var swiftSettings: [(SwiftSetting, String)] {
53+
get throws {
54+
var settings: [(SwiftSetting, String)] = []
55+
for rawSetting in self._swiftSettings {
56+
let (name, value) = rawSetting.spm_split(around: "=")
57+
58+
guard let setting = SwiftSetting(rawValue: name) else {
59+
throw ValidationError("Unknown Swift language setting: \(name)")
60+
}
61+
62+
settings.append((setting, value ?? ""))
63+
}
64+
65+
return settings
66+
}
67+
}
68+
69+
func run(_ swiftCommandState: SwiftCommandState) throws {
70+
let workspace = try swiftCommandState.getActiveWorkspace()
71+
guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else {
72+
throw StringError("unknown package")
73+
}
74+
75+
try self.applyEdits(packagePath: packagePath, workspace: workspace)
76+
}
77+
78+
private func applyEdits(
79+
packagePath: Basics.AbsolutePath,
80+
workspace: Workspace
81+
) throws {
82+
// Load the manifest file
83+
let fileSystem = workspace.fileSystem
84+
let manifestPath = packagePath.appending(component: Manifest.filename)
85+
86+
for (setting, value) in try self.swiftSettings {
87+
let manifestContents: ByteString
88+
do {
89+
manifestContents = try fileSystem.readFileContents(manifestPath)
90+
} catch {
91+
throw StringError("cannot find package manifest in \(manifestPath)")
92+
}
93+
94+
// Parse the manifest.
95+
let manifestSyntax = manifestContents.withData { data in
96+
data.withUnsafeBytes { buffer in
97+
buffer.withMemoryRebound(to: UInt8.self) { buffer in
98+
Parser.parse(source: buffer)
99+
}
100+
}
101+
}
102+
103+
let editResult: PackageEditResult
104+
105+
switch setting {
106+
case .experimentalFeature:
107+
editResult = try AddSwiftSetting.experimentalFeature(
108+
to: self.target,
109+
name: value,
110+
manifest: manifestSyntax
111+
)
112+
case .upcomingFeature:
113+
editResult = try AddSwiftSetting.upcomingFeature(
114+
to: self.target,
115+
name: value,
116+
manifest: manifestSyntax
117+
)
118+
case .languageMode:
119+
guard let mode = SwiftLanguageVersion(string: value) else {
120+
throw ValidationError("Unknown Swift language mode: \(value)")
121+
}
122+
123+
editResult = try AddSwiftSetting.languageMode(
124+
to: self.target,
125+
mode: mode,
126+
manifest: manifestSyntax
127+
)
128+
case .strictMemorySafety:
129+
guard value.isEmpty else {
130+
throw ValidationError("'strictMemorySafety' doesn't have an argument")
131+
}
132+
133+
editResult = try AddSwiftSetting.strictMemorySafety(
134+
to: self.target,
135+
manifest: manifestSyntax
136+
)
137+
}
138+
139+
try editResult.applyEdits(
140+
to: fileSystem,
141+
manifest: manifestSyntax,
142+
manifestPath: manifestPath,
143+
verbose: !self.globalOptions.logging.quiet
144+
)
145+
}
146+
}
147+
}
148+
}

Sources/Commands/PackageCommands/SwiftPackageCommand.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand {
3737
AddProduct.self,
3838
AddTarget.self,
3939
AddTargetDependency.self,
40+
AddSetting.self,
4041
Clean.self,
4142
PurgeCache.self,
4243
Reset.self,
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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 Basics
14+
import PackageModel
15+
import SwiftParser
16+
import SwiftSyntax
17+
import SwiftSyntaxBuilder
18+
import struct TSCUtility.Version
19+
20+
/// Add a swift setting to a manifest's source code.
21+
public enum AddSwiftSetting {
22+
/// The set of argument labels that can occur after the "targets"
23+
/// argument in the Package initializers.
24+
private static let argumentLabelsAfterSwiftSettings: Set<String> = [
25+
"linkerSettings",
26+
"plugins",
27+
]
28+
29+
public static func upcomingFeature(
30+
to target: String,
31+
name: String,
32+
manifest: SourceFileSyntax
33+
) throws -> PackageEditResult {
34+
try self.addToTarget(
35+
target,
36+
name: "enableUpcomingFeature",
37+
value: name,
38+
firstIntroduced: .v5_8,
39+
manifest: manifest
40+
)
41+
}
42+
43+
public static func experimentalFeature(
44+
to target: String,
45+
name: String,
46+
manifest: SourceFileSyntax
47+
) throws -> PackageEditResult {
48+
try self.addToTarget(
49+
target,
50+
name: "enableExperimentalFeature",
51+
value: name,
52+
firstIntroduced: .v5_8,
53+
manifest: manifest
54+
)
55+
}
56+
57+
public static func languageMode(
58+
to target: String,
59+
mode: SwiftLanguageVersion,
60+
manifest: SourceFileSyntax
61+
) throws -> PackageEditResult {
62+
try self.addToTarget(
63+
target,
64+
name: "swiftLanguageMode",
65+
value: mode,
66+
firstIntroduced: .v6_0,
67+
manifest: manifest
68+
)
69+
}
70+
71+
public static func strictMemorySafety(
72+
to target: String,
73+
manifest: SourceFileSyntax
74+
) throws -> PackageEditResult {
75+
try self.addToTarget(
76+
target, name: "strictMemorySafety",
77+
value: String?.none,
78+
firstIntroduced: .v6_2,
79+
manifest: manifest
80+
)
81+
}
82+
83+
private static func addToTarget(
84+
_ target: String,
85+
name: String,
86+
value: (some ManifestSyntaxRepresentable)?,
87+
firstIntroduced: ToolsVersion,
88+
manifest: SourceFileSyntax
89+
) throws -> PackageEditResult {
90+
try manifest.checkManifestAtLeast(firstIntroduced)
91+
92+
guard let packageCall = manifest.findCall(calleeName: "Package") else {
93+
throw ManifestEditError.cannotFindPackage
94+
}
95+
96+
guard let targetsArgument = packageCall.findArgument(labeled: "targets"),
97+
let targetArray = targetsArgument.expression.findArrayArgument()
98+
else {
99+
throw ManifestEditError.cannotFindTargets
100+
}
101+
102+
guard let targetCall = FunctionCallExprSyntax.findFirst(in: targetArray, matching: {
103+
if let nameArgument = $0.findArgument(labeled: "name"),
104+
let nameLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self),
105+
nameLiteral.representedLiteralValue == target
106+
{
107+
return true
108+
}
109+
return false
110+
}) else {
111+
throw ManifestEditError.cannotFindTarget(targetName: target)
112+
}
113+
114+
if let memberRef = targetCall.calledExpression.as(MemberAccessExprSyntax.self),
115+
memberRef.declName.baseName.text == "plugin"
116+
{
117+
throw ManifestEditError.cannotAddSettingsToPluginTarget
118+
}
119+
120+
let newTargetCall = if let value {
121+
try targetCall.appendingToArrayArgument(
122+
label: "swiftSettings",
123+
trailingLabels: self.argumentLabelsAfterSwiftSettings,
124+
newElement: ".\(raw: name)(\(value.asSyntax()))"
125+
)
126+
} else {
127+
try targetCall.appendingToArrayArgument(
128+
label: "swiftSettings",
129+
trailingLabels: self.argumentLabelsAfterSwiftSettings,
130+
newElement: ".\(raw: name)"
131+
)
132+
}
133+
134+
return PackageEditResult(
135+
manifestEdits: [
136+
.replace(targetCall, with: newTargetCall.description),
137+
]
138+
)
139+
}
140+
}
141+
142+
extension SwiftLanguageVersion: ManifestSyntaxRepresentable {
143+
func asSyntax() -> ExprSyntax {
144+
if !Self.supportedSwiftLanguageVersions.contains(self) {
145+
return ".version(\"\(raw: rawValue)\")"
146+
}
147+
148+
if minor == 0 {
149+
return ".v\(raw: major)"
150+
}
151+
152+
return ".v\(raw: major)_\(raw: minor)"
153+
}
154+
}

Sources/PackageModelSyntax/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
add_library(PackageModelSyntax
1010
AddPackageDependency.swift
1111
AddProduct.swift
12+
AddSwiftSetting.swift
1213
AddTarget.swift
1314
AddTargetDependency.swift
1415
ManifestEditError.swift

Sources/PackageModelSyntax/ManifestEditError.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ package enum ManifestEditError: Error {
2121
case cannotFindTargets
2222
case cannotFindTarget(targetName: String)
2323
case cannotFindArrayLiteralArgument(argumentName: String, node: Syntax)
24-
case oldManifest(ToolsVersion)
24+
case oldManifest(ToolsVersion, expected: ToolsVersion)
25+
case cannotAddSettingsToPluginTarget
2526
}
2627

2728
extension ToolsVersion {
@@ -41,8 +42,10 @@ extension ManifestEditError: CustomStringConvertible {
4142
"unable to find target named '\(name)' in package"
4243
case .cannotFindArrayLiteralArgument(argumentName: let name, node: _):
4344
"unable to find array literal for '\(name)' argument"
44-
case .oldManifest(let version):
45-
"package manifest version \(version) is too old: please update to manifest version \(ToolsVersion.minimumManifestEditVersion) or newer"
45+
case .oldManifest(let version, let expectedVersion):
46+
"package manifest version \(version) is too old: please update to manifest version \(expectedVersion) or newer"
47+
case .cannotAddSettingsToPluginTarget:
48+
"plugin targets do not support settings"
4649
}
4750
}
4851
}
@@ -53,7 +56,14 @@ extension SourceFileSyntax {
5356
func checkEditManifestToolsVersion() throws {
5457
let toolsVersion = try ToolsVersionParser.parse(utf8String: description)
5558
if toolsVersion < ToolsVersion.minimumManifestEditVersion {
56-
throw ManifestEditError.oldManifest(toolsVersion)
59+
throw ManifestEditError.oldManifest(toolsVersion, expected: ToolsVersion.minimumManifestEditVersion)
60+
}
61+
}
62+
63+
func checkManifestAtLeast(_ version: ToolsVersion) throws {
64+
let toolsVersion = try ToolsVersionParser.parse(utf8String: description)
65+
if toolsVersion < version {
66+
throw ManifestEditError.oldManifest(toolsVersion, expected: version)
5767
}
5868
}
5969
}

0 commit comments

Comments
 (0)