├── .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 | --------------------------------------------------------------------------------