Skip to content

Commit eccff82

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 6b256f3 commit eccff82

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)