Skip to content

Commit 2a109f8

Browse files
committed
[Commands] Initial implementation of swift package add-setting command
Add 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.
1 parent 95ce2a3 commit 2a109f8

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

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: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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+
///
25+
/// TODO: Could we generate this from the the PackageDescription module, so
26+
/// we don't have keep it up-to-date manually?
27+
private static let argumentLabelsAfterSwiftSettings: Set<String> = [
28+
"linkerSettings",
29+
"plugins",
30+
]
31+
32+
public static func upcomingFeature(
33+
to target: String,
34+
name: String,
35+
manifest: SourceFileSyntax
36+
) throws -> PackageEditResult {
37+
try self.addToTarget(
38+
target,
39+
name: "enableUpcomingFeature",
40+
value: name,
41+
firstIntroduced: .v5_8,
42+
manifest: manifest
43+
)
44+
}
45+
46+
public static func experimentalFeature(
47+
to target: String,
48+
name: String,
49+
manifest: SourceFileSyntax
50+
) throws -> PackageEditResult {
51+
try self.addToTarget(
52+
target,
53+
name: "enableExperimentalFeature",
54+
value: name,
55+
firstIntroduced: .v5_8,
56+
manifest: manifest
57+
)
58+
}
59+
60+
public static func languageMode(
61+
to target: String,
62+
mode: SwiftLanguageVersion,
63+
manifest: SourceFileSyntax
64+
) throws -> PackageEditResult {
65+
try self.addToTarget(
66+
target,
67+
name: "swiftLanguageMode",
68+
value: mode,
69+
firstIntroduced: .v6_0,
70+
manifest: manifest
71+
)
72+
}
73+
74+
public static func strictMemorySafety(
75+
to target: String,
76+
manifest: SourceFileSyntax
77+
) throws -> PackageEditResult {
78+
try self.addToTarget(
79+
target, name: "strictMemorySafety",
80+
value: String?.none,
81+
firstIntroduced: .v6_2,
82+
manifest: manifest
83+
)
84+
}
85+
86+
private static func addToTarget(
87+
_ target: String,
88+
name: String,
89+
value: (some ManifestSyntaxRepresentable)?,
90+
firstIntroduced: ToolsVersion,
91+
manifest: SourceFileSyntax
92+
) throws -> PackageEditResult {
93+
try manifest.checkManifestAtLeast(firstIntroduced)
94+
95+
guard let packageCall = manifest.findCall(calleeName: "Package") else {
96+
throw ManifestEditError.cannotFindPackage
97+
}
98+
99+
guard let targetsArgument = packageCall.findArgument(labeled: "targets"),
100+
let targetArray = targetsArgument.expression.findArrayArgument()
101+
else {
102+
throw ManifestEditError.cannotFindTargets
103+
}
104+
105+
guard let targetCall = FunctionCallExprSyntax.findFirst(in: targetArray, matching: {
106+
if let nameArgument = $0.findArgument(labeled: "name"),
107+
let nameLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self),
108+
nameLiteral.representedLiteralValue == target
109+
{
110+
return true
111+
}
112+
return false
113+
}) else {
114+
throw ManifestEditError.cannotFindTarget(targetName: target)
115+
}
116+
117+
if let memberRef = targetCall.calledExpression.as(MemberAccessExprSyntax.self),
118+
memberRef.declName.baseName.text == "plugin"
119+
{
120+
throw ManifestEditError.cannotAddSettingsToPluginTarget
121+
}
122+
123+
let newTargetCall = if let value {
124+
try targetCall.appendingToArrayArgument(
125+
label: "swiftSettings",
126+
trailingLabels: self.argumentLabelsAfterSwiftSettings,
127+
newElement: ".\(raw: name)(\(value.asSyntax()))"
128+
)
129+
} else {
130+
try targetCall.appendingToArrayArgument(
131+
label: "swiftSettings",
132+
trailingLabels: self.argumentLabelsAfterSwiftSettings,
133+
newElement: ".\(raw: name)"
134+
)
135+
}
136+
137+
return PackageEditResult(
138+
manifestEdits: [
139+
.replace(targetCall, with: newTargetCall.description),
140+
]
141+
)
142+
}
143+
}
144+
145+
extension SwiftLanguageVersion: ManifestSyntaxRepresentable {
146+
func asSyntax() -> ExprSyntax {
147+
if !Self.supportedSwiftLanguageVersions.contains(self) {
148+
return ".version(\"\(raw: rawValue)\")"
149+
}
150+
151+
if minor == 0 {
152+
return ".v\(raw: major)"
153+
}
154+
155+
return ".v\(raw: major)_\(raw: minor)"
156+
}
157+
}

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)