33import asyncio
44import argparse
55import os
6+ import platform
67import re
78import shlex
89import shutil
@@ -247,7 +248,13 @@ def make_host_python(context):
247248 # flags to be duplicated. So we don't use the `host` argument here.
248249 os .chdir (host_dir )
249250 run (["make" , "-j" , str (os .cpu_count ())])
250- run (["make" , "install" , f"prefix={ prefix_dir } " ])
251+
252+ # The `make install` output is very verbose and rarely useful, so
253+ # suppress it by default.
254+ run (
255+ ["make" , "install" , f"prefix={ prefix_dir } " ],
256+ capture_output = not context .verbose ,
257+ )
251258
252259
253260def build_all (context ):
@@ -266,6 +273,18 @@ def clean_all(context):
266273 clean (host )
267274
268275
276+ def setup_ci ():
277+ # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/
278+ if "GITHUB_ACTIONS" in os .environ and platform .system () == "Linux" :
279+ run (
280+ ["sudo" , "tee" , "/etc/udev/rules.d/99-kvm4all.rules" ],
281+ input = 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"\n ' ,
282+ text = True ,
283+ )
284+ run (["sudo" , "udevadm" , "control" , "--reload-rules" ])
285+ run (["sudo" , "udevadm" , "trigger" , "--name-match=kvm" ])
286+
287+
269288def setup_sdk ():
270289 sdkmanager = android_home / (
271290 "cmdline-tools/latest/bin/sdkmanager"
@@ -578,6 +597,7 @@ async def gradle_task(context):
578597
579598
580599async def run_testbed (context ):
600+ setup_ci ()
581601 setup_sdk ()
582602 setup_testbed ()
583603
@@ -671,11 +691,63 @@ def package(context):
671691 else :
672692 shutil .copy2 (src , dst , follow_symlinks = False )
673693
694+ # Strip debug information.
695+ if not context .debug :
696+ so_files = glob (f"{ temp_dir } /**/*.so" , recursive = True )
697+ run ([android_env (context .host )["STRIP" ], * so_files ], log = False )
698+
674699 dist_dir = subdir (context .host , "dist" , create = True )
675700 package_path = shutil .make_archive (
676701 f"{ dist_dir } /python-{ version } -{ context .host } " , "gztar" , temp_dir
677702 )
678703 print (f"Wrote { package_path } " )
704+ return package_path
705+
706+
707+ def ci (context ):
708+ for step in [
709+ configure_build_python ,
710+ make_build_python ,
711+ configure_host_python ,
712+ make_host_python ,
713+ package ,
714+ ]:
715+ caption = (
716+ step .__name__ .replace ("_" , " " )
717+ .capitalize ()
718+ .replace ("python" , "Python" )
719+ )
720+ print (f"::group::{ caption } " )
721+ result = step (context )
722+ if step is package :
723+ package_path = result
724+ print ("::endgroup::" )
725+
726+ if (
727+ "GITHUB_ACTIONS" in os .environ
728+ and (platform .system (), platform .machine ()) != ("Linux" , "x86_64" )
729+ ):
730+ print (
731+ "Skipping tests: GitHub Actions does not support the Android "
732+ "emulator on this platform."
733+ )
734+ else :
735+ with TemporaryDirectory (prefix = SCRIPT_NAME ) as temp_dir :
736+ print ("::group::Tests" )
737+ # Prove the package is self-contained by using it to run the tests.
738+ shutil .unpack_archive (package_path , temp_dir )
739+
740+ # Arguments are similar to --fast-ci, but in single-process mode.
741+ launcher_args = ["--managed" , "maxVersion" , "-v" ]
742+ test_args = [
743+ "--single-process" , "--fail-env-changed" , "--rerun" , "--slowest" ,
744+ "--verbose3" , "-u" , "all,-cpu" , "--timeout=600"
745+ ]
746+ run (
747+ ["./android.py" , "test" , * launcher_args , "--" , * test_args ],
748+ cwd = temp_dir
749+ )
750+ print ("::endgroup::" )
679751
680752
681753def env (context ):
@@ -695,49 +767,52 @@ def parse_args():
695767 parser = argparse .ArgumentParser ()
696768 subcommands = parser .add_subparsers (dest = "subcommand" , required = True )
697769
770+ def add_parser (* args , ** kwargs ):
771+ parser = subcommands .add_parser (* args , ** kwargs )
772+ parser .add_argument (
773+ "-v" , "--verbose" , action = "count" , default = 0 ,
774+ help = "Show verbose output. Use twice to be even more verbose." )
775+ return parser
776+
698777 # Subcommands
699- build = subcommands . add_parser (
778+ build = add_parser (
700779 "build" , help = "Run configure-build, make-build, configure-host and "
701780 "make-host" )
702- configure_build = subcommands . add_parser (
781+ configure_build = add_parser (
703782 "configure-build" , help = "Run `configure` for the build Python" )
704- subcommands . add_parser (
783+ add_parser (
705784 "make-build" , help = "Run `make` for the build Python" )
706- configure_host = subcommands . add_parser (
785+ configure_host = add_parser (
707786 "configure-host" , help = "Run `configure` for Android" )
708- make_host = subcommands . add_parser (
787+ make_host = add_parser (
709788 "make-host" , help = "Run `make` for Android" )
710789
711- subcommands .add_parser ("clean" , help = "Delete all build directories" )
712- subcommands .add_parser ("build-testbed" , help = "Build the testbed app" )
713- test = subcommands .add_parser ("test" , help = "Run the testbed app" )
714- package = subcommands .add_parser ("package" , help = "Make a release package" )
715- env = subcommands .add_parser ("env" , help = "Print environment variables" )
790+ add_parser ("clean" , help = "Delete all build directories" )
791+ add_parser ("build-testbed" , help = "Build the testbed app" )
792+ test = add_parser ("test" , help = "Run the testbed app" )
793+ package = add_parser ("package" , help = "Make a release package" )
794+ ci = add_parser ("ci" , help = "Run build, package and test" )
795+ env = add_parser ("env" , help = "Print environment variables" )
716796
717797 # Common arguments
718- for subcommand in build , configure_build , configure_host :
798+ for subcommand in [ build , configure_build , configure_host , ci ] :
719799 subcommand .add_argument (
720800 "--clean" , action = "store_true" , default = False , dest = "clean" ,
721801 help = "Delete the relevant build directories first" )
722802
723- host_commands = [build , configure_host , make_host , package ]
803+ host_commands = [build , configure_host , make_host , package , ci ]
724804 if in_source_tree :
725805 host_commands .append (env )
726806 for subcommand in host_commands :
727807 subcommand .add_argument (
728808 "host" , metavar = "HOST" , choices = HOSTS ,
729809 help = "Host triplet: choices=[%(choices)s]" )
730810
731- for subcommand in build , configure_build , configure_host :
811+ for subcommand in [ build , configure_build , configure_host , ci ] :
732812 subcommand .add_argument ("args" , nargs = "*" ,
733813 help = "Extra arguments to pass to `configure`" )
734814
735815 # Test arguments
736- test .add_argument (
737- "-v" , "--verbose" , action = "count" , default = 0 ,
738- help = "Show Gradle output, and non-Python logcat messages. "
739- "Use twice to include high-volume messages which are rarely useful." )
740-
741816 device_group = test .add_mutually_exclusive_group (required = True )
742817 device_group .add_argument (
743818 "--connected" , metavar = "SERIAL" , help = "Run on a connected device. "
@@ -765,6 +840,12 @@ def parse_args():
765840 "args" , nargs = "*" , help = f"Arguments to add to sys.argv. "
766841 f"Separate them from { SCRIPT_NAME } 's own arguments with `--`." )
767842
843+ # Package arguments.
844+ for subcommand in [package , ci ]:
845+ subcommand .add_argument (
846+ "-g" , action = "store_true" , default = False , dest = "debug" ,
847+ help = "Include debug information in package" )
848+
768849 return parser .parse_args ()
769850
770851
@@ -788,6 +869,7 @@ def main():
788869 "build-testbed" : build_testbed ,
789870 "test" : run_testbed ,
790871 "package" : package ,
872+ "ci" : ci ,
791873 "env" : env ,
792874 }
793875
@@ -803,6 +885,8 @@ def main():
803885def print_called_process_error (e ):
804886 for stream_name in ["stdout" , "stderr" ]:
805887 content = getattr (e , stream_name )
888+ if isinstance (content , bytes ):
889+ content = content .decode (* DECODE_ARGS )
806890 stream = getattr (sys , stream_name )
807891 if content :
808892 stream .write (content )
0 commit comments