From 31158200cde3cb6a24af54960d63f79863f20ee2 Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 28 Sep 2020 15:38:32 +0300 Subject: [PATCH 01/20] vk-399-api-stability: added basic swift script to run sourcekitten doc and compare results. --- .../APIDiffReport.xcodeproj/project.pbxproj | 307 ++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/APIDiffReport.xcscheme | 92 +++++ scripts/APIDiffReport/Package.swift | 27 ++ .../Sources/APIDiffReport/diffreport.swift | 338 ++++++++++++++++++ .../Sources/APIDiffReport/main.swift | 96 +++++ 6 files changed, 867 insertions(+) create mode 100644 scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj create mode 100644 scripts/APIDiffReport/APIDiffReport.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 scripts/APIDiffReport/APIDiffReport.xcodeproj/xcshareddata/xcschemes/APIDiffReport.xcscheme create mode 100644 scripts/APIDiffReport/Package.swift create mode 100644 scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift create mode 100644 scripts/APIDiffReport/Sources/APIDiffReport/main.swift diff --git a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..787ea2e9e0e --- /dev/null +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj @@ -0,0 +1,307 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 2B237961251E3C3000CCF9C4 /* diffreport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B23795F251E3C3000CCF9C4 /* diffreport.swift */; }; + 2B237962251E3C3000CCF9C4 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B237960251E3C3000CCF9C4 /* main.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 2B20711C251E11A200001493 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 2B20711E251E11A200001493 /* APIDiffReport */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = APIDiffReport; sourceTree = BUILT_PRODUCTS_DIR; }; + 2B23795F251E3C3000CCF9C4 /* diffreport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = diffreport.swift; sourceTree = ""; }; + 2B237960251E3C3000CCF9C4 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 2B237963251E3C4000CCF9C4 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2B20711B251E11A200001493 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2B207115251E11A200001493 = { + isa = PBXGroup; + children = ( + 2B237963251E3C4000CCF9C4 /* Package.swift */, + 2B23795D251E3C3000CCF9C4 /* Sources */, + 2B20711F251E11A200001493 /* Products */, + ); + sourceTree = ""; + }; + 2B20711F251E11A200001493 /* Products */ = { + isa = PBXGroup; + children = ( + 2B20711E251E11A200001493 /* APIDiffReport */, + ); + name = Products; + sourceTree = ""; + }; + 2B23795D251E3C3000CCF9C4 /* Sources */ = { + isa = PBXGroup; + children = ( + 2B23795E251E3C3000CCF9C4 /* APIDiffReport */, + ); + path = Sources; + sourceTree = ""; + }; + 2B23795E251E3C3000CCF9C4 /* APIDiffReport */ = { + isa = PBXGroup; + children = ( + 2B23795F251E3C3000CCF9C4 /* diffreport.swift */, + 2B237960251E3C3000CCF9C4 /* main.swift */, + ); + path = APIDiffReport; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2B20711D251E11A200001493 /* APIDiffReport */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2B207125251E11A200001493 /* Build configuration list for PBXNativeTarget "APIDiffReport" */; + buildPhases = ( + 2B20711A251E11A200001493 /* Sources */, + 2B20711B251E11A200001493 /* Frameworks */, + 2B20711C251E11A200001493 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = APIDiffReport; + productName = APIDiffReport; + productReference = 2B20711E251E11A200001493 /* APIDiffReport */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2B207116251E11A200001493 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = mapbox; + TargetAttributes = { + 2B20711D251E11A200001493 = { + CreatedOnToolsVersion = 11.3.1; + }; + }; + }; + buildConfigurationList = 2B207119251E11A200001493 /* Build configuration list for PBXProject "APIDiffReport" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2B207115251E11A200001493; + productRefGroup = 2B20711F251E11A200001493 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2B20711D251E11A200001493 /* APIDiffReport */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 2B20711A251E11A200001493 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B237962251E3C3000CCF9C4 /* main.swift in Sources */, + 2B237961251E3C3000CCF9C4 /* diffreport.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 2B207123251E11A200001493 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 2B207124251E11A200001493 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 2B207126251E11A200001493 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = APIDiffReport.entitlements; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 2B207127251E11A200001493 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = APIDiffReport.entitlements; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2B207119251E11A200001493 /* Build configuration list for PBXProject "APIDiffReport" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2B207123251E11A200001493 /* Debug */, + 2B207124251E11A200001493 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2B207125251E11A200001493 /* Build configuration list for PBXNativeTarget "APIDiffReport" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2B207126251E11A200001493 /* Debug */, + 2B207127251E11A200001493 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 2B207116251E11A200001493 /* Project object */; +} diff --git a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..a656db67da7 --- /dev/null +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/scripts/APIDiffReport/APIDiffReport.xcodeproj/xcshareddata/xcschemes/APIDiffReport.xcscheme b/scripts/APIDiffReport/APIDiffReport.xcodeproj/xcshareddata/xcschemes/APIDiffReport.xcscheme new file mode 100644 index 00000000000..4a15dfedd86 --- /dev/null +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/xcshareddata/xcschemes/APIDiffReport.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/APIDiffReport/Package.swift b/scripts/APIDiffReport/Package.swift new file mode 100644 index 00000000000..2f7c2390982 --- /dev/null +++ b/scripts/APIDiffReport/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version:4.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "APIDiffReport", + products: [ + // Products define the executables and libraries produced by a package, and make them visible to other packages. + .executable( + name: "APIDiffReport", + targets: ["APIDiffReport"] + ), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages which this package depends on. + .target( + name: "APIDiffReport", + dependencies: [] + ), + ] +) diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift new file mode 100644 index 00000000000..88b5dfa6462 --- /dev/null +++ b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift @@ -0,0 +1,338 @@ +/* + Copyright 2016-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +public typealias JSONObject = Any + +typealias SourceKittenNode = [String: Any] +typealias APINode = [String: Any] +typealias ApiNameNodeMap = [String: APINode] + +/** A type of API change. */ +public enum ApiChange { + case addition(apiType: String, name: String) + case deletion(apiType: String, name: String) + case modification(apiType: String, name: String, modificationType: String, from: String, to: String) +} + +/** Generates an API diff report from two SourceKitten JSON outputs. */ +public func diffreport(oldApi: JSONObject, newApi: JSONObject) throws -> [String: [ApiChange]] { + let oldApiNameNodeMap = extractAPINodeMap(from: oldApi as! [SourceKittenNode]) + let newApiNameNodeMap = extractAPINodeMap(from: newApi as! [SourceKittenNode]) + + let oldApiNames = Set(oldApiNameNodeMap.keys) + let newApiNames = Set(newApiNameNodeMap.keys) + + let addedApiNames = newApiNames.subtracting(oldApiNames) + let deletedApiNames = oldApiNames.subtracting(newApiNames) + let persistedApiNames = oldApiNames.intersection(newApiNames) + + var changes: [String: [ApiChange]] = [:] + + // Additions + + for usr in (addedApiNames.map { usr in newApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) { + let apiType = prettyString(forKind: usr["key.kind"] as! String) + let name = prettyName(forApi: usr, apis: newApiNameNodeMap) + let root = rootName(forApi: usr, apis: newApiNameNodeMap) + changes[root, withDefault: []].append(.addition(apiType: apiType, name: name)) + } + + // Deletions + + for usr in (deletedApiNames.map { usr in oldApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) { + let apiType = prettyString(forKind: usr["key.kind"] as! String) + let name = prettyName(forApi: usr, apis: oldApiNameNodeMap) + let root = rootName(forApi: usr, apis: oldApiNameNodeMap) + changes[root, withDefault: []].append(.deletion(apiType: apiType, name: name)) + } + + // Modifications + + let ignoredKeys = Set(arrayLiteral: "key.doc.line", "key.parsed_scope.end", "key.parsed_scope.start", "key.doc.column", "key.doc.comment", "key.bodyoffset", "key.nameoffset", "key.doc.full_as_xml", "key.offset", "key.fully_annotated_decl", "key.length", "key.bodylength", "key.namelength", "key.annotated_decl", "key.doc.parameters", "key.elements", "key.related_decls", + "key.filepath", "key.attributes", + "key.parsed_declaration", + "key.docoffset", "key.attributes") + + for usr in persistedApiNames { + let oldApi = oldApiNameNodeMap[usr]! + let newApi = newApiNameNodeMap[usr]! + let root = rootName(forApi: newApi, apis: newApiNameNodeMap) + let allKeys = Set(oldApi.keys).union(Set(newApi.keys)) + + for key in allKeys { + if ignoredKeys.contains(key) { + continue + } + if let oldValue = oldApi[key] as? String, let newValue = newApi[key] as? String, oldValue != newValue { + let apiType = prettyString(forKind: newApi["key.kind"] as! String) + let name = prettyName(forApi: newApi, apis: newApiNameNodeMap) + let modificationType = prettyString(forModificationKind: key) + if apiType == "class" && key == "key.parsed_declaration" { + // Ignore declarations for classes because it's a complete representation of the class's + // code, which is not helpful diff information. + continue + } + changes[root, withDefault: []].append(.modification(apiType: apiType, + name: name, + modificationType: modificationType, + from: oldValue, + to: newValue)) + } + } + } + + return changes +} + +extension ApiChange { + public func toMarkdown() -> String { + switch self { + case .addition(let apiType, let name): + return "*new* \(apiType): \(name)" + case .deletion(let apiType, let name): + return "*removed* \(apiType): \(name)" + case .modification(let apiType, let name, let modificationType, let from, let to): + return [ + "*modified* \(apiType): \(name)", + "", + "| Type of change: | \(modificationType) |", + "|---|---|", + "| From: | `\(from.replacingOccurrences(of: "\n", with: " "))` |", + "| To: | `\(to.replacingOccurrences(of: "\n", with: " "))` |" + ].joined(separator: "\n") + } + } +} + +extension ApiChange: Equatable {} + +public func == (left: ApiChange, right: ApiChange) -> Bool { + switch (left, right) { + case (let .addition(apiType, name), let .addition(apiType2, name2)): + return apiType == apiType2 && name == name2 + case (let .deletion(apiType, name), let .deletion(apiType2, name2)): + return apiType == apiType2 && name == name2 + case (let .modification(apiType, name, modificationType, from, to), + let .modification(apiType2, name2, modificationType2, from2, to2)): + return apiType == apiType2 && name == name2 && modificationType == modificationType2 && from == from2 && to == to2 + default: + return false + } +} + +/** + get-with-default API for Dictionary + + Example usage: dict[key, withDefault: []] + */ +extension Dictionary { + subscript(key: Key, withDefault value: @autoclosure () -> Value) -> Value { + mutating get { + if self[key] == nil { + self[key] = value() + } + return self[key]! + } + set { + self[key] = newValue + } + } +} + +/** + Sorting function for APINode instances. + + Sorts by filename. + + Example usage: sorted(by: apiNodeIsOrderedBefore) + */ +func apiNodeIsOrderedBefore(prev: APINode, next: APINode) -> Bool { + if let prevFile = prev["key.doc.file"] as? String, let nextFile = next["key.doc.file"] as? String { + return prevFile < nextFile + } + return false +} + +/** Union two dictionaries, preferring existing values if they possess a parent.usr key. */ +func += (left: inout ApiNameNodeMap, right: ApiNameNodeMap) { + for (k, v) in right { + if left[k] == nil { + left.updateValue(v, forKey: k) + } else if let object = left[k], object["parent.usr"] == nil { + left.updateValue(v, forKey: k) + } + } +} + +func prettyString(forKind kind: String) -> String { + if let pretty = [ + // Objective-C + "sourcekitten.source.lang.objc.decl.protocol": "protocol", + "sourcekitten.source.lang.objc.decl.typedef": "typedef", + "sourcekitten.source.lang.objc.decl.method.instance": "method", + "sourcekitten.source.lang.objc.decl.property": "property", + "sourcekitten.source.lang.objc.decl.class": "class", + "sourcekitten.source.lang.objc.decl.constant": "constant", + "sourcekitten.source.lang.objc.decl.enum": "enum", + "sourcekitten.source.lang.objc.decl.enumcase": "enum value", + "sourcekitten.source.lang.objc.decl.category": "category", + "sourcekitten.source.lang.objc.decl.method.class": "class method", + "sourcekitten.source.lang.objc.decl.struct": "struct", + "sourcekitten.source.lang.objc.decl.field": "field", + + // Swift + "source.lang.swift.decl.function.method.static": "static method", + "source.lang.swift.decl.function.method.instance": "method", + "source.lang.swift.decl.var.instance": "var", + "source.lang.swift.decl.class": "class", + "source.lang.swift.decl.var.static": "static var", + "source.lang.swift.decl.enum": "enum", + "source.lang.swift.decl.function.free": "function", + "source.lang.swift.decl.var.global": "global var", + "source.lang.swift.decl.protocol": "protocol", + "source.lang.swift.decl.enumelement": "enum value" + ][kind] { + return pretty + } + return kind +} + +func prettyString(forModificationKind kind: String) -> String { + switch kind { + case "key.swift_declaration": return "Swift declaration" + case "key.parsed_declaration": return "Declaration" + case "key.doc.declaration": return "Declaration" + case "key.typename": return "Declaration" + case "key.always_deprecated": return "Deprecation" + case "key.deprecation_message": return "Deprecation message" + default: return kind + } +} + +/** Walk the APINode to the root node. */ +func rootName(forApi api: APINode, apis: ApiNameNodeMap) -> String { + let name = api["key.name"] as! String + if let parentUsr = api["parent.usr"] as? String, let parentApi = apis[parentUsr] { + return rootName(forApi: parentApi, apis: apis) + } + return name +} + +func prettyName(forApi api: APINode, apis: ApiNameNodeMap) -> String { + let name = api["key.name"] as! String + if let parentUsr = api["parent.usr"] as? String, let parentApi = apis[parentUsr] { + return "`\(name)` in \(prettyName(forApi: parentApi, apis: apis))" + } + return "`\(name)`" +} + +/** Normalize data contained in an API node json dictionary. */ +func apiNode(from sourceKittenNode: SourceKittenNode) -> APINode { + var data = sourceKittenNode + data.removeValue(forKey: "key.substructure") + for (key, value) in data { + data[key] = String(describing: value) + } + return data +} + +/** + Recursively iterate over each sourcekitten node and extract a flattened map of USR identifier to + APINode instance. + */ +func extractAPINodeMap(from sourceKittenNodes: [SourceKittenNode]) -> ApiNameNodeMap { + var map: ApiNameNodeMap = [:] + for file in sourceKittenNodes { + for (_, information) in file { + let substructure = (information as! SourceKittenNode)["key.substructure"] as! [SourceKittenNode] + for jsonNode in substructure { + map += extractAPINodeMap(from: jsonNode) + } + } + } + return map +} + +/** + Recursively iterate over a sourcekitten node and extract a flattened map of USR identifier to + APINode instance. + */ +func extractAPINodeMap(from sourceKittenNode: SourceKittenNode, parentUsr: String? = nil) -> ApiNameNodeMap { + var map: ApiNameNodeMap = [:] + for (key, value) in sourceKittenNode { + switch key { + case "key.usr": + if let accessibility = sourceKittenNode["key.accessibility"] { + if accessibility as! String != "source.lang.swift.accessibility.public" && + accessibility as! String != "source.lang.swift.accessibility.open" { + continue + } + } else if let kind = sourceKittenNode["key.kind"] as? String, kind == "source.lang.swift.decl.extension" { + continue + } + var node = apiNode(from: sourceKittenNode) + + // Create a reference to the parent node + node["parent.usr"] = parentUsr + + // Store the API node in the map + map[value as! String] = node + + case "key.substructure": + let substructure = value as! [SourceKittenNode] + for subSourceKittenNode in substructure { + map += extractAPINodeMap(from: subSourceKittenNode, parentUsr: sourceKittenNode["key.usr"] as? String) + } + default: + continue + } + } + return map +} + +/** + Execute sourcekitten with a given umbrella header. + + Only meant to be used in unit test builds. + + @param header Absolute path to an umbrella header. + */ +func runSourceKitten(withHeader header: String) throws -> JSONObject { + let task = Process() + task.launchPath = "/usr/bin/env" + task.arguments = [ + "/usr/local/bin/sourcekitten", + "doc", + "--objc", + header, + "--", + "-x", + "objective-c", + ] + let standardOutput = Pipe() + task.standardOutput = standardOutput + task.launch() + task.waitUntilExit() + var data = standardOutput.fileHandleForReading.readDataToEndOfFile() + let tmpDir = ProcessInfo.processInfo.environment["TMPDIR"]!.replacingOccurrences(of: "/", with: "\\/") + let string = String(data: data, encoding: String.Encoding.utf8)! + .replacingOccurrences(of: tmpDir + "old\\/", with: "") + .replacingOccurrences(of: tmpDir + "new\\/", with: "") + data = string.data(using: String.Encoding.utf8)! + return try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)) +} diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/main.swift b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift new file mode 100644 index 00000000000..55b55589f3b --- /dev/null +++ b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift @@ -0,0 +1,96 @@ +import Foundation + + +if ProcessInfo.processInfo.arguments.count < 2 { + print("usage: APIDiffReport [forwarding sourcekitten args]") + exit(1) +} + +func readJson(at path: String) throws -> Any { + let url = URL(fileURLWithPath: path) + let data = try Data(contentsOf: url) + + if !data.isEmpty { + return try JSONSerialization.jsonObject(with: data) + } else { + return [] + } +} + +func runSourcekitten(apiFolder: String, args: [String]) -> Data? { +// let resultFileURL = NSURL.fileURL(withPathComponents: [NSTemporaryDirectory(), NSUUID().uuidString]) + // sourcekitten doc --module-name test -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=13.3,name=iPhone 8 Plus' -project ../updated/test.xcodeproj -scheme test clean build > ../updated.txt + let task = Process() + task.launchPath = "/usr/local/bin/sourcekitten" + task.currentDirectoryPath = apiFolder + task.arguments = args + + let standardOutput = Pipe() + let standardError = Pipe() + let outputHandle = standardError.fileHandleForReading + outputHandle.waitForDataInBackgroundAndNotify() + outputHandle.readabilityHandler = { pipe in + guard let currentOutput = String(data: pipe.availableData, encoding: .utf8) else { + print("Error decoding output data: \(pipe.availableData)") + return + } + + guard !currentOutput.isEmpty else { + return + } + DispatchQueue.main.async { + print(currentOutput) + } + } + + task.standardOutput = standardOutput + task.standardError = standardError + task.launch() + task.waitUntilExit() + + if task.terminationStatus == 0 { + print("Sourcekitten succeeded.") + return standardOutput.fileHandleForReading.readDataToEndOfFile() + } else { + print("Sourcekitten failed.") + return nil + } +} + +let oldApiFolder = ProcessInfo.processInfo.arguments[1] +let newApiFolder = ProcessInfo.processInfo.arguments[2] +var sourcekittenArgs = Array() + +if ProcessInfo.processInfo.arguments.count > 2 { + sourcekittenArgs = Array(ProcessInfo.processInfo.arguments.suffix(from: 3)) +} + +print("Running 'Old API' Sourcekitten... ") +guard let oldAPIDoc = runSourcekitten(apiFolder: oldApiFolder, + args: sourcekittenArgs) else { + exit(1) +} + +print("Running 'New API' Sourcekitten... ") +guard let newAPIDoc = runSourcekitten(apiFolder: newApiFolder, + args: sourcekittenArgs) else { + exit(1) +} + +let oldApi = try JSONSerialization.jsonObject(with: oldAPIDoc) +let newApi = try JSONSerialization.jsonObject(with: newAPIDoc) + +let report = try diffreport(oldApi: oldApi, newApi: newApi) + +if report.isEmpty { + print("No breaking changes detected!") +} else { + print("\n**** BREAKING CHANGES DETECTED ****") + for (symbol, change) in report { + print("\nBreaking changes in '\(symbol)'") + print(change.map({ $0.toMarkdown() }).joined(separator: "\n\n")) + } + exit(2) +} + + From fb923e7d2e5f7b34b287c9ad59c91360795ed694 Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 29 Sep 2020 12:37:52 +0300 Subject: [PATCH 02/20] vk-399-api-stability: added CI job to run API diff. --- .circleci/config.yml | 85 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b8a3b77d916..c27dcfbe99c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -72,8 +72,8 @@ step-library: run: name: Install Dependencies command: | - carthage bootstrap --platform ios --cache-builds --configuration Debug --no-use-binaries --use-netrc - carthage outdated --xcode-warnings # prints warnings if there are outdated carthage dependencies + cd << parameters.dependencies_path >> && carthage bootstrap --platform ios --cache-builds --configuration Debug --no-use-binaries --use-netrc + cd << parameters.dependencies_path >> && carthage outdated --xcode-warnings # prints warnings if there are outdated carthage dependencies - &install-dependencies-12 when: @@ -83,7 +83,7 @@ step-library: run: name: Install Dependencies command: | - ./scripts/wcarthage.sh bootstrap --platform ios --cache-builds --configuration Debug --no-use-binaries --use-netrc + cd << parameters.dependencies_path >> && ./scripts/wcarthage.sh bootstrap --platform ios --cache-builds --configuration Debug --no-use-binaries --use-netrc - &build-Example run: @@ -101,6 +101,20 @@ step-library: echo "MOBILE_METRICS_TOKEN not provided" fi +commands: + install-dependencies-guided: + description: "todo" + parameters: + xcode: + type: string + default: "11.4.1" + dependencies_path: + type: string + default: "./" + steps: + - *install-dependencies + - *install-dependencies-12 + jobs: pod-job: parameters: @@ -164,6 +178,9 @@ jobs: delete_private_deps: type: boolean default: false + dependencies_path: + type: string + default: "./" macos: xcode: << parameters.xcode >> environment: @@ -208,6 +225,9 @@ jobs: xcode: type: string default: "11.4.1" + dependencies_path: + type: string + default: "./" macos: xcode: << parameters.xcode >> environment: @@ -234,6 +254,64 @@ jobs: steps: - *trigger-metrics + api-diff-job: + parameters: + xcode: + type: string + default: "11.4.1" + device: + type: string + default: "iPhone 8 Plus" + iOS: + type: string + default: "13.4.1" + base_api_tag: + type: string + default: "v0.40.0" + base_api_folder: + type: string + default: "base_api_folder" + dependencies_path: + type: string + default: "./" + macos: + xcode: << parameters.xcode >> + environment: + HOMEBREW_NO_AUTO_UPDATE: 1 + steps: + - checkout + - *prepare-mapbox-file + - *prepare-netrc-file + - *update-carthage-version + - *restore-cache + - *install-dependencies + - *install-dependencies-12 + - *save-cache + - run: + name: Creating temporary Base API container folder + command: mkdir << parameters.base_api_folder >> + - checkout: + path: << parameters.base_api_folder >> + - run: + name: Checking out Base API + working_directory: << parameters.base_api_folder >> + command: git checkout << parameters.base_api_tag >> + - install-dependencies-guided: + dependencies_path: << parameters.base_api_folder >> + xcode: << parameters.xcode >> + - run: + name: Install prerequisites + command: if [ $(xcversion simulators | grep -cF "iOS << parameters.iOS >> Simulator (installed)") -eq 0 ]; then xcversion simulators --install="iOS << parameters.iOS >>" || true; fi + - run: + name: Building API Diff Report + command: cd scripts/APIDiffReport && swift build + - run: + name: MapboxCoreNavigation API Diff Report + command: swift run scripts/APIDiffReport/APIDiffReport << parameters.base_api_folder >> ./ doc --module-name MapboxCoreNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxCoreNavigation clean build + - run: + name: MapboxCoreNavigation API Diff Report + command: swift run scripts/APIDiffReport/APIDiffReport << parameters.base_api_folder >> ./ doc --module-name MapboxNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxNavigation clean build + workflows: workflow: jobs: @@ -286,3 +364,4 @@ workflows: filters: branches: only: main + - api-diff-job From 011e0e1d8c6f921cd60b85784839df9cf2ce8097 Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 29 Sep 2020 12:42:55 +0300 Subject: [PATCH 03/20] vk-399-api-diff: updated LICENSE info --- LICENSE.md | 19 +++++++++++++++++++ .../Sources/APIDiffReport/diffreport.swift | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/LICENSE.md b/LICENSE.md index 5647c270278..87932189f5e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -25,3 +25,22 @@ 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. + + +----- + +Contains modified version of https://github.com/material-motion/apidiff/blob/e78f92ae310cd4affc86a4510bb7b9f9609662d2/apple/diffreport/Sources/diffreportlib/diffreport.swift + +Copyright 2016-present The Material Motion Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift index 88b5dfa6462..fd019e14fd7 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift @@ -12,6 +12,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + Code modified by Mapbox to adjust API filetring criteria, original at https://github.com/material-motion/apidiff/blob/e78f92ae310cd4affc86a4510bb7b9f9609662d2/apple/diffreport/Sources/diffreportlib/diffreport.swift + + TODO: log briefly all code updates */ import Foundation From 0b061eae71a150ae29a197a8c014170029e98f2a Mon Sep 17 00:00:00 2001 From: udumft Date: Wed, 30 Sep 2020 18:52:46 +0300 Subject: [PATCH 04/20] vk-399-api-stability: refined config.yml. Made APIDiffReport separately generate logs and diffs. --- .circleci/config.yml | 151 +++++++++++------- .../APIDiffReport.xcodeproj/project.pbxproj | 8 + .../xcschemes/APIDiffReport.xcscheme | 24 ++- .../Sources/APIDiffReport/apiDiff.swift | 33 ++++ .../Sources/APIDiffReport/apiLog.swift | 53 ++++++ .../Sources/APIDiffReport/main.swift | 131 ++++++--------- 6 files changed, 262 insertions(+), 138 deletions(-) create mode 100644 scripts/APIDiffReport/Sources/APIDiffReport/apiDiff.swift create mode 100644 scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift diff --git a/.circleci/config.yml b/.circleci/config.yml index c27dcfbe99c..55743bef2c4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,42 @@ version: 2.1 +commands: + save-api-diff-cache: + parameters: + api_diff_cache: + type: string + steps: + - save_cache: + key: nav-api-diff-cache-v5-{{ .Environment.CIRCLE_JOB }}-{{ << parameters.api_diff_cache >> }} + paths: + - << pipeline.parameters.api_log_file >> + + restore-api-diff-cache: + parameters: + api_diff_cache: + type: string + steps: + - restore_cache: + keys: + - nav-api-diff-cache-v5-{{ .Environment.CIRCLE_JOB }}-{{ << parameters.api_diff_cache >> }} + + run_api_log: + parameters: + xcode: + type: string + device: + type: string + steps: + - run: + name: Building API Diff Report + command: cd scripts/APIDiffReport && swift build + - run: + name: Generating MapboxCoreNavigation API Log + command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. << pipeline.parameters.api_log_file >>/core_navigation_log.json doc --module-name MapboxCoreNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxCoreNavigation clean build + - run: + name: Generating MapboxNavigation API Log + command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. << pipeline.parameters.api_log_file >>/navigation_log.json doc --module-name MapboxNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxNavigation clean build + step-library: - &restore-cache restore_cache: @@ -72,8 +109,8 @@ step-library: run: name: Install Dependencies command: | - cd << parameters.dependencies_path >> && carthage bootstrap --platform ios --cache-builds --configuration Debug --no-use-binaries --use-netrc - cd << parameters.dependencies_path >> && carthage outdated --xcode-warnings # prints warnings if there are outdated carthage dependencies + carthage bootstrap --platform ios --cache-builds --configuration Debug --no-use-binaries --use-netrc + carthage outdated --xcode-warnings # prints warnings if there are outdated carthage dependencies - &install-dependencies-12 when: @@ -83,7 +120,7 @@ step-library: run: name: Install Dependencies command: | - cd << parameters.dependencies_path >> && ./scripts/wcarthage.sh bootstrap --platform ios --cache-builds --configuration Debug --no-use-binaries --use-netrc + ./scripts/wcarthage.sh bootstrap --platform ios --cache-builds --configuration Debug --no-use-binaries --use-netrc - &build-Example run: @@ -101,20 +138,6 @@ step-library: echo "MOBILE_METRICS_TOKEN not provided" fi -commands: - install-dependencies-guided: - description: "todo" - parameters: - xcode: - type: string - default: "11.4.1" - dependencies_path: - type: string - default: "./" - steps: - - *install-dependencies - - *install-dependencies-12 - jobs: pod-job: parameters: @@ -178,9 +201,9 @@ jobs: delete_private_deps: type: boolean default: false - dependencies_path: - type: string - default: "./" + generate_api_log: + type: boolean + default: false macos: xcode: << parameters.xcode >> environment: @@ -219,15 +242,20 @@ jobs: condition: << parameters.codecoverage >> steps: - run: bash <(curl -s https://codecov.io/bash) + - when: + condition: << parameters.generate_api_log >> + steps: + - run_api_log: + xcode: << parameters.xcode >> + device: << parameters.device >> + - save-api-diff-cache: + api_diff_cache: $CIRCLE_SHA1 xcode-11-examples: parameters: xcode: type: string default: "11.4.1" - dependencies_path: - type: string - default: "./" macos: xcode: << parameters.xcode >> environment: @@ -268,49 +296,58 @@ jobs: base_api_tag: type: string default: "v0.40.0" - base_api_folder: - type: string - default: "base_api_folder" - dependencies_path: - type: string - default: "./" macos: xcode: << parameters.xcode >> environment: HOMEBREW_NO_AUTO_UPDATE: 1 steps: - checkout - - *prepare-mapbox-file - - *prepare-netrc-file - - *update-carthage-version - - *restore-cache - - *install-dependencies - - *install-dependencies-12 - - *save-cache - - run: - name: Creating temporary Base API container folder - command: mkdir << parameters.base_api_folder >> - - checkout: - path: << parameters.base_api_folder >> - run: name: Checking out Base API - working_directory: << parameters.base_api_folder >> command: git checkout << parameters.base_api_tag >> - - install-dependencies-guided: - dependencies_path: << parameters.base_api_folder >> - xcode: << parameters.xcode >> + - restore-api-diff-cache: + api_diff_cache: $CIRCLE_SHA1 - run: - name: Install prerequisites - command: if [ $(xcversion simulators | grep -cF "iOS << parameters.iOS >> Simulator (installed)") -eq 0 ]; then xcversion simulators --install="iOS << parameters.iOS >>" || true; fi + name: Move Api Diff + command: mv << pipeline.parameters.api_log_file >> original_api + - restore-api-diff-cache: + api_diff_cache: << parameters.base_api_tag >> - run: - name: Building API Diff Report - command: cd scripts/APIDiffReport && swift build + name: Verify Base API report exists + command: | + if test -f "<< pipeline.parameters.api_log_file >>"; then \ + echo "Base API log file exists" \ + export API_CACHE_EXISTS=TRUE \ + fi + - unless: + condition: .Environment.API_CACHE_EXISTS + steps: + - *prepare-mapbox-file + - *prepare-netrc-file + - *update-carthage-version + - *restore-cache + - *install-dependencies + - *install-dependencies-12 + - run: + name: Install prerequisites + command: if [ $(xcversion simulators | grep -cF "iOS << parameters.iOS >> Simulator (installed)") -eq 0 ]; then xcversion simulators --install="iOS << parameters.iOS >>" || true; fi + - *save-cache + - save-api-diff-cache: + api_diff_cache: << parameters.base_api_tag >> + - run_api_log: + xcode: << parameters.xcode >> + device: << parameters.device >> - run: - name: MapboxCoreNavigation API Diff Report - command: swift run scripts/APIDiffReport/APIDiffReport << parameters.base_api_folder >> ./ doc --module-name MapboxCoreNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxCoreNavigation clean build + name: Generating MapboxCoreNavigation API Diff + command: cd scripts/APIDiffReport && swift run APIDiffReport diff original_api/core_navigation_log.json << pipeline.parameters.api_log_file>>/core_navigation_log.json - run: - name: MapboxCoreNavigation API Diff Report - command: swift run scripts/APIDiffReport/APIDiffReport << parameters.base_api_folder >> ./ doc --module-name MapboxNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxNavigation clean build + name: Generating MapboxNavigation API Diff + command: cd scripts/APIDiffReport && swift run APIDiffReport diff original_api/navigation_log.json << pipeline.parameters.api_log_file>>/navigation_log.json + +parameters: + api_log_file: + type: string + default: api_logs workflows: workflow: @@ -321,6 +358,7 @@ workflows: iOS: "14.0" test: false device: "iPhone 8 Plus" + generate_api_log: true - build-job: name: "Xcode_11.5_iOS_13.5" xcode: "11.5.0" @@ -364,4 +402,9 @@ workflows: filters: branches: only: main - - api-diff-job + - api-diff-job-approval: + type: approval + - api-diff-job: + requires: + - "Xcode_12.0_iOS_14.0" + - api-diff-job-approval diff --git a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj index 787ea2e9e0e..c8a9af0ef66 100644 --- a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 2B237961251E3C3000CCF9C4 /* diffreport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B23795F251E3C3000CCF9C4 /* diffreport.swift */; }; 2B237962251E3C3000CCF9C4 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B237960251E3C3000CCF9C4 /* main.swift */; }; + 2B2379662524717100CCF9C4 /* apiLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2379652524717100CCF9C4 /* apiLog.swift */; }; + 2B2379682524768600CCF9C4 /* apiDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2379672524768600CCF9C4 /* apiDiff.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -28,6 +30,8 @@ 2B23795F251E3C3000CCF9C4 /* diffreport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = diffreport.swift; sourceTree = ""; }; 2B237960251E3C3000CCF9C4 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 2B237963251E3C4000CCF9C4 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + 2B2379652524717100CCF9C4 /* apiLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = apiLog.swift; sourceTree = ""; }; + 2B2379672524768600CCF9C4 /* apiDiff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = apiDiff.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -71,6 +75,8 @@ children = ( 2B23795F251E3C3000CCF9C4 /* diffreport.swift */, 2B237960251E3C3000CCF9C4 /* main.swift */, + 2B2379652524717100CCF9C4 /* apiLog.swift */, + 2B2379672524768600CCF9C4 /* apiDiff.swift */, ); path = APIDiffReport; sourceTree = ""; @@ -134,7 +140,9 @@ buildActionMask = 2147483647; files = ( 2B237962251E3C3000CCF9C4 /* main.swift in Sources */, + 2B2379662524717100CCF9C4 /* apiLog.swift in Sources */, 2B237961251E3C3000CCF9C4 /* diffreport.swift in Sources */, + 2B2379682524768600CCF9C4 /* apiDiff.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/scripts/APIDiffReport/APIDiffReport.xcodeproj/xcshareddata/xcschemes/APIDiffReport.xcscheme b/scripts/APIDiffReport/APIDiffReport.xcodeproj/xcshareddata/xcschemes/APIDiffReport.xcscheme index 4a15dfedd86..3d4073261e3 100644 --- a/scripts/APIDiffReport/APIDiffReport.xcodeproj/xcshareddata/xcschemes/APIDiffReport.xcscheme +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/xcshareddata/xcschemes/APIDiffReport.xcscheme @@ -51,16 +51,36 @@ + + + isEnabled = "NO"> + isEnabled = "NO"> + + + + + + + + diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/apiDiff.swift b/scripts/APIDiffReport/Sources/APIDiffReport/apiDiff.swift new file mode 100644 index 00000000000..04641406e88 --- /dev/null +++ b/scripts/APIDiffReport/Sources/APIDiffReport/apiDiff.swift @@ -0,0 +1,33 @@ + +import Foundation + +struct ApiDiff { + + static func runApiDiff(oldApiPath: URL, newApiPath: URL) throws -> Bool { + let oldApi = try readJson(at: oldApiPath) + let newApi = try readJson(at: newApiPath) + let report = try diffreport(oldApi: oldApi, newApi: newApi) + + if report.isEmpty { + print("No breaking changes detected!") + return true + } else { + print("\n**** BREAKING CHANGES DETECTED ****") + for (symbol, change) in report { + print("\nBreaking changes in '\(symbol)'") + print(change.map({ $0.toMarkdown() }).joined(separator: "\n\n")) + } + return false + } + } + + static private func readJson(at path: URL) throws -> Any { + let data = try Data(contentsOf: path) + + if !data.isEmpty { + return try JSONSerialization.jsonObject(with: data) + } else { + return [] + } + } +} diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift b/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift new file mode 100644 index 00000000000..0fed212ce3e --- /dev/null +++ b/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift @@ -0,0 +1,53 @@ + +import Foundation + +struct ApiLog { + + static func runApiLog(apiFolder: String, args: [String]) throws -> String? { + print("Running API Logging... ") + guard let APIDoc = runSourcekitten(apiFolder: apiFolder, + args: args) else { + exit(1) + } + + return String(data: APIDoc, encoding: .utf8) + } + + static private func runSourcekitten(apiFolder: String, args: [String]) -> Data? { + let task = Process() + task.launchPath = "/usr/local/bin/sourcekitten" + task.currentDirectoryPath = apiFolder + task.arguments = args + + let standardOutput = Pipe() + let standardError = Pipe() + let outputHandle = standardError.fileHandleForReading + outputHandle.waitForDataInBackgroundAndNotify() + outputHandle.readabilityHandler = { pipe in + guard let currentOutput = String(data: pipe.availableData, encoding: .utf8) else { + print("Error decoding output data: \(pipe.availableData)") + return + } + + guard !currentOutput.isEmpty else { + return + } + DispatchQueue.main.async { + print(currentOutput) + } + } + + task.standardOutput = standardOutput + task.standardError = standardError + task.launch() + task.waitUntilExit() + + if task.terminationStatus == 0 { + print("Sourcekitten succeeded.") + return standardOutput.fileHandleForReading.readDataToEndOfFile() + } else { + print("Sourcekitten failed.") + return nil + } + } +} diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/main.swift b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift index 55b55589f3b..613595e90d1 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/main.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift @@ -1,96 +1,63 @@ import Foundation -if ProcessInfo.processInfo.arguments.count < 2 { - print("usage: APIDiffReport [forwarding sourcekitten args]") - exit(1) +func printUsage() { + print("Usage:") + print(" log [forwarding sourcekitten args]") + print(" - Parses provided project and logs it's API structure in JSON format.") + print(" diff ") + print(" - Runs a comparison between 2 JSON API logs and prints detected breaking changes.") } -func readJson(at path: String) throws -> Any { - let url = URL(fileURLWithPath: path) - let data = try Data(contentsOf: url) - - if !data.isEmpty { - return try JSONSerialization.jsonObject(with: data) - } else { - return [] +func absURL ( _ path: String ) -> URL { + // some methods cannot correctly expand '~' in a path, so we'll do it manually + guard path != "~" else { + return FileManager.default.homeDirectoryForCurrentUser } -} - -func runSourcekitten(apiFolder: String, args: [String]) -> Data? { -// let resultFileURL = NSURL.fileURL(withPathComponents: [NSTemporaryDirectory(), NSUUID().uuidString]) - // sourcekitten doc --module-name test -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=13.3,name=iPhone 8 Plus' -project ../updated/test.xcodeproj -scheme test clean build > ../updated.txt - let task = Process() - task.launchPath = "/usr/local/bin/sourcekitten" - task.currentDirectoryPath = apiFolder - task.arguments = args - - let standardOutput = Pipe() - let standardError = Pipe() - let outputHandle = standardError.fileHandleForReading - outputHandle.waitForDataInBackgroundAndNotify() - outputHandle.readabilityHandler = { pipe in - guard let currentOutput = String(data: pipe.availableData, encoding: .utf8) else { - print("Error decoding output data: \(pipe.availableData)") - return - } - - guard !currentOutput.isEmpty else { - return - } - DispatchQueue.main.async { - print(currentOutput) - } - } - - task.standardOutput = standardOutput - task.standardError = standardError - task.launch() - task.waitUntilExit() - - if task.terminationStatus == 0 { - print("Sourcekitten succeeded.") - return standardOutput.fileHandleForReading.readDataToEndOfFile() - } else { - print("Sourcekitten failed.") - return nil - } -} - -let oldApiFolder = ProcessInfo.processInfo.arguments[1] -let newApiFolder = ProcessInfo.processInfo.arguments[2] -var sourcekittenArgs = Array() - -if ProcessInfo.processInfo.arguments.count > 2 { - sourcekittenArgs = Array(ProcessInfo.processInfo.arguments.suffix(from: 3)) -} + guard path.hasPrefix("~/") else { return URL(fileURLWithPath: path) } -print("Running 'Old API' Sourcekitten... ") -guard let oldAPIDoc = runSourcekitten(apiFolder: oldApiFolder, - args: sourcekittenArgs) else { - exit(1) + var relativePath = path + relativePath.removeFirst(2) + return URL(fileURLWithPath: relativePath, + relativeTo: FileManager.default.homeDirectoryForCurrentUser + ) } -print("Running 'New API' Sourcekitten... ") -guard let newAPIDoc = runSourcekitten(apiFolder: newApiFolder, - args: sourcekittenArgs) else { +guard ProcessInfo.processInfo.arguments.count > 2 else { + printUsage() exit(1) } -let oldApi = try JSONSerialization.jsonObject(with: oldAPIDoc) -let newApi = try JSONSerialization.jsonObject(with: newAPIDoc) - -let report = try diffreport(oldApi: oldApi, newApi: newApi) - -if report.isEmpty { - print("No breaking changes detected!") -} else { - print("\n**** BREAKING CHANGES DETECTED ****") - for (symbol, change) in report { - print("\nBreaking changes in '\(symbol)'") - print(change.map({ $0.toMarkdown() }).joined(separator: "\n\n")) +switch ProcessInfo.processInfo.arguments[1] { +case "log": + guard ProcessInfo.processInfo.arguments.count > 3 else { + printUsage() + exit(1) + } + + let apiFolder = ProcessInfo.processInfo.arguments[2] + let outputFile = ProcessInfo.processInfo.arguments[3] + var sourcekittenArgs = Array() + if ProcessInfo.processInfo.arguments.count > 4 { + sourcekittenArgs = Array(ProcessInfo.processInfo.arguments.suffix(from: 4)) + } + + guard let log = try ApiLog.runApiLog(apiFolder: apiFolder, args: sourcekittenArgs) else { + print("Decoding 'sourcekitten' output failed.") + exit(1) } - exit(2) + + try log.write(to: absURL(outputFile), + atomically: true, + encoding: .utf8) +case "diff": + let oldApi = ProcessInfo.processInfo.arguments[2] + let newApi = ProcessInfo.processInfo.arguments[3] + + guard try ApiDiff.runApiDiff(oldApiPath: absURL(oldApi), newApiPath: absURL(newApi)) else { + exit(1) + } +default: + printUsage() + exit(1) } - - From b0ffb6b4d519525636ec889a1d7ffbe63d2660e7 Mon Sep 17 00:00:00 2001 From: udumft Date: Wed, 30 Sep 2020 19:36:32 +0300 Subject: [PATCH 05/20] vk-399-api-stability: older MacOS compatibility added --- scripts/APIDiffReport/Sources/APIDiffReport/main.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/main.swift b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift index 613595e90d1..671a6524c05 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/main.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift @@ -11,16 +11,16 @@ func printUsage() { func absURL ( _ path: String ) -> URL { // some methods cannot correctly expand '~' in a path, so we'll do it manually + let homeDirectory = URL(fileURLWithPath: NSHomeDirectory()) guard path != "~" else { - return FileManager.default.homeDirectoryForCurrentUser + return homeDirectory } guard path.hasPrefix("~/") else { return URL(fileURLWithPath: path) } var relativePath = path relativePath.removeFirst(2) - return URL(fileURLWithPath: relativePath, - relativeTo: FileManager.default.homeDirectoryForCurrentUser - ) + return URL(string: relativePath, + relativeTo: homeDirectory) ?? homeDirectory } guard ProcessInfo.processInfo.arguments.count > 2 else { From fb44ec107a3e38ca677df6481c895e4ca72e31b4 Mon Sep 17 00:00:00 2001 From: udumft Date: Wed, 30 Sep 2020 20:18:25 +0300 Subject: [PATCH 06/20] vk-399-api-stability: corrected workflow with sourcekitten installation as well as correcting typos and parameters values. --- .circleci/config.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 55743bef2c4..945b83d24b9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,20 +22,23 @@ commands: run_api_log: parameters: - xcode: + iOS: type: string device: type: string steps: + - run: + name: Install Sourcekitten + command: brew update && brew install sourcekitten - run: name: Building API Diff Report command: cd scripts/APIDiffReport && swift build - run: name: Generating MapboxCoreNavigation API Log - command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. << pipeline.parameters.api_log_file >>/core_navigation_log.json doc --module-name MapboxCoreNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxCoreNavigation clean build + command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. << pipeline.parameters.api_log_file >>/core_navigation_log.json doc --module-name MapboxCoreNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=<< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxCoreNavigation clean build - run: name: Generating MapboxNavigation API Log - command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. << pipeline.parameters.api_log_file >>/navigation_log.json doc --module-name MapboxNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxNavigation clean build + command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. << pipeline.parameters.api_log_file >>/navigation_log.json doc --module-name MapboxNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=<< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxNavigation clean build step-library: - &restore-cache @@ -246,7 +249,7 @@ jobs: condition: << parameters.generate_api_log >> steps: - run_api_log: - xcode: << parameters.xcode >> + iOS: << parameters.iOS >> device: << parameters.device >> - save-api-diff-cache: api_diff_cache: $CIRCLE_SHA1 @@ -335,7 +338,7 @@ jobs: - save-api-diff-cache: api_diff_cache: << parameters.base_api_tag >> - run_api_log: - xcode: << parameters.xcode >> + iOS: << parameters.iOS >> device: << parameters.device >> - run: name: Generating MapboxCoreNavigation API Diff @@ -347,7 +350,7 @@ jobs: parameters: api_log_file: type: string - default: api_logs + default: $CIRCLE_WORKING_DIRECTORY/api_logs workflows: workflow: From 283de3285647536d01eb85e71d7b0155898ecccc Mon Sep 17 00:00:00 2001 From: udumft Date: Wed, 30 Sep 2020 21:19:22 +0300 Subject: [PATCH 07/20] vk-399-api-stability: resolved command line output freeze, added creating intermediate output directories --- .../APIDiffReport/Sources/APIDiffReport/apiLog.swift | 10 ++++++++-- scripts/APIDiffReport/Sources/APIDiffReport/main.swift | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift b/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift index 0fed212ce3e..d0cdf576bea 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift @@ -14,6 +14,7 @@ struct ApiLog { } static private func runSourcekitten(apiFolder: String, args: [String]) -> Data? { + var result = Data() let task = Process() task.launchPath = "/usr/local/bin/sourcekitten" task.currentDirectoryPath = apiFolder @@ -21,9 +22,14 @@ struct ApiLog { let standardOutput = Pipe() let standardError = Pipe() - let outputHandle = standardError.fileHandleForReading + let outputHandle = standardOutput.fileHandleForReading + let errorHandle = standardError.fileHandleForReading outputHandle.waitForDataInBackgroundAndNotify() + errorHandle.waitForDataInBackgroundAndNotify() outputHandle.readabilityHandler = { pipe in + result.append(pipe.availableData) + } + errorHandle.readabilityHandler = { pipe in guard let currentOutput = String(data: pipe.availableData, encoding: .utf8) else { print("Error decoding output data: \(pipe.availableData)") return @@ -44,7 +50,7 @@ struct ApiLog { if task.terminationStatus == 0 { print("Sourcekitten succeeded.") - return standardOutput.fileHandleForReading.readDataToEndOfFile() + return result } else { print("Sourcekitten failed.") return nil diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/main.swift b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift index 671a6524c05..9e52ed48a6d 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/main.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift @@ -46,8 +46,10 @@ case "log": print("Decoding 'sourcekitten' output failed.") exit(1) } - - try log.write(to: absURL(outputFile), + let outputURL = absURL(outputFile) + try FileManager.default.createDirectory(at: outputURL.deletingLastPathComponent(), + withIntermediateDirectories: true) + try log.write(to: outputURL, atomically: true, encoding: .utf8) case "diff": From e161eb02ea33010ed6580390d1ee34f30c2e4a7f Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:22:36 +0300 Subject: [PATCH 08/20] Safe cache configuration. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 945b83d24b9..a079b5359c6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ commands: type: string steps: - save_cache: - key: nav-api-diff-cache-v5-{{ .Environment.CIRCLE_JOB }}-{{ << parameters.api_diff_cache >> }} + key: nav-api-diff-cache-v5-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ << parameters.api_diff_cache >> }} paths: - << pipeline.parameters.api_log_file >> From a39be1719f596376cf7ecf72ea8ae2a5bda4131f Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:22:54 +0300 Subject: [PATCH 09/20] Update Xcode project version. --- scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj index c8a9af0ef66..93d7abbf9a0 100644 --- a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ From 49b7b9286589b9633bf78f69d29f84252b37674c Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:23:31 +0300 Subject: [PATCH 10/20] Remove apiDiff.swift/apiLog.swift implementation. --- .../APIDiffReport.xcodeproj/project.pbxproj | 11 +--- .../Sources/APIDiffReport/apiDiff.swift | 33 ----------- .../Sources/APIDiffReport/apiLog.swift | 59 ------------------- 3 files changed, 3 insertions(+), 100 deletions(-) delete mode 100644 scripts/APIDiffReport/Sources/APIDiffReport/apiDiff.swift delete mode 100644 scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift diff --git a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj index 93d7abbf9a0..56ddb6453ce 100644 --- a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj @@ -9,8 +9,6 @@ /* Begin PBXBuildFile section */ 2B237961251E3C3000CCF9C4 /* diffreport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B23795F251E3C3000CCF9C4 /* diffreport.swift */; }; 2B237962251E3C3000CCF9C4 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B237960251E3C3000CCF9C4 /* main.swift */; }; - 2B2379662524717100CCF9C4 /* apiLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2379652524717100CCF9C4 /* apiLog.swift */; }; - 2B2379682524768600CCF9C4 /* apiDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2379672524768600CCF9C4 /* apiDiff.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -30,8 +28,6 @@ 2B23795F251E3C3000CCF9C4 /* diffreport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = diffreport.swift; sourceTree = ""; }; 2B237960251E3C3000CCF9C4 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 2B237963251E3C4000CCF9C4 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; - 2B2379652524717100CCF9C4 /* apiLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = apiLog.swift; sourceTree = ""; }; - 2B2379672524768600CCF9C4 /* apiDiff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = apiDiff.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,8 +71,6 @@ children = ( 2B23795F251E3C3000CCF9C4 /* diffreport.swift */, 2B237960251E3C3000CCF9C4 /* main.swift */, - 2B2379652524717100CCF9C4 /* apiLog.swift */, - 2B2379672524768600CCF9C4 /* apiDiff.swift */, ); path = APIDiffReport; sourceTree = ""; @@ -139,10 +133,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B260FC42563E7F90043C171 /* LogCommand.swift in Sources */, + 2B260FC32563E7F90043C171 /* DiffCommand.swift in Sources */, + 2B260FC52563E7F90043C171 /* DiffReportOptions.swift in Sources */, 2B237962251E3C3000CCF9C4 /* main.swift in Sources */, - 2B2379662524717100CCF9C4 /* apiLog.swift in Sources */, 2B237961251E3C3000CCF9C4 /* diffreport.swift in Sources */, - 2B2379682524768600CCF9C4 /* apiDiff.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/apiDiff.swift b/scripts/APIDiffReport/Sources/APIDiffReport/apiDiff.swift deleted file mode 100644 index 04641406e88..00000000000 --- a/scripts/APIDiffReport/Sources/APIDiffReport/apiDiff.swift +++ /dev/null @@ -1,33 +0,0 @@ - -import Foundation - -struct ApiDiff { - - static func runApiDiff(oldApiPath: URL, newApiPath: URL) throws -> Bool { - let oldApi = try readJson(at: oldApiPath) - let newApi = try readJson(at: newApiPath) - let report = try diffreport(oldApi: oldApi, newApi: newApi) - - if report.isEmpty { - print("No breaking changes detected!") - return true - } else { - print("\n**** BREAKING CHANGES DETECTED ****") - for (symbol, change) in report { - print("\nBreaking changes in '\(symbol)'") - print(change.map({ $0.toMarkdown() }).joined(separator: "\n\n")) - } - return false - } - } - - static private func readJson(at path: URL) throws -> Any { - let data = try Data(contentsOf: path) - - if !data.isEmpty { - return try JSONSerialization.jsonObject(with: data) - } else { - return [] - } - } -} diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift b/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift deleted file mode 100644 index d0cdf576bea..00000000000 --- a/scripts/APIDiffReport/Sources/APIDiffReport/apiLog.swift +++ /dev/null @@ -1,59 +0,0 @@ - -import Foundation - -struct ApiLog { - - static func runApiLog(apiFolder: String, args: [String]) throws -> String? { - print("Running API Logging... ") - guard let APIDoc = runSourcekitten(apiFolder: apiFolder, - args: args) else { - exit(1) - } - - return String(data: APIDoc, encoding: .utf8) - } - - static private func runSourcekitten(apiFolder: String, args: [String]) -> Data? { - var result = Data() - let task = Process() - task.launchPath = "/usr/local/bin/sourcekitten" - task.currentDirectoryPath = apiFolder - task.arguments = args - - let standardOutput = Pipe() - let standardError = Pipe() - let outputHandle = standardOutput.fileHandleForReading - let errorHandle = standardError.fileHandleForReading - outputHandle.waitForDataInBackgroundAndNotify() - errorHandle.waitForDataInBackgroundAndNotify() - outputHandle.readabilityHandler = { pipe in - result.append(pipe.availableData) - } - errorHandle.readabilityHandler = { pipe in - guard let currentOutput = String(data: pipe.availableData, encoding: .utf8) else { - print("Error decoding output data: \(pipe.availableData)") - return - } - - guard !currentOutput.isEmpty else { - return - } - DispatchQueue.main.async { - print(currentOutput) - } - } - - task.standardOutput = standardOutput - task.standardError = standardError - task.launch() - task.waitUntilExit() - - if task.terminationStatus == 0 { - print("Sourcekitten succeeded.") - return result - } else { - print("Sourcekitten failed.") - return nil - } - } -} From fc60ea4219c64b1de831ef467d1f7b06c97b90a8 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:24:04 +0300 Subject: [PATCH 11/20] Overload += func for diff report. --- .../Sources/APIDiffReport/diffreport.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift index fd019e14fd7..6707b6d734d 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift @@ -26,6 +26,17 @@ typealias SourceKittenNode = [String: Any] typealias APINode = [String: Any] typealias ApiNameNodeMap = [String: APINode] +/** Union two dictionaries, preferring existing values if they possess a parent.usr key. */ +func += (left: inout ApiNameNodeMap, right: ApiNameNodeMap) { + for (k, v) in right { + if left[k] == nil { + left.updateValue(v, forKey: k) + } else if let object = left[k], object["parent.usr"] == nil { + left.updateValue(v, forKey: k) + } + } +} + /** A type of API change. */ public enum ApiChange { case addition(apiType: String, name: String) From 83c9b631736ea204099dd962ada6297dd849e09d Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:24:41 +0300 Subject: [PATCH 12/20] Update APIDiffReport's implementation with new approach. --- .../Sources/APIDiffReport/main.swift | 65 ++----------------- 1 file changed, 5 insertions(+), 60 deletions(-) diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/main.swift b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift index 9e52ed48a6d..295c6a3b301 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/main.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift @@ -1,65 +1,10 @@ import Foundation - - -func printUsage() { - print("Usage:") - print(" log [forwarding sourcekitten args]") - print(" - Parses provided project and logs it's API structure in JSON format.") - print(" diff ") - print(" - Runs a comparison between 2 JSON API logs and prints detected breaking changes.") -} +import SwiftCLI func absURL ( _ path: String ) -> URL { - // some methods cannot correctly expand '~' in a path, so we'll do it manually - let homeDirectory = URL(fileURLWithPath: NSHomeDirectory()) - guard path != "~" else { - return homeDirectory - } - guard path.hasPrefix("~/") else { return URL(fileURLWithPath: path) } - - var relativePath = path - relativePath.removeFirst(2) - return URL(string: relativePath, - relativeTo: homeDirectory) ?? homeDirectory + return URL(fileURLWithPath: (path as NSString).expandingTildeInPath) } -guard ProcessInfo.processInfo.arguments.count > 2 else { - printUsage() - exit(1) -} - -switch ProcessInfo.processInfo.arguments[1] { -case "log": - guard ProcessInfo.processInfo.arguments.count > 3 else { - printUsage() - exit(1) - } - - let apiFolder = ProcessInfo.processInfo.arguments[2] - let outputFile = ProcessInfo.processInfo.arguments[3] - var sourcekittenArgs = Array() - if ProcessInfo.processInfo.arguments.count > 4 { - sourcekittenArgs = Array(ProcessInfo.processInfo.arguments.suffix(from: 4)) - } - - guard let log = try ApiLog.runApiLog(apiFolder: apiFolder, args: sourcekittenArgs) else { - print("Decoding 'sourcekitten' output failed.") - exit(1) - } - let outputURL = absURL(outputFile) - try FileManager.default.createDirectory(at: outputURL.deletingLastPathComponent(), - withIntermediateDirectories: true) - try log.write(to: outputURL, - atomically: true, - encoding: .utf8) -case "diff": - let oldApi = ProcessInfo.processInfo.arguments[2] - let newApi = ProcessInfo.processInfo.arguments[3] - - guard try ApiDiff.runApiDiff(oldApiPath: absURL(oldApi), newApiPath: absURL(newApi)) else { - exit(1) - } -default: - printUsage() - exit(1) -} +CLI(name: "APIDiffReport", + description: "A tool to detect Public API breaking changes", + commands: [LogCommand(), DiffCommand()]).goAndExit() From fe54922c76c3f61169719499e4d60809620c6914 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:25:09 +0300 Subject: [PATCH 13/20] Add SwiftCLI dependency into APIDiffReport/Package.swift. --- scripts/APIDiffReport/Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/APIDiffReport/Package.swift b/scripts/APIDiffReport/Package.swift index 2f7c2390982..c194c8a2ca8 100644 --- a/scripts/APIDiffReport/Package.swift +++ b/scripts/APIDiffReport/Package.swift @@ -15,13 +15,14 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), + .package(url: "https://github.com/jakeheis/SwiftCLI", from: "6.0.0") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "APIDiffReport", - dependencies: [] + dependencies: ["SwiftCLI"] ), ] ) From 902d8ad4988dc22dfce90ed3f0f3354f5bb55883 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:26:01 +0300 Subject: [PATCH 14/20] Add DiffCommand/DiffReportOptions/LogCommand (rebased version). --- .../Sources/APIDiffReport/DiffCommand.swift | 62 ++++++++++++++ .../APIDiffReport/DiffReportOptions.swift | 44 ++++++++++ .../Sources/APIDiffReport/LogCommand.swift | 80 +++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 scripts/APIDiffReport/Sources/APIDiffReport/DiffCommand.swift create mode 100644 scripts/APIDiffReport/Sources/APIDiffReport/DiffReportOptions.swift create mode 100644 scripts/APIDiffReport/Sources/APIDiffReport/LogCommand.swift diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/DiffCommand.swift b/scripts/APIDiffReport/Sources/APIDiffReport/DiffCommand.swift new file mode 100644 index 00000000000..e71795ad006 --- /dev/null +++ b/scripts/APIDiffReport/Sources/APIDiffReport/DiffCommand.swift @@ -0,0 +1,62 @@ + +import Foundation +import SwiftCLI + +class DiffCommand: Command { + var name = "diff" + var shortDescription: String = "Runs a comparison between 2 JSON API logs and prints detected breaking changes." + + @Param var oldProjectPath: String + @Param var newProjectPath: String + + @Flag("-i", "--ignore", description: "Flags if only documented symbols should be checked.") + var ignoreUndocumented: Bool + + @VariadicKey("-a", "--accessibility", description: "Include only entities with specified access level. May be repeated to contain mutilple values. Defaults to `public` and `open`.") + var accessLevels: [DiffReportOptions.Accessibility] + + + func execute() throws { + guard try runApiDiff(oldApiPath: absURL(oldProjectPath), + newApiPath: absURL(newProjectPath)) else { + exit(1) + } + } + + private func runApiDiff(oldApiPath: URL, newApiPath: URL) throws -> Bool { + var options = DiffReportOptions() + options.ignoreUndocumented = ignoreUndocumented + if accessLevels.isEmpty { + options.accessibilityLevels = [DiffReportOptions.Accessibility.public, DiffReportOptions.Accessibility.open] + } else { + options.accessibilityLevels = accessLevels + } + + let diffReport = DiffReport(reportOptions: options) + let oldApi = try readJson(at: oldApiPath) + let newApi = try readJson(at: newApiPath) + let report = try diffReport.generateReport(oldApi: oldApi, newApi: newApi) + + if report.isEmpty { + print("No breaking changes detected!") + return true + } else { + print("\n**** BREAKING CHANGES DETECTED ****") + for (symbol, change) in report { + print("\nBreaking changes in '\(symbol)'") + print(change.map({ $0.toMarkdown() }).joined(separator: "\n\n")) + } + return false + } + } + + private func readJson(at path: URL) throws -> Any { + let data = try Data(contentsOf: path) + + if !data.isEmpty { + return try JSONSerialization.jsonObject(with: data) + } else { + return [] + } + } +} diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/DiffReportOptions.swift b/scripts/APIDiffReport/Sources/APIDiffReport/DiffReportOptions.swift new file mode 100644 index 00000000000..cce8bb638cd --- /dev/null +++ b/scripts/APIDiffReport/Sources/APIDiffReport/DiffReportOptions.swift @@ -0,0 +1,44 @@ + +import Foundation +import SwiftCLI + +struct DiffReportOptions { + enum Accessibility: String, ConvertibleFromString { + case `fileprivate` = "source.lang.swift.accessibility.fileprivate" + case `private` = "source.lang.swift.accessibility.private" + case `internal` = "source.lang.swift.accessibility.internal" + case `open` = "source.lang.swift.accessibility.open" + case `public` = "source.lang.swift.accessibility.public" + + init?(input: String) { + switch input { + case "public": + self = .public + case "open": + self = .open + case "internal": + self = .internal + case "private": + self = .private + case "fileprivate": + self = .fileprivate + default: + self.init(rawValue: input) + } + } + } + + var accessibilityLevels: [Accessibility] = [.open, .public] + var ignoreUndocumented = true + var ignoredKeys = Set(arrayLiteral: "key.doc.line", "key.parsed_scope.end", "key.parsed_scope.start", "key.doc.column", "key.doc.comment", "key.bodyoffset", "key.nameoffset", "key.doc.full_as_xml", "key.offset", "key.fully_annotated_decl", "key.length", "key.bodylength", "key.namelength", "key.annotated_decl", "key.doc.parameters", "key.elements", "key.related_decls", + "key.filepath", "key.attributes", + "key.parsed_declaration", + "key.docoffset", "key.attributes") + + func verifyAccessibility(_ accessibility: String) -> Bool { + if let target = Accessibility(rawValue: accessibility) { + return accessibilityLevels.contains(target) + } + return false + } +} diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/LogCommand.swift b/scripts/APIDiffReport/Sources/APIDiffReport/LogCommand.swift new file mode 100644 index 00000000000..e9c5e7eeafd --- /dev/null +++ b/scripts/APIDiffReport/Sources/APIDiffReport/LogCommand.swift @@ -0,0 +1,80 @@ + +import Foundation +import SwiftCLI + +class LogCommand: Command { + var name = "log" + var shortDescription: String = "Parses provided project and logs it's API structure in JSON format." + + @Param var projectPath: String + @Param var outputPath: String + @CollectedParam var sourcekittenArgs: [String] + + func execute() throws { + guard let log = try runApiLog(apiFolder: projectPath, + args: sourcekittenArgs) else { + print("Decoding 'sourcekitten' output failed.") + exit(1) + } + let outputURL = absURL(outputPath) + try FileManager.default.createDirectory(at: outputURL.deletingLastPathComponent(), + withIntermediateDirectories: true) + try log.write(to: outputURL, + atomically: true, + encoding: .utf8) + } + + private func runApiLog(apiFolder: String, args: [String]) throws -> String? { + print("Running API Logging... ") + guard let APIDoc = runSourcekitten(apiFolder: apiFolder, + args: args) else { + exit(1) + } + + return String(data: APIDoc, encoding: .utf8) + } + + private func runSourcekitten(apiFolder: String, args: [String]) -> Data? { + var result = Data() + let task = Process() + task.launchPath = "/usr/local/bin/sourcekitten" + task.currentDirectoryPath = apiFolder + task.arguments = args + + let standardOutput = Pipe() + let standardError = Pipe() + let outputHandle = standardOutput.fileHandleForReading + let errorHandle = standardError.fileHandleForReading + outputHandle.waitForDataInBackgroundAndNotify() + errorHandle.waitForDataInBackgroundAndNotify() + outputHandle.readabilityHandler = { pipe in + result.append(pipe.availableData) + } + errorHandle.readabilityHandler = { pipe in + guard let currentOutput = String(data: pipe.availableData, encoding: .utf8) else { + print("Error decoding output data: \(pipe.availableData)") + return + } + + guard !currentOutput.isEmpty else { + return + } + DispatchQueue.main.async { + print(currentOutput) + } + } + + task.standardOutput = standardOutput + task.standardError = standardError + task.launch() + task.waitUntilExit() + + if task.terminationStatus == 0 { + print("Sourcekitten succeeded.") + return result + } else { + print("Sourcekitten failed.") + return nil + } + } +} From e6791c3bfa742ed5f4c8ab7c53cb2b8ad3f82a16 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:26:14 +0300 Subject: [PATCH 15/20] Update Xcode project file (rebased). --- .../APIDiffReport.xcodeproj/project.pbxproj | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj index 56ddb6453ce..8aa3154376f 100644 --- a/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 2B237961251E3C3000CCF9C4 /* diffreport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B23795F251E3C3000CCF9C4 /* diffreport.swift */; }; 2B237962251E3C3000CCF9C4 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B237960251E3C3000CCF9C4 /* main.swift */; }; + 2B260FC32563E7F90043C171 /* DiffCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B260FC02563E7F90043C171 /* DiffCommand.swift */; }; + 2B260FC42563E7F90043C171 /* LogCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B260FC12563E7F90043C171 /* LogCommand.swift */; }; + 2B260FC52563E7F90043C171 /* DiffReportOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B260FC22563E7F90043C171 /* DiffReportOptions.swift */; }; + 2B260FCB2563E8960043C171 /* SwiftCLI in Frameworks */ = {isa = PBXBuildFile; productRef = 2B260FCA2563E8960043C171 /* SwiftCLI */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -28,6 +32,9 @@ 2B23795F251E3C3000CCF9C4 /* diffreport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = diffreport.swift; sourceTree = ""; }; 2B237960251E3C3000CCF9C4 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 2B237963251E3C4000CCF9C4 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + 2B260FC02563E7F90043C171 /* DiffCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffCommand.swift; sourceTree = ""; }; + 2B260FC12563E7F90043C171 /* LogCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogCommand.swift; sourceTree = ""; }; + 2B260FC22563E7F90043C171 /* DiffReportOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffReportOptions.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -35,6 +42,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2B260FCB2563E8960043C171 /* SwiftCLI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -69,6 +77,9 @@ 2B23795E251E3C3000CCF9C4 /* APIDiffReport */ = { isa = PBXGroup; children = ( + 2B260FC02563E7F90043C171 /* DiffCommand.swift */, + 2B260FC22563E7F90043C171 /* DiffReportOptions.swift */, + 2B260FC12563E7F90043C171 /* LogCommand.swift */, 2B23795F251E3C3000CCF9C4 /* diffreport.swift */, 2B237960251E3C3000CCF9C4 /* main.swift */, ); @@ -91,6 +102,9 @@ dependencies = ( ); name = APIDiffReport; + packageProductDependencies = ( + 2B260FCA2563E8960043C171 /* SwiftCLI */, + ); productName = APIDiffReport; productReference = 2B20711E251E11A200001493 /* APIDiffReport */; productType = "com.apple.product-type.tool"; @@ -119,6 +133,9 @@ Base, ); mainGroup = 2B207115251E11A200001493; + packageReferences = ( + 2B260FC92563E8960043C171 /* XCRemoteSwiftPackageReference "SwiftCLI" */, + ); productRefGroup = 2B20711F251E11A200001493 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -305,6 +322,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2B260FC92563E8960043C171 /* XCRemoteSwiftPackageReference "SwiftCLI" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jakeheis/SwiftCLI"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.0.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2B260FCA2563E8960043C171 /* SwiftCLI */ = { + isa = XCSwiftPackageProductDependency; + package = 2B260FC92563E8960043C171 /* XCRemoteSwiftPackageReference "SwiftCLI" */; + productName = SwiftCLI; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 2B207116251E11A200001493 /* Project object */; } From a3ada78de50c6f693deab99e7c36a932a5bbca2a Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:27:11 +0300 Subject: [PATCH 16/20] Update diffreport.swift: remove diffreport(). --- .../Sources/APIDiffReport/diffreport.swift | 70 ------------------- 1 file changed, 70 deletions(-) diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift index 6707b6d734d..e2f0b74d7a6 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift @@ -44,76 +44,6 @@ public enum ApiChange { case modification(apiType: String, name: String, modificationType: String, from: String, to: String) } -/** Generates an API diff report from two SourceKitten JSON outputs. */ -public func diffreport(oldApi: JSONObject, newApi: JSONObject) throws -> [String: [ApiChange]] { - let oldApiNameNodeMap = extractAPINodeMap(from: oldApi as! [SourceKittenNode]) - let newApiNameNodeMap = extractAPINodeMap(from: newApi as! [SourceKittenNode]) - - let oldApiNames = Set(oldApiNameNodeMap.keys) - let newApiNames = Set(newApiNameNodeMap.keys) - - let addedApiNames = newApiNames.subtracting(oldApiNames) - let deletedApiNames = oldApiNames.subtracting(newApiNames) - let persistedApiNames = oldApiNames.intersection(newApiNames) - - var changes: [String: [ApiChange]] = [:] - - // Additions - - for usr in (addedApiNames.map { usr in newApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) { - let apiType = prettyString(forKind: usr["key.kind"] as! String) - let name = prettyName(forApi: usr, apis: newApiNameNodeMap) - let root = rootName(forApi: usr, apis: newApiNameNodeMap) - changes[root, withDefault: []].append(.addition(apiType: apiType, name: name)) - } - - // Deletions - - for usr in (deletedApiNames.map { usr in oldApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) { - let apiType = prettyString(forKind: usr["key.kind"] as! String) - let name = prettyName(forApi: usr, apis: oldApiNameNodeMap) - let root = rootName(forApi: usr, apis: oldApiNameNodeMap) - changes[root, withDefault: []].append(.deletion(apiType: apiType, name: name)) - } - - // Modifications - - let ignoredKeys = Set(arrayLiteral: "key.doc.line", "key.parsed_scope.end", "key.parsed_scope.start", "key.doc.column", "key.doc.comment", "key.bodyoffset", "key.nameoffset", "key.doc.full_as_xml", "key.offset", "key.fully_annotated_decl", "key.length", "key.bodylength", "key.namelength", "key.annotated_decl", "key.doc.parameters", "key.elements", "key.related_decls", - "key.filepath", "key.attributes", - "key.parsed_declaration", - "key.docoffset", "key.attributes") - - for usr in persistedApiNames { - let oldApi = oldApiNameNodeMap[usr]! - let newApi = newApiNameNodeMap[usr]! - let root = rootName(forApi: newApi, apis: newApiNameNodeMap) - let allKeys = Set(oldApi.keys).union(Set(newApi.keys)) - - for key in allKeys { - if ignoredKeys.contains(key) { - continue - } - if let oldValue = oldApi[key] as? String, let newValue = newApi[key] as? String, oldValue != newValue { - let apiType = prettyString(forKind: newApi["key.kind"] as! String) - let name = prettyName(forApi: newApi, apis: newApiNameNodeMap) - let modificationType = prettyString(forModificationKind: key) - if apiType == "class" && key == "key.parsed_declaration" { - // Ignore declarations for classes because it's a complete representation of the class's - // code, which is not helpful diff information. - continue - } - changes[root, withDefault: []].append(.modification(apiType: apiType, - name: name, - modificationType: modificationType, - from: oldValue, - to: newValue)) - } - } - } - - return changes -} - extension ApiChange { public func toMarkdown() -> String { switch self { From f5d3e38342b6248e24fb3adbbc35a7fe7e0ded10 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:28:14 +0300 Subject: [PATCH 17/20] Update diffreport.swift: add updated diffreport func. --- .../Sources/APIDiffReport/diffreport.swift | 183 +++++++++++++----- 1 file changed, 130 insertions(+), 53 deletions(-) diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift index e2f0b74d7a6..a1ebc2ff87d 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift @@ -99,64 +99,141 @@ extension Dictionary { } } -/** - Sorting function for APINode instances. - - Sorts by filename. - - Example usage: sorted(by: apiNodeIsOrderedBefore) - */ -func apiNodeIsOrderedBefore(prev: APINode, next: APINode) -> Bool { - if let prevFile = prev["key.doc.file"] as? String, let nextFile = next["key.doc.file"] as? String { - return prevFile < nextFile +struct DiffReport { + + var reportOptions: DiffReportOptions + + /** Generates an API diff report from two SourceKitten JSON outputs. */ + public func generateReport(oldApi: JSONObject, newApi: JSONObject) throws -> [String: [ApiChange]] { + let oldApiNameNodeMap = extractAPINodeMap(from: oldApi as! [SourceKittenNode]) + let newApiNameNodeMap = extractAPINodeMap(from: newApi as! [SourceKittenNode]) + + let oldApiNames = Set(oldApiNameNodeMap.keys) + let newApiNames = Set(newApiNameNodeMap.keys) + + let addedApiNames = newApiNames.subtracting(oldApiNames) + let deletedApiNames = oldApiNames.subtracting(newApiNames) + let persistedApiNames = oldApiNames.intersection(newApiNames) + + var changes: [String: [ApiChange]] = [:] + + // Additions + + for usr in (addedApiNames.map { usr in newApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) { + guard verifyDocumentationCheck(apiNode: usr) else { + continue + } + let apiType = prettyString(forKind: usr["key.kind"] as! String) + let name = prettyName(forApi: usr, apis: newApiNameNodeMap) + let root = rootName(forApi: usr, apis: newApiNameNodeMap) + changes[root, withDefault: []].append(.addition(apiType: apiType, name: name)) + } + + // Deletions + + for usr in (deletedApiNames.map { usr in oldApiNameNodeMap[usr]! }.sorted(by: apiNodeIsOrderedBefore)) { + guard verifyDocumentationCheck(apiNode: usr) else { + continue + } + let apiType = prettyString(forKind: usr["key.kind"] as! String) + let name = prettyName(forApi: usr, apis: oldApiNameNodeMap) + let root = rootName(forApi: usr, apis: oldApiNameNodeMap) + changes[root, withDefault: []].append(.deletion(apiType: apiType, name: name)) + } + + // Modifications + + for usr in persistedApiNames { + let oldApi = oldApiNameNodeMap[usr]! + let newApi = newApiNameNodeMap[usr]! + let root = rootName(forApi: newApi, apis: newApiNameNodeMap) + let allKeys = Set(oldApi.keys).union(Set(newApi.keys)) + + guard verifyDocumentationCheck(apiNode: oldApi) || + verifyDocumentationCheck(apiNode: newApi) else { + continue + } + + for key in allKeys { + guard !reportOptions.ignoredKeys.contains(key) else { + continue + } + if let oldValue = oldApi[key] as? String, let newValue = newApi[key] as? String, oldValue != newValue { + let apiType = prettyString(forKind: newApi["key.kind"] as! String) + let name = prettyName(forApi: newApi, apis: newApiNameNodeMap) + let modificationType = prettyString(forModificationKind: key) + if apiType == "class" && key == "key.parsed_declaration" { + // Ignore declarations for classes because it's a complete representation of the class's + // code, which is not helpful diff information. + continue + } + changes[root, withDefault: []].append(.modification(apiType: apiType, + name: name, + modificationType: modificationType, + from: oldValue, + to: newValue)) + } + } + } + + return changes } - return false -} - -/** Union two dictionaries, preferring existing values if they possess a parent.usr key. */ -func += (left: inout ApiNameNodeMap, right: ApiNameNodeMap) { - for (k, v) in right { - if left[k] == nil { - left.updateValue(v, forKey: k) - } else if let object = left[k], object["parent.usr"] == nil { - left.updateValue(v, forKey: k) + + private func verifyDocumentationCheck(apiNode: APINode) -> Bool { + if reportOptions.ignoreUndocumented { + let nodocValue = ":nodoc:" + let comment = apiNode["key.doc.comment"] as? String ?? nodocValue + return !comment.starts(with: nodocValue) } + return true } -} - -func prettyString(forKind kind: String) -> String { - if let pretty = [ - // Objective-C - "sourcekitten.source.lang.objc.decl.protocol": "protocol", - "sourcekitten.source.lang.objc.decl.typedef": "typedef", - "sourcekitten.source.lang.objc.decl.method.instance": "method", - "sourcekitten.source.lang.objc.decl.property": "property", - "sourcekitten.source.lang.objc.decl.class": "class", - "sourcekitten.source.lang.objc.decl.constant": "constant", - "sourcekitten.source.lang.objc.decl.enum": "enum", - "sourcekitten.source.lang.objc.decl.enumcase": "enum value", - "sourcekitten.source.lang.objc.decl.category": "category", - "sourcekitten.source.lang.objc.decl.method.class": "class method", - "sourcekitten.source.lang.objc.decl.struct": "struct", - "sourcekitten.source.lang.objc.decl.field": "field", - - // Swift - "source.lang.swift.decl.function.method.static": "static method", - "source.lang.swift.decl.function.method.instance": "method", - "source.lang.swift.decl.var.instance": "var", - "source.lang.swift.decl.class": "class", - "source.lang.swift.decl.var.static": "static var", - "source.lang.swift.decl.enum": "enum", - "source.lang.swift.decl.function.free": "function", - "source.lang.swift.decl.var.global": "global var", - "source.lang.swift.decl.protocol": "protocol", - "source.lang.swift.decl.enumelement": "enum value" + + /** + Sorting function for APINode instances. + + Sorts by filename. + + Example usage: sorted(by: apiNodeIsOrderedBefore) + */ + func apiNodeIsOrderedBefore(prev: APINode, next: APINode) -> Bool { + if let prevFile = prev["key.doc.file"] as? String, let nextFile = next["key.doc.file"] as? String { + return prevFile < nextFile + } + return false + } + + func prettyString(forKind kind: String) -> String { + if let pretty = [ + // Objective-C + "sourcekitten.source.lang.objc.decl.protocol": "protocol", + "sourcekitten.source.lang.objc.decl.typedef": "typedef", + "sourcekitten.source.lang.objc.decl.method.instance": "method", + "sourcekitten.source.lang.objc.decl.property": "property", + "sourcekitten.source.lang.objc.decl.class": "class", + "sourcekitten.source.lang.objc.decl.constant": "constant", + "sourcekitten.source.lang.objc.decl.enum": "enum", + "sourcekitten.source.lang.objc.decl.enumcase": "enum value", + "sourcekitten.source.lang.objc.decl.category": "category", + "sourcekitten.source.lang.objc.decl.method.class": "class method", + "sourcekitten.source.lang.objc.decl.struct": "struct", + "sourcekitten.source.lang.objc.decl.field": "field", + + // Swift + "source.lang.swift.decl.function.method.static": "static method", + "source.lang.swift.decl.function.method.instance": "method", + "source.lang.swift.decl.var.instance": "var", + "source.lang.swift.decl.class": "class", + "source.lang.swift.decl.var.static": "static var", + "source.lang.swift.decl.enum": "enum", + "source.lang.swift.decl.function.free": "function", + "source.lang.swift.decl.var.global": "global var", + "source.lang.swift.decl.protocol": "protocol", + "source.lang.swift.decl.enumelement": "enum value" ][kind] { - return pretty + return pretty + } + return kind } - return kind -} - func prettyString(forModificationKind kind: String) -> String { switch kind { case "key.swift_declaration": return "Swift declaration" From 37e6c128d717112cbcfe20f5d182e4e75b2af9ef Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:28:26 +0300 Subject: [PATCH 18/20] Update formatting. --- .../Sources/APIDiffReport/diffreport.swift | 222 +++++++++--------- 1 file changed, 112 insertions(+), 110 deletions(-) diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift index a1ebc2ff87d..012172787da 100644 --- a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift +++ b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift @@ -234,127 +234,129 @@ struct DiffReport { } return kind } -func prettyString(forModificationKind kind: String) -> String { - switch kind { - case "key.swift_declaration": return "Swift declaration" - case "key.parsed_declaration": return "Declaration" - case "key.doc.declaration": return "Declaration" - case "key.typename": return "Declaration" - case "key.always_deprecated": return "Deprecation" - case "key.deprecation_message": return "Deprecation message" - default: return kind + + func prettyString(forModificationKind kind: String) -> String { + switch kind { + case "key.swift_declaration": return "Swift declaration" + case "key.parsed_declaration": return "Declaration" + case "key.doc.declaration": return "Declaration" + case "key.typename": return "Declaration" + case "key.always_deprecated": return "Deprecation" + case "key.deprecation_message": return "Deprecation message" + default: return kind + } } -} - -/** Walk the APINode to the root node. */ -func rootName(forApi api: APINode, apis: ApiNameNodeMap) -> String { - let name = api["key.name"] as! String - if let parentUsr = api["parent.usr"] as? String, let parentApi = apis[parentUsr] { - return rootName(forApi: parentApi, apis: apis) + + /** Walk the APINode to the root node. */ + func rootName(forApi api: APINode, apis: ApiNameNodeMap) -> String { + let name = api["key.name"] as! String + if let parentUsr = api["parent.usr"] as? String, let parentApi = apis[parentUsr] { + return rootName(forApi: parentApi, apis: apis) + } + return name } - return name -} - -func prettyName(forApi api: APINode, apis: ApiNameNodeMap) -> String { - let name = api["key.name"] as! String - if let parentUsr = api["parent.usr"] as? String, let parentApi = apis[parentUsr] { - return "`\(name)` in \(prettyName(forApi: parentApi, apis: apis))" + + func prettyName(forApi api: APINode, apis: ApiNameNodeMap) -> String { + let name = api["key.name"] as! String + if let parentUsr = api["parent.usr"] as? String, let parentApi = apis[parentUsr] { + return "`\(name)` in \(prettyName(forApi: parentApi, apis: apis))" + } + return "`\(name)`" } - return "`\(name)`" -} - -/** Normalize data contained in an API node json dictionary. */ -func apiNode(from sourceKittenNode: SourceKittenNode) -> APINode { - var data = sourceKittenNode - data.removeValue(forKey: "key.substructure") - for (key, value) in data { - data[key] = String(describing: value) + + /** Normalize data contained in an API node json dictionary. */ + func apiNode(from sourceKittenNode: SourceKittenNode) -> APINode { + var data = sourceKittenNode + data.removeValue(forKey: "key.substructure") + for (key, value) in data { + data[key] = String(describing: value) + } + return data } - return data -} - -/** - Recursively iterate over each sourcekitten node and extract a flattened map of USR identifier to - APINode instance. - */ -func extractAPINodeMap(from sourceKittenNodes: [SourceKittenNode]) -> ApiNameNodeMap { - var map: ApiNameNodeMap = [:] - for file in sourceKittenNodes { - for (_, information) in file { - let substructure = (information as! SourceKittenNode)["key.substructure"] as! [SourceKittenNode] - for jsonNode in substructure { - map += extractAPINodeMap(from: jsonNode) + + /** + Recursively iterate over each sourcekitten node and extract a flattened map of USR identifier to + APINode instance. + */ + func extractAPINodeMap(from sourceKittenNodes: [SourceKittenNode]) -> ApiNameNodeMap { + var map: ApiNameNodeMap = [:] + for file in sourceKittenNodes { + for (_, information) in file { + let substructure = (information as! SourceKittenNode)["key.substructure"] as! [SourceKittenNode] + for jsonNode in substructure { + map += extractAPINodeMap(from: jsonNode) + } } } + return map } - return map -} - -/** - Recursively iterate over a sourcekitten node and extract a flattened map of USR identifier to - APINode instance. - */ -func extractAPINodeMap(from sourceKittenNode: SourceKittenNode, parentUsr: String? = nil) -> ApiNameNodeMap { - var map: ApiNameNodeMap = [:] - for (key, value) in sourceKittenNode { - switch key { - case "key.usr": - if let accessibility = sourceKittenNode["key.accessibility"] { - if accessibility as! String != "source.lang.swift.accessibility.public" && - accessibility as! String != "source.lang.swift.accessibility.open" { + + /** + Recursively iterate over a sourcekitten node and extract a flattened map of USR identifier to + APINode instance. + */ + func extractAPINodeMap(from sourceKittenNode: SourceKittenNode, parentUsr: String? = nil) -> ApiNameNodeMap { + var map: ApiNameNodeMap = [:] + for (key, value) in sourceKittenNode { + switch key { + case "key.usr": + if let accessibility = sourceKittenNode["key.accessibility"] { + if !reportOptions.verifyAccessibility(accessibility as! String) { + continue + } + } else if let kind = sourceKittenNode["key.kind"] as? String, kind == "source.lang.swift.decl.extension" { continue } - } else if let kind = sourceKittenNode["key.kind"] as? String, kind == "source.lang.swift.decl.extension" { + var node = apiNode(from: sourceKittenNode) + + // Create a reference to the parent node + node["parent.usr"] = parentUsr + + // Store the API node in the map + map[value as! String] = node + + case "key.substructure": + let substructure = value as! [SourceKittenNode] + for subSourceKittenNode in substructure { + map += extractAPINodeMap(from: subSourceKittenNode, parentUsr: sourceKittenNode["key.usr"] as? String) + } + default: continue } - var node = apiNode(from: sourceKittenNode) - - // Create a reference to the parent node - node["parent.usr"] = parentUsr - - // Store the API node in the map - map[value as! String] = node - - case "key.substructure": - let substructure = value as! [SourceKittenNode] - for subSourceKittenNode in substructure { - map += extractAPINodeMap(from: subSourceKittenNode, parentUsr: sourceKittenNode["key.usr"] as? String) - } - default: - continue } + return map } - return map -} - -/** - Execute sourcekitten with a given umbrella header. - - Only meant to be used in unit test builds. - - @param header Absolute path to an umbrella header. - */ -func runSourceKitten(withHeader header: String) throws -> JSONObject { - let task = Process() - task.launchPath = "/usr/bin/env" - task.arguments = [ - "/usr/local/bin/sourcekitten", - "doc", - "--objc", - header, - "--", - "-x", - "objective-c", - ] - let standardOutput = Pipe() - task.standardOutput = standardOutput - task.launch() - task.waitUntilExit() - var data = standardOutput.fileHandleForReading.readDataToEndOfFile() - let tmpDir = ProcessInfo.processInfo.environment["TMPDIR"]!.replacingOccurrences(of: "/", with: "\\/") - let string = String(data: data, encoding: String.Encoding.utf8)! - .replacingOccurrences(of: tmpDir + "old\\/", with: "") - .replacingOccurrences(of: tmpDir + "new\\/", with: "") - data = string.data(using: String.Encoding.utf8)! - return try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)) + + /** + Execute sourcekitten with a given umbrella header. + + Only meant to be used in unit test builds. + + @param header Absolute path to an umbrella header. + */ + func runSourceKitten(withHeader header: String) throws -> JSONObject { + let task = Process() + task.launchPath = "/usr/bin/env" + task.arguments = [ + "/usr/local/bin/sourcekitten", + "doc", + "--objc", + header, + "--", + "-x", + "objective-c", + ] + let standardOutput = Pipe() + task.standardOutput = standardOutput + task.launch() + task.waitUntilExit() + var data = standardOutput.fileHandleForReading.readDataToEndOfFile() + let tmpDir = ProcessInfo.processInfo.environment["TMPDIR"]!.replacingOccurrences(of: "/", with: "\\/") + let string = String(data: data, encoding: String.Encoding.utf8)! + .replacingOccurrences(of: tmpDir + "old\\/", with: "") + .replacingOccurrences(of: tmpDir + "new\\/", with: "") + data = string.data(using: String.Encoding.utf8)! + return try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)) + } + } From 9349df0bf96a0137e7589204e4b88e7f2c558550 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:32:08 +0300 Subject: [PATCH 19/20] Use cached API difference from the previous CI run. --- .circleci/config.yml | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a079b5359c6..f877cb679e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,17 @@ commands: - save_cache: key: nav-api-diff-cache-v5-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ << parameters.api_diff_cache >> }} paths: - - << pipeline.parameters.api_log_file >> + - api_logs + + save-api-diff-cache-by-tag: + parameters: + api_diff_cache: + type: string + steps: + - save_cache: + key: nav-api-diff-cache-v5-{{ .Environment.CIRCLE_WORKFLOW_ID }}-<< parameters.api_diff_cache >> + paths: + - api_logs restore-api-diff-cache: parameters: @@ -18,14 +28,18 @@ commands: steps: - restore_cache: keys: - - nav-api-diff-cache-v5-{{ .Environment.CIRCLE_JOB }}-{{ << parameters.api_diff_cache >> }} + - nav-api-diff-cache-v5-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ << parameters.api_diff_cache >> }} - run_api_log: + restore-api-diff-cache-by-tag: parameters: - iOS: + api_diff_cache: type: string - device: - type: string + steps: + - restore_cache: + keys: + - nav-api-diff-cache-v5-{{ .Environment.CIRCLE_WORKFLOW_ID }}-<< parameters.api_diff_cache >> + + build-api-diff-report: steps: - run: name: Install Sourcekitten From 66b49a8f8c542bd8ab08e36ab23ba87b530d1df8 Mon Sep 17 00:00:00 2001 From: Dersim Davaod Date: Wed, 28 Apr 2021 17:32:28 +0300 Subject: [PATCH 20/20] Run API diff report check. --- .circleci/config.yml | 70 +++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f877cb679e0..fa51ada54be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,12 +47,20 @@ commands: - run: name: Building API Diff Report command: cd scripts/APIDiffReport && swift build + + run_api_log: + parameters: + iOS: + type: string + device: + type: string + steps: - run: name: Generating MapboxCoreNavigation API Log - command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. << pipeline.parameters.api_log_file >>/core_navigation_log.json doc --module-name MapboxCoreNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=<< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxCoreNavigation clean build + command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. $CIRCLE_WORKING_DIRECTORY/api_logs/core_navigation_log.json doc --module-name MapboxCoreNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=<< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxCoreNavigation clean build - run: name: Generating MapboxNavigation API Log - command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. << pipeline.parameters.api_log_file >>/navigation_log.json doc --module-name MapboxNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=<< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxNavigation clean build + command: cd scripts/APIDiffReport && swift run APIDiffReport log ../.. $CIRCLE_WORKING_DIRECTORY/api_logs/navigation_log.json doc --module-name MapboxNavigation -- -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=<< parameters.iOS >>,name=<< parameters.device >>' -project MapboxNavigation.xcodeproj -scheme MapboxNavigation clean build step-library: - &restore-cache @@ -262,11 +270,12 @@ jobs: - when: condition: << parameters.generate_api_log >> steps: + - build-api-diff-report - run_api_log: iOS: << parameters.iOS >> device: << parameters.device >> - save-api-diff-cache: - api_diff_cache: $CIRCLE_SHA1 + api_diff_cache: .Environment.CIRCLE_SHA1 xcode-11-examples: parameters: @@ -319,52 +328,47 @@ jobs: HOMEBREW_NO_AUTO_UPDATE: 1 steps: - checkout + - build-api-diff-report + - run: + name: Store latest APIDiffReport + command: cp -R scripts/APIDiffReport ~/. - run: name: Checking out Base API command: git checkout << parameters.base_api_tag >> - restore-api-diff-cache: - api_diff_cache: $CIRCLE_SHA1 + api_diff_cache: .Environment.CIRCLE_SHA1 - run: name: Move Api Diff - command: mv << pipeline.parameters.api_log_file >> original_api - - restore-api-diff-cache: + command: | + mv api_logs original_api + - restore-api-diff-cache-by-tag: + api_diff_cache: << parameters.base_api_tag >> + - *prepare-mapbox-file + - *prepare-netrc-file + - *update-carthage-version + - *restore-cache + - *install-dependencies + - *install-dependencies-12 + - run: + name: Install prerequisites + command: if [ $(xcversion simulators | grep -cF "iOS << parameters.iOS >> Simulator (installed)") -eq 0 ]; then xcversion simulators --install="iOS << parameters.iOS >>" || true; fi + - *save-cache + - save-api-diff-cache-by-tag: api_diff_cache: << parameters.base_api_tag >> - run: - name: Verify Base API report exists + name: Restore latest APIDiffReport command: | - if test -f "<< pipeline.parameters.api_log_file >>"; then \ - echo "Base API log file exists" \ - export API_CACHE_EXISTS=TRUE \ - fi - - unless: - condition: .Environment.API_CACHE_EXISTS - steps: - - *prepare-mapbox-file - - *prepare-netrc-file - - *update-carthage-version - - *restore-cache - - *install-dependencies - - *install-dependencies-12 - - run: - name: Install prerequisites - command: if [ $(xcversion simulators | grep -cF "iOS << parameters.iOS >> Simulator (installed)") -eq 0 ]; then xcversion simulators --install="iOS << parameters.iOS >>" || true; fi - - *save-cache - - save-api-diff-cache: - api_diff_cache: << parameters.base_api_tag >> + rm -rf scripts/APIDiffReport + cp -R ~/APIDiffReport scripts/. - run_api_log: iOS: << parameters.iOS >> device: << parameters.device >> - run: name: Generating MapboxCoreNavigation API Diff - command: cd scripts/APIDiffReport && swift run APIDiffReport diff original_api/core_navigation_log.json << pipeline.parameters.api_log_file>>/core_navigation_log.json + command: cd scripts/APIDiffReport && swift run APIDiffReport diff $CIRCLE_WORKING_DIRECTORY/original_api/core_navigation_log.json -i $CIRCLE_WORKING_DIRECTORY/api_logs/core_navigation_log.json - run: name: Generating MapboxNavigation API Diff - command: cd scripts/APIDiffReport && swift run APIDiffReport diff original_api/navigation_log.json << pipeline.parameters.api_log_file>>/navigation_log.json - -parameters: - api_log_file: - type: string - default: $CIRCLE_WORKING_DIRECTORY/api_logs + command: cd scripts/APIDiffReport && swift run APIDiffReport diff $CIRCLE_WORKING_DIRECTORY/original_api/navigation_log.json -i $CIRCLE_WORKING_DIRECTORY/api_logs/navigation_log.json workflows: workflow: