diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c203a5d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Adyen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.swift b/Package.swift index 628c45e..53030c0 100644 --- a/Package.swift +++ b/Package.swift @@ -47,6 +47,7 @@ let package = Package( "PADSwiftInterfaceDiff", "PADOutputGenerator", "PADPackageFileAnalyzer", + "PADSwiftInterfaceFileLocator", .product(name: "ArgumentParser", package: "swift-argument-parser") ], path: "Sources/ExecutableTargets/CommandLineTool" @@ -81,6 +82,7 @@ let package = Package( dependencies: [ "PADCore", "PADLogging", + "PADSwiftInterfaceFileLocator", "FileHandlingModule", "ShellModule", "SwiftPackageFileHelperModule" @@ -104,6 +106,11 @@ let package = Package( dependencies: ["FileHandlingModule"], path: "Sources/Shared/Public/PADLogging" ), + .target( + name: "PADSwiftInterfaceFileLocator", + dependencies: ["FileHandlingModule", "ShellModule", "PADLogging"], + path: "Sources/Shared/Public/PADSwiftInterfaceFileLocator" + ), // MARK: - Shared/Package diff --git a/README.md b/README.md index 2c39679..704601f 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,29 @@ This tool allows comparing 2 versions of a swift (sdk) project and lists all changes in a human readable way. -It makes use of `.swiftinterface` files that get produced during the archiving of a swift project and parses them using [`swift-syntax`](https://github.com/swiftlang/swift-syntax). +It makes use of `.swiftinterface` files that get produced during the archiving of a swift project and parses them using [`swift-syntax`](https://github.com/swiftlang/swift-syntax). + +## Contributing +We strongly encourage you to contribute to our repository. Find out more in our [contribution guidelines](https://github.com/Adyen/.github/blob/master/CONTRIBUTING.md) + +## Requirements +- **Xcode** >= 16.0 (incl. Xcode command line tools) +- **Swift** >= 5.9 ## Usage ### From Project to Output - +This method requires an iOS 17.5 Simulator to be installed + +``` +swift run public-api-diff + project + --new "develop~https://github.com/Adyen/adyen-ios.git" + --old "5.12.0~https://github.com/Adyen/adyen-ios.git" +``` + +
--help: + ``` USAGE: public-api-diff project --new --old [--scheme ] [--swift-interface-type ] [--output ] [--log-output ] [--log-level ] @@ -28,17 +45,18 @@ OPTIONS: (default: default) -h, --help Show help information. ``` +
+ +### From `.swiftinterface` to Output -#### Run as debug build ``` -# From Project to Output swift run public-api-diff - project - --new "develop~https://github.com/Adyen/adyen-ios.git" - --old "5.12.0~https://github.com/Adyen/adyen-ios.git" + swift-interface + --new "new/path/to/project.swiftinterface" + --old "old/path/to/project.swiftinterface" ``` -### From `.swiftinterface` to Output +
--help: ``` USAGE: public-api-diff swift-interface --new --old [--target-name ] [--old-version-name ] [--new-version-name ] [--output ] [--log-output ] [--log-level ] @@ -62,22 +80,53 @@ OPTIONS: (default: default) -h, --help Show help information. ``` +
+ +### From `.framework` to Output -#### Run as debug build ``` -# From Project to Output swift run public-api-diff - swift-interface - --new "new/path/to/project.swiftinterface" - --old "old/path/to/project.swiftinterface" + framework + --target-name "TargetName" + --new "new/path/to/project.framework" + --old "old/path/to/project.framework" ``` -## How to create a release build +
--help: + +``` +USAGE: public-api-diff framework --new --old --target-name [--swift-interface-type ] [--old-version-name ] [--new-version-name ] [--output ] [--log-output ] [--log-level ] + +OPTIONS: + --new Specify the updated .framework to compare to + --old Specify the old .framework to compare to + --target-name + The name of your target/module to show in the output + --swift-interface-type + [Optional] Specify the type of .swiftinterface you + want to compare (public/private) (default: public) + --old-version-name + [Optional] The name of your old version (e.g. v1.0 / + main) to show in the output + --new-version-name + [Optional] The name of your new version (e.g. v2.0 / + develop) to show in the output + --output [Optional] Where to output the result (File path) + --log-output + [Optional] Where to output the logs (File path) + --log-level [Optional] The log level to use during execution + (default: default) + -h, --help Show help information. +``` +
+ +## Release Build +### Create ``` swift build --configuration release ``` -## Run release build +### Run ``` ./public-api-diff project @@ -88,13 +137,25 @@ swift build --configuration release swift-interface --new "new/path/to/project.swiftinterface" --old "old/path/to/project.swiftinterface" + +./public-api-diff + framework + --target-name "TargetName" + --new "new/path/to/project.framework" + --old "old/path/to/project.framework" ``` -# Alternatives +## Alternatives - **swift-api-digester** - `xcrun swift-api-digester -dump-sdk` - `xcrun swift-api-digester -diagnose-sdk` -# Inspiration +## Inspiration - https://github.com/sdidla/Hatch/blob/main/Sources/Hatch/SymbolParser.swift - For parsing swift files using [swift-syntax](https://github.com/swiftlang/swift-syntax)'s [`SyntaxVisitor`](https://github.com/swiftlang/swift-syntax/blob/main/Sources/SwiftSyntax/generated/SyntaxVisitor.swift) + +## Support +If you have a feature request, or spotted a bug or a technical problem, create a GitHub issue. + +## License +MIT license. For more information, see the LICENSE file. diff --git a/Sources/ExecutableTargets/CommandLineTool/CommandLineTool+Extensions.swift b/Sources/ExecutableTargets/CommandLineTool/CommandLineTool+Extensions.swift index efe2cc0..6e7e328 100644 --- a/Sources/ExecutableTargets/CommandLineTool/CommandLineTool+Extensions.swift +++ b/Sources/ExecutableTargets/CommandLineTool/CommandLineTool+Extensions.swift @@ -1,6 +1,6 @@ import ArgumentParser -import PADProjectBuilder +import PADSwiftInterfaceFileLocator import PADLogging extension SwiftInterfaceType: ExpressibleByArgument { diff --git a/Sources/ExecutableTargets/CommandLineTool/CommandLineTool.swift b/Sources/ExecutableTargets/CommandLineTool/CommandLineTool.swift index 2f9d213..09430a3 100644 --- a/Sources/ExecutableTargets/CommandLineTool/CommandLineTool.swift +++ b/Sources/ExecutableTargets/CommandLineTool/CommandLineTool.swift @@ -22,7 +22,8 @@ struct PublicApiDiff: AsyncParsableCommand { commandName: "public-api-diff", subcommands: [ ProjectToOutputCommand.self, - SwiftInterfaceToOutputCommand.self + SwiftInterfaceToOutputCommand.self, + FrameworkToOutputCommand.self ] ) diff --git a/Sources/ExecutableTargets/CommandLineTool/FrameworkToOutputCommand.swift b/Sources/ExecutableTargets/CommandLineTool/FrameworkToOutputCommand.swift new file mode 100644 index 0000000..3b1b82d --- /dev/null +++ b/Sources/ExecutableTargets/CommandLineTool/FrameworkToOutputCommand.swift @@ -0,0 +1,159 @@ +import ArgumentParser +import Foundation + +import PADCore +import PADLogging + +import PADSwiftInterfaceDiff +import PADSwiftInterfaceFileLocator +import PADOutputGenerator +import PADPackageFileAnalyzer + +/// Command that analyzes the differences between an old and new project and produces a human readable output +struct FrameworkToOutputCommand: AsyncParsableCommand { + + static var configuration: CommandConfiguration = .init(commandName: "framework") + + /// The path to the new/updated xcframework + @Option(help: "Specify the updated .framework to compare to") + public var new: String + + /// The path to the old/reference xcframework + @Option(help: "Specify the old .framework to compare to") + public var old: String + + /// The name of the target/module to show in the output + @Option(help: "The name of your target/module to show in the output") + public var targetName: String + + @Option(help: "[Optional] Specify the type of .swiftinterface you want to compare (public/private)") + public var swiftInterfaceType: SwiftInterfaceType = .public + + @Option(help: "[Optional] The name of your old version (e.g. v1.0 / main) to show in the output") + public var oldVersionName: String? + + @Option(help: "[Optional] The name of your new version (e.g. v2.0 / develop) to show in the output") + public var newVersionName: String? + + /// The (optional) output file path + /// + /// If not defined the output will be printed to the console + @Option(help: "[Optional] Where to output the result (File path)") + public var output: String? + + /// The (optional) path to the log output file + @Option(help: "[Optional] Where to output the logs (File path)") + public var logOutput: String? + + @Option(help: "[Optional] The log level to use during execution") + public var logLevel: LogLevel = .default + + /// Entry point of the command line tool + public func run() async throws { + + let logger = PublicApiDiff.logger(with: logLevel, logOutputFilePath: logOutput) + + do { + // MARK: - Locating .swiftinterface files + + let swiftInterfaceFiles = try Self.locateSwiftInterfaceFiles( + targetName: targetName, + oldPath: old, + newPath: new, + swiftInterfaceType: swiftInterfaceType, + logger: logger + ) + + // MARK: - Analyzing .swiftinterface files + + let swiftInterfaceChanges = try await Self.analyzeSwiftInterfaceFiles( + swiftInterfaceFiles: swiftInterfaceFiles, + logger: logger + ) + + // MARK: - Generate Output + + let generatedOutput = try Self.generateOutput( + for: swiftInterfaceChanges, + warnings: [], + allTargets: [targetName], + oldVersionName: oldVersionName, + newVersionName: newVersionName + ) + + // MARK: - + + if let output { + try FileManager.default.write(generatedOutput, to: output) + } else { + // We're not using a logger here as we always want to have it printed if no output was specified + print(generatedOutput) + } + + logger.log("✅ Success", from: "Main") + } catch { + logger.log("💥 \(error.localizedDescription)", from: "Main") + } + } +} + +// MARK: - Privates + +private extension FrameworkToOutputCommand { + + static func locateSwiftInterfaceFiles( + targetName: String, + oldPath: String, + newPath: String, + swiftInterfaceType: SwiftInterfaceType, + logger: any Logging + ) throws -> [SwiftInterfaceFile] { + let locator = SwiftInterfaceFileLocator(logger: logger) + + let oldSwiftInterfaceFileUrl = try locator.locate( + for: targetName, + derivedDataPath: oldPath, + type: swiftInterfaceType + ) + let newSwiftInterfaceFileUrl = try locator.locate( + for: targetName, + derivedDataPath: newPath, + type: swiftInterfaceType + ) + + return [.init( + name: targetName, + oldFilePath: oldSwiftInterfaceFileUrl.path(), + newFilePath: newSwiftInterfaceFileUrl.path() + )] + } + + static func analyzeSwiftInterfaceFiles( + swiftInterfaceFiles: [SwiftInterfaceFile], + logger: any Logging + ) async throws -> [String: [Change]] { + let swiftInterfaceDiff = SwiftInterfaceDiff(logger: logger) + + return try await swiftInterfaceDiff.run( + with: swiftInterfaceFiles + ) + } + + static func generateOutput( + for changes: [String: [Change]], + warnings: [String], + allTargets: [String]?, + oldVersionName: String?, + newVersionName: String? + ) throws -> String { + let outputGenerator: any OutputGenerating = MarkdownOutputGenerator() + + return try outputGenerator.generate( + from: changes, + allTargets: allTargets, + oldVersionName: oldVersionName, + newVersionName: newVersionName, + warnings: warnings + ) + } +} diff --git a/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift b/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift index 256675c..8018053 100644 --- a/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift +++ b/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift @@ -3,6 +3,7 @@ import Foundation import PADCore import PADLogging +import PADSwiftInterfaceFileLocator import PADSwiftInterfaceDiff import PADProjectBuilder diff --git a/Sources/ExecutableTargets/CommandLineTool/SwiftInterfaceToOutputCommand.swift b/Sources/ExecutableTargets/CommandLineTool/SwiftInterfaceToOutputCommand.swift index 8b4d9ce..50accef 100644 --- a/Sources/ExecutableTargets/CommandLineTool/SwiftInterfaceToOutputCommand.swift +++ b/Sources/ExecutableTargets/CommandLineTool/SwiftInterfaceToOutputCommand.swift @@ -14,11 +14,11 @@ struct SwiftInterfaceToOutputCommand: AsyncParsableCommand { static var configuration: CommandConfiguration = .init(commandName: "swift-interface") - /// The representation of the new/updated project source + /// The path to the new/updated .swiftinterface file @Option(help: "Specify the updated .swiftinterface file to compare to") public var new: String - /// The representation of the old/reference project source + /// The path to the old/reference .swiftinterface file @Option(help: "Specify the old .swiftinterface file to compare to") public var old: String diff --git a/Sources/PublicModules/PADProjectBuilder/PADProjectBuilder.swift b/Sources/PublicModules/PADProjectBuilder/PADProjectBuilder.swift index 2b081a2..41fa2a9 100644 --- a/Sources/PublicModules/PADProjectBuilder/PADProjectBuilder.swift +++ b/Sources/PublicModules/PADProjectBuilder/PADProjectBuilder.swift @@ -2,6 +2,7 @@ import Foundation import PADLogging import PADCore +import PADSwiftInterfaceFileLocator import ShellModule import FileHandlingModule diff --git a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift index ff8c8e4..d697afd 100644 --- a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift +++ b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift @@ -2,6 +2,7 @@ import Foundation import PADLogging import PADCore +import PADSwiftInterfaceFileLocator import FileHandlingModule import ShellModule diff --git a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceFileLocator.swift b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift similarity index 85% rename from Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceFileLocator.swift rename to Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift index 04bbc57..5120be0 100644 --- a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceFileLocator.swift +++ b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift @@ -5,15 +5,15 @@ import ShellModule import PADLogging /// A helper to locate `.swiftinterface` files -struct SwiftInterfaceFileLocator { +public struct SwiftInterfaceFileLocator { let fileHandler: any FileHandling let shell: any ShellHandling let logger: (any Logging)? - init( - fileHandler: any FileHandling = FileManager.default, - shell: any ShellHandling = Shell(), + package init( + fileHandler: any FileHandling, + shell: any ShellHandling, logger: (any Logging)? ) { self.fileHandler = fileHandler @@ -21,6 +21,15 @@ struct SwiftInterfaceFileLocator { self.logger = logger } + public init( + logger: (any Logging)? = nil + ) { + self.init( + fileHandler: FileManager.default, + shell: Shell(), + logger: logger + ) + } /// Tries to locate a `.swiftinterface` files in the derivedData folder for a specific scheme /// - Parameters: @@ -29,7 +38,7 @@ struct SwiftInterfaceFileLocator { /// - type: The swift interface type (.public, .private) to look for /// - Returns: The file url to the found `.swiftinterface` /// - Throws: An error if no `.swiftinterface` file can be found for the given scheme + derived data path - func locate(for scheme: String, derivedDataPath: String, type: SwiftInterfaceType) throws -> URL { + public func locate(for scheme: String, derivedDataPath: String, type: SwiftInterfaceType) throws -> URL { let schemeSwiftModuleName = "\(scheme).swiftmodule" let swiftModulePathsForScheme = shell.execute("cd '\(derivedDataPath)'; find . -type d -name '\(schemeSwiftModuleName)'") diff --git a/Sources/PublicModules/PADProjectBuilder/PADSwiftInterfaceType.swift b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift similarity index 100% rename from Sources/PublicModules/PADProjectBuilder/PADSwiftInterfaceType.swift rename to Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift