From f6b757907fe54347ad8908edf8bcdef1fe56bd05 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Sun, 19 Jul 2026 11:51:46 -0700 Subject: [PATCH 01/17] test: Adds new python smoke tests for building against iOS; fenix --- automation/build_against_all.py | 84 +++++++ automation/build_against_fenix.py | 205 +++++++++++++++++ automation/build_against_ios.py | 261 ++++++++++++++++++++++ automation/shared.py | 18 +- docs/howtos/smoke-testing-app-services.md | 29 ++- 5 files changed, 592 insertions(+), 5 deletions(-) create mode 100755 automation/build_against_all.py create mode 100755 automation/build_against_fenix.py create mode 100755 automation/build_against_ios.py diff --git a/automation/build_against_all.py b/automation/build_against_all.py new file mode 100755 index 0000000000..a8c51f4ff2 --- /dev/null +++ b/automation/build_against_all.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +# Purpose: Run various smoke tests against this application-services working tree. +# Requirements: +# - python +# - application-services built and working. +# For the mac builds: +# - xcpretty (`gem install xcpretty`) +# - xcode + xcodebuild + xcodetools setup and running (a successful build of the firefox-ios repository) +# Arguments: +# --action => Can be either `run-tests` (default) or `build-without-testing` +# --use-local-repo => Use a local firefox-ios repository instead (at the provided path). Exclusive with `remote-repo-url`. +# --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) +# --branch => What branch of the firefox-ios repo to use +# TODO: args + +import argparse +import time +from shared import err_msg, step_msg +from build_against_fenix import build_against_fenix +from build_against_ios import build_against_ios +from build_against_hnt import build_against_hnt + +parser = argparse.ArgumentParser( + description="Run groups of tests against this application-services working tree." +) + +group = parser.add_mutually_exclusive_group() +parser.add_argument( + "--firefox-dir", + required=True, + help="Path to existing mozilla-central directory.", +) +parser.add_argument("--verbose", help="Includes subprocess running logs.", action=argparse.BooleanOptionalAction) +parser.add_argument('--allow-clears', + help="Clear existing uniffi bindings, swift files, and so on during the various build processes.", + action=argparse.BooleanOptionalAction) +parser.add_argument( + "--action", + required=True, + choices=["run-tests", "build-without-testing"], + help="Run the following action once firefox-ios is set up.", +) + +args = parser.parse_args() +firefox_dir = args.firefox_dir +verbose = args.verbose if args.verbose else False +allow_clears = args.allow_clears +action = args.action + +# Build against iOS +# TODO: We *may* be able to run this one concurrently to the other two? +start_time_ios = time.time() +success_ios = build_against_ios(None, None, clear_previous_bindings=allow_clears, clean_ios_caches=allow_clears, verbose=verbose, action=action) +time_diff_ios = time.time() - start_time_ios + +# Build against Fenix +start_time_fenix = time.time() +success_fenix = build_against_fenix(firefox_dir, None, None, None, clear_bindings=allow_clears, verbose=verbose, action=action) +time_diff_fenix = time.time() - start_time_fenix + + +did_tests_string = "" if action != "run-tests" else " (and tested)" +do_tests_string = "" if action != "run-tests" else " (and test)" +step_msg("Finished building. Results:") +if success_ios: + step_msg(f"Successfully built{did_tests_string} against iOS (elapsed {time_diff_ios:.2f}s)") +else: + err_msg(f"Failed to build{do_tests_string} against iOS (elapsed {time_diff_ios:.2f}s)") +if success_fenix: + step_msg(f"Successfully built{did_tests_string} against Fenix (elapsed {time_diff_fenix:.2f}s)") +else: + err_msg(f"Failed to build{do_tests_string} against Fenix (elapsed {time_diff_fenix:.2f}s)") + +# TODO: This is currently skipped for draft form of PR, pending discussion. +# if skipped_hnt: +# print("Skipped building against HNT. Pass `--use-monorepo-a-s` to build using the `mozilla-central` A-S tree") +# elif success_hnt: +# step_msg(f"Successfully built{did_tests_string} against HNT in (elapsed {time_diff_hnt:.2f}s)") +# else: +# err_msg(f"Failed to build{do_tests_string} against HNT (elapsed {time_diff_hnt:.2f}s)") diff --git a/automation/build_against_fenix.py b/automation/build_against_fenix.py new file mode 100755 index 0000000000..f5688a14e1 --- /dev/null +++ b/automation/build_against_fenix.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +# Purpose: Run Firefox fenix tests against this application-services working tree. +# https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-fenix.md +# So, for now, we need to use an existing respository. +# +# Requirements: +# - python +# - application-services built and working. +# - a `firefox`/`mozilla-central` repository set up and working to use. +# - See: https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html +# +# Usage: ./automation/build_against_fenix.py --action build-without-testing --firefox-dir ../firefox --prefix-ff fenix --prefix-as ads-client --verbose +# +# Arguments: +# --action => Can be either `run-tests` (default) or `build-without-testing` +# --firefox-dir => Working mozilla-central directory +# https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html +# --mozconfig => Absolute path to the mozconfig file to be used. +# --prefix-ff => Prefix to be used in gradle commands to firefox repo. eg: `./gradlew fenix:assembleDebug`. For example: `geckoview`, `fenix`, `focus`." +# --prefix-as => Crate prefix to be used in gradle commands to application-services repo. eg: `./gradlew ads-client:assembleDebug`. For example: `ads-client`, `fxaclient`." +# --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) +import argparse +import subprocess +import os +import tempfile +from pathlib import Path +from shared import find_app_services_root, set_gradle_substitution_path, step_msg, err_msg, run_cmd_is_successful + +DEFAULT_MOZ_CONFIG_LOCATION = "mozconfig_android" +DEFAULT_MOZ_CONFIG = """ +ac_add_options --enable-project=mobile/android +""" + +# TODO: Disable the gradle cache in mozilla-central - edit ./gradle.properties, comment out org.gradle.configuration-cache=true +def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, clear_bindings, verbose, action): + subprocess_stdout = None + if not verbose: + subprocess_stdout = subprocess.DEVNULL + subprocess_stderr = None + if not verbose: + subprocess_stderr = subprocess.DEVNULL + + if action is None: + action = "run-tests" + + firefox_repo_path = Path(firefox_dir) + tmp_dir_path = Path(tempfile.mkdtemp(suffix="-test-fenix")) + + app_services_path = find_app_services_root() + + # Basic sanity check here. Not remotely exhaustive, just to make sure some very wrong directory wasn't passed. + # TODO: modularize this + step_msg("Checking for sanity of application-services repository...") + for example_file in [ + "megazords", "components" + ]: + if not os.path.isfile(app_services_path / example_file) and not os.path.isdir(app_services_path / example_file): + err_msg(f"`{example_file}` is missing in root of `application-services` directory. Please confirm this is a valid local copy of `application-services` at: {app_services_path}") + return False + + + prefix_as_string = "" + if prefix_as: + prefix_as_string = f"{prefix_as}:" + prefix_ff_string = "" + if prefix_ff: + prefix_ff_string = f"{prefix_ff}:" + + + ## MOZCONFIG handling. + # Idea here is that mozconfig settings (primary indicator of how firefox is built) can't be passed + # without `configure`, which is not recommended. However, we can pass test fixture mozconfig files themselves as env variables. + if moz_config_location is None: + moz_config_location = os.path.abspath(tmp_dir_path / DEFAULT_MOZ_CONFIG_LOCATION) + with open(moz_config_location, "w") as file: + file.write(DEFAULT_MOZ_CONFIG) + + if not os.path.isabs(moz_config_location): + err_msg(f"`mozconfig` path passed: `{moz_config_location}` must be an absolute path.") + return False + if not os.path.isfile(moz_config_location): + err_msg(f"`mozconfig` path passed: `{moz_config_location}` could not be found.") + return False + step_msg(f"Using `mozconfig` path: `{moz_config_location}`. Displaying:") + with open(moz_config_location) as f: + print(f.read()) + + + # Basic sanity check here. Not remotely exhaustive, just to make sure the wrong directory wasn't passed. + step_msg("Checking for sanity of firefox repository...") + for example_file in [ + "mach", "CLOBBER", "gradlew", "Cargo.toml" + ]: + if not os.path.isfile(firefox_repo_path / example_file): + err_msg(f"`{example_file}` is missing in root of firefox directory. Please confirm this is a valid local copy of mozilla-central.") + return False + + step_msg(f"Configuring {firefox_repo_path} to autopublish appservices") + if not set_gradle_substitution_path( + firefox_repo_path, "autoPublish.application-services.dir", find_app_services_root() + ): + err_msg("Failed in attempting to set `local.properties` `autoPublish.application-services.dir`") + return False + + # Verification check + step_msg("Verifying Android environment...") + if not run_cmd_is_successful( + "./libs/verify-android-environment.sh", + cwd=app_services_path, + shell=True, + stdout=subprocess_stdout, + stderr=subprocess_stderr + ): + err_msg("Failed to run `./libs/verify-android-environment.sh` in app-services environment. Run this script and follow any instructions given until it succeeds, then try again.") + return False + + if clear_bindings: + step_msg("Cleaning application-services with gradle to clear cached android bindings...") + if not run_cmd_is_successful(f"./gradlew {prefix_as_string}clean", + cwd=app_services_path, + shell=True, + stdout=subprocess_stdout + ): + err_msg("Could not run ./gradlew clean. Please check to ensure the mozilla-center folder structure is sound.") + return False + + step_msg("Compiling application-services with gradle to test android bindings...") + if not run_cmd_is_successful(f"./gradlew {prefix_as_string}assembleDebug", + cwd=app_services_path, + shell=True, + stdout=subprocess_stdout + ): + err_msg("Failed to compile application-services with gradle.") + return False + + step_msg(f"Compiling firefox with mozconfig with `./gradlew {prefix_ff_string}assembleDebug` (mozconfig=`{moz_config_location}`)...") + if not run_cmd_is_successful(f"MOZCONFIG={moz_config_location} ./gradlew {prefix_ff_string}assembleDebug", + cwd=firefox_repo_path, + shell=True, + stdout=subprocess_stdout + ): + err_msg("Failed to compile firefox with gradle.") + return False + + if action == "run-tests": + step_msg(f"Compiling firefox with mozconfig with `./gradlew {prefix_ff_string}testDebug` (mozconfig=`{moz_config_location}`)...") + if not run_cmd_is_successful(f"MOZCONFIG={moz_config_location} ./gradlew {prefix_ff_string}testDebug", + cwd=firefox_repo_path, + shell=True, + stdout=subprocess_stdout + ): + err_msg("Failed to run tests against firefox with gradle.") + return False + step_msg("Successfully built against Android!") + return True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Run Firefox Android tests against this application-services working tree." + ) + + parser.add_argument("--verbose", help="Includes subprocess running logs.", action=argparse.BooleanOptionalAction) + parser.add_argument( + "--action", + choices=["run-tests", "build-without-testing"], + help="Whether to run tests after the build step is complete..", + ) + parser.add_argument( + "--firefox-dir", + required=True, + help="Path to existing mozilla-central directory.", + ) + + parser.add_argument( + "--mozconfig", + help="Absolute path to the desired mozconfig file. This affects the build destination, ensure it specifies android if you override it.", + ) + parser.add_argument( + "--prefix-ff", + help="Prefix name to pass to mozilla-central compilation to reduce the amount needing to build or test. For example: `geckoview`, `fenix`, `focus`.", + ) + parser.add_argument( + "--prefix-as", + help="Crate name to pass for preliminary application-services android building step. For example: `ads-client`, `fxaclient`", + ) + + # TODO: Is this needed? It seems like rebuilding it doesn't clear 'old' files (eg: ones that are not overwritten but should be gone, such as one under a different name) + parser.add_argument('--clear-previous-bindings', + help="Clear existing uniffi binding files from the firefox android build folder. (`/gradlew clean`). This shares any prefixes supplied by `--prefix-as`. If unrelated files need to be cleared, do not pass a --prefix-as argument", + action=argparse.BooleanOptionalAction) + + args = parser.parse_args() + firefox_dir = args.firefox_dir + verbose = args.verbose + moz_config_location = args.mozconfig + action = args.action + prefix_ff = args.prefix_ff + prefix_as = args.prefix_as + clear_bindings = args.clear_previous_bindings + build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, clear_bindings, verbose, action) \ No newline at end of file diff --git a/automation/build_against_ios.py b/automation/build_against_ios.py new file mode 100755 index 0000000000..abbe68691b --- /dev/null +++ b/automation/build_against_ios.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +# Purpose: Run Firefox-iOS tests against this application-services working tree. +# https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-firefox-ios.md +# Requirements: +# - python +# - application-services built and working. +# - xcpretty (`gem install xcpretty`) +# - xcode + xcodebuild + xcodetools setup and running (a successful build of the firefox-ios repository) +# +# Usage: ./automation/build_against_ios.py +# Arguments: +# --action => Can be either `run-tests` (default) or `build-without-testing` +# --remote-repo-url => Fetch firefox-ios repository from this URL instead. Exclusive with `use-local-repo` +# --use-local-repo => Use a local firefox-ios repository instead (at the provided path). Exclusive with `remote-repo-url`. +# --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) +# --clear-previous-bindings => Clear existing uniffi binding swift files from both the iOS and A-S generated folders. Use if files were created that need to be cleared (eg: a file of a name that is no longer used). +# --clean-ios-caches => Runs the code equivalent of Xcode's 'Clean Build Folder' +# +import argparse +import subprocess +import os +import tempfile +from pathlib import Path +import shutil +import glob +from shared import fatal_err, find_app_services_root, run_cmd_checked, step_msg, err_msg, run_cmd_is_successful + +DEFAULT_REMOTE_REPO_URL = "https://github.com/mozilla-mobile/firefox-ios.git" + +def build_against_ios(local_ios_repo_path, remote_repo_url, clear_previous_bindings, clean_ios_caches, verbose, action): + + subprocess_stdout = None + if not verbose: + subprocess_stdout = subprocess.DEVNULL + subprocess_stderr = None + if not verbose: + subprocess_stderr = subprocess.DEVNULL + + if action is None: + action = "run-tests" + + ios_repo_path = local_ios_repo_path + app_services_path = find_app_services_root() + + # Basic sanity check here. Not remotely exhaustive, just to make sure some very wrong directory wasn't passed. + # TODO: modularize + step_msg("Checking for sanity of application-services repository...") + for example_file in [ + "megazords", "components" + ]: + new_path = app_services_path / example_file + if not os.path.isfile(new_path) and not os.path.isdir(new_path): + err_msg(f"`{example_file}` is missing in root of `application-services` directory. Please confirm this is a valid local copy of `application-services` at: {app_services_path}") + return False + + + + step_msg("Checking for existence of xcodebuild...") + if not run_cmd_is_successful("xcodebuild -version", cwd=ios_repo_path, shell=True): + err_msg("xcodebuild is required to compile application-services for iOS. Please clone the firefox-ios repository and follow the instructions therein.") + return False + + # Creating temp directory and cloning repository + step_msg(f"Building application-services against iOS with action: `{action}`") + if local_ios_repo_path is None: + ios_repo_path = tempfile.mkdtemp(suffix="-test-ios") + if remote_repo_url is None: + remote_repo_url = DEFAULT_REMOTE_REPO_URL + step_msg(f"Cloning {remote_repo_url}") + run_cmd_checked(["git", "clone", remote_repo_url, ios_repo_path]) + + ios_generated_uniffi_files_path = f"{ios_repo_path}/MozillaRustComponents/Sources/MozillaRustComponentsWrapper/Generated" + local_repo_generated_uniffi_files_path = f"{app_services_path}/megazords/ios-rust/Sources/MozillaRustComponentsWrapper/Generated" + + # Bootstrapping the iOS repository + step_msg("Running the firefox-ios bootstrap script...") + if not run_cmd_is_successful("./bootstrap.sh", + cwd=ios_repo_path, + shell=True, + stdout=subprocess_stdout + ): + err_msg("Failed to bootstrap firefox-ios repository. Please clone the firefox-ios repository and follow the instructions therein.") + return False + + # Verification check + step_msg("Verifying iOS environment...") + if not run_cmd_is_successful( + "./libs/verify-ios-environment.sh", + cwd=app_services_path, + shell=True, + stdout=subprocess_stdout, + stderr=subprocess_stderr + ): + err_msg("Failed to verify environment for iOS. Please run `./libs/verify-ios-environment.sh`, making suggested changes until it succeeds.") + return False + + # Uniffi sanity check + if not os.path.isdir(ios_generated_uniffi_files_path): + err_msg(f"Expected path `{ios_generated_uniffi_files_path}` in firefox-ios is missing. Please confirm the repository structure or try cloning it again.") + return False + + if clear_previous_bindings: + step_msg("'clear-previous-bindings' is set, clearing uniffi folders") + + # Clear the uniffi bindings in the A-S repository as extra files created are not deleted + # Not relevant to tmp repository + if os.path.isdir(local_repo_generated_uniffi_files_path): + for p in Path(local_repo_generated_uniffi_files_path).glob("*.swift"): + p.unlink() + + # Clear equivalents from the ios repository + # (Relevant if we are using an existing directory) + if not run_cmd_is_successful(["git", "checkout", "."], cwd=ios_generated_uniffi_files_path): + fatal_err("Found an error running git commands to clear previous uniffi folders. Exiting.") + if not run_cmd_is_successful(["git", "clean", "-f"], cwd=ios_generated_uniffi_files_path): + fatal_err("Found an error running git commands to clear previous uniffi folders. Exiting.") + + # Build artifacts + # Unfortunately build_ios_artifacts is writing to stderr, so we need to hide it when it's not verbose. + step_msg("Building application-services iOS artifacts...") + if not run_cmd_is_successful( + "./automation/build_ios_artifacts.sh", + cwd=app_services_path, + shell=True, + check=True, + stdout=subprocess_stdout, + stderr=subprocess_stderr + ): + err_msg("Failed to build ios artifacts. Please ensure the code compiles and try running `./automation/build_ios_artifacts.sh` from the folder.") + return False + + if not os.path.isdir(local_repo_generated_uniffi_files_path): + err_msg(f"Expected path `{local_repo_generated_uniffi_files_path}` in application-services is missing after building. Please confirm the repository structure or try cloning it again.") + return False + + + step_msg("Copying uniffi bindings from:") + step_msg(f"{local_repo_generated_uniffi_files_path} -> {ios_generated_uniffi_files_path}") + for file in glob.glob('*.swift', root_dir=local_repo_generated_uniffi_files_path): + shutil.copy( + f"{local_repo_generated_uniffi_files_path}/{file}", + ios_generated_uniffi_files_path + ) + + # Remove the glean_sym file. + # https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-firefox-ios.md + step_msg(f"Removing: {ios_generated_uniffi_files_path}/glean_sym.swift") + os.remove(f"{ios_generated_uniffi_files_path}/glean_sym.swift") + + if clean_ios_caches: + # TODO: "Reset package caches" part not done yet + + # Clean build folder + step_msg("Cleaning build folder...") + if not run_cmd_is_successful( + """\ + set -o pipefail && \ + xcodebuild \ + -workspace ./firefox-ios/Client.xcodeproj/project.xcworkspace \ + -scheme Fennec \ + clean | \ + xcpretty + """, + cwd=ios_repo_path, + shell=True, + stdout=subprocess_stdout + ): + err_msg("Failed to clean and compile tests on iOS",) + return False + + # Run the build action + if action == "build-without-testing": + step_msg("Running xcodebuild without testing (this may take a few minutes)...") + if not run_cmd_is_successful( + """\ + set -o pipefail && \ + xcodebuild \ + -workspace ./firefox-ios/Client.xcodeproj/project.xcworkspace \ + -scheme Fennec \ + -destination 'platform=iOS Simulator,name=iPhone 17' \ + build-for-testing | \ + xcpretty + """, + cwd=ios_repo_path, + shell=True, + stdout=subprocess_stdout + ): + err_msg("Failed to compile and run tests on iOS",) + return False + + elif action == "run-tests": + step_msg("Building firefox-ios and running tests (this may take a few minutes)...") + if not run_cmd_is_successful( + """\ + set -o pipefail && \ + xcodebuild \ + -workspace ./firefox-ios/Client.xcodeproj/project.xcworkspace \ + -scheme Fennec \ + -destination 'platform=iOS Simulator,name=iPhone 17' \ + test | \ + xcpretty + """, + cwd=ios_repo_path, + shell=True, + stdout=subprocess_stdout + ): + err_msg("Failed to compile and run tests on iOS") + return False + + else: + err_msg("You must either run `--action run-tests` or `--action build-without-testing` ") + return False + + step_msg("Successfully built against iOS!") + return True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Run Firefox-iOS tests against this application-services working tree." + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--use-local-repo", + metavar="LOCAL_IOS_REPO_PATH", + help="Use a local copy of firefox-ios instead of cloning it.", + ) + group.add_argument( + "--remote-repo-url", + metavar="REMOTE_REPO_PATH", + help="Clone a different firefox-ios repository.", + ) + + parser.add_argument("--verbose", help="Includes subprocess running logs.", action=argparse.BooleanOptionalAction) + parser.add_argument('--clear-previous-bindings', + help="Clear existing uniffi binding swift files from both the iOS and A-S generated folders. Use if files were created that need to be cleared (eg: a file of a name that is no longer used).", + action=argparse.BooleanOptionalAction) + + parser.add_argument('--clean-ios-caches', + help="Run Xcode 'Clean Build Folder'", + action=argparse.BooleanOptionalAction) + + parser.add_argument( + "--action", + choices=["run-tests", "build-without-testing"], + help="Run the following action once firefox-ios is set up.", + ) + + args = parser.parse_args() + local_ios_repo_path = args.use_local_repo + remote_repo_url = args.remote_repo_url + clear_previous_bindings = args.clear_previous_bindings + clean_ios_caches = args.clean_ios_caches + verbose = args.verbose + action = args.action + + build_against_ios(local_ios_repo_path, remote_repo_url, clear_previous_bindings, clean_ios_caches, verbose, action) \ No newline at end of file diff --git a/automation/shared.py b/automation/shared.py index 3a490e7cb2..e18bf0943b 100644 --- a/automation/shared.py +++ b/automation/shared.py @@ -15,15 +15,24 @@ def step_msg(msg): def fatal_err(msg): - print(f"\033[31mError: {msg}\033[0m") + err_msg(msg) sys.exit(1) +def err_msg(msg, allow_continue = False): + print(f"\033[31mError: {msg}\033[0m") + if not allow_continue: + sys.exit(1) + def run_cmd_checked(*args, **kwargs): """Run a command, throwing an exception if it exits with non-zero status.""" kwargs["check"] = True return subprocess.run(*args, **kwargs) # noqa: PLW1510 +def run_cmd_is_successful(*args, **kwargs): + """Run a subprocess command, returning False if it exits with non-zero status (True otherwise).""" + return subprocess.run(*args, **kwargs).returncode == 0 + def check_output(*args, **kwargs): """Run a command, throwing an exception if it exits with non-zero status.""" @@ -69,6 +78,8 @@ def set_gradle_substitution_path(project_dir, name, value): If the named property already exists with the correct value then it will silently succeed; if the named property already exists with a different value then it will noisily fail. + + Returns False on such a failure, otherwise returns True. """ project_dir = Path(project_dir).resolve() properties_file = project_dir / "local.properties" @@ -86,7 +97,9 @@ def set_gradle_substitution_path(project_dir, name, value): fatal_err( f"Conflicting property {name}={cur_value} (not {abs_value})" ) - return + return False + else: + return True # The file does not contain the required property, append it. # Note that the project probably expects a path relative to the project root. ancestor = Path(os.path.commonpath([project_dir, abs_value])) @@ -98,6 +111,7 @@ def set_gradle_substitution_path(project_dir, name, value): step_msg(f"Setting relative path from {project_dir} to {abs_value} as {relpath}") with properties_file.open("a") as f: f.write(f"{name}={relpath}\n") + return True class RefNames: diff --git a/docs/howtos/smoke-testing-app-services.md b/docs/howtos/smoke-testing-app-services.md index a2c0a8f9dd..3ad67746a7 100644 --- a/docs/howtos/smoke-testing-app-services.md +++ b/docs/howtos/smoke-testing-app-services.md @@ -7,17 +7,40 @@ The testing can be done manually using substitution scripts, but we also have sc Run `pip3 install -r automation/requirements.txt` to install the required Python packages. -## Android Components + +## Usage + +You can easily run a smoke test against iOS and Fenix by running the following: + +`./automation/build_against_all.py --firefox-dir ../firefox --action build-without-testing --allow-clears` + +In this case, `firefox-dir` must point to a bootstrapped and working installation of `mozilla-central` (see instructions [here](https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html)). It is used for the compilation and test of the Android build. + +You can also run against specific platforms the following way: + +- iOS: + + ```./automation/build_against_ios.py --clear-previous-bindings --clean-ios-caches --action build-without-testing``` + +- Fenix: + + ```./automation/build_against_fenix.py --action build-without-testing --firefox-dir ../firefox``` + +All test scripts also accept the `--verbose` argument to show the output of run subprocesses (such as `./mach build`). + +## Deprecated tests + +### Android Components The `automation/smoke-test-android-components.py` script will clone (or use a local version) of android-components and run a subset of its tests against the current `application-services` worktree. It tries to only run tests that might be relevant to `application-services` functionality. -## Fenix +### Fenix The `automation/smoke-test-fenix.py` script will clone (or use a local version) of Fenix and run tests against the current `application-services` worktree. -## Firefox iOS +### Firefox iOS The `automation/smoke-test-fxios.py` script will clone (or use a local version) of Firefox iOS and run tests against the current `application-services` worktree. From e0a8b674ac989d1b3eeb543e1c16f08787016fa9 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Sun, 19 Jul 2026 22:14:50 -0700 Subject: [PATCH 02/17] test: Adds scheme, tests --- automation/build_against_all.py | 2 +- automation/build_against_ios.py | 33 +++++++++++++++++------ docs/howtos/smoke-testing-app-services.md | 16 ++++++++--- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/automation/build_against_all.py b/automation/build_against_all.py index a8c51f4ff2..122026221a 100755 --- a/automation/build_against_all.py +++ b/automation/build_against_all.py @@ -59,7 +59,7 @@ # Build against Fenix start_time_fenix = time.time() -success_fenix = build_against_fenix(firefox_dir, None, None, None, clear_bindings=allow_clears, verbose=verbose, action=action) +success_fenix = build_against_fenix(firefox_dir, None, prefix_ff="fenix", prefix_as=None, clear_bindings=allow_clears, verbose=verbose, action=action) time_diff_fenix = time.time() - start_time_fenix diff --git a/automation/build_against_ios.py b/automation/build_against_ios.py index abbe68691b..652dc5cffd 100755 --- a/automation/build_against_ios.py +++ b/automation/build_against_ios.py @@ -31,7 +31,7 @@ DEFAULT_REMOTE_REPO_URL = "https://github.com/mozilla-mobile/firefox-ios.git" -def build_against_ios(local_ios_repo_path, remote_repo_url, clear_previous_bindings, clean_ios_caches, verbose, action): +def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, clear_previous_bindings, clean_ios_caches, verbose, action): subprocess_stdout = None if not verbose: @@ -151,17 +151,19 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, clear_previous_bindi step_msg(f"Removing: {ios_generated_uniffi_files_path}/glean_sym.swift") os.remove(f"{ios_generated_uniffi_files_path}/glean_sym.swift") + scheme = "Fennec" if scheme is None else scheme + test_plan = "Smoketest" if test_plan is None else test_plan if clean_ios_caches: # TODO: "Reset package caches" part not done yet # Clean build folder step_msg("Cleaning build folder...") if not run_cmd_is_successful( - """\ + f"""\ set -o pipefail && \ xcodebuild \ -workspace ./firefox-ios/Client.xcodeproj/project.xcworkspace \ - -scheme Fennec \ + -scheme {scheme} \ clean | \ xcpretty """, @@ -176,11 +178,11 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, clear_previous_bindi if action == "build-without-testing": step_msg("Running xcodebuild without testing (this may take a few minutes)...") if not run_cmd_is_successful( - """\ + f"""\ set -o pipefail && \ xcodebuild \ -workspace ./firefox-ios/Client.xcodeproj/project.xcworkspace \ - -scheme Fennec \ + -scheme {scheme} \ -destination 'platform=iOS Simulator,name=iPhone 17' \ build-for-testing | \ xcpretty @@ -195,12 +197,13 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, clear_previous_bindi elif action == "run-tests": step_msg("Building firefox-ios and running tests (this may take a few minutes)...") if not run_cmd_is_successful( - """\ + f"""\ set -o pipefail && \ xcodebuild \ -workspace ./firefox-ios/Client.xcodeproj/project.xcworkspace \ - -scheme Fennec \ + -scheme {scheme} \ -destination 'platform=iOS Simulator,name=iPhone 17' \ + -testPlan {test_plan} \ test | \ xcpretty """, @@ -244,6 +247,18 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, clear_previous_bindi help="Run Xcode 'Clean Build Folder'", action=argparse.BooleanOptionalAction) + parser.add_argument( + "--scheme", + help="The scheme to run. Likely: `Fennec` (default) or `Firefox`", + default="Fennec" + ) + + parser.add_argument( + "--test-plan", + help="The test plan to test with. Likely: `Smoketest` (default) or `FullFunctionalTestPlan`", + default="Smoketest" + ) + parser.add_argument( "--action", choices=["run-tests", "build-without-testing"], @@ -255,7 +270,9 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, clear_previous_bindi remote_repo_url = args.remote_repo_url clear_previous_bindings = args.clear_previous_bindings clean_ios_caches = args.clean_ios_caches + scheme = args.scheme + test_plan = args.test_plan verbose = args.verbose action = args.action - build_against_ios(local_ios_repo_path, remote_repo_url, clear_previous_bindings, clean_ios_caches, verbose, action) \ No newline at end of file + build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, clear_previous_bindings, clean_ios_caches, verbose, action) \ No newline at end of file diff --git a/docs/howtos/smoke-testing-app-services.md b/docs/howtos/smoke-testing-app-services.md index 3ad67746a7..61f778ab71 100644 --- a/docs/howtos/smoke-testing-app-services.md +++ b/docs/howtos/smoke-testing-app-services.md @@ -16,15 +16,25 @@ You can easily run a smoke test against iOS and Fenix by running the following: In this case, `firefox-dir` must point to a bootstrapped and working installation of `mozilla-central` (see instructions [here](https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html)). It is used for the compilation and test of the Android build. -You can also run against specific platforms the following way: +You can also run against specific platforms with the following examples: - iOS: - ```./automation/build_against_ios.py --clear-previous-bindings --clean-ios-caches --action build-without-testing``` + ```./automation/build_against_ios.py --action build-without-testing --clear-previous-bindings --clean-ios-caches --use-local-repo ../firefox-ios``` + + - By default, this script creates a `tmp` directory for `firefox-ios` and uses it. If you have a running `firefox-ios`, you can add the argument `--use-local-repo ../firefox-ios` (as shown), which may result in speed gains and the ability to run it on XCode immediately after a failure. + + - You can also customize the run scheme (`--scheme`) or test plan (`--test-plan`). Available schemes include `Fennec` (default) and `Firefox`. Available test plans include: `Smoketest`, `FullFunctionalTestPlan` and `UnitTest`. + + - A full list of schemes and their corresponding test plans can be found [in the firefox-ios](https://github.com/mozilla-mobile/firefox-ios/tree/main/firefox-ios/Client.xcodeproj/xcshareddata/xcschemes) respository. - Fenix: - ```./automation/build_against_fenix.py --action build-without-testing --firefox-dir ../firefox``` + ```./automation/build_against_fenix.py --action build-without-testing --firefox-dir ../firefox --prefix-ff fenix --clear-previous-bindings``` + + - The `--prefix-ff` argument here refers to the prefix passed to commands like `./gradlew fenix:assembleDebug`. It can be omitted, but may cause failures on non-fenix projects. + + - The `--clear-previous-bindings` argument here runs a `./gradlew prefix:clear` before recompiling. It is not always necessary, and can be excluded for some speed gains, but can result in some cache reuse. All test scripts also accept the `--verbose` argument to show the output of run subprocesses (such as `./mach build`). From 5a509491f469f0f1ea1d01118316c67433876971 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 20 Jul 2026 08:34:33 -0700 Subject: [PATCH 03/17] fix: Modularization and missing xcframework change --- automation/build_against_all.py | 11 ++--- automation/build_against_fenix.py | 51 ++++++-------------- automation/build_against_ios.py | 78 ++++++++++++++++++++++--------- automation/shared.py | 12 +++++ 4 files changed, 89 insertions(+), 63 deletions(-) diff --git a/automation/build_against_all.py b/automation/build_against_all.py index 122026221a..9da52f146d 100755 --- a/automation/build_against_all.py +++ b/automation/build_against_all.py @@ -14,15 +14,14 @@ # --action => Can be either `run-tests` (default) or `build-without-testing` # --use-local-repo => Use a local firefox-ios repository instead (at the provided path). Exclusive with `remote-repo-url`. # --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) -# --branch => What branch of the firefox-ios repo to use -# TODO: args +# --allow-clears => Clear existing uniffi bindings, swift files, and so on during the various build processes. +# --action => Either 'run-tests' or 'build-without-testing import argparse import time from shared import err_msg, step_msg from build_against_fenix import build_against_fenix from build_against_ios import build_against_ios -from build_against_hnt import build_against_hnt parser = argparse.ArgumentParser( description="Run groups of tests against this application-services working tree." @@ -42,7 +41,7 @@ "--action", required=True, choices=["run-tests", "build-without-testing"], - help="Run the following action once firefox-ios is set up.", + help="Run the following action for target's test", ) args = parser.parse_args() @@ -52,9 +51,9 @@ action = args.action # Build against iOS -# TODO: We *may* be able to run this one concurrently to the other two? start_time_ios = time.time() -success_ios = build_against_ios(None, None, clear_previous_bindings=allow_clears, clean_ios_caches=allow_clears, verbose=verbose, action=action) +success_ios = build_against_ios(None, None, scheme="Fennec", test_plan="Smoketest", + clear_previous_bindings=allow_clears, clean_ios_caches=allow_clears, verbose=verbose, action=action) time_diff_ios = time.time() - start_time_ios # Build against Fenix diff --git a/automation/build_against_fenix.py b/automation/build_against_fenix.py index f5688a14e1..31aa0abff5 100755 --- a/automation/build_against_fenix.py +++ b/automation/build_against_fenix.py @@ -28,7 +28,7 @@ import os import tempfile from pathlib import Path -from shared import find_app_services_root, set_gradle_substitution_path, step_msg, err_msg, run_cmd_is_successful +from shared import find_app_services_root, set_gradle_substitution_path, step_msg, err_msg, run_cmd_is_successful, dir_file_sanity_check DEFAULT_MOZ_CONFIG_LOCATION = "mozconfig_android" DEFAULT_MOZ_CONFIG = """ @@ -37,12 +37,8 @@ # TODO: Disable the gradle cache in mozilla-central - edit ./gradle.properties, comment out org.gradle.configuration-cache=true def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, clear_bindings, verbose, action): - subprocess_stdout = None - if not verbose: - subprocess_stdout = subprocess.DEVNULL - subprocess_stderr = None - if not verbose: - subprocess_stderr = subprocess.DEVNULL + subprocess_stdout = None if verbose else subprocess.DEVNULL + subprocess_stderr = None if verbose else subprocess.DEVNULL if action is None: action = "run-tests" @@ -52,33 +48,21 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, app_services_path = find_app_services_root() - # Basic sanity check here. Not remotely exhaustive, just to make sure some very wrong directory wasn't passed. - # TODO: modularize this step_msg("Checking for sanity of application-services repository...") - for example_file in [ - "megazords", "components" - ]: - if not os.path.isfile(app_services_path / example_file) and not os.path.isdir(app_services_path / example_file): - err_msg(f"`{example_file}` is missing in root of `application-services` directory. Please confirm this is a valid local copy of `application-services` at: {app_services_path}") - return False - - - prefix_as_string = "" - if prefix_as: - prefix_as_string = f"{prefix_as}:" - prefix_ff_string = "" - if prefix_ff: - prefix_ff_string = f"{prefix_ff}:" + if not dir_file_sanity_check(app_services_path, "application-services", ["megazords", "components"]): + return False + prefix_as_string = f"{prefix_as}:" if prefix_as else "" + prefix_ff_string = f"{prefix_ff}:" if prefix_ff else "" - ## MOZCONFIG handling. + # MOZCONFIG handling. # Idea here is that mozconfig settings (primary indicator of how firefox is built) can't be passed # without `configure`, which is not recommended. However, we can pass test fixture mozconfig files themselves as env variables. if moz_config_location is None: moz_config_location = os.path.abspath(tmp_dir_path / DEFAULT_MOZ_CONFIG_LOCATION) with open(moz_config_location, "w") as file: file.write(DEFAULT_MOZ_CONFIG) - + if not os.path.isabs(moz_config_location): err_msg(f"`mozconfig` path passed: `{moz_config_location}` must be an absolute path.") return False @@ -89,16 +73,12 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, with open(moz_config_location) as f: print(f.read()) - # Basic sanity check here. Not remotely exhaustive, just to make sure the wrong directory wasn't passed. step_msg("Checking for sanity of firefox repository...") - for example_file in [ - "mach", "CLOBBER", "gradlew", "Cargo.toml" - ]: - if not os.path.isfile(firefox_repo_path / example_file): - err_msg(f"`{example_file}` is missing in root of firefox directory. Please confirm this is a valid local copy of mozilla-central.") - return False + if not dir_file_sanity_check(firefox_repo_path, "mozilla-central", ["mach", "CLOBBER", "gradlew", "Cargo.toml", "local.properties"]): + return False + # Key step: gradle can use a different application-services directory step_msg(f"Configuring {firefox_repo_path} to autopublish appservices") if not set_gradle_substitution_path( firefox_repo_path, "autoPublish.application-services.dir", find_app_services_root() @@ -106,7 +86,7 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, err_msg("Failed in attempting to set `local.properties` `autoPublish.application-services.dir`") return False - # Verification check + # Environment verification check step_msg("Verifying Android environment...") if not run_cmd_is_successful( "./libs/verify-android-environment.sh", @@ -118,6 +98,7 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, err_msg("Failed to run `./libs/verify-android-environment.sh` in app-services environment. Run this script and follow any instructions given until it succeeds, then try again.") return False + # Gradle clean cached files if clear_bindings: step_msg("Cleaning application-services with gradle to clear cached android bindings...") if not run_cmd_is_successful(f"./gradlew {prefix_as_string}clean", @@ -128,6 +109,7 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, err_msg("Could not run ./gradlew clean. Please check to ensure the mozilla-center folder structure is sound.") return False + # Run gradle compilations and tests step_msg("Compiling application-services with gradle to test android bindings...") if not run_cmd_is_successful(f"./gradlew {prefix_as_string}assembleDebug", cwd=app_services_path, @@ -175,7 +157,6 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, required=True, help="Path to existing mozilla-central directory.", ) - parser.add_argument( "--mozconfig", help="Absolute path to the desired mozconfig file. This affects the build destination, ensure it specifies android if you override it.", @@ -188,8 +169,6 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, "--prefix-as", help="Crate name to pass for preliminary application-services android building step. For example: `ads-client`, `fxaclient`", ) - - # TODO: Is this needed? It seems like rebuilding it doesn't clear 'old' files (eg: ones that are not overwritten but should be gone, such as one under a different name) parser.add_argument('--clear-previous-bindings', help="Clear existing uniffi binding files from the firefox android build folder. (`/gradlew clean`). This shares any prefixes supplied by `--prefix-as`. If unrelated files need to be cleared, do not pass a --prefix-as argument", action=argparse.BooleanOptionalAction) diff --git a/automation/build_against_ios.py b/automation/build_against_ios.py index 652dc5cffd..9931db9930 100755 --- a/automation/build_against_ios.py +++ b/automation/build_against_ios.py @@ -19,6 +19,8 @@ # --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) # --clear-previous-bindings => Clear existing uniffi binding swift files from both the iOS and A-S generated folders. Use if files were created that need to be cleared (eg: a file of a name that is no longer used). # --clean-ios-caches => Runs the code equivalent of Xcode's 'Clean Build Folder' +# --scheme => The XCode scheme to build with, such as 'Fennec' or 'Firefox' +# --test-plan => The XCode test plan to run tests with (if action is `run-tests`), such as 'Smoketest' or 'FullFunctionalTestPlan' # import argparse import subprocess @@ -27,18 +29,53 @@ from pathlib import Path import shutil import glob -from shared import fatal_err, find_app_services_root, run_cmd_checked, step_msg, err_msg, run_cmd_is_successful +import re +from shared import fatal_err, find_app_services_root, run_cmd_checked, step_msg, err_msg, run_cmd_is_successful, dir_file_sanity_check DEFAULT_REMOTE_REPO_URL = "https://github.com/mozilla-mobile/firefox-ios.git" +MOZILLA_RUST_COMPONENTS_IOS_PATH = "MozillaRustComponents/Package.swift" +MOZILLA_RUST_COMPONENTS_AS_PATH = "megazords/ios-rust/MozillaRustComponents.xcframework" -def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, clear_previous_bindings, clean_ios_caches, verbose, action): +def replace_swift_package_artifact(ios_repo_path, as_repo_path): + """ + Replaces the local artifact pursuant to step 2 here. + https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-firefox-ios.md#step-2--point-firefox-ios-to-your-local-artifact + """ + + package_file_path = Path(ios_repo_path) / MOZILLA_RUST_COMPONENTS_IOS_PATH + xcframework_file_path = Path(as_repo_path) / MOZILLA_RUST_COMPONENTS_AS_PATH + + if not os.path.isfile(package_file_path): + err_msg("Could not find an instance of `MozillaRustComponents/Package.swift` to modify. Please ensure the iOS directory is lined up correctly.") + return False + if not os.path.isdir(xcframework_file_path): + err_msg(f"Could not find an instance of `MozillaRustComponents.xcframework` to link at {xcframework_file_path}. Please ensure the a-s directory is lined up correctly.") + return False + xcframework_file_path_relative = os.path.relpath(xcframework_file_path, Path(package_file_path.parent)) - subprocess_stdout = None - if not verbose: - subprocess_stdout = subprocess.DEVNULL - subprocess_stderr = None - if not verbose: - subprocess_stderr = subprocess.DEVNULL + # Uses regex. Matches the binaryTarget listed and replaces it with the following string. + replace_with = f""" + binaryTarget( + name: "MozillaRustComponents", + path: "{xcframework_file_path_relative}" + ) + """ + step_msg(f"Writing to MozillaRustComponents/Package.swift:\n{replace_with}") + regex = re.compile(r'binaryTarget\([\n\s]*name\: \"MozillaRustComponents\",[\S\n\t\v ]*MozillaRustComponents\.xcframework\"[\n\s]*\)') + + with open(package_file_path, "r+") as f: + data = f.read() + # Regex string matches this tidbit. + package_file = regex.sub(replace_with, data) + f.seek(0) + f.write(package_file) + f.truncate() + + return True + +def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, clear_previous_bindings, clean_ios_caches, verbose, action): + subprocess_stdout = None if verbose else subprocess.DEVNULL + subprocess_stderr = None if verbose else subprocess.DEVNULL if action is None: action = "run-tests" @@ -46,18 +83,11 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c ios_repo_path = local_ios_repo_path app_services_path = find_app_services_root() - # Basic sanity check here. Not remotely exhaustive, just to make sure some very wrong directory wasn't passed. - # TODO: modularize + # Naive sanity check here. Not remotely exhaustive, just to make sure some extremely incorrect directory wasn't passed. step_msg("Checking for sanity of application-services repository...") - for example_file in [ - "megazords", "components" - ]: - new_path = app_services_path / example_file - if not os.path.isfile(new_path) and not os.path.isdir(new_path): - err_msg(f"`{example_file}` is missing in root of `application-services` directory. Please confirm this is a valid local copy of `application-services` at: {app_services_path}") - return False - - + if not dir_file_sanity_check(app_services_path, "application-services", ["megazords", "components"]): + return False + step_msg("Checking for existence of xcodebuild...") if not run_cmd_is_successful("xcodebuild -version", cwd=ios_repo_path, shell=True): @@ -132,12 +162,18 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c ): err_msg("Failed to build ios artifacts. Please ensure the code compiles and try running `./automation/build_ios_artifacts.sh` from the folder.") return False + + # Replace with regex some data in the swift package file + # https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-firefox-ios.md#step-2--point-firefox-ios-to-your-local-artifact + if not replace_swift_package_artifact(ios_repo_path, app_services_path): + err_msg("Failed to point firefox-ios to local a-s xcframework artifact") + return False if not os.path.isdir(local_repo_generated_uniffi_files_path): err_msg(f"Expected path `{local_repo_generated_uniffi_files_path}` in application-services is missing after building. Please confirm the repository structure or try cloning it again.") return False - + # Copies folder of uniffi bindings. step_msg("Copying uniffi bindings from:") step_msg(f"{local_repo_generated_uniffi_files_path} -> {ios_generated_uniffi_files_path}") for file in glob.glob('*.swift', root_dir=local_repo_generated_uniffi_files_path): @@ -151,6 +187,7 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c step_msg(f"Removing: {ios_generated_uniffi_files_path}/glean_sym.swift") os.remove(f"{ios_generated_uniffi_files_path}/glean_sym.swift") + # Clean packages in xcodebuild scheme = "Fennec" if scheme is None else scheme test_plan = "Smoketest" if test_plan is None else test_plan if clean_ios_caches: @@ -193,7 +230,6 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c ): err_msg("Failed to compile and run tests on iOS",) return False - elif action == "run-tests": step_msg("Building firefox-ios and running tests (this may take a few minutes)...") if not run_cmd_is_successful( diff --git a/automation/shared.py b/automation/shared.py index e18bf0943b..3c1432a138 100644 --- a/automation/shared.py +++ b/automation/shared.py @@ -53,6 +53,18 @@ def find_app_services_root(): return cur_dir.absolute() +def dir_file_sanity_check(directory_path, dir_name, example_file_names): + """ + Extremely rudimentary and naive check for a few basic files in a directory, for better handling if the wrong directory is passed. + """ + for example_file in example_file_names: + new_path = Path(directory_path) / example_file + if not os.path.isfile(new_path) and not os.path.isdir(new_path): + err_msg(f"`{example_file}` is missing in root of `{dir_name}` directory. Please confirm this is a valid local copy of `{dir_name}` at: {directory_path}") + return False + return True + + def get_moz_remote(): """ Get the name of the remote for the official mozilla application-services repo From 0f4649a0755c9af9a1f052e733f749fa84535844 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 20 Jul 2026 08:41:14 -0700 Subject: [PATCH 04/17] fix: linting --- automation/build_against_all.py | 62 +++++++---- automation/build_against_fenix.py | 126 +++++++++++++++------ automation/build_against_ios.py | 177 +++++++++++++++++++++--------- 3 files changed, 259 insertions(+), 106 deletions(-) diff --git a/automation/build_against_all.py b/automation/build_against_all.py index 9da52f146d..a89080fef5 100755 --- a/automation/build_against_all.py +++ b/automation/build_against_all.py @@ -4,7 +4,7 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # Purpose: Run various smoke tests against this application-services working tree. -# Requirements: +# Requirements: # - python # - application-services built and working. # For the mac builds: @@ -33,10 +33,16 @@ required=True, help="Path to existing mozilla-central directory.", ) -parser.add_argument("--verbose", help="Includes subprocess running logs.", action=argparse.BooleanOptionalAction) -parser.add_argument('--allow-clears', - help="Clear existing uniffi bindings, swift files, and so on during the various build processes.", - action=argparse.BooleanOptionalAction) +parser.add_argument( + "--verbose", + help="Includes subprocess running logs.", + action=argparse.BooleanOptionalAction, +) +parser.add_argument( + "--allow-clears", + help="Clear existing uniffi bindings, swift files, and so on during the various build processes.", + action=argparse.BooleanOptionalAction, +) parser.add_argument( "--action", required=True, @@ -52,13 +58,29 @@ # Build against iOS start_time_ios = time.time() -success_ios = build_against_ios(None, None, scheme="Fennec", test_plan="Smoketest", - clear_previous_bindings=allow_clears, clean_ios_caches=allow_clears, verbose=verbose, action=action) +success_ios = build_against_ios( + None, + None, + scheme="Fennec", + test_plan="Smoketest", + clear_previous_bindings=allow_clears, + clean_ios_caches=allow_clears, + verbose=verbose, + action=action, +) time_diff_ios = time.time() - start_time_ios # Build against Fenix start_time_fenix = time.time() -success_fenix = build_against_fenix(firefox_dir, None, prefix_ff="fenix", prefix_as=None, clear_bindings=allow_clears, verbose=verbose, action=action) +success_fenix = build_against_fenix( + firefox_dir, + None, + prefix_ff="fenix", + prefix_as=None, + clear_bindings=allow_clears, + verbose=verbose, + action=action, +) time_diff_fenix = time.time() - start_time_fenix @@ -66,18 +88,18 @@ do_tests_string = "" if action != "run-tests" else " (and test)" step_msg("Finished building. Results:") if success_ios: - step_msg(f"Successfully built{did_tests_string} against iOS (elapsed {time_diff_ios:.2f}s)") + step_msg( + f"Successfully built{did_tests_string} against iOS (elapsed {time_diff_ios:.2f}s)" + ) else: - err_msg(f"Failed to build{do_tests_string} against iOS (elapsed {time_diff_ios:.2f}s)") + err_msg( + f"Failed to build{do_tests_string} against iOS (elapsed {time_diff_ios:.2f}s)" + ) if success_fenix: - step_msg(f"Successfully built{did_tests_string} against Fenix (elapsed {time_diff_fenix:.2f}s)") + step_msg( + f"Successfully built{did_tests_string} against Fenix (elapsed {time_diff_fenix:.2f}s)" + ) else: - err_msg(f"Failed to build{do_tests_string} against Fenix (elapsed {time_diff_fenix:.2f}s)") - -# TODO: This is currently skipped for draft form of PR, pending discussion. -# if skipped_hnt: -# print("Skipped building against HNT. Pass `--use-monorepo-a-s` to build using the `mozilla-central` A-S tree") -# elif success_hnt: -# step_msg(f"Successfully built{did_tests_string} against HNT in (elapsed {time_diff_hnt:.2f}s)") -# else: -# err_msg(f"Failed to build{do_tests_string} against HNT (elapsed {time_diff_hnt:.2f}s)") + err_msg( + f"Failed to build{do_tests_string} against Fenix (elapsed {time_diff_fenix:.2f}s)" + ) diff --git a/automation/build_against_fenix.py b/automation/build_against_fenix.py index 31aa0abff5..5e65f0a8d2 100755 --- a/automation/build_against_fenix.py +++ b/automation/build_against_fenix.py @@ -6,37 +6,53 @@ # Purpose: Run Firefox fenix tests against this application-services working tree. # https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-fenix.md # So, for now, we need to use an existing respository. -# -# Requirements: +# +# Requirements: # - python # - application-services built and working. -# - a `firefox`/`mozilla-central` repository set up and working to use. +# - a `firefox`/`mozilla-central` repository set up and working to use. # - See: https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html # # Usage: ./automation/build_against_fenix.py --action build-without-testing --firefox-dir ../firefox --prefix-ff fenix --prefix-as ads-client --verbose # # Arguments: # --action => Can be either `run-tests` (default) or `build-without-testing` -# --firefox-dir => Working mozilla-central directory +# --firefox-dir => Working mozilla-central directory # https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html # --mozconfig => Absolute path to the mozconfig file to be used. -# --prefix-ff => Prefix to be used in gradle commands to firefox repo. eg: `./gradlew fenix:assembleDebug`. For example: `geckoview`, `fenix`, `focus`." -# --prefix-as => Crate prefix to be used in gradle commands to application-services repo. eg: `./gradlew ads-client:assembleDebug`. For example: `ads-client`, `fxaclient`." +# --prefix-ff => Prefix to be used in gradle commands to firefox repo. eg: `./gradlew fenix:assembleDebug`. For example: `geckoview`, `fenix`, `focus`." +# --prefix-as => Crate prefix to be used in gradle commands to application-services repo. eg: `./gradlew ads-client:assembleDebug`. For example: `ads-client`, `fxaclient`." # --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) import argparse import subprocess import os import tempfile from pathlib import Path -from shared import find_app_services_root, set_gradle_substitution_path, step_msg, err_msg, run_cmd_is_successful, dir_file_sanity_check +from shared import ( + find_app_services_root, + set_gradle_substitution_path, + step_msg, + err_msg, + run_cmd_is_successful, + dir_file_sanity_check, +) DEFAULT_MOZ_CONFIG_LOCATION = "mozconfig_android" DEFAULT_MOZ_CONFIG = """ ac_add_options --enable-project=mobile/android """ + # TODO: Disable the gradle cache in mozilla-central - edit ./gradle.properties, comment out org.gradle.configuration-cache=true -def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, clear_bindings, verbose, action): +def build_against_fenix( + firefox_dir, + moz_config_location, + prefix_ff, + prefix_as, + clear_bindings, + verbose, + action, +): subprocess_stdout = None if verbose else subprocess.DEVNULL subprocess_stderr = None if verbose else subprocess.DEVNULL @@ -49,7 +65,9 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, app_services_path = find_app_services_root() step_msg("Checking for sanity of application-services repository...") - if not dir_file_sanity_check(app_services_path, "application-services", ["megazords", "components"]): + if not dir_file_sanity_check( + app_services_path, "application-services", ["megazords", "components"] + ): return False prefix_as_string = f"{prefix_as}:" if prefix_as else "" @@ -59,12 +77,16 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, # Idea here is that mozconfig settings (primary indicator of how firefox is built) can't be passed # without `configure`, which is not recommended. However, we can pass test fixture mozconfig files themselves as env variables. if moz_config_location is None: - moz_config_location = os.path.abspath(tmp_dir_path / DEFAULT_MOZ_CONFIG_LOCATION) + moz_config_location = os.path.abspath( + tmp_dir_path / DEFAULT_MOZ_CONFIG_LOCATION + ) with open(moz_config_location, "w") as file: file.write(DEFAULT_MOZ_CONFIG) - + if not os.path.isabs(moz_config_location): - err_msg(f"`mozconfig` path passed: `{moz_config_location}` must be an absolute path.") + err_msg( + f"`mozconfig` path passed: `{moz_config_location}` must be an absolute path." + ) return False if not os.path.isfile(moz_config_location): err_msg(f"`mozconfig` path passed: `{moz_config_location}` could not be found.") @@ -75,15 +97,23 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, # Basic sanity check here. Not remotely exhaustive, just to make sure the wrong directory wasn't passed. step_msg("Checking for sanity of firefox repository...") - if not dir_file_sanity_check(firefox_repo_path, "mozilla-central", ["mach", "CLOBBER", "gradlew", "Cargo.toml", "local.properties"]): + if not dir_file_sanity_check( + firefox_repo_path, + "mozilla-central", + ["mach", "CLOBBER", "gradlew", "Cargo.toml", "local.properties"], + ): return False # Key step: gradle can use a different application-services directory step_msg(f"Configuring {firefox_repo_path} to autopublish appservices") if not set_gradle_substitution_path( - firefox_repo_path, "autoPublish.application-services.dir", find_app_services_root() + firefox_repo_path, + "autoPublish.application-services.dir", + find_app_services_root(), ): - err_msg("Failed in attempting to set `local.properties` `autoPublish.application-services.dir`") + err_msg( + "Failed in attempting to set `local.properties` `autoPublish.application-services.dir`" + ) return False # Environment verification check @@ -93,47 +123,61 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, cwd=app_services_path, shell=True, stdout=subprocess_stdout, - stderr=subprocess_stderr + stderr=subprocess_stderr, ): - err_msg("Failed to run `./libs/verify-android-environment.sh` in app-services environment. Run this script and follow any instructions given until it succeeds, then try again.") + err_msg( + "Failed to run `./libs/verify-android-environment.sh` in app-services environment. Run this script and follow any instructions given until it succeeds, then try again." + ) return False # Gradle clean cached files if clear_bindings: - step_msg("Cleaning application-services with gradle to clear cached android bindings...") - if not run_cmd_is_successful(f"./gradlew {prefix_as_string}clean", + step_msg( + "Cleaning application-services with gradle to clear cached android bindings..." + ) + if not run_cmd_is_successful( + f"./gradlew {prefix_as_string}clean", cwd=app_services_path, shell=True, - stdout=subprocess_stdout + stdout=subprocess_stdout, ): - err_msg("Could not run ./gradlew clean. Please check to ensure the mozilla-center folder structure is sound.") + err_msg( + "Could not run ./gradlew clean. Please check to ensure the mozilla-center folder structure is sound." + ) return False # Run gradle compilations and tests step_msg("Compiling application-services with gradle to test android bindings...") - if not run_cmd_is_successful(f"./gradlew {prefix_as_string}assembleDebug", + if not run_cmd_is_successful( + f"./gradlew {prefix_as_string}assembleDebug", cwd=app_services_path, shell=True, - stdout=subprocess_stdout + stdout=subprocess_stdout, ): err_msg("Failed to compile application-services with gradle.") return False - step_msg(f"Compiling firefox with mozconfig with `./gradlew {prefix_ff_string}assembleDebug` (mozconfig=`{moz_config_location}`)...") - if not run_cmd_is_successful(f"MOZCONFIG={moz_config_location} ./gradlew {prefix_ff_string}assembleDebug", + step_msg( + f"Compiling firefox with mozconfig with `./gradlew {prefix_ff_string}assembleDebug` (mozconfig=`{moz_config_location}`)..." + ) + if not run_cmd_is_successful( + f"MOZCONFIG={moz_config_location} ./gradlew {prefix_ff_string}assembleDebug", cwd=firefox_repo_path, shell=True, - stdout=subprocess_stdout + stdout=subprocess_stdout, ): err_msg("Failed to compile firefox with gradle.") return False if action == "run-tests": - step_msg(f"Compiling firefox with mozconfig with `./gradlew {prefix_ff_string}testDebug` (mozconfig=`{moz_config_location}`)...") - if not run_cmd_is_successful(f"MOZCONFIG={moz_config_location} ./gradlew {prefix_ff_string}testDebug", + step_msg( + f"Compiling firefox with mozconfig with `./gradlew {prefix_ff_string}testDebug` (mozconfig=`{moz_config_location}`)..." + ) + if not run_cmd_is_successful( + f"MOZCONFIG={moz_config_location} ./gradlew {prefix_ff_string}testDebug", cwd=firefox_repo_path, shell=True, - stdout=subprocess_stdout + stdout=subprocess_stdout, ): err_msg("Failed to run tests against firefox with gradle.") return False @@ -146,7 +190,11 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, description="Run Firefox Android tests against this application-services working tree." ) - parser.add_argument("--verbose", help="Includes subprocess running logs.", action=argparse.BooleanOptionalAction) + parser.add_argument( + "--verbose", + help="Includes subprocess running logs.", + action=argparse.BooleanOptionalAction, + ) parser.add_argument( "--action", choices=["run-tests", "build-without-testing"], @@ -169,9 +217,11 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, "--prefix-as", help="Crate name to pass for preliminary application-services android building step. For example: `ads-client`, `fxaclient`", ) - parser.add_argument('--clear-previous-bindings', - help="Clear existing uniffi binding files from the firefox android build folder. (`/gradlew clean`). This shares any prefixes supplied by `--prefix-as`. If unrelated files need to be cleared, do not pass a --prefix-as argument", - action=argparse.BooleanOptionalAction) + parser.add_argument( + "--clear-previous-bindings", + help="Clear existing uniffi binding files from the firefox android build folder. (`/gradlew clean`). This shares any prefixes supplied by `--prefix-as`. If unrelated files need to be cleared, do not pass a --prefix-as argument", + action=argparse.BooleanOptionalAction, + ) args = parser.parse_args() firefox_dir = args.firefox_dir @@ -181,4 +231,12 @@ def build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, prefix_ff = args.prefix_ff prefix_as = args.prefix_as clear_bindings = args.clear_previous_bindings - build_against_fenix(firefox_dir, moz_config_location, prefix_ff, prefix_as, clear_bindings, verbose, action) \ No newline at end of file + build_against_fenix( + firefox_dir, + moz_config_location, + prefix_ff, + prefix_as, + clear_bindings, + verbose, + action, + ) diff --git a/automation/build_against_ios.py b/automation/build_against_ios.py index 9931db9930..9d8e007659 100755 --- a/automation/build_against_ios.py +++ b/automation/build_against_ios.py @@ -5,7 +5,7 @@ # Purpose: Run Firefox-iOS tests against this application-services working tree. # https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-firefox-ios.md -# Requirements: +# Requirements: # - python # - application-services built and working. # - xcpretty (`gem install xcpretty`) @@ -18,10 +18,10 @@ # --use-local-repo => Use a local firefox-ios repository instead (at the provided path). Exclusive with `remote-repo-url`. # --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) # --clear-previous-bindings => Clear existing uniffi binding swift files from both the iOS and A-S generated folders. Use if files were created that need to be cleared (eg: a file of a name that is no longer used). -# --clean-ios-caches => Runs the code equivalent of Xcode's 'Clean Build Folder' +# --clean-ios-caches => Runs the code equivalent of Xcode's 'Clean Build Folder' # --scheme => The XCode scheme to build with, such as 'Fennec' or 'Firefox' # --test-plan => The XCode test plan to run tests with (if action is `run-tests`), such as 'Smoketest' or 'FullFunctionalTestPlan' -# +# import argparse import subprocess import os @@ -30,12 +30,21 @@ import shutil import glob import re -from shared import fatal_err, find_app_services_root, run_cmd_checked, step_msg, err_msg, run_cmd_is_successful, dir_file_sanity_check +from shared import ( + fatal_err, + find_app_services_root, + run_cmd_checked, + step_msg, + err_msg, + run_cmd_is_successful, + dir_file_sanity_check, +) DEFAULT_REMOTE_REPO_URL = "https://github.com/mozilla-mobile/firefox-ios.git" MOZILLA_RUST_COMPONENTS_IOS_PATH = "MozillaRustComponents/Package.swift" MOZILLA_RUST_COMPONENTS_AS_PATH = "megazords/ios-rust/MozillaRustComponents.xcframework" + def replace_swift_package_artifact(ios_repo_path, as_repo_path): """ Replaces the local artifact pursuant to step 2 here. @@ -46,12 +55,18 @@ def replace_swift_package_artifact(ios_repo_path, as_repo_path): xcframework_file_path = Path(as_repo_path) / MOZILLA_RUST_COMPONENTS_AS_PATH if not os.path.isfile(package_file_path): - err_msg("Could not find an instance of `MozillaRustComponents/Package.swift` to modify. Please ensure the iOS directory is lined up correctly.") + err_msg( + "Could not find an instance of `MozillaRustComponents/Package.swift` to modify. Please ensure the iOS directory is lined up correctly." + ) return False if not os.path.isdir(xcframework_file_path): - err_msg(f"Could not find an instance of `MozillaRustComponents.xcframework` to link at {xcframework_file_path}. Please ensure the a-s directory is lined up correctly.") + err_msg( + f"Could not find an instance of `MozillaRustComponents.xcframework` to link at {xcframework_file_path}. Please ensure the a-s directory is lined up correctly." + ) return False - xcframework_file_path_relative = os.path.relpath(xcframework_file_path, Path(package_file_path.parent)) + xcframework_file_path_relative = os.path.relpath( + xcframework_file_path, Path(package_file_path.parent) + ) # Uses regex. Matches the binaryTarget listed and replaces it with the following string. replace_with = f""" @@ -61,7 +76,9 @@ def replace_swift_package_artifact(ios_repo_path, as_repo_path): ) """ step_msg(f"Writing to MozillaRustComponents/Package.swift:\n{replace_with}") - regex = re.compile(r'binaryTarget\([\n\s]*name\: \"MozillaRustComponents\",[\S\n\t\v ]*MozillaRustComponents\.xcframework\"[\n\s]*\)') + regex = re.compile( + r"binaryTarget\([\n\s]*name\: \"MozillaRustComponents\",[\S\n\t\v ]*MozillaRustComponents\.xcframework\"[\n\s]*\)" + ) with open(package_file_path, "r+") as f: data = f.read() @@ -73,7 +90,17 @@ def replace_swift_package_artifact(ios_repo_path, as_repo_path): return True -def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, clear_previous_bindings, clean_ios_caches, verbose, action): + +def build_against_ios( + local_ios_repo_path, + remote_repo_url, + scheme, + test_plan, + clear_previous_bindings, + clean_ios_caches, + verbose, + action, +): subprocess_stdout = None if verbose else subprocess.DEVNULL subprocess_stderr = None if verbose else subprocess.DEVNULL @@ -85,13 +112,16 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c # Naive sanity check here. Not remotely exhaustive, just to make sure some extremely incorrect directory wasn't passed. step_msg("Checking for sanity of application-services repository...") - if not dir_file_sanity_check(app_services_path, "application-services", ["megazords", "components"]): + if not dir_file_sanity_check( + app_services_path, "application-services", ["megazords", "components"] + ): return False - step_msg("Checking for existence of xcodebuild...") if not run_cmd_is_successful("xcodebuild -version", cwd=ios_repo_path, shell=True): - err_msg("xcodebuild is required to compile application-services for iOS. Please clone the firefox-ios repository and follow the instructions therein.") + err_msg( + "xcodebuild is required to compile application-services for iOS. Please clone the firefox-ios repository and follow the instructions therein." + ) return False # Creating temp directory and cloning repository @@ -108,14 +138,14 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c # Bootstrapping the iOS repository step_msg("Running the firefox-ios bootstrap script...") - if not run_cmd_is_successful("./bootstrap.sh", - cwd=ios_repo_path, - shell=True, - stdout=subprocess_stdout + if not run_cmd_is_successful( + "./bootstrap.sh", cwd=ios_repo_path, shell=True, stdout=subprocess_stdout ): - err_msg("Failed to bootstrap firefox-ios repository. Please clone the firefox-ios repository and follow the instructions therein.") + err_msg( + "Failed to bootstrap firefox-ios repository. Please clone the firefox-ios repository and follow the instructions therein." + ) return False - + # Verification check step_msg("Verifying iOS environment...") if not run_cmd_is_successful( @@ -123,19 +153,23 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c cwd=app_services_path, shell=True, stdout=subprocess_stdout, - stderr=subprocess_stderr + stderr=subprocess_stderr, ): - err_msg("Failed to verify environment for iOS. Please run `./libs/verify-ios-environment.sh`, making suggested changes until it succeeds.") - return False - + err_msg( + "Failed to verify environment for iOS. Please run `./libs/verify-ios-environment.sh`, making suggested changes until it succeeds." + ) + return False + # Uniffi sanity check if not os.path.isdir(ios_generated_uniffi_files_path): - err_msg(f"Expected path `{ios_generated_uniffi_files_path}` in firefox-ios is missing. Please confirm the repository structure or try cloning it again.") + err_msg( + f"Expected path `{ios_generated_uniffi_files_path}` in firefox-ios is missing. Please confirm the repository structure or try cloning it again." + ) return False if clear_previous_bindings: step_msg("'clear-previous-bindings' is set, clearing uniffi folders") - + # Clear the uniffi bindings in the A-S repository as extra files created are not deleted # Not relevant to tmp repository if os.path.isdir(local_repo_generated_uniffi_files_path): @@ -144,10 +178,18 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c # Clear equivalents from the ios repository # (Relevant if we are using an existing directory) - if not run_cmd_is_successful(["git", "checkout", "."], cwd=ios_generated_uniffi_files_path): - fatal_err("Found an error running git commands to clear previous uniffi folders. Exiting.") - if not run_cmd_is_successful(["git", "clean", "-f"], cwd=ios_generated_uniffi_files_path): - fatal_err("Found an error running git commands to clear previous uniffi folders. Exiting.") + if not run_cmd_is_successful( + ["git", "checkout", "."], cwd=ios_generated_uniffi_files_path + ): + fatal_err( + "Found an error running git commands to clear previous uniffi folders. Exiting." + ) + if not run_cmd_is_successful( + ["git", "clean", "-f"], cwd=ios_generated_uniffi_files_path + ): + fatal_err( + "Found an error running git commands to clear previous uniffi folders. Exiting." + ) # Build artifacts # Unfortunately build_ios_artifacts is writing to stderr, so we need to hide it when it's not verbose. @@ -158,11 +200,13 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c shell=True, check=True, stdout=subprocess_stdout, - stderr=subprocess_stderr + stderr=subprocess_stderr, ): - err_msg("Failed to build ios artifacts. Please ensure the code compiles and try running `./automation/build_ios_artifacts.sh` from the folder.") + err_msg( + "Failed to build ios artifacts. Please ensure the code compiles and try running `./automation/build_ios_artifacts.sh` from the folder." + ) return False - + # Replace with regex some data in the swift package file # https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-firefox-ios.md#step-2--point-firefox-ios-to-your-local-artifact if not replace_swift_package_artifact(ios_repo_path, app_services_path): @@ -170,16 +214,20 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c return False if not os.path.isdir(local_repo_generated_uniffi_files_path): - err_msg(f"Expected path `{local_repo_generated_uniffi_files_path}` in application-services is missing after building. Please confirm the repository structure or try cloning it again.") + err_msg( + f"Expected path `{local_repo_generated_uniffi_files_path}` in application-services is missing after building. Please confirm the repository structure or try cloning it again." + ) return False # Copies folder of uniffi bindings. step_msg("Copying uniffi bindings from:") - step_msg(f"{local_repo_generated_uniffi_files_path} -> {ios_generated_uniffi_files_path}") - for file in glob.glob('*.swift', root_dir=local_repo_generated_uniffi_files_path): + step_msg( + f"{local_repo_generated_uniffi_files_path} -> {ios_generated_uniffi_files_path}" + ) + for file in glob.glob("*.swift", root_dir=local_repo_generated_uniffi_files_path): shutil.copy( f"{local_repo_generated_uniffi_files_path}/{file}", - ios_generated_uniffi_files_path + ios_generated_uniffi_files_path, ) # Remove the glean_sym file. @@ -206,9 +254,11 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c """, cwd=ios_repo_path, shell=True, - stdout=subprocess_stdout + stdout=subprocess_stdout, ): - err_msg("Failed to clean and compile tests on iOS",) + err_msg( + "Failed to clean and compile tests on iOS", + ) return False # Run the build action @@ -226,12 +276,16 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c """, cwd=ios_repo_path, shell=True, - stdout=subprocess_stdout + stdout=subprocess_stdout, ): - err_msg("Failed to compile and run tests on iOS",) + err_msg( + "Failed to compile and run tests on iOS", + ) return False elif action == "run-tests": - step_msg("Building firefox-ios and running tests (this may take a few minutes)...") + step_msg( + "Building firefox-ios and running tests (this may take a few minutes)..." + ) if not run_cmd_is_successful( f"""\ set -o pipefail && \ @@ -245,13 +299,15 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c """, cwd=ios_repo_path, shell=True, - stdout=subprocess_stdout + stdout=subprocess_stdout, ): err_msg("Failed to compile and run tests on iOS") return False else: - err_msg("You must either run `--action run-tests` or `--action build-without-testing` ") + err_msg( + "You must either run `--action run-tests` or `--action build-without-testing` " + ) return False step_msg("Successfully built against iOS!") @@ -274,25 +330,33 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c help="Clone a different firefox-ios repository.", ) - parser.add_argument("--verbose", help="Includes subprocess running logs.", action=argparse.BooleanOptionalAction) - parser.add_argument('--clear-previous-bindings', - help="Clear existing uniffi binding swift files from both the iOS and A-S generated folders. Use if files were created that need to be cleared (eg: a file of a name that is no longer used).", - action=argparse.BooleanOptionalAction) + parser.add_argument( + "--verbose", + help="Includes subprocess running logs.", + action=argparse.BooleanOptionalAction, + ) + parser.add_argument( + "--clear-previous-bindings", + help="Clear existing uniffi binding swift files from both the iOS and A-S generated folders. Use if files were created that need to be cleared (eg: a file of a name that is no longer used).", + action=argparse.BooleanOptionalAction, + ) - parser.add_argument('--clean-ios-caches', - help="Run Xcode 'Clean Build Folder'", - action=argparse.BooleanOptionalAction) + parser.add_argument( + "--clean-ios-caches", + help="Run Xcode 'Clean Build Folder'", + action=argparse.BooleanOptionalAction, + ) parser.add_argument( "--scheme", help="The scheme to run. Likely: `Fennec` (default) or `Firefox`", - default="Fennec" + default="Fennec", ) parser.add_argument( "--test-plan", help="The test plan to test with. Likely: `Smoketest` (default) or `FullFunctionalTestPlan`", - default="Smoketest" + default="Smoketest", ) parser.add_argument( @@ -311,4 +375,13 @@ def build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, c verbose = args.verbose action = args.action - build_against_ios(local_ios_repo_path, remote_repo_url, scheme, test_plan, clear_previous_bindings, clean_ios_caches, verbose, action) \ No newline at end of file + build_against_ios( + local_ios_repo_path, + remote_repo_url, + scheme, + test_plan, + clear_previous_bindings, + clean_ios_caches, + verbose, + action, + ) From 01dc2e332dd5314e982f884338bcac153bfaacb2 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 20 Jul 2026 08:46:27 -0700 Subject: [PATCH 05/17] fix: Lints, docs --- automation/build_against_all.py | 1 - automation/build_against_fenix.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/automation/build_against_all.py b/automation/build_against_all.py index a89080fef5..75fd02aa24 100755 --- a/automation/build_against_all.py +++ b/automation/build_against_all.py @@ -15,7 +15,6 @@ # --use-local-repo => Use a local firefox-ios repository instead (at the provided path). Exclusive with `remote-repo-url`. # --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) # --allow-clears => Clear existing uniffi bindings, swift files, and so on during the various build processes. -# --action => Either 'run-tests' or 'build-without-testing import argparse import time diff --git a/automation/build_against_fenix.py b/automation/build_against_fenix.py index 5e65f0a8d2..8f88b2163a 100755 --- a/automation/build_against_fenix.py +++ b/automation/build_against_fenix.py @@ -23,6 +23,7 @@ # --prefix-ff => Prefix to be used in gradle commands to firefox repo. eg: `./gradlew fenix:assembleDebug`. For example: `geckoview`, `fenix`, `focus`." # --prefix-as => Crate prefix to be used in gradle commands to application-services repo. eg: `./gradlew ads-client:assembleDebug`. For example: `ads-client`, `fxaclient`." # --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) +# --clear-bindings => Whether or not to clear existing bindings and cached artifacts, such as with "./gradlew fenix:clean" import argparse import subprocess import os From 54c93919e888456765b5acffd7472ab9fb66ba6a Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 20 Jul 2026 08:49:59 -0700 Subject: [PATCH 06/17] fix: Adds deprecation notes --- automation/smoke-test-fenix.py | 2 +- automation/smoke-test-fxios.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/automation/smoke-test-fenix.py b/automation/smoke-test-fenix.py index c3afffb76e..55ea1f123b 100755 --- a/automation/smoke-test-fenix.py +++ b/automation/smoke-test-fenix.py @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. - +# DEPRECATED. Please consider using ./build_against_fenix.py # Purpose: Run Fenix tests against this application-services working tree. # Usage: ./automation/smoke-test-fenix.py diff --git a/automation/smoke-test-fxios.py b/automation/smoke-test-fxios.py index eb8df81183..246266a916 100755 --- a/automation/smoke-test-fxios.py +++ b/automation/smoke-test-fxios.py @@ -3,6 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +# DEPRECATED. Please consider using ./build_against_ios.py # Purpose: Run Firefox-iOS tests against this application-services working tree. # Usage: ./automation/smoke-test-fxios.py From a223a2fdd708d6034687adef913ba7ce9b7c03c3 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 20 Jul 2026 09:21:21 -0700 Subject: [PATCH 07/17] fix: Adds regex for fenix + docs --- automation/build_against_all.py | 6 ++-- automation/build_against_fenix.py | 46 +++++++++++++++++++++++++++++-- automation/build_against_ios.py | 2 +- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/automation/build_against_all.py b/automation/build_against_all.py index 75fd02aa24..a2e7e4e134 100755 --- a/automation/build_against_all.py +++ b/automation/build_against_all.py @@ -30,16 +30,16 @@ parser.add_argument( "--firefox-dir", required=True, - help="Path to existing mozilla-central directory.", + help="Path to existing bootstrapped `mozilla-central` directory.", ) parser.add_argument( "--verbose", - help="Includes subprocess running logs.", + help="Display subprocess logs for compilation processes (off by default).", action=argparse.BooleanOptionalAction, ) parser.add_argument( "--allow-clears", - help="Clear existing uniffi bindings, swift files, and so on during the various build processes.", + help="Clear existing uniffi bindings, swift files, and so on during the various build processes (what gets cleared varies per platform test).", action=argparse.BooleanOptionalAction, ) parser.add_argument( diff --git a/automation/build_against_fenix.py b/automation/build_against_fenix.py index 8f88b2163a..4d89ae2aba 100755 --- a/automation/build_against_fenix.py +++ b/automation/build_against_fenix.py @@ -29,6 +29,7 @@ import os import tempfile from pathlib import Path +import re from shared import ( find_app_services_root, set_gradle_substitution_path, @@ -42,9 +43,39 @@ DEFAULT_MOZ_CONFIG = """ ac_add_options --enable-project=mobile/android """ +MOZILLA_FF_GRADLE_PROPERTIES_PATH = "gradle.properties" + + +# Replaces org.gradle.configuration-cache=true with a commented version. +def comment_gradle_cache_line(firefox_repo_path): + """ + Comments out the gradle cache line pursuant to step 2 here. + https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-fenix.md#pre-requisites + """ + + properties_file_path = Path(firefox_repo_path) / MOZILLA_FF_GRADLE_PROPERTIES_PATH + if not os.path.isfile(properties_file_path): + err_msg( + "Could not find an instance of `gradle.properties` to modify. Please ensure the `m-c`/`firefox` directory is lined up correctly." + ) + return False + + # Uses regex. Matches the binaryTarget listed and replaces it with the following string. + replace_with = """# org.gradle.configuration-cache=true""" + step_msg(f"Writing to gradle.properties:\n{replace_with}") + regex = re.compile(r"^#*\s*org\.gradle\.configuration-cache=true\s*$", re.MULTILINE) + + with open(properties_file_path, "r+") as f: + data = f.read() + # Regex string matches this tidbit. + package_file = regex.sub(replace_with, data) + f.seek(0) + f.write(package_file) + f.truncate() + + return True -# TODO: Disable the gradle cache in mozilla-central - edit ./gradle.properties, comment out org.gradle.configuration-cache=true def build_against_fenix( firefox_dir, moz_config_location, @@ -105,6 +136,7 @@ def build_against_fenix( ): return False + # The following steps modify several property files in the m-c repository provided: # Key step: gradle can use a different application-services directory step_msg(f"Configuring {firefox_repo_path} to autopublish appservices") if not set_gradle_substitution_path( @@ -117,6 +149,14 @@ def build_against_fenix( ) return False + # Comments out gradle cache in local.properties + step_msg(f"Configuring {firefox_repo_path} to disable gradle configuration-cache") + if not comment_gradle_cache_line(firefox_repo_path): + err_msg( + "Failed in attempting to set `gradle.properties` `#org.gradle.configuration-cache=true`" + ) + return False + # Environment verification check step_msg("Verifying Android environment...") if not run_cmd_is_successful( @@ -193,7 +233,7 @@ def build_against_fenix( parser.add_argument( "--verbose", - help="Includes subprocess running logs.", + help="Display subprocess logs for compilation processes (off by default).", action=argparse.BooleanOptionalAction, ) parser.add_argument( @@ -204,7 +244,7 @@ def build_against_fenix( parser.add_argument( "--firefox-dir", required=True, - help="Path to existing mozilla-central directory.", + help="Path to existing bootstrapped `mozilla-central` directory.", ) parser.add_argument( "--mozconfig", diff --git a/automation/build_against_ios.py b/automation/build_against_ios.py index 9d8e007659..6ff050e2c7 100755 --- a/automation/build_against_ios.py +++ b/automation/build_against_ios.py @@ -332,7 +332,7 @@ def build_against_ios( parser.add_argument( "--verbose", - help="Includes subprocess running logs.", + help="Display subprocess logs for compilation processes (off by default).", action=argparse.BooleanOptionalAction, ) parser.add_argument( From de33222506a7c725df1609f8291c8c6b9394ac0b Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 20 Jul 2026 09:26:26 -0700 Subject: [PATCH 08/17] fix: Clarifies a todo --- automation/build_against_ios.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/automation/build_against_ios.py b/automation/build_against_ios.py index 6ff050e2c7..eebe3538a5 100755 --- a/automation/build_against_ios.py +++ b/automation/build_against_ios.py @@ -239,7 +239,8 @@ def build_against_ios( scheme = "Fennec" if scheme is None else scheme test_plan = "Smoketest" if test_plan is None else test_plan if clean_ios_caches: - # TODO: "Reset package caches" part not done yet + # TODO: "Reset package caches" part not done yet. Currently not a great script way to do it seemingly other + # than deleting ~/Library files, so needs examination for a code or xcodebuild based solution. # Clean build folder step_msg("Cleaning build folder...") From 119a717823a7e02cdaade4bb6e0735c73259e285 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 20 Jul 2026 09:46:28 -0700 Subject: [PATCH 09/17] fix: missed err_msg change --- automation/shared.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/automation/shared.py b/automation/shared.py index 3c1432a138..11d6a62994 100644 --- a/automation/shared.py +++ b/automation/shared.py @@ -18,11 +18,8 @@ def fatal_err(msg): err_msg(msg) sys.exit(1) -def err_msg(msg, allow_continue = False): +def err_msg(msg): print(f"\033[31mError: {msg}\033[0m") - if not allow_continue: - sys.exit(1) - def run_cmd_checked(*args, **kwargs): """Run a command, throwing an exception if it exits with non-zero status.""" From b2f472fb38ff4d309891502d7dba656722bd8715 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Wed, 22 Jul 2026 13:18:33 -0700 Subject: [PATCH 10/17] feat: Adds HNT tests, fixes some review issues --- automation/build_against_all.py | 74 ++++- automation/build_against_hnt.py | 289 ++++++++++++++++++ automation/build_against_ios.py | 28 +- ...lly-published-components-in-firefox-hnt.md | 95 ++++++ docs/howtos/smoke-testing-app-services.md | 28 +- 5 files changed, 486 insertions(+), 28 deletions(-) create mode 100755 automation/build_against_hnt.py create mode 100644 docs/howtos/locally-published-components-in-firefox-hnt.md diff --git a/automation/build_against_all.py b/automation/build_against_all.py index a2e7e4e134..8fcdb63b8d 100755 --- a/automation/build_against_all.py +++ b/automation/build_against_all.py @@ -11,14 +11,18 @@ # - xcpretty (`gem install xcpretty`) # - xcode + xcodebuild + xcodetools setup and running (a successful build of the firefox-ios repository) # Arguments: -# --action => Can be either `run-tests` (default) or `build-without-testing` -# --use-local-repo => Use a local firefox-ios repository instead (at the provided path). Exclusive with `remote-repo-url`. -# --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) -# --allow-clears => Clear existing uniffi bindings, swift files, and so on during the various build processes. - +# --action => Can be either `run-tests` (default) or `build-without-testing` +# --use_local_firefox_ios => Use a local firefox-ios repository instead (at the provided path). +# --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) +# --allow-clears => Clear existing uniffi bindings, swift files, and so on during the various build processes. +# --use-local-firefox-ios => Use a local copy of firefox-ios instead of cloning it for iOS tests. Exclusive with `remote-ios-repo-url` +# --remote-ios-repo-url => Clone a different firefox-ios repository for iOS tests. Exclusive with `use-local-firefox-ios` +# --ios-scheme => The scheme to run for iOS tests. Likely: `Fennec` (default) or `Firefox` +# --ios-test-plan => The test plan to test with for iOS tests. Likely: `Smoketest` (default) or `FullFunctionalTestPlan` import argparse import time from shared import err_msg, step_msg +from build_against_hnt import build_against_hnt from build_against_fenix import build_against_fenix from build_against_ios import build_against_ios @@ -49,19 +53,57 @@ help="Run the following action for target's test", ) +# iOS arguments to pass down +group = parser.add_mutually_exclusive_group() +group.add_argument( + "--use-local-firefox-ios", + metavar="LOCAL_IOS_REPO_PATH", + help="Use a local copy of firefox-ios instead of cloning it for iOS tests. Exclusive with `remote-ios-repo-url`", +) +group.add_argument( + "--remote-ios-repo-url", + metavar="REMOTE_REPO_PATH", + help="Clone a different firefox-ios repository for iOS tests. Exclusive with `use-local-firefox-ios`", +) +parser.add_argument( + "--ios-scheme", + help="The scheme to run for iOS tests. Likely: `Fennec` (default) or `Firefox`", + default="Fennec", +) +parser.add_argument( + "--ios-test-plan", + help="The test plan to test with for iOS tests. Likely: `Smoketest` (default) or `FullFunctionalTestPlan`", + default="Smoketest", +) + +# Fenix arguments to pass down +parser.add_argument( + "--prefix-ff", + help="Prefix name to pass to mozilla-central gradlew compilation to reduce the amount needing to build or test. For example: `geckoview`, `fenix`, `focus`.", + default="fenix", +) + + args = parser.parse_args() firefox_dir = args.firefox_dir verbose = args.verbose if args.verbose else False allow_clears = args.allow_clears action = args.action +local_firefox_ios = args.use_local_firefox_ios +remote_ios_repo_url = args.remote_ios_repo_url +ios_scheme = args.ios_scheme +ios_test_plan = args.ios_test_plan + +prefix_ff = args.prefix_ff + # Build against iOS start_time_ios = time.time() success_ios = build_against_ios( - None, - None, - scheme="Fennec", - test_plan="Smoketest", + local_firefox_ios, + remote_ios_repo_url, + ios_scheme, + ios_test_plan, clear_previous_bindings=allow_clears, clean_ios_caches=allow_clears, verbose=verbose, @@ -74,7 +116,7 @@ success_fenix = build_against_fenix( firefox_dir, None, - prefix_ff="fenix", + prefix_ff, prefix_as=None, clear_bindings=allow_clears, verbose=verbose, @@ -82,6 +124,10 @@ ) time_diff_fenix = time.time() - start_time_fenix +# Build against Desktop +start_time_hnt = time.time() +success_hnt = build_against_hnt(firefox_dir, None, True, verbose=verbose, action=action) +time_diff_hnt = time.time() - start_time_hnt did_tests_string = "" if action != "run-tests" else " (and tested)" do_tests_string = "" if action != "run-tests" else " (and test)" @@ -102,3 +148,11 @@ err_msg( f"Failed to build{do_tests_string} against Fenix (elapsed {time_diff_fenix:.2f}s)" ) +if success_hnt: + step_msg( + f"Successfully built{did_tests_string} against HNT (elapsed {time_diff_hnt:.2f}s)" + ) +else: + err_msg( + f"Failed to build{do_tests_string} against HNT (elapsed {time_diff_hnt:.2f}s)" + ) \ No newline at end of file diff --git a/automation/build_against_hnt.py b/automation/build_against_hnt.py new file mode 100755 index 0000000000..fabf85d970 --- /dev/null +++ b/automation/build_against_hnt.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +# Purpose: Run Firefox fenix tests against this application-services working tree. +# https://github.com/mozilla/application-services/blob/main/docs/howtos/locally-published-components-in-fenix.md +# So, for now, we need to use an existing respository. +# +# Requirements: +# - python +# - application-services built and working. +# - a `firefox`/`mozilla-central` repository set up and working to use. +# - See: https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html +# Usage: ./automation/build_against_hnt.py --action build-without-testing --firefox-dir ../firefox --verbose +# and arg to clean up only? +# Arguments: +# --action => Can be either `run-tests` (default) or `build-without-testing`, or `run` (which runs it locally with `./mach run`) +# --firefox-dir => Working mozilla-central directory +# https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html +# --mozconfig => Absolute path to the mozconfig file to be used. +# --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) +# --clean-up => Whether to perform the on-success cleanup step at the end of a successful build (default is True). This clean-up step happens either way on an error or graceful exit (such as with `--action run`). +import argparse +import subprocess +import os +import signal +import tempfile +import sys +from pathlib import Path +from shared import ( + find_app_services_root, + step_msg, + err_msg, + run_cmd_is_successful, + dir_file_sanity_check, +) + +DEFAULT_MOZ_CONFIG_LOCATION = "mozconfig_desktop" +DEFAULT_MOZ_CONFIG = """ +ac_add_options --enable-project=browser +""" +MOZILLA_FF_GRADLE_PROPERTIES_PATH = "gradle.properties" +COMPONENTS_FOLDER_AS_SUBPATH = "components" +COMPONENTS_FOLDER_MC_SUBPATH = "third_party/application-services/components" +COMPONENTS_FOLDER_MC_SUBPATH_TMP = "third_party/application-services/components_tmp" + + +# Catch sigint escape (for example, for long running tests) to still safely clean up the m-z directory +def safe_exit(firefox_repo_path): + step_msg("Exit signal caught, gracefully exiting...") + clean_up_func(firefox_repo_path) + step_msg("Exiting...") + sys.exit(0) + + +# Clean up symlinks/modified files that need to revert to their previous state +# Running this doesn't indicate something went wrong +def clean_up_func(firefox_repo_path): + symlink_src = firefox_repo_path / COMPONENTS_FOLDER_MC_SUBPATH + components_tmp_dir = firefox_repo_path / COMPONENTS_FOLDER_MC_SUBPATH_TMP + step_msg("Cleaning up, restoring symlinks...") + + # Test to see if we were interrupted/ended after making the symlink + try: + if os.path.islink(symlink_src) and os.path.isdir(components_tmp_dir): + os.unlink(symlink_src) + # if symlink does not exist (or no longer does) but components was moved, move it back + if os.path.isdir(components_tmp_dir): + os.rename(components_tmp_dir, symlink_src) + except OSError: + err_msg( + "Failed to restore the m-c state. Please remove the symlink and return the 'components' directory to it's intended spot." + ) + return False + return True + + +# External function handle cleanup on failure +def build_against_hnt( + firefox_dir, + moz_config_location, + clean_up, + verbose, + action, +): + # Run cleanup at start + firefox_repo_path = Path(firefox_dir) + clean_up_func(firefox_repo_path) + + # Catch sigint for graceful exit + signal.signal(signal.SIGINT, lambda _s, _h: safe_exit(firefox_repo_path)) + step_msg("Registered sigint trap...") + + if build_against_hnt_inner( + firefox_dir, + moz_config_location, + verbose, + action, + ): + step_msg("Finished running against HNT.") + else: + err_msg("Building against HNT failed.") + + if clean_up: + step_msg("Cleaning up...") + clean_up_func(firefox_repo_path) + else: + step_msg( + "Skipping cleanup step. Rerunning the command will cleanup before recompiling." + ) + + +def build_against_hnt_inner( + firefox_dir, + moz_config_location, + verbose, + action, +): + subprocess_stdout = None if verbose else subprocess.DEVNULL + subprocess_stderr = None if verbose else subprocess.DEVNULL + + if action is None: + action = "run-tests" + + firefox_repo_path = Path(firefox_dir) + tmp_dir_path = Path(tempfile.mkdtemp(suffix="-test-fenix")) + + app_services_path = find_app_services_root() + + step_msg("Checking for sanity of application-services repository...") + if not dir_file_sanity_check( + app_services_path, "application-services", ["megazords", "components"] + ): + return False + + # MOZCONFIG handling. + # Idea here is that mozconfig settings (primary indicator of how firefox is built) can't be passed + # without `configure`, which is not recommended. However, we can pass test fixture mozconfig files themselves as env variables. + if moz_config_location is None: + moz_config_location = os.path.abspath( + tmp_dir_path / DEFAULT_MOZ_CONFIG_LOCATION + ) + with open(moz_config_location, "w") as file: + file.write(DEFAULT_MOZ_CONFIG) + + if not os.path.isabs(moz_config_location): + err_msg( + f"`mozconfig` path passed: `{moz_config_location}` must be an absolute path." + ) + return False + if not os.path.isfile(moz_config_location): + err_msg(f"`mozconfig` path passed: `{moz_config_location}` could not be found.") + return False + step_msg(f"Using `mozconfig` path: `{moz_config_location}`. Displaying:") + with open(moz_config_location) as f: + print(f.read()) + + # Basic sanity check here. Not remotely exhaustive, just to make sure the wrong directory wasn't passed. + step_msg("Checking for sanity of firefox repository...") + if not dir_file_sanity_check( + firefox_repo_path, + "mozilla-central", + ["mach", "CLOBBER", "gradlew", "Cargo.toml", "local.properties"], + ): + return False + + # Environment verification check + step_msg("Verifying Desktop environment...") + if not run_cmd_is_successful( + "./libs/verify-desktop-environment.sh", + cwd=app_services_path, + shell=True, + stdout=subprocess_stdout, + stderr=subprocess_stderr, + ): + err_msg( + "Failed to run `./libs/verify-android-environment.sh` in app-services environment. Run this script and follow any instructions given until it succeeds, then try again." + ) + return False + + # The following steps *modify* a couple key parts of the m-c directory. + # TODO: cleanup function on failure + symlink_dest = app_services_path / COMPONENTS_FOLDER_AS_SUBPATH + symlink_src = firefox_repo_path / COMPONENTS_FOLDER_MC_SUBPATH + components_tmp_dir = firefox_repo_path / COMPONENTS_FOLDER_MC_SUBPATH_TMP + step_msg(f"Creating symlink {firefox_repo_path} to autopublish appservices") + + # First, move /components folder in m-c to a temporary backup. + os.rename(symlink_src, components_tmp_dir) + + # Then, create a symlink between the app-services/components and m-c/third_party/app-services folder. + os.symlink(symlink_dest, symlink_src) + + # We are pointing to a new area as if we vendored, so we regenerate. + step_msg("Regenerating uniffi bindings (mozconfig=`{moz_config_location}`)...") + if not run_cmd_is_successful( + "./mach uniffi generate", + cwd=firefox_repo_path, + shell=True, + stdout=subprocess_stdout, + ): + err_msg("Failed to generate uniffi bindings with: `./mach uniffi generate`/") + return False + + step_msg( + f"Compiling firefox with `./mach build` (mozconfig=`{moz_config_location}`)..." + ) + if not run_cmd_is_successful( + f"MOZCONFIG={moz_config_location} ./mach build", + cwd=firefox_repo_path, + shell=True, + stdout=subprocess_stdout, + ): + err_msg("Failed to compile firefox with `./mach build`.") + return False + + if action == "run-tests": + # TODO: Need a more specific test suite here + step_msg( + f"Compiling firefox with mozconfig with `./mach test --auto` (mozconfig=`{moz_config_location}`)..." + ) + if not run_cmd_is_successful( + f"MOZCONFIG={moz_config_location} ./mach test --auto", + cwd=firefox_repo_path, + shell=True, + stdout=subprocess_stdout, + ): + err_msg("Failed to run tests against firefox with ./mach test --auto.") + return False + elif action == "run": + # TODO: do this for the rest of them? or some other alternative? + # or 'no-clean-up' version + step_msg( + f"Running firefox with mozconfig with `./mach run` (mozconfig=`{moz_config_location}`)..." + ) + # TODO: a different gutter for sigint? + if not run_cmd_is_successful( + f"MOZCONFIG={moz_config_location} ./mach run", + cwd=firefox_repo_path, + shell=True, + stdout=subprocess_stdout, + ): + err_msg("Failed to run tests against firefox with ./mach run.") + return False + + step_msg("Successfully built against HNT!") + return True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Run Firefox HNT tests against this application-services working tree." + ) + + parser.add_argument( + "--verbose", + help="Display subprocess logs for compilation processes (off by default).", + action=argparse.BooleanOptionalAction, + ) + parser.add_argument( + "--action", + choices=["run", "run-tests", "build-without-testing"], + help="Whether to run tests after the build step is complete..", + ) + parser.add_argument( + "--firefox-dir", + required=True, + help="Path to existing bootstrapped `mozilla-central` directory.", + ) + parser.add_argument( + "--mozconfig", + help="Absolute path to the desired mozconfig file. This affects the build destination, ensure it specifies android if you override it.", + ) + + parser.add_argument( + "--clean-up", + help="Skip the on-success cleanup step done at the end of a successful build. This does not skip the cleanup step if there is an error or graceful exit (such as with `--action run`).", + action=argparse.BooleanOptionalAction, + default=True, + ) + + args = parser.parse_args() + firefox_dir = args.firefox_dir + verbose = args.verbose + moz_config_location = args.mozconfig + action = args.action + clean_up = args.clean_up + build_against_hnt(firefox_dir, moz_config_location, clean_up, verbose, action) diff --git a/automation/build_against_ios.py b/automation/build_against_ios.py index eebe3538a5..d7c4514160 100755 --- a/automation/build_against_ios.py +++ b/automation/build_against_ios.py @@ -14,8 +14,8 @@ # Usage: ./automation/build_against_ios.py # Arguments: # --action => Can be either `run-tests` (default) or `build-without-testing` -# --remote-repo-url => Fetch firefox-ios repository from this URL instead. Exclusive with `use-local-repo` -# --use-local-repo => Use a local firefox-ios repository instead (at the provided path). Exclusive with `remote-repo-url`. +# --remote-ios-repo-url => Fetch firefox-ios repository from this URL instead. Exclusive with `use-local-firefox-ios` +# --use-local-firefox-ios => Use a local firefox-ios repository instead (at the provided path). Exclusive with `remote-ios-repo-url`. # --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) # --clear-previous-bindings => Clear existing uniffi binding swift files from both the iOS and A-S generated folders. Use if files were created that need to be cleared (eg: a file of a name that is no longer used). # --clean-ios-caches => Runs the code equivalent of Xcode's 'Clean Build Folder' @@ -93,7 +93,7 @@ def replace_swift_package_artifact(ios_repo_path, as_repo_path): def build_against_ios( local_ios_repo_path, - remote_repo_url, + remote_ios_repo_url, scheme, test_plan, clear_previous_bindings, @@ -128,10 +128,10 @@ def build_against_ios( step_msg(f"Building application-services against iOS with action: `{action}`") if local_ios_repo_path is None: ios_repo_path = tempfile.mkdtemp(suffix="-test-ios") - if remote_repo_url is None: - remote_repo_url = DEFAULT_REMOTE_REPO_URL - step_msg(f"Cloning {remote_repo_url}") - run_cmd_checked(["git", "clone", remote_repo_url, ios_repo_path]) + if remote_ios_repo_url is None: + remote_ios_repo_url = DEFAULT_REMOTE_REPO_URL + step_msg(f"Cloning {remote_ios_repo_url}") + run_cmd_checked(["git", "clone", remote_ios_repo_url, ios_repo_path]) ios_generated_uniffi_files_path = f"{ios_repo_path}/MozillaRustComponents/Sources/MozillaRustComponentsWrapper/Generated" local_repo_generated_uniffi_files_path = f"{app_services_path}/megazords/ios-rust/Sources/MozillaRustComponentsWrapper/Generated" @@ -321,14 +321,14 @@ def build_against_ios( ) group = parser.add_mutually_exclusive_group() group.add_argument( - "--use-local-repo", + "--use-local-firefox-ios", metavar="LOCAL_IOS_REPO_PATH", - help="Use a local copy of firefox-ios instead of cloning it.", + help="Use a local copy of firefox-ios instead of cloning it. Exclusive with `remote-ios-repo-url`", ) group.add_argument( - "--remote-repo-url", + "--remote-ios-repo-url", metavar="REMOTE_REPO_PATH", - help="Clone a different firefox-ios repository.", + help="Clone a different firefox-ios repository. Exclusive with `use-local-firefox-ios`", ) parser.add_argument( @@ -367,8 +367,8 @@ def build_against_ios( ) args = parser.parse_args() - local_ios_repo_path = args.use_local_repo - remote_repo_url = args.remote_repo_url + local_ios_repo_path = args.use_local_firefox_ios + remote_ios_repo_url = args.remote_ios_repo_url clear_previous_bindings = args.clear_previous_bindings clean_ios_caches = args.clean_ios_caches scheme = args.scheme @@ -378,7 +378,7 @@ def build_against_ios( build_against_ios( local_ios_repo_path, - remote_repo_url, + remote_ios_repo_url, scheme, test_plan, clear_previous_bindings, diff --git a/docs/howtos/locally-published-components-in-firefox-hnt.md b/docs/howtos/locally-published-components-in-firefox-hnt.md new file mode 100644 index 0000000000..a9a73853d7 --- /dev/null +++ b/docs/howtos/locally-published-components-in-firefox-hnt.md @@ -0,0 +1,95 @@ +# How to locally test application-services components on HNT / Desktop + +> This guide explains how to build and test **HNT against a local Application Services** checkout. + +--- +# TODO: may not work on windows +# TODO: add a note about the other strategy +## At a glance + +**Goal:** Build a local Firefox Desktop against a local Application Services. + +**Current workflow (recommended):** + +# TODO: Resummarize this + +1. Build an **XCFramework** from your local `application-services`. +2. Point **Firefox iOS’s local Swift package** (`MozillaRustComponents/Package.swift`) at that artifact (either an HTTPS URL + checksum, **or** a local `path:`). +3. Update UniFFI generated swift source files. +3. Reset package caches in Xcode and build Firefox iOS. + +A legacy flow that uses the **`rust-components-swift`** package is documented at the end while we're in mid-transition to the new system. + +--- + +## Prerequisites + +1. Ensure you have a regular [build of application-services working](../building.md). +2. Ensure you have a regular [build of firefox from mozilla-central](https://firefox-source-docs.mozilla.org/setup/index.html#for-firefox-desktop) testable with `./mach build` and ./mach run + + +--- + +## Step 1 — Verify the local build of A-S is ready for a desktop build. + +From the root of your `application-services` checkout, execute: + +```bash +./libs/verify-desktop-environment.sh +``` + +This will check for environment variables. If it provides any instruction on environment variables to set, follow the instructions until it passes. + + +## Step 2 - Move the A-S components folder in M-C/Firefox to a temporary rename + +We will be temporarily replacing the components in the `application-services` repository in `mozilla-central` with a symlink that points to our local `application-services` build. To conserve the old folder, we temporarily rename it. From the **mozilla-central** root. + +```bash +mv third_party/application-services/components third_party/application-services/components-tmp +``` + +## Step 3 - Create a symlink between the A-S and M-C components folders. + +Now, the former `components` path should have a symlink to the local `application-services` components. Assuming `application-services` is in the same folder as your `mozilla-central` checkout, you can run (from the **mozilla-central** root): + +```bash +ln -s $(realpath ../application-services/components) third_party/application-services/components +``` + +## Step 4 - Generate uniffi bindings. + +You may need to regenerate uniffi bindings, as if you vendored new `A-S` code. From the mozilla-central root: + +```bash +./mach uniffi generate +``` + + +## Step 5 - Run and build! + +Now that `components` will read from your local build, you can build, run, and test. From your local m-c checkout, run: + +```bash +./mach build +``` + +And if so desired: +```bash +./mach run +``` + +## Step 6 - Cleanup + +After completing your tests, you should revert your files and symlinks to ensure `m-c` continues to behave as expected: + +```bash +unlink third_party/application-services/components +mv third_party/application-services/components-tmp third_party/application-services/components +``` + + +# Automated testing + +You can also automate this process by running the Desktop smoke test found at `automation/build_against_hnt.py`. You can see more detailed instructions about this [here](./smoke-testing-app-services.md). + diff --git a/docs/howtos/smoke-testing-app-services.md b/docs/howtos/smoke-testing-app-services.md index 61f778ab71..c33787b174 100644 --- a/docs/howtos/smoke-testing-app-services.md +++ b/docs/howtos/smoke-testing-app-services.md @@ -14,20 +14,26 @@ You can easily run a smoke test against iOS and Fenix by running the following: `./automation/build_against_all.py --firefox-dir ../firefox --action build-without-testing --allow-clears` -In this case, `firefox-dir` must point to a bootstrapped and working installation of `mozilla-central` (see instructions [here](https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html)). It is used for the compilation and test of the Android build. +- In this case, `firefox-dir` must point to a bootstrapped and working installation of `mozilla-central` (see instructions [here](https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html)). It is used for the compilation and test of the Android and HNT builds. -You can also run against specific platforms with the following examples: +- The `--action` argument can be either `build-without-testing` or `run-tests`. + +- The `--allow-clears` argument allows the various subscripts to clear their various caches as appropriate (such as XCode cleaning iOS caches). + +You can also run against specific platforms directly with the following examples: - iOS: - ```./automation/build_against_ios.py --action build-without-testing --clear-previous-bindings --clean-ios-caches --use-local-repo ../firefox-ios``` + ```./automation/build_against_ios.py --action build-without-testing --clear-previous-bindings --clean-ios-caches --use-local-firefox-ios ../firefox-ios``` - - By default, this script creates a `tmp` directory for `firefox-ios` and uses it. If you have a running `firefox-ios`, you can add the argument `--use-local-repo ../firefox-ios` (as shown), which may result in speed gains and the ability to run it on XCode immediately after a failure. + - By default, this script creates a `tmp` directory for `firefox-ios` and uses it. If you have a running `firefox-ios`, you can add the optional argument `--use-local-firefox-ios ../firefox-ios` (as shown), which may result in speed gains and the ability to run it on XCode immediately after a failure. This can also be passed to `./build_against_all.py`. - You can also customize the run scheme (`--scheme`) or test plan (`--test-plan`). Available schemes include `Fennec` (default) and `Firefox`. Available test plans include: `Smoketest`, `FullFunctionalTestPlan` and `UnitTest`. - A full list of schemes and their corresponding test plans can be found [in the firefox-ios](https://github.com/mozilla-mobile/firefox-ios/tree/main/firefox-ios/Client.xcodeproj/xcshareddata/xcschemes) respository. + - These can be used in `./build_against_all.py` as `--ios-scheme` and `--ios-test-plan` respectively. + - Fenix: ```./automation/build_against_fenix.py --action build-without-testing --firefox-dir ../firefox --prefix-ff fenix --clear-previous-bindings``` @@ -36,8 +42,22 @@ You can also run against specific platforms with the following examples: - The `--clear-previous-bindings` argument here runs a `./gradlew prefix:clear` before recompiling. It is not always necessary, and can be excluded for some speed gains, but can result in some cache reuse. +- HNT: + + ``` ./automation/build_against_hnt.py --action build-without-testing --firefox-dir ../firefox``` + + - This script uses the symlink method described [here](./locally-published-components-in-firefox-hnt.md), cleaning up after a successful run. You can avoid this cleanup process (specifically on a successful run) by passing `--no-clean-up`, which will keep the symlinks. For example, you might run with `--action build-without-testing --no-clean-up` to experiment after with `./mach run`. + + - Unlike the other tests, HNT has the additional `action` variant of `--action run`, because it can be run from the terminal directly. + + All test scripts also accept the `--verbose` argument to show the output of run subprocesses (such as `./mach build`). + +## Limitations + +Note that these tests are primarily smoke tests against the building and compilation of application-services. There are a wide array of possible regressions that can only be caught with tests, including ones that crash the build immediately on running. To ensure any regressions for your component are caught, tests should be created for them rather than just building. + ## Deprecated tests ### Android Components From a5a8e2071c337e8ef2af37296db4318451dae355 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Wed, 22 Jul 2026 14:23:27 -0700 Subject: [PATCH 11/17] fix: Some missing docs --- automation/build_against_hnt.py | 7 +++-- ...lly-published-components-in-firefox-hnt.md | 26 +++++++------------ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/automation/build_against_hnt.py b/automation/build_against_hnt.py index fabf85d970..042e4a175a 100755 --- a/automation/build_against_hnt.py +++ b/automation/build_against_hnt.py @@ -92,12 +92,14 @@ def build_against_hnt( signal.signal(signal.SIGINT, lambda _s, _h: safe_exit(firefox_repo_path)) step_msg("Registered sigint trap...") - if build_against_hnt_inner( + success = build_against_hnt_inner( firefox_dir, moz_config_location, verbose, action, - ): + ) + + if success: step_msg("Finished running against HNT.") else: err_msg("Building against HNT failed.") @@ -110,6 +112,7 @@ def build_against_hnt( "Skipping cleanup step. Rerunning the command will cleanup before recompiling." ) + return success def build_against_hnt_inner( firefox_dir, diff --git a/docs/howtos/locally-published-components-in-firefox-hnt.md b/docs/howtos/locally-published-components-in-firefox-hnt.md index a9a73853d7..80211dcd59 100644 --- a/docs/howtos/locally-published-components-in-firefox-hnt.md +++ b/docs/howtos/locally-published-components-in-firefox-hnt.md @@ -3,34 +3,30 @@ > This guide explains how to build and test **HNT against a local Application Services** checkout. --- -# TODO: may not work on windows -# TODO: add a note about the other strategy + ## At a glance **Goal:** Build a local Firefox Desktop against a local Application Services. **Current workflow (recommended):** -# TODO: Resummarize this - -1. Build an **XCFramework** from your local `application-services`. -2. Point **Firefox iOS’s local Swift package** (`MozillaRustComponents/Package.swift`) at that artifact (either an HTTPS URL + checksum, **or** a local `path:`). -3. Update UniFFI generated swift source files. -3. Reset package caches in Xcode and build Firefox iOS. - -A legacy flow that uses the **`rust-components-swift`** package is documented at the end while we're in mid-transition to the new system. +1. Verify the local build of `A-S` is ready for a desktop build. +2. Move the A-S components folder in M-C/Firefox to a temporary rename. +3. Create a symlink between the A-S and M-C components folders. +4. Generate uniffi bindings. +5. Run and build. +6. Cleanup of symlink and temporary renames. --- ## Prerequisites 1. Ensure you have a regular [build of application-services working](../building.md). -2. Ensure you have a regular [build of firefox from mozilla-central](https://firefox-source-docs.mozilla.org/setup/index.html#for-firefox-desktop) testable with `./mach build` and ./mach run - +2. Ensure you have a regular [build of firefox from mozilla-central](https://firefox-source-docs.mozilla.org/setup/index.html#for-firefox-desktop) testable with `./mach build` and `./mach run`. --- -## Step 1 — Verify the local build of A-S is ready for a desktop build. +## Step 1 — Verify the local build of A-S is ready for a desktop build From the root of your `application-services` checkout, execute: @@ -65,7 +61,6 @@ You may need to regenerate uniffi bindings, as if you vendored new `A-S` code. F ./mach uniffi generate ``` - ## Step 5 - Run and build! Now that `components` will read from your local build, you can build, run, and test. From your local m-c checkout, run: @@ -75,6 +70,7 @@ Now that `components` will read from your local build, you can build, run, and t ``` And if so desired: + ```bash ./mach run ``` @@ -88,8 +84,6 @@ unlink third_party/application-services/components mv third_party/application-services/components-tmp third_party/application-services/components ``` - # Automated testing You can also automate this process by running the Desktop smoke test found at `automation/build_against_hnt.py`. You can see more detailed instructions about this [here](./smoke-testing-app-services.md). - From a82029aa1534f88a623a8a6454f559185f658a0b Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Wed, 22 Jul 2026 14:26:12 -0700 Subject: [PATCH 12/17] fix: Small edits --- docs/howtos/locally-published-components-in-firefox-hnt.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/howtos/locally-published-components-in-firefox-hnt.md b/docs/howtos/locally-published-components-in-firefox-hnt.md index 80211dcd59..1a22df7bcf 100644 --- a/docs/howtos/locally-published-components-in-firefox-hnt.md +++ b/docs/howtos/locally-published-components-in-firefox-hnt.md @@ -36,7 +36,6 @@ From the root of your `application-services` checkout, execute: This will check for environment variables. If it provides any instruction on environment variables to set, follow the instructions until it passes. - ## Step 2 - Move the A-S components folder in M-C/Firefox to a temporary rename We will be temporarily replacing the components in the `application-services` repository in `mozilla-central` with a symlink that points to our local `application-services` build. To conserve the old folder, we temporarily rename it. From the **mozilla-central** root. @@ -84,6 +83,6 @@ unlink third_party/application-services/components mv third_party/application-services/components-tmp third_party/application-services/components ``` -# Automated testing +## Automated testing -You can also automate this process by running the Desktop smoke test found at `automation/build_against_hnt.py`. You can see more detailed instructions about this [here](./smoke-testing-app-services.md). +You can also automate this process by running the Desktop smoke test found at `automation/build_against_hnt.py`. You can see more detailed instructions about this [at the smoke testing guide](./smoke-testing-app-services.md). From 3174384785ac5f9e39057c667a79fa86e2ef9199 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Wed, 22 Jul 2026 16:22:57 -0700 Subject: [PATCH 13/17] fix: adds hnt test --- automation/build_against_hnt.py | 21 ++++++++++++++------- automation/build_against_ios.py | 12 ++++++------ docs/howtos/smoke-testing-app-services.md | 7 +++++-- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/automation/build_against_hnt.py b/automation/build_against_hnt.py index 042e4a175a..5b4fc195bd 100755 --- a/automation/build_against_hnt.py +++ b/automation/build_against_hnt.py @@ -21,6 +21,7 @@ # --mozconfig => Absolute path to the mozconfig file to be used. # --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) # --clean-up => Whether to perform the on-success cleanup step at the end of a successful build (default is True). This clean-up step happens either way on an error or graceful exit (such as with `--action run`). +# --hnt-test => Test name to run with `./mach test`. If `run-tests` is attached, but no `--test` is provided, the default command will be `./mach test --auto` where appropriate tests will be guessed. import argparse import subprocess import os @@ -81,6 +82,7 @@ def build_against_hnt( firefox_dir, moz_config_location, clean_up, + hnt_test, verbose, action, ): @@ -95,6 +97,7 @@ def build_against_hnt( success = build_against_hnt_inner( firefox_dir, moz_config_location, + hnt_test, verbose, action, ) @@ -117,6 +120,7 @@ def build_against_hnt( def build_against_hnt_inner( firefox_dir, moz_config_location, + hnt_test, verbose, action, ): @@ -219,12 +223,13 @@ def build_against_hnt_inner( return False if action == "run-tests": - # TODO: Need a more specific test suite here step_msg( - f"Compiling firefox with mozconfig with `./mach test --auto` (mozconfig=`{moz_config_location}`)..." + f"Compiling firefox with mozconfig with `./mach test` (mozconfig=`{moz_config_location}`)..." ) + test_string = hnt_test if hnt_test is not None else "--auto" + step_msg(f"Running test command `./mach test {test_string}`") if not run_cmd_is_successful( - f"MOZCONFIG={moz_config_location} ./mach test --auto", + f"MOZCONFIG={moz_config_location} ./mach test {test_string}", cwd=firefox_repo_path, shell=True, stdout=subprocess_stdout, @@ -232,12 +237,9 @@ def build_against_hnt_inner( err_msg("Failed to run tests against firefox with ./mach test --auto.") return False elif action == "run": - # TODO: do this for the rest of them? or some other alternative? - # or 'no-clean-up' version step_msg( f"Running firefox with mozconfig with `./mach run` (mozconfig=`{moz_config_location}`)..." ) - # TODO: a different gutter for sigint? if not run_cmd_is_successful( f"MOZCONFIG={moz_config_location} ./mach run", cwd=firefox_repo_path, @@ -275,6 +277,10 @@ def build_against_hnt_inner( "--mozconfig", help="Absolute path to the desired mozconfig file. This affects the build destination, ensure it specifies android if you override it.", ) + parser.add_argument( + "--hnt-test", + help="Name of the test file to run, as if you were running `./mach test ARG`.", + ) parser.add_argument( "--clean-up", @@ -289,4 +295,5 @@ def build_against_hnt_inner( moz_config_location = args.mozconfig action = args.action clean_up = args.clean_up - build_against_hnt(firefox_dir, moz_config_location, clean_up, verbose, action) + hnt_test = args.hnt_test + build_against_hnt(firefox_dir, moz_config_location, clean_up, hnt_test, verbose, action) diff --git a/automation/build_against_ios.py b/automation/build_against_ios.py index d7c4514160..086ff5aa9f 100755 --- a/automation/build_against_ios.py +++ b/automation/build_against_ios.py @@ -19,8 +19,8 @@ # --verbose => Includes the stdout of subprocesses (like the xcodebuild output, or other bootstrapping scripts) # --clear-previous-bindings => Clear existing uniffi binding swift files from both the iOS and A-S generated folders. Use if files were created that need to be cleared (eg: a file of a name that is no longer used). # --clean-ios-caches => Runs the code equivalent of Xcode's 'Clean Build Folder' -# --scheme => The XCode scheme to build with, such as 'Fennec' or 'Firefox' -# --test-plan => The XCode test plan to run tests with (if action is `run-tests`), such as 'Smoketest' or 'FullFunctionalTestPlan' +# --ios-scheme => The XCode scheme to build with, such as 'Fennec' or 'Firefox' +# --ios-test-plan => The XCode test plan to run tests with (if action is `run-tests`), such as 'Smoketest' or 'FullFunctionalTestPlan' # import argparse import subprocess @@ -349,13 +349,13 @@ def build_against_ios( ) parser.add_argument( - "--scheme", + "--ios-scheme", help="The scheme to run. Likely: `Fennec` (default) or `Firefox`", default="Fennec", ) parser.add_argument( - "--test-plan", + "--ios-test-plan", help="The test plan to test with. Likely: `Smoketest` (default) or `FullFunctionalTestPlan`", default="Smoketest", ) @@ -371,8 +371,8 @@ def build_against_ios( remote_ios_repo_url = args.remote_ios_repo_url clear_previous_bindings = args.clear_previous_bindings clean_ios_caches = args.clean_ios_caches - scheme = args.scheme - test_plan = args.test_plan + scheme = args.ios_scheme + test_plan = args.ios_test_plan verbose = args.verbose action = args.action diff --git a/docs/howtos/smoke-testing-app-services.md b/docs/howtos/smoke-testing-app-services.md index c33787b174..13d598cbf8 100644 --- a/docs/howtos/smoke-testing-app-services.md +++ b/docs/howtos/smoke-testing-app-services.md @@ -28,11 +28,11 @@ You can also run against specific platforms directly with the following examples - By default, this script creates a `tmp` directory for `firefox-ios` and uses it. If you have a running `firefox-ios`, you can add the optional argument `--use-local-firefox-ios ../firefox-ios` (as shown), which may result in speed gains and the ability to run it on XCode immediately after a failure. This can also be passed to `./build_against_all.py`. - - You can also customize the run scheme (`--scheme`) or test plan (`--test-plan`). Available schemes include `Fennec` (default) and `Firefox`. Available test plans include: `Smoketest`, `FullFunctionalTestPlan` and `UnitTest`. + - You can also customize the run scheme (`--ios-scheme`) or test plan (`--ios-test-plan`). Available schemes include `Fennec` (default) and `Firefox`. Available test plans include: `Smoketest`, `FullFunctionalTestPlan` and `UnitTest`. - A full list of schemes and their corresponding test plans can be found [in the firefox-ios](https://github.com/mozilla-mobile/firefox-ios/tree/main/firefox-ios/Client.xcodeproj/xcshareddata/xcschemes) respository. - - These can be used in `./build_against_all.py` as `--ios-scheme` and `--ios-test-plan` respectively. + - Both of these arguments can be used in `./build_against_all.py`, and they will be passed to the underlying `build_against_ios.py` call. - Fenix: @@ -50,6 +50,8 @@ You can also run against specific platforms directly with the following examples - Unlike the other tests, HNT has the additional `action` variant of `--action run`, because it can be run from the terminal directly. + - You can pass `--hnt-test` (eg: `--hnt-test dom/notification`) to pass a set of tests to use if the `action` argument is `run-tests`. Otherwise, `./mach test --auto` will be used, which takes a guess at which tests would be best to run. You can pass `--hnt-test` to the `build_against_all.py` script as well. + All test scripts also accept the `--verbose` argument to show the output of run subprocesses (such as `./mach build`). @@ -72,5 +74,6 @@ The `automation/smoke-test-fenix.py` script will clone (or use a local version) run tests against the current `application-services` worktree. ### Firefox iOS + The `automation/smoke-test-fxios.py` script will clone (or use a local version) of Firefox iOS and run tests against the current `application-services` worktree. From 203c371a2ed7f98cacd1fa9fff57d9629c6e9976 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Wed, 22 Jul 2026 16:26:10 -0700 Subject: [PATCH 14/17] fix: Build against all --- automation/build_against_all.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/automation/build_against_all.py b/automation/build_against_all.py index 8fcdb63b8d..a99801f48c 100755 --- a/automation/build_against_all.py +++ b/automation/build_against_all.py @@ -76,6 +76,13 @@ default="Smoketest", ) +# HNT argument to pass down +parser.add_argument( + "--hnt-test", + help="Name of the test file to run, as if you were running `./mach test ARG`.", +) + + # Fenix arguments to pass down parser.add_argument( "--prefix-ff", @@ -95,6 +102,8 @@ ios_scheme = args.ios_scheme ios_test_plan = args.ios_test_plan +hnt_test = args.hnt_test + prefix_ff = args.prefix_ff # Build against iOS @@ -126,7 +135,7 @@ # Build against Desktop start_time_hnt = time.time() -success_hnt = build_against_hnt(firefox_dir, None, True, verbose=verbose, action=action) +success_hnt = build_against_hnt(firefox_dir, None, True, hnt_test=hnt_test, verbose=verbose, action=action) time_diff_hnt = time.time() - start_time_hnt did_tests_string = "" if action != "run-tests" else " (and tested)" From fd689b0367a0ca866fef58e23c7d4580942d921b Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Wed, 22 Jul 2026 18:31:28 -0700 Subject: [PATCH 15/17] fix: Small log correction --- automation/build_against_hnt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automation/build_against_hnt.py b/automation/build_against_hnt.py index 5b4fc195bd..5ae6f1d867 100755 --- a/automation/build_against_hnt.py +++ b/automation/build_against_hnt.py @@ -234,7 +234,7 @@ def build_against_hnt_inner( shell=True, stdout=subprocess_stdout, ): - err_msg("Failed to run tests against firefox with ./mach test --auto.") + err_msg(f"Failed to run tests against firefox with ./mach test {test_string}.") return False elif action == "run": step_msg( From 38258005360ac1e0029b6fb1ae514bfad99bb5fe Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Thu, 23 Jul 2026 09:27:23 -0700 Subject: [PATCH 16/17] fix: some doc changes --- automation/build_against_hnt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/automation/build_against_hnt.py b/automation/build_against_hnt.py index 5ae6f1d867..b579e7d41e 100755 --- a/automation/build_against_hnt.py +++ b/automation/build_against_hnt.py @@ -187,11 +187,10 @@ def build_against_hnt_inner( return False # The following steps *modify* a couple key parts of the m-c directory. - # TODO: cleanup function on failure symlink_dest = app_services_path / COMPONENTS_FOLDER_AS_SUBPATH symlink_src = firefox_repo_path / COMPONENTS_FOLDER_MC_SUBPATH components_tmp_dir = firefox_repo_path / COMPONENTS_FOLDER_MC_SUBPATH_TMP - step_msg(f"Creating symlink {firefox_repo_path} to autopublish appservices") + step_msg(f"Creating symlink in {firefox_repo_path} to link to local appservices") # First, move /components folder in m-c to a temporary backup. os.rename(symlink_src, components_tmp_dir) From 29f33aeeb371da84f21e9b2789771926f838c0c8 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Fri, 24 Jul 2026 12:53:32 -0700 Subject: [PATCH 17/17] fix: some readme linting --- docs/howtos/smoke-testing-app-services.md | 37 +++++++++++------------ 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/howtos/smoke-testing-app-services.md b/docs/howtos/smoke-testing-app-services.md index 13d598cbf8..92ffe07e8c 100644 --- a/docs/howtos/smoke-testing-app-services.md +++ b/docs/howtos/smoke-testing-app-services.md @@ -7,58 +7,55 @@ The testing can be done manually using substitution scripts, but we also have sc Run `pip3 install -r automation/requirements.txt` to install the required Python packages. - ## Usage -You can easily run a smoke test against iOS and Fenix by running the following: +You can easily run a smoke test against iOS and Fenix by running the following: `./automation/build_against_all.py --firefox-dir ../firefox --action build-without-testing --allow-clears` -- In this case, `firefox-dir` must point to a bootstrapped and working installation of `mozilla-central` (see instructions [here](https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html)). It is used for the compilation and test of the Android and HNT builds. +- In this case, `firefox-dir` must point to a bootstrapped and working installation of `mozilla-central` ([see instructions here](https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html)). It is used for the compilation and test of the Android and HNT builds. -- The `--action` argument can be either `build-without-testing` or `run-tests`. +- The `--action` argument can be either `build-without-testing` or `run-tests`. - The `--allow-clears` argument allows the various subscripts to clear their various caches as appropriate (such as XCode cleaning iOS caches). You can also run against specific platforms directly with the following examples: -- iOS: - +- iOS: + ```./automation/build_against_ios.py --action build-without-testing --clear-previous-bindings --clean-ios-caches --use-local-firefox-ios ../firefox-ios``` - - By default, this script creates a `tmp` directory for `firefox-ios` and uses it. If you have a running `firefox-ios`, you can add the optional argument `--use-local-firefox-ios ../firefox-ios` (as shown), which may result in speed gains and the ability to run it on XCode immediately after a failure. This can also be passed to `./build_against_all.py`. + - By default, this script creates a `tmp` directory for `firefox-ios` and uses it. If you have a running `firefox-ios`, you can add the optional argument `--use-local-firefox-ios ../firefox-ios` (as shown), which may result in speed gains and the ability to run it on XCode immediately after a failure. This can also be passed to `./build_against_all.py`. - - You can also customize the run scheme (`--ios-scheme`) or test plan (`--ios-test-plan`). Available schemes include `Fennec` (default) and `Firefox`. Available test plans include: `Smoketest`, `FullFunctionalTestPlan` and `UnitTest`. + - You can also customize the run scheme (`--ios-scheme`) or test plan (`--ios-test-plan`). Available schemes include `Fennec` (default) and `Firefox`. Available test plans include: `Smoketest`, `FullFunctionalTestPlan` and `UnitTest`. - - A full list of schemes and their corresponding test plans can be found [in the firefox-ios](https://github.com/mozilla-mobile/firefox-ios/tree/main/firefox-ios/Client.xcodeproj/xcshareddata/xcschemes) respository. + - A full list of schemes and their corresponding test plans can be found [in the firefox-ios](https://github.com/mozilla-mobile/firefox-ios/tree/main/firefox-ios/Client.xcodeproj/xcshareddata/xcschemes) respository. - - Both of these arguments can be used in `./build_against_all.py`, and they will be passed to the underlying `build_against_ios.py` call. + - Both of these arguments can be used in `./build_against_all.py`, and they will be passed to the underlying `build_against_ios.py` call. -- Fenix: +- Fenix: ```./automation/build_against_fenix.py --action build-without-testing --firefox-dir ../firefox --prefix-ff fenix --clear-previous-bindings``` - - The `--prefix-ff` argument here refers to the prefix passed to commands like `./gradlew fenix:assembleDebug`. It can be omitted, but may cause failures on non-fenix projects. + - The `--prefix-ff` argument here refers to the prefix passed to commands like `./gradlew fenix:assembleDebug`. It can be omitted, but may cause failures on non-fenix projects. - - The `--clear-previous-bindings` argument here runs a `./gradlew prefix:clear` before recompiling. It is not always necessary, and can be excluded for some speed gains, but can result in some cache reuse. + - The `--clear-previous-bindings` argument here runs a `./gradlew prefix:clear` before recompiling. It is not always necessary, and can be excluded for some speed gains, but can result in some cache reuse. -- HNT: +- HNT / Desktop: - ``` ./automation/build_against_hnt.py --action build-without-testing --firefox-dir ../firefox``` + ```./automation/build_against_hnt.py --action build-without-testing --firefox-dir ../firefox``` - - This script uses the symlink method described [here](./locally-published-components-in-firefox-hnt.md), cleaning up after a successful run. You can avoid this cleanup process (specifically on a successful run) by passing `--no-clean-up`, which will keep the symlinks. For example, you might run with `--action build-without-testing --no-clean-up` to experiment after with `./mach run`. + - This script uses the symlink method described [at the local A-S against HNT tutorial](./locally-published-components-in-firefox-hnt.md), cleaning up after a successful run. You can avoid this cleanup process (specifically on a successful run) by passing `--no-clean-up`, which will keep the symlinks. For example, you might run with `--action build-without-testing --no-clean-up` to experiment after with `./mach run`. - - Unlike the other tests, HNT has the additional `action` variant of `--action run`, because it can be run from the terminal directly. + - Unlike the other tests, HNT has the additional `action` variant of `--action run`, because it can be run from the terminal directly. - You can pass `--hnt-test` (eg: `--hnt-test dom/notification`) to pass a set of tests to use if the `action` argument is `run-tests`. Otherwise, `./mach test --auto` will be used, which takes a guess at which tests would be best to run. You can pass `--hnt-test` to the `build_against_all.py` script as well. - All test scripts also accept the `--verbose` argument to show the output of run subprocesses (such as `./mach build`). - ## Limitations -Note that these tests are primarily smoke tests against the building and compilation of application-services. There are a wide array of possible regressions that can only be caught with tests, including ones that crash the build immediately on running. To ensure any regressions for your component are caught, tests should be created for them rather than just building. +Note that these tests are primarily smoke tests against the building and compilation of application-services. There are a wide array of possible regressions that can only be caught with tests, including ones that crash the build immediately on running. To ensure any regressions for your component are caught, tests should be created for them rather than just building. ## Deprecated tests