Skip to content

Commit 5d2bdf2

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 5d2bdf2

File tree

8 files changed

+622
-5
lines changed

8 files changed

+622
-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 PackageDescription
17+
import PackageModel
18+
import PackageModelSyntax
19+
import SwiftParser
20+
import SwiftSyntax
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.split("=")
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: 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)