summaryrefslogtreecommitdiffstats
path: root/libexec/qt-android-runner.py
diff options
context:
space:
mode:
Diffstat (limited to 'libexec/qt-android-runner.py')
-rw-r--r--libexec/qt-android-runner.py205
1 files changed, 205 insertions, 0 deletions
diff --git a/libexec/qt-android-runner.py b/libexec/qt-android-runner.py
new file mode 100644
index 00000000000..17a5cefcfbb
--- /dev/null
+++ b/libexec/qt-android-runner.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+import os
+import subprocess
+import sys
+import base64
+import time
+import signal
+import argparse
+
+from datetime import datetime
+
+def status(msg):
+ print(f"\n-- {msg}")
+
+def error(msg):
+ print(f"Error: {msg}", file=sys.stderr)
+
+def die(msg):
+ error(msg)
+ sys.exit(1)
+
+# Define and parse arguments
+parser = argparse.ArgumentParser(description="Qt for Android app runner.",
+ epilog=f'''
+This is a helper script to run Qt for Android apps directly from the terminal.
+It supports starting apps with parameters and forwards environment variables to
+the device. It prints live logcat messages as the app is running. The script exits
+once the app has exited on the device and terminates the app on the device if the
+script is terminated.
+
+If an APK path is provided, it will first be installed to the device only if the
+--install parameter is passed.
+
+Use --serial parameter or adb's ANDROID_SERIAL environment variable to specify an
+Android target serial number (obtained from "adb devices" command) on which to run
+the app or test.
+''', formatter_class=argparse.RawTextHelpFormatter)
+
+parser.add_argument('-a', '--adb', metavar='path', type=str, help='Path to adb executable.')
+parser.add_argument('-b', '--build-path', metavar='path', type=str,
+ help='Path to the Android build directory.')
+parser.add_argument('-i', '--install', action='store_true', help='Install the APK.')
+parser.add_argument('-s', '--serial', type=str, metavar='serial',
+ help='Android device serial (override $ANDROID_SERIAL).')
+parser.add_argument('-p', '--apk', type=str, metavar='path', help='Path to the APK file.')
+
+
+args, remaining_args = parser.parse_known_args()
+
+# Validate required arguments
+if not args.build_path:
+ die("App build path is not provided")
+
+adb = args.adb
+if not adb:
+ adb = 'adb'
+ null_dev = subprocess.DEVNULL
+ if subprocess.call(['command', '-v', adb], stdout=null_dev, stderr=null_dev) != 0:
+ die("adb tool path is not provided and is not found in PATH")
+
+try:
+ devices = []
+ output = subprocess.check_output(f"{adb} devices", shell=True).decode().strip()
+ for line in output.splitlines():
+ if '\tdevice' in line:
+ serial = line.split('\t')[0]
+ devices.append(serial)
+ if not devices:
+ die(f"No devices are connected.")
+
+ if args.serial and not args.serial in devices:
+ die(f"No connected devices with the specified serial number.")
+except Exception as e:
+ die(f"Failed to check for running devices, received error: {e}")
+
+if args.serial:
+ adb = f"{adb} -s {args.serial}"
+
+if args.build_path is None:
+ die("App build path is not provided")
+
+if args.apk and args.install:
+ status(f"Installing the app APK {args.apk}")
+ try:
+ subprocess.run(f"{adb} install \"{args.apk}\"", check=True, shell=True)
+ except Exception as e:
+ error(f"Failed to install the APK, received error: {e}")
+
+
+def get_package_name(build_path):
+ try:
+ manifest_file = os.path.join(args.build_path, "AndroidManifest.xml")
+ if os.path.isfile(manifest_file):
+ with open(manifest_file) as f:
+ for line in f:
+ if 'package="' in line:
+ return line.split('package="')[1].split('"')[0]
+
+ gradle_file = os.path.join(args.build_path, "build.gradle")
+ if os.path.isfile(gradle_file):
+ with open(gradle_file) as f:
+ for line in f:
+ if line.strip().startswith("namespace"):
+ return line.split('=')[1].strip().strip('"')
+
+ properties_file = os.path.join(args.build_path, "gradle.properties")
+ if os.path.isfile(properties_file):
+ with open(properties_file) as f:
+ for line in f:
+ if line.startswith("androidPackageName="):
+ return line.split('=')[1].strip()
+ except Exception as e:
+ error(f"Failed to retrieve the app's package name, received error: {e}")
+
+ return None
+
+# Get app details
+package_name = get_package_name(args.build_path)
+if not package_name:
+ die("Failed to retrieve the package name of the app")
+
+activity_name = "org.qtproject.qt.android.bindings.QtActivity"
+start_cmd = f"{adb} shell am start -n {package_name}/{activity_name}"
+
+# Get environment variables
+env_vars = " ".join(f"{key}={value}" for key, value in os.environ.items())
+encoded_env_vars = base64.b64encode(env_vars.encode()).decode()
+start_cmd += f" -e extraenvvars \"{encoded_env_vars}\""
+
+# Get app arguments
+if remaining_args:
+ start_cmd += f" -e applicationArguments \"{' '.join(remaining_args)}\""
+
+# Get formatted time from device
+start_timestamp = ""
+try:
+ start_timestamp = subprocess.check_output(f"{adb} shell \"date +'%Y-%m-%d %H:%M:%S.%3N'\"",
+ shell=True).decode().strip()
+except Exception as e:
+ die(f"Failed to get formatted time from the device, received error: {e}")
+
+try:
+ subprocess.run(start_cmd, check=True, shell=True)
+except Exception as e:
+ die(f"Failed to start the app {package_name}, received error: {e}")
+
+# Wait for the app to start and retrieve its pid
+start_timeout = 5
+time_limit = time.time() + start_timeout
+pid = None
+while pid is None:
+ if time.time() > time_limit:
+ die(f"Couldn't retrieve the app's PID within {start_timeout} seconds")
+ time.sleep(0.5)
+ try:
+ pidof_output = subprocess.check_output(f"{adb} shell pidof {package_name}", shell=True)
+ pid = pidof_output.decode().strip().split()[0]
+ except subprocess.CalledProcessError:
+ continue
+
+# Add a signal handler to stop the app if the script is terminated
+interrupted = False
+def terminate_app(signum, frame):
+ global interrupted
+ interrupted = True
+
+signal.signal(signal.SIGINT, terminate_app)
+
+# Show app's logs
+try:
+ format_arg = "-v brief -v color"
+ time_arg = f"-T '{start_timestamp}'"
+ # escape char and color followed with fatal tag
+ fatal_regex = f"-e $'^\x1b\\[[0-9]*mF/'"
+ pid_regex = f"-e '([ ]*{pid}):'"
+ logcat_cmd = f"{adb} shell \"logcat {time_arg} {format_arg} | grep {pid_regex} {fatal_regex}\""
+ logcat_process = subprocess.Popen(logcat_cmd, shell=True)
+except Exception as e:
+ die(f"Failed to get logcat for the app {package_name}, received error: {e}")
+
+# Monitor the app's pid
+try:
+ while not interrupted:
+ time.sleep(1)
+ try:
+ pidof_output = subprocess.check_output(f"{adb} shell pidof {package_name}", shell=True)
+ pid = pidof_output.decode().strip()
+ if not pid:
+ status(f"The app \"{package_name}\" has exited")
+ break
+ except subprocess.CalledProcessError:
+ status(f"The app \"{package_name}\" has exited")
+ break
+finally:
+ logcat_process.terminate()
+
+if interrupted:
+ try:
+ subprocess.Popen(f"{adb} shell am force-stop {package_name}", shell=True)
+ status(f"The app \"{package_name}\" with {pid} has been terminated")
+ except Exception as e:
+ error(f"Failed to terminate the app {package_name}, received error: {e}")