Convert run-test bash build scripts to python.

Tests use the build script for two distinct purposes:

 * Some tests generate their source code before completion.
   Keep this code in bash for now and rename it "generate-sources".

 * To customize arguments passed to the default compilation script.
   Since the default compilation script is in python, make this
   a normal python call, and add any customization as arguments.

This speeds up the run-test compilation.  There was small cost to
pay due to the switching from python to bash to python.
This added up for the thousands of run-test compilations that we do.

It also removes the need for argument parser in the default script.
We could also remove the data passing via environment (to-do later).
It moves the definition to more build-system-like look and feel.

Test: The outputs are bit-for-bit identical as before.
Change-Id: Idca4181a4676036f06aae0a8f6eea3a3c30d390e
diff --git a/test/000-nop/build b/test/000-nop/build
deleted file mode 100644
index 5233a2d..0000000
--- a/test/000-nop/build
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-# Nothing to do here.
diff --git a/test/2038-hiddenapi-jvmti-ext/build b/test/000-nop/build.py
similarity index 83%
rename from test/2038-hiddenapi-jvmti-ext/build
rename to test/000-nop/build.py
index f4b029f..846e9a6 100644
--- a/test/2038-hiddenapi-jvmti-ext/build
+++ b/test/000-nop/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+# Nothing to do here.
diff --git a/test/003-omnibus-opcodes/build b/test/003-omnibus-opcodes/build
deleted file mode 100644
index a14ddc9..0000000
--- a/test/003-omnibus-opcodes/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2008 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Stop if something fails.
-set -e
-
-./default-build "$@"
diff --git a/test/004-JniTest/build b/test/004-JniTest/build.py
old mode 100755
new mode 100644
similarity index 80%
rename from test/004-JniTest/build
rename to test/004-JniTest/build.py
index 460f2db..e31d770
--- a/test/004-JniTest/build
+++ b/test/004-JniTest/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,6 +13,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from art_build_rules import build_run_test
+import shutil, os
+
 #
 # Perform a mostly normal build.
 # Since this test imports 'dalvik.annotation.optimization.FastNative' (and CriticalNative),
@@ -25,12 +27,11 @@
 # has a different ABI and cannot be tested on RI.
 #
 
-# Stop on failure.
-set -e
-
 # Use release mode to check optimizations do not break JNI.
-export D8_FLAGS=--release
-./default-build "$@"
+build_run_test(d8_flags=['--release'])
 
 # Remove the *-aotex build artifacts (but keep src-aotex) with dalvik.* annotations.
-rm -rf classes-aotex classes-aotex.jar $TEST_NAME-aotex.jar
+shutil.rmtree("classes-aotex")
+if os.environ["BUILD_MODE"] != "jvm":
+  os.remove("classes-aotex.jar")
+  os.remove("004-JniTest-aotex.jar")
diff --git a/test/004-ReferenceMap/build b/test/004-ReferenceMap/generate-sources
old mode 100644
new mode 100755
similarity index 97%
rename from test/004-ReferenceMap/build
rename to test/004-ReferenceMap/generate-sources
index 6203c97..9edc200
--- a/test/004-ReferenceMap/build
+++ b/test/004-ReferenceMap/generate-sources
@@ -23,4 +23,3 @@
 ######################################################################
 
 ${SOONG_ZIP} --jar -o classes.jar -f classes.dex
-./default-build "$@"
diff --git a/test/004-StackWalk/build b/test/004-StackWalk/generate-sources
old mode 100644
new mode 100755
similarity index 97%
rename from test/004-StackWalk/build
rename to test/004-StackWalk/generate-sources
index 6203c97..9edc200
--- a/test/004-StackWalk/build
+++ b/test/004-StackWalk/generate-sources
@@ -23,4 +23,3 @@
 ######################################################################
 
 ${SOONG_ZIP} --jar -o classes.jar -f classes.dex
-./default-build "$@"
diff --git a/test/005-annotations/build b/test/005-annotations/build.py
similarity index 73%
rename from test/005-annotations/build
rename to test/005-annotations/build.py
index bdc1950..8cf988c 100644
--- a/test/005-annotations/build
+++ b/test/005-annotations/build.py
@@ -1,12 +1,11 @@
-#!/bin/bash
 #
-# Copyright (C) 2012 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
 #
-#     http://www.apache.org/licenses/LICENSE-2.0
+#      http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,9 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Stop if something fails.
-set -e
+from art_build_rules import build_run_test
 
 # Build intermediate object to preserve class-retention annotations.
-export D8_FLAGS=--intermediate
-./default-build "$@"
+build_run_test(d8_flags=['--intermediate'])
diff --git a/test/023-many-interfaces/build b/test/023-many-interfaces/generate-sources
old mode 100644
new mode 100755
similarity index 96%
rename from test/023-many-interfaces/build
rename to test/023-many-interfaces/generate-sources
index 6d1c8e3..f9ba1b8
--- a/test/023-many-interfaces/build
+++ b/test/023-many-interfaces/generate-sources
@@ -19,5 +19,3 @@
 
 # Write out a bunch of interface source files.
 ./iface-gen
-
-./default-build "$@"
diff --git a/test/056-const-string-jumbo/build b/test/056-const-string-jumbo/generate-sources
old mode 100644
new mode 100755
similarity index 98%
rename from test/056-const-string-jumbo/build
rename to test/056-const-string-jumbo/generate-sources
index c1d711b..5c7ade8
--- a/test/056-const-string-jumbo/build
+++ b/test/056-const-string-jumbo/generate-sources
@@ -38,5 +38,3 @@
     }
     printf("}\n") > fileName;
 }'
-
-./default-build "$@"
diff --git a/test/065-mismatched-implements/build b/test/065-mismatched-implements/build.py
old mode 100755
new mode 100644
similarity index 85%
rename from test/065-mismatched-implements/build
rename to test/065-mismatched-implements/build.py
index 41823b5..4284d83
--- a/test/065-mismatched-implements/build
+++ b/test/065-mismatched-implements/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -13,15 +12,12 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-# Make us exit on a failure.
 
-set -e
+from art_build_rules import build_run_test
 
 # Don't use desugar because the build fails when it encounters ICCE.
 #
 # Exception in thread "main" java.lang.IllegalArgumentException
 #  at com.google.common.base.Preconditions.checkArgument(Preconditions.java:108)
 #  at com.google.devtools.build.android.desugar.DefaultMethodClassFixer$DefaultMethodFinder.visit(DefaultMethodClassFixer.java:295)
-export USE_DESUGAR=false
-
-./default-build "$@"
+build_run_test(use_desugar=False)
diff --git a/test/066-mismatched-super/build b/test/066-mismatched-super/build
deleted file mode 100644
index 50aceb4..0000000
--- a/test/066-mismatched-super/build
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2018 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-USE_DESUGAR=false ./default-build "$@"
diff --git a/test/677-fsi/build b/test/066-mismatched-super/build.py
old mode 100755
new mode 100644
similarity index 80%
rename from test/677-fsi/build
rename to test/066-mismatched-super/build.py
index b90b408..337bd29
--- a/test/677-fsi/build
+++ b/test/066-mismatched-super/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(use_desugar=False)
diff --git a/test/071-dexfile-get-static-size/build b/test/071-dexfile-get-static-size/generate-sources
similarity index 100%
rename from test/071-dexfile-get-static-size/build
rename to test/071-dexfile-get-static-size/generate-sources
diff --git a/test/071-dexfile-map-clean/build b/test/071-dexfile-map-clean/build.py
old mode 100755
new mode 100644
similarity index 82%
rename from test/071-dexfile-map-clean/build
rename to test/071-dexfile-map-clean/build.py
index a171fc3..f8c3efd
--- a/test/071-dexfile-map-clean/build
+++ b/test/071-dexfile-map-clean/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,8 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from art_build_rules import build_run_test
+
 # Any JAR files used by this test shall have their classes.dex be stored, NOT compressed.
 # This is needed for our test correctness which validates classes.dex are mapped file-backed.
 #
 # In addition, align to at least 4 bytes since that's the dex alignment requirement.
-./default-build "$@" --zip-compression-method store --zip-align 4
+build_run_test(zip_compression_method="store", zip_align_bytes="4")
diff --git a/test/089-many-methods/build.py b/test/089-many-methods/build.py
new file mode 100644
index 0000000..052ae72
--- /dev/null
+++ b/test/089-many-methods/build.py
@@ -0,0 +1,26 @@
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from art_build_rules import build_run_test
+
+# Specify old API level as d8 automagically produces a multidex file
+# when the API level is above 20. Failing the build here is deliberate.
+# Force DEX generation so test also passes with --jvm.
+try:
+  build_run_test(api_level=20, need_dex=True)
+  assert False, "Test was not expected to build successfully"
+except Exception as e:
+  # Check that a build failure happened (the test is not expected to run).
+  assert "Cannot fit requested classes in a single dex" in str(e), e
diff --git a/test/089-many-methods/build b/test/089-many-methods/generate-sources
old mode 100644
new mode 100755
similarity index 72%
rename from test/089-many-methods/build
rename to test/089-many-methods/generate-sources
index 5225e3f..f38a025
--- a/test/089-many-methods/build
+++ b/test/089-many-methods/generate-sources
@@ -42,15 +42,3 @@
     }
     printf("}\n") > fileName;
 }'
-
-# Force DEX generation so test also passes with --jvm.
-export NEED_DEX=true
-
-# Specify old API level as d8 automagically produces a multidex file
-# when the API level is above 20. Failing the build here is deliberate.
-./default-build --api-level 20 "$@" > /dev/null 2> stderr.txt || true
-
-# Check that a build failure happened (the test is not expected to run).
-EXPECTED_ERROR="Cannot fit requested classes in a single dex"
-grep -q "$EXPECTED_ERROR" stderr.txt
-rm stderr.txt  # Check passed. Remove output due to non-deterministic paths.
diff --git a/test/091-override-package-private-method/build b/test/091-override-package-private-method/build
deleted file mode 100755
index 6b298f7..0000000
--- a/test/091-override-package-private-method/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Stop if something fails.
-set -e
-
-./default-build "$@"
diff --git a/test/1001-app-image-regions/build b/test/1001-app-image-regions/generate-sources
similarity index 97%
rename from test/1001-app-image-regions/build
rename to test/1001-app-image-regions/generate-sources
index 16c3a5b..0fbe23f 100755
--- a/test/1001-app-image-regions/build
+++ b/test/1001-app-image-regions/generate-sources
@@ -30,5 +30,3 @@
   echo "  static class Inner${i} { void test(){} }" >> "${other_file}"
 done
 echo "}" >> "${other_file}"
-
-./default-build "$@"
diff --git a/test/1003-metadata-section-strings/build b/test/1003-metadata-section-strings/generate-sources
similarity index 97%
rename from test/1003-metadata-section-strings/build
rename to test/1003-metadata-section-strings/generate-sources
index cd2cacd..c3e73f5 100755
--- a/test/1003-metadata-section-strings/build
+++ b/test/1003-metadata-section-strings/generate-sources
@@ -37,5 +37,3 @@
 }
 }
 EOF
-
-./default-build "$@"
diff --git a/test/111-unresolvable-exception/build b/test/111-unresolvable-exception/build
deleted file mode 100644
index f378df1..0000000
--- a/test/111-unresolvable-exception/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2014 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Stop if something fails.
-set -e
-
-./default-build "$@"
diff --git a/test/124-missing-classes/build b/test/124-missing-classes/build
deleted file mode 100644
index a40cbc9..0000000
--- a/test/124-missing-classes/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2012 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Stop if something fails.
-set -e
-
-./default-build "$@"
diff --git a/test/126-miranda-multidex/build b/test/126-miranda-multidex/generate-sources
old mode 100644
new mode 100755
similarity index 96%
rename from test/126-miranda-multidex/build
rename to test/126-miranda-multidex/generate-sources
index c827e55..b35d5ef
--- a/test/126-miranda-multidex/build
+++ b/test/126-miranda-multidex/generate-sources
@@ -19,4 +19,3 @@
 
 # Signal to default-build that this is a multidex test.
 mkdir src-multidex
-./default-build "$@"
diff --git a/test/127-checker-secondarydex/build b/test/127-checker-secondarydex/build
deleted file mode 100755
index f378df1..0000000
--- a/test/127-checker-secondarydex/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2014 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Stop if something fails.
-set -e
-
-./default-build "$@"
diff --git a/test/138-duplicate-classes-check2/build b/test/138-duplicate-classes-check2/build
deleted file mode 100755
index 6b298f7..0000000
--- a/test/138-duplicate-classes-check2/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Stop if something fails.
-set -e
-
-./default-build "$@"
diff --git a/test/160-read-barrier-stress/build b/test/160-read-barrier-stress/build
deleted file mode 100755
index 90b6b95..0000000
--- a/test/160-read-barrier-stress/build
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2020 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-./default-build "$@" --experimental var-handles
diff --git a/test/807-method-handle-and-mr/build b/test/160-read-barrier-stress/build.py
old mode 100755
new mode 100644
similarity index 79%
rename from test/807-method-handle-and-mr/build
rename to test/160-read-barrier-stress/build.py
index 12a8e18..d6b35b1
--- a/test/807-method-handle-and-mr/build
+++ b/test/160-read-barrier-stress/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="var-handles")
diff --git a/test/161-final-abstract-class/build b/test/161-final-abstract-class/generate-sources
old mode 100644
new mode 100755
similarity index 100%
rename from test/161-final-abstract-class/build
rename to test/161-final-abstract-class/generate-sources
diff --git a/test/166-bad-interface-super/build b/test/166-bad-interface-super/build.py
similarity index 73%
rename from test/166-bad-interface-super/build
rename to test/166-bad-interface-super/build.py
index bba6184..f74f0e9 100644
--- a/test/166-bad-interface-super/build
+++ b/test/166-bad-interface-super/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,14 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from art_build_rules import build_run_test
+import os
+
 # Use the jasmin sources for JVM, otherwise the smali sources.
-extra_arg="--no-jasmin"
-
-for arg in "$@"; do
-  if [[ "$arg" == "--jvm" ]]; then
-    extra_arg="--no-smali"
-    break
-  fi
-done
-
-./default-build "$@" "$extra_arg"
+if os.environ["BUILD_MODE"] == "jvm":
+  build_run_test(has_smali=False)
+else:
+  build_run_test(has_jasmin=False)
diff --git a/test/180-native-default-method/build b/test/180-native-default-method/build
deleted file mode 100644
index b6b604f..0000000
--- a/test/180-native-default-method/build
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2020 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@"
-
-if [[ $@ != *"--jvm"* ]]; then
-  # Change the generated dex file to have a v35 magic number if it is version 38
-  if test -f classes.dex && head -c 7 classes.dex | grep -q 038; then
-    # place ascii value '035' into the classes.dex file starting at byte 4.
-    printf '035' | dd status=none conv=notrunc of=classes.dex bs=1 seek=4 count=3
-    rm -f $TEST_NAME.jar
-    ${SOONG_ZIP} -o $TEST_NAME.jar -f classes.dex
-  else
-    echo Unexpected dex verison
-    exit 1
-  fi
-fi
diff --git a/test/180-native-default-method/build.py b/test/180-native-default-method/build.py
new file mode 100644
index 0000000..122bcc0
--- /dev/null
+++ b/test/180-native-default-method/build.py
@@ -0,0 +1,29 @@
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from art_build_rules import build_run_test
+import subprocess, os
+
+build_run_test()
+
+if os.environ["BUILD_MODE"] != "jvm":
+  # Change the generated dex file to have a v35 magic number if it is version 38
+  with open("classes.dex", "rb+") as f:
+    assert f.read(8) == b'dex\n038\x00'
+    f.seek(0)
+    f.write(b'dex\n035\x00')
+  os.remove("180-native-default-method.jar")
+  subprocess.run([os.environ["SOONG_ZIP"], "-o",
+                  "180-native-default-method.jar", "-f", "classes.dex"], check=True)
diff --git a/test/181-default-methods/build b/test/181-default-methods/build
deleted file mode 100644
index 9cd5738..0000000
--- a/test/181-default-methods/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2022 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental default-methods
diff --git a/test/677-fsi/build b/test/181-default-methods/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/181-default-methods/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/181-default-methods/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/677-fsi/build b/test/1948-obsolete-const-method-handle/build.py
old mode 100755
new mode 100644
similarity index 80%
copy from test/677-fsi/build
copy to test/1948-obsolete-const-method-handle/build.py
index b90b408..193aa91
--- a/test/677-fsi/build
+++ b/test/1948-obsolete-const-method-handle/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(api_level=28)
diff --git a/test/1948-obsolete-const-method-handle/build b/test/1948-obsolete-const-method-handle/generate-sources
old mode 100644
new mode 100755
similarity index 94%
rename from test/1948-obsolete-const-method-handle/build
rename to test/1948-obsolete-const-method-handle/generate-sources
index d0e7a8c..9cdc749
--- a/test/1948-obsolete-const-method-handle/build
+++ b/test/1948-obsolete-const-method-handle/generate-sources
@@ -19,5 +19,3 @@
 
 mkdir classes
 ./util-src/build-classes $PWD/classes
-
-./default-build --api-level 28 "$@"
diff --git a/test/1965-get-set-local-primitive-no-tables/build b/test/1965-get-set-local-primitive-no-tables/generate-sources
old mode 100644
new mode 100755
similarity index 96%
rename from test/1965-get-set-local-primitive-no-tables/build
rename to test/1965-get-set-local-primitive-no-tables/generate-sources
index 6631df9..c2f8653
--- a/test/1965-get-set-local-primitive-no-tables/build
+++ b/test/1965-get-set-local-primitive-no-tables/generate-sources
@@ -22,4 +22,3 @@
 else
   mv smali smali-unused
 fi
-./default-build "$@" 
diff --git a/test/1966-get-set-local-objects-no-table/build b/test/1966-get-set-local-objects-no-table/build
deleted file mode 100644
index 6631df9..0000000
--- a/test/1966-get-set-local-objects-no-table/build
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2019 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-if [[ $@ != *"--jvm"* ]]; then
-  mv jasmin jasmin-unused
-else
-  mv smali smali-unused
-fi
-./default-build "$@" 
diff --git a/test/1965-get-set-local-primitive-no-tables/build b/test/1966-get-set-local-objects-no-table/generate-sources
old mode 100644
new mode 100755
similarity index 96%
copy from test/1965-get-set-local-primitive-no-tables/build
copy to test/1966-get-set-local-objects-no-table/generate-sources
index 6631df9..c2f8653
--- a/test/1965-get-set-local-primitive-no-tables/build
+++ b/test/1966-get-set-local-objects-no-table/generate-sources
@@ -22,4 +22,3 @@
 else
   mv smali smali-unused
 fi
-./default-build "$@" 
diff --git a/test/1981-structural-redef-private-method-handles/build b/test/1981-structural-redef-private-method-handles/build
deleted file mode 100755
index c80d7ad..0000000
--- a/test/1981-structural-redef-private-method-handles/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2019 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make us exit on a failure
-set -e
-
-./default-build "$@" --experimental var-handles
diff --git a/test/807-method-handle-and-mr/build b/test/1981-structural-redef-private-method-handles/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/807-method-handle-and-mr/build
copy to test/1981-structural-redef-private-method-handles/build.py
index 12a8e18..d6b35b1
--- a/test/807-method-handle-and-mr/build
+++ b/test/1981-structural-redef-private-method-handles/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="var-handles")
diff --git a/test/1983-structural-redefinition-failures/build b/test/1983-structural-redefinition-failures/build
deleted file mode 100755
index c80d7ad..0000000
--- a/test/1983-structural-redefinition-failures/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2019 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make us exit on a failure
-set -e
-
-./default-build "$@" --experimental var-handles
diff --git a/test/807-method-handle-and-mr/build b/test/1983-structural-redefinition-failures/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/807-method-handle-and-mr/build
copy to test/1983-structural-redefinition-failures/build.py
index 12a8e18..d6b35b1
--- a/test/807-method-handle-and-mr/build
+++ b/test/1983-structural-redefinition-failures/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="var-handles")
diff --git a/test/677-fsi/build b/test/2000-virtual-list-structural/build.py
old mode 100755
new mode 100644
similarity index 68%
copy from test/677-fsi/build
copy to test/2000-virtual-list-structural/build.py
index b90b408..0d8dc0c
--- a/test/677-fsi/build
+++ b/test/2000-virtual-list-structural/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+import os
+
+build_run_test(use_desugar=False)
+
+os.rename("src-ex/java/util/AbstractCollection.bak",
+          "src-ex/java/util/AbstractCollection.java")
+
diff --git a/test/2000-virtual-list-structural/build b/test/2000-virtual-list-structural/generate-sources
similarity index 82%
rename from test/2000-virtual-list-structural/build
rename to test/2000-virtual-list-structural/generate-sources
index f8496bec..5e21f12 100755
--- a/test/2000-virtual-list-structural/build
+++ b/test/2000-virtual-list-structural/generate-sources
@@ -23,9 +23,3 @@
 
 # Patch the copied version.
 patch src-ex/java/util/AbstractCollection.java AbstractCollection.patch
-
-USE_DESUGAR=false ./default-build "$@"
-
-# restore the symlink
-rm src-ex/java/util/AbstractCollection.java
-mv src-ex/java/util/AbstractCollection.bak src-ex/java/util/AbstractCollection.java
diff --git a/test/975-iface-private/build b/test/2034-spaces-in-SimpleName/build.py
old mode 100755
new mode 100644
similarity index 73%
rename from test/975-iface-private/build
rename to test/2034-spaces-in-SimpleName/build.py
index 14230c2..7601e12
--- a/test/975-iface-private/build
+++ b/test/2034-spaces-in-SimpleName/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2015 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# make us exit on a failure
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental default-methods
+# Use API level 10000 for spaces in SimpleName
+build_run_test(use_desugar=False, api_level="10000")
diff --git a/test/2034-spaces-in-SimpleName/build b/test/2034-spaces-in-SimpleName/generate-sources
similarity index 89%
rename from test/2034-spaces-in-SimpleName/build
rename to test/2034-spaces-in-SimpleName/generate-sources
index 8261ed2..99fc7e0 100755
--- a/test/2034-spaces-in-SimpleName/build
+++ b/test/2034-spaces-in-SimpleName/generate-sources
@@ -24,6 +24,3 @@
 ${JAVA:-java} -cp "$ASM_JAR:." SpacesInSimpleName.java
 mkdir ../classes && mv Main.class ../classes/Main.class
 cd ..
-
-# Use API level 10000 for spaces in SimpleName
-USE_DESUGAR=false ./default-build "$@" --api-level 10000
diff --git a/test/2038-hiddenapi-jvmti-ext/build b/test/2038-hiddenapi-jvmti-ext/build.py
similarity index 80%
copy from test/2038-hiddenapi-jvmti-ext/build
copy to test/2038-hiddenapi-jvmti-ext/build.py
index f4b029f..8367ad0 100644
--- a/test/2038-hiddenapi-jvmti-ext/build
+++ b/test/2038-hiddenapi-jvmti-ext/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+from art_build_rules import build_run_test
+
+build_run_test(use_hiddenapi=True)
diff --git a/test/807-method-handle-and-mr/build b/test/2239-varhandle-perf/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/807-method-handle-and-mr/build
copy to test/2239-varhandle-perf/build.py
index 12a8e18..d6b35b1
--- a/test/807-method-handle-and-mr/build
+++ b/test/2239-varhandle-perf/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="var-handles")
diff --git a/test/2239-varhandle-perf/build b/test/2239-varhandle-perf/generate-sources
similarity index 94%
rename from test/2239-varhandle-perf/build
rename to test/2239-varhandle-perf/generate-sources
index 115a0fb..310ad06 100755
--- a/test/2239-varhandle-perf/build
+++ b/test/2239-varhandle-perf/generate-sources
@@ -28,5 +28,3 @@
 
 # Generate tests and Main that covers both the generated tests and manual tests
 python3 ./util-src/generate_java.py "${GENERATED_SRC}"
-
-./default-build "$@" --experimental var-handles
diff --git a/test/303-verification-stress/build b/test/303-verification-stress/generate-sources
old mode 100644
new mode 100755
similarity index 96%
rename from test/303-verification-stress/build
rename to test/303-verification-stress/generate-sources
index 6e4a1d6..2a2cdb6
--- a/test/303-verification-stress/build
+++ b/test/303-verification-stress/generate-sources
@@ -19,5 +19,3 @@
 
 # Write out a bunch of source files.
 ./classes-gen
-
-./default-build "$@"
diff --git a/test/370-dex-v37/build b/test/370-dex-v37/build
deleted file mode 100755
index f472428..0000000
--- a/test/370-dex-v37/build
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@"
-
-if [[ $@ != *"--jvm"* ]]; then
-  # Change the generated dex file to have a v36 magic number if it is version 35
-  if test -f classes.dex && head -c 7 classes.dex | grep -q 035; then
-    # place ascii value '037' into the classes.dex file starting at byte 4.
-    printf '037' | dd status=none conv=notrunc of=classes.dex bs=1 seek=4 count=3
-    rm -f $TEST_NAME.jar
-    zip $TEST_NAME.jar classes.dex
-  fi
-fi
diff --git a/test/370-dex-v37/build.py b/test/370-dex-v37/build.py
new file mode 100644
index 0000000..5ae5a03
--- /dev/null
+++ b/test/370-dex-v37/build.py
@@ -0,0 +1,29 @@
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from art_build_rules import build_run_test
+import subprocess, os
+
+build_run_test()
+
+if os.environ["BUILD_MODE"] != "jvm":
+  # Change the generated dex file to have a v37 magic number if it is version 35
+  with open("classes.dex", "rb+") as f:
+    if f.read(8) == b'dex\n035\x00':
+      f.seek(0)
+      f.write(b'dex\n037\x00')
+      os.remove("370-dex-v37.jar")
+      subprocess.run([os.environ["SOONG_ZIP"], "-o",
+                      "370-dex-v37.jar", "-f", "classes.dex"], check=True)
diff --git a/test/616-cha-interface-default/build b/test/616-cha-interface-default/build
deleted file mode 100644
index d9654f8..0000000
--- a/test/616-cha-interface-default/build
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-./default-build "$@" --experimental default-methods
diff --git a/test/677-fsi/build b/test/616-cha-interface-default/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/616-cha-interface-default/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/616-cha-interface-default/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/638-no-line-number/build b/test/638-no-line-number/build.py
similarity index 74%
rename from test/638-no-line-number/build
rename to test/638-no-line-number/build.py
index 9cd1955..bc8b681 100644
--- a/test/638-no-line-number/build
+++ b/test/638-no-line-number/build.py
@@ -1,12 +1,11 @@
-#!/bin/bash
 #
-# Copyright (C) 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
 #
-#     http://www.apache.org/licenses/LICENSE-2.0
+#      http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,9 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Stop if something fails.
-set -e
+from art_build_rules import build_run_test
 
 # Only keep the source name, to make sure we do remove it in the stack trace
 # when there is no line number mapping.
-JAVAC_ARGS="$JAVAC_ARGS -g:source" ./default-build "$@"
+build_run_test(javac_args=["-g:source"])
diff --git a/test/648-many-direct-methods/build b/test/648-many-direct-methods/generate-sources
similarity index 96%
rename from test/648-many-direct-methods/build
rename to test/648-many-direct-methods/generate-sources
index 7e888e5..8907645 100755
--- a/test/648-many-direct-methods/build
+++ b/test/648-many-direct-methods/generate-sources
@@ -21,5 +21,3 @@
 
 # Generate the Java file or fail.
 ./util-src/generate_java.py ./src
-
-./default-build "$@"
diff --git a/test/663-odd-dex-size2/build b/test/663-odd-dex-size2/build
deleted file mode 100644
index 5636558..0000000
--- a/test/663-odd-dex-size2/build
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Nothing to do
diff --git a/test/2038-hiddenapi-jvmti-ext/build b/test/663-odd-dex-size2/build.py
similarity index 83%
copy from test/2038-hiddenapi-jvmti-ext/build
copy to test/663-odd-dex-size2/build.py
index f4b029f..76845c9 100644
--- a/test/2038-hiddenapi-jvmti-ext/build
+++ b/test/663-odd-dex-size2/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+# Nothing to do.
diff --git a/test/663-odd-dex-size3/build b/test/663-odd-dex-size3/build
deleted file mode 100644
index 5636558..0000000
--- a/test/663-odd-dex-size3/build
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Nothing to do
diff --git a/test/2038-hiddenapi-jvmti-ext/build b/test/663-odd-dex-size3/build.py
similarity index 83%
copy from test/2038-hiddenapi-jvmti-ext/build
copy to test/663-odd-dex-size3/build.py
index f4b029f..c4fc9ee 100644
--- a/test/2038-hiddenapi-jvmti-ext/build
+++ b/test/663-odd-dex-size3/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+# Nothing to do
diff --git a/test/663-odd-dex-size4/build b/test/663-odd-dex-size4/build
deleted file mode 100644
index 5636558..0000000
--- a/test/663-odd-dex-size4/build
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Nothing to do
diff --git a/test/2038-hiddenapi-jvmti-ext/build b/test/663-odd-dex-size4/build.py
similarity index 83%
copy from test/2038-hiddenapi-jvmti-ext/build
copy to test/663-odd-dex-size4/build.py
index f4b029f..c4fc9ee 100644
--- a/test/2038-hiddenapi-jvmti-ext/build
+++ b/test/663-odd-dex-size4/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+# Nothing to do
diff --git a/test/670-bitstring-type-check/build b/test/670-bitstring-type-check/generate-sources
old mode 100644
new mode 100755
similarity index 99%
rename from test/670-bitstring-type-check/build
rename to test/670-bitstring-type-check/generate-sources
index 38307f2..4d88839
--- a/test/670-bitstring-type-check/build
+++ b/test/670-bitstring-type-check/generate-sources
@@ -212,5 +212,3 @@
   }
 }
 EOF
-
-./default-build "$@"
diff --git a/test/674-hiddenapi/build b/test/674-hiddenapi/build.py
similarity index 81%
rename from test/674-hiddenapi/build
rename to test/674-hiddenapi/build.py
index 330a6de..f518d51 100644
--- a/test/674-hiddenapi/build
+++ b/test/674-hiddenapi/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-set -e
+from art_build_rules import build_run_test, rm
+import os
 
 # Build the jars twice. First with applying hiddenapi, creating a boot jar, then
 # a second time without to create a normal jar. We need to do this because we
@@ -23,16 +23,14 @@
 # hidden API access flags in dex files. DexFileVerifier is not invoked on boot
 # class path dex files, so the boot jar loads fine in the latter case.
 
-export USE_HIDDENAPI=true
-./default-build "$@"
+build_run_test(use_hiddenapi=True)
 
 # Move the jar file into the resource folder to be bundled with the test.
-mkdir res
-mv ${TEST_NAME}.jar res/boot.jar
+os.mkdir("res")
+os.rename("674-hiddenapi.jar", "res/boot.jar")
 
 # Clear all intermediate files otherwise default-build would either skip
 # compilation or fail rebuilding.
-rm -rf classes*
+rm("classes*")
 
-export USE_HIDDENAPI=false
-./default-build "$@"
+build_run_test(use_hiddenapi=False)
diff --git a/test/674-vdex-uncompress/build b/test/674-vdex-uncompress/build.py
old mode 100755
new mode 100644
similarity index 79%
rename from test/674-vdex-uncompress/build
rename to test/674-vdex-uncompress/build.py
index 7b1804d..a694a9d
--- a/test/674-vdex-uncompress/build
+++ b/test/674-vdex-uncompress/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from art_build_rules import build_run_test
+
 # Uncompress and align the dex files so that dex2oat will not copy the dex
 # code to the .vdex file.
-./default-build "$@" --zip-compression-method store --zip-align 4
+build_run_test(zip_compression_method="store", zip_align_bytes="4")
diff --git a/test/677-fsi/build b/test/677-fsi/build.py
old mode 100755
new mode 100644
similarity index 76%
copy from test/677-fsi/build
copy to test/677-fsi/build.py
index b90b408..d16a996
--- a/test/677-fsi/build
+++ b/test/677-fsi/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(zip_compression_method="store", zip_align_bytes=4)
diff --git a/test/690-hiddenapi-same-name-methods/build b/test/690-hiddenapi-same-name-methods/build.py
similarity index 80%
rename from test/690-hiddenapi-same-name-methods/build
rename to test/690-hiddenapi-same-name-methods/build.py
index c364b3b..8367ad0 100644
--- a/test/690-hiddenapi-same-name-methods/build
+++ b/test/690-hiddenapi-same-name-methods/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2019 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+from art_build_rules import build_run_test
+
+build_run_test(use_hiddenapi=True)
diff --git a/test/691-hiddenapi-proxy/build b/test/691-hiddenapi-proxy/build.py
similarity index 80%
rename from test/691-hiddenapi-proxy/build
rename to test/691-hiddenapi-proxy/build.py
index c364b3b..8367ad0 100644
--- a/test/691-hiddenapi-proxy/build
+++ b/test/691-hiddenapi-proxy/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2019 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+from art_build_rules import build_run_test
+
+build_run_test(use_hiddenapi=True)
diff --git a/test/701-easy-div-rem/build b/test/701-easy-div-rem/generate-sources
old mode 100644
new mode 100755
similarity index 96%
rename from test/701-easy-div-rem/build
rename to test/701-easy-div-rem/generate-sources
index 6d114b6..efe4b05
--- a/test/701-easy-div-rem/build
+++ b/test/701-easy-div-rem/generate-sources
@@ -20,5 +20,3 @@
 # Write out the source file.
 mkdir src
 python3 ./genMain.py
-
-./default-build "$@"
diff --git a/test/702-LargeBranchOffset/build b/test/702-LargeBranchOffset/generate-sources
old mode 100644
new mode 100755
similarity index 96%
rename from test/702-LargeBranchOffset/build
rename to test/702-LargeBranchOffset/generate-sources
index 2505b0a..b01afc4
--- a/test/702-LargeBranchOffset/build
+++ b/test/702-LargeBranchOffset/generate-sources
@@ -20,5 +20,3 @@
 # Write out the source file.
 mkdir -p src
 ./generate
-
-./default-build "$@"
diff --git a/test/710-varhandle-creation/build b/test/710-varhandle-creation/build
deleted file mode 100644
index ca1e557..0000000
--- a/test/710-varhandle-creation/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Stop if something fails.
-set -e
-
-./default-build "$@" --experimental var-handles
diff --git a/test/807-method-handle-and-mr/build b/test/710-varhandle-creation/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/807-method-handle-and-mr/build
copy to test/710-varhandle-creation/build.py
index 12a8e18..d6b35b1
--- a/test/807-method-handle-and-mr/build
+++ b/test/710-varhandle-creation/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="var-handles")
diff --git a/test/807-method-handle-and-mr/build b/test/712-varhandle-invocations/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/807-method-handle-and-mr/build
copy to test/712-varhandle-invocations/build.py
index 12a8e18..d6b35b1
--- a/test/807-method-handle-and-mr/build
+++ b/test/712-varhandle-invocations/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="var-handles")
diff --git a/test/712-varhandle-invocations/build b/test/712-varhandle-invocations/generate-sources
similarity index 95%
rename from test/712-varhandle-invocations/build
rename to test/712-varhandle-invocations/generate-sources
index 9a6e96e..d025b26 100755
--- a/test/712-varhandle-invocations/build
+++ b/test/712-varhandle-invocations/generate-sources
@@ -31,5 +31,3 @@
 
 # Generate tests and Main that covers both the generated tests and manual tests
 python3 ./util-src/generate_java.py "${GENERATED_SRC}" ${MANUAL_TESTS}
-
-./default-build "$@" --experimental var-handles
diff --git a/test/713-varhandle-invokers/build b/test/713-varhandle-invokers/build
deleted file mode 100755
index 09d376b..0000000
--- a/test/713-varhandle-invokers/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2018 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make us exit on a failure
-set -e
-
-./default-build "$@" --experimental var-handles
diff --git a/test/807-method-handle-and-mr/build b/test/713-varhandle-invokers/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/807-method-handle-and-mr/build
copy to test/713-varhandle-invokers/build.py
index 12a8e18..d6b35b1
--- a/test/807-method-handle-and-mr/build
+++ b/test/713-varhandle-invokers/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="var-handles")
diff --git a/test/714-invoke-custom-lambda-metafactory/build b/test/714-invoke-custom-lambda-metafactory/build
deleted file mode 100644
index b5002ba..0000000
--- a/test/714-invoke-custom-lambda-metafactory/build
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2018 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make us exit on a failure
-set -e
-
-# Opt-out from desugaring to ensure offending lambda is in the DEX.
-export USE_DESUGAR=false
-./default-build "$@" --experimental method-handles
diff --git a/test/677-fsi/build b/test/714-invoke-custom-lambda-metafactory/build.py
old mode 100755
new mode 100644
similarity index 76%
copy from test/677-fsi/build
copy to test/714-invoke-custom-lambda-metafactory/build.py
index b90b408..7563062
--- a/test/677-fsi/build
+++ b/test/714-invoke-custom-lambda-metafactory/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(use_desugar=False, experimental="method-handles")
diff --git a/test/715-clinit-implicit-parameter-annotations/build b/test/715-clinit-implicit-parameter-annotations/build
deleted file mode 100644
index 2b5f92c..0000000
--- a/test/715-clinit-implicit-parameter-annotations/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2018 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make us exit on a failure
-set -e
-
-./default-build "$@" --experimental parameter-annotations
diff --git a/test/677-fsi/build b/test/715-clinit-implicit-parameter-annotations/build.py
old mode 100755
new mode 100644
similarity index 77%
copy from test/677-fsi/build
copy to test/715-clinit-implicit-parameter-annotations/build.py
index b90b408..19c759c
--- a/test/677-fsi/build
+++ b/test/715-clinit-implicit-parameter-annotations/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="parameter-annotations")
diff --git a/test/716-jli-jit-samples/build b/test/716-jli-jit-samples/build
deleted file mode 100755
index 730a8a1..0000000
--- a/test/716-jli-jit-samples/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2018 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental var-handles
diff --git a/test/807-method-handle-and-mr/build b/test/716-jli-jit-samples/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/807-method-handle-and-mr/build
copy to test/716-jli-jit-samples/build.py
index 12a8e18..d6b35b1
--- a/test/807-method-handle-and-mr/build
+++ b/test/716-jli-jit-samples/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="var-handles")
diff --git a/test/719-varhandle-concurrency/build b/test/719-varhandle-concurrency/build
deleted file mode 100755
index 98a9967..0000000
--- a/test/719-varhandle-concurrency/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2022 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Make us exit on a failure
-set -e
-
-./default-build "$@" --experimental var-handles
diff --git a/test/807-method-handle-and-mr/build b/test/719-varhandle-concurrency/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/807-method-handle-and-mr/build
copy to test/719-varhandle-concurrency/build.py
index 12a8e18..d6b35b1
--- a/test/807-method-handle-and-mr/build
+++ b/test/719-varhandle-concurrency/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="var-handles")
diff --git a/test/804-class-extends-itself/build b/test/804-class-extends-itself/generate-sources
old mode 100644
new mode 100755
similarity index 100%
rename from test/804-class-extends-itself/build
rename to test/804-class-extends-itself/generate-sources
diff --git a/test/807-method-handle-and-mr/build b/test/807-method-handle-and-mr/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/807-method-handle-and-mr/build
copy to test/807-method-handle-and-mr/build.py
index 12a8e18..e8e08eb
--- a/test/807-method-handle-and-mr/build
+++ b/test/807-method-handle-and-mr/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2017 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Exit on failure.
-set -e
+from art_build_rules import build_run_test
 
-./default-build "$@" --experimental method-handles
+build_run_test(experimental="method-handles")
diff --git a/test/817-hiddenapi/build b/test/817-hiddenapi/build
deleted file mode 100644
index 330a6de..0000000
--- a/test/817-hiddenapi/build
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2018 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set -e
-
-# Build the jars twice. First with applying hiddenapi, creating a boot jar, then
-# a second time without to create a normal jar. We need to do this because we
-# want to load the jar once as an app module and once as a member of the boot
-# class path. The DexFileVerifier would fail on the former as it does not allow
-# hidden API access flags in dex files. DexFileVerifier is not invoked on boot
-# class path dex files, so the boot jar loads fine in the latter case.
-
-export USE_HIDDENAPI=true
-./default-build "$@"
-
-# Move the jar file into the resource folder to be bundled with the test.
-mkdir res
-mv ${TEST_NAME}.jar res/boot.jar
-
-# Clear all intermediate files otherwise default-build would either skip
-# compilation or fail rebuilding.
-rm -rf classes*
-
-export USE_HIDDENAPI=false
-./default-build "$@"
diff --git a/test/674-hiddenapi/build b/test/817-hiddenapi/build.py
similarity index 81%
copy from test/674-hiddenapi/build
copy to test/817-hiddenapi/build.py
index 330a6de..1de286a 100644
--- a/test/674-hiddenapi/build
+++ b/test/817-hiddenapi/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,7 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-set -e
+from art_build_rules import build_run_test, rm
+import os
 
 # Build the jars twice. First with applying hiddenapi, creating a boot jar, then
 # a second time without to create a normal jar. We need to do this because we
@@ -23,16 +23,14 @@
 # hidden API access flags in dex files. DexFileVerifier is not invoked on boot
 # class path dex files, so the boot jar loads fine in the latter case.
 
-export USE_HIDDENAPI=true
-./default-build "$@"
+build_run_test(use_hiddenapi=True)
 
 # Move the jar file into the resource folder to be bundled with the test.
-mkdir res
-mv ${TEST_NAME}.jar res/boot.jar
+os.mkdir("res")
+os.rename("817-hiddenapi.jar", "res/boot.jar")
 
 # Clear all intermediate files otherwise default-build would either skip
 # compilation or fail rebuilding.
-rm -rf classes*
+rm("classes*")
 
-export USE_HIDDENAPI=false
-./default-build "$@"
+build_run_test(use_hiddenapi=False)
diff --git a/test/822-hiddenapi-future/build b/test/822-hiddenapi-future/build.py
similarity index 80%
rename from test/822-hiddenapi-future/build
rename to test/822-hiddenapi-future/build.py
index 02ce549..8367ad0 100644
--- a/test/822-hiddenapi-future/build
+++ b/test/822-hiddenapi-future/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2021 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+from art_build_rules import build_run_test
+
+build_run_test(use_hiddenapi=True)
diff --git a/test/829-unresolved-enclosing/build b/test/829-unresolved-enclosing/build
deleted file mode 100644
index f378df1..0000000
--- a/test/829-unresolved-enclosing/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2014 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Stop if something fails.
-set -e
-
-./default-build "$@"
diff --git a/test/948-change-annotations/build b/test/948-change-annotations/build
deleted file mode 100755
index 898e2e5..0000000
--- a/test/948-change-annotations/build
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-./default-build "$@" --experimental agents
diff --git a/test/677-fsi/build b/test/948-change-annotations/build.py
old mode 100755
new mode 100644
similarity index 79%
copy from test/677-fsi/build
copy to test/948-change-annotations/build.py
index b90b408..f0496d7
--- a/test/677-fsi/build
+++ b/test/948-change-annotations/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="agents")
diff --git a/test/677-fsi/build b/test/952-invoke-custom/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/952-invoke-custom/build.py
index b90b408..ce6dd89
--- a/test/677-fsi/build
+++ b/test/952-invoke-custom/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(use_desugar=False, api_level=28)
diff --git a/test/952-invoke-custom/build b/test/952-invoke-custom/generate-sources
similarity index 89%
rename from test/952-invoke-custom/build
rename to test/952-invoke-custom/generate-sources
index e835517..4244f8c 100755
--- a/test/952-invoke-custom/build
+++ b/test/952-invoke-custom/generate-sources
@@ -24,6 +24,3 @@
 ${JAVAC:-javac} ${JAVAC_ARGS} -cp "${ASM_JAR}" -d classes $(find util-src -name '*.java')
 ${SOONG_ZIP} --jar -o transformer.jar -C classes -D classes
 rm -rf classes
-
-# Use API level 28 for invoke-custom bytecode support.
-USE_DESUGAR=false ./default-build "$@" --api-level 28
diff --git a/test/952-invoke-custom/javac_wrapper.sh b/test/952-invoke-custom/javac_wrapper.sh
index 8659030..c8e0716 100755
--- a/test/952-invoke-custom/javac_wrapper.sh
+++ b/test/952-invoke-custom/javac_wrapper.sh
@@ -16,6 +16,8 @@
 
 set -e # Stop on error - the caller script may not have this set.
 
+export ASM_JAR="${ANDROID_BUILD_TOP}/prebuilts/misc/common/asm/asm-9.2.jar"
+
 # Update arguments to add transformer and ASM to the compiler classpath.
 classpath="./transformer.jar:$ASM_JAR"
 args=(-cp $classpath)
diff --git a/test/953-invoke-polymorphic-compiler/build b/test/953-invoke-polymorphic-compiler/build
deleted file mode 100755
index 2b0b2c1..0000000
--- a/test/953-invoke-polymorphic-compiler/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental method-handles
diff --git a/test/677-fsi/build b/test/953-invoke-polymorphic-compiler/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/953-invoke-polymorphic-compiler/build.py
index b90b408..e8e08eb
--- a/test/677-fsi/build
+++ b/test/953-invoke-polymorphic-compiler/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="method-handles")
diff --git a/test/954-invoke-polymorphic-verifier/build b/test/954-invoke-polymorphic-verifier/build
deleted file mode 100755
index 2b0b2c1..0000000
--- a/test/954-invoke-polymorphic-verifier/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental method-handles
diff --git a/test/677-fsi/build b/test/954-invoke-polymorphic-verifier/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/954-invoke-polymorphic-verifier/build.py
index b90b408..e8e08eb
--- a/test/677-fsi/build
+++ b/test/954-invoke-polymorphic-verifier/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="method-handles")
diff --git a/test/955-methodhandles-smali/build b/test/955-methodhandles-smali/build
deleted file mode 100755
index 2b0b2c1..0000000
--- a/test/955-methodhandles-smali/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental method-handles
diff --git a/test/677-fsi/build b/test/955-methodhandles-smali/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/955-methodhandles-smali/build.py
index b90b408..e8e08eb
--- a/test/677-fsi/build
+++ b/test/955-methodhandles-smali/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="method-handles")
diff --git a/test/956-methodhandles/build b/test/956-methodhandles/build
deleted file mode 100755
index 2b0b2c1..0000000
--- a/test/956-methodhandles/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental method-handles
diff --git a/test/677-fsi/build b/test/956-methodhandles/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/956-methodhandles/build.py
index b90b408..e8e08eb
--- a/test/677-fsi/build
+++ b/test/956-methodhandles/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="method-handles")
diff --git a/test/957-methodhandle-transforms/build b/test/957-methodhandle-transforms/build
deleted file mode 100755
index 2b0b2c1..0000000
--- a/test/957-methodhandle-transforms/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental method-handles
diff --git a/test/677-fsi/build b/test/957-methodhandle-transforms/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/957-methodhandle-transforms/build.py
index b90b408..e8e08eb
--- a/test/677-fsi/build
+++ b/test/957-methodhandle-transforms/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="method-handles")
diff --git a/test/958-methodhandle-stackframe/build b/test/958-methodhandle-stackframe/build
deleted file mode 100755
index 2b0b2c1..0000000
--- a/test/958-methodhandle-stackframe/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental method-handles
diff --git a/test/677-fsi/build b/test/958-methodhandle-stackframe/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/958-methodhandle-stackframe/build.py
index b90b408..e8e08eb
--- a/test/677-fsi/build
+++ b/test/958-methodhandle-stackframe/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="method-handles")
diff --git a/test/959-invoke-polymorphic-accessors/build b/test/959-invoke-polymorphic-accessors/build
deleted file mode 100644
index 2b0b2c1..0000000
--- a/test/959-invoke-polymorphic-accessors/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental method-handles
diff --git a/test/677-fsi/build b/test/959-invoke-polymorphic-accessors/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/959-invoke-polymorphic-accessors/build.py
index b90b408..e8e08eb
--- a/test/677-fsi/build
+++ b/test/959-invoke-polymorphic-accessors/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="method-handles")
diff --git a/test/677-fsi/build b/test/960-default-smali/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/960-default-smali/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/960-default-smali/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/960-default-smali/build b/test/960-default-smali/generate-sources
similarity index 93%
rename from test/960-default-smali/build
rename to test/960-default-smali/generate-sources
index 44d6bd2..1fffc71 100755
--- a/test/960-default-smali/build
+++ b/test/960-default-smali/generate-sources
@@ -19,5 +19,3 @@
 
 # Generate the Main.java file or fail
 ${ANDROID_BUILD_TOP}/art/test/utils/python/generate_java_main.py ./src
-
-./default-build "$@" --experimental default-methods
diff --git a/test/677-fsi/build b/test/961-default-iface-resolution-gen/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/961-default-iface-resolution-gen/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/961-default-iface-resolution-gen/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/961-default-iface-resolution-gen/build b/test/961-default-iface-resolution-gen/generate-sources
similarity index 93%
rename from test/961-default-iface-resolution-gen/build
rename to test/961-default-iface-resolution-gen/generate-sources
index b9b36d0..4d12e5f 100755
--- a/test/961-default-iface-resolution-gen/build
+++ b/test/961-default-iface-resolution-gen/generate-sources
@@ -21,5 +21,3 @@
 
 # Generate the smali files and expected-stdout.txt or fail
 ./util-src/generate_java.py ./src ./expected-stdout.txt
-
-./default-build "$@" --experimental default-methods
diff --git a/test/962-iface-static/build b/test/962-iface-static/build
deleted file mode 100644
index 82f4931..0000000
--- a/test/962-iface-static/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental default-methods
diff --git a/test/677-fsi/build b/test/962-iface-static/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/962-iface-static/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/962-iface-static/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/677-fsi/build b/test/964-default-iface-init-gen/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/964-default-iface-init-gen/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/964-default-iface-init-gen/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/964-default-iface-init-gen/build b/test/964-default-iface-init-gen/generate-sources
similarity index 93%
rename from test/964-default-iface-init-gen/build
rename to test/964-default-iface-init-gen/generate-sources
index b9b36d0..4d12e5f 100755
--- a/test/964-default-iface-init-gen/build
+++ b/test/964-default-iface-init-gen/generate-sources
@@ -21,5 +21,3 @@
 
 # Generate the smali files and expected-stdout.txt or fail
 ./util-src/generate_java.py ./src ./expected-stdout.txt
-
-./default-build "$@" --experimental default-methods
diff --git a/test/677-fsi/build b/test/968-default-partial-compile-gen/build.py
old mode 100755
new mode 100644
similarity index 73%
copy from test/677-fsi/build
copy to test/968-default-partial-compile-gen/build.py
index b90b408..e4416ec
--- a/test/677-fsi/build
+++ b/test/968-default-partial-compile-gen/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+import os
+
+if os.environ["BUILD_MODE"] != "jvm":
+  build_run_test(experimental="default-methods")
diff --git a/test/968-default-partial-compile-gen/build b/test/968-default-partial-compile-gen/generate-sources
similarity index 93%
rename from test/968-default-partial-compile-gen/build
rename to test/968-default-partial-compile-gen/generate-sources
index 04532b0..83aad65 100755
--- a/test/968-default-partial-compile-gen/build
+++ b/test/968-default-partial-compile-gen/generate-sources
@@ -31,6 +31,4 @@
   mkdir -p ./smali
   # Generate the smali files and expected-stdout.txt or fail
   ./util-src/generate_smali.py ./smali ./expected-stdout.txt
-  # Use the default build script
-  ./default-build "$@" --experimental default-methods
 fi
diff --git a/test/677-fsi/build b/test/969-iface-super/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/969-iface-super/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/969-iface-super/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/969-iface-super/build b/test/969-iface-super/generate-sources
similarity index 93%
rename from test/969-iface-super/build
rename to test/969-iface-super/generate-sources
index 44d6bd2..1fffc71 100755
--- a/test/969-iface-super/build
+++ b/test/969-iface-super/generate-sources
@@ -19,5 +19,3 @@
 
 # Generate the Main.java file or fail
 ${ANDROID_BUILD_TOP}/art/test/utils/python/generate_java_main.py ./src
-
-./default-build "$@" --experimental default-methods
diff --git a/test/677-fsi/build b/test/970-iface-super-resolution-gen/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/970-iface-super-resolution-gen/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/970-iface-super-resolution-gen/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/970-iface-super-resolution-gen/build b/test/970-iface-super-resolution-gen/generate-sources
similarity index 94%
rename from test/970-iface-super-resolution-gen/build
rename to test/970-iface-super-resolution-gen/generate-sources
index 6eecd71..175497f 100755
--- a/test/970-iface-super-resolution-gen/build
+++ b/test/970-iface-super-resolution-gen/generate-sources
@@ -29,5 +29,3 @@
   mkdir -p smali
   ./util-src/generate_smali.py ./smali ./expected-stdout.txt
 fi
-
-./default-build "$@" --experimental default-methods
diff --git a/test/677-fsi/build b/test/971-iface-super/build.py
old mode 100755
new mode 100644
similarity index 73%
copy from test/677-fsi/build
copy to test/971-iface-super/build.py
index b90b408..e4416ec
--- a/test/677-fsi/build
+++ b/test/971-iface-super/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+import os
+
+if os.environ["BUILD_MODE"] != "jvm":
+  build_run_test(experimental="default-methods")
diff --git a/test/971-iface-super/build b/test/971-iface-super/generate-sources
similarity index 93%
rename from test/971-iface-super/build
rename to test/971-iface-super/generate-sources
index 04532b0..83aad65 100755
--- a/test/971-iface-super/build
+++ b/test/971-iface-super/generate-sources
@@ -31,6 +31,4 @@
   mkdir -p ./smali
   # Generate the smali files and expected-stdout.txt or fail
   ./util-src/generate_smali.py ./smali ./expected-stdout.txt
-  # Use the default build script
-  ./default-build "$@" --experimental default-methods
 fi
diff --git a/test/677-fsi/build b/test/975-iface-private/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/975-iface-private/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/975-iface-private/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/978-virtual-interface/build b/test/978-virtual-interface/build
deleted file mode 100755
index 14230c2..0000000
--- a/test/978-virtual-interface/build
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Copyright 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# make us exit on a failure
-set -e
-
-./default-build "$@" --experimental default-methods
diff --git a/test/677-fsi/build b/test/978-virtual-interface/build.py
old mode 100755
new mode 100644
similarity index 78%
copy from test/677-fsi/build
copy to test/978-virtual-interface/build.py
index b90b408..48cb485
--- a/test/677-fsi/build
+++ b/test/978-virtual-interface/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(experimental="default-methods")
diff --git a/test/677-fsi/build b/test/979-const-method-handle/build.py
old mode 100755
new mode 100644
similarity index 80%
copy from test/677-fsi/build
copy to test/979-const-method-handle/build.py
index b90b408..193aa91
--- a/test/677-fsi/build
+++ b/test/979-const-method-handle/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-build "$@" --zip-compression-method store --zip-align 4
+from art_build_rules import build_run_test
+
+build_run_test(api_level=28)
diff --git a/test/979-const-method-handle/build b/test/979-const-method-handle/generate-sources
similarity index 89%
rename from test/979-const-method-handle/build
rename to test/979-const-method-handle/generate-sources
index fa6a0ea..d102d73 100755
--- a/test/979-const-method-handle/build
+++ b/test/979-const-method-handle/generate-sources
@@ -24,6 +24,3 @@
 ${JAVAC:-javac} ${JAVAC_ARGS} -cp "${ASM_JAR}" -d classes $(find util-src -name '*.java')
 ${SOONG_ZIP} --jar -o transformer.jar -C classes -D classes
 rm -rf classes
-
-# Use API level 28 for DEX file support constant method handles.
-./default-build "$@" --api-level 28
diff --git a/test/979-const-method-handle/javac_wrapper.sh b/test/979-const-method-handle/javac_wrapper.sh
index 77b6bc3..3e2a0a3 100755
--- a/test/979-const-method-handle/javac_wrapper.sh
+++ b/test/979-const-method-handle/javac_wrapper.sh
@@ -16,6 +16,8 @@
 
 set -e
 
+export ASM_JAR="${ANDROID_BUILD_TOP}/prebuilts/misc/common/asm/asm-9.2.jar"
+
 # Add annotation src files to our compiler inputs.
 asrcs=util-src/annotations/*.java
 
diff --git a/test/999-redefine-hiddenapi/build b/test/999-redefine-hiddenapi/build.py
similarity index 80%
rename from test/999-redefine-hiddenapi/build
rename to test/999-redefine-hiddenapi/build.py
index f4b029f..8367ad0 100644
--- a/test/999-redefine-hiddenapi/build
+++ b/test/999-redefine-hiddenapi/build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+from art_build_rules import build_run_test
+
+build_run_test(use_hiddenapi=True)
diff --git a/test/Android.bp b/test/Android.bp
index 9907347..1e28ac0 100644
--- a/test/Android.bp
+++ b/test/Android.bp
@@ -1868,11 +1868,13 @@
         "art_module_source_build_genrule_defaults",
     ],
     tool_files: [
+        "art_build_rules.py",
         "run-test-build.py",
         "buildfailures.json",
-        "etc/default-build",
-        "etc/default-run",
+        "etc/default-build.py",
         "etc/default-check",
+        "etc/default-run",
+        "etc/run-test-jar",
         ":art-run-test-bootclasspath",
     ],
     tools: [
diff --git a/test/art_build_rules.py b/test/art_build_rules.py
new file mode 100644
index 0000000..4e4a959
--- /dev/null
+++ b/test/art_build_rules.py
@@ -0,0 +1,403 @@
+#
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This is the default build script for run-tests.
+
+It can be overwrite by specific run-tests if needed.
+It is used from soong build and not intended to be called directly.
+"""
+
+import argparse
+import functools
+import glob
+import os
+from os import path
+import shlex
+import shutil
+import subprocess
+import tempfile
+import zipfile
+from shutil import rmtree
+from os import remove
+
+def rm(*patterns):
+  for pattern in patterns:
+    for path in glob.glob(pattern):
+      if os.path.isdir(path):
+        shutil.rmtree(path)
+      else:
+        os.remove(path)
+
+def build_run_test(
+    use_desugar=True,
+    use_hiddenapi=True,
+    need_dex=None,
+    experimental="no-experiment",
+    zip_compression_method="deflate",
+    zip_align_bytes=None,
+    api_level=None,
+    javac_args=[],
+    d8_flags=[],
+    smali_args=[],
+    has_smali=None,
+    has_jasmin=None,
+  ):
+
+  def parse_bool(text):
+    return {"true": True, "false": False}[text.lower()]
+
+  TEST_NAME = os.environ["TEST_NAME"]
+  ART_TEST_RUN_TEST_BOOTCLASSPATH = os.environ["ART_TEST_RUN_TEST_BOOTCLASSPATH"]
+  NEED_DEX = parse_bool(os.environ["NEED_DEX"]) if need_dex is None else need_dex
+
+  # Set default values for directories.
+  HAS_SMALI = path.exists("smali") if has_smali is None else has_smali
+  HAS_JASMIN = path.exists("jasmin") if has_jasmin is None else has_jasmin
+  HAS_SRC = path.exists("src")
+  HAS_SRC_ART = path.exists("src-art")
+  HAS_SRC2 = path.exists("src2")
+  HAS_SRC_MULTIDEX = path.exists("src-multidex")
+  HAS_SMALI_MULTIDEX = path.exists("smali-multidex")
+  HAS_JASMIN_MULTIDEX = path.exists("jasmin-multidex")
+  HAS_SMALI_EX = path.exists("smali-ex")
+  HAS_SRC_EX = path.exists("src-ex")
+  HAS_SRC_EX2 = path.exists("src-ex2")
+  HAS_SRC_AOTEX = path.exists("src-aotex")
+  HAS_SRC_BCPEX = path.exists("src-bcpex")
+  HAS_HIDDENAPI_SPEC = path.exists("hiddenapi-flags.csv")
+
+  JAVAC_ARGS = shlex.split(os.environ.get("JAVAC_ARGS", "")) + javac_args
+  SMALI_ARGS = shlex.split(os.environ.get("SMALI_ARGS", "")) + smali_args
+  D8_FLAGS = shlex.split(os.environ.get("D8_FLAGS", "")) + d8_flags
+
+  BUILD_MODE = os.environ["BUILD_MODE"]
+
+  # Setup experimental API level mappings in a bash associative array.
+  EXPERIMENTAL_API_LEVEL = {}
+  EXPERIMENTAL_API_LEVEL["no-experiment"] = "26"
+  EXPERIMENTAL_API_LEVEL["default-methods"] = "24"
+  EXPERIMENTAL_API_LEVEL["parameter-annotations"] = "25"
+  EXPERIMENTAL_API_LEVEL["agents"] = "26"
+  EXPERIMENTAL_API_LEVEL["method-handles"] = "26"
+  EXPERIMENTAL_API_LEVEL["var-handles"] = "28"
+
+  if BUILD_MODE == "jvm":
+    # No desugaring on jvm because it supports the latest functionality.
+    use_desugar = False
+    # Do not attempt to build src-art directories on jvm,
+    # since it would fail without libcore.
+    HAS_SRC_ART = False
+
+  # Set API level for smali and d8.
+  if not api_level:
+    api_level = EXPERIMENTAL_API_LEVEL[experimental]
+
+  # Add API level arguments to smali and dx
+  SMALI_ARGS.extend(["--api", str(api_level)])
+  D8_FLAGS.extend(["--min-api", str(api_level)])
+
+
+  def run(executable, args):
+    cmd = shlex.split(executable) + args
+    if executable.endswith(".sh"):
+      cmd = ["/bin/bash"] + cmd
+    p = subprocess.run(cmd,
+                       encoding=os.sys.stdout.encoding,
+                       stderr=subprocess.STDOUT,
+                       stdout=subprocess.PIPE)
+    if p.returncode != 0:
+      raise Exception("Command failed with exit code {}\n$ {}\n{}".format(
+                      p.returncode, " ".join(cmd), p.stdout))
+
+
+  # Helper functions to execute tools.
+  soong_zip = functools.partial(run, os.environ["SOONG_ZIP"])
+  zipalign = functools.partial(run, os.environ["ZIPALIGN"])
+  javac = functools.partial(run, os.environ["JAVAC"])
+  jasmin = functools.partial(run, os.environ["JASMIN"])
+  smali = functools.partial(run, os.environ["SMALI"])
+  d8 = functools.partial(run, os.environ["D8"])
+  hiddenapi = functools.partial(run, os.environ["HIDDENAPI"])
+
+  # If wrapper script exists, use it instead of the default javac.
+  if os.path.exists("javac_wrapper.sh"):
+    javac = functools.partial(run, "javac_wrapper.sh")
+
+  def find(root, name):
+    return sorted(glob.glob(path.join(root, "**", name), recursive=True))
+
+
+  def zip(zip_target, *files):
+    zip_args = ["-o", zip_target]
+    if zip_compression_method == "store":
+      zip_args.extend(["-L", "0"])
+    for f in files:
+      zip_args.extend(["-f", f])
+    soong_zip(zip_args)
+
+    if zip_align_bytes:
+      # zipalign does not operate in-place, so write results to a temp file.
+      with tempfile.TemporaryDirectory(dir=".") as tmp_dir:
+        tmp_file = path.join(tmp_dir, "aligned.zip")
+        zipalign(["-f", str(zip_align_bytes), zip_target, tmp_file])
+        # replace original zip target with our temp file.
+        os.rename(tmp_file, zip_target)
+
+
+  def make_jasmin(out_directory, jasmin_sources):
+    os.makedirs(out_directory, exist_ok=True)
+    jasmin(["-d", out_directory] + sorted(jasmin_sources))
+
+
+  # Like regular javac but may include libcore on the bootclasspath.
+  def javac_with_bootclasspath(args):
+    flags = JAVAC_ARGS + ["-encoding", "utf8"]
+    if BUILD_MODE != "jvm":
+      flags.extend(["-bootclasspath", ART_TEST_RUN_TEST_BOOTCLASSPATH])
+    javac(flags + args)
+
+
+  # Make a "dex" file given a directory of classes. This will be
+  # packaged in a jar file.
+  def make_dex(name):
+    d8_inputs = find(name, "*.class")
+    d8_output = name + ".jar"
+    dex_output = name + ".dex"
+    if use_desugar:
+      flags = ["--lib", ART_TEST_RUN_TEST_BOOTCLASSPATH]
+    else:
+      flags = ["--no-desugaring"]
+    assert d8_inputs
+    d8(D8_FLAGS + flags + ["--output", d8_output] + d8_inputs)
+
+    # D8 outputs to JAR files today rather than DEX files as DX used
+    # to. To compensate, we extract the DEX from d8's output to meet the
+    # expectations of make_dex callers.
+    with tempfile.TemporaryDirectory(dir=".") as tmp_dir:
+      zipfile.ZipFile(d8_output, "r").extractall(tmp_dir)
+      os.rename(path.join(tmp_dir, "classes.dex"), dex_output)
+
+
+  # Merge all the dex files.
+  # Skip non-existing files, but at least 1 file must exist.
+  def make_dexmerge(*dex_files_to_merge):
+    # Dex file that acts as the destination.
+    dst_file = dex_files_to_merge[0]
+
+    # Skip any non-existing files.
+    dex_files_to_merge = list(filter(path.exists, dex_files_to_merge))
+
+    # NB: We merge even if there is just single input.
+    # It is useful to normalize non-deterministic smali output.
+
+    with tempfile.TemporaryDirectory(dir=".") as tmp_dir:
+      d8(["--min-api", str(api_level), "--output", tmp_dir] + dex_files_to_merge)
+      assert not path.exists(path.join(tmp_dir, "classes2.dex"))
+      for input_dex in dex_files_to_merge:
+        os.remove(input_dex)
+      os.rename(path.join(tmp_dir, "classes.dex"), dst_file)
+
+
+  def make_hiddenapi(*dex_files):
+    args = ["encode"]
+    for dex_file in dex_files:
+      args.extend(["--input-dex=" + dex_file, "--output-dex=" + dex_file])
+    args.append("--api-flags=hiddenapi-flags.csv")
+    args.append("--no-force-assign-all")
+    hiddenapi(args)
+
+
+  if path.exists("classes.dex"):
+    zip(TEST_NAME + ".jar", "classes.dex")
+    return
+
+
+  def has_multidex():
+    return HAS_SRC_MULTIDEX or HAS_JASMIN_MULTIDEX or HAS_SMALI_MULTIDEX
+
+
+  def add_to_cp_args(old_cp_args, path):
+    if len(old_cp_args) == 0:
+      return ["-cp", path]
+    else:
+      return ["-cp", old_cp_args[1] + ":" + path]
+
+
+  src_tmp_all = []
+
+  if HAS_JASMIN:
+    make_jasmin("jasmin_classes", find("jasmin", "*.j"))
+    src_tmp_all = add_to_cp_args(src_tmp_all, "jasmin_classes")
+
+  if HAS_JASMIN_MULTIDEX:
+    make_jasmin("jasmin_classes2", find("jasmin-multidex", "*.j"))
+    src_tmp_all = add_to_cp_args(src_tmp_all, "jasmin_classes2")
+
+  if HAS_SRC and (HAS_SRC_MULTIDEX or HAS_SRC_AOTEX or HAS_SRC_BCPEX or
+                  HAS_SRC_EX or HAS_SRC_ART or HAS_SRC2 or HAS_SRC_EX2):
+    # To allow circular references, compile src/, src-multidex/, src-aotex/,
+    # src-bcpex/, src-ex/ together and pass the output as class path argument.
+    # Replacement sources in src-art/, src2/ and src-ex2/ can replace symbols
+    # used by the other src-* sources we compile here but everything needed to
+    # compile the other src-* sources should be present in src/ (and jasmin*/).
+    os.makedirs("classes-tmp-all")
+    javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
+                             ["-d", "classes-tmp-all"] +
+                             find("src", "*.java") +
+                             find("src-multidex", "*.java") +
+                             find("src-aotex", "*.java") +
+                             find("src-bcpex", "*.java") +
+                             find("src-ex", "*.java"))
+    src_tmp_all = add_to_cp_args(src_tmp_all, "classes-tmp-all")
+
+  if HAS_SRC_AOTEX:
+    os.makedirs("classes-aotex")
+    javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
+                             ["-d", "classes-aotex"] +
+                             find("src-aotex", "*.java"))
+    if NEED_DEX:
+      make_dex("classes-aotex")
+      # rename it so it shows up as "classes.dex" in the zip file.
+      os.rename("classes-aotex.dex", "classes.dex")
+      zip(TEST_NAME + "-aotex.jar", "classes.dex")
+
+  if HAS_SRC_BCPEX:
+    os.makedirs("classes-bcpex")
+    javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
+                             ["-d", "classes-bcpex"] +
+                             find("src-bcpex", "*.java"))
+    if NEED_DEX:
+      make_dex("classes-bcpex")
+      # rename it so it shows up as "classes.dex" in the zip file.
+      os.rename("classes-bcpex.dex", "classes.dex")
+      zip(TEST_NAME + "-bcpex.jar", "classes.dex")
+
+  if HAS_SRC:
+    os.makedirs("classes", exist_ok=True)
+    javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
+                             ["-d", "classes"] + find("src", "*.java"))
+
+  if HAS_SRC_ART:
+    os.makedirs("classes", exist_ok=True)
+    javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
+                             ["-d", "classes"] + find("src-art", "*.java"))
+
+  if HAS_SRC_MULTIDEX:
+    os.makedirs("classes2")
+    javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
+                             ["-d", "classes2"] +
+                             find("src-multidex", "*.java"))
+    if NEED_DEX:
+      make_dex("classes2")
+
+  if HAS_SRC2:
+    os.makedirs("classes", exist_ok=True)
+    javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
+                             ["-d", "classes"] +
+                             find("src2", "*.java"))
+
+  # If the classes directory is not-empty, package classes in a DEX file.
+  # NB: some tests provide classes rather than java files.
+  if find("classes", "*"):
+    if NEED_DEX:
+      make_dex("classes")
+
+  if HAS_JASMIN:
+    # Compile Jasmin classes as if they were part of the classes.dex file.
+    if NEED_DEX:
+      make_dex("jasmin_classes")
+      make_dexmerge("classes.dex", "jasmin_classes.dex")
+    else:
+      # Move jasmin classes into classes directory so that they are picked up
+      # with -cp classes.
+      os.makedirs("classes", exist_ok=True)
+      shutil.copytree("jasmin_classes", "classes", dirs_exist_ok=True)
+
+  if HAS_SMALI and NEED_DEX:
+    # Compile Smali classes
+    smali(["-JXmx512m", "assemble"] + SMALI_ARGS +
+          ["--output", "smali_classes.dex"] + find("smali", "*.smali"))
+    assert path.exists("smali_classes.dex")
+    # Merge smali files into classes.dex,
+    # this takes priority over any jasmin files.
+    make_dexmerge("classes.dex", "smali_classes.dex")
+
+  # Compile Jasmin classes in jasmin-multidex as if they were part of
+  # the classes2.jar
+  if HAS_JASMIN_MULTIDEX:
+    if NEED_DEX:
+      make_dex("jasmin_classes2")
+      make_dexmerge("classes2.dex", "jasmin_classes2.dex")
+    else:
+      # Move jasmin classes into classes2 directory so that
+      # they are picked up with -cp classes2.
+      os.makedirs("classes2", exist_ok=True)
+      shutil.copytree("jasmin_classes2", "classes2", dirs_exist_ok=True)
+      shutil.rmtree("jasmin_classes2")
+
+  if HAS_SMALI_MULTIDEX and NEED_DEX:
+    # Compile Smali classes
+    smali(["-JXmx512m", "assemble"] + SMALI_ARGS +
+          ["--output", "smali_classes2.dex"] + find("smali-multidex", "*.smali"))
+
+    # Merge smali_classes2.dex into classes2.dex
+    make_dexmerge("classes2.dex", "smali_classes2.dex")
+
+  if HAS_SRC_EX:
+    os.makedirs("classes-ex", exist_ok=True)
+    javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
+                             ["-d", "classes-ex"] + find("src-ex", "*.java"))
+
+  if HAS_SRC_EX2:
+    os.makedirs("classes-ex", exist_ok=True)
+    javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
+                             ["-d", "classes-ex"] + find("src-ex2", "*.java"))
+
+  if path.exists("classes-ex") and NEED_DEX:
+    make_dex("classes-ex")
+
+  if HAS_SMALI_EX and NEED_DEX:
+    # Compile Smali classes
+    smali(["-JXmx512m", "assemble"] + SMALI_ARGS +
+          ["--output", "smali_classes-ex.dex"] + find("smali-ex", "*.smali"))
+    assert path.exists("smali_classes-ex.dex")
+    # Merge smali files into classes-ex.dex.
+    make_dexmerge("classes-ex.dex", "smali_classes-ex.dex")
+
+  if path.exists("classes-ex.dex"):
+    # Apply hiddenapi on the dex files if the test has API list file(s).
+    if use_hiddenapi and HAS_HIDDENAPI_SPEC:
+      make_hiddenapi("classes-ex.dex")
+
+    # quick shuffle so that the stored name is "classes.dex"
+    os.rename("classes.dex", "classes-1.dex")
+    os.rename("classes-ex.dex", "classes.dex")
+    zip(TEST_NAME + "-ex.jar", "classes.dex")
+    os.rename("classes.dex", "classes-ex.dex")
+    os.rename("classes-1.dex", "classes.dex")
+
+  # Apply hiddenapi on the dex files if the test has API list file(s).
+  if NEED_DEX and use_hiddenapi and HAS_HIDDENAPI_SPEC:
+    if has_multidex():
+      make_hiddenapi("classes.dex", "classes2.dex")
+    else:
+      make_hiddenapi("classes.dex")
+
+  # Create a single dex jar with two dex files for multidex.
+  if NEED_DEX:
+    if path.exists("classes2.dex"):
+      zip(TEST_NAME + ".jar", "classes.dex", "classes2.dex")
+    else:
+      zip(TEST_NAME + ".jar", "classes.dex")
diff --git a/test/etc/default-build b/test/etc/default-build
deleted file mode 100755
index 26820f2..0000000
--- a/test/etc/default-build
+++ /dev/null
@@ -1,430 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""This is the default build script for run-tests.
-
-It can be overwrite by specific run-tests if needed.
-It is used from soong build and not intended to be called directly.
-"""
-
-import argparse
-import functools
-import glob
-import os
-from os import path
-import shlex
-import shutil
-import subprocess
-import tempfile
-import zipfile
-
-if not os.sys.argv:
-  print(
-      'Error: default-build should have the parameters from the "build" script forwarded to it'
-  )
-  print('Error: An example of how do it correctly is ./default-build "$@"')
-  os.sys.exit(1)
-
-
-def parse_bool(text):
-  return {"true": True, "false": False}[text.lower()]
-
-
-TEST_NAME = os.environ["TEST_NAME"]
-ART_TEST_RUN_TEST_BOOTCLASSPATH = os.environ["ART_TEST_RUN_TEST_BOOTCLASSPATH"]
-NEED_DEX = parse_bool(os.environ["NEED_DEX"])
-
-# Set default values for directories.
-HAS_SMALI = path.exists("smali")
-HAS_JASMIN = path.exists("jasmin")
-HAS_SRC = path.exists("src")
-HAS_SRC_ART = path.exists("src-art")
-HAS_SRC2 = path.exists("src2")
-HAS_SRC_MULTIDEX = path.exists("src-multidex")
-HAS_SMALI_MULTIDEX = path.exists("smali-multidex")
-HAS_JASMIN_MULTIDEX = path.exists("jasmin-multidex")
-HAS_SMALI_EX = path.exists("smali-ex")
-HAS_SRC_EX = path.exists("src-ex")
-HAS_SRC_EX2 = path.exists("src-ex2")
-HAS_SRC_AOTEX = path.exists("src-aotex")
-HAS_SRC_BCPEX = path.exists("src-bcpex")
-HAS_HIDDENAPI_SPEC = path.exists("hiddenapi-flags.csv")
-
-# USE_HIDDENAPI=false run-test... will disable hiddenapi.
-USE_HIDDENAPI = parse_bool(os.environ.get("USE_HIDDENAPI", "true"))
-
-# USE_DESUGAR=false run-test... will disable desugaring.
-USE_DESUGAR = parse_bool(os.environ.get("USE_DESUGAR", "true"))
-
-JAVAC_ARGS = shlex.split(os.environ.get("JAVAC_ARGS", ""))
-SMALI_ARGS = shlex.split(os.environ.get("SMALI_ARGS", ""))
-D8_FLAGS = shlex.split(os.environ.get("D8_FLAGS", ""))
-
-# Allow overriding ZIP_COMPRESSION_METHOD with e.g. 'store'
-ZIP_COMPRESSION_METHOD = "deflate"
-# Align every ZIP file made by calling $ZIPALIGN command?
-ZIP_ALIGN_BYTES = None
-
-DEV_MODE = False
-BUILD_MODE = "target"
-API_LEVEL = None
-DEFAULT_EXPERIMENT = "no-experiment"
-EXPERIMENTAL = DEFAULT_EXPERIMENT
-
-# Setup experimental API level mappings in a bash associative array.
-EXPERIMENTAL_API_LEVEL = {}
-EXPERIMENTAL_API_LEVEL[DEFAULT_EXPERIMENT] = "26"
-EXPERIMENTAL_API_LEVEL["default-methods"] = "24"
-EXPERIMENTAL_API_LEVEL["parameter-annotations"] = "25"
-EXPERIMENTAL_API_LEVEL["agents"] = "26"
-EXPERIMENTAL_API_LEVEL["method-handles"] = "26"
-EXPERIMENTAL_API_LEVEL["var-handles"] = "28"
-
-# Parse command line arguments.
-opt_bool = argparse.BooleanOptionalAction  # Bool also accepts the --no- prefix.
-parser = argparse.ArgumentParser(description=__doc__)
-parser.add_argument("--src", dest="HAS_SRC", action=opt_bool)
-parser.add_argument("--src2", dest="HAS_SRC2", action=opt_bool)
-parser.add_argument("--src-multidex", dest="HAS_SRC_MULTIDEX", action=opt_bool)
-parser.add_argument(
-    "--smali-multidex", dest="HAS_SMALI_MULTIDEX", action=opt_bool)
-parser.add_argument("--src-ex", dest="HAS_SRC_EX", action=opt_bool)
-parser.add_argument("--src-ex2", dest="HAS_SRC_EX2", action=opt_bool)
-parser.add_argument("--smali", dest="HAS_SMALI", action=opt_bool)
-parser.add_argument("--jasmin", dest="HAS_JASMIN", action=opt_bool)
-parser.add_argument("--api-level", dest="API_LEVEL", type=int)
-parser.add_argument(
-    "--experimental", dest="EXPERIMENTAL", type=str)
-parser.add_argument(
-    "--zip-compression-method", dest="ZIP_COMPRESSION_METHOD", type=str)
-parser.add_argument("--zip-align", dest="ZIP_ALIGN_BYTES", type=int)
-parser.add_argument(
-    "--host", dest="BUILD_MODE", action="store_const", const="host")
-parser.add_argument(
-    "--target", dest="BUILD_MODE", action="store_const", const="target")
-parser.add_argument(
-    "--jvm", dest="BUILD_MODE", action="store_const", const="jvm")
-parser.add_argument("--dev", dest="DEV_MODE", action=opt_bool)
-# Update variables with command line arguments that were set.
-globals().update(
-    {k: v for k, v in parser.parse_args().__dict__.items() if v is not None})
-
-if BUILD_MODE == "jvm":
-  # No desugaring on jvm because it supports the latest functionality.
-  USE_DESUGAR = False
-  # Do not attempt to build src-art directories on jvm,
-  # since it would fail without libcore.
-  HAS_SRC_ART = False
-
-# Set API level for smali and d8.
-if not API_LEVEL:
-  API_LEVEL = EXPERIMENTAL_API_LEVEL[EXPERIMENTAL]
-
-# Add API level arguments to smali and dx
-SMALI_ARGS.extend(["--api", str(API_LEVEL)])
-D8_FLAGS.extend(["--min-api", str(API_LEVEL)])
-
-
-def run(executable, args):
-  cmd = shlex.split(executable) + args
-  if executable.endswith(".sh"):
-    cmd = ["/bin/bash"] + cmd
-  if DEV_MODE:
-    print("Run:", " ".join(cmd))
-  p = subprocess.run(cmd, check=True)
-  if p.returncode != 0:
-    raise Exception("Failed command: " + " ".join(cmd))
-
-
-# Helper functions to execute tools.
-soong_zip = functools.partial(run, os.environ["SOONG_ZIP"])
-zipalign = functools.partial(run, os.environ["ZIPALIGN"])
-javac = functools.partial(run, os.environ["JAVAC"])
-jasmin = functools.partial(run, os.environ["JASMIN"])
-smali = functools.partial(run, os.environ["SMALI"])
-d8 = functools.partial(run, os.environ["D8"])
-hiddenapi = functools.partial(run, os.environ["HIDDENAPI"])
-
-# If wrapper script exists, use it instead of the default javac.
-if os.path.exists("javac_wrapper.sh"):
-  javac = functools.partial(run, "javac_wrapper.sh")
-
-def find(root, name):
-  return sorted(glob.glob(path.join(root, "**", name), recursive=True))
-
-
-def zip(zip_target, *files):
-  zip_args = ["-o", zip_target]
-  if ZIP_COMPRESSION_METHOD == "store":
-    zip_args.extend(["-L", "0"])
-  for f in files:
-    zip_args.extend(["-f", f])
-  soong_zip(zip_args)
-
-  if ZIP_ALIGN_BYTES:
-    # zipalign does not operate in-place, so write results to a temp file.
-    with tempfile.TemporaryDirectory(dir=".") as tmp_dir:
-      tmp_file = path.join(tmp_dir, "aligned.zip")
-      zipalign(["-f", str(ZIP_ALIGN_BYTES), zip_target, tmp_file])
-      # replace original zip target with our temp file.
-      os.rename(tmp_file, zip_target)
-
-
-def make_jasmin(out_directory, jasmin_sources):
-  os.makedirs(out_directory, exist_ok=True)
-  jasmin(["-d", out_directory] + sorted(jasmin_sources))
-
-
-# Like regular javac but may include libcore on the bootclasspath.
-def javac_with_bootclasspath(args):
-  flags = JAVAC_ARGS + ["-encoding", "utf8"]
-  if BUILD_MODE != "jvm":
-    flags.extend(["-bootclasspath", ART_TEST_RUN_TEST_BOOTCLASSPATH])
-  javac(flags + args)
-
-
-# Make a "dex" file given a directory of classes. This will be
-# packaged in a jar file.
-def make_dex(name):
-  d8_inputs = find(name, "*.class")
-  d8_output = name + ".jar"
-  dex_output = name + ".dex"
-  if USE_DESUGAR:
-    flags = ["--lib", ART_TEST_RUN_TEST_BOOTCLASSPATH]
-  else:
-    flags = ["--no-desugaring"]
-  assert d8_inputs
-  d8(D8_FLAGS + flags + ["--output", d8_output] + d8_inputs)
-
-  # D8 outputs to JAR files today rather than DEX files as DX used
-  # to. To compensate, we extract the DEX from d8's output to meet the
-  # expectations of make_dex callers.
-  with tempfile.TemporaryDirectory(dir=".") as tmp_dir:
-    zipfile.ZipFile(d8_output, "r").extractall(tmp_dir)
-    os.rename(path.join(tmp_dir, "classes.dex"), dex_output)
-
-
-# Merge all the dex files.
-# Skip non-existing files, but at least 1 file must exist.
-def make_dexmerge(*dex_files_to_merge):
-  # Dex file that acts as the destination.
-  dst_file = dex_files_to_merge[0]
-
-  # Skip any non-existing files.
-  dex_files_to_merge = list(filter(path.exists, dex_files_to_merge))
-
-  # NB: We merge even if there is just single input.
-  # It is useful to normalize non-deterministic smali output.
-
-  with tempfile.TemporaryDirectory(dir=".") as tmp_dir:
-    d8(["--min-api", API_LEVEL, "--output", tmp_dir] + dex_files_to_merge)
-    assert not path.exists(path.join(tmp_dir, "classes2.dex"))
-    for input_dex in dex_files_to_merge:
-      os.remove(input_dex)
-    os.rename(path.join(tmp_dir, "classes.dex"), dst_file)
-
-
-def make_hiddenapi(*dex_files):
-  args = ["encode"]
-  for dex_file in dex_files:
-    args.extend(["--input-dex=" + dex_file, "--output-dex=" + dex_file])
-  args.append("--api-flags=hiddenapi-flags.csv")
-  args.append("--no-force-assign-all")
-  hiddenapi(args)
-
-
-if path.exists("classes.dex"):
-  zip(TEST_NAME + ".jar", "classes.dex")
-  os.sys.exit(0)
-
-
-def has_multidex():
-  return HAS_SRC_MULTIDEX or HAS_JASMIN_MULTIDEX or HAS_SMALI_MULTIDEX
-
-
-def add_to_cp_args(old_cp_args, path):
-  if len(old_cp_args) == 0:
-    return ["-cp", path]
-  else:
-    return ["-cp", old_cp_args[1] + ":" + path]
-
-
-src_tmp_all = []
-
-if HAS_JASMIN:
-  make_jasmin("jasmin_classes", find("jasmin", "*.j"))
-  src_tmp_all = add_to_cp_args(src_tmp_all, "jasmin_classes")
-
-if HAS_JASMIN_MULTIDEX:
-  make_jasmin("jasmin_classes2", find("jasmin-multidex", "*.j"))
-  src_tmp_all = add_to_cp_args(src_tmp_all, "jasmin_classes2")
-
-if HAS_SRC and (HAS_SRC_MULTIDEX or HAS_SRC_AOTEX or HAS_SRC_BCPEX or
-                HAS_SRC_EX or HAS_SRC_ART or HAS_SRC2 or HAS_SRC_EX2):
-  # To allow circular references, compile src/, src-multidex/, src-aotex/,
-  # src-bcpex/, src-ex/ together and pass the output as class path argument.
-  # Replacement sources in src-art/, src2/ and src-ex2/ can replace symbols
-  # used by the other src-* sources we compile here but everything needed to
-  # compile the other src-* sources should be present in src/ (and jasmin*/).
-  os.makedirs("classes-tmp-all")
-  javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
-                           ["-d", "classes-tmp-all"] +
-                           find("src", "*.java") +
-                           find("src-multidex", "*.java") +
-                           find("src-aotex", "*.java") +
-                           find("src-bcpex", "*.java") +
-                           find("src-ex", "*.java"))
-  src_tmp_all = add_to_cp_args(src_tmp_all, "classes-tmp-all")
-
-if HAS_SRC_AOTEX:
-  os.makedirs("classes-aotex")
-  javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
-                           ["-d", "classes-aotex"] +
-                           find("src-aotex", "*.java"))
-  if NEED_DEX:
-    make_dex("classes-aotex")
-    # rename it so it shows up as "classes.dex" in the zip file.
-    os.rename("classes-aotex.dex", "classes.dex")
-    zip(TEST_NAME + "-aotex.jar", "classes.dex")
-
-if HAS_SRC_BCPEX:
-  os.makedirs("classes-bcpex")
-  javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
-                           ["-d", "classes-bcpex"] +
-                           find("src-bcpex", "*.java"))
-  if NEED_DEX:
-    make_dex("classes-bcpex")
-    # rename it so it shows up as "classes.dex" in the zip file.
-    os.rename("classes-bcpex.dex", "classes.dex")
-    zip(TEST_NAME + "-bcpex.jar", "classes.dex")
-
-if HAS_SRC:
-  os.makedirs("classes", exist_ok=True)
-  javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
-                           ["-d", "classes"] + find("src", "*.java"))
-
-if HAS_SRC_ART:
-  os.makedirs("classes", exist_ok=True)
-  javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
-                           ["-d", "classes"] + find("src-art", "*.java"))
-
-if HAS_SRC_MULTIDEX:
-  os.makedirs("classes2")
-  javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
-                           ["-d", "classes2"] +
-                           find("src-multidex", "*.java"))
-  if NEED_DEX:
-    make_dex("classes2")
-
-if HAS_SRC2:
-  os.makedirs("classes", exist_ok=True)
-  javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
-                           ["-d", "classes"] +
-                           find("src2", "*.java"))
-
-# If the classes directory is not-empty, package classes in a DEX file.
-# NB: some tests provide classes rather than java files.
-if find("classes", "*"):
-  if NEED_DEX:
-    make_dex("classes")
-
-if HAS_JASMIN:
-  # Compile Jasmin classes as if they were part of the classes.dex file.
-  if NEED_DEX:
-    make_dex("jasmin_classes")
-    make_dexmerge("classes.dex", "jasmin_classes.dex")
-  else:
-    # Move jasmin classes into classes directory so that they are picked up
-    # with -cp classes.
-    os.makedirs("classes", exist_ok=True)
-    shutil.copytree("jasmin_classes", "classes", dirs_exist_ok=True)
-
-if HAS_SMALI and NEED_DEX:
-  # Compile Smali classes
-  smali(["-JXmx512m", "assemble"] + SMALI_ARGS +
-        ["--output", "smali_classes.dex"] + find("smali", "*.smali"))
-  assert path.exists("smali_classes.dex")
-  # Merge smali files into classes.dex,
-  # this takes priority over any jasmin files.
-  make_dexmerge("classes.dex", "smali_classes.dex")
-
-# Compile Jasmin classes in jasmin-multidex as if they were part of
-# the classes2.jar
-if HAS_JASMIN_MULTIDEX:
-  if NEED_DEX:
-    make_dex("jasmin_classes2")
-    make_dexmerge("classes2.dex", "jasmin_classes2.dex")
-  else:
-    # Move jasmin classes into classes2 directory so that
-    # they are picked up with -cp classes2.
-    os.makedirs("classes2", exist_ok=True)
-    shutil.copytree("jasmin_classes2", "classes2", dirs_exist_ok=True)
-    shutil.rmtree("jasmin_classes2")
-
-if HAS_SMALI_MULTIDEX and NEED_DEX:
-  # Compile Smali classes
-  smali(["-JXmx512m", "assemble"] + SMALI_ARGS +
-        ["--output", "smali_classes2.dex"] + find("smali-multidex", "*.smali"))
-
-  # Merge smali_classes2.dex into classes2.dex
-  make_dexmerge("classes2.dex", "smali_classes2.dex")
-
-if HAS_SRC_EX:
-  os.makedirs("classes-ex", exist_ok=True)
-  javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
-                           ["-d", "classes-ex"] + find("src-ex", "*.java"))
-
-if HAS_SRC_EX2:
-  os.makedirs("classes-ex", exist_ok=True)
-  javac_with_bootclasspath(["-implicit:none"] + src_tmp_all +
-                           ["-d", "classes-ex"] + find("src-ex2", "*.java"))
-
-if path.exists("classes-ex") and NEED_DEX:
-  make_dex("classes-ex")
-
-if HAS_SMALI_EX and NEED_DEX:
-  # Compile Smali classes
-  smali(["-JXmx512m", "assemble"] + SMALI_ARGS +
-        ["--output", "smali_classes-ex.dex"] + find("smali-ex", "*.smali"))
-  assert path.exists("smali_classes-ex.dex")
-  # Merge smali files into classes-ex.dex.
-  make_dexmerge("classes-ex.dex", "smali_classes-ex.dex")
-
-if path.exists("classes-ex.dex"):
-  # Apply hiddenapi on the dex files if the test has API list file(s).
-  if USE_HIDDENAPI and HAS_HIDDENAPI_SPEC:
-    make_hiddenapi("classes-ex.dex")
-
-  # quick shuffle so that the stored name is "classes.dex"
-  os.rename("classes.dex", "classes-1.dex")
-  os.rename("classes-ex.dex", "classes.dex")
-  zip(TEST_NAME + "-ex.jar", "classes.dex")
-  os.rename("classes.dex", "classes-ex.dex")
-  os.rename("classes-1.dex", "classes.dex")
-
-# Apply hiddenapi on the dex files if the test has API list file(s).
-if NEED_DEX and USE_HIDDENAPI and HAS_HIDDENAPI_SPEC:
-  if has_multidex():
-    make_hiddenapi("classes.dex", "classes2.dex")
-  else:
-    make_hiddenapi("classes.dex")
-
-# Create a single dex jar with two dex files for multidex.
-if NEED_DEX:
-  if path.exists("classes2.dex"):
-    zip(TEST_NAME + ".jar", "classes.dex", "classes2.dex")
-  else:
-    zip(TEST_NAME + ".jar", "classes.dex")
diff --git a/test/2038-hiddenapi-jvmti-ext/build b/test/etc/default-build.py
similarity index 82%
copy from test/2038-hiddenapi-jvmti-ext/build
copy to test/etc/default-build.py
index f4b029f..7a229c7 100644
--- a/test/2038-hiddenapi-jvmti-ext/build
+++ b/test/etc/default-build.py
@@ -1,6 +1,5 @@
-#!/bin/bash
 #
-# Copyright 2018 The Android Open Source Project
+# Copyright (C) 2022 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,4 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-USE_HIDDENAPI=true ./default-build "$@"
+from art_build_rules import build_run_test
+
+build_run_test()
diff --git a/test/run-test-build.py b/test/run-test-build.py
index f8eb283..93e9599 100755
--- a/test/run-test-build.py
+++ b/test/run-test-build.py
@@ -19,7 +19,9 @@
 It is intended to be used only from soong genrule.
 """
 
-import argparse, os, tempfile, shutil, subprocess, glob, textwrap, re, json, concurrent.futures
+import argparse, os, shutil, subprocess, glob, re, json, multiprocessing, pathlib
+import art_build_rules
+from importlib.machinery import SourceFileLoader
 
 ZIP = "prebuilts/build-tools/linux-x86/bin/soong_zip"
 BUILDFAILURES = json.loads(open(os.path.join("art", "test", "buildfailures.json"), "rt").read())
@@ -41,7 +43,7 @@
   shutil.copytree(srcdir, dstdir)
 
   # Copy the default scripts if the test does not have a custom ones.
-  for name in ["build", "run", "check"]:
+  for name in ["build.py", "run", "check"]:
     src, dst = f"art/test/etc/default-{name}", join(dstdir, name)
     if os.path.exists(dst):
       shutil.copy2(src, dstdir)  # Copy default script next to the custom script.
@@ -51,18 +53,20 @@
 
   return dstdir
 
-def build_test(args, mode, dstdir):
+def build_test(args, mode, build_top, sbox, dstdir):
   """Run the build script for single run-test"""
 
   join = os.path.join
-  build_top = os.getcwd()
   java_home = os.environ.get("JAVA_HOME")
   tools_dir = os.path.abspath(join(os.path.dirname(__file__), "../../../out/bin"))
-  env = {
-    "PATH": os.environ.get("PATH"),
+  test_name = os.path.basename(dstdir)
+  env = dict(os.environ)
+  env.update({
+    "BUILD_MODE": mode,
     "ANDROID_BUILD_TOP": build_top,
+    "SBOX_PATH":   sbox,
     "ART_TEST_RUN_TEST_BOOTCLASSPATH": join(build_top, args.bootclasspath),
-    "TEST_NAME":   os.path.basename(dstdir),
+    "TEST_NAME":   test_name,
     "SOONG_ZIP":   join(build_top, "prebuilts/build-tools/linux-x86/bin/soong_zip"),
     "ZIPALIGN":    join(build_top, "prebuilts/build-tools/linux-x86/bin/zipalign"),
     "JAVA":        join(java_home, "bin/java"),
@@ -73,15 +77,23 @@
     "JASMIN":      join(tools_dir, "jasmin"),
     "SMALI":       join(tools_dir, "smali"),
     "NEED_DEX":    {"host": "true", "target": "true", "jvm": "false"}[mode],
-    "USE_DESUGAR": "true",
-  }
-  proc = subprocess.run([join(dstdir, "build"), "--" + mode],
-                        cwd=dstdir,
-                        env=env,
-                        encoding=os.sys.stdout.encoding,
-                        stderr=subprocess.STDOUT,
-                        stdout=subprocess.PIPE)
-  return proc.stdout, proc.returncode
+  })
+
+  generate_sources = join(dstdir, "generate-sources")
+  if os.path.exists(generate_sources):
+    proc = subprocess.run([generate_sources, "--" + mode],
+                          cwd=dstdir,
+                          env=env,
+                          encoding=os.sys.stdout.encoding,
+                          stderr=subprocess.STDOUT,
+                          stdout=subprocess.PIPE)
+    if proc.returncode:
+      raise Exception("Failed to generate sources for " + test_name + ":\n" + proc.stdout)
+
+  os.chdir(dstdir)
+  for name, value in env.items():
+    os.environ[name] = str(value)
+  SourceFileLoader("build_" + test_name, join(dstdir, "build.py")).load_module()
 
 def main():
   parser = argparse.ArgumentParser(description=__doc__)
@@ -91,19 +103,26 @@
   parser.add_argument("--bootclasspath", help="JAR files used for javac compilation")
   args = parser.parse_args()
 
-  with tempfile.TemporaryDirectory(prefix=os.path.basename(__file__)) as tmp:
-    srcdirs = sorted(glob.glob(os.path.join("art", "test", "*")))
-    srcdirs = filter(lambda srcdir: re.match(".*/\d*{}-.*".format(args.shard), srcdir), srcdirs)
-    dstdirs = [copy_sources(args, tmp, args.mode, srcdir) for srcdir in srcdirs]
-    dstdirs = filter(lambda dstdir: dstdir, dstdirs)  # Remove None (skipped tests).
-    with concurrent.futures.ThreadPoolExecutor() as pool:
-      for stdout, exitcode in pool.map(lambda dstdir: build_test(args, args.mode, dstdir), dstdirs):
-        if stdout:
-          print(stdout.strip())
-        assert(exitcode == 0) # Build failed. Add test to buildfailures.json if this is expected.
+  build_top = os.getcwd()
+  sbox = pathlib.Path(__file__).absolute().parent.parent.parent.parent.parent
+  assert sbox.parent.name == "sbox" and len(sbox.name) == 40
 
-    # Create the final zip file which contains the content of the temporary directory.
-    proc = subprocess.run([ZIP, "-o", args.out, "-C", tmp, "-D", tmp], check=True)
+  ziproot = os.path.join(sbox, "zip")
+  srcdirs = sorted(glob.glob(os.path.join("art", "test", "*")))
+  srcdirs = filter(lambda srcdir: re.match(".*/\d*{}-.*".format(args.shard), srcdir), srcdirs)
+  dstdirs = [copy_sources(args, ziproot, args.mode, srcdir) for srcdir in srcdirs]
+  dstdirs = filter(lambda dstdir: dstdir, dstdirs)  # Remove None (skipped tests).
+  # Use multiprocess (i.e. forking) since tests modify their current working directory.
+  with multiprocessing.Pool() as pool:
+    jobs = [(d, pool.apply_async(build_test, (args, args.mode, build_top, sbox, d))) for d in dstdirs]
+    for dstdir, job in jobs:
+      try:
+        job.get()
+      except Exception as e:
+        raise Exception("Failed to build " + os.path.basename(dstdir)) from e.__cause__
+
+  # Create the final zip file which contains the content of the temporary directory.
+  proc = subprocess.run([ZIP, "-o", args.out, "-C", ziproot, "-D", ziproot], check=True)
 
 if __name__ == "__main__":
   main()
diff --git a/test/utils/regen-test-files b/test/utils/regen-test-files
index 046f02d..934b801 100755
--- a/test/utils/regen-test-files
+++ b/test/utils/regen-test-files
@@ -335,13 +335,12 @@
     def is_blank(line):
       return re.match("^\\s*$", line)
 
-    # Is `line` a `set -e` statement?
-    def is_set_e(line):
-      return re.match("^\\s*set -e\\s*", line)
+    def is_import(line):
+      return re.match("^\\s*from .* import .*", line)
 
     # Should `line` be kept in the canonized build script?
     def keep_line(line):
-      return not (is_comment(line) or is_blank(line) or is_set_e(line))
+      return not (is_comment(line) or is_blank(line) or is_import(line))
 
     with open(build_file, "r") as f:
       lines = f.readlines()
@@ -351,7 +350,7 @@
   def can_ignore_build_script(self, build_file):
     build_script = self.canonize_build_script(build_file)
     if len(build_script) == 1:
-      if build_script[0] == "./default-build \"$@\" --experimental var-handles\n":
+      if build_script[0] == 'build_run_test(experimental="var-handles")\n':
         # Soong builds JARs with VarHandle support by default (i.e. by
         # using an API level greater or equal to 28), so we can ignore
         # build scripts that just request support for this feature.
@@ -365,8 +364,11 @@
 
     # Skip tests with non-default build rules, unless these build
     # rules can be safely ignored.
-    if os.path.isfile(os.path.join(run_test_path, "build")):
-      if not self.can_ignore_build_script(os.path.join(run_test_path, "build")):
+    if (os.path.isfile(os.path.join(run_test_path, "generate-sources")) or
+        os.path.isfile(os.path.join(run_test_path, "javac_wrapper.sh"))):
+      return False
+    if os.path.isfile(os.path.join(run_test_path, "build.py")):
+      if not self.can_ignore_build_script(os.path.join(run_test_path, "build.py")):
         return False
     # Skip tests with sources outside the `src` directory.
     for subdir in ["jasmin",