diff --git a/.travis.yml b/.travis.yml index 0f24d0de..e5795169 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,3 +46,25 @@ matrix: - sudo chmod -R a+rwx /usr/local/ - make install - DEBUG="*" danger-swift ci + + - os: osx + name: Danger with SPM + osx_image: xcode10 + install: + - node -v + - npm install -g danger + script: + - swift run danger-swift ci + + - os: linux + name: Danger with SPM + language: generic + sudo: required + dist: trusty + install: + - node -v + - npm install -g danger + - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" + - swiftenv global 4.2 + script: + - swift run danger-swift ci diff --git a/CHANGELOG.md b/CHANGELOG.md index c50861d0..8a78bdc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ## Master +- Support a full Danger SPM usage [#174](https://github.com/danger/danger-swift/pull/174) by [@f-meloni][] - Replace codable where was not needed by [@f-meloni][] - [#177](https://github.com/danger/swift/pull/177) - Fix malformed Swiftlint inline paths by [@absolute-heike][] - [#176](https://github.com/danger/swift/pull/176) diff --git a/Package.swift b/Package.swift index b451797f..b3c0cbca 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,7 @@ let package = Package( products: [ .library(name: "Danger", type: .dynamic, targets: ["Danger"]), .library(name: "DangerFixtures", type: .dynamic, targets: ["DangerFixtures"]), + .library(name: "DangerDeps", type: .dynamic, targets: ["Danger-Swift"]), // dev .executable(name: "danger-swift", targets: ["Runner"]), ], dependencies: [ @@ -23,8 +24,10 @@ let package = Package( .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.35.8"), // dev .package(url: "https://github.com/Realm/SwiftLint", from: "0.28.1"), // dev .package(url: "https://github.com/f-meloni/Rocket", from: "0.4.0"), // dev + .package(url: "https://github.com/jpsim/Yams.git", from: "1.0.0"), // dev ], targets: [ + .target(name: "Danger-Swift", dependencies: ["Danger", "Yams"]), // dev .target(name: "Danger", dependencies: ["ShellOut", "OctoKit", "Logger"]), .target(name: "RunnerLib", dependencies: ["Logger", "ShellOut"]), .target(name: "Runner", dependencies: ["RunnerLib", "MarathonCore", "Logger"]), diff --git a/Sources/Danger-Swift/Fake.swift b/Sources/Danger-Swift/Fake.swift new file mode 100644 index 00000000..e69de29b diff --git a/Sources/Runner/Commands/Edit.swift b/Sources/Runner/Commands/Edit.swift index 0c10cce9..c34f5174 100644 --- a/Sources/Runner/Commands/Edit.swift +++ b/Sources/Runner/Commands/Edit.swift @@ -20,12 +20,24 @@ func editDanger(logger: Logger) throws { // If dangerfile was not found, attempt to create one at Dangerfile.swift let dangerfilePath = Runtime.getDangerfile() ?? createDangerfile() - guard let libPath = Runtime.getLibDangerPath() else { - let potentialFolders = Runtime.potentialLibraryFolders - logger.logError("Could not find a libDanger to link against at any of: \(potentialFolders)", - "Or via Homebrew, or Marathon", - separator: "\n") - exit(1) + let absoluteLibPath: String + let libName: String + + if let spmDanger = SPMDanger() { + spmDanger.buildDepsIfNeeded() + absoluteLibPath = FileManager.default.currentDirectoryPath + "/" + SPMDanger.buildFolder + libName = spmDanger.depsLibName + } else { + guard let libPath = Runtime.getLibDangerPath() else { + let potentialFolders = Runtime.potentialLibraryFolders + logger.logError("Could not find a libDanger to link against at any of: \(potentialFolders)", + "Or via Homebrew, or Marathon", + separator: "\n") + exit(1) + } + + absoluteLibPath = try Folder(path: libPath).path + libName = "Danger" } guard let dangerfileContent = try? File(path: dangerfilePath).readAsString() else { @@ -36,16 +48,13 @@ func editDanger(logger: Logger) throws { let importsFinder = ImportsFinder() let importedFiles = importsFinder.findImports(inString: dangerfileContent) - let absoluteLibPath = try Folder(path: libPath).path - let arguments = CommandLine.arguments let scriptManager = try getScriptManager(logger) let script = try scriptManager.script(atPath: dangerfilePath, allowRemote: true) - let path = NSTemporaryDirectory() - let configPath = path + "config.xcconfig" + let configPath = NSTemporaryDirectory() + "config.xcconfig" - try createConfig(atPath: configPath, lib: absoluteLibPath) + try createConfig(atPath: configPath, libPath: absoluteLibPath, libName: libName) try script.setupForEdit(arguments: arguments, importedFiles: importedFiles, configPath: configPath) diff --git a/Sources/Runner/Commands/Runner.swift b/Sources/Runner/Commands/Runner.swift index d4ca4225..501bf8a6 100644 --- a/Sources/Runner/Commands/Runner.swift +++ b/Sources/Runner/Commands/Runner.swift @@ -48,45 +48,56 @@ func runDanger(logger: Logger) throws { } logger.debug("Running Dangerfile at: \(dangerfilePath)") - guard let libDangerPath = Runtime.getLibDangerPath() else { - let potentialFolders = Runtime.potentialLibraryFolders - logger.logError("Could not find a libDanger to link against at any of: \(potentialFolders)", - "Or via Homebrew, or Marathon", - separator: "\n") - exit(1) - } - var libArgs: [String] = [] - libArgs += ["-L", libDangerPath] // Link to libDanger inside this folder - libArgs += ["-I", libDangerPath] // Find libDanger inside this folder // Set up plugin infra let importsOnly = try File(path: dangerfilePath).readAsString() - let importExternalDeps = importsOnly.components(separatedBy: .newlines).filter { $0.hasPrefix("import") && $0.contains("package: ") } // swiftlint:disable:this line_length - - if importExternalDeps.count > 0 { - logger.logInfo("Cloning and building inline dependencies:", - "\(importExternalDeps.joined(separator: ", ")),", - "this might take some time.") - - try Folder(path: ".").createFileIfNeeded(withName: "_dangerfile_imports.swift") - let tempDangerfile = try File(path: "_dangerfile_imports.swift") - try tempDangerfile.write(string: importExternalDeps.joined(separator: "\n")) - defer { try? tempDangerfile.delete() } - - let scriptManager = try getScriptManager(logger) - let script = try scriptManager.script(atPath: tempDangerfile.path, allowRemote: true) - - try script.build() - let marathonPath = script.folder.path - let artifactPaths = [".build/debug", ".build/release"] - - let marathonLibPath = artifactPaths.first(where: { fileManager.fileExists(atPath: marathonPath + $0) }) - if marathonLibPath != nil { - libArgs += ["-L", marathonPath + marathonLibPath!] - libArgs += ["-I", marathonPath + marathonLibPath!] - libArgs += ["-lMarathonDependencies"] + + if let spmDanger = SPMDanger() { + spmDanger.buildDepsIfNeeded() + libArgs += ["-L", SPMDanger.buildFolder] + libArgs += ["-I", SPMDanger.buildFolder] + libArgs += [spmDanger.libImport] + } else { + guard let libDangerPath = Runtime.getLibDangerPath() else { + let potentialFolders = Runtime.potentialLibraryFolders + logger.logError("Could not find a libDanger to link against at any of: \(potentialFolders)", + "Or via Homebrew, or Marathon", + separator: "\n") + exit(1) + } + + libArgs += ["-L", libDangerPath] // Link to libDanger inside this folder + libArgs += ["-I", libDangerPath] // Find libDanger inside this folder + + let importExternalDeps = importsOnly.components(separatedBy: .newlines).filter { $0.hasPrefix("import") && $0.contains("package: ") } // swiftlint:disable:this line_length + + if importExternalDeps.count > 0 { + logger.logInfo("Cloning and building inline dependencies:", + "\(importExternalDeps.joined(separator: ", ")),", + "this might take some time.") + + try Folder(path: ".").createFileIfNeeded(withName: "_dangerfile_imports.swift") + let tempDangerfile = try File(path: "_dangerfile_imports.swift") + try tempDangerfile.write(string: importExternalDeps.joined(separator: "\n")) + defer { try? tempDangerfile.delete() } + + let scriptManager = try getScriptManager(logger) + let script = try scriptManager.script(atPath: tempDangerfile.path, allowRemote: true) + + try script.build() + let marathonPath = script.folder.path + let artifactPaths = [".build/debug", ".build/release"] + + let marathonLibPath = artifactPaths.first(where: { fileManager.fileExists(atPath: marathonPath + $0) }) + if marathonLibPath != nil { + libArgs += ["-L", marathonPath + marathonLibPath!] + libArgs += ["-I", marathonPath + marathonLibPath!] + libArgs += ["-lMarathonDependencies"] + } } + + libArgs += ["-lDanger"] // Eval the code with the Target Danger added } logger.debug("Preparing to compile") @@ -112,9 +123,6 @@ func runDanger(logger: Logger) throws { var args = [String]() args += ["--driver-mode=swift"] // Eval in swift mode, I think? - args += ["-L", libDangerPath] // Find libs inside this folder - args += ["-I", libDangerPath] // Find libs inside this folder - args += ["-lDanger"] // Eval the code with the Target Danger added args += libArgs args += [tempDangerfilePath] // The Dangerfile args += Array(CommandLine.arguments.dropFirst()) // Arguments sent to Danger diff --git a/Sources/Runner/EditXcodeProj.swift b/Sources/Runner/EditXcodeProj.swift index 0edf4092..60b808e8 100644 --- a/Sources/Runner/EditXcodeProj.swift +++ b/Sources/Runner/EditXcodeProj.swift @@ -2,11 +2,11 @@ import Files import Foundation // Creates an xcconfig file that can be used to correctly link danger library to the xcodeproj -func createConfig(atPath configPath: String, lib: String) throws { +func createConfig(atPath configPath: String, libPath: String, libName: String) throws { let config = """ - LIBRARY_SEARCH_PATHS = \(lib) - OTHER_SWIFT_FLAGS = -DXcode -I \(lib) -L \(lib) - OTHER_LDFLAGS = -l danger + LIBRARY_SEARCH_PATHS = \(libPath) + OTHER_SWIFT_FLAGS = -DXcode -I \(libPath) -L \(libPath) + OTHER_LDFLAGS = -l \(libName) """ try config.write(toFile: configPath, atomically: false, encoding: .utf8) diff --git a/Sources/Runner/main.swift b/Sources/Runner/main.swift index 01612d3f..d8ae0c41 100644 --- a/Sources/Runner/main.swift +++ b/Sources/Runner/main.swift @@ -3,8 +3,8 @@ import Logger import RunnerLib /// Version for showing in verbose mode -let DangerVersion = "1.1.0" -let MinimumDangerJSVersion = "6.1.6" +let DangerVersion = "1.1.0" // swiftlint:disable:this identifier_name +let MinimumDangerJSVersion = "6.1.6" // swiftlint:disable:this identifier_name private func runCommand(_ command: DangerCommand, logger: Logger) throws { switch command { @@ -19,11 +19,11 @@ private func runCommand(_ command: DangerCommand, logger: Logger) throws { } let cliLength = ProcessInfo.processInfo.arguments.count -do { - let isVerbose = CommandLine.arguments.contains("--verbose") || (ProcessInfo.processInfo.environment["DEBUG"] != nil) - let isSilent = CommandLine.arguments.contains("--silent") - let logger = Logger(isVerbose: isVerbose, isSilent: isSilent) +let isVerbose = CommandLine.arguments.contains("--verbose") || (ProcessInfo.processInfo.environment["DEBUG"] != nil) +let isSilent = CommandLine.arguments.contains("--silent") +let logger = Logger(isVerbose: isVerbose, isSilent: isSilent) +do { if cliLength > 1 { logger.debug("Launching Danger Swift \(CommandLine.arguments[1]) (v\(DangerVersion))") @@ -43,5 +43,6 @@ do { try runDanger(logger: logger) } } catch { + logger.logError(error) exit(1) } diff --git a/Sources/RunnerLib/DangerJSVersionFinder.swift b/Sources/RunnerLib/DangerJSVersionFinder.swift index 1436883a..5d3966da 100644 --- a/Sources/RunnerLib/DangerJSVersionFinder.swift +++ b/Sources/RunnerLib/DangerJSVersionFinder.swift @@ -14,10 +14,13 @@ public final class DangerJSVersionFinder { } public protocol ShellOutExecuting { + @discardableResult func shellOut(command: String) throws -> String } public struct ShellOutExecutor: ShellOutExecuting { + public init() {} + public func shellOut(command: String) throws -> String { return try ShellOut.shellOut(to: command) } diff --git a/Sources/RunnerLib/SPMDanger.swift b/Sources/RunnerLib/SPMDanger.swift new file mode 100644 index 00000000..214a4b2a --- /dev/null +++ b/Sources/RunnerLib/SPMDanger.swift @@ -0,0 +1,36 @@ +import Foundation + +public struct SPMDanger { + private static let dangerDepsPrefix = "DangerDeps" + public static let buildFolder = ".build/debug" + public let depsLibName: String + + public init?(packagePath: String = "Package.swift") { + let packageContent = (try? String(contentsOfFile: packagePath)) ?? "" + + let regex = try? NSRegularExpression(pattern: "\\.library\\(name:[\\ ]?\"(\(SPMDanger.dangerDepsPrefix)[A-Za-z]*)", + options: .allowCommentsAndWhitespace) + let firstMatch = regex?.firstMatch(in: packageContent, + options: .withTransparentBounds, + range: NSRange(location: 0, length: packageContent.count)) + + if let depsLibNameRange = firstMatch?.range(at: 1), + let range = Range(depsLibNameRange, in: packageContent) { + depsLibName = String(packageContent[range]) + } else { + return nil + } + } + + public func buildDepsIfNeeded(executor: ShellOutExecuting = ShellOutExecutor(), + fileManager: FileManager = .default) { + if !fileManager.fileExists(atPath: "\(SPMDanger.buildFolder)/lib\(depsLibName).dylib"), // OSX + !fileManager.fileExists(atPath: "\(SPMDanger.buildFolder)/lib\(depsLibName).so") { // Linux + _ = try? executor.shellOut(command: "swift build --product \(depsLibName)") + } + } + + public var libImport: String { + return "-l\(depsLibName)" + } +} diff --git a/Tests/RunnerLibTests/DangerJSVersionFinderTests.swift b/Tests/RunnerLibTests/DangerJSVersionFinderTests.swift index 1bb63b2c..1c385853 100644 --- a/Tests/RunnerLibTests/DangerJSVersionFinderTests.swift +++ b/Tests/RunnerLibTests/DangerJSVersionFinderTests.swift @@ -14,13 +14,3 @@ final class DangerJSVersionFinderTests: XCTestCase { XCTAssertEqual(version, executor.result) } } - -private final class MockedExecutor: ShellOutExecuting { - var receivedCommand: String! - var result = "" - - func shellOut(command: String) throws -> String { - receivedCommand = command - return result - } -} diff --git a/Tests/RunnerLibTests/MockedExecutor.swift b/Tests/RunnerLibTests/MockedExecutor.swift new file mode 100644 index 00000000..e63ea801 --- /dev/null +++ b/Tests/RunnerLibTests/MockedExecutor.swift @@ -0,0 +1,11 @@ +import RunnerLib + +final class MockedExecutor: ShellOutExecuting { + var receivedCommand: String! + var result = "" + + func shellOut(command: String) throws -> String { + receivedCommand = command + return result + } +} diff --git a/Tests/RunnerLibTests/SPMDangerTests.swift b/Tests/RunnerLibTests/SPMDangerTests.swift new file mode 100644 index 00000000..67fdf734 --- /dev/null +++ b/Tests/RunnerLibTests/SPMDangerTests.swift @@ -0,0 +1,69 @@ +@testable import RunnerLib +import XCTest + +final class SPMDangerTests: XCTestCase { + let testPackage = "testPackage.swift" + + override func tearDown() { + super.tearDown() + try? FileManager.default.removeItem(atPath: testPackage) + } + + func testItReturnsTrueWhenThePackageHasTheDangerLib() { + try! ".library(name: \"DangerDeps\"".write(toFile: testPackage, atomically: false, encoding: .utf8) + + let spmDanger = SPMDanger(packagePath: testPackage) + XCTAssertEqual(spmDanger?.depsLibName, "DangerDeps") + } + + func testItAcceptsAnythingStartsWithDangerDeps() { + try! ".library(name: \"DangerDepsEigen\"".write(toFile: testPackage, atomically: false, encoding: .utf8) + + let spmDanger = SPMDanger(packagePath: testPackage) + XCTAssertEqual(spmDanger?.depsLibName, "DangerDepsEigen") + } + + func testItReturnsFalseWhenThePackageHasNotTheDangerLib() { + try! "".write(toFile: testPackage, atomically: false, encoding: .utf8) + + XCTAssertNil(SPMDanger(packagePath: testPackage)) + } + + func testItReturnsFalseWhenThereIsNoPackage() { + XCTAssertNil(SPMDanger(packagePath: testPackage)) + } + + func testItBuildsTheDependenciesIfTheDepsLibIsNotPresent() { + let executor = MockedExecutor() + let fileManager = StubbedFileManager() + fileManager.stubbedFileExists = false + + try! ".library(name: \"DangerDeps\"".write(toFile: testPackage, atomically: false, encoding: .utf8) + SPMDanger(packagePath: testPackage)?.buildDepsIfNeeded(executor: executor, fileManager: fileManager) + + XCTAssertTrue(executor.receivedCommand == "swift build --product DangerDeps") + } + + func testItDoesntBuildTheDependenciesIfTheDepsLibIsPresent() { + let executor = MockedExecutor() + let fileManager = StubbedFileManager() + fileManager.stubbedFileExists = true + + SPMDanger(packagePath: testPackage)?.buildDepsIfNeeded(executor: executor, fileManager: fileManager) + + XCTAssertTrue(executor.receivedCommand == nil) + } + + func testItReturnsTheCorrectDepsImport() { + try! ".library(name: \"DangerDepsEigen\"".write(toFile: testPackage, atomically: false, encoding: .utf8) + XCTAssertEqual(SPMDanger(packagePath: testPackage)?.libImport, "-lDangerDepsEigen") + } +} + +private class StubbedFileManager: FileManager { + fileprivate var stubbedFileExists: Bool = true + + override func fileExists(atPath _: String) -> Bool { + return stubbedFileExists + } +} diff --git a/Tests/RunnerLibTests/XCTestManifests.swift b/Tests/RunnerLibTests/XCTestManifests.swift index 67445470..afde8067 100644 --- a/Tests/RunnerLibTests/XCTestManifests.swift +++ b/Tests/RunnerLibTests/XCTestManifests.swift @@ -42,6 +42,18 @@ extension ImportsFinderTests { ] } +extension SPMDangerTests { + static let __allTests = [ + ("testItAcceptsAnythingStartsWithDangerDeps", testItAcceptsAnythingStartsWithDangerDeps), + ("testItBuildsTheDependenciesIfTheDepsLibIsNotPresent", testItBuildsTheDependenciesIfTheDepsLibIsNotPresent), + ("testItDoesntBuildTheDependenciesIfTheDepsLibIsPresent", testItDoesntBuildTheDependenciesIfTheDepsLibIsPresent), + ("testItReturnsFalseWhenThePackageHasNotTheDangerLib", testItReturnsFalseWhenThePackageHasNotTheDangerLib), + ("testItReturnsFalseWhenThereIsNoPackage", testItReturnsFalseWhenThereIsNoPackage), + ("testItReturnsTheCorrectDepsImport", testItReturnsTheCorrectDepsImport), + ("testItReturnsTrueWhenThePackageHasTheDangerLib", testItReturnsTrueWhenThePackageHasTheDangerLib), + ] +} + #if !os(macOS) public func __allTests() -> [XCTestCaseEntry] { return [ @@ -51,6 +63,7 @@ extension ImportsFinderTests { testCase(DangerJSVersionFinderTests.__allTests), testCase(HelpMessagePresenterTests.__allTests), testCase(ImportsFinderTests.__allTests), + testCase(SPMDangerTests.__allTests), ] } #endif