├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Package.swift
├── README.md
├── Sources
├── PackageSyntax
│ ├── Formatting.swift
│ ├── ManifestRewriter.swift
│ └── PackageEditor.swift
└── swift-package-editor
│ └── SwiftPackageEditorTool.swift
├── Tests
├── IntegrationTests
│ ├── Fixtures
│ │ ├── Empty
│ │ │ ├── LocalBinary.xcframework
│ │ │ │ └── contents
│ │ │ └── Package.swift
│ │ └── OneProduct
│ │ │ ├── Package.swift
│ │ │ └── Sources
│ │ │ └── Library
│ │ │ └── Library.swift
│ └── IntegrationTests.swift
└── PackageSyntaxTests
│ ├── AddPackageDependencyTests.swift
│ ├── AddProductTests.swift
│ ├── AddTargetDependencyTests.swift
│ ├── AddTargetTests.swift
│ ├── ArrayFormattingTests.swift
│ ├── InMemoryGitRepository.swift
│ ├── PackageEditorTests.swift
│ ├── Resources.swift
│ └── TestSupport.swift
└── Utilities
└── build-script-helper.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm
8 | Package.resolved
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 |
3 | /*
4 | This source file is part of the Swift.org open source project
5 |
6 | Copyright (c) 2021 Apple Inc. and the Swift project authors
7 | Licensed under Apache License v2.0 with Runtime Library Exception
8 |
9 | See http://swift.org/LICENSE.txt for license information
10 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
11 | */
12 |
13 | import PackageDescription
14 | import Foundation
15 |
16 | let package = Package(
17 | name: "swift-package-editor",
18 | platforms: [
19 | .macOS(.v10_15),
20 | .iOS(.v13)
21 | ],
22 | products: [
23 | .executable(
24 | name: "swift-package-editor",
25 | targets: ["swift-package-editor"])
26 | ],
27 | targets: [
28 | .executableTarget(name: "swift-package-editor", dependencies: [
29 | "PackageSyntax",
30 | .product(name: "ArgumentParser", package: "swift-argument-parser"),
31 | .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
32 | .product(name: "SwiftPM-auto", package: "swift-package-manager"),
33 | ]),
34 | .target(name: "PackageSyntax", dependencies: [
35 | .product(name: "SwiftSyntax", package: "swift-syntax"),
36 | .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
37 | .product(name: "SwiftPM-auto", package: "swift-package-manager")
38 | ]),
39 | .testTarget(name: "PackageSyntaxTests", dependencies: [
40 | "PackageSyntax",
41 | .product(name: "SwiftSyntax", package: "swift-syntax"),
42 | .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
43 | .product(name: "SwiftPM-auto", package: "swift-package-manager"),
44 | .product(name: "PackageDescription", package: "swift-package-manager")
45 | ]),
46 | .testTarget(name: "IntegrationTests", dependencies: [
47 | "swift-package-editor",
48 | .product(name: "TSCTestSupport", package: "swift-tools-support-core")
49 | ], resources: [.copy("Fixtures/")])
50 | ]
51 | )
52 |
53 | let relatedDependenciesBranch = "main"
54 |
55 | if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
56 | package.dependencies += [
57 | .package(url: "https://github.com/apple/swift-tools-support-core.git", .branch(relatedDependenciesBranch)),
58 | // The 'swift-argument-parser' version declared here must match that
59 | // used by 'swift-driver', 'sourcekit-lsp', and 'swiftpm'. Please coordinate
60 | // dependency version changes here with those projects.
61 | .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.4.3")),
62 | .package(url: "https://github.com/apple/swift-package-manager.git", .branch(relatedDependenciesBranch)),
63 | .package(url: "https://github.com/apple/swift-syntax.git", .branch(relatedDependenciesBranch))
64 | ]
65 | } else {
66 | package.dependencies += [
67 | .package(path: "../swift-tools-support-core"),
68 | .package(path: "../swift-argument-parser"),
69 | .package(name: "swift-package-manager", path: "../swiftpm"),
70 | .package(path: "../swift-syntax")
71 | ]
72 | }
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # swift-package-editor
2 |
3 | Mechanical editing support for `Package.swift` manifests. Implements Swift Evolution proposal [SE-301](https://github.com/apple/swift-evolution/blob/main/proposals/0301-package-editing-commands.md)
4 |
5 | ## Usage
6 |
7 | - Adding dependencies: `swift-package-editor add-dependency https://github.com/apple/swift-nio.git --from 2.0.0`
8 | - Adding targets: `swift-package-editor add-target Foo --type executable --dependencies Bar NIO`
9 | - Adding products: `swift-package-editor add-product MyLibrary --dependencies Foo`
10 |
11 | See `swift-package-editor --help` for more information.
12 |
13 | ## Building
14 |
15 | Currently, `swift-package-editor` can only be built with the SwiftPM CLI. Building the package with Xcode will succeed, but fail at runtime due to linker issues.
16 |
17 | Because `swift-package-editor` depends on `swift-syntax` to edit `Package.swift` files, it must also be built using a toolchain which closely matches the resolved version of that package. Because `swift-syntax` is integrated using a branch dependency on `main`, usually this is the most recent Swift nightly snapshot. If `SWIFTCI_USE_LOCAL_DEPS` is set, a checkout of `swift-syntax` next to `swift-package-editor` will be used instead. This is intended for use in a build-script build of the Swift toolchain.
18 |
19 | ## Installing
20 |
21 | Run `./Utilities/build-script-helper.py install -h` for details.
22 |
--------------------------------------------------------------------------------
/Sources/PackageSyntax/Formatting.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import SwiftSyntax
12 |
13 | extension ArrayExprSyntax {
14 | public func withAdditionalElementExpr(_ expr: ExprSyntax) -> ArrayExprSyntax {
15 | if self.elements.count >= 2 {
16 | // If the array expression has >=2 elements, use the trivia between
17 | // the last and second-to-last elements to determine how we insert
18 | // the new one.
19 | let lastElement = self.elements.last!
20 | let secondToLastElement = self.elements[self.elements.index(self.elements.endIndex, offsetBy: -2)]
21 |
22 | let newElements = self.elements
23 | .removingLast()
24 | .appending(
25 | lastElement.withTrailingComma(
26 | SyntaxFactory.makeCommaToken(
27 | trailingTrivia: (lastElement.trailingTrivia ?? []) +
28 | rightSquare.leadingTrivia.droppingPiecesAfterLastComment() +
29 | (secondToLastElement.trailingTrivia ?? [])
30 | )
31 | )
32 | )
33 | .appending(
34 | SyntaxFactory.makeArrayElement(
35 | expression: expr,
36 | trailingComma: SyntaxFactory.makeCommaToken()
37 | ).withLeadingTrivia(lastElement.leadingTrivia?.droppingPiecesUpToAndIncludingLastComment() ?? [])
38 | )
39 |
40 | return self.withElements(newElements)
41 | .withRightSquare(
42 | self.rightSquare.withLeadingTrivia(
43 | self.rightSquare.leadingTrivia.droppingPiecesUpToAndIncludingLastComment()
44 | )
45 | )
46 | } else {
47 | // For empty and single-element array exprs, we determine the indent
48 | // of the line the opening square bracket appears on, and then use
49 | // that to indent the added element and closing brace onto newlines.
50 | let (indentTrivia, unitIndent) = self.leftSquare.determineIndentOfStartingLine()
51 | var newElements: [ArrayElementSyntax] = []
52 | if !self.elements.isEmpty {
53 | let existingElement = self.elements.first!
54 | newElements.append(
55 | SyntaxFactory.makeArrayElement(expression: existingElement.expression,
56 | trailingComma: SyntaxFactory.makeCommaToken())
57 | .withLeadingTrivia(indentTrivia + unitIndent)
58 | .withTrailingTrivia((existingElement.trailingTrivia ?? []) + .newlines(1))
59 | )
60 | }
61 |
62 | newElements.append(
63 | SyntaxFactory.makeArrayElement(expression: expr, trailingComma: SyntaxFactory.makeCommaToken())
64 | .withLeadingTrivia(indentTrivia + unitIndent)
65 | )
66 |
67 | return self.withLeftSquare(self.leftSquare.withTrailingTrivia(.newlines(1)))
68 | .withElements(SyntaxFactory.makeArrayElementList(newElements))
69 | .withRightSquare(self.rightSquare.withLeadingTrivia(.newlines(1) + indentTrivia))
70 | }
71 | }
72 | }
73 |
74 | extension ArrayExprSyntax {
75 | func reindentingLastCallExprElement() -> ArrayExprSyntax {
76 | let lastElement = elements.last!
77 | let (indent, unitIndent) = lastElement.determineIndentOfStartingLine()
78 | let formattingVisitor = MultilineArgumentListRewriter(indent: indent, unitIndent: unitIndent)
79 | let formattedLastElement = formattingVisitor.visit(lastElement).as(ArrayElementSyntax.self)!
80 | return self.withElements(elements.replacing(childAt: elements.count - 1, with: formattedLastElement))
81 | }
82 | }
83 |
84 | fileprivate extension TriviaPiece {
85 | var isComment: Bool {
86 | switch self {
87 | case .spaces, .tabs, .verticalTabs, .formfeeds, .newlines,
88 | .carriageReturns, .carriageReturnLineFeeds, .garbageText:
89 | return false
90 | case .lineComment, .blockComment, .docLineComment, .docBlockComment:
91 | return true
92 | }
93 | }
94 |
95 | var isHorizontalWhitespace: Bool {
96 | switch self {
97 | case .spaces, .tabs:
98 | return true
99 | default:
100 | return false
101 | }
102 | }
103 |
104 | var isSpaces: Bool {
105 | guard case .spaces = self else { return false }
106 | return true
107 | }
108 |
109 | var isTabs: Bool {
110 | guard case .tabs = self else { return false }
111 | return true
112 | }
113 | }
114 |
115 | fileprivate extension Trivia {
116 | func droppingPiecesAfterLastComment() -> Trivia {
117 | Trivia(pieces: .init(self.lazy.reversed().drop(while: { !$0.isComment }).reversed()))
118 | }
119 |
120 | func droppingPiecesUpToAndIncludingLastComment() -> Trivia {
121 | Trivia(pieces: .init(self.lazy.reversed().prefix(while: { !$0.isComment }).reversed()))
122 | }
123 | }
124 |
125 | extension SyntaxProtocol {
126 | func determineIndentOfStartingLine() -> (indent: Trivia, unitIndent: Trivia) {
127 | let sourceLocationConverter = SourceLocationConverter(file: "", tree: self.root.as(SourceFileSyntax.self)!)
128 | let line = startLocation(converter: sourceLocationConverter).line ?? 0
129 | let visitor = DetermineLineIndentVisitor(lineNumber: line, sourceLocationConverter: sourceLocationConverter)
130 | visitor.walk(self.root)
131 | return (indent: visitor.lineIndent, unitIndent: visitor.lineUnitIndent)
132 | }
133 | }
134 |
135 | public final class DetermineLineIndentVisitor: SyntaxVisitor {
136 |
137 | let lineNumber: Int
138 | let locationConverter: SourceLocationConverter
139 | private var bestMatch: TokenSyntax?
140 |
141 | public var lineIndent: Trivia {
142 | guard let pieces = bestMatch?.leadingTrivia
143 | .lazy
144 | .reversed()
145 | .prefix(while: \.isHorizontalWhitespace)
146 | .reversed() else { return .spaces(4) }
147 | return Trivia(pieces: Array(pieces))
148 | }
149 |
150 | public var lineUnitIndent: Trivia {
151 | if lineIndent.allSatisfy(\.isSpaces) {
152 | let addedSpaces = lineIndent.reduce(0, {
153 | guard case .spaces(let count) = $1 else { fatalError() }
154 | return $0 + count
155 | }) % 4 == 0 ? 4 : 2
156 | return .spaces(addedSpaces)
157 | } else if lineIndent.allSatisfy(\.isTabs) {
158 | return .tabs(1)
159 | } else {
160 | // If we can't determine the indent, default to 4 spaces.
161 | return .spaces(4)
162 | }
163 | }
164 |
165 | public init(lineNumber: Int, sourceLocationConverter: SourceLocationConverter) {
166 | self.lineNumber = lineNumber
167 | self.locationConverter = sourceLocationConverter
168 | }
169 |
170 | public override func visit(_ tokenSyntax: TokenSyntax) -> SyntaxVisitorContinueKind {
171 | let range = tokenSyntax.sourceRange(converter: locationConverter,
172 | afterLeadingTrivia: false,
173 | afterTrailingTrivia: true)
174 | guard let startLine = range.start.line,
175 | let endLine = range.end.line,
176 | let startColumn = range.start.column,
177 | let endColumn = range.end.column else {
178 | return .skipChildren
179 | }
180 |
181 | if (startLine, startColumn) <= (lineNumber, 1),
182 | (lineNumber, 1) <= (endLine, endColumn) {
183 | bestMatch = tokenSyntax
184 | return .visitChildren
185 | } else {
186 | return .skipChildren
187 | }
188 | }
189 | }
190 |
191 | /// Moves each argument to a function call expression onto a new line and indents them appropriately.
192 | final class MultilineArgumentListRewriter: SyntaxRewriter {
193 | let indent: Trivia
194 | let unitIndent: Trivia
195 |
196 | init(indent: Trivia, unitIndent: Trivia) {
197 | self.indent = indent
198 | self.unitIndent = unitIndent
199 | }
200 |
201 | override func visit(_ token: TokenSyntax) -> Syntax {
202 | guard token.tokenKind == .rightParen else { return Syntax(token) }
203 | return Syntax(token.withLeadingTrivia(.newlines(1) + indent))
204 | }
205 |
206 | override func visit(_ node: TupleExprElementSyntax) -> Syntax {
207 | return Syntax(node.withLeadingTrivia(.newlines(1) + indent + unitIndent))
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/Sources/PackageSyntax/ManifestRewriter.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import SwiftSyntax
12 | import TSCBasic
13 | import TSCUtility
14 | import PackageModel
15 |
16 | /// A package manifest rewriter.
17 | ///
18 | /// This class provides functionality for rewriting the
19 | /// Swift package manifest using the SwiftSyntax library.
20 | ///
21 | /// Similar to SwiftSyntax, this class only deals with the
22 | /// syntax and has no functionality for semantics of the manifest.
23 | public final class ManifestRewriter {
24 |
25 | /// The contents of the original manifest.
26 | public let originalManifest: String
27 |
28 | /// The contents of the edited manifest.
29 | public var editedManifest: String {
30 | return editedSource.description
31 | }
32 |
33 | /// The edited manifest syntax.
34 | private var editedSource: SourceFileSyntax
35 |
36 | /// Engine used to report manifest rewrite failures.
37 | private let diagnosticsEngine: DiagnosticsEngine
38 |
39 | /// Create a new manfiest editor with the given contents.
40 | public init(_ manifest: String, diagnosticsEngine: DiagnosticsEngine) throws {
41 | self.originalManifest = manifest
42 | self.diagnosticsEngine = diagnosticsEngine
43 | self.editedSource = try SyntaxParser.parse(source: manifest)
44 | }
45 |
46 | /// Add a package dependency.
47 | public func addPackageDependency(
48 | name: String?,
49 | url: String,
50 | requirement: PackageDependencyRequirement,
51 | branchAndRevisionConvenienceMethodsSupported: Bool
52 | ) throws {
53 | let initFnExpr = try findPackageInit()
54 |
55 | // Find dependencies section in the argument list of Package(...).
56 | let packageDependenciesFinder = ArrayExprArgumentFinder(expectedLabel: "dependencies")
57 | packageDependenciesFinder.walk(initFnExpr.argumentList)
58 |
59 | let packageDependencies: ArrayExprSyntax
60 | switch packageDependenciesFinder.result {
61 | case .found(let existingPackageDependencies):
62 | packageDependencies = existingPackageDependencies
63 | case .missing:
64 | // We didn't find a dependencies section, so insert one.
65 | let argListWithDependencies = EmptyArrayArgumentWriter(argumentLabel: "dependencies",
66 | followingArgumentLabels:
67 | "targets",
68 | "swiftLanguageVersions",
69 | "cLanguageStandard",
70 | "cxxLanguageStandard")
71 | .visit(initFnExpr.argumentList)
72 |
73 | // Find the inserted section.
74 | let packageDependenciesFinder = ArrayExprArgumentFinder(expectedLabel: "dependencies")
75 | packageDependenciesFinder.walk(argListWithDependencies)
76 | guard case .found(let newPackageDependencies) = packageDependenciesFinder.result else {
77 | fatalError("Could not find just inserted dependencies array")
78 | }
79 | packageDependencies = newPackageDependencies
80 | case .incompatibleExpr:
81 | diagnosticsEngine.emit(.incompatibleArgument(name: "targets"))
82 | throw Diagnostics.fatalError
83 | }
84 |
85 | // Add the the package dependency entry.
86 | let newManifest = PackageDependencyWriter(
87 | name: name,
88 | url: url,
89 | requirement: requirement,
90 | branchAndRevisionConvenienceMethodsSupported: branchAndRevisionConvenienceMethodsSupported
91 | ).visit(packageDependencies).root
92 |
93 | self.editedSource = newManifest.as(SourceFileSyntax.self)!
94 | }
95 |
96 | /// Add a target dependency.
97 | public func addByNameTargetDependency(
98 | target: String,
99 | dependency: String
100 | ) throws {
101 | let targetDependencies = try findTargetDependenciesArrayExpr(target: target)
102 |
103 | // Add the target dependency entry.
104 | let newManifest = targetDependencies.withAdditionalElementExpr(ExprSyntax(
105 | SyntaxFactory.makeStringLiteralExpr(dependency)
106 | )).root
107 |
108 | self.editedSource = newManifest.as(SourceFileSyntax.self)!
109 | }
110 |
111 | public func addProductTargetDependency(
112 | target: String,
113 | product: String,
114 | package: String
115 | ) throws {
116 | let targetDependencies = try findTargetDependenciesArrayExpr(target: target)
117 |
118 | let dotProductExpr = SyntaxFactory.makeMemberAccessExpr(base: nil,
119 | dot: SyntaxFactory.makePeriodToken(),
120 | name: SyntaxFactory.makeIdentifier("product"),
121 | declNameArguments: nil)
122 |
123 | let argumentList = SyntaxFactory.makeTupleExprElementList([
124 | SyntaxFactory.makeTupleExprElement(label: SyntaxFactory.makeIdentifier("name"),
125 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
126 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(product)),
127 | trailingComma: SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1))),
128 | SyntaxFactory.makeTupleExprElement(label: SyntaxFactory.makeIdentifier("package"),
129 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
130 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(package)),
131 | trailingComma: nil)
132 | ])
133 |
134 | let callExpr = SyntaxFactory.makeFunctionCallExpr(calledExpression: ExprSyntax(dotProductExpr),
135 | leftParen: SyntaxFactory.makeLeftParenToken(),
136 | argumentList: argumentList,
137 | rightParen: SyntaxFactory.makeRightParenToken(),
138 | trailingClosure: nil,
139 | additionalTrailingClosures: nil)
140 |
141 | // Add the target dependency entry.
142 | let newManifest = targetDependencies.withAdditionalElementExpr(ExprSyntax(callExpr)).root
143 | self.editedSource = newManifest.as(SourceFileSyntax.self)!
144 | }
145 |
146 | private func findTargetDependenciesArrayExpr(target: String) throws -> ArrayExprSyntax {
147 | let initFnExpr = try findPackageInit()
148 |
149 | // Find the `targets: []` array.
150 | let targetsArrayFinder = ArrayExprArgumentFinder(expectedLabel: "targets")
151 | targetsArrayFinder.walk(initFnExpr.argumentList)
152 | guard case .found(let targetsArrayExpr) = targetsArrayFinder.result else {
153 | diagnosticsEngine.emit(.missingPackageInitArgument(name: "targets"))
154 | throw Diagnostics.fatalError
155 | }
156 |
157 | // Find the target node.
158 | let targetFinder = NamedEntityArgumentListFinder(name: target)
159 | targetFinder.walk(targetsArrayExpr)
160 | guard let targetNode = targetFinder.foundEntity else {
161 | diagnosticsEngine.emit(.missingTarget(name: target))
162 | throw Diagnostics.fatalError
163 | }
164 |
165 | let targetDependencyFinder = ArrayExprArgumentFinder(expectedLabel: "dependencies")
166 | targetDependencyFinder.walk(targetNode)
167 |
168 | guard case .found(let targetDependencies) = targetDependencyFinder.result else {
169 | diagnosticsEngine.emit(.missingArgument(name: "dependencies", parent: "target '\(target)'"))
170 | throw Diagnostics.fatalError
171 | }
172 | return targetDependencies
173 | }
174 |
175 | /// Add a new target.
176 | public func addTarget(
177 | targetName: String,
178 | factoryMethodName: String
179 | ) throws {
180 | let initFnExpr = try findPackageInit()
181 | let targetsNode = try findOrCreateTargetsList(in: initFnExpr)
182 |
183 | let dotTargetExpr = SyntaxFactory.makeMemberAccessExpr(
184 | base: nil,
185 | dot: SyntaxFactory.makePeriodToken(),
186 | name: SyntaxFactory.makeIdentifier(factoryMethodName),
187 | declNameArguments: nil
188 | )
189 |
190 | let nameArg = SyntaxFactory.makeTupleExprElement(
191 | label: SyntaxFactory.makeIdentifier("name"),
192 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
193 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(targetName)),
194 | trailingComma: SyntaxFactory.makeCommaToken()
195 | )
196 |
197 | let emptyArray = SyntaxFactory.makeArrayExpr(leftSquare: SyntaxFactory.makeLeftSquareBracketToken(), elements: SyntaxFactory.makeBlankArrayElementList(), rightSquare: SyntaxFactory.makeRightSquareBracketToken())
198 | let depenenciesArg = SyntaxFactory.makeTupleExprElement(
199 | label: SyntaxFactory.makeIdentifier("dependencies"),
200 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
201 | expression: ExprSyntax(emptyArray),
202 | trailingComma: nil
203 | )
204 |
205 | let expr = SyntaxFactory.makeFunctionCallExpr(
206 | calledExpression: ExprSyntax(dotTargetExpr),
207 | leftParen: SyntaxFactory.makeLeftParenToken(),
208 | argumentList: SyntaxFactory.makeTupleExprElementList([
209 | nameArg, depenenciesArg,
210 | ]),
211 | rightParen: SyntaxFactory.makeRightParenToken(),
212 | trailingClosure: nil,
213 | additionalTrailingClosures: nil
214 | )
215 |
216 | let newManifest = targetsNode
217 | .withAdditionalElementExpr(ExprSyntax(expr))
218 | .reindentingLastCallExprElement()
219 | .root
220 |
221 | self.editedSource = newManifest.as(SourceFileSyntax.self)!
222 | }
223 |
224 | public func addBinaryTarget(targetName: String,
225 | urlOrPath: String,
226 | checksum: String?) throws {
227 | let initFnExpr = try findPackageInit()
228 | let targetsNode = try findOrCreateTargetsList(in: initFnExpr)
229 |
230 | let dotTargetExpr = SyntaxFactory.makeMemberAccessExpr(
231 | base: nil,
232 | dot: SyntaxFactory.makePeriodToken(),
233 | name: SyntaxFactory.makeIdentifier("binaryTarget"),
234 | declNameArguments: nil
235 | )
236 |
237 | var args: [TupleExprElementSyntax] = []
238 |
239 | let nameArg = SyntaxFactory.makeTupleExprElement(
240 | label: SyntaxFactory.makeIdentifier("name"),
241 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
242 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(targetName)),
243 | trailingComma: SyntaxFactory.makeCommaToken()
244 | )
245 | args.append(nameArg)
246 |
247 | if TSCUtility.URL.scheme(urlOrPath) == nil {
248 | guard checksum == nil else {
249 | diagnosticsEngine.emit(.unexpectedChecksumForBinaryTarget(path: urlOrPath))
250 | throw Diagnostics.fatalError
251 | }
252 |
253 | let pathArg = SyntaxFactory.makeTupleExprElement(
254 | label: SyntaxFactory.makeIdentifier("path"),
255 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
256 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(urlOrPath)),
257 | trailingComma: nil
258 | )
259 | args.append(pathArg)
260 | } else {
261 | guard let checksum = checksum else {
262 | diagnosticsEngine.emit(.missingChecksumForBinaryTarget(url: urlOrPath))
263 | throw Diagnostics.fatalError
264 | }
265 |
266 | let urlArg = SyntaxFactory.makeTupleExprElement(
267 | label: SyntaxFactory.makeIdentifier("url"),
268 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
269 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(urlOrPath)),
270 | trailingComma: SyntaxFactory.makeCommaToken()
271 | )
272 | args.append(urlArg)
273 |
274 | let checksumArg = SyntaxFactory.makeTupleExprElement(
275 | label: SyntaxFactory.makeIdentifier("checksum"),
276 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
277 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(checksum)),
278 | trailingComma: nil
279 | )
280 | args.append(checksumArg)
281 | }
282 |
283 | let expr = SyntaxFactory.makeFunctionCallExpr(
284 | calledExpression: ExprSyntax(dotTargetExpr),
285 | leftParen: SyntaxFactory.makeLeftParenToken(),
286 | argumentList: SyntaxFactory.makeTupleExprElementList(args),
287 | rightParen: SyntaxFactory.makeRightParenToken(),
288 | trailingClosure: nil,
289 | additionalTrailingClosures: nil
290 | )
291 |
292 | let newManifest = targetsNode
293 | .withAdditionalElementExpr(ExprSyntax(expr))
294 | .reindentingLastCallExprElement()
295 | .root
296 |
297 | self.editedSource = newManifest.as(SourceFileSyntax.self)!
298 | }
299 |
300 | // Add a new product.
301 | public func addProduct(name: String, type: ProductType) throws {
302 | let initFnExpr = try findPackageInit()
303 |
304 | let productsFinder = ArrayExprArgumentFinder(expectedLabel: "products")
305 | productsFinder.walk(initFnExpr.argumentList)
306 | let productsNode: ArrayExprSyntax
307 |
308 | switch productsFinder.result {
309 | case .found(let existingProducts):
310 | productsNode = existingProducts
311 | case .missing:
312 | // We didn't find a products section, so insert one.
313 | let argListWithProducts = EmptyArrayArgumentWriter(argumentLabel: "products",
314 | followingArgumentLabels:
315 | "dependencies",
316 | "targets",
317 | "swiftLanguageVersions",
318 | "cLanguageStandard",
319 | "cxxLanguageStandard")
320 | .visit(initFnExpr.argumentList)
321 |
322 | // Find the inserted section.
323 | let productsFinder = ArrayExprArgumentFinder(expectedLabel: "products")
324 | productsFinder.walk(argListWithProducts)
325 | guard case .found(let newProducts) = productsFinder.result else {
326 | fatalError("Could not find just inserted products array")
327 | }
328 | productsNode = newProducts
329 | case .incompatibleExpr:
330 | diagnosticsEngine.emit(.incompatibleArgument(name: "products"))
331 | throw Diagnostics.fatalError
332 | }
333 |
334 | let newManifest = NewProductWriter(
335 | name: name, type: type
336 | ).visit(productsNode).root
337 |
338 | self.editedSource = newManifest.as(SourceFileSyntax.self)!
339 | }
340 |
341 | // Add a target to a product.
342 | public func addProductTarget(product: String, target: String) throws {
343 | let initFnExpr = try findPackageInit()
344 |
345 | // Find the `products: []` array.
346 | let productsArrayFinder = ArrayExprArgumentFinder(expectedLabel: "products")
347 | productsArrayFinder.walk(initFnExpr.argumentList)
348 | guard case .found(let productsArrayExpr) = productsArrayFinder.result else {
349 | diagnosticsEngine.emit(.missingPackageInitArgument(name: "products"))
350 | throw Diagnostics.fatalError
351 | }
352 |
353 | // Find the product node.
354 | let productFinder = NamedEntityArgumentListFinder(name: product)
355 | productFinder.walk(productsArrayExpr)
356 | guard let productNode = productFinder.foundEntity else {
357 | diagnosticsEngine.emit(.missingProduct(name: product))
358 | throw Diagnostics.fatalError
359 | }
360 |
361 | let productTargetsFinder = ArrayExprArgumentFinder(expectedLabel: "targets")
362 | productTargetsFinder.walk(productNode)
363 |
364 | guard case .found(let productTargets) = productTargetsFinder.result else {
365 | diagnosticsEngine.emit(.missingArgument(name: "targets", parent: "product '\(product)'"))
366 | throw Diagnostics.fatalError
367 | }
368 |
369 | let newManifest = productTargets.withAdditionalElementExpr(ExprSyntax(
370 | SyntaxFactory.makeStringLiteralExpr(target)
371 | )).root
372 |
373 | self.editedSource = newManifest.as(SourceFileSyntax.self)!
374 | }
375 |
376 | private func findOrCreateTargetsList(in packageInitExpr: FunctionCallExprSyntax) throws -> ArrayExprSyntax {
377 | let targetsFinder = ArrayExprArgumentFinder(expectedLabel: "targets")
378 | targetsFinder.walk(packageInitExpr.argumentList)
379 |
380 | let targetsNode: ArrayExprSyntax
381 | switch targetsFinder.result {
382 | case .found(let existingTargets):
383 | targetsNode = existingTargets
384 | case .missing:
385 | // We didn't find a targets section, so insert one.
386 | let argListWithTargets = EmptyArrayArgumentWriter(argumentLabel: "targets",
387 | followingArgumentLabels:
388 | "swiftLanguageVersions",
389 | "cLanguageStandard",
390 | "cxxLanguageStandard")
391 | .visit(packageInitExpr.argumentList)
392 |
393 | // Find the inserted section.
394 | let targetsFinder = ArrayExprArgumentFinder(expectedLabel: "targets")
395 | targetsFinder.walk(argListWithTargets)
396 | guard case .found(let newTargets) = targetsFinder.result else {
397 | fatalError("Could not find just-inserted targets array")
398 | }
399 | targetsNode = newTargets
400 | case .incompatibleExpr:
401 | diagnosticsEngine.emit(.incompatibleArgument(name: "targets"))
402 | throw Diagnostics.fatalError
403 | }
404 |
405 | return targetsNode
406 | }
407 |
408 | private func findPackageInit() throws -> FunctionCallExprSyntax {
409 | // Find Package initializer.
410 | let packageFinder = PackageInitFinder()
411 | packageFinder.walk(editedSource)
412 | switch packageFinder.result {
413 | case .found(let initFnExpr):
414 | return initFnExpr
415 | case .foundMultiple:
416 | diagnosticsEngine.emit(.multiplePackageInits)
417 | throw Diagnostics.fatalError
418 | case .missing:
419 | diagnosticsEngine.emit(.missingPackageInit)
420 | throw Diagnostics.fatalError
421 | }
422 | }
423 | }
424 |
425 | // MARK: - Syntax Visitors
426 |
427 | /// Package init finder.
428 | final class PackageInitFinder: SyntaxVisitor {
429 |
430 | enum Result {
431 | case found(FunctionCallExprSyntax)
432 | case foundMultiple
433 | case missing
434 | }
435 |
436 | /// Reference to the function call of the package initializer.
437 | private(set) var result: Result = .missing
438 |
439 | override func visit(_ node: InitializerClauseSyntax) -> SyntaxVisitorContinueKind {
440 | if let fnCall = FunctionCallExprSyntax(Syntax(node.value)),
441 | let identifier = fnCall.calledExpression.firstToken,
442 | identifier.text == "Package" {
443 | if case .missing = result {
444 | result = .found(fnCall)
445 | } else {
446 | result = .foundMultiple
447 | }
448 | }
449 | return .skipChildren
450 | }
451 | }
452 |
453 | /// Finder for an array expression used as or as part of a labeled argument.
454 | final class ArrayExprArgumentFinder: SyntaxVisitor {
455 |
456 | enum Result {
457 | case found(ArrayExprSyntax)
458 | case missing
459 | case incompatibleExpr
460 | }
461 |
462 | private(set) var result: Result
463 | private let expectedLabel: String
464 |
465 | init(expectedLabel: String) {
466 | self.expectedLabel = expectedLabel
467 | self.result = .missing
468 | super.init()
469 | }
470 |
471 | override func visit(_ node: TupleExprElementSyntax) -> SyntaxVisitorContinueKind {
472 | guard node.label?.text == expectedLabel else {
473 | return .skipChildren
474 | }
475 |
476 | // We have custom code like foo + bar + [] (hopefully there is an array expr here).
477 | if let seq = node.expression.as(SequenceExprSyntax.self),
478 | let arrayExpr = seq.elements.first(where: { $0.is(ArrayExprSyntax.self) })?.as(ArrayExprSyntax.self) {
479 | result = .found(arrayExpr)
480 | } else if let arrayExpr = node.expression.as(ArrayExprSyntax.self) {
481 | result = .found(arrayExpr)
482 | } else {
483 | result = .incompatibleExpr
484 | }
485 |
486 | return .skipChildren
487 | }
488 | }
489 |
490 | /// Given an Array expression of call expressions, find the argument list of the call
491 | /// expression with the specified `name` argument.
492 | final class NamedEntityArgumentListFinder: SyntaxVisitor {
493 |
494 | let entityToFind: String
495 | private(set) var foundEntity: TupleExprElementListSyntax?
496 |
497 | init(name: String) {
498 | self.entityToFind = name
499 | }
500 |
501 | override func visit(_ node: TupleExprElementSyntax) -> SyntaxVisitorContinueKind {
502 | guard case .identifier(let label)? = node.label?.tokenKind else {
503 | return .skipChildren
504 | }
505 | guard label == "name", let targetNameExpr = node.expression.as(StringLiteralExprSyntax.self),
506 | targetNameExpr.segments.count == 1, let segment = targetNameExpr.segments.first?.as(StringSegmentSyntax.self) else {
507 | return .skipChildren
508 | }
509 |
510 | guard case .stringSegment(let name) = segment.content.tokenKind else {
511 | return .skipChildren
512 | }
513 |
514 | if name == self.entityToFind {
515 | self.foundEntity = node.parent?.as(TupleExprElementListSyntax.self)
516 | return .skipChildren
517 | }
518 |
519 | return .skipChildren
520 | }
521 | }
522 |
523 | // MARK: - Syntax Rewriters
524 |
525 | /// Writer for an empty array argument.
526 | final class EmptyArrayArgumentWriter: SyntaxRewriter {
527 | let argumentLabel: String
528 | let followingArgumentLabels: Set
529 |
530 | init(argumentLabel: String, followingArgumentLabels: String...) {
531 | self.argumentLabel = argumentLabel
532 | self.followingArgumentLabels = .init(followingArgumentLabels)
533 | }
534 |
535 | override func visit(_ node: TupleExprElementListSyntax) -> Syntax {
536 | let leadingTrivia = node.firstToken?.leadingTrivia ?? .zero
537 |
538 | let existingLabels = node.map(\.label?.text)
539 | let insertionIndex = existingLabels.firstIndex {
540 | followingArgumentLabels.contains($0 ?? "")
541 | } ?? existingLabels.endIndex
542 |
543 | let dependenciesArg = SyntaxFactory.makeTupleExprElement(
544 | label: SyntaxFactory.makeIdentifier(argumentLabel, leadingTrivia: leadingTrivia),
545 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
546 | expression: ExprSyntax(SyntaxFactory.makeArrayExpr(
547 | leftSquare: SyntaxFactory.makeLeftSquareBracketToken(),
548 | elements: SyntaxFactory.makeBlankArrayElementList(),
549 | rightSquare: SyntaxFactory.makeRightSquareBracketToken())),
550 | trailingComma: insertionIndex != existingLabels.endIndex ? SyntaxFactory.makeCommaToken() : nil
551 | )
552 |
553 | var newNode = node
554 | if let lastArgument = newNode.last,
555 | insertionIndex == existingLabels.endIndex {
556 | // If the new argument is being added at the end of the list, the argument before it needs a comma.
557 | newNode = newNode.replacing(childAt: newNode.count-1,
558 | with: lastArgument.withTrailingComma(SyntaxFactory.makeCommaToken()))
559 | }
560 |
561 | return Syntax(newNode.inserting(dependenciesArg, at: insertionIndex))
562 | }
563 | }
564 |
565 | /// Package dependency writer.
566 | final class PackageDependencyWriter: SyntaxRewriter {
567 |
568 | /// The dependency name to write.
569 | let name: String?
570 |
571 | /// The dependency url to write.
572 | let url: String
573 |
574 | /// The dependency requirement.
575 | let requirement: PackageDependencyRequirement
576 |
577 | /// Whether convenience methods for branch and revision dependencies are supported.
578 | let branchAndRevisionConvenienceMethodsSupported: Bool
579 |
580 | init(name: String?,
581 | url: String,
582 | requirement: PackageDependencyRequirement,
583 | branchAndRevisionConvenienceMethodsSupported: Bool) {
584 | self.name = name
585 | self.url = url
586 | self.requirement = requirement
587 | self.branchAndRevisionConvenienceMethodsSupported = branchAndRevisionConvenienceMethodsSupported
588 | }
589 |
590 | override func visit(_ node: ArrayExprSyntax) -> ExprSyntax {
591 |
592 | let dotPackageExpr = SyntaxFactory.makeMemberAccessExpr(
593 | base: nil,
594 | dot: SyntaxFactory.makePeriodToken(),
595 | name: SyntaxFactory.makeIdentifier("package"),
596 | declNameArguments: nil
597 | )
598 |
599 | var args: [TupleExprElementSyntax] = []
600 |
601 | if let name = self.name {
602 | let nameArg = SyntaxFactory.makeTupleExprElement(
603 | label: SyntaxFactory.makeIdentifier("name"),
604 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
605 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(name)),
606 | trailingComma: SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1))
607 | )
608 | args.append(nameArg)
609 | }
610 |
611 | let locationArgLabel = requirement == .localPackage ? "path" : "url"
612 | let locationArg = SyntaxFactory.makeTupleExprElement(
613 | label: SyntaxFactory.makeIdentifier(locationArgLabel),
614 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
615 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(self.url)),
616 | trailingComma: requirement == .localPackage ? nil : SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1))
617 | )
618 | args.append(locationArg)
619 |
620 | let addUnlabeledImplicitMemberCallWithStringArg = { (baseName: String, argumentLabel: String?, argumentString: String) in
621 | let memberExpr = SyntaxFactory.makeMemberAccessExpr(base: nil,
622 | dot: SyntaxFactory.makePeriodToken(),
623 | name: SyntaxFactory.makeIdentifier(baseName),
624 | declNameArguments: nil)
625 | let argList = SyntaxFactory.makeTupleExprElementList([
626 | SyntaxFactory.makeTupleExprElement(label: argumentLabel.map { SyntaxFactory.makeIdentifier($0) },
627 | colon: argumentLabel.map { _ in SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)) },
628 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(argumentString)),
629 | trailingComma: nil)
630 | ])
631 | let exactExpr = SyntaxFactory.makeFunctionCallExpr(calledExpression: ExprSyntax(memberExpr),
632 | leftParen: SyntaxFactory.makeLeftParenToken(),
633 | argumentList: argList,
634 | rightParen: SyntaxFactory.makeRightParenToken(),
635 | trailingClosure: nil,
636 | additionalTrailingClosures: nil)
637 | let exactArg = SyntaxFactory.makeTupleExprElement(label: nil,
638 | colon: nil,
639 | expression: ExprSyntax(exactExpr),
640 | trailingComma: nil)
641 | args.append(exactArg)
642 | }
643 |
644 | let addUnlabeledRangeArg = { (start: String, end: String, rangeOperator: String) in
645 | let rangeExpr = SyntaxFactory.makeSequenceExpr(elements: SyntaxFactory.makeExprList([
646 | ExprSyntax(SyntaxFactory.makeStringLiteralExpr(start)),
647 | ExprSyntax(SyntaxFactory.makeBinaryOperatorExpr(
648 | operatorToken: SyntaxFactory.makeUnspacedBinaryOperator(rangeOperator))
649 | ),
650 | ExprSyntax(SyntaxFactory.makeStringLiteralExpr(end))
651 | ]))
652 | let arg = SyntaxFactory.makeTupleExprElement(label: nil,
653 | colon: nil,
654 | expression: ExprSyntax(rangeExpr),
655 | trailingComma: nil)
656 | args.append(arg)
657 | }
658 |
659 | let addLabeledStringArg = { (label: String, literalString: String) in
660 | let arg = SyntaxFactory.makeTupleExprElement(label: SyntaxFactory.makeIdentifier(label),
661 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
662 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(literalString)),
663 | trailingComma: nil)
664 | args.append(arg)
665 | }
666 |
667 | switch requirement {
668 | case .exact(let version):
669 | addUnlabeledImplicitMemberCallWithStringArg("exact", nil, version)
670 | case .revision(let revision):
671 | if branchAndRevisionConvenienceMethodsSupported {
672 | addLabeledStringArg("revision", revision)
673 | } else {
674 | addUnlabeledImplicitMemberCallWithStringArg("revision", nil, revision)
675 | }
676 | case .branch(let branch):
677 | if branchAndRevisionConvenienceMethodsSupported {
678 | addLabeledStringArg("branch", branch)
679 | } else {
680 | addUnlabeledImplicitMemberCallWithStringArg("branch", nil, branch)
681 | }
682 | case .upToNextMajor(let version):
683 | addUnlabeledImplicitMemberCallWithStringArg("upToNextMajor", "from", version)
684 | case .upToNextMinor(let version):
685 | addUnlabeledImplicitMemberCallWithStringArg("upToNextMinor", "from", version)
686 | case .range(let start, let end):
687 | addUnlabeledRangeArg(start, end, "..<")
688 | case .closedRange(let start, let end):
689 | addUnlabeledRangeArg(start, end, "...")
690 | case .localPackage:
691 | break
692 | }
693 |
694 | let expr = SyntaxFactory.makeFunctionCallExpr(
695 | calledExpression: ExprSyntax(dotPackageExpr),
696 | leftParen: SyntaxFactory.makeLeftParenToken(),
697 | argumentList: SyntaxFactory.makeTupleExprElementList(args),
698 | rightParen: SyntaxFactory.makeRightParenToken(),
699 | trailingClosure: nil,
700 | additionalTrailingClosures: nil
701 | )
702 |
703 | return ExprSyntax(node.withAdditionalElementExpr(ExprSyntax(expr)))
704 | }
705 | }
706 |
707 | /// Writer for inserting a new product in a products array.
708 | final class NewProductWriter: SyntaxRewriter {
709 |
710 | let name: String
711 | let type: ProductType
712 |
713 | init(name: String, type: ProductType) {
714 | self.name = name
715 | self.type = type
716 | }
717 |
718 | override func visit(_ node: ArrayExprSyntax) -> ExprSyntax {
719 | let dotExpr = SyntaxFactory.makeMemberAccessExpr(
720 | base: nil,
721 | dot: SyntaxFactory.makePeriodToken(),
722 | name: SyntaxFactory.makeIdentifier(type == .executable ? "executable" : "library"),
723 | declNameArguments: nil
724 | )
725 |
726 | var args: [TupleExprElementSyntax] = []
727 |
728 | let nameArg = SyntaxFactory.makeTupleExprElement(
729 | label: SyntaxFactory.makeIdentifier("name"),
730 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
731 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(name)),
732 | trailingComma: SyntaxFactory.makeCommaToken()
733 | )
734 | args.append(nameArg)
735 |
736 | if case .library(let kind) = type, kind != .automatic {
737 | let typeExpr = SyntaxFactory.makeMemberAccessExpr(base: nil,
738 | dot: SyntaxFactory.makePeriodToken(),
739 | name: SyntaxFactory.makeIdentifier(kind == .dynamic ? "dynamic" : "static"),
740 | declNameArguments: nil)
741 | let typeArg = SyntaxFactory.makeTupleExprElement(
742 | label: SyntaxFactory.makeIdentifier("type"),
743 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
744 | expression: ExprSyntax(typeExpr),
745 | trailingComma: SyntaxFactory.makeCommaToken()
746 | )
747 | args.append(typeArg)
748 | }
749 |
750 | let emptyArray = SyntaxFactory.makeArrayExpr(leftSquare: SyntaxFactory.makeLeftSquareBracketToken(),
751 | elements: SyntaxFactory.makeBlankArrayElementList(),
752 | rightSquare: SyntaxFactory.makeRightSquareBracketToken())
753 | let targetsArg = SyntaxFactory.makeTupleExprElement(
754 | label: SyntaxFactory.makeIdentifier("targets"),
755 | colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)),
756 | expression: ExprSyntax(emptyArray),
757 | trailingComma: nil
758 | )
759 | args.append(targetsArg)
760 |
761 | let expr = SyntaxFactory.makeFunctionCallExpr(
762 | calledExpression: ExprSyntax(dotExpr),
763 | leftParen: SyntaxFactory.makeLeftParenToken(),
764 | argumentList: SyntaxFactory.makeTupleExprElementList(args),
765 | rightParen: SyntaxFactory.makeRightParenToken(),
766 | trailingClosure: nil,
767 | additionalTrailingClosures: nil
768 | )
769 |
770 | return ExprSyntax(node
771 | .withAdditionalElementExpr(ExprSyntax(expr))
772 | .reindentingLastCallExprElement())
773 | }
774 | }
775 |
776 | private extension TSCBasic.Diagnostic.Message {
777 | static var missingPackageInit: Self =
778 | .error("couldn't find Package initializer")
779 | static var multiplePackageInits: Self =
780 | .error("found multiple Package initializers")
781 | static func missingPackageInitArgument(name: String) -> Self {
782 | .error("couldn't find '\(name)' argument in Package initializer")
783 | }
784 | static func missingArgument(name: String, parent: String) -> Self {
785 | .error("couldn't find '\(name)' argument of \(parent)")
786 | }
787 | static func incompatibleArgument(name: String) -> Self {
788 | .error("'\(name)' argument is not an array literal or concatenation of array literals")
789 | }
790 | static func missingProduct(name: String) -> Self {
791 | .error("couldn't find product '\(name)'")
792 | }
793 | static func missingTarget(name: String) -> Self {
794 | .error("couldn't find target '\(name)'")
795 | }
796 | static func unexpectedChecksumForBinaryTarget(path: String) -> Self {
797 | .error("'\(path)' is a local path, but a checksum was specified for the binary target")
798 | }
799 | static func missingChecksumForBinaryTarget(url: String) -> Self {
800 | .error("'\(url)' is a remote URL, but no checksum was specified for the binary target")
801 | }
802 | }
803 |
--------------------------------------------------------------------------------
/Sources/PackageSyntax/PackageEditor.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import TSCUtility
12 | import TSCBasic
13 | import SourceControl
14 | import PackageLoading
15 | import PackageModel
16 | import Workspace
17 | import Foundation
18 |
19 | /// An editor for Swift packages.
20 | ///
21 | /// This class provides high-level functionality for performing
22 | /// editing operations a package.
23 | public final class PackageEditor {
24 |
25 | /// Reference to the package editor context.
26 | let context: PackageEditorContext
27 |
28 | /// Create a package editor instance.
29 | public convenience init(manifestPath: AbsolutePath,
30 | repositoryManager: RepositoryManager,
31 | toolchain: UserToolchain,
32 | diagnosticsEngine: DiagnosticsEngine) throws {
33 | self.init(context: try PackageEditorContext(manifestPath: manifestPath,
34 | repositoryManager: repositoryManager,
35 | toolchain: toolchain,
36 | diagnosticsEngine: diagnosticsEngine))
37 | }
38 |
39 | /// Create a package editor instance.
40 | public init(context: PackageEditorContext) {
41 | self.context = context
42 | }
43 |
44 | /// The file system to perform disk operations on.
45 | var fs: FileSystem {
46 | return context.fs
47 | }
48 |
49 | /// Add a package dependency.
50 | public func addPackageDependency(url: String, requirement: PackageDependencyRequirement?) throws {
51 | var requirement = requirement
52 | let manifestPath = context.manifestPath
53 | // Validate that the package doesn't already contain this dependency.
54 | let loadedManifest = try context.loadManifest(at: context.manifestPath.parentDirectory)
55 |
56 | try diagnoseUnsupportedToolsVersions(manifest: loadedManifest)
57 |
58 | let containsDependency = loadedManifest.dependencies.contains {
59 | return PackageIdentity(url: url) == $0.identity
60 | }
61 | guard !containsDependency else {
62 | context.diagnosticsEngine.emit(.packageDependencyAlreadyExists(url: url,
63 | packageName: loadedManifest.name))
64 | throw Diagnostics.fatalError
65 | }
66 |
67 | // If the input URL is a path, force the requirement to be a local package.
68 | if TSCUtility.URL.scheme(url) == nil {
69 | guard requirement == nil || requirement == .localPackage else {
70 | context.diagnosticsEngine.emit(.nonLocalRequirementSpecifiedForLocalPath(path: url))
71 | throw Diagnostics.fatalError
72 | }
73 | requirement = .localPackage
74 | }
75 |
76 | // Load the dependency manifest depending on the inputs.
77 | let dependencyManifest: Manifest
78 | if requirement == .localPackage {
79 | let path = AbsolutePath(url, relativeTo: fs.currentWorkingDirectory!)
80 | dependencyManifest = try context.loadManifest(at: path)
81 | requirement = .localPackage
82 | } else {
83 | // Otherwise, first lookup the dependency.
84 | let spec = RepositorySpecifier(url: url)
85 | let handle = try tsc_await{
86 | context.repositoryManager.lookup(repository: spec,
87 | on: .global(qos: .userInitiated),
88 | completion: $0)
89 | }
90 | let repo = try handle.open()
91 |
92 | // Compute the requirement.
93 | if let inputRequirement = requirement {
94 | requirement = inputRequirement
95 | } else {
96 | // Use the latest version or the main/master branch.
97 | let versions = try repo.getTags().compactMap{ Version(string: $0) }
98 | let latestVersion = versions.filter({ $0.prereleaseIdentifiers.isEmpty }).max() ?? versions.max()
99 | let mainExists = (try? repo.resolveRevision(identifier: "main")) != nil
100 | requirement = latestVersion.map{ PackageDependencyRequirement.upToNextMajor($0.description) } ??
101 | (mainExists ? PackageDependencyRequirement.branch("main") : PackageDependencyRequirement.branch("master"))
102 | }
103 |
104 | // Load the manifest.
105 | let revision = try repo.resolveRevision(identifier: requirement!.ref!)
106 | let repoFS = try repo.openFileView(revision: revision)
107 | dependencyManifest = try context.loadManifest(at: .root, fs: repoFS)
108 | }
109 |
110 | // Add the package dependency.
111 | let manifestContents = try fs.readFileContents(manifestPath).cString
112 | let editor = try ManifestRewriter(manifestContents, diagnosticsEngine: context.diagnosticsEngine)
113 |
114 | // Only tools-version 5.2, 5.3, & 5.4 should specify a package name.
115 | // At this point, we've already diagnosed tools-versions less than 5.2 as unsupported
116 | if loadedManifest.toolsVersion < .v5_5 {
117 | try editor.addPackageDependency(name: dependencyManifest.name,
118 | url: url,
119 | requirement: requirement!,
120 | branchAndRevisionConvenienceMethodsSupported: false)
121 | } else {
122 | try editor.addPackageDependency(name: nil,
123 | url: url,
124 | requirement: requirement!,
125 | branchAndRevisionConvenienceMethodsSupported: true)
126 | }
127 |
128 | try context.verifyEditedManifest(contents: editor.editedManifest)
129 | try fs.writeFileContents(manifestPath, bytes: ByteString(encodingAsUTF8: editor.editedManifest))
130 | }
131 |
132 | /// Add a new target.
133 | public func addTarget(_ newTarget: NewTarget, productPackageNameMapping: [String: String]) throws {
134 | let manifestPath = context.manifestPath
135 |
136 | // Validate that the package doesn't already contain a target with the same name.
137 | let loadedManifest = try context.loadManifest(at: manifestPath.parentDirectory)
138 |
139 | try diagnoseUnsupportedToolsVersions(manifest: loadedManifest)
140 |
141 | if loadedManifest.targets.contains(where: { $0.name == newTarget.name }) {
142 | context.diagnosticsEngine.emit(.targetAlreadyExists(name: newTarget.name,
143 | packageName: loadedManifest.name))
144 | throw Diagnostics.fatalError
145 | }
146 |
147 | let manifestContents = try fs.readFileContents(manifestPath).cString
148 | let editor = try ManifestRewriter(manifestContents, diagnosticsEngine: context.diagnosticsEngine)
149 |
150 | switch newTarget {
151 | case .library(name: let name, includeTestTarget: _, dependencyNames: let dependencyNames),
152 | .executable(name: let name, dependencyNames: let dependencyNames),
153 | .test(name: let name, dependencyNames: let dependencyNames):
154 | try editor.addTarget(targetName: newTarget.name,
155 | factoryMethodName: newTarget.factoryMethodName(for: loadedManifest.toolsVersion))
156 |
157 | for dependency in dependencyNames {
158 | if loadedManifest.targets.map(\.name).contains(dependency) {
159 | try editor.addByNameTargetDependency(target: name, dependency: dependency)
160 | } else if let productPackage = productPackageNameMapping[dependency] {
161 | if productPackage == dependency {
162 | try editor.addByNameTargetDependency(target: name, dependency: dependency)
163 | } else {
164 | try editor.addProductTargetDependency(target: name, product: dependency, package: productPackage)
165 | }
166 | } else {
167 | context.diagnosticsEngine.emit(.missingProductOrTarget(name: dependency))
168 | throw Diagnostics.fatalError
169 | }
170 | }
171 | case .binary(name: let name, urlOrPath: let urlOrPath, checksum: let checksum):
172 | guard loadedManifest.toolsVersion >= .v5_3 else {
173 | context.diagnosticsEngine.emit(.unsupportedToolsVersionForBinaryTargets)
174 | throw Diagnostics.fatalError
175 | }
176 | try editor.addBinaryTarget(targetName: name, urlOrPath: urlOrPath, checksum: checksum)
177 | }
178 |
179 | try context.verifyEditedManifest(contents: editor.editedManifest)
180 | try fs.writeFileContents(manifestPath, bytes: ByteString(encodingAsUTF8: editor.editedManifest))
181 |
182 | // Write template files.
183 | try writeTemplateFilesForTarget(newTarget)
184 |
185 | if case .library(name: let name, includeTestTarget: true, dependencyNames: _) = newTarget {
186 | try self.addTarget(.test(name: "\(name)Tests", dependencyNames: [name]),
187 | productPackageNameMapping: productPackageNameMapping)
188 | }
189 | }
190 |
191 | private func diagnoseUnsupportedToolsVersions(manifest: Manifest) throws {
192 | guard manifest.toolsVersion >= .v5_2 else {
193 | context.diagnosticsEngine.emit(.unsupportedToolsVersionForEditing)
194 | throw Diagnostics.fatalError
195 | }
196 | }
197 |
198 | private func writeTemplateFilesForTarget(_ newTarget: NewTarget) throws {
199 | switch newTarget {
200 | case .library:
201 | let targetPath = context.manifestPath.parentDirectory.appending(components: "Sources", newTarget.name)
202 | if !localFileSystem.exists(targetPath) {
203 | let file = targetPath.appending(component: "\(newTarget.name).swift")
204 | try fs.createDirectory(targetPath, recursive: true)
205 | try fs.writeFileContents(file, bytes: "")
206 | }
207 | case .executable:
208 | let targetPath = context.manifestPath.parentDirectory.appending(components: "Sources", newTarget.name)
209 | if !localFileSystem.exists(targetPath) {
210 | let file = targetPath.appending(component: "main.swift")
211 | try fs.createDirectory(targetPath, recursive: true)
212 | try fs.writeFileContents(file, bytes: "")
213 | }
214 | case .test:
215 | let testTargetPath = context.manifestPath.parentDirectory.appending(components: "Tests", newTarget.name)
216 | if !fs.exists(testTargetPath) {
217 | let file = testTargetPath.appending(components: newTarget.name + ".swift")
218 | try fs.createDirectory(testTargetPath, recursive: true)
219 | try fs.writeFileContents(file) {
220 | $0 <<< """
221 | import XCTest
222 | @testable import <#Module#>
223 |
224 | final class <#TestCase#>: XCTestCase {
225 | func testExample() {
226 |
227 | }
228 | }
229 | """
230 | }
231 | }
232 | case .binary:
233 | break
234 | }
235 | }
236 |
237 | public func addProduct(name: String, type: ProductType, targets: [String]) throws {
238 | let manifestPath = context.manifestPath
239 |
240 | // Validate that the package doesn't already contain a product with the same name.
241 | let loadedManifest = try context.loadManifest(at: manifestPath.parentDirectory)
242 |
243 | try diagnoseUnsupportedToolsVersions(manifest: loadedManifest)
244 |
245 | guard !loadedManifest.products.contains(where: { $0.name == name }) else {
246 | context.diagnosticsEngine.emit(.productAlreadyExists(name: name,
247 | packageName: loadedManifest.name))
248 | throw Diagnostics.fatalError
249 | }
250 |
251 | let manifestContents = try fs.readFileContents(manifestPath).cString
252 | let editor = try ManifestRewriter(manifestContents, diagnosticsEngine: context.diagnosticsEngine)
253 | try editor.addProduct(name: name, type: type)
254 |
255 |
256 | for target in targets {
257 | guard loadedManifest.targets.map(\.name).contains(target) else {
258 | context.diagnosticsEngine.emit(.noTarget(name: target, packageName: loadedManifest.name))
259 | throw Diagnostics.fatalError
260 | }
261 | try editor.addProductTarget(product: name, target: target)
262 | }
263 |
264 | try context.verifyEditedManifest(contents: editor.editedManifest)
265 | try fs.writeFileContents(manifestPath, bytes: ByteString(encodingAsUTF8: editor.editedManifest))
266 | }
267 | }
268 |
269 | extension Array where Element == TargetDescription.Dependency {
270 | func containsDependency(_ other: String) -> Bool {
271 | return self.contains {
272 | switch $0 {
273 | case .target(name: let name, condition: _),
274 | .product(name: let name, package: _, condition: _),
275 | .byName(name: let name, condition: _):
276 | return name == other
277 | }
278 | }
279 | }
280 | }
281 |
282 | /// The types of target.
283 | public enum NewTarget {
284 | case library(name: String, includeTestTarget: Bool, dependencyNames: [String])
285 | case executable(name: String, dependencyNames: [String])
286 | case test(name: String, dependencyNames: [String])
287 | case binary(name: String, urlOrPath: String, checksum: String?)
288 |
289 | /// The name of the factory method for a target type.
290 | func factoryMethodName(for toolsVersion: ToolsVersion) -> String {
291 | switch self {
292 | case .executable:
293 | if toolsVersion >= .v5_4 {
294 | return "executableTarget"
295 | } else {
296 | return "target"
297 | }
298 | case .library: return "target"
299 | case .test: return "testTarget"
300 | case .binary: return "binaryTarget"
301 | }
302 | }
303 |
304 | /// The name of the new target.
305 | var name: String {
306 | switch self {
307 | case .library(name: let name, includeTestTarget: _, dependencyNames: _),
308 | .executable(name: let name, dependencyNames: _),
309 | .test(name: let name, dependencyNames: _),
310 | .binary(name: let name, urlOrPath: _, checksum: _):
311 | return name
312 | }
313 | }
314 | }
315 |
316 | public enum PackageDependencyRequirement: Equatable {
317 | case exact(String)
318 | case revision(String)
319 | case branch(String)
320 | case upToNextMajor(String)
321 | case upToNextMinor(String)
322 | case range(String, String)
323 | case closedRange(String, String)
324 | case localPackage
325 |
326 | var ref: String? {
327 | switch self {
328 | case .exact(let ref): return ref
329 | case .revision(let ref): return ref
330 | case .branch(let ref): return ref
331 | case .upToNextMajor(let ref): return ref
332 | case .upToNextMinor(let ref): return ref
333 | case .range(let start, _): return start
334 | case .closedRange(let start, _): return start
335 | case .localPackage: return nil
336 | }
337 | }
338 | }
339 |
340 | extension ProductType {
341 | var isLibrary: Bool {
342 | switch self {
343 | case .library:
344 | return true
345 | case .executable, .test, .plugin:
346 | return false
347 | }
348 | }
349 | }
350 |
351 | /// The global context for package editor.
352 | public final class PackageEditorContext {
353 | /// Path to the package manifest.
354 | let manifestPath: AbsolutePath
355 |
356 | /// The manifest loader.
357 | let manifestLoader: ManifestLoaderProtocol
358 |
359 | /// The repository manager.
360 | let repositoryManager: RepositoryManager
361 |
362 | /// The file system in use.
363 | let fs: FileSystem
364 |
365 | /// The diagnostics engine used to report errors.
366 | let diagnosticsEngine: DiagnosticsEngine
367 |
368 | public init(manifestPath: AbsolutePath,
369 | repositoryManager: RepositoryManager,
370 | toolchain: UserToolchain,
371 | diagnosticsEngine: DiagnosticsEngine,
372 | fs: FileSystem = localFileSystem) throws {
373 | self.manifestPath = manifestPath
374 | self.repositoryManager = repositoryManager
375 | self.diagnosticsEngine = diagnosticsEngine
376 | self.fs = fs
377 |
378 | self.manifestLoader = ManifestLoader(manifestResources: toolchain.manifestResources)
379 | }
380 |
381 | func verifyEditedManifest(contents: String) throws {
382 | do {
383 | try withTemporaryDirectory {
384 | let path = $0
385 | try localFileSystem.writeFileContents(path.appending(component: "Package.swift"),
386 | bytes: ByteString(encodingAsUTF8: contents))
387 | _ = try loadManifest(at: path, fs: localFileSystem)
388 | }
389 | } catch {
390 | diagnosticsEngine.emit(.failedToLoadEditedManifest(error: error))
391 | throw Diagnostics.fatalError
392 | }
393 | }
394 |
395 | /// Load the manifest at the given path.
396 | func loadManifest(
397 | at path: AbsolutePath,
398 | fs: FileSystem? = nil
399 | ) throws -> Manifest {
400 | let fs = fs ?? self.fs
401 |
402 | let toolsVersion = try ToolsVersionLoader().load(
403 | at: path, fileSystem: fs)
404 | return try tsc_await {
405 | manifestLoader.load(
406 | at: path,
407 | packageIdentity: .plain(""),
408 | packageKind: .local,
409 | packageLocation: path.pathString,
410 | version: nil,
411 | revision: nil,
412 | toolsVersion: toolsVersion,
413 | identityResolver: DefaultIdentityResolver(),
414 | fileSystem: fs,
415 | diagnostics: .init(),
416 | on: .global(),
417 | completion: $0
418 | )
419 | }
420 | }
421 | }
422 |
423 | private extension Diagnostic.Message {
424 | static func failedToLoadEditedManifest(error: Error) -> Diagnostic.Message {
425 | .error("discarding changes because the edited manifest could not be loaded: \(error)")
426 | }
427 | static var unsupportedToolsVersionForEditing: Diagnostic.Message =
428 | .error("command line editing of manifests is only supported for packages with a swift-tools-version of 5.2 and later")
429 | static var unsupportedToolsVersionForBinaryTargets: Diagnostic.Message =
430 | .error("binary targets are only supported in packages with a swift-tools-version of 5.3 and later")
431 | static func productAlreadyExists(name: String, packageName: String) -> Diagnostic.Message {
432 | .error("a product named '\(name)' already exists in '\(packageName)'")
433 | }
434 | static func packageDependencyAlreadyExists(url: String, packageName: String) -> Diagnostic.Message {
435 | .error("'\(packageName)' already has a dependency on '\(url)'")
436 | }
437 | static func noTarget(name: String, packageName: String) -> Diagnostic.Message {
438 | .error("no target named '\(name)' in '\(packageName)'")
439 | }
440 | static func targetAlreadyExists(name: String, packageName: String) -> Diagnostic.Message {
441 | .error("a target named '\(name)' already exists in '\(packageName)'")
442 | }
443 | static func nonLocalRequirementSpecifiedForLocalPath(path: String) -> Diagnostic.Message {
444 | .error("'\(path)' is a local package, but a non-local requirement was specified")
445 | }
446 | static func missingProductOrTarget(name: String) -> Diagnostic.Message {
447 | .error("could not find a product or target named '\(name)'")
448 | }
449 | }
450 |
--------------------------------------------------------------------------------
/Sources/swift-package-editor/SwiftPackageEditorTool.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import ArgumentParser
12 | import Basics
13 | import TSCBasic
14 | import PackageModel
15 | import PackageGraph
16 | import SourceControl
17 | import Workspace
18 | import Foundation
19 | import PackageSyntax
20 |
21 | @main
22 | public struct SwiftPackageEditorTool: ParsableCommand {
23 | public static var configuration = CommandConfiguration(
24 | commandName: "package-editor",
25 | _superCommandName: "swift",
26 | abstract: "Edit Package.swift files",
27 | version: SwiftVersion.currentVersion.completeDisplayString,
28 | subcommands: [AddDependency.self, AddTarget.self, AddProduct.self],
29 | helpNames: [.short, .long, .customLong("help", withSingleDash: true)])
30 | public init() {}
31 | }
32 |
33 | final class EditorTool {
34 | let diagnostics: DiagnosticsEngine
35 | let packageRoot: AbsolutePath
36 | let toolchain: UserToolchain
37 | let packageEditor: PackageEditor
38 | private var cachedPackageGraph: PackageGraph?
39 |
40 | init() throws {
41 | diagnostics = DiagnosticsEngine(handlers: [print(diagnostic:)])
42 |
43 | guard let cwd = localFileSystem.currentWorkingDirectory else {
44 | diagnostics.emit(.error("could not determine current working directory"))
45 | throw ExitCode.failure
46 | }
47 |
48 | var root = cwd
49 | while !localFileSystem.isFile(root.appending(component: Manifest.filename)) {
50 | root = root.parentDirectory
51 | guard !root.isRoot else {
52 | diagnostics.emit(.error("could not find package manifest"))
53 | throw ExitCode.failure
54 | }
55 | }
56 | packageRoot = root
57 |
58 | toolchain = try UserToolchain(destination: Destination.hostDestination(originalWorkingDirectory: cwd))
59 |
60 | let manifestPath = try Manifest.path(atPackagePath: packageRoot,
61 | fileSystem: localFileSystem)
62 | let repositoryManager = RepositoryManager(path: packageRoot.appending(component: ".build"),
63 | provider: GitRepositoryProvider())
64 | packageEditor = try PackageEditor(manifestPath: manifestPath,
65 | repositoryManager: repositoryManager,
66 | toolchain: toolchain,
67 | diagnosticsEngine: diagnostics)
68 | }
69 |
70 | func loadPackageGraph() throws -> PackageGraph {
71 | if let cachedPackageGraph = cachedPackageGraph {
72 | return cachedPackageGraph
73 | }
74 | let graph = try Workspace.loadRootGraph(at: packageRoot,
75 | swiftCompiler: toolchain.swiftCompiler,
76 | swiftCompilerFlags: [],
77 | diagnostics: diagnostics)
78 | guard !diagnostics.hasErrors else {
79 | throw ExitCode.failure
80 | }
81 | cachedPackageGraph = graph
82 | return graph
83 | }
84 | }
85 |
86 | protocol EditorCommand: ParsableCommand {
87 | func run(_ editorTool: EditorTool) throws
88 | }
89 |
90 | extension EditorCommand {
91 | public func run() throws {
92 | let editorTool = try EditorTool()
93 | try self.run(editorTool)
94 | if editorTool.diagnostics.hasErrors {
95 | throw ExitCode.failure
96 | }
97 | }
98 | }
99 |
100 | struct AddDependency: EditorCommand {
101 | static let configuration = CommandConfiguration(
102 | abstract: "Add a dependency to the current package.")
103 |
104 | @Argument(help: "The URL of a remote package, or the path to a local package")
105 | var dependencyURL: String
106 |
107 | @Option(help: "Specifies an exact package version requirement")
108 | var exact: Version?
109 |
110 | @Option(help: "Specifies a package revision requirement")
111 | var revision: String?
112 |
113 | @Option(help: "Specifies a package branch requirement")
114 | var branch: String?
115 |
116 | @Option(help: "Specifies a package version requirement from the specified version up to the next major version")
117 | var from: Version?
118 |
119 | @Option(help: "Specifies a package version requirement from the specified version up to the next minor version")
120 | var upToNextMinorFrom: Version?
121 |
122 | @Option(help: "Specifies the upper bound of a range-based package version requirement")
123 | var to: Version?
124 |
125 | @Option(help: "Specifies the upper bound of a closed range-based package version requirement")
126 | var through: Version?
127 |
128 | func run(_ editorTool: EditorTool) throws {
129 | var requirements: [PackageDependencyRequirement] = []
130 | if let exactVersion = exact {
131 | requirements.append(.exact(exactVersion.description))
132 | }
133 | if let revision = revision {
134 | requirements.append(.revision(revision))
135 | }
136 | if let branch = branch {
137 | requirements.append(.branch(branch))
138 | }
139 | if let version = from {
140 | requirements.append(.upToNextMajor(version.description))
141 | }
142 | if let version = upToNextMinorFrom {
143 | requirements.append(.upToNextMinor(version.description))
144 | }
145 |
146 | guard requirements.count <= 1 else {
147 | editorTool.diagnostics.emit(.error("only one requirement is allowed when specifiying a dependency"))
148 | throw ExitCode.failure
149 | }
150 |
151 | var requirement = requirements.first
152 |
153 | if case .upToNextMajor(let rangeStart) = requirement {
154 | guard to == nil || through == nil else {
155 | editorTool.diagnostics.emit(.error("'--to' and '--through' may not be used in the same requirement"))
156 | throw ExitCode.failure
157 | }
158 | if let rangeEnd = to {
159 | requirement = .range(rangeStart.description, rangeEnd.description)
160 | } else if let closedRangeEnd = through {
161 | requirement = .closedRange(rangeStart.description, closedRangeEnd.description)
162 | }
163 | } else {
164 | guard to == nil, through == nil else {
165 | editorTool.diagnostics.emit(.error("'--to' and '--through' may only be used with '--from' to specify a range requirement"))
166 | throw ExitCode.failure
167 | }
168 | }
169 |
170 | do {
171 | try editorTool.packageEditor.addPackageDependency(url: dependencyURL, requirement: requirement)
172 | } catch Diagnostics.fatalError {
173 | throw ExitCode.failure
174 | }
175 | }
176 | }
177 |
178 | struct AddTarget: EditorCommand {
179 | static let configuration = CommandConfiguration(
180 | abstract: "Add a target to the current package.")
181 |
182 | @Argument(help: "The name of the new target")
183 | var name: String
184 |
185 | @Option(help: "The type of the new target (library, executable, test, or binary)")
186 | var type: String = "library"
187 |
188 | @Flag(help: "If present, no corresponding test target will be created for a new library target")
189 | var noTestTarget: Bool = false
190 |
191 | @Option(parsing: .upToNextOption,
192 | help: "A list of target dependency names (targets and/or dependency products)")
193 | var dependencies: [String] = []
194 |
195 | @Option(help: "The URL for a remote binary target")
196 | var url: String?
197 |
198 | @Option(help: "The checksum for a remote binary target")
199 | var checksum: String?
200 |
201 | @Option(help: "The path for a local binary target")
202 | var path: String?
203 |
204 | func run(_ editorTool: EditorTool) throws {
205 | let newTarget: NewTarget
206 | switch type {
207 | case "library":
208 | try verifyNoTargetBinaryOptionsPassed(diagnostics: editorTool.diagnostics)
209 | newTarget = .library(name: name,
210 | includeTestTarget: !noTestTarget,
211 | dependencyNames: dependencies)
212 | case "executable":
213 | try verifyNoTargetBinaryOptionsPassed(diagnostics: editorTool.diagnostics)
214 | newTarget = .executable(name: name,
215 | dependencyNames: dependencies)
216 | case "test":
217 | try verifyNoTargetBinaryOptionsPassed(diagnostics: editorTool.diagnostics)
218 | newTarget = .test(name: name,
219 | dependencyNames: dependencies)
220 | case "binary":
221 | guard dependencies.isEmpty else {
222 | editorTool.diagnostics.emit(.error("option '--dependencies' is not supported for binary targets"))
223 | throw ExitCode.failure
224 | }
225 | // This check is somewhat forgiving, and does the right thing if
226 | // the user passes a url with --path or a path with --url.
227 | guard let urlOrPath = url ?? path, url == nil || path == nil else {
228 | editorTool.diagnostics.emit(.error("binary targets must specify either a path or both a URL and a checksum"))
229 | throw ExitCode.failure
230 | }
231 | newTarget = .binary(name: name,
232 | urlOrPath: urlOrPath,
233 | checksum: checksum)
234 | default:
235 | editorTool.diagnostics.emit(.error("unsupported target type '\(type)'; supported types are library, executable, test, and binary"))
236 | throw ExitCode.failure
237 | }
238 |
239 | do {
240 | let mapping = try createProductPackageNameMapping(packageGraph: editorTool.loadPackageGraph())
241 | try editorTool.packageEditor.addTarget(newTarget, productPackageNameMapping: mapping)
242 | } catch Diagnostics.fatalError {
243 | throw ExitCode.failure
244 | }
245 | }
246 |
247 | private func createProductPackageNameMapping(packageGraph: PackageGraph) throws -> [String: String] {
248 | var productPackageNameMapping: [String: String] = [:]
249 | for dependencyPackage in packageGraph.rootPackages.flatMap(\.dependencies) {
250 | for product in dependencyPackage.products {
251 | productPackageNameMapping[product.name] = dependencyPackage.manifestName
252 | }
253 | }
254 | return productPackageNameMapping
255 | }
256 |
257 | private func verifyNoTargetBinaryOptionsPassed(diagnostics: DiagnosticsEngine) throws {
258 | guard url == nil else {
259 | diagnostics.emit(.error("option '--url' is only supported for binary targets"))
260 | throw ExitCode.failure
261 | }
262 | guard path == nil else {
263 | diagnostics.emit(.error("option '--path' is only supported for binary targets"))
264 | throw ExitCode.failure
265 | }
266 | guard checksum == nil else {
267 | diagnostics.emit(.error("option '--checksum' is only supported for binary targets"))
268 | throw ExitCode.failure
269 | }
270 | }
271 | }
272 |
273 | struct AddProduct: EditorCommand {
274 | static let configuration = CommandConfiguration(
275 | abstract: "Add a product to the current package.")
276 |
277 | @Argument(help: "The name of the new product")
278 | var name: String
279 |
280 | @Option(help: "The type of the new product (library, static-library, dynamic-library, or executable)")
281 | var type: ProductType?
282 |
283 | @Option(parsing: .upToNextOption,
284 | help: "A list of target names to add to the new product")
285 | var targets: [String]
286 |
287 | func run(_ editorTool: EditorTool) throws {
288 | do {
289 | try editorTool.packageEditor.addProduct(name: name, type: type ?? .library(.automatic), targets: targets)
290 | } catch Diagnostics.fatalError {
291 | throw ExitCode.failure
292 | }
293 | }
294 | }
295 |
296 | extension Version: ExpressibleByArgument {
297 | public init?(argument: String) {
298 | self.init(string: argument)
299 | }
300 | }
301 |
302 | extension ProductType: ExpressibleByArgument {
303 | public init?(argument: String) {
304 | switch argument {
305 | case "library":
306 | self = .library(.automatic)
307 | case "static-library":
308 | self = .library(.static)
309 | case "dynamic-library":
310 | self = .library(.dynamic)
311 | case "executable":
312 | self = .executable
313 | default:
314 | return nil
315 | }
316 | }
317 |
318 | public static var defaultCompletionKind: CompletionKind {
319 | .list(["library", "static-library", "dynamic-library", "executable"])
320 | }
321 | }
322 |
323 | func print(diagnostic: Diagnostic) {
324 | if !(diagnostic.location is UnknownLocation) {
325 | stderrStream <<< diagnostic.location.description <<< ": "
326 | }
327 | switch diagnostic.message.behavior {
328 | case .error:
329 | stderrStream <<< "error: "
330 | case .warning:
331 | stderrStream <<< "warning: "
332 | case .note:
333 | stderrStream <<< "note"
334 | case .remark:
335 | stderrStream <<< "remark: "
336 | case .ignored:
337 | break
338 | }
339 | stderrStream <<< diagnostic.description <<< "\n"
340 | stderrStream.flush()
341 | }
342 |
--------------------------------------------------------------------------------
/Tests/IntegrationTests/Fixtures/Empty/LocalBinary.xcframework/contents:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Tests/IntegrationTests/Fixtures/Empty/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "MyPackage"
6 | )
--------------------------------------------------------------------------------
/Tests/IntegrationTests/Fixtures/OneProduct/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "MyPackage2",
6 | products: [
7 | .library(name: "Library", targets: ["Library"])
8 | ],
9 | targets: [
10 | .target(name: "Library")
11 | ]
12 | )
13 |
--------------------------------------------------------------------------------
/Tests/IntegrationTests/Fixtures/OneProduct/Sources/Library/Library.swift:
--------------------------------------------------------------------------------
1 | let x = 42
2 |
--------------------------------------------------------------------------------
/Tests/IntegrationTests/IntegrationTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import Foundation
12 | import TSCTestSupport
13 | import TSCBasic
14 | import XCTest
15 |
16 | enum SwiftPackageEditor: TSCTestSupport.Product {
17 | case executable
18 |
19 | var exec: RelativePath {
20 | RelativePath("swift-package-editor")
21 | }
22 | }
23 |
24 | final class IntegrationTests: XCTestCase {
25 |
26 | func execute(_ args: [String]) throws {
27 | let env = ["SWIFTPM_MODULECACHE_OVERRIDE": ProcessInfo.processInfo.environment["SWIFTPM_MODULECACHE_OVERRIDE"] ??
28 | SwiftPackageEditor.executable.path.parentDirectory.appending(component: ".integration-test-module-cache")
29 | .pathString]
30 | try SwiftPackageEditor.executable.execute(args, env: env)
31 | }
32 |
33 | func assertFailure(args: String..., stderr: String) {
34 | do {
35 | try execute(args)
36 | XCTFail()
37 | } catch SwiftPMProductError.executionFailure(_, _, let stderrOutput) {
38 | XCTAssertEqual(stderrOutput, stderr)
39 | } catch {
40 | XCTFail("unexpected error: \(error)")
41 | }
42 | }
43 |
44 | func withFixture(named name: String, _ block: (AbsolutePath) throws -> Void) throws {
45 | let relativePath = RelativePath("Fixtures").appending(component: name)
46 | let fixturePath = AbsolutePath(Bundle.module.url(forResource: relativePath.pathString, withExtension: nil)!.path)
47 | try withTemporaryDirectory { tmpDir in
48 | let destPath = tmpDir.appending(component: name)
49 | try localFileSystem.copy(from: fixturePath, to: destPath)
50 | try block(destPath)
51 | }
52 | }
53 |
54 | func testAddDependencyArgValidation() throws {
55 | assertFailure(args: "add-dependency", "http://www.githost.com/repo.git", "--exact", "1.0.0", "--from", "1.0.0",
56 | stderr: "error: only one requirement is allowed when specifiying a dependency\n")
57 | assertFailure(args: "add-dependency", "http://www.githost.com/repo.git", "--exact", "1.0.0", "--to", "2.0.0",
58 | stderr: "error: '--to' and '--through' may only be used with '--from' to specify a range requirement\n")
59 | assertFailure(args: "add-dependency", "http://www.githost.com/repo.git", "--from", "1.0.0", "--to", "2.0.0", "--through", "3.0.0",
60 | stderr: "error: '--to' and '--through' may not be used in the same requirement\n")
61 | }
62 |
63 | func testAddTargetArgValidation() throws {
64 | assertFailure(args: "add-target", "MyLibrary", "--type", "binary",
65 | stderr: "error: binary targets must specify either a path or both a URL and a checksum\n")
66 | assertFailure(args: "add-target", "MyLibrary", "--checksum", "checksum",
67 | stderr: "error: option '--checksum' is only supported for binary targets\n")
68 | assertFailure(args: "add-target", "MyLibrary", "--type", "binary", "--dependencies", "MyLibrary",
69 | stderr: "error: option '--dependencies' is not supported for binary targets\n")
70 | assertFailure(args: "add-target", "MyLibrary", "--type", "unsupported",
71 | stderr: "error: unsupported target type 'unsupported'; supported types are library, executable, test, and binary\n")
72 | }
73 |
74 | func testAddDependencyEndToEnd() throws {
75 | try withFixture(named: "Empty") { emptyPath in
76 | try withFixture(named: "OneProduct") { oneProductPath in
77 | try localFileSystem.changeCurrentWorkingDirectory(to: emptyPath)
78 | try execute(["add-dependency", oneProductPath.pathString])
79 | let newManifest = try localFileSystem.readFileContents(emptyPath.appending(component: "Package.swift")).validDescription
80 | XCTAssertEqual(newManifest, """
81 | // swift-tools-version:5.3
82 | import PackageDescription
83 |
84 | let package = Package(
85 | name: "MyPackage",
86 | dependencies: [
87 | .package(name: "MyPackage2", path: "\(oneProductPath.pathString)"),
88 | ]
89 | )
90 | """)
91 | assertFailure(args: "add-dependency", oneProductPath.pathString,
92 | stderr: "error: 'MyPackage' already has a dependency on '\(oneProductPath.pathString)'\n")
93 | }
94 | }
95 | }
96 |
97 | func testAddTargetEndToEnd() throws {
98 | try withFixture(named: "Empty") { emptyPath in
99 | try withFixture(named: "OneProduct") { oneProductPath in
100 | try localFileSystem.changeCurrentWorkingDirectory(to: emptyPath)
101 | try execute(["add-dependency", oneProductPath.pathString])
102 | try execute(["add-target", "MyLibrary", "--dependencies", "Library"])
103 | try execute(["add-target", "MyExecutable", "--type", "executable",
104 | "--dependencies", "MyLibrary"])
105 | try execute(["add-target", "--type", "test", "IntegrationTests",
106 | "--dependencies", "MyLibrary"])
107 | let newManifest = try localFileSystem.readFileContents(emptyPath.appending(component: "Package.swift")).validDescription
108 | XCTAssertEqual(newManifest, """
109 | // swift-tools-version:5.3
110 | import PackageDescription
111 |
112 | let package = Package(
113 | name: "MyPackage",
114 | dependencies: [
115 | .package(name: "MyPackage2", path: "\(oneProductPath.pathString)"),
116 | ],
117 | targets: [
118 | .target(
119 | name: "MyLibrary",
120 | dependencies: [
121 | .product(name: "Library", package: "MyPackage2"),
122 | ]
123 | ),
124 | .testTarget(
125 | name: "MyLibraryTests",
126 | dependencies: [
127 | "MyLibrary",
128 | ]
129 | ),
130 | .target(
131 | name: "MyExecutable",
132 | dependencies: [
133 | "MyLibrary",
134 | ]
135 | ),
136 | .testTarget(
137 | name: "IntegrationTests",
138 | dependencies: [
139 | "MyLibrary",
140 | ]
141 | ),
142 | ]
143 | )
144 | """)
145 | XCTAssertTrue(localFileSystem.exists(emptyPath.appending(components: "Sources", "MyLibrary", "MyLibrary.swift")))
146 | XCTAssertTrue(localFileSystem.exists(emptyPath.appending(components: "Tests", "MyLibraryTests", "MyLibraryTests.swift")))
147 | XCTAssertTrue(localFileSystem.exists(emptyPath.appending(components: "Sources", "MyExecutable", "main.swift")))
148 | XCTAssertTrue(localFileSystem.exists(emptyPath.appending(components: "Tests", "IntegrationTests", "IntegrationTests.swift")))
149 | assertFailure(args: "add-target", "MyLibrary",
150 | stderr: "error: a target named 'MyLibrary' already exists in 'MyPackage'\n")
151 | }
152 | }
153 | }
154 |
155 | func testAddProductEndToEnd() throws {
156 | try withFixture(named: "Empty") { emptyPath in
157 | try localFileSystem.changeCurrentWorkingDirectory(to: emptyPath)
158 | try execute(["add-target", "MyLibrary", "--no-test-target"])
159 | try execute(["add-target", "MyLibrary2", "--no-test-target"])
160 | try execute(["add-product", "LibraryProduct",
161 | "--targets", "MyLibrary", "MyLibrary2"])
162 | try execute(["add-product", "DynamicLibraryProduct",
163 | "--type", "dynamic-library",
164 | "--targets", "MyLibrary"])
165 | try execute(["add-product", "StaticLibraryProduct",
166 | "--type", "static-library",
167 | "--targets", "MyLibrary"])
168 | try execute(["add-product", "ExecutableProduct",
169 | "--type", "executable",
170 | "--targets", "MyLibrary2"])
171 | let newManifest = try localFileSystem.readFileContents(emptyPath.appending(component: "Package.swift")).validDescription
172 | XCTAssertEqual(newManifest, """
173 | // swift-tools-version:5.3
174 | import PackageDescription
175 |
176 | let package = Package(
177 | name: "MyPackage",
178 | products: [
179 | .library(
180 | name: "LibraryProduct",
181 | targets: [
182 | "MyLibrary",
183 | "MyLibrary2",
184 | ]
185 | ),
186 | .library(
187 | name: "DynamicLibraryProduct",
188 | type: .dynamic,
189 | targets: [
190 | "MyLibrary",
191 | ]
192 | ),
193 | .library(
194 | name: "StaticLibraryProduct",
195 | type: .static,
196 | targets: [
197 | "MyLibrary",
198 | ]
199 | ),
200 | .executable(
201 | name: "ExecutableProduct",
202 | targets: [
203 | "MyLibrary2",
204 | ]
205 | ),
206 | ],
207 | targets: [
208 | .target(
209 | name: "MyLibrary",
210 | dependencies: []
211 | ),
212 | .target(
213 | name: "MyLibrary2",
214 | dependencies: []
215 | ),
216 | ]
217 | )
218 | """)
219 | assertFailure(args: "add-product", "LibraryProduct", "--targets", "MyLibrary,MyLibrary2",
220 | stderr: "error: a product named 'LibraryProduct' already exists in 'MyPackage'\n")
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/Tests/PackageSyntaxTests/AddPackageDependencyTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import XCTest
12 |
13 | import PackageSyntax
14 |
15 | final class AddPackageDependencyTests: XCTestCase {
16 | func testAddPackageDependency() throws {
17 | let manifest = """
18 | // swift-tools-version:5.2
19 | import PackageDescription
20 |
21 | let package = Package(
22 | name: "exec",
23 | dependencies: [
24 | ],
25 | targets: [
26 | .target(
27 | name: "exec",
28 | dependencies: []),
29 | .testTarget(
30 | name: "execTests",
31 | dependencies: ["exec"]),
32 | ]
33 | )
34 | """
35 |
36 |
37 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
38 | try editor.addPackageDependency(
39 | name: "goo",
40 | url: "https://github.com/foo/goo",
41 | requirement: .upToNextMajor("1.0.1"),
42 | branchAndRevisionConvenienceMethodsSupported: false
43 | )
44 |
45 | XCTAssertEqual(editor.editedManifest, """
46 | // swift-tools-version:5.2
47 | import PackageDescription
48 |
49 | let package = Package(
50 | name: "exec",
51 | dependencies: [
52 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
53 | ],
54 | targets: [
55 | .target(
56 | name: "exec",
57 | dependencies: []),
58 | .testTarget(
59 | name: "execTests",
60 | dependencies: ["exec"]),
61 | ]
62 | )
63 | """)
64 | }
65 |
66 | func testAddPackageDependency2() throws {
67 | let manifest = """
68 | let package = Package(
69 | name: "exec",
70 | dependencies: [],
71 | targets: [
72 | .target(name: "exec"),
73 | ]
74 | )
75 | """
76 |
77 |
78 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
79 | try editor.addPackageDependency(
80 | name: "goo",
81 | url: "https://github.com/foo/goo",
82 | requirement: .upToNextMajor("1.0.1"),
83 | branchAndRevisionConvenienceMethodsSupported: false
84 | )
85 |
86 | XCTAssertEqual(editor.editedManifest, """
87 | let package = Package(
88 | name: "exec",
89 | dependencies: [
90 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
91 | ],
92 | targets: [
93 | .target(name: "exec"),
94 | ]
95 | )
96 | """)
97 | }
98 |
99 | func testAddPackageDependency3() throws {
100 | let manifest = """
101 | let package = Package(
102 | name: "exec",
103 | dependencies: [
104 | // Here is a comment.
105 | .package(url: "https://github.com/foo/bar", .branch("master")),
106 | ],
107 | targets: [
108 | .target(name: "exec"),
109 | ]
110 | )
111 | """
112 |
113 |
114 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
115 | try editor.addPackageDependency(
116 | name: "goo",
117 | url: "https://github.com/foo/goo",
118 | requirement: .upToNextMajor("1.0.1"),
119 | branchAndRevisionConvenienceMethodsSupported: false
120 | )
121 |
122 | // FIXME: preserve comment
123 | XCTAssertEqual(editor.editedManifest, """
124 | let package = Package(
125 | name: "exec",
126 | dependencies: [
127 | .package(url: "https://github.com/foo/bar", .branch("master")),
128 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
129 | ],
130 | targets: [
131 | .target(name: "exec"),
132 | ]
133 | )
134 | """)
135 | }
136 |
137 | func testAddPackageDependency4() throws {
138 | let manifest = """
139 | let package = Package(
140 | name: "exec",
141 | targets: [
142 | .target(name: "exec"),
143 | ]
144 | )
145 | """
146 |
147 |
148 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
149 | try editor.addPackageDependency(
150 | name: "goo",
151 | url: "https://github.com/foo/goo",
152 | requirement: .upToNextMajor("1.0.1"),
153 | branchAndRevisionConvenienceMethodsSupported: false
154 | )
155 |
156 | XCTAssertEqual(editor.editedManifest, """
157 | let package = Package(
158 | name: "exec",
159 | dependencies: [
160 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
161 | ],
162 | targets: [
163 | .target(name: "exec"),
164 | ]
165 | )
166 | """)
167 | }
168 |
169 | func testAddPackageDependency5() throws {
170 | // FIXME: This is broken, we end up removing the comment.
171 | let manifest = """
172 | let package = Package(
173 | name: "exec",
174 | dependencies: [
175 | // Here is a comment.
176 | ],
177 | targets: [
178 | .target(name: "exec"),
179 | ]
180 | )
181 | """
182 |
183 |
184 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
185 | try editor.addPackageDependency(
186 | name: "goo",
187 | url: "https://github.com/foo/goo",
188 | requirement: .upToNextMajor("1.0.1"),
189 | branchAndRevisionConvenienceMethodsSupported: false
190 | )
191 |
192 | XCTAssertEqual(editor.editedManifest, """
193 | let package = Package(
194 | name: "exec",
195 | dependencies: [
196 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
197 | ],
198 | targets: [
199 | .target(name: "exec"),
200 | ]
201 | )
202 | """)
203 | }
204 |
205 | func testAddPackageDependency6() throws {
206 | let manifest = """
207 | let myDeps = [
208 | .package(url: "https://github.com/foo/foo", from: "1.0.2"),
209 | ]
210 |
211 | let package = Package(
212 | name: "exec",
213 | dependencies: myDeps + [
214 | .package(url: "https://github.com/foo/bar", from: "1.0.3"),
215 | ],
216 | targets: [
217 | .target(name: "exec"),
218 | ]
219 | )
220 | """
221 |
222 |
223 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
224 | try editor.addPackageDependency(
225 | name: "goo",
226 | url: "https://github.com/foo/goo",
227 | requirement: .upToNextMajor("1.0.1"),
228 | branchAndRevisionConvenienceMethodsSupported: false
229 | )
230 |
231 | XCTAssertEqual(editor.editedManifest, """
232 | let myDeps = [
233 | .package(url: "https://github.com/foo/foo", from: "1.0.2"),
234 | ]
235 |
236 | let package = Package(
237 | name: "exec",
238 | dependencies: myDeps + [
239 | .package(url: "https://github.com/foo/bar", from: "1.0.3"),
240 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
241 | ],
242 | targets: [
243 | .target(name: "exec"),
244 | ]
245 | )
246 | """)
247 | }
248 |
249 | func testAddPackageDependency7() throws {
250 | let manifest = """
251 | let package = Package(
252 | name: "exec",
253 | dependencies: [
254 | .package(url: "https://github.com/foo/bar", from: "1.0.3")
255 | ],
256 | targets: [
257 | .target(name: "exec")
258 | ]
259 | )
260 | """
261 |
262 |
263 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
264 | try editor.addPackageDependency(
265 | name: "goo",
266 | url: "https://github.com/foo/goo",
267 | requirement: .upToNextMajor("1.0.1"),
268 | branchAndRevisionConvenienceMethodsSupported: false
269 | )
270 |
271 | XCTAssertEqual(editor.editedManifest, """
272 | let package = Package(
273 | name: "exec",
274 | dependencies: [
275 | .package(url: "https://github.com/foo/bar", from: "1.0.3"),
276 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
277 | ],
278 | targets: [
279 | .target(name: "exec")
280 | ]
281 | )
282 | """)
283 | }
284 |
285 | func testAddPackageDependency8() throws {
286 | let manifest = """
287 | let package = Package(
288 | name: "exec",
289 | platforms: [.iOS],
290 | targets: [
291 | .target(name: "exec"),
292 | ]
293 | )
294 | """
295 |
296 |
297 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
298 | try editor.addPackageDependency(
299 | name: "goo",
300 | url: "https://github.com/foo/goo",
301 | requirement: .upToNextMajor("1.0.1"),
302 | branchAndRevisionConvenienceMethodsSupported: false
303 | )
304 |
305 | XCTAssertEqual(editor.editedManifest, """
306 | let package = Package(
307 | name: "exec",
308 | platforms: [.iOS],
309 | dependencies: [
310 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
311 | ],
312 | targets: [
313 | .target(name: "exec"),
314 | ]
315 | )
316 | """)
317 | }
318 |
319 | func testAddPackageDependency9() throws {
320 | let manifest = """
321 | let package = Package(
322 | name: "exec",
323 | platforms: [.iOS],
324 | swiftLanguageVersions: []
325 | )
326 | """
327 |
328 |
329 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
330 | try editor.addPackageDependency(
331 | name: "goo",
332 | url: "https://github.com/foo/goo",
333 | requirement: .upToNextMajor("1.0.1"),
334 | branchAndRevisionConvenienceMethodsSupported: false
335 | )
336 |
337 | XCTAssertEqual(editor.editedManifest, """
338 | let package = Package(
339 | name: "exec",
340 | platforms: [.iOS],
341 | dependencies: [
342 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
343 | ],
344 | swiftLanguageVersions: []
345 | )
346 | """)
347 | }
348 |
349 | func testAddPackageDependency10() throws {
350 | let manifest = """
351 | let package = Package(
352 | name: "exec",
353 | platforms: [.iOS],
354 | )
355 | """
356 |
357 |
358 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
359 | try editor.addPackageDependency(
360 | name: "goo",
361 | url: "https://github.com/foo/goo",
362 | requirement: .upToNextMajor("1.0.1"),
363 | branchAndRevisionConvenienceMethodsSupported: false
364 | )
365 |
366 | XCTAssertEqual(editor.editedManifest, """
367 | let package = Package(
368 | name: "exec",
369 | platforms: [.iOS],
370 | dependencies: [
371 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")),
372 | ]
373 | )
374 | """)
375 | }
376 |
377 | func testAddPackageDependencyWithExactRequirement() throws {
378 | let manifest = """
379 | let package = Package(
380 | name: "exec",
381 | platforms: [.iOS],
382 | )
383 | """
384 |
385 |
386 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
387 | try editor.addPackageDependency(
388 | name: "goo",
389 | url: "https://github.com/foo/goo",
390 | requirement: .exact("2.0.2"),
391 | branchAndRevisionConvenienceMethodsSupported: false
392 | )
393 |
394 | XCTAssertEqual(editor.editedManifest, """
395 | let package = Package(
396 | name: "exec",
397 | platforms: [.iOS],
398 | dependencies: [
399 | .package(name: "goo", url: "https://github.com/foo/goo", .exact("2.0.2")),
400 | ]
401 | )
402 | """)
403 | }
404 |
405 | func testAddPackageDependencyWithBranchRequirement() throws {
406 | let manifest = """
407 | let package = Package(
408 | name: "exec",
409 | platforms: [.iOS],
410 | )
411 | """
412 |
413 |
414 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
415 | try editor.addPackageDependency(
416 | name: "goo",
417 | url: "https://github.com/foo/goo",
418 | requirement: .branch("main"),
419 | branchAndRevisionConvenienceMethodsSupported: false
420 | )
421 |
422 | XCTAssertEqual(editor.editedManifest, """
423 | let package = Package(
424 | name: "exec",
425 | platforms: [.iOS],
426 | dependencies: [
427 | .package(name: "goo", url: "https://github.com/foo/goo", .branch("main")),
428 | ]
429 | )
430 | """)
431 | }
432 |
433 | func testAddPackageDependencyWithRevisionRequirement() throws {
434 | let manifest = """
435 | let package = Package(
436 | name: "exec",
437 | platforms: [.iOS],
438 | )
439 | """
440 |
441 |
442 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
443 | try editor.addPackageDependency(
444 | name: "goo",
445 | url: "https://github.com/foo/goo",
446 | requirement: .revision("abcde"),
447 | branchAndRevisionConvenienceMethodsSupported: false
448 | )
449 |
450 | XCTAssertEqual(editor.editedManifest, """
451 | let package = Package(
452 | name: "exec",
453 | platforms: [.iOS],
454 | dependencies: [
455 | .package(name: "goo", url: "https://github.com/foo/goo", .revision("abcde")),
456 | ]
457 | )
458 | """)
459 | }
460 |
461 | func testAddPackageDependencyWithBranchRequirementUsingConvenienceMethods() throws {
462 | let manifest = """
463 | let package = Package(
464 | name: "exec",
465 | platforms: [.iOS],
466 | )
467 | """
468 |
469 |
470 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
471 | try editor.addPackageDependency(
472 | name: "goo",
473 | url: "https://github.com/foo/goo",
474 | requirement: .branch("main"),
475 | branchAndRevisionConvenienceMethodsSupported: true
476 | )
477 |
478 | XCTAssertEqual(editor.editedManifest, """
479 | let package = Package(
480 | name: "exec",
481 | platforms: [.iOS],
482 | dependencies: [
483 | .package(name: "goo", url: "https://github.com/foo/goo", branch: "main"),
484 | ]
485 | )
486 | """)
487 | }
488 |
489 | func testAddPackageDependencyWithRevisionRequirementUsingConvenienceMethods() throws {
490 | let manifest = """
491 | let package = Package(
492 | name: "exec",
493 | platforms: [.iOS],
494 | )
495 | """
496 |
497 |
498 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
499 | try editor.addPackageDependency(
500 | name: "goo",
501 | url: "https://github.com/foo/goo",
502 | requirement: .revision("abcde"),
503 | branchAndRevisionConvenienceMethodsSupported: true
504 | )
505 |
506 | XCTAssertEqual(editor.editedManifest, """
507 | let package = Package(
508 | name: "exec",
509 | platforms: [.iOS],
510 | dependencies: [
511 | .package(name: "goo", url: "https://github.com/foo/goo", revision: "abcde"),
512 | ]
513 | )
514 | """)
515 | }
516 |
517 | func testAddPackageDependencyWithUpToNextMinorRequirement() throws {
518 | let manifest = """
519 | let package = Package(
520 | name: "exec",
521 | platforms: [.iOS],
522 | )
523 | """
524 |
525 |
526 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
527 | try editor.addPackageDependency(
528 | name: "goo",
529 | url: "https://github.com/foo/goo",
530 | requirement: .upToNextMinor("1.1.1"),
531 | branchAndRevisionConvenienceMethodsSupported: false
532 | )
533 |
534 | XCTAssertEqual(editor.editedManifest, """
535 | let package = Package(
536 | name: "exec",
537 | platforms: [.iOS],
538 | dependencies: [
539 | .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMinor(from: "1.1.1")),
540 | ]
541 | )
542 | """)
543 | }
544 |
545 | func testAddPackageDependenciesWithRangeRequirements() throws {
546 | let manifest = """
547 | let package = Package(
548 | name: "exec",
549 | platforms: [.iOS],
550 | )
551 | """
552 |
553 |
554 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
555 | try editor.addPackageDependency(
556 | name: "goo",
557 | url: "https://github.com/foo/goo",
558 | requirement: .range("1.1.1", "2.2.2"),
559 | branchAndRevisionConvenienceMethodsSupported: false
560 | )
561 | try editor.addPackageDependency(
562 | name: "goo",
563 | url: "https://github.com/foo/goo",
564 | requirement: .closedRange("2.2.2", "3.3.3"),
565 | branchAndRevisionConvenienceMethodsSupported: false
566 | )
567 |
568 | XCTAssertEqual(editor.editedManifest, """
569 | let package = Package(
570 | name: "exec",
571 | platforms: [.iOS],
572 | dependencies: [
573 | .package(name: "goo", url: "https://github.com/foo/goo", "1.1.1"..<"2.2.2"),
574 | .package(name: "goo", url: "https://github.com/foo/goo", "2.2.2"..."3.3.3"),
575 | ]
576 | )
577 | """)
578 | }
579 | }
580 |
--------------------------------------------------------------------------------
/Tests/PackageSyntaxTests/AddProductTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import XCTest
12 |
13 | import PackageSyntax
14 |
15 | final class AddProductTests: XCTestCase {
16 | func testAddProduct() throws {
17 | let manifest = """
18 | // swift-tools-version:5.2
19 | import PackageDescription
20 |
21 | let package = Package(
22 | name: "exec",
23 | products: [
24 | .executable(name: "abc", targets: ["foo"]),
25 | ]
26 | targets: [
27 | .target(
28 | name: "foo",
29 | dependencies: []),
30 | .target(
31 | name: "bar",
32 | dependencies: []),
33 | .testTarget(
34 | name: "fooTests",
35 | dependencies: ["foo", "bar"]),
36 | ]
37 | )
38 | """
39 |
40 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
41 | try editor.addProduct(name: "exec", type: .executable)
42 | try editor.addProduct(name: "lib", type: .library(.automatic))
43 | try editor.addProduct(name: "staticLib", type: .library(.static))
44 | try editor.addProduct(name: "dynamicLib", type: .library(.dynamic))
45 |
46 | XCTAssertEqual(editor.editedManifest, """
47 | // swift-tools-version:5.2
48 | import PackageDescription
49 |
50 | let package = Package(
51 | name: "exec",
52 | products: [
53 | .executable(name: "abc", targets: ["foo"]),
54 | .executable(
55 | name: "exec",
56 | targets: []
57 | ),
58 | .library(
59 | name: "lib",
60 | targets: []
61 | ),
62 | .library(
63 | name: "staticLib",
64 | type: .static,
65 | targets: []
66 | ),
67 | .library(
68 | name: "dynamicLib",
69 | type: .dynamic,
70 | targets: []
71 | ),
72 | ]
73 | targets: [
74 | .target(
75 | name: "foo",
76 | dependencies: []),
77 | .target(
78 | name: "bar",
79 | dependencies: []),
80 | .testTarget(
81 | name: "fooTests",
82 | dependencies: ["foo", "bar"]),
83 | ]
84 | )
85 | """)
86 | }
87 |
88 | func testAddProduct2() throws {
89 | let manifest = """
90 | // swift-tools-version:5.2
91 | import PackageDescription
92 |
93 | let package = Package(
94 | name: "exec",
95 | targets: [
96 | .target(
97 | name: "foo",
98 | dependencies: []),
99 | .target(
100 | name: "bar",
101 | dependencies: []),
102 | .testTarget(
103 | name: "fooTests",
104 | dependencies: ["foo", "bar"]),
105 | ]
106 | )
107 | """
108 |
109 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
110 | try editor.addProduct(name: "exec", type: .executable)
111 | try editor.addProduct(name: "lib", type: .library(.automatic))
112 | try editor.addProduct(name: "staticLib", type: .library(.static))
113 | try editor.addProduct(name: "dynamicLib", type: .library(.dynamic))
114 |
115 | // FIXME: weird indentation
116 | XCTAssertEqual(editor.editedManifest, """
117 | // swift-tools-version:5.2
118 | import PackageDescription
119 |
120 | let package = Package(
121 | name: "exec",
122 | products: [
123 | .executable(
124 | name: "exec",
125 | targets: []
126 | ),
127 | .library(
128 | name: "lib",
129 | targets: []
130 | ),
131 | .library(
132 | name: "staticLib",
133 | type: .static,
134 | targets: []
135 | ),
136 | .library(
137 | name: "dynamicLib",
138 | type: .dynamic,
139 | targets: []
140 | ),
141 | ],
142 | targets: [
143 | .target(
144 | name: "foo",
145 | dependencies: []),
146 | .target(
147 | name: "bar",
148 | dependencies: []),
149 | .testTarget(
150 | name: "fooTests",
151 | dependencies: ["foo", "bar"]),
152 | ]
153 | )
154 | """)
155 | }
156 |
157 | func testAddProduct3() throws {
158 | let manifest = """
159 | // swift-tools-version:5.2
160 | import PackageDescription
161 |
162 | let package = Package(
163 | \tname: "exec",
164 | \ttargets: [
165 | \t\t.target(
166 | \t\t\tname: "foo",
167 | \t\t\tdependencies: []
168 | \t\t),
169 | \t]
170 | )
171 | """
172 |
173 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
174 | try editor.addProduct(name: "exec", type: .executable)
175 |
176 | // FIXME: weird indentation
177 | XCTAssertEqual(editor.editedManifest, """
178 | // swift-tools-version:5.2
179 | import PackageDescription
180 |
181 | let package = Package(
182 | \tname: "exec",
183 | \tproducts: [
184 | \t\t.executable(
185 | \t\t\tname: "exec",
186 | \t\t\ttargets: []
187 | \t\t),
188 | \t],
189 | \ttargets: [
190 | \t\t.target(
191 | \t\t\tname: "foo",
192 | \t\t\tdependencies: []
193 | \t\t),
194 | \t]
195 | )
196 | """)
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/Tests/PackageSyntaxTests/AddTargetDependencyTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import XCTest
12 |
13 | import PackageSyntax
14 |
15 | final class AddTargetDependencyTests: XCTestCase {
16 | func testAddTargetDependency() throws {
17 | let manifest = """
18 | // swift-tools-version:5.2
19 | import PackageDescription
20 |
21 | let package = Package(
22 | name: "exec",
23 | dependencies: [
24 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
25 | ],
26 | targets: [
27 | .target(
28 | name: "a",
29 | dependencies: []),
30 | .target(
31 | name: "exec",
32 | dependencies: []),
33 | .target(
34 | name: "c",
35 | dependencies: []),
36 | .testTarget(
37 | name: "execTests",
38 | dependencies: ["exec"]),
39 | ]
40 | )
41 | """
42 |
43 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
44 | try editor.addByNameTargetDependency(
45 | target: "exec", dependency: "foo")
46 | try editor.addByNameTargetDependency(
47 | target: "exec", dependency: "bar")
48 | try editor.addByNameTargetDependency(
49 | target: "execTests", dependency: "foo")
50 |
51 | XCTAssertEqual(editor.editedManifest, """
52 | // swift-tools-version:5.2
53 | import PackageDescription
54 |
55 | let package = Package(
56 | name: "exec",
57 | dependencies: [
58 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
59 | ],
60 | targets: [
61 | .target(
62 | name: "a",
63 | dependencies: []),
64 | .target(
65 | name: "exec",
66 | dependencies: [
67 | "foo",
68 | "bar",
69 | ]),
70 | .target(
71 | name: "c",
72 | dependencies: []),
73 | .testTarget(
74 | name: "execTests",
75 | dependencies: [
76 | "exec",
77 | "foo",
78 | ]),
79 | ]
80 | )
81 | """)
82 | }
83 |
84 | func testAddTargetDependency2() throws {
85 | let manifest = """
86 | let package = Package(
87 | name: "exec",
88 | targets: [
89 | .target(
90 | name: "foo",
91 | dependencies: ["bar"]),
92 | .target(
93 | name: "foo1",
94 | dependencies: ["bar",]),
95 | .target(
96 | name: "foo2",
97 | dependencies: []),
98 | .target(
99 | name: "foo3",
100 | dependencies: ["foo", "bar"]),
101 | .target(
102 | name: "foo4",
103 | dependencies: [
104 | "foo", "bar"
105 | ]),
106 | ]
107 | )
108 | """
109 |
110 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
111 | try editor.addByNameTargetDependency(
112 | target: "foo", dependency: "dep")
113 | try editor.addByNameTargetDependency(
114 | target: "foo1", dependency: "dep")
115 | try editor.addByNameTargetDependency(
116 | target: "foo2", dependency: "dep")
117 | try editor.addByNameTargetDependency(
118 | target: "foo3", dependency: "dep")
119 | try editor.addByNameTargetDependency(
120 | target: "foo4", dependency: "dep")
121 |
122 | XCTAssertEqual(editor.editedManifest, """
123 | let package = Package(
124 | name: "exec",
125 | targets: [
126 | .target(
127 | name: "foo",
128 | dependencies: [
129 | "bar",
130 | "dep",
131 | ]),
132 | .target(
133 | name: "foo1",
134 | dependencies: [
135 | "bar",
136 | "dep",
137 | ]),
138 | .target(
139 | name: "foo2",
140 | dependencies: [
141 | "dep",
142 | ]),
143 | .target(
144 | name: "foo3",
145 | dependencies: ["foo", "bar", "dep",]),
146 | .target(
147 | name: "foo4",
148 | dependencies: [
149 | "foo", "bar", "dep",
150 | ]),
151 | ]
152 | )
153 | """)
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/Tests/PackageSyntaxTests/AddTargetTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import XCTest
12 |
13 | import PackageSyntax
14 |
15 | final class AddTargetTests: XCTestCase {
16 | func testAddTarget() throws {
17 | let manifest = """
18 | // swift-tools-version:5.2
19 | import PackageDescription
20 |
21 | let package = Package(
22 | name: "exec",
23 | dependencies: [
24 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
25 | ],
26 | targets: [
27 | .target(
28 | name: "foo",
29 | dependencies: []),
30 | .target(
31 | name: "bar",
32 | dependencies: []),
33 | .testTarget(
34 | name: "fooTests",
35 | dependencies: ["foo", "bar"]),
36 | ]
37 | )
38 | """
39 |
40 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
41 | try editor.addTarget(targetName: "NewTarget", factoryMethodName: "target")
42 | try editor.addTarget(targetName: "NewTargetTests", factoryMethodName: "testTarget")
43 | try editor.addByNameTargetDependency(target: "NewTargetTests", dependency: "NewTarget")
44 |
45 | XCTAssertEqual(editor.editedManifest, """
46 | // swift-tools-version:5.2
47 | import PackageDescription
48 |
49 | let package = Package(
50 | name: "exec",
51 | dependencies: [
52 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
53 | ],
54 | targets: [
55 | .target(
56 | name: "foo",
57 | dependencies: []),
58 | .target(
59 | name: "bar",
60 | dependencies: []),
61 | .testTarget(
62 | name: "fooTests",
63 | dependencies: ["foo", "bar"]),
64 | .target(
65 | name: "NewTarget",
66 | dependencies: []
67 | ),
68 | .testTarget(
69 | name: "NewTargetTests",
70 | dependencies: [
71 | "NewTarget",
72 | ]
73 | ),
74 | ]
75 | )
76 | """)
77 | }
78 |
79 | func testAddTarget2() throws {
80 | let manifest = """
81 | // swift-tools-version:5.2
82 | import PackageDescription
83 |
84 | let package = Package(
85 | name: "exec",
86 | dependencies: [
87 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
88 | ]
89 | )
90 | """
91 |
92 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
93 | try editor.addTarget(targetName: "NewTarget", factoryMethodName: "target")
94 |
95 | XCTAssertEqual(editor.editedManifest, """
96 | // swift-tools-version:5.2
97 | import PackageDescription
98 |
99 | let package = Package(
100 | name: "exec",
101 | dependencies: [
102 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
103 | ],
104 | targets: [
105 | .target(
106 | name: "NewTarget",
107 | dependencies: []
108 | ),
109 | ]
110 | )
111 | """)
112 | }
113 |
114 | func testAddTarget3() throws {
115 | let manifest = """
116 | // swift-tools-version:5.2
117 | import PackageDescription
118 |
119 | let package = Package(
120 | \tname: "exec",
121 | \tdependencies: [
122 | \t\t.package(url: "https://github.com/foo/goo", from: "1.0.1"),
123 | \t]
124 | )
125 | """
126 |
127 | let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init())
128 | try editor.addTarget(targetName: "NewTarget", factoryMethodName: "target")
129 |
130 | XCTAssertEqual(editor.editedManifest, """
131 | // swift-tools-version:5.2
132 | import PackageDescription
133 |
134 | let package = Package(
135 | \tname: "exec",
136 | \tdependencies: [
137 | \t\t.package(url: "https://github.com/foo/goo", from: "1.0.1"),
138 | \t],
139 | \ttargets: [
140 | \t\t.target(
141 | \t\t\tname: "NewTarget",
142 | \t\t\tdependencies: []
143 | \t\t),
144 | \t]
145 | )
146 | """)
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Tests/PackageSyntaxTests/ArrayFormattingTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import XCTest
12 | import PackageSyntax
13 | import SwiftSyntax
14 |
15 | final class ArrayFormattingTests: XCTestCase {
16 | func assertAdding(string: String, to arrayLiteralCode: String, produces result: String) {
17 | let sourceFileSyntax = try! SyntaxParser.parse(source: arrayLiteralCode)
18 | let arrayExpr = sourceFileSyntax.statements.first?.item.as(ArrayExprSyntax.self)!
19 | let outputSyntax = arrayExpr?.withAdditionalElementExpr(ExprSyntax(SyntaxFactory.makeStringLiteralExpr(string)))
20 | XCTAssertEqual(outputSyntax!.description, result)
21 | }
22 |
23 | func assertAdding(string: String, toFunctionCallArg functionCallCode: String, produces result: String) {
24 | let sourceFileSyntax = try! SyntaxParser.parse(source: functionCallCode)
25 | let funcExpr = sourceFileSyntax.statements.first!.item.as(FunctionCallExprSyntax.self)!
26 | let arg = funcExpr.argumentList.first!
27 | let arrayExpr = arg.expression.as(ArrayExprSyntax.self)!
28 | let newExpr = arrayExpr.withAdditionalElementExpr(ExprSyntax(SyntaxFactory.makeStringLiteralExpr(string)))
29 | let outputSyntax = funcExpr.withArgumentList(
30 | funcExpr.argumentList.replacing(childAt: 0,
31 | with: arg.withExpression(ExprSyntax(newExpr)))
32 | )
33 | XCTAssertEqual(outputSyntax.description, result)
34 | }
35 |
36 | func testInsertingIntoArrayExprWith2PlusElements() throws {
37 | assertAdding(string: "c", to: #"["a", "b"]"#, produces: #"["a", "b", "c",]"#)
38 | assertAdding(string: "c", to: #"["a", "b",]"#, produces: #"["a", "b", "c",]"#)
39 | assertAdding(string: "c", to: #"["a","b"]"#, produces: #"["a","b","c",]"#)
40 | assertAdding(string: "c",
41 | to: #"["a", /*hello*/"b"/*world!*/]"#,
42 | produces: #"["a", /*hello*/"b",/*world!*/ "c",]"#)
43 | assertAdding(string: "c", to: """
44 | [
45 | "a",
46 | "b"
47 | ]
48 | """, produces: """
49 | [
50 | "a",
51 | "b",
52 | "c",
53 | ]
54 | """)
55 | assertAdding(string: "c", to: """
56 | [
57 | "a",
58 | "b",
59 | ]
60 | """, produces: """
61 | [
62 | "a",
63 | "b",
64 | "c",
65 | ]
66 | """)
67 | assertAdding(string: "c", to: """
68 | [
69 | "a", "b"
70 | ]
71 | """, produces: """
72 | [
73 | "a", "b", "c",
74 | ]
75 | """)
76 | assertAdding(string: "e", to: """
77 | [
78 | "a", "b",
79 | "c", "d",
80 | ]
81 | """, produces: """
82 | [
83 | "a", "b",
84 | "c", "d", "e",
85 | ]
86 | """)
87 | assertAdding(string: "e", to: """
88 | ["a", "b",
89 | "c", "d"]
90 | """, produces: """
91 | ["a", "b",
92 | "c", "d", "e",]
93 | """)
94 | assertAdding(string: "c", to: """
95 | \t[
96 | \t\t"a",
97 | \t\t"b",
98 | \t]
99 | """, produces: """
100 | \t[
101 | \t\t"a",
102 | \t\t"b",
103 | \t\t"c",
104 | \t]
105 | """)
106 | assertAdding(string: "c", to: """
107 | [
108 | "a", // Comment about a
109 | "b",
110 | ]
111 | """, produces: """
112 | [
113 | "a", // Comment about a
114 | "b",
115 | "c",
116 | ]
117 | """)
118 | assertAdding(string: "c", to: """
119 | [
120 | "a",
121 | "b", // Comment about b
122 | ]
123 | """, produces: """
124 | [
125 | "a",
126 | "b", // Comment about b
127 | "c",
128 | ]
129 | """)
130 | assertAdding(string: "c", to: """
131 | [
132 | "a",
133 | "b",
134 | /*comment*/
135 | ]
136 | """, produces: """
137 | [
138 | "a",
139 | "b",
140 | /*comment*/
141 | "c",
142 | ]
143 | """)
144 | assertAdding(string: "c", to: """
145 | [
146 | /*
147 | 1
148 | */
149 | "a",
150 | /*
151 | 2
152 | */
153 | "b",
154 | /*
155 | 3
156 | */
157 | ]
158 | """, produces: """
159 | [
160 | /*
161 | 1
162 | */
163 | "a",
164 | /*
165 | 2
166 | */
167 | "b",
168 | /*
169 | 3
170 | */
171 | "c",
172 | ]
173 | """)
174 | assertAdding(string: "c", to: """
175 | [
176 | /// Comment
177 |
178 | "a",
179 |
180 |
181 | "b",
182 | ]
183 | """, produces: """
184 | [
185 | /// Comment
186 |
187 | "a",
188 |
189 |
190 | "b",
191 |
192 |
193 | "c",
194 | ]
195 | """)
196 | assertAdding(string: "3", toFunctionCallArg: """
197 | foo(someArg: ["1", "2"])
198 | """, produces: """
199 | foo(someArg: ["1", "2", "3",])
200 | """)
201 | assertAdding(string: "3", toFunctionCallArg: """
202 | foo(someArg: ["1",
203 | "2"])
204 | """, produces: """
205 | foo(someArg: ["1",
206 | "2",
207 | "3",])
208 | """)
209 | assertAdding(string: "3", toFunctionCallArg: """
210 | foo(
211 | arg1: ["1", "2"], arg2: []
212 | )
213 | """, produces: """
214 | foo(
215 | arg1: ["1", "2", "3",], arg2: []
216 | )
217 | """)
218 | assertAdding(string: "3", toFunctionCallArg: """
219 | foo(someArg: [
220 | "1",
221 | "2",
222 | ])
223 | """, produces: """
224 | foo(someArg: [
225 | "1",
226 | "2",
227 | "3",
228 | ])
229 | """)
230 | assertAdding(string: "3", toFunctionCallArg: """
231 | foo(
232 | arg1: [
233 | "1",
234 | "2",
235 | ], arg2: []
236 | )
237 | """, produces: """
238 | foo(
239 | arg1: [
240 | "1",
241 | "2",
242 | "3",
243 | ], arg2: []
244 | )
245 | """)
246 | }
247 |
248 | func testInsertingIntoEmptyArrayExpr() {
249 | assertAdding(string: "1", to: #"[]"#, produces: """
250 | [
251 | "1",
252 | ]
253 | """)
254 | assertAdding(string: "1", to: """
255 | [
256 |
257 | ]
258 | """, produces: """
259 | [
260 | "1",
261 | ]
262 | """)
263 | assertAdding(string: "1", to: """
264 | [
265 | ]
266 | """, produces: """
267 | [
268 | "1",
269 | ]
270 | """)
271 | assertAdding(string: "1", to: """
272 | [
273 |
274 | ]
275 | """, produces: """
276 | [
277 | "1",
278 | ]
279 | """)
280 | assertAdding(string: "1", to: """
281 | \t[
282 |
283 | \t]
284 | """, produces: """
285 | \t[
286 | \t\t"1",
287 | \t]
288 | """)
289 | assertAdding(string: "1", toFunctionCallArg: """
290 | foo(someArg: [])
291 | """, produces: """
292 | foo(someArg: [
293 | "1",
294 | ])
295 | """)
296 | assertAdding(string: "1", toFunctionCallArg: """
297 | foo(someArg: [])
298 | """, produces: """
299 | foo(someArg: [
300 | "1",
301 | ])
302 | """)
303 | assertAdding(string: "1", toFunctionCallArg: """
304 | foo(
305 | arg1: [], arg2: []
306 | )
307 | """, produces: """
308 | foo(
309 | arg1: [
310 | "1",
311 | ], arg2: []
312 | )
313 | """)
314 | assertAdding(string: "1", toFunctionCallArg: """
315 | \tfoo(someArg: [])
316 | """, produces: """
317 | \tfoo(someArg: [
318 | \t\t"1",
319 | \t])
320 | """)
321 | }
322 |
323 | func testInsertingIntoSingleElementArrayExpr() {
324 | assertAdding(string: "b", to: """
325 | ["a"]
326 | """, produces: """
327 | [
328 | "a",
329 | "b",
330 | ]
331 | """)
332 | assertAdding(string: "b", to: """
333 | [
334 | "a"
335 | ]
336 | """, produces: """
337 | [
338 | "a",
339 | "b",
340 | ]
341 | """)
342 | assertAdding(string: "b", to: """
343 | ["a",]
344 | """, produces: """
345 | [
346 | "a",
347 | "b",
348 | ]
349 | """)
350 | assertAdding(string: "b", to: """
351 | [
352 | "a",
353 | ]
354 | """, produces: """
355 | [
356 | "a",
357 | "b",
358 | ]
359 | """)
360 | assertAdding(string: "2", toFunctionCallArg: """
361 | foo(someArg: ["1"])
362 | """, produces: """
363 | foo(someArg: [
364 | "1",
365 | "2",
366 | ])
367 | """)
368 | assertAdding(string: "2", toFunctionCallArg: """
369 | foo(someArg: ["1"])
370 | """, produces: """
371 | foo(someArg: [
372 | "1",
373 | "2",
374 | ])
375 | """)
376 | assertAdding(string: "2", toFunctionCallArg: """
377 | foo(
378 | arg1: ["1"], arg2: []
379 | )
380 | """, produces: """
381 | foo(
382 | arg1: [
383 | "1",
384 | "2",
385 | ], arg2: []
386 | )
387 | """)
388 | assertAdding(string: "2", toFunctionCallArg: """
389 | foo(someArg: [
390 | "1"
391 | ])
392 | """, produces: """
393 | foo(someArg: [
394 | "1",
395 | "2",
396 | ])
397 | """)
398 | assertAdding(string: "2", toFunctionCallArg: """
399 | foo(someArg: [
400 | "1"
401 | ])
402 | """, produces: """
403 | foo(someArg: [
404 | "1",
405 | "2",
406 | ])
407 | """)
408 | assertAdding(string: "2", toFunctionCallArg: """
409 | foo(
410 | arg1: [
411 | "1"
412 | ], arg2: []
413 | )
414 | """, produces: """
415 | foo(
416 | arg1: [
417 | "1",
418 | "2",
419 | ], arg2: []
420 | )
421 | """)
422 | }
423 |
424 | func assert(code: String, hasIndent indent: Trivia, forLine line: Int) {
425 | let sourceFileSyntax = try! SyntaxParser.parse(source: code)
426 | let converter = SourceLocationConverter(file: "test.swift", tree: sourceFileSyntax)
427 | let visitor = DetermineLineIndentVisitor(lineNumber: line, sourceLocationConverter: converter)
428 | visitor.walk(sourceFileSyntax)
429 | XCTAssertEqual(visitor.lineIndent, indent)
430 | }
431 |
432 | func testIndentVisitor() throws {
433 | assert(code: """
434 | foo(
435 | arg: []
436 | )
437 | """, hasIndent: [.spaces(4)], forLine: 2)
438 | assert(code: """
439 | foo(
440 | \targ: []
441 | )
442 | """, hasIndent: [.tabs(1)], forLine: 2)
443 | assert(code: """
444 | foo(
445 | arg1: [], arg2: []
446 | )
447 | """, hasIndent: [.spaces(4)], forLine: 2)
448 | assert(code: """
449 | foo(
450 | bar(
451 | arg1: [],
452 | arg2: []
453 | )
454 | )
455 | """, hasIndent: [.spaces(8)], forLine: 3)
456 | assert(code: """
457 | foo(
458 | bar(arg1: [],
459 | arg2: [])
460 | )
461 | """, hasIndent: [.spaces(4)], forLine: 2)
462 | assert(code: """
463 | foo(
464 | bar(arg1: [],
465 | arg2: [])
466 | )
467 | """, hasIndent: [.spaces(8)], forLine: 3)
468 | }
469 | }
470 |
--------------------------------------------------------------------------------
/Tests/PackageSyntaxTests/InMemoryGitRepository.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | // FIXME: Share this with SwiftPM
12 |
13 | import Basics
14 | import TSCBasic
15 | import TSCUtility
16 | import SourceControl
17 | import Dispatch
18 | import class Foundation.NSUUID
19 |
20 | /// The error encountered during in memory git repository operations.
21 | public enum InMemoryGitRepositoryError: Swift.Error {
22 | case unknownRevision
23 | case unknownTag
24 | case tagAlreadyPresent
25 | }
26 |
27 | /// A class that implements basic git features on in-memory file system. It takes the path and file system reference
28 | /// where the repository should be created. The class itself is a file system pointing to current revision state
29 | /// i.e. HEAD. All mutations should be made on file system interface of this class and then they can be committed using
30 | /// commit() method. Calls to checkout related methods will checkout the HEAD on the passed file system at the
31 | /// repository path, as well as on the file system interface of this class.
32 | /// Note: This class is intended to be used as testing infrastructure only.
33 | /// Note: This class is not thread safe yet.
34 | public final class InMemoryGitRepository {
35 | /// The revision identifier.
36 | public typealias RevisionIdentifier = String
37 |
38 | /// A struct representing a revision state. Minimally it contains a hash identifier for the revision
39 | /// and the file system state.
40 | fileprivate struct RevisionState {
41 | /// The revision identifier hash. It should be unique amoung all the identifiers.
42 | var hash: RevisionIdentifier
43 |
44 | /// The filesystem state contained in this revision.
45 | let fileSystem: InMemoryFileSystem
46 |
47 | /// Creates copy of the state.
48 | func copy() -> RevisionState {
49 | return RevisionState(hash: self.hash, fileSystem: self.fileSystem.copy())
50 | }
51 | }
52 |
53 | /// THe HEAD i.e. the current checked out state.
54 | fileprivate var head: RevisionState
55 |
56 | /// The history dictionary.
57 | fileprivate var history: [RevisionIdentifier: RevisionState] = [:]
58 |
59 | /// The map containing tag name to revision identifier values.
60 | fileprivate var tagsMap: [String: RevisionIdentifier] = [:]
61 |
62 | /// Indicates whether there are any uncommited changes in the repository.
63 | fileprivate var isDirty = false
64 |
65 | /// The path at which this repository is located.
66 | fileprivate let path: AbsolutePath
67 |
68 | /// The file system in which this repository should be installed.
69 | private let fs: InMemoryFileSystem
70 |
71 | private let lock = Lock()
72 |
73 | /// Create a new repository at the given path and filesystem.
74 | public init(path: AbsolutePath, fs: InMemoryFileSystem) {
75 | self.path = path
76 | self.fs = fs
77 | // Point head to a new revision state with empty hash to begin with.
78 | self.head = RevisionState(hash: "", fileSystem: InMemoryFileSystem())
79 | }
80 |
81 | /// The array of current tags in the repository.
82 | public func getTags() throws -> [String] {
83 | self.lock.withLock {
84 | Array(self.tagsMap.keys)
85 | }
86 | }
87 |
88 | /// The list of revisions in the repository.
89 | public var revisions: [RevisionIdentifier] {
90 | self.lock.withLock {
91 | Array(self.history.keys)
92 | }
93 | }
94 |
95 | /// Copy/clone this repository.
96 | fileprivate func copy(at newPath: AbsolutePath? = nil) throws -> InMemoryGitRepository {
97 | let path = newPath ?? self.path
98 | try self.fs.createDirectory(path, recursive: true)
99 | let repo = InMemoryGitRepository(path: path, fs: self.fs)
100 | self.lock.withLock {
101 | for (revision, state) in self.history {
102 | repo.history[revision] = state.copy()
103 | }
104 | repo.tagsMap = self.tagsMap
105 | repo.head = self.head.copy()
106 | }
107 | return repo
108 | }
109 |
110 | /// Commits the current state of the repository filesystem and returns the commit identifier.
111 | @discardableResult
112 | public func commit() throws -> String {
113 | // Create a fake hash for thie commit.
114 | let hash = String((NSUUID().uuidString + NSUUID().uuidString).prefix(40))
115 | self.lock.withLock {
116 | self.head.hash = hash
117 | // Store the commit in history.
118 | self.history[hash] = head.copy()
119 | // We are not dirty anymore.
120 | self.isDirty = false
121 | }
122 | // Install the current HEAD i.e. this commit to the filesystem that was passed.
123 | try installHead()
124 | return hash
125 | }
126 |
127 | /// Checks out the provided revision.
128 | public func checkout(revision: RevisionIdentifier) throws {
129 | guard let state = (self.lock.withLock { history[revision] }) else {
130 | throw InMemoryGitRepositoryError.unknownRevision
131 | }
132 | // Point the head to the revision state.
133 | self.lock.withLock {
134 | self.head = state
135 | self.isDirty = false
136 | }
137 | // Install this state on the passed filesystem.
138 | try self.installHead()
139 | }
140 |
141 | /// Checks out a given tag.
142 | public func checkout(tag: String) throws {
143 | guard let hash = (self.lock.withLock { tagsMap[tag] }) else {
144 | throw InMemoryGitRepositoryError.unknownTag
145 | }
146 | // Point the head to the revision state of the tag.
147 | // It should be impossible that a tag exisits which doesnot have a state.
148 | self.lock.withLock {
149 | self.head = history[hash]!
150 | self.isDirty = false
151 | }
152 | // Install this state on the passed filesystem.
153 | try self.installHead()
154 | }
155 |
156 | /// Installs (or checks out) current head on the filesystem on which this repository exists.
157 | fileprivate func installHead() throws {
158 | // Remove the old state.
159 | try self.fs.removeFileTree(self.path)
160 | // Create the repository directory.
161 | try self.fs.createDirectory(self.path, recursive: true)
162 | // Get the file system state at the HEAD,
163 | let headFs = self.lock.withLock { self.head.fileSystem }
164 |
165 | /// Recursively copies the content at HEAD to fs.
166 | func install(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
167 | assert(headFs.isDirectory(sourcePath))
168 | for entry in try headFs.getDirectoryContents(sourcePath) {
169 | // The full path of the entry.
170 | let sourceEntryPath = sourcePath.appending(component: entry)
171 | let destinationEntryPath = destinationPath.appending(component: entry)
172 | if headFs.isFile(sourceEntryPath) {
173 | // If we have a file just write the file.
174 | let bytes = try headFs.readFileContents(sourceEntryPath)
175 | try self.fs.writeFileContents(destinationEntryPath, bytes: bytes)
176 | } else if headFs.isDirectory(sourceEntryPath) {
177 | // If we have a directory, create that directory and copy its contents.
178 | try self.fs.createDirectory(destinationEntryPath, recursive: false)
179 | try install(from: sourceEntryPath, to: destinationEntryPath)
180 | }
181 | }
182 | }
183 | // Install at the repository path.
184 | try install(from: .root, to: path)
185 | }
186 |
187 | /// Tag the current HEAD with the given name.
188 | public func tag(name: String) throws {
189 | guard (self.lock.withLock { self.tagsMap[name] }) == nil else {
190 | throw InMemoryGitRepositoryError.tagAlreadyPresent
191 | }
192 | self.lock.withLock {
193 | self.tagsMap[name] = self.head.hash
194 | }
195 | }
196 |
197 | public func hasUncommittedChanges() -> Bool {
198 | self.lock.withLock {
199 | isDirty
200 | }
201 | }
202 |
203 | public func fetch() throws {
204 | // TODO.
205 | }
206 | }
207 |
208 | extension InMemoryGitRepository: FileSystem {
209 |
210 | public func exists(_ path: AbsolutePath, followSymlink: Bool) -> Bool {
211 | self.lock.withLock {
212 | self.head.fileSystem.exists(path, followSymlink: followSymlink)
213 | }
214 | }
215 |
216 | public func isDirectory(_ path: AbsolutePath) -> Bool {
217 | self.lock.withLock {
218 | self.head.fileSystem.isDirectory(path)
219 | }
220 | }
221 |
222 | public func isFile(_ path: AbsolutePath) -> Bool {
223 | self.lock.withLock {
224 | self.head.fileSystem.isFile(path)
225 | }
226 | }
227 |
228 | public func isSymlink(_ path: AbsolutePath) -> Bool {
229 | self.lock.withLock {
230 | self.head.fileSystem.isSymlink(path)
231 | }
232 | }
233 |
234 | public func isExecutableFile(_ path: AbsolutePath) -> Bool {
235 | self.lock.withLock {
236 | self.head.fileSystem.isExecutableFile(path)
237 | }
238 | }
239 |
240 | public var currentWorkingDirectory: AbsolutePath? {
241 | return AbsolutePath("/")
242 | }
243 |
244 | public func changeCurrentWorkingDirectory(to path: AbsolutePath) throws {
245 | throw FileSystemError(.unsupported, path)
246 | }
247 |
248 | public var homeDirectory: AbsolutePath {
249 | fatalError("Unsupported")
250 | }
251 |
252 | public var cachesDirectory: AbsolutePath? {
253 | fatalError("Unsupported")
254 | }
255 |
256 | public func getDirectoryContents(_ path: AbsolutePath) throws -> [String] {
257 | try self.lock.withLock {
258 | try self.head.fileSystem.getDirectoryContents(path)
259 | }
260 | }
261 |
262 | public func createDirectory(_ path: AbsolutePath, recursive: Bool) throws {
263 | try self.lock.withLock {
264 | try self.head.fileSystem.createDirectory(path, recursive: recursive)
265 | }
266 | }
267 |
268 | public func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws {
269 | throw FileSystemError(.unsupported, path)
270 | }
271 |
272 | public func readFileContents(_ path: AbsolutePath) throws -> ByteString {
273 | try self.lock.withLock {
274 | return try head.fileSystem.readFileContents(path)
275 | }
276 | }
277 |
278 | public func writeFileContents(_ path: AbsolutePath, bytes: ByteString) throws {
279 | try self.lock.withLock {
280 | try self.head.fileSystem.writeFileContents(path, bytes: bytes)
281 | self.isDirty = true
282 | }
283 | }
284 |
285 | public func removeFileTree(_ path: AbsolutePath) throws {
286 | try self.lock.withLock {
287 | try self.head.fileSystem.removeFileTree(path)
288 | }
289 | }
290 |
291 | public func chmod(_ mode: FileMode, path: AbsolutePath, options: Set) throws {
292 | try self.lock.withLock {
293 | try self.head.fileSystem.chmod(mode, path: path, options: options)
294 | }
295 | }
296 |
297 | public func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
298 | try self.lock.withLock {
299 | try self.head.fileSystem.copy(from: sourcePath, to: destinationPath)
300 | }
301 | }
302 |
303 | public func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
304 | try self.lock.withLock {
305 | try self.head.fileSystem.move(from: sourcePath, to: destinationPath)
306 | }
307 | }
308 | }
309 |
310 | extension InMemoryGitRepository: Repository {
311 | public func resolveRevision(tag: String) throws -> Revision {
312 | self.lock.withLock {
313 | return Revision(identifier: self.tagsMap[tag]!)
314 | }
315 | }
316 |
317 | public func resolveRevision(identifier: String) throws -> Revision {
318 | self.lock.withLock {
319 | return Revision(identifier: self.tagsMap[identifier] ?? identifier)
320 | }
321 | }
322 |
323 | public func exists(revision: Revision) -> Bool {
324 | self.lock.withLock {
325 | return self.history[revision.identifier] != nil
326 | }
327 | }
328 |
329 | public func openFileView(revision: Revision) throws -> FileSystem {
330 | self.lock.withLock {
331 | return self.history[revision.identifier]!.fileSystem
332 | }
333 | }
334 |
335 | public func openFileView(tag: String) throws -> FileSystem {
336 | let revision = try self.resolveRevision(tag: tag)
337 | return try self.openFileView(revision: revision)
338 | }
339 | }
340 |
341 | extension InMemoryGitRepository: WorkingCheckout {
342 | public func getCurrentRevision() throws -> Revision {
343 | self.lock.withLock {
344 | return Revision(identifier: self.head.hash)
345 | }
346 | }
347 |
348 | public func checkout(revision: Revision) throws {
349 | // will lock
350 | try checkout(revision: revision.identifier)
351 | }
352 |
353 | public func hasUnpushedCommits() throws -> Bool {
354 | return false
355 | }
356 |
357 | public func checkout(newBranch: String) throws {
358 | self.lock.withLock {
359 | self.history[newBranch] = head
360 | }
361 | }
362 |
363 | public func isAlternateObjectStoreValid() -> Bool {
364 | return true
365 | }
366 |
367 | public func areIgnored(_ paths: [AbsolutePath]) throws -> [Bool] {
368 | return [false]
369 | }
370 | }
371 |
372 | /// This class implement provider for in memeory git repository.
373 | public final class InMemoryGitRepositoryProvider: RepositoryProvider {
374 | /// Contains the repository added to this provider.
375 | public var specifierMap = ThreadSafeKeyValueStore()
376 |
377 | /// Contains the repositories which are fetched using this provider.
378 | public var fetchedMap = ThreadSafeKeyValueStore()
379 |
380 | /// Contains the repositories which are checked out using this provider.
381 | public var checkoutsMap = ThreadSafeKeyValueStore()
382 |
383 | /// Create a new provider.
384 | public init() {
385 | }
386 |
387 | /// Add a repository to this provider. Only the repositories added with this interface can be operated on
388 | /// with this provider.
389 | public func add(specifier: RepositorySpecifier, repository: InMemoryGitRepository) {
390 | // Save the repository in specifer map.
391 | specifierMap[specifier] = repository
392 | }
393 |
394 | /// This method returns the stored reference to the git repository which was fetched or checked out.
395 | public func openRepo(at path: AbsolutePath) -> InMemoryGitRepository {
396 | return fetchedMap[path] ?? checkoutsMap[path]!
397 | }
398 |
399 | // MARK: - RepositoryProvider conformance
400 | // Note: These methods use force unwrap (instead of throwing) to honor their preconditions.
401 |
402 | public func fetch(repository: RepositorySpecifier, to path: AbsolutePath) throws {
403 | let repo = specifierMap[RepositorySpecifier(url: repository.url.spm_dropGitSuffix())]!
404 | fetchedMap[path] = try repo.copy()
405 | add(specifier: RepositorySpecifier(url: path.asURL.absoluteString), repository: repo)
406 | }
407 |
408 | public func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
409 | let repo = fetchedMap[sourcePath]!
410 | fetchedMap[destinationPath] = try repo.copy()
411 | }
412 |
413 | public func open(repository: RepositorySpecifier, at path: AbsolutePath) throws -> Repository {
414 | return fetchedMap[path]!
415 | }
416 |
417 | public func createWorkingCopy(
418 | repository: RepositorySpecifier,
419 | sourcePath: AbsolutePath,
420 | at destinationPath: AbsolutePath,
421 | editable: Bool
422 | ) throws -> WorkingCheckout {
423 | let checkout = try fetchedMap[sourcePath]!.copy(at: destinationPath)
424 | checkoutsMap[destinationPath] = checkout
425 | return checkout
426 | }
427 |
428 | public func workingCopyExists(at path: AbsolutePath) throws -> Bool {
429 | return checkoutsMap.contains(path)
430 | }
431 |
432 | public func openWorkingCopy(at path: AbsolutePath) throws -> WorkingCheckout {
433 | return checkoutsMap[path]!
434 | }
435 | }
436 |
--------------------------------------------------------------------------------
/Tests/PackageSyntaxTests/PackageEditorTests.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | import XCTest
12 | import TSCBasic
13 | import TSCUtility
14 | import SourceControl
15 |
16 | import PackageSyntax
17 |
18 | final class PackageEditorTests: XCTestCase {
19 |
20 | func testAddDependency5_2_to_5_4() throws {
21 | for version in ["5.2", "5.3", "5.4"] {
22 | let manifest = """
23 | // swift-tools-version:\(version)
24 | import PackageDescription
25 |
26 | let package = Package(
27 | name: "exec",
28 | dependencies: [
29 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
30 | ],
31 | targets: [
32 | .target(
33 | name: "foo",
34 | dependencies: []),
35 | .target(
36 | name: "bar",
37 | dependencies: []),
38 | .testTarget(
39 | name: "fooTests",
40 | dependencies: ["foo", "bar"]),
41 | ]
42 | )
43 | """
44 |
45 | let fs = InMemoryFileSystem(emptyFiles:
46 | "/pkg/Package.swift",
47 | "/pkg/Sources/foo/source.swift",
48 | "/pkg/Sources/bar/source.swift",
49 | "/pkg/Tests/fooTests/source.swift",
50 | "end")
51 |
52 | let manifestPath = AbsolutePath("/pkg/Package.swift")
53 | try fs.writeFileContents(manifestPath) { $0 <<< manifest }
54 | try fs.createDirectory(.init("/pkg/repositories"), recursive: false)
55 | try fs.createDirectory(.init("/pkg/repo"), recursive: false)
56 |
57 |
58 | let provider = InMemoryGitRepositoryProvider()
59 | let repo = InMemoryGitRepository(path: .init("/pkg/repo"), fs: fs)
60 | try repo.writeFileContents(.init("/Package.swift"), bytes: .init(encodingAsUTF8: """
61 | // swift-tools-version:5.2
62 | import PackageDescription
63 |
64 | let package = Package(name: "repo")
65 | """))
66 | try repo.commit()
67 | try repo.tag(name: "1.1.1")
68 | provider.add(specifier: .init(url: "http://www.githost.com/repo"), repository: repo)
69 |
70 | let context = try PackageEditorContext(
71 | manifestPath: AbsolutePath("/pkg/Package.swift"),
72 | repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: provider, fileSystem: fs),
73 | toolchain: Resources.default.toolchain,
74 | diagnosticsEngine: .init(),
75 | fs: fs
76 | )
77 | let editor = PackageEditor(context: context)
78 |
79 | try editor.addPackageDependency(url: "http://www.githost.com/repo.git", requirement: .exact("1.1.1"))
80 |
81 | let newManifest = try fs.readFileContents(manifestPath).cString
82 | XCTAssertEqual(newManifest, """
83 | // swift-tools-version:\(version)
84 | import PackageDescription
85 |
86 | let package = Package(
87 | name: "exec",
88 | dependencies: [
89 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
90 | .package(name: "repo", url: "http://www.githost.com/repo.git", .exact("1.1.1")),
91 | ],
92 | targets: [
93 | .target(
94 | name: "foo",
95 | dependencies: []),
96 | .target(
97 | name: "bar",
98 | dependencies: []),
99 | .testTarget(
100 | name: "fooTests",
101 | dependencies: ["foo", "bar"]),
102 | ]
103 | )
104 | """)
105 | }
106 | }
107 |
108 | func testAddDependency5_5() throws {
109 | let manifest = """
110 | // swift-tools-version:5.5
111 | import PackageDescription
112 |
113 | let package = Package(
114 | name: "exec",
115 | dependencies: [
116 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
117 | ],
118 | targets: [
119 | .target(
120 | name: "foo",
121 | dependencies: []),
122 | .target(
123 | name: "bar",
124 | dependencies: []),
125 | .testTarget(
126 | name: "fooTests",
127 | dependencies: ["foo", "bar"]),
128 | ]
129 | )
130 | """
131 |
132 | let fs = InMemoryFileSystem(emptyFiles:
133 | "/pkg/Package.swift",
134 | "/pkg/Sources/foo/source.swift",
135 | "/pkg/Sources/bar/source.swift",
136 | "/pkg/Tests/fooTests/source.swift",
137 | "end")
138 |
139 | let manifestPath = AbsolutePath("/pkg/Package.swift")
140 | try fs.writeFileContents(manifestPath) { $0 <<< manifest }
141 | try fs.createDirectory(.init("/pkg/repositories"), recursive: false)
142 | try fs.createDirectory(.init("/pkg/repo"), recursive: false)
143 |
144 |
145 | let provider = InMemoryGitRepositoryProvider()
146 | let repo = InMemoryGitRepository(path: .init("/pkg/repo"), fs: fs)
147 | try repo.writeFileContents(.init("/Package.swift"), bytes: .init(encodingAsUTF8: """
148 | // swift-tools-version:5.2
149 | import PackageDescription
150 |
151 | let package = Package(name: "repo")
152 | """))
153 | try repo.commit()
154 | try repo.tag(name: "1.1.1")
155 | provider.add(specifier: .init(url: "http://www.githost.com/repo"), repository: repo)
156 |
157 | let context = try PackageEditorContext(
158 | manifestPath: AbsolutePath("/pkg/Package.swift"),
159 | repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: provider, fileSystem: fs),
160 | toolchain: Resources.default.toolchain,
161 | diagnosticsEngine: .init(),
162 | fs: fs
163 | )
164 | let editor = PackageEditor(context: context)
165 |
166 | try editor.addPackageDependency(url: "http://www.githost.com/repo.git", requirement: .exact("1.1.1"))
167 |
168 | let newManifest = try fs.readFileContents(manifestPath).cString
169 | XCTAssertEqual(newManifest, """
170 | // swift-tools-version:5.5
171 | import PackageDescription
172 |
173 | let package = Package(
174 | name: "exec",
175 | dependencies: [
176 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
177 | .package(url: "http://www.githost.com/repo.git", .exact("1.1.1")),
178 | ],
179 | targets: [
180 | .target(
181 | name: "foo",
182 | dependencies: []),
183 | .target(
184 | name: "bar",
185 | dependencies: []),
186 | .testTarget(
187 | name: "fooTests",
188 | dependencies: ["foo", "bar"]),
189 | ]
190 | )
191 | """)
192 | }
193 |
194 | func testAddTarget5_2() throws {
195 | let manifest = """
196 | // swift-tools-version:5.2
197 | import PackageDescription
198 |
199 | let package = Package(
200 | name: "exec",
201 | dependencies: [
202 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
203 | ],
204 | targets: [
205 | .target(
206 | name: "foo",
207 | dependencies: []),
208 | .target(
209 | name: "bar",
210 | dependencies: []),
211 | .testTarget(
212 | name: "fooTests",
213 | dependencies: ["foo", "bar"]),
214 | ]
215 | )
216 | """
217 |
218 | let fs = InMemoryFileSystem(emptyFiles:
219 | "/pkg/Package.swift",
220 | "/pkg/Sources/foo/source.swift",
221 | "/pkg/Sources/bar/source.swift",
222 | "/pkg/Tests/fooTests/source.swift",
223 | "end")
224 |
225 | let manifestPath = AbsolutePath("/pkg/Package.swift")
226 | try fs.writeFileContents(manifestPath) { $0 <<< manifest }
227 |
228 | let diags = DiagnosticsEngine()
229 | let context = try PackageEditorContext(
230 | manifestPath: AbsolutePath("/pkg/Package.swift"),
231 | repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()),
232 | toolchain: Resources.default.toolchain,
233 | diagnosticsEngine: diags,
234 | fs: fs)
235 | let editor = PackageEditor(context: context)
236 |
237 | XCTAssertThrows(Diagnostics.fatalError) {
238 | try editor.addTarget(.library(name: "foo", includeTestTarget: true, dependencyNames: []),
239 | productPackageNameMapping: [:])
240 | }
241 | XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'"])
242 | XCTAssertThrows(Diagnostics.fatalError) {
243 | try editor.addTarget(.library(name: "Error", includeTestTarget: true, dependencyNames: ["NotFound"]),
244 | productPackageNameMapping: [:])
245 | }
246 | XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'",
247 | "could not find a product or target named 'NotFound'"])
248 |
249 | try editor.addTarget(.library(name: "baz", includeTestTarget: true, dependencyNames: []),
250 | productPackageNameMapping: [:])
251 | try editor.addTarget(.executable(name: "qux", dependencyNames: ["foo", "baz"]),
252 | productPackageNameMapping: [:])
253 | try editor.addTarget(.test(name: "IntegrationTests", dependencyNames: ["OtherProduct", "goo"]),
254 | productPackageNameMapping: ["goo": "goo", "OtherProduct": "goo"])
255 |
256 | let newManifest = try fs.readFileContents(manifestPath).cString
257 | XCTAssertEqual(newManifest, """
258 | // swift-tools-version:5.2
259 | import PackageDescription
260 |
261 | let package = Package(
262 | name: "exec",
263 | dependencies: [
264 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
265 | ],
266 | targets: [
267 | .target(
268 | name: "foo",
269 | dependencies: []),
270 | .target(
271 | name: "bar",
272 | dependencies: []),
273 | .testTarget(
274 | name: "fooTests",
275 | dependencies: ["foo", "bar"]),
276 | .target(
277 | name: "baz",
278 | dependencies: []
279 | ),
280 | .testTarget(
281 | name: "bazTests",
282 | dependencies: [
283 | "baz",
284 | ]
285 | ),
286 | .target(
287 | name: "qux",
288 | dependencies: [
289 | "foo",
290 | "baz",
291 | ]
292 | ),
293 | .testTarget(
294 | name: "IntegrationTests",
295 | dependencies: [
296 | .product(name: "OtherProduct", package: "goo"),
297 | "goo",
298 | ]
299 | ),
300 | ]
301 | )
302 | """)
303 |
304 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/baz/baz.swift")))
305 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/bazTests/bazTests.swift")))
306 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/qux/main.swift")))
307 | XCTAssertFalse(fs.exists(AbsolutePath("/pkg/Tests/quxTests")))
308 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/IntegrationTests/IntegrationTests.swift")))
309 | }
310 |
311 |
312 | func testAddTarget5_3() throws {
313 | let manifest = """
314 | // swift-tools-version:5.3
315 | import PackageDescription
316 |
317 | let package = Package(
318 | name: "exec",
319 | dependencies: [
320 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
321 | ],
322 | targets: [
323 | .target(
324 | name: "foo",
325 | dependencies: []),
326 | .target(
327 | name: "bar",
328 | dependencies: []),
329 | .testTarget(
330 | name: "fooTests",
331 | dependencies: ["foo", "bar"]),
332 | ]
333 | )
334 | """
335 |
336 | let fs = InMemoryFileSystem(emptyFiles:
337 | "/pkg/Package.swift",
338 | "/pkg/Sources/foo/source.swift",
339 | "/pkg/Sources/bar/source.swift",
340 | "/pkg/Tests/fooTests/source.swift",
341 | "end")
342 |
343 | let manifestPath = AbsolutePath("/pkg/Package.swift")
344 | try fs.writeFileContents(manifestPath) { $0 <<< manifest }
345 |
346 | let diags = DiagnosticsEngine()
347 | let context = try PackageEditorContext(
348 | manifestPath: AbsolutePath("/pkg/Package.swift"),
349 | repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()),
350 | toolchain: Resources.default.toolchain,
351 | diagnosticsEngine: diags,
352 | fs: fs)
353 | let editor = PackageEditor(context: context)
354 |
355 | XCTAssertThrows(Diagnostics.fatalError) {
356 | try editor.addTarget(.library(name: "foo", includeTestTarget: true, dependencyNames: []),
357 | productPackageNameMapping: [:])
358 | }
359 | XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'"])
360 | XCTAssertThrows(Diagnostics.fatalError) {
361 | try editor.addTarget(.library(name: "Error", includeTestTarget: true, dependencyNames: ["NotFound"]),
362 | productPackageNameMapping: [:])
363 | }
364 | XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'",
365 | "could not find a product or target named 'NotFound'"])
366 |
367 | try editor.addTarget(.library(name: "baz", includeTestTarget: true, dependencyNames: []),
368 | productPackageNameMapping: [:])
369 | try editor.addTarget(.executable(name: "qux", dependencyNames: ["foo", "baz"]),
370 | productPackageNameMapping: [:])
371 | try editor.addTarget(.test(name: "IntegrationTests", dependencyNames: ["OtherProduct", "goo"]),
372 | productPackageNameMapping: ["goo": "goo", "OtherProduct": "goo"])
373 | try editor.addTarget(.binary(name: "LocalBinary",
374 | urlOrPath: "/some/local/binary/target.xcframework",
375 | checksum: nil),
376 | productPackageNameMapping: [:])
377 | try editor.addTarget(.binary(name: "RemoteBinary",
378 | urlOrPath: "https://mybinaries.com/RemoteBinary.zip",
379 | checksum: "totallylegitchecksum"),
380 | productPackageNameMapping: [:])
381 |
382 | let newManifest = try fs.readFileContents(manifestPath).cString
383 | XCTAssertEqual(newManifest, """
384 | // swift-tools-version:5.3
385 | import PackageDescription
386 |
387 | let package = Package(
388 | name: "exec",
389 | dependencies: [
390 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
391 | ],
392 | targets: [
393 | .target(
394 | name: "foo",
395 | dependencies: []),
396 | .target(
397 | name: "bar",
398 | dependencies: []),
399 | .testTarget(
400 | name: "fooTests",
401 | dependencies: ["foo", "bar"]),
402 | .target(
403 | name: "baz",
404 | dependencies: []
405 | ),
406 | .testTarget(
407 | name: "bazTests",
408 | dependencies: [
409 | "baz",
410 | ]
411 | ),
412 | .target(
413 | name: "qux",
414 | dependencies: [
415 | "foo",
416 | "baz",
417 | ]
418 | ),
419 | .testTarget(
420 | name: "IntegrationTests",
421 | dependencies: [
422 | .product(name: "OtherProduct", package: "goo"),
423 | "goo",
424 | ]
425 | ),
426 | .binaryTarget(
427 | name: "LocalBinary",
428 | path: "/some/local/binary/target.xcframework"
429 | ),
430 | .binaryTarget(
431 | name: "RemoteBinary",
432 | url: "https://mybinaries.com/RemoteBinary.zip",
433 | checksum: "totallylegitchecksum"
434 | ),
435 | ]
436 | )
437 | """)
438 |
439 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/baz/baz.swift")))
440 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/bazTests/bazTests.swift")))
441 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/qux/main.swift")))
442 | XCTAssertFalse(fs.exists(AbsolutePath("/pkg/Tests/quxTests")))
443 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/IntegrationTests/IntegrationTests.swift")))
444 | }
445 |
446 | func testAddTarget5_4_to_5_5() throws {
447 | for version in ["5.4", "5.5"] {
448 | let manifest = """
449 | // swift-tools-version:\(version)
450 | import PackageDescription
451 |
452 | let package = Package(
453 | name: "exec",
454 | dependencies: [
455 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
456 | ],
457 | targets: [
458 | .target(
459 | name: "foo",
460 | dependencies: []),
461 | .target(
462 | name: "bar",
463 | dependencies: []),
464 | .testTarget(
465 | name: "fooTests",
466 | dependencies: ["foo", "bar"]),
467 | ]
468 | )
469 | """
470 |
471 | let fs = InMemoryFileSystem(emptyFiles:
472 | "/pkg/Package.swift",
473 | "/pkg/Sources/foo/source.swift",
474 | "/pkg/Sources/bar/source.swift",
475 | "/pkg/Tests/fooTests/source.swift",
476 | "end")
477 |
478 | let manifestPath = AbsolutePath("/pkg/Package.swift")
479 | try fs.writeFileContents(manifestPath) { $0 <<< manifest }
480 |
481 | let diags = DiagnosticsEngine()
482 | let context = try PackageEditorContext(
483 | manifestPath: AbsolutePath("/pkg/Package.swift"),
484 | repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()),
485 | toolchain: Resources.default.toolchain,
486 | diagnosticsEngine: diags,
487 | fs: fs)
488 | let editor = PackageEditor(context: context)
489 |
490 | XCTAssertThrows(Diagnostics.fatalError) {
491 | try editor.addTarget(.library(name: "foo", includeTestTarget: true, dependencyNames: []),
492 | productPackageNameMapping: [:])
493 | }
494 | XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'"])
495 | XCTAssertThrows(Diagnostics.fatalError) {
496 | try editor.addTarget(.library(name: "Error", includeTestTarget: true, dependencyNames: ["NotFound"]),
497 | productPackageNameMapping: [:])
498 | }
499 | XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'",
500 | "could not find a product or target named 'NotFound'"])
501 |
502 | try editor.addTarget(.library(name: "baz", includeTestTarget: true, dependencyNames: []),
503 | productPackageNameMapping: [:])
504 | try editor.addTarget(.executable(name: "qux", dependencyNames: ["foo", "baz"]),
505 | productPackageNameMapping: [:])
506 | try editor.addTarget(.test(name: "IntegrationTests", dependencyNames: ["OtherProduct", "goo"]),
507 | productPackageNameMapping: ["goo": "goo", "OtherProduct": "goo"])
508 | try editor.addTarget(.binary(name: "LocalBinary",
509 | urlOrPath: "/some/local/binary/target.xcframework",
510 | checksum: nil),
511 | productPackageNameMapping: [:])
512 | try editor.addTarget(.binary(name: "RemoteBinary",
513 | urlOrPath: "https://mybinaries.com/RemoteBinary.zip",
514 | checksum: "totallylegitchecksum"),
515 | productPackageNameMapping: [:])
516 |
517 | let newManifest = try fs.readFileContents(manifestPath).cString
518 | XCTAssertEqual(newManifest, """
519 | // swift-tools-version:\(version)
520 | import PackageDescription
521 |
522 | let package = Package(
523 | name: "exec",
524 | dependencies: [
525 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
526 | ],
527 | targets: [
528 | .target(
529 | name: "foo",
530 | dependencies: []),
531 | .target(
532 | name: "bar",
533 | dependencies: []),
534 | .testTarget(
535 | name: "fooTests",
536 | dependencies: ["foo", "bar"]),
537 | .target(
538 | name: "baz",
539 | dependencies: []
540 | ),
541 | .testTarget(
542 | name: "bazTests",
543 | dependencies: [
544 | "baz",
545 | ]
546 | ),
547 | .executableTarget(
548 | name: "qux",
549 | dependencies: [
550 | "foo",
551 | "baz",
552 | ]
553 | ),
554 | .testTarget(
555 | name: "IntegrationTests",
556 | dependencies: [
557 | .product(name: "OtherProduct", package: "goo"),
558 | "goo",
559 | ]
560 | ),
561 | .binaryTarget(
562 | name: "LocalBinary",
563 | path: "/some/local/binary/target.xcframework"
564 | ),
565 | .binaryTarget(
566 | name: "RemoteBinary",
567 | url: "https://mybinaries.com/RemoteBinary.zip",
568 | checksum: "totallylegitchecksum"
569 | ),
570 | ]
571 | )
572 | """)
573 |
574 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/baz/baz.swift")))
575 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/bazTests/bazTests.swift")))
576 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/qux/main.swift")))
577 | XCTAssertFalse(fs.exists(AbsolutePath("/pkg/Tests/quxTests")))
578 | XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/IntegrationTests/IntegrationTests.swift")))
579 | }
580 | }
581 |
582 | func testAddProduct5_2_to_5_5() throws {
583 | for version in ["5.2", "5.3", "5.4", "5.5"] {
584 | let manifest = """
585 | // swift-tools-version:\(version)
586 | import PackageDescription
587 |
588 | let package = Package(
589 | name: "exec",
590 | products: [
591 | .executable(name: "abc", targets: ["foo"]),
592 | ],
593 | dependencies: [
594 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
595 | ],
596 | targets: [
597 | .target(
598 | name: "foo",
599 | dependencies: []),
600 | .target(
601 | name: "bar",
602 | dependencies: []),
603 | .testTarget(
604 | name: "fooTests",
605 | dependencies: ["foo", "bar"]),
606 | ]
607 | )
608 | """
609 |
610 | let fs = InMemoryFileSystem(emptyFiles:
611 | "/pkg/Package.swift",
612 | "/pkg/Sources/foo/source.swift",
613 | "/pkg/Sources/bar/source.swift",
614 | "/pkg/Tests/fooTests/source.swift",
615 | "end")
616 |
617 | let manifestPath = AbsolutePath("/pkg/Package.swift")
618 | try fs.writeFileContents(manifestPath) { $0 <<< manifest }
619 |
620 | let diags = DiagnosticsEngine()
621 | let context = try PackageEditorContext(
622 | manifestPath: AbsolutePath("/pkg/Package.swift"),
623 | repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()),
624 | toolchain: Resources.default.toolchain,
625 | diagnosticsEngine: diags,
626 | fs: fs)
627 | let editor = PackageEditor(context: context)
628 |
629 | XCTAssertThrows(Diagnostics.fatalError) {
630 | try editor.addProduct(name: "abc", type: .library(.automatic), targets: [])
631 | }
632 |
633 | XCTAssertThrows(Diagnostics.fatalError) {
634 | try editor.addProduct(name: "SomeProduct", type: .library(.automatic), targets: ["nonexistent"])
635 | }
636 |
637 | XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a product named 'abc' already exists in 'exec'",
638 | "no target named 'nonexistent' in 'exec'"])
639 |
640 | try editor.addProduct(name: "xyz", type: .executable, targets: ["bar"])
641 | try editor.addProduct(name: "libxyz", type: .library(.dynamic), targets: ["foo", "bar"])
642 |
643 | let newManifest = try fs.readFileContents(manifestPath).cString
644 | XCTAssertEqual(newManifest, """
645 | // swift-tools-version:\(version)
646 | import PackageDescription
647 |
648 | let package = Package(
649 | name: "exec",
650 | products: [
651 | .executable(name: "abc", targets: ["foo"]),
652 | .executable(
653 | name: "xyz",
654 | targets: [
655 | "bar",
656 | ]
657 | ),
658 | .library(
659 | name: "libxyz",
660 | type: .dynamic,
661 | targets: [
662 | "foo",
663 | "bar",
664 | ]
665 | ),
666 | ],
667 | dependencies: [
668 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
669 | ],
670 | targets: [
671 | .target(
672 | name: "foo",
673 | dependencies: []),
674 | .target(
675 | name: "bar",
676 | dependencies: []),
677 | .testTarget(
678 | name: "fooTests",
679 | dependencies: ["foo", "bar"]),
680 | ]
681 | )
682 | """)
683 | }
684 | }
685 |
686 | func testToolsVersionTest() throws {
687 | let manifest = """
688 | // swift-tools-version:5.0
689 | import PackageDescription
690 |
691 | let package = Package(
692 | name: "exec",
693 | dependencies: [
694 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
695 | ],
696 | targets: [
697 | .target(
698 | name: "foo",
699 | dependencies: []),
700 | ]
701 | )
702 | """
703 |
704 | let fs = InMemoryFileSystem(emptyFiles:
705 | "/pkg/Package.swift",
706 | "/pkg/Sources/foo/source.swift",
707 | "end")
708 |
709 | let manifestPath = AbsolutePath("/pkg/Package.swift")
710 | try fs.writeFileContents(manifestPath) { $0 <<< manifest }
711 |
712 | let diags = DiagnosticsEngine()
713 | let context = try PackageEditorContext(
714 | manifestPath: AbsolutePath("/pkg/Package.swift"),
715 | repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()),
716 | toolchain: Resources.default.toolchain, diagnosticsEngine: diags, fs: fs)
717 | let editor = PackageEditor(context: context)
718 |
719 | XCTAssertThrows(Diagnostics.fatalError) {
720 | try editor.addTarget(.library(name: "bar", includeTestTarget: true, dependencyNames: []),
721 | productPackageNameMapping: [:])
722 | }
723 | XCTAssertEqual(diags.diagnostics.map(\.message.text),
724 | ["command line editing of manifests is only supported for packages with a swift-tools-version of 5.2 and later"])
725 | }
726 |
727 | func testEditingManifestsWithComplexArgumentExpressions() throws {
728 | let manifest = """
729 | // swift-tools-version:5.3
730 | import PackageDescription
731 |
732 | let flag = false
733 | let extraDeps: [Package.Dependency] = []
734 |
735 | let package = Package(
736 | name: "exec",
737 | products: [
738 | .library(name: "Library", targets: ["foo"])
739 | ].filter { _ in true },
740 | dependencies: extraDeps + [
741 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
742 | ],
743 | targets: flag ? [] : [
744 | .target(
745 | name: "foo",
746 | dependencies: []),
747 | ]
748 | )
749 | """
750 |
751 | let fs = InMemoryFileSystem(emptyFiles:
752 | "/pkg/Package.swift",
753 | "/pkg/Sources/foo/source.swift",
754 | "end")
755 |
756 | let manifestPath = AbsolutePath("/pkg/Package.swift")
757 | try fs.writeFileContents(manifestPath) { $0 <<< manifest }
758 |
759 | try fs.createDirectory(.init("/pkg/repositories"), recursive: false)
760 | try fs.createDirectory(.init("/pkg/repo"), recursive: false)
761 |
762 |
763 | let provider = InMemoryGitRepositoryProvider()
764 | let repo = InMemoryGitRepository(path: .init("/pkg/repo"), fs: fs)
765 | try repo.writeFileContents(.init("/Package.swift"), bytes: .init(encodingAsUTF8: """
766 | // swift-tools-version:5.2
767 | import PackageDescription
768 |
769 | let package = Package(name: "repo")
770 | """))
771 | try repo.commit()
772 | try repo.tag(name: "1.1.1")
773 | provider.add(specifier: .init(url: "http://www.githost.com/repo"), repository: repo)
774 |
775 | let diags = DiagnosticsEngine()
776 | let context = try PackageEditorContext(
777 | manifestPath: AbsolutePath("/pkg/Package.swift"),
778 | repositoryManager: RepositoryManager(path: .init("/pkg/repositories"),
779 | provider: provider,
780 | fileSystem: fs),
781 | toolchain: Resources.default.toolchain,
782 | diagnosticsEngine: diags,
783 | fs: fs)
784 | let editor = PackageEditor(context: context)
785 | try editor.addPackageDependency(url: "http://www.githost.com/repo.git", requirement: .exact("1.1.1"))
786 | XCTAssertThrows(Diagnostics.fatalError) {
787 | try editor.addTarget(.library(name: "Library", includeTestTarget: false, dependencyNames: []),
788 | productPackageNameMapping: [:])
789 | }
790 | XCTAssertEqual(diags.diagnostics.map(\.message.text),
791 | ["'targets' argument is not an array literal or concatenation of array literals"])
792 | XCTAssertThrows(Diagnostics.fatalError) {
793 | try editor.addProduct(name: "Executable", type: .executable, targets: ["foo"])
794 | }
795 | XCTAssertEqual(diags.diagnostics.map(\.message.text),
796 | ["'targets' argument is not an array literal or concatenation of array literals",
797 | "'products' argument is not an array literal or concatenation of array literals"])
798 |
799 | let newManifest = try fs.readFileContents(manifestPath).cString
800 | XCTAssertEqual(newManifest, """
801 | // swift-tools-version:5.3
802 | import PackageDescription
803 |
804 | let flag = false
805 | let extraDeps: [Package.Dependency] = []
806 |
807 | let package = Package(
808 | name: "exec",
809 | products: [
810 | .library(name: "Library", targets: ["foo"])
811 | ].filter { _ in true },
812 | dependencies: extraDeps + [
813 | .package(url: "https://github.com/foo/goo", from: "1.0.1"),
814 | .package(name: "repo", url: "http://www.githost.com/repo.git", .exact("1.1.1")),
815 | ],
816 | targets: flag ? [] : [
817 | .target(
818 | name: "foo",
819 | dependencies: []),
820 | ]
821 | )
822 | """)
823 | }
824 |
825 | func testEditingConditionalPackageInit() throws {
826 | let manifest = """
827 | // swift-tools-version:5.3
828 | import PackageDescription
829 |
830 | #if os(macOS)
831 | let package = Package(
832 | name: "macOSPackage"
833 | )
834 | #else
835 | let package = Package(
836 | name: "otherPlatformsPackage"
837 | )
838 | #endif
839 | """
840 |
841 | let fs = InMemoryFileSystem(emptyFiles:
842 | "/pkg/Package.swift",
843 | "/pkg/Sources/foo/source.swift",
844 | "end")
845 |
846 | let manifestPath = AbsolutePath("/pkg/Package.swift")
847 | try fs.writeFileContents(manifestPath) { $0 <<< manifest }
848 |
849 | try fs.createDirectory(.init("/pkg/repositories"), recursive: false)
850 |
851 | let diags = DiagnosticsEngine()
852 | let context = try PackageEditorContext(
853 | manifestPath: AbsolutePath("/pkg/Package.swift"),
854 | repositoryManager: RepositoryManager(path: .init("/pkg/repositories"),
855 | provider: InMemoryGitRepositoryProvider(),
856 | fileSystem: fs),
857 | toolchain: Resources.default.toolchain,
858 | diagnosticsEngine: diags,
859 | fs: fs)
860 | let editor = PackageEditor(context: context)
861 | XCTAssertThrows(Diagnostics.fatalError) {
862 | try editor.addTarget(.library(name: "Library", includeTestTarget: false, dependencyNames: []),
863 | productPackageNameMapping: [:])
864 | }
865 | XCTAssertEqual(diags.diagnostics.map(\.message.text), ["found multiple Package initializers"])
866 | }
867 | }
868 |
--------------------------------------------------------------------------------
/Tests/PackageSyntaxTests/Resources.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | // FIXME: Share with SwiftPM
12 |
13 | import TSCBasic
14 | import SPMBuildCore
15 | import Foundation
16 | import PackageLoading
17 | import Workspace
18 |
19 | #if os(macOS)
20 | private func bundleRoot() -> AbsolutePath {
21 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
22 | return AbsolutePath(bundle.bundlePath).parentDirectory
23 | }
24 | fatalError()
25 | }
26 | #endif
27 |
28 | public class Resources: ManifestResourceProvider {
29 |
30 | public var swiftCompiler: AbsolutePath {
31 | return toolchain.manifestResources.swiftCompiler
32 | }
33 |
34 | public var libDir: AbsolutePath {
35 | return toolchain.manifestResources.libDir
36 | }
37 |
38 | public var binDir: AbsolutePath? {
39 | return toolchain.manifestResources.binDir
40 | }
41 |
42 | public var swiftCompilerFlags: [String] {
43 | return []
44 | }
45 |
46 | #if os(macOS)
47 | public var sdkPlatformFrameworksPath: AbsolutePath {
48 | return Destination.sdkPlatformFrameworkPaths()!.fwk
49 | }
50 | #endif
51 |
52 | public let toolchain: UserToolchain
53 |
54 | public static let `default` = Resources()
55 |
56 | private init() {
57 | let binDir: AbsolutePath
58 | #if os(macOS)
59 | binDir = bundleRoot()
60 | #else
61 | binDir = AbsolutePath(CommandLine.arguments[0], relativeTo: localFileSystem.currentWorkingDirectory!).parentDirectory
62 | #endif
63 | toolchain = try! UserToolchain(destination: Destination.hostDestination(binDir))
64 | }
65 |
66 | /// True if SwiftPM has PackageDescription 4 runtime available.
67 | public static var havePD4Runtime: Bool {
68 | return Resources.default.binDir == nil
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Tests/PackageSyntaxTests/TestSupport.swift:
--------------------------------------------------------------------------------
1 | /*
2 | This source file is part of the Swift.org open source project
3 |
4 | Copyright (c) 2021 Apple Inc. and the Swift project authors
5 | Licensed under Apache License v2.0 with Runtime Library Exception
6 |
7 | See http://swift.org/LICENSE.txt for license information
8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9 | */
10 |
11 | // FIXME: Find a way to share this with SwiftPM
12 | import TSCBasic
13 | import XCTest
14 |
15 | extension InMemoryFileSystem {
16 | /// Create a new file system with an empty file at each provided path.
17 | convenience init(emptyFiles files: String...) {
18 | self.init(emptyFiles: files)
19 | }
20 |
21 | /// Create a new file system with an empty file at each provided path.
22 | convenience init(emptyFiles files: [String]) {
23 | self.init()
24 | self.createEmptyFiles(at: .root, files: files)
25 | }
26 | }
27 |
28 | extension FileSystem {
29 | func createEmptyFiles(at root: AbsolutePath, files: String...) {
30 | self.createEmptyFiles(at: root, files: files)
31 | }
32 |
33 | func createEmptyFiles(at root: AbsolutePath, files: [String]) {
34 | do {
35 | try createDirectory(root, recursive: true)
36 | for path in files {
37 | let path = root.appending(RelativePath(String(path.dropFirst())))
38 | try createDirectory(path.parentDirectory, recursive: true)
39 | try writeFileContents(path, bytes: "")
40 | }
41 | } catch {
42 | fatalError("Failed to create empty files: \(error)")
43 | }
44 | }
45 | }
46 |
47 | func XCTAssertThrows(
48 | _ expectedError: T,
49 | file: StaticString = #file,
50 | line: UInt = #line,
51 | _ body: () throws -> Void
52 | ) where T: Equatable {
53 | do {
54 | try body()
55 | XCTFail("body completed successfully", file: file, line: line)
56 | } catch let error as T {
57 | XCTAssertEqual(error, expectedError, file: file, line: line)
58 | } catch {
59 | XCTFail("unexpected error thrown: \(error)", file: file, line: line)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Utilities/build-script-helper.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from __future__ import print_function
4 |
5 | import argparse
6 | import os
7 | import platform
8 | import shutil
9 | import subprocess
10 | import sys
11 |
12 | def swiftpm(action, swift_exec, swiftpm_args, env=None):
13 | cmd = [swift_exec, action] + swiftpm_args
14 | print(' '.join(cmd))
15 | subprocess.check_call(cmd, env=env)
16 |
17 | def swiftpm_bin_path(swift_exec, swiftpm_args, env=None):
18 | swiftpm_args = list(filter(lambda arg: arg != '-v' and arg != '--verbose', swiftpm_args))
19 | cmd = [swift_exec, 'build', '--show-bin-path'] + swiftpm_args
20 | print(' '.join(cmd))
21 | return subprocess.check_output(cmd, env=env, universal_newlines=True).strip()
22 |
23 | def get_swiftpm_options(args):
24 | swiftpm_args = [
25 | '--package-path', args.package_path,
26 | '--build-path', args.build_path,
27 | '--configuration', args.configuration,
28 | ]
29 |
30 | if args.verbose:
31 | swiftpm_args += ['--verbose']
32 |
33 | if platform.system() == 'Darwin':
34 | swiftpm_args += [
35 | # Relative library rpath for swift; will only be used when /usr/lib/swift
36 | # is not available.
37 | '-Xlinker', '-rpath', '-Xlinker', '@executable_path/../lib/swift/macosx',
38 | ]
39 | else:
40 | swiftpm_args += [
41 | # Dispatch headers
42 | '-Xcxx', '-I', '-Xcxx',
43 | os.path.join(args.toolchain, 'lib', 'swift'),
44 | # For
45 | '-Xcxx', '-I', '-Xcxx',
46 | os.path.join(args.toolchain, 'lib', 'swift', 'Block'),
47 | ]
48 |
49 | if 'ANDROID_DATA' in os.environ:
50 | swiftpm_args += [
51 | '-Xlinker', '-rpath', '-Xlinker', '$ORIGIN/../lib/swift/android',
52 | # SwiftPM will otherwise try to compile against GNU strerror_r on
53 | # Android and fail.
54 | '-Xswiftc', '-Xcc', '-Xswiftc', '-U_GNU_SOURCE',
55 | ]
56 | else:
57 | # Library rpath for swift, dispatch, Foundation, etc. when installing
58 | swiftpm_args += [
59 | '-Xlinker', '-rpath', '-Xlinker', '$ORIGIN/../lib/swift/linux',
60 | ]
61 |
62 | return swiftpm_args
63 |
64 | def install(swiftpm_bin_path, toolchain):
65 | toolchain_bin = os.path.join(toolchain, 'bin')
66 | for exe in ['swift-package-editor']:
67 | install_binary(exe, swiftpm_bin_path, toolchain_bin, toolchain)
68 |
69 | def install_binary(exe, source_dir, install_dir, toolchain):
70 | cmd = ['rsync', '-a', os.path.join(source_dir, exe), install_dir]
71 | print(' '.join(cmd))
72 | subprocess.check_call(cmd)
73 |
74 | if platform.system() == 'Darwin':
75 | result_path = os.path.join(install_dir, exe)
76 | stdlib_rpath = os.path.join(toolchain, 'lib', 'swift', 'macosx')
77 | delete_rpath(stdlib_rpath, result_path)
78 |
79 | def delete_rpath(rpath, binary):
80 | cmd = ["install_name_tool", "-delete_rpath", rpath, binary]
81 | print(' '.join(cmd))
82 | subprocess.check_call(cmd)
83 |
84 |
85 | def handle_invocation(swift_exec, args):
86 | swiftpm_args = get_swiftpm_options(args)
87 |
88 | env = os.environ
89 |
90 | # Use local dependencies.
91 | if not args.no_local_deps:
92 | env['SWIFTCI_USE_LOCAL_DEPS'] = "1"
93 |
94 | print('Cleaning ' + args.build_path)
95 | shutil.rmtree(args.build_path, ignore_errors=True)
96 |
97 | if args.action == 'build':
98 | swiftpm('build', swift_exec, swiftpm_args, env)
99 | elif args.action == 'test':
100 | bin_path = swiftpm_bin_path(swift_exec, swiftpm_args, env)
101 | swiftpm('test', swift_exec, swiftpm_args, env)
102 | elif args.action == 'install':
103 | bin_path = swiftpm_bin_path(swift_exec, swiftpm_args, env)
104 | swiftpm_args += ['-Xswiftc', '-no-toolchain-stdlib-rpath']
105 | swiftpm('build', swift_exec, swiftpm_args, env)
106 | install(bin_path, args.toolchain)
107 | else:
108 | assert False, 'unknown action \'{}\''.format(args.action)
109 |
110 |
111 | def main():
112 | parser = argparse.ArgumentParser(description='Build along with the Swift build-script.')
113 | def add_common_args(parser):
114 | parser.add_argument('--package-path', metavar='PATH', help='directory of the package to build', default='.')
115 | parser.add_argument('--toolchain', required=True, metavar='PATH', help='build using the toolchain at PATH')
116 | parser.add_argument('--build-path', metavar='PATH', default='.build', help='build in the given path')
117 | parser.add_argument('--configuration', '-c', default='debug', help='build using configuration (release|debug)')
118 | parser.add_argument('--no-local-deps', action='store_true', help='use normal remote dependencies when building')
119 | parser.add_argument('--verbose', '-v', action='store_true', help='enable verbose output')
120 |
121 | subparsers = parser.add_subparsers(title='subcommands', dest='action', metavar='action')
122 | build_parser = subparsers.add_parser('build', help='build the package')
123 | add_common_args(build_parser)
124 |
125 | test_parser = subparsers.add_parser('test', help='test the package')
126 | add_common_args(test_parser)
127 |
128 | install_parser = subparsers.add_parser('install', help='build the package')
129 | add_common_args(install_parser)
130 |
131 | args = parser.parse_args(sys.argv[1:])
132 |
133 | # Canonicalize paths
134 | args.package_path = os.path.abspath(args.package_path)
135 | args.build_path = os.path.abspath(args.build_path)
136 | args.toolchain = os.path.abspath(args.toolchain)
137 |
138 | if args.toolchain:
139 | swift_exec = os.path.join(args.toolchain, 'bin', 'swift')
140 | else:
141 | swift_exec = 'swift'
142 |
143 | handle_invocation(swift_exec, args)
144 |
145 | if __name__ == '__main__':
146 | main()
147 |
--------------------------------------------------------------------------------