Skip to content

Individual swiftinterface providing #30

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 52 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
fbe2cd6
Adding initial support to parse interface changes between swift files
goergisn Sep 16, 2024
04fe183
Adding initial SwiftInterface parsing
goergisn Oct 1, 2024
8414dae
Started work on change detection
goergisn Oct 1, 2024
65202af
Adding full support for individual changes
goergisn Oct 2, 2024
a51691f
Wrapping up initial version of .swiftinterface
goergisn Oct 4, 2024
b4ccf0a
Merging extensions with extended types
goergisn Oct 7, 2024
8f591dc
Fixing tests
goergisn Oct 7, 2024
637abd7
Fixing integration tests
goergisn Oct 7, 2024
d803935
Merge branch 'main' into swiftinterface-file-support
goergisn Oct 7, 2024
6fc4ffb
Fixing file name
goergisn Oct 7, 2024
7a0bcd5
Simplifying and fixing logic
goergisn Oct 8, 2024
26ff072
fixing dependency resolving
goergisn Oct 9, 2024
665c45e
fixing build
goergisn Oct 9, 2024
97eb0c2
Merge branch 'main' into swiftinterface-file-support
goergisn Oct 9, 2024
ff8ab89
updating package.resolved
goergisn Oct 9, 2024
b48d1f6
Removing SDKDump related code
goergisn Oct 9, 2024
7ea68bc
Removing more code
goergisn Oct 9, 2024
625b16e
Renaming and documentation
goergisn Oct 9, 2024
8eab64c
Removing empty header
goergisn Oct 9, 2024
54a700c
Consolidating DeclSyntax extensions
goergisn Oct 9, 2024
f7ba528
Adding more documentation
goergisn Oct 9, 2024
fd20f61
More refactoring
goergisn Oct 9, 2024
c8dcb83
Refactoring
goergisn Oct 9, 2024
30393c4
Update README.md
goergisn Oct 10, 2024
7219c8b
Update README.md
goergisn Oct 10, 2024
ff8e026
Implemented pipeline tests
goergisn Oct 10, 2024
73c1e78
Added test status badge
goergisn Oct 10, 2024
72023a4
Adding XcodeToolsTests
goergisn Oct 10, 2024
04d9364
Adding logger tests
goergisn Oct 10, 2024
28f12ed
Merge branch 'swiftinterface-file-support' of github.com:Adyen/adyen-…
goergisn Oct 10, 2024
3e508d1
more logger tests
goergisn Oct 10, 2024
4267736
Fixing some more tests
goergisn Oct 10, 2024
8a6bd75
fixing output
goergisn Oct 10, 2024
6ec7920
Modularize Package.swift (#27)
goergisn Oct 11, 2024
1ada9a7
Adding more documentation
goergisn Oct 11, 2024
bf99220
Fixing test
goergisn Oct 11, 2024
6e3fae7
More documentation
goergisn Oct 11, 2024
7208f82
Update README.md
goergisn Oct 11, 2024
6d84af0
Updating documentation
goergisn Oct 11, 2024
a3430e3
fixing some docs
goergisn Oct 11, 2024
a763aa0
Moving images outside the Resources folder
goergisn Oct 14, 2024
bae8b9f
updating graphs with transparent background
goergisn Oct 14, 2024
087e226
Updating image again
goergisn Oct 14, 2024
cd901e6
moving PADPackageFileAnalyzer to its own module
goergisn Oct 14, 2024
f9282e8
Removing PAD prefix from structs/classes
goergisn Oct 14, 2024
6442a19
Add more documentation
goergisn Oct 14, 2024
2c03a2f
Fixing docc
goergisn Oct 14, 2024
3e73e69
Fixing documentation
goergisn Oct 14, 2024
4b2d37a
Adding new swift-interface only command
goergisn Oct 14, 2024
6bbd818
Fixing structure
goergisn Oct 14, 2024
5cdfc62
Fixing tests
goergisn Oct 14, 2024
7faa4cc
Merge branch 'main' into individual-swiftinterface-providing
goergisn Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 74 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,93 @@
[![🧪 Run Tests](https://github.com/Adyen/adyen-swift-public-api-diff/actions/workflows/run-tests.yml/badge.svg)](https://github.com/Adyen/adyen-swift-public-api-diff/actions/workflows/run-tests.yml)
[![🧪 Run Tests](https://github.com/Adyen/adyen-swift-public-api-diff/actions/workflows/run-tests.yml/badge.svg)](https://github.com/Adyen/adyen-swift-public-api-diff/actions/workflows/run-tests.yml)

# Swift Public API diff
# Swift Public API diff

This tool allows comparing 2 versions of a swift (sdk) project and lists all changes in a human readable way.
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).

## Usage
## Usage

### From Project to Output

```
USAGE: public-api-diff project --new <new> --old <old> [--scheme <scheme>] [--swift-interface-type <swift-interface-type>] [--output <output>] [--log-output <log-output>] [--log-level <log-level>]

OPTIONS:
--new <new> Specify the updated version to compare to
--old <old> Specify the old version to compare to
--scheme <scheme> [Optional] Which scheme to build (Needed when
comparing 2 xcode projects)
--swift-interface-type <swift-interface-type>
[Optional] Specify the type of .swiftinterface you
want to compare (public/private) (default: public)
--output <output> [Optional] Where to output the result (File path)
--log-output <log-output>
[Optional] Where to output the logs (File path)
--log-level <log-level> [Optional] The log level to use during execution
(default: default)
-h, --help Show help information.
```
USAGE: public-api-diff --new <new> --old <old> [--output <output>] [--log-output <log-output>] [--scheme <scheme>]

OPTIONS:
--new <new> Specify the updated version to compare to
--old <old> Specify the old version to compare to
--output <output> Where to output the result (File path)
--log-output <log-output>
Where to output the logs (File path)
--scheme <scheme> Which scheme to build (Needed when comparing 2 xcode projects)
-h, --help Show help information.
```

#### 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"
```

### From `.swiftinterface` to Output

```
USAGE: public-api-diff swift-interface --new <new> --old <old> [--target-name <target-name>] [--old-version-name <old-version-name>] [--new-version-name <new-version-name>] [--output <output>] [--log-output <log-output>] [--log-level <log-level>]

OPTIONS:
--new <new> Specify the updated .swiftinterface file to compare to
--old <old> Specify the old .swiftinterface file to compare to
--target-name <target-name>
[Optional] The name of your target/module to show in
the output
--old-version-name <old-version-name>
[Optional] The name of your old version (e.g. v1.0 /
main) to show in the output
--new-version-name <new-version-name>
[Optional] The name of your new version (e.g. v2.0 /
develop) to show in the output
--output <output> [Optional] Where to output the result (File path)
--log-output <log-output>
[Optional] Where to output the logs (File path)
--log-level <log-level> [Optional] The log level to use during execution
(default: default)
-h, --help Show help information.
```

### Run as debug build
#### Run as debug build
```
swift run public-api-diff
--new "some/local/path"
--old "develop~https://github.com/some/repository"
--output "path/to/output.md"
# From Project to Output
swift run public-api-diff
swift-interface
--new "new/path/to/project.swiftinterface"
--old "old/path/to/project.swiftinterface"
```

### How to create a release build
## How to create a release build
```
swift build --configuration release
```

### Run release build
## Run release build
```
./public-api-diff
--new "some/local/path"
--old "develop~https://github.com/some/repository"
--output "path/to/output.md"
project
--new "develop~https://github.com/Adyen/adyen-ios.git"
--old "5.12.0~https://github.com/Adyen/adyen-ios.git"

./public-api-diff
swift-interface
--new "new/path/to/project.swiftinterface"
--old "old/path/to/project.swiftinterface"
```

# Alternatives
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ArgumentParser

import PADProjectBuilder
import PADLogging

extension SwiftInterfaceType: ExpressibleByArgument {
public init?(argument: String) {
switch argument {
case "public": self = .public
case "private": self = .private
default: return nil
}
}
}

extension LogLevel: ExpressibleByArgument {
public init?(argument: String) {
switch argument {
case "quiet": self = .quiet
case "default": self = .default
case "debug": self = .debug
default: return nil
}
}
}
48 changes: 48 additions & 0 deletions Sources/ExecutableTargets/CommandLineTool/CommandLineTool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import ArgumentParser
import Foundation

import PADCore
import PADLogging

import PADSwiftInterfaceDiff
import PADProjectBuilder
import PADOutputGenerator
import PADPackageFileAnalyzer

@main
struct PublicApiDiff: AsyncParsableCommand {

static var configuration: CommandConfiguration = .init(
commandName: "public-api-diff",
subcommands: [
ProjectToOutputCommand.self,
SwiftInterfaceToOutputCommand.self
]
)

public func run() async throws {
fatalError("No sub command provided")
}
}

extension PublicApiDiff {

static func logger(
with logLevel: LogLevel,
logOutputFilePath: String?
) -> any Logging {
var loggers = [any Logging]()
if let logOutputFilePath {
loggers += [LogFileLogger(outputFilePath: logOutputFilePath)]
}
loggers += [SystemLogger().withLogLevel(logLevel)]

return LoggingGroup(with: loggers)
}
}
202 changes: 202 additions & 0 deletions Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import ArgumentParser
import Foundation

import PADCore
import PADLogging

import PADSwiftInterfaceDiff
import PADProjectBuilder
import PADOutputGenerator
import PADPackageFileAnalyzer

/// Command that analyzes the differences between an old and new project and produces a human readable output
struct ProjectToOutputCommand: AsyncParsableCommand {

static var configuration: CommandConfiguration = .init(commandName: "project")

/// The representation of the new/updated project source
@Option(help: "Specify the updated version to compare to")
public var new: String

/// The representation of the old/reference project source
@Option(help: "Specify the old version to compare to")
public var old: String

/// The (optional) scheme to build
///
/// Needed when comparing 2 xcode projects
@Option(help: "[Optional] Which scheme to build (Needed when comparing 2 xcode projects)")
public var scheme: String?

@Option(help: "[Optional] Specify the type of .swiftinterface you want to compare (public/private)")
public var swiftInterfaceType: SwiftInterfaceType = .public

/// 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 projectType: ProjectType = {
if let scheme { return .xcodeProject(scheme: scheme) }
return .swiftPackage
}()

let logger = PublicApiDiff.logger(with: logLevel, logOutputFilePath: logOutput)

do {
var warnings = [String]()
var projectChanges = [Change]()

let oldSource: ProjectSource = try .from(old)
let newSource: ProjectSource = try .from(new)

// MARK: - Producing .swiftinterface files

let projectBuilderResult = try await Self.buildProject(
oldSource: oldSource,
newSource: newSource,
projectType: projectType,
swiftInterfaceType: swiftInterfaceType,
logger: logger
)

// MARK: - Analyzing .swiftinterface files

let swiftInterfaceChanges = try await Self.analyzeSwiftInterfaceFiles(
swiftInterfaceFiles: projectBuilderResult.swiftInterfaceFiles,
logger: logger
)

// MARK: - Analyzing Package.swift

try Self.analyzeProject(
ofType: projectType,
projectDirectories: projectBuilderResult.projectDirectories,
changes: &projectChanges,
warnings: &warnings,
logger: logger
)

// MARK: - Merging Changes

var changes = swiftInterfaceChanges
if !projectChanges.isEmpty {
changes["Package.swift"] = projectChanges
}

// MARK: - Generate Output

let generatedOutput = try Self.generateOutput(
for: changes,
warnings: warnings,
allTargets: projectBuilderResult.swiftInterfaceFiles.map(\.name).sorted(),
oldVersionName: oldSource.description,
newVersionName: newSource.description
)

// 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 ProjectToOutputCommand {

static func buildProject(
oldSource: ProjectSource,
newSource: ProjectSource,
projectType: ProjectType,
swiftInterfaceType: SwiftInterfaceType,
logger: any Logging
) async throws -> ProjectBuilder.Result {

let projectBuilder = ProjectBuilder(
projectType: projectType,
swiftInterfaceType: swiftInterfaceType,
logger: logger
)

return try await projectBuilder.build(
oldSource: oldSource,
newSource: newSource
)
}

static func analyzeProject(
ofType projectType: ProjectType,
projectDirectories: (old: URL, new: URL),
changes: inout [Change],
warnings: inout [String],
logger: any Logging
) throws {
switch projectType {
case .swiftPackage:
let swiftPackageFileAnalyzer = SwiftPackageFileAnalyzer(
logger: logger
)
let swiftPackageAnalysis = try swiftPackageFileAnalyzer.analyze(
oldProjectUrl: projectDirectories.old,
newProjectUrl: projectDirectories.new
)

warnings = swiftPackageAnalysis.warnings
changes = swiftPackageAnalysis.changes
case .xcodeProject:
warnings = []
changes = []
break // Nothing to do
}
}

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<String> = MarkdownOutputGenerator()

return try outputGenerator.generate(
from: changes,
allTargets: allTargets,
oldVersionName: oldVersionName,
newVersionName: newVersionName,
warnings: warnings
)
}
}
Loading
Loading