diff --git a/.github/workflows/api_report.yml b/.github/workflows/api_report.yml index c981d2ee3db..bd30b61d9b0 100644 --- a/.github/workflows/api_report.yml +++ b/.github/workflows/api_report.yml @@ -12,14 +12,41 @@ jobs: - name: Checkout PR branch uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - id: get_changed_files + run: echo "file_list=$(git diff --name-only -r HEAD^1 HEAD | tr '\n' ' ')" >> $GITHUB_OUTPUT + + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: 3.7 - - name: Make diff directory - run: mkdir ~/diff + - name: Copy script + run: | + cp scripts/api_diff_report/api_info.py ~/api_info.py + cp scripts/api_diff_report/api_diff.py ~/api_diff.py - - name: List changed files - run: git diff --name-only ${{ github.base_ref }}) + - name: Generate API files for PR branch + run: python ~/api_info.py -f ${{ steps.get_changed_files.outputs.file_list }} -o "~/merge_branch" - name: Checkout master uses: actions/checkout@v3 with: ref: ${{ github.base_ref }} + + - name: Generate API files for Base branch + run: python ~/api_info.py -f ${{ steps.get_changed_files.outputs.file_list }} -o "~/base_branch" + + - name: Diff + run: python ~/api_diff.py -m "~/merge_branch" -b "~/base_branch" + + - uses: actions/upload-artifact@v3 + if: ${{ !cancelled() }} + with: + name: api_info + path: | + ~/merge_branch + ~/base_branch + retention-days: 1 diff --git a/FirebaseAppDistribution/Sources/Public/FirebaseAppDistribution/FIRAppDistribution.h b/FirebaseAppDistribution/Sources/Public/FirebaseAppDistribution/FIRAppDistribution.h index 3b491729805..ff0553414f6 100644 --- a/FirebaseAppDistribution/Sources/Public/FirebaseAppDistribution/FIRAppDistribution.h +++ b/FirebaseAppDistribution/Sources/Public/FirebaseAppDistribution/FIRAppDistribution.h @@ -41,14 +41,13 @@ NS_SWIFT_NAME(AppDistribution) /** * Sign-in the App Distribution tester */ -- (void)signInTesterWithCompletion:(void (^)(NSError *_Nullable error))completion +- (void)signInTesterWithCompletion:(void (^)(NSError * error))completion NS_SWIFT_NAME(signInTester(completion:)); /** * Check to see whether a new distribution is available */ -- (void)checkForUpdateWithCompletion:(void (^)(FIRAppDistributionRelease *_Nullable_result release, - NSError *_Nullable error))completion +- (void)checkForUpdateWithCompletion:(void (^)(FIRAppDistributionRelease *_Nullable_result release))completion NS_SWIFT_NAME(checkForUpdate(completion:)); /** diff --git a/FirebaseCore/Sources/Public/FirebaseCore/FIRApp.h b/FirebaseCore/Sources/Public/FirebaseCore/FIRApp.h index 58ef2a62594..3122d35938e 100644 --- a/FirebaseCore/Sources/Public/FirebaseCore/FIRApp.h +++ b/FirebaseCore/Sources/Public/FirebaseCore/FIRApp.h @@ -24,6 +24,9 @@ NS_ASSUME_NONNULL_BEGIN typedef void (^FIRAppVoidBoolCallback)(BOOL success) NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead."); +typedef void (^FIRAppVoidBoolCallbackTest)(BOOL success) + NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead."); + /** * The entry point of Firebase SDKs. * @@ -61,7 +64,7 @@ NS_SWIFT_NAME(FirebaseApp) * * @param options The Firebase application options used to configure the service. */ -+ (void)configureWithOptions:(FIROptions *)options NS_SWIFT_NAME(configure(options:)); ++ (void)configureWithOptions:(FIROptions *__nullable)options NS_SWIFT_NAME(configure(options:)); /** * Configures a Firebase app with the given name and options. Raises an exception if any diff --git a/FirebaseCore/Sources/Public/FirebaseCore/FIRLoggerLevel.h b/FirebaseCore/Sources/Public/FirebaseCore/FIRLoggerLevel.h index dca3aa0b01f..5582f3fd487 100644 --- a/FirebaseCore/Sources/Public/FirebaseCore/FIRLoggerLevel.h +++ b/FirebaseCore/Sources/Public/FirebaseCore/FIRLoggerLevel.h @@ -21,6 +21,7 @@ * The log levels used by internal logging. */ typedef NS_ENUM(NSInteger, FIRLoggerLevel) { + FIRLoggerLevelTest = 999, /** Error level, matches ASL_LEVEL_ERR. */ FIRLoggerLevelError = 3, /** Warning level, matches ASL_LEVEL_WARNING. */ diff --git a/FirebaseCore/Sources/Public/FirebaseCore/FIRVersion.h b/FirebaseCore/Sources/Public/FirebaseCore/FIRVersion.h index 651edaf5c8b..217f8b8aeb0 100644 --- a/FirebaseCore/Sources/Public/FirebaseCore/FIRVersion.h +++ b/FirebaseCore/Sources/Public/FirebaseCore/FIRVersion.h @@ -20,6 +20,6 @@ NS_ASSUME_NONNULL_BEGIN /** Returns the current version of Firebase. */ NS_SWIFT_NAME(FirebaseVersion()) -NSString* FIRFirebaseVersion(void); +NSString* FIRFirebaseVersion(NSString *); NS_ASSUME_NONNULL_END diff --git a/FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift b/FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift index 05256933f29..b3be129303d 100644 --- a/FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift +++ b/FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift @@ -15,7 +15,7 @@ import Foundation /// Task to download model file to device. -class ModelDownloadTask { +public final class ModelDownloadTask { /// Name of the app associated with this instance of ModelDownloadTask. private let appName: String diff --git a/FirebaseMLModelDownloader/Sources/ModelDownloader.swift b/FirebaseMLModelDownloader/Sources/ModelDownloader.swift index 6094ae02b9e..55702a5abef 100644 --- a/FirebaseMLModelDownloader/Sources/ModelDownloader.swift +++ b/FirebaseMLModelDownloader/Sources/ModelDownloader.swift @@ -96,13 +96,21 @@ public class ModelDownloader { } /// Model downloader with default app. - public static func modelDownloader() -> ModelDownloader { + public static func modelDownloader() -> ModelDownloader? { guard let defaultApp = FirebaseApp.app() else { fatalError(ModelDownloader.ErrorDescription.defaultAppNotConfigured) } return modelDownloader(app: defaultApp) } + /// Model downloader with default app. + public static func modelDownloaderNullable() -> ModelDownloader? { + guard let defaultApp = FirebaseApp.app() else { + fatalError(ModelDownloader.ErrorDescription.defaultAppNotConfigured) + } + return nil + } + /// Model Downloader with custom app. public static func modelDownloader(app: FirebaseApp) -> ModelDownloader { if let downloader = modelDownloaderDictionary[app.name] { diff --git a/scripts/api_diff_report/api_diff.py b/scripts/api_diff_report/api_diff.py new file mode 100644 index 00000000000..f479a4d7933 --- /dev/null +++ b/scripts/api_diff_report/api_diff.py @@ -0,0 +1,167 @@ +# Copyright 2023 Google LLC +# +# 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 json +import argparse +import logging +import os +import subprocess + +OBJC_EXTENSION = "h" +SWIFT_EXTENSION = "swift" + +KEY_KIND = { + "source.lang.swift.decl.function.method.instance": "", + "source.lang.swift.decl.function.method.static": "static" +} + +def main(): + logging.getLogger().setLevel(logging.INFO) + + args = parse_cmdline_args() + logging.info(args) + + merged_branch = os.path.expanduser(args.merged_branch) + base_branch = os.path.expanduser(args.base_branch) + + for file_name in os.listdir(merged_branch): + logging.info(f"\n\nDetect API changes in {file_name}") + merged_file = os.path.join(merged_branch, file_name) + base_file = os.path.join(base_branch, file_name) + api_diff(merged_file, base_file) + text_diff(merged_file, base_file) + + +def text_diff(merged_file, base_file): + result = subprocess.Popen(f"git diff --no-index --word-diff {merged_file} {base_file}", + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE) + logging.info("------------") + api_info = result.stdout.read() + logging.info(api_info) + + +def api_diff(merged_file, base_file): + file_extension = merged_file.split(".")[1] + if file_extension == OBJC_EXTENSION: + pr_apis = get_objc_public_apis(json.load(open(merged_file))) + base_apis = get_objc_public_apis(json.load(open(base_file))) + + pr_only_apis = get_objc_diff(pr_apis, base_apis) + base_only_apis = get_objc_diff(base_apis, pr_apis) + logging.info("Added APIs") + print_objc_diff(pr_only_apis) + logging.info("\nRemoved APIS") + print_objc_diff(base_only_apis) + elif file_extension == SWIFT_EXTENSION: + pr_apis = get_swift_public_apis(json.load(open(merged_file))) + base_apis = get_swift_public_apis(json.load(open(base_file))) + + pr_only_apis = get_swift_diff(pr_apis, base_apis) + base_only_apis = get_swift_diff(base_apis, pr_apis) + logging.info("Added APIs") + print_swift_diff(pr_only_apis) + logging.info("\nRemoved APIS") + print_swift_diff(base_only_apis) + + +def get_swift_public_apis(api_json): + # key: file name + # value: all classes and functions + # only one key, value pair + for key, value in api_json.items(): + # filter out non-public classes + public_apis = [sc for sc in value["key.substructure"] if "key.accessibility" in sc and sc["key.accessibility"] == "source.lang.swift.accessibility.public"] + for sc in public_apis: + # filter out non-public functions + sc["key.substructure"] = [f for f in sc["key.substructure"] if "key.accessibility" in f and f["key.accessibility"] == "source.lang.swift.accessibility.public"] + return public_apis + + +def get_swift_diff(target, base): + diff = {"class":[], "function":[]} + + for tc in target: + for bc in base: + # check for same public classes + if tc["key.kind"] == bc["key.kind"] and tc["key.name"] == bc["key.name"]: + # check for same public functions + for tf in tc["key.substructure"]: + if "key.typename" in tf: + for bf in bc["key.substructure"]: + if tf["key.kind"] == bf["key.kind"] and tf["key.name"] == bf["key.name"] and tf["key.typename"] == bf["key.typename"]: + break + else: + diff["function"].append(tf) + break + else: + diff["class"].append(tc) + return diff + + +def print_swift_diff(diff): + for c in diff["class"]: + logging.info(f'{c["key.name"]}') + for f in diff["function"]: + logging.info(f'Function: public {KEY_KIND[f["key.kind"]]} func {f["key.name"]} -> {f["key.typename"]}') + + +def get_objc_public_apis(api_json): + for key, value in api_json[0].items(): + return value["key.substructure"] + + +def get_objc_diff(target, base): + diff = {"class":[], "function":[]} + + for tc in target: + for bc in base: + # check for same public classes + if tc["key.kind"] == bc["key.kind"] and tc["key.name"] == bc["key.name"] and tc["key.parsed_declaration"] == bc["key.parsed_declaration"]: + # check for same public functions + if "key.substructure" in tc: + for tf in tc["key.substructure"]: + for bf in bc["key.substructure"]: + if tf["key.kind"] == bf["key.kind"] and tf["key.name"] == bf["key.name"] and tf["key.parsed_declaration"] == bf["key.parsed_declaration"]: + break + else: + diff["function"].append(tf) + break + else: + diff["class"].append(tc) + return diff + + +def print_objc_diff(diff): + for c in diff["class"]: + logging.info(f'Class: {c["key.kind"]} {c["key.name"]}') + for f in diff["function"]: + logging.info(f'OBJC Function: {f["key.parsed_declaration"]}') + for f in diff["function"]: + if "key.swift_declaration" in f: + logging.info(f'SWIFT Function: {f["key.swift_declaration"]}') + + +def parse_cmdline_args(): + parser = argparse.ArgumentParser() + parser.add_argument('-m', '--merged_branch') + parser.add_argument('-b', '--base_branch') + + args = parser.parse_args() + return args + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/scripts/api_diff_report/api_info.py b/scripts/api_diff_report/api_info.py new file mode 100644 index 00000000000..4abebfe77bd --- /dev/null +++ b/scripts/api_diff_report/api_info.py @@ -0,0 +1,141 @@ +# Copyright 2023 Google LLC +# +# 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 json +import argparse +import logging +import os +import subprocess +import re + +PRODUCT_LIST = [ + "ABTesting", + "AppCheck", + "AppDistribution", + "Analytics", + "Authentication", + "Core", + "Crashlytics", + "Database", + "DynamicLinks", + "Firestore", + "Functions", + "InAppMessaging", + "Installations", + "Messaging", + "Performance", + "RemoteConfig", + "Storage" +] + +def main(): + logging.getLogger().setLevel(logging.INFO) + + args = parse_cmdline_args() + logging.info(args) + + output_dir = os.path.expanduser(args.output_dir) + isExist = os.path.exists(output_dir) + if not isExist: + os.makedirs(output_dir) + + swift_to_objc = {} + for file_path in args.file_list: + logging.info(file_path) + if file_path.endswith('.swift'): + result = subprocess.Popen(f"sourcekitten doc --single-file {file_path}", + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE) + logging.info("------------") + api_info = result.stdout.read() + output_path = os.path.join(output_dir, os.path.basename(file_path) + ".json") + logging.info(output_path) + with open(output_path, 'w') as f: + f.write(api_info) + + match = re.search(fr"Firebase(.*?){os.sep}", file_path) + if match: + scheme = f"Firebase{match.groups()[0]}" + if scheme not in swift_to_objc: + swift_to_objc[scheme] = f"{scheme}-Swift.h" + else: + logging.error(f"no target matching file: {file_path}") + elif file_path.endswith('.h') and "Public" in file_path: + result = subprocess.Popen(f"sourcekitten doc --objc {file_path} -- -x objective-c -isysroot $(xcrun --show-sdk-path) -I $(pwd)", + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE) + logging.info("------------") + api_info = result.stdout.read() + output_path = os.path.join(output_dir, os.path.basename(file_path) + ".json") + logging.info(output_path) + with open(output_path, 'w') as f: + f.write(api_info) + + for scheme, objc_header in swift_to_objc.items(): + result = subprocess.Popen("scripts/setup_spm_tests.sh", + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE) + logging.info("------------") + build_info = result.stdout.read() + logging.info(build_info) + derived_data_location = os.path.expanduser(f"~/{args.output_dir}/{scheme}/build/") + subprocess.Popen(f"defaults write com.apple.dt.Xcode IDECustomDerivedDataLocation {derived_data_location}", + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE) + + result = subprocess.Popen(f"xcodebuild -scheme {scheme} -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 11' ONLY_ACTIVE_ARCH=YES CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=YES COMPILER_INDEX_STORE_ENABLE=NO CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ IPHONEOS_DEPLOYMENT_TARGET=13.0 TVOS_DEPLOYMENT_TARGET=13.0", + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE) + logging.info("------------") + build_info = result.stdout.read() + logging.info(build_info) + + # derived_data_location = os.path.expanduser("~/Library/Developer/Xcode/DerivedData/") + for file_dir, _, file_names in os.walk(derived_data_location): + for file_name in file_names: + if file_name == objc_header: + file_path = os.path.join(file_dir, file_name) + logging.info(file_path) + subprocess.Popen(f"cp {file_path} {os.path.join(output_dir, file_name)}", + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE) + result = subprocess.Popen(f"sourcekitten doc --objc {file_path} -- -x objective-c -isysroot $(xcrun --show-sdk-path) -I $(pwd)", + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE) + logging.info("------------") + api_info = result.stdout.read() + output_path = os.path.join(output_dir, file_name + ".json") + logging.info(output_path) + with open(output_path, 'w') as f: + f.write(api_info) + + +def parse_cmdline_args(): + parser = argparse.ArgumentParser() + parser.add_argument('-f', '--file_list', nargs='+', default=[]) + parser.add_argument('-o', '--output_dir', default="") + + args = parser.parse_args() + return args + + +if __name__ == '__main__': + main() \ No newline at end of file