diff --git a/.circleci/config.yml b/.circleci/config.yml index b8a3b77d916..fa51ada54be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,67 @@ 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_WORKFLOW_ID }}-{{ << parameters.api_diff_cache >> }} + paths: + - 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: + api_diff_cache: + type: string + steps: + - restore_cache: + keys: + - nav-api-diff-cache-v5-{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ << parameters.api_diff_cache >> }} + + restore-api-diff-cache-by-tag: + parameters: + api_diff_cache: + 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 + command: brew update && brew install sourcekitten + - 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 ../.. $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 ../.. $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 restore_cache: @@ -164,6 +226,9 @@ jobs: delete_private_deps: type: boolean default: false + generate_api_log: + type: boolean + default: false macos: xcode: << parameters.xcode >> environment: @@ -202,6 +267,15 @@ jobs: condition: << parameters.codecoverage >> steps: - run: bash <(curl -s https://codecov.io/bash) + - 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: .Environment.CIRCLE_SHA1 xcode-11-examples: parameters: @@ -234,6 +308,68 @@ 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" + macos: + xcode: << parameters.xcode >> + environment: + 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: .Environment.CIRCLE_SHA1 + - run: + name: Move Api Diff + 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: Restore latest APIDiffReport + command: | + 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 $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 $CIRCLE_WORKING_DIRECTORY/original_api/navigation_log.json -i $CIRCLE_WORKING_DIRECTORY/api_logs/navigation_log.json + workflows: workflow: jobs: @@ -243,6 +379,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" @@ -286,3 +423,9 @@ workflows: filters: branches: only: main + - api-diff-job-approval: + type: approval + - api-diff-job: + requires: + - "Xcode_12.0_iOS_14.0" + - api-diff-job-approval 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/APIDiffReport.xcodeproj/project.pbxproj b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..8aa3154376f --- /dev/null +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/project.pbxproj @@ -0,0 +1,346 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + 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 */; }; + 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 */ + 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 = ""; }; + 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 */ + 2B20711B251E11A200001493 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B260FCB2563E8960043C171 /* SwiftCLI in Frameworks */, + ); + 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 = ( + 2B260FC02563E7F90043C171 /* DiffCommand.swift */, + 2B260FC22563E7F90043C171 /* DiffReportOptions.swift */, + 2B260FC12563E7F90043C171 /* LogCommand.swift */, + 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; + packageProductDependencies = ( + 2B260FCA2563E8960043C171 /* SwiftCLI */, + ); + 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; + packageReferences = ( + 2B260FC92563E8960043C171 /* XCRemoteSwiftPackageReference "SwiftCLI" */, + ); + productRefGroup = 2B20711F251E11A200001493 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2B20711D251E11A200001493 /* APIDiffReport */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 2B20711A251E11A200001493 /* Sources */ = { + 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 */, + 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 */ + +/* 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 */; +} 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..3d4073261e3 --- /dev/null +++ b/scripts/APIDiffReport/APIDiffReport.xcodeproj/xcshareddata/xcschemes/APIDiffReport.xcscheme @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/APIDiffReport/Package.swift b/scripts/APIDiffReport/Package.swift new file mode 100644 index 00000000000..c194c8a2ca8 --- /dev/null +++ b/scripts/APIDiffReport/Package.swift @@ -0,0 +1,28 @@ +// 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"), + .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: ["SwiftCLI"] + ), + ] +) 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 + } + } +} diff --git a/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift new file mode 100644 index 00000000000..012172787da --- /dev/null +++ b/scripts/APIDiffReport/Sources/APIDiffReport/diffreport.swift @@ -0,0 +1,362 @@ +/* + 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. + + 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 + +public typealias JSONObject = Any + +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) + case deletion(apiType: String, name: String) + case modification(apiType: String, name: String, modificationType: String, from: String, to: String) +} + +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 + } + } +} + +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 + } + + 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 + } + + /** + 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 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 !reportOptions.verifyAccessibility(accessibility as! String) { + 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..295c6a3b301 --- /dev/null +++ b/scripts/APIDiffReport/Sources/APIDiffReport/main.swift @@ -0,0 +1,10 @@ +import Foundation +import SwiftCLI + +func absURL ( _ path: String ) -> URL { + return URL(fileURLWithPath: (path as NSString).expandingTildeInPath) +} + +CLI(name: "APIDiffReport", + description: "A tool to detect Public API breaking changes", + commands: [LogCommand(), DiffCommand()]).goAndExit()