Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ let package = Package(
"PADSwiftInterfaceDiff",
"PADOutputGenerator",
"PADPackageFileAnalyzer",
"PADSwiftInterfaceFileLocator",
.product(name: "ArgumentParser", package: "swift-argument-parser")
],
path: "Sources/ExecutableTargets/CommandLineTool"
Expand Down Expand Up @@ -81,6 +82,7 @@ let package = Package(
dependencies: [
"PADCore",
"PADLogging",
"PADSwiftInterfaceFileLocator",
"FileHandlingModule",
"ShellModule",
"SwiftPackageFileHelperModule"
Expand All @@ -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

Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,48 @@ OPTIONS:

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

### From `.framework` to Output

```
USAGE: public-api-diff framework --new <new> --old <old> --target-name <target-name> [--swift-interface-type <swift-interface-type>] [--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 .framework to compare to
--old <old> Specify the old .framework to compare to
--target-name <target-name>
The name of your target/module to show in the output
--swift-interface-type <swift-interface-type>
[Optional] Specify the type of .swiftinterface you
want to compare (public/private) (default: public)
--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
```
swift run public-api-diff
framework
--target-name "TargetName"
--new "new/path/to/project.framework"
--old "old/path/to/project.framework"
```

## How to create a release build
```
swift build --configuration release
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ArgumentParser

import PADProjectBuilder
import PADSwiftInterfaceFileLocator
import PADLogging

extension SwiftInterfaceType: ExpressibleByArgument {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ struct PublicApiDiff: AsyncParsableCommand {
commandName: "public-api-diff",
subcommands: [
ProjectToOutputCommand.self,
SwiftInterfaceToOutputCommand.self
SwiftInterfaceToOutputCommand.self,
FrameworkToOutputCommand.self
]
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> = MarkdownOutputGenerator()

return try outputGenerator.generate(
from: changes,
allTargets: allTargets,
oldVersionName: oldVersionName,
newVersionName: newVersionName,
warnings: warnings
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Foundation

import PADCore
import PADLogging
import PADSwiftInterfaceFileLocator

import PADSwiftInterfaceDiff
import PADProjectBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation

import PADLogging
import PADCore
import PADSwiftInterfaceFileLocator

import ShellModule
import FileHandlingModule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation

import PADLogging
import PADCore
import PADSwiftInterfaceFileLocator

import FileHandlingModule
import ShellModule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,31 @@ 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
self.shell = shell
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:
Expand All @@ -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)'")
Expand Down
Loading