Skip to content

Commit

Permalink
Api change detection improvements (#1717)
Browse files Browse the repository at this point in the history
## Summary
- Modifies the Package.swift to contain all targets
- Only builds the current project + comparison project once
- Automatically considers all available targets (instead of hardcoding
them)
  • Loading branch information
goergisn authored Jun 19, 2024
1 parent ea3916e commit 7094702
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 89 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/detect_api_changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

- name: 🔍 Detect Changes
run: |
Scripts/generate_public_interface_definition.sh ${branch} ${baseRepo}
Scripts/detect_public_api_changes/compare.sh ${branch} ${baseRepo}
env:
branch: '${{github.event.pull_request.base.ref}}'
baseRepo: '${{github.server_url}}/${{github.repository}}.git'
Expand Down
157 changes: 157 additions & 0 deletions Scripts/detect_public_api_changes/compare.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#!/bin/bash

# -------------------------------------------------------------------
# Setup & Info
# -------------------------------------------------------------------

BRANCH=$1
REPO=$2
TARGET="x86_64-apple-ios17.4-simulator"
SDK="`xcrun --sdk iphonesimulator --show-sdk-path`"
COMPARISON_VERSION_DIR_NAME="comparison_version_$BRANCH"
DERIVED_DATA_PATH=".build"
SDK_DUMP_INPUT_PATH="$DERIVED_DATA_PATH/Build/Products/Debug-iphonesimulator"
ALL_TARGETS_LIBRARY_NAME="AdyenAllTargets"
MODULE_NAMES=($(./Scripts/detect_public_api_changes/package_file_helper.swift Package.swift -print-available-targets))

# -------------------------------------------------------------------
# Convenience Functions
# -------------------------------------------------------------------

# Echos the entered information for validation
function printRunInfo() {

echo "Branch: " $BRANCH
echo "Repo: " $REPO
echo "Target: " $TARGET
echo "Dir: " $COMPARISON_VERSION_DIR_NAME

echo "Modules:"
for MODULE in ${MODULE_NAMES[@]}; do
echo "-" $MODULE
done
}

# Clones the comparison project into a custom directory
function setupComparisonRepo() {

rm -rf $COMPARISON_VERSION_DIR_NAME
mkdir $COMPARISON_VERSION_DIR_NAME
cd $COMPARISON_VERSION_DIR_NAME
git clone -b $BRANCH $REPO
cd ..
}

# Builds the project and temporarily modifies
# some files to optimize the process
#
# $1 - The path to the current project dir
#
# Examples
#
# buildProject . # If we're in the current project dir
# buildProject ../.. # If we're in the comparison project dir
function buildProject() {

# Removing derived data if available
rm -rf .build

# We have to obscure the project file so `xcodebuild` uses the Package.swift to build the module
mv Adyen.xcodeproj Adyen.xcode_proj

# Copying the Package.swift so we can revert the change done by the next step
cp Package.swift Package.sw_ift

# Modify the Package.swift file to generate a product/library that contains all targets
# so we only have to build it once and can use it to diff all modules
$1/Scripts/detect_public_api_changes/package_file_helper.swift Package.swift -add-consolidated-library $ALL_TARGETS_LIBRARY_NAME

xcodebuild -scheme $ALL_TARGETS_LIBRARY_NAME \
-sdk $SDK \
-derivedDataPath $DERIVED_DATA_PATH \
-destination "platform=iOS,name=Any iOS Device" \
-target $TARGET \
-quiet \
-skipPackagePluginValidation

# Reverting the tmp changes
rm Package.swift
mv Adyen.xcode_proj Adyen.xcodeproj
mv Package.sw_ift Package.swift
}

# Generates an sdk-dump in form of a json file
#
# $1 - The module to generate the dump for
# $2 - The output path for the generated json file
#
# Examples
#
# generateSdkDump "AdyenDropIn" "api_dump.json"
#
function generateSdkDump() {

xcrun swift-api-digester -dump-sdk \
-module $1 \
-o $2 \
-I $SDK_DUMP_INPUT_PATH \
-sdk $SDK \
-target $TARGET
}

# Compares both versions of the provided module
# and writes the result into a .md file (Handled by the diff.swift script)
#
# $1 - The module to compare
#
# Examples
#
# diffModuleVersions "AdyenDropIn"
#
function diffModuleVersions() {

echo "📋 [$1] Generating current sdk dump"
generateSdkDump $1 "api_dump.json"

cd $COMPARISON_VERSION_DIR_NAME/adyen-ios

echo "📋 [$1] Generating comparison sdk dump"
generateSdkDump $1 "../../api_dump_comparison.json"

cd ../..

./Scripts/detect_public_api_changes/diff.swift "api_dump.json" "api_dump_comparison.json" $1

# Cleaning up afterwards
rm api_dump.json
rm api_dump_comparison.json
}

# -------------------------------------------------------------------
# Main Execution
# -------------------------------------------------------------------

printRunInfo

echo "↘️ Setting up comparison project"
setupComparisonRepo

# Move into the comparison project
cd $COMPARISON_VERSION_DIR_NAME/adyen-ios

echo "🛠️ Building '$ALL_TARGETS_LIBRARY_NAME' comparison project"
buildProject ../..

# Move back to the current project dir
cd ../..

echo "🛠️ Building '$ALL_TARGETS_LIBRARY_NAME' current project"
buildProject .

echo "👷 Diffing all Modules"

for MODULE in ${MODULE_NAMES[@]}; do

diffModuleVersions $MODULE

done
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import Foundation

let currentDirectory = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)

// TODO: Pass all modules at once so we can write the file out all at once
// (This also allows us to indicate in the title whether or not there were any changes)
let old = CommandLine.arguments[1]
let new = CommandLine.arguments[2]
let moduleName = CommandLine.arguments[3]
Expand Down Expand Up @@ -62,14 +64,14 @@ class Element: Codable, Equatable, CustomDebugStringConvertible {
if let declKind {
if declKind == "Constructor" {
definition += "func "
} else {
} else {
definition += "\(declKind.lowercased()) "
}
}

definition += "\(printedName)"

if let conformanceNames = conformances?.map({ $0.printedName }), !conformanceNames.isEmpty {
if let conformanceNames = conformances?.map(\.printedName), !conformanceNames.isEmpty {
definition += " : \(conformanceNames.joined(separator: ", "))"
}

Expand Down Expand Up @@ -150,7 +152,7 @@ func recursiveCompare(element lhs: Element, to rhs: Element, oldFirst: Bool) ->
return []
}

if lhs.isSpiInternal && rhs.isSpiInternal {
if lhs.isSpiInternal, rhs.isSpiInternal {
// If both elements are spi internal we can ignore them as they are not in the public interface
return []
}
Expand All @@ -159,12 +161,11 @@ func recursiveCompare(element lhs: Element, to rhs: Element, oldFirst: Bool) ->

// TODO: Add check if accessor changed (e.g. changed from get/set to get only...)

if oldFirst, (
lhs.printedName != rhs.printedName ||
lhs.spiGroupNames != rhs.spiGroupNames ||
lhs.conformances != rhs.conformances ||
lhs.declAttributes != rhs.declAttributes
) {
if oldFirst,
lhs.printedName != rhs.printedName ||
lhs.spiGroupNames != rhs.spiGroupNames ||
lhs.conformances != rhs.conformances ||
lhs.declAttributes != rhs.declAttributes {
// TODO: Show what exactly changed (name, spi, conformance, declAttributes, ...) as a bullet list maybe (add a `changeList` property to `Change`)
changes += [.init(changeType: .change, parentName: lhs.parentPath, changeDescription: "`\(lhs)` ➡️ `\(rhs)`")]
}
Expand Down Expand Up @@ -212,7 +213,7 @@ func compare() throws {
)

if decodedOldDefinition == decodedNewDefinition {
try persistComparison(fileContent: "## 🫧 `\(moduleName)`\n- No changes detected")
try persistComparison(fileContent: "## 🫧 `\(moduleName)` - No changes detected")
return
}

Expand Down
70 changes: 70 additions & 0 deletions Scripts/detect_public_api_changes/package_file_helper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env xcrun swift

import Foundation

// MARK: - Convenience Methods

/// Extracts all targets in the targets section
///
/// Returns: All available target names
func availableTargets(from packageContent: String) -> [String] {
let scanner = Scanner(string: packageContent)
_ = scanner.scanUpToString("targets: [")

var availableTargets = Set<String>()

while scanner.scanUpToString(".target(") != nil {
let nameStartTag = "name: \""
let nameEndTag = "\""

_ = scanner.scanUpToString(nameStartTag)
_ = scanner.scanString(nameStartTag)

if let targetName = scanner.scanUpToString(nameEndTag) {
availableTargets.insert(targetName)
}
}

return availableTargets.sorted()
}

/// Generates a library entry from the name and available target names to be inserted into the `Package.swift` file
func consolidatedLibraryEntry(_ name: String, from availableTargets: [String]) -> String {
"""
.library(
name: "\(name)",
targets: [\(availableTargets.map { "\"\($0)\"" }.joined(separator: ", "))]
),
"""
}

/// Generates the updated content for the `Package.swift` adding the consolidated library entry (containing all targets) in the products section
func updatedContent(with consolidatedEntry: String) -> String {
// Update the Package.swift content
var updatedContent = packageContent
if let productsRange = packageContent.range(of: "products: [", options: .caseInsensitive) {
updatedContent.insert(contentsOf: consolidatedEntry, at: productsRange.upperBound)
} else {
print("Products section not found")
}
return updatedContent
}

// MARK: - Main

let packagePath = CommandLine.arguments[1] // Path to the Package.swift file
let packageContent = try String(contentsOfFile: packagePath)
let targets = availableTargets(from: packageContent)

if CommandLine.arguments[2] == "-add-consolidated-library" {
// Inserts a new library into the targets section containing all targets from the target section
let consolidatedLibraryName = CommandLine.arguments[3] // Name of the library containing all targets
let consolidatedEntry = consolidatedLibraryEntry(consolidatedLibraryName, from: targets)
let updatedPackageContent = updatedContent(with: consolidatedEntry)
// Write the updated content back to the file
try updatedPackageContent.write(toFile: packagePath, atomically: true, encoding: .utf8)
} else if CommandLine.arguments[2] == "-print-available-targets" {
// Prints the targets to the console so it can be consumed by the "compare.sh" script and transformed in to an array
print(targets.joined(separator: " "))
}
78 changes: 0 additions & 78 deletions Scripts/generate_public_interface_definition.sh

This file was deleted.

0 comments on commit 7094702

Please sign in to comment.