Skip to content

Commit 1d8b674

Browse files
authored
Individual swiftinterface providing (#30)
1 parent 5be7fee commit 1d8b674

File tree

8 files changed

+487
-41
lines changed

8 files changed

+487
-41
lines changed

README.md

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,93 @@
1-
[![🧪 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)
1+
[![🧪 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)
22

3-
# Swift Public API diff
3+
# Swift Public API diff
44

5-
This tool allows comparing 2 versions of a swift (sdk) project and lists all changes in a human readable way.
5+
This tool allows comparing 2 versions of a swift (sdk) project and lists all changes in a human readable way.
66

7-
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).
7+
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).
88

9-
## Usage
9+
## Usage
10+
11+
### From Project to Output
12+
13+
```
14+
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>]
1015
16+
OPTIONS:
17+
--new <new> Specify the updated version to compare to
18+
--old <old> Specify the old version to compare to
19+
--scheme <scheme> [Optional] Which scheme to build (Needed when
20+
comparing 2 xcode projects)
21+
--swift-interface-type <swift-interface-type>
22+
[Optional] Specify the type of .swiftinterface you
23+
want to compare (public/private) (default: public)
24+
--output <output> [Optional] Where to output the result (File path)
25+
--log-output <log-output>
26+
[Optional] Where to output the logs (File path)
27+
--log-level <log-level> [Optional] The log level to use during execution
28+
(default: default)
29+
-h, --help Show help information.
1130
```
12-
USAGE: public-api-diff --new <new> --old <old> [--output <output>] [--log-output <log-output>] [--scheme <scheme>]
13-
14-
OPTIONS:
15-
--new <new> Specify the updated version to compare to
16-
--old <old> Specify the old version to compare to
17-
--output <output> Where to output the result (File path)
18-
--log-output <log-output>
19-
Where to output the logs (File path)
20-
--scheme <scheme> Which scheme to build (Needed when comparing 2 xcode projects)
21-
-h, --help Show help information.
22-
```
31+
32+
#### Run as debug build
33+
```
34+
# From Project to Output
35+
swift run public-api-diff
36+
project
37+
--new "develop~https://github.com/Adyen/adyen-ios.git"
38+
--old "5.12.0~https://github.com/Adyen/adyen-ios.git"
39+
```
40+
41+
### From `.swiftinterface` to Output
42+
43+
```
44+
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>]
45+
46+
OPTIONS:
47+
--new <new> Specify the updated .swiftinterface file to compare to
48+
--old <old> Specify the old .swiftinterface file to compare to
49+
--target-name <target-name>
50+
[Optional] The name of your target/module to show in
51+
the output
52+
--old-version-name <old-version-name>
53+
[Optional] The name of your old version (e.g. v1.0 /
54+
main) to show in the output
55+
--new-version-name <new-version-name>
56+
[Optional] The name of your new version (e.g. v2.0 /
57+
develop) to show in the output
58+
--output <output> [Optional] Where to output the result (File path)
59+
--log-output <log-output>
60+
[Optional] Where to output the logs (File path)
61+
--log-level <log-level> [Optional] The log level to use during execution
62+
(default: default)
63+
-h, --help Show help information.
64+
```
2365

24-
### Run as debug build
66+
#### Run as debug build
2567
```
26-
swift run public-api-diff
27-
--new "some/local/path"
28-
--old "develop~https://github.com/some/repository"
29-
--output "path/to/output.md"
68+
# From Project to Output
69+
swift run public-api-diff
70+
swift-interface
71+
--new "new/path/to/project.swiftinterface"
72+
--old "old/path/to/project.swiftinterface"
3073
```
3174

32-
### How to create a release build
75+
## How to create a release build
3376
```
3477
swift build --configuration release
3578
```
3679

37-
### Run release build
80+
## Run release build
3881
```
3982
./public-api-diff
40-
--new "some/local/path"
41-
--old "develop~https://github.com/some/repository"
42-
--output "path/to/output.md"
83+
project
84+
--new "develop~https://github.com/Adyen/adyen-ios.git"
85+
--old "5.12.0~https://github.com/Adyen/adyen-ios.git"
86+
87+
./public-api-diff
88+
swift-interface
89+
--new "new/path/to/project.swiftinterface"
90+
--old "old/path/to/project.swiftinterface"
4391
```
4492

4593
# Alternatives
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import ArgumentParser
2+
3+
import PADProjectBuilder
4+
import PADLogging
5+
6+
extension SwiftInterfaceType: ExpressibleByArgument {
7+
public init?(argument: String) {
8+
switch argument {
9+
case "public": self = .public
10+
case "private": self = .private
11+
default: return nil
12+
}
13+
}
14+
}
15+
16+
extension LogLevel: ExpressibleByArgument {
17+
public init?(argument: String) {
18+
switch argument {
19+
case "quiet": self = .quiet
20+
case "default": self = .default
21+
case "debug": self = .debug
22+
default: return nil
23+
}
24+
}
25+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// Copyright (c) 2024 Adyen N.V.
3+
//
4+
// This file is open source and available under the MIT license. See the LICENSE file for more info.
5+
//
6+
7+
import ArgumentParser
8+
import Foundation
9+
10+
import PADCore
11+
import PADLogging
12+
13+
import PADSwiftInterfaceDiff
14+
import PADProjectBuilder
15+
import PADOutputGenerator
16+
import PADPackageFileAnalyzer
17+
18+
@main
19+
struct PublicApiDiff: AsyncParsableCommand {
20+
21+
static var configuration: CommandConfiguration = .init(
22+
commandName: "public-api-diff",
23+
subcommands: [
24+
ProjectToOutputCommand.self,
25+
SwiftInterfaceToOutputCommand.self
26+
]
27+
)
28+
29+
public func run() async throws {
30+
fatalError("No sub command provided")
31+
}
32+
}
33+
34+
extension PublicApiDiff {
35+
36+
static func logger(
37+
with logLevel: LogLevel,
38+
logOutputFilePath: String?
39+
) -> any Logging {
40+
var loggers = [any Logging]()
41+
if let logOutputFilePath {
42+
loggers += [LogFileLogger(outputFilePath: logOutputFilePath)]
43+
}
44+
loggers += [SystemLogger().withLogLevel(logLevel)]
45+
46+
return LoggingGroup(with: loggers)
47+
}
48+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import ArgumentParser
2+
import Foundation
3+
4+
import PADCore
5+
import PADLogging
6+
7+
import PADSwiftInterfaceDiff
8+
import PADProjectBuilder
9+
import PADOutputGenerator
10+
import PADPackageFileAnalyzer
11+
12+
/// Command that analyzes the differences between an old and new project and produces a human readable output
13+
struct ProjectToOutputCommand: AsyncParsableCommand {
14+
15+
static var configuration: CommandConfiguration = .init(commandName: "project")
16+
17+
/// The representation of the new/updated project source
18+
@Option(help: "Specify the updated version to compare to")
19+
public var new: String
20+
21+
/// The representation of the old/reference project source
22+
@Option(help: "Specify the old version to compare to")
23+
public var old: String
24+
25+
/// The (optional) scheme to build
26+
///
27+
/// Needed when comparing 2 xcode projects
28+
@Option(help: "[Optional] Which scheme to build (Needed when comparing 2 xcode projects)")
29+
public var scheme: String?
30+
31+
@Option(help: "[Optional] Specify the type of .swiftinterface you want to compare (public/private)")
32+
public var swiftInterfaceType: SwiftInterfaceType = .public
33+
34+
/// The (optional) output file path
35+
///
36+
/// If not defined the output will be printed to the console
37+
@Option(help: "[Optional] Where to output the result (File path)")
38+
public var output: String?
39+
40+
/// The (optional) path to the log output file
41+
@Option(help: "[Optional] Where to output the logs (File path)")
42+
public var logOutput: String?
43+
44+
@Option(help: "[Optional] The log level to use during execution")
45+
public var logLevel: LogLevel = .default
46+
47+
/// Entry point of the command line tool
48+
public func run() async throws {
49+
50+
let projectType: ProjectType = {
51+
if let scheme { return .xcodeProject(scheme: scheme) }
52+
return .swiftPackage
53+
}()
54+
55+
let logger = PublicApiDiff.logger(with: logLevel, logOutputFilePath: logOutput)
56+
57+
do {
58+
var warnings = [String]()
59+
var projectChanges = [Change]()
60+
61+
let oldSource: ProjectSource = try .from(old)
62+
let newSource: ProjectSource = try .from(new)
63+
64+
// MARK: - Producing .swiftinterface files
65+
66+
let projectBuilderResult = try await Self.buildProject(
67+
oldSource: oldSource,
68+
newSource: newSource,
69+
projectType: projectType,
70+
swiftInterfaceType: swiftInterfaceType,
71+
logger: logger
72+
)
73+
74+
// MARK: - Analyzing .swiftinterface files
75+
76+
let swiftInterfaceChanges = try await Self.analyzeSwiftInterfaceFiles(
77+
swiftInterfaceFiles: projectBuilderResult.swiftInterfaceFiles,
78+
logger: logger
79+
)
80+
81+
// MARK: - Analyzing Package.swift
82+
83+
try Self.analyzeProject(
84+
ofType: projectType,
85+
projectDirectories: projectBuilderResult.projectDirectories,
86+
changes: &projectChanges,
87+
warnings: &warnings,
88+
logger: logger
89+
)
90+
91+
// MARK: - Merging Changes
92+
93+
var changes = swiftInterfaceChanges
94+
if !projectChanges.isEmpty {
95+
changes["Package.swift"] = projectChanges
96+
}
97+
98+
// MARK: - Generate Output
99+
100+
let generatedOutput = try Self.generateOutput(
101+
for: changes,
102+
warnings: warnings,
103+
allTargets: projectBuilderResult.swiftInterfaceFiles.map(\.name).sorted(),
104+
oldVersionName: oldSource.description,
105+
newVersionName: newSource.description
106+
)
107+
108+
// MARK: -
109+
110+
if let output {
111+
try FileManager.default.write(generatedOutput, to: output)
112+
} else {
113+
// We're not using a logger here as we always want to have it printed if no output was specified
114+
print(generatedOutput)
115+
}
116+
117+
logger.log("✅ Success", from: "Main")
118+
} catch {
119+
logger.log("💥 \(error.localizedDescription)", from: "Main")
120+
}
121+
}
122+
}
123+
124+
// MARK: - Privates
125+
126+
private extension ProjectToOutputCommand {
127+
128+
static func buildProject(
129+
oldSource: ProjectSource,
130+
newSource: ProjectSource,
131+
projectType: ProjectType,
132+
swiftInterfaceType: SwiftInterfaceType,
133+
logger: any Logging
134+
) async throws -> ProjectBuilder.Result {
135+
136+
let projectBuilder = ProjectBuilder(
137+
projectType: projectType,
138+
swiftInterfaceType: swiftInterfaceType,
139+
logger: logger
140+
)
141+
142+
return try await projectBuilder.build(
143+
oldSource: oldSource,
144+
newSource: newSource
145+
)
146+
}
147+
148+
static func analyzeProject(
149+
ofType projectType: ProjectType,
150+
projectDirectories: (old: URL, new: URL),
151+
changes: inout [Change],
152+
warnings: inout [String],
153+
logger: any Logging
154+
) throws {
155+
switch projectType {
156+
case .swiftPackage:
157+
let swiftPackageFileAnalyzer = SwiftPackageFileAnalyzer(
158+
logger: logger
159+
)
160+
let swiftPackageAnalysis = try swiftPackageFileAnalyzer.analyze(
161+
oldProjectUrl: projectDirectories.old,
162+
newProjectUrl: projectDirectories.new
163+
)
164+
165+
warnings = swiftPackageAnalysis.warnings
166+
changes = swiftPackageAnalysis.changes
167+
case .xcodeProject:
168+
warnings = []
169+
changes = []
170+
break // Nothing to do
171+
}
172+
}
173+
174+
static func analyzeSwiftInterfaceFiles(
175+
swiftInterfaceFiles: [SwiftInterfaceFile],
176+
logger: any Logging
177+
) async throws -> [String: [Change]] {
178+
let swiftInterfaceDiff = SwiftInterfaceDiff(logger: logger)
179+
180+
return try await swiftInterfaceDiff.run(
181+
with: swiftInterfaceFiles
182+
)
183+
}
184+
185+
static func generateOutput(
186+
for changes: [String: [Change]],
187+
warnings: [String],
188+
allTargets: [String],
189+
oldVersionName: String,
190+
newVersionName: String
191+
) throws -> String {
192+
let outputGenerator: any OutputGenerating<String> = MarkdownOutputGenerator()
193+
194+
return try outputGenerator.generate(
195+
from: changes,
196+
allTargets: allTargets,
197+
oldVersionName: oldVersionName,
198+
newVersionName: newVersionName,
199+
warnings: warnings
200+
)
201+
}
202+
}

0 commit comments

Comments
 (0)