Skip to content

Commit 23def1b

Browse files
BenHenningadhiamboperesseanlip
authored
Fix part of oppia#1719, part of oppia#3709: Add build stats CI workflow (oppia#4092)
## Explanation Fixes part of oppia#1719 and oppia#3709 This PR introduces a new script & CI workflow for computing build stats to compare both AABs and universal APKs between develop and the changes in a given PR, as part of fixing oppia#1719 (though this PR doesn't cover everything outlined in that PR). This information is then detailed and uploaded as a CI build artifact, and summarized & posted as a comment in the PR. Some details included in the summary report: - APK file/download size differences - Method count differences - Feature/permission differences - New/removed resources & assets The script supports computing differences for multiple "profiles" at the same time, and the CI workflow has been set up to compute four: 1. dev 2. alpha 3. beta 4. GA This workflow will be optional since it's very expensive to run (it has to assemble 8 builds, 6 of which are Proguarded). It also doesn't really need to be run in order to approve a PR, though reviewers may insist on waiting for large or suspicious changes (such as PRs introducing new dependencies) to ensure the actual affected changes are as expected. In order to mitigate this expense, the CI workflow runs on a scheduled cron job off of develop across all open PRs and checks them in a group. It runs at most once per day (based on https://github.com/orgs/community/discussions/55768#discussioncomment-5941720) so multiple changes to a PR will be picked up with a single check in the next cron run. Currently, it will run even for a PR that hasn't changed since the last run (but this is something that can be improved in the future if it needs to be). It's being scheduled for 2:30am (02:30) UTC which seems to have a few specific benefits: - Per GitHub documentation, initiating the workflow outside the start of the hour should reduce likelihood of cancellation (since the start of the hour tends to use the most resources): https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule. - This corresponds to 7:30pm PT, 2:30am GMT, 8:00am IST, 5:30am EAT, and 12:30pm AEST (just as a basis for a different part of the world). It's actually a very nice time that shouldn't overlap with almost any development in main locations around the world, so it hopefully won't impact Oppia organization GitHub CI resources. The example output from these workflows can be observed in a few places: - Later in this PR (for back when the PR was configured to run the new workflow per PR change). - In oppia#4261 which demonstrates the large math PRs and how they changed the builds back before those were merged. - https://github.com/BenHenning/oppia-android/actions/workflows/stats.yml and https://github.com/BenHenning/oppia-android/pulls (specifically: BenHenning#14, BenHenning#13, BenHenning#12) which demonstrates the workflow running correctly from a scheduled cron (https://github.com/BenHenning/oppia-android/actions/runs/9232187176) and posting the updates to open PRs. Beyond that, implementing this utility involved several significant changes to various systems, including the build graph: - Three new utilities were added for the script: Aapt2Client, ApkAnalyzerClient, and BundleToolClient. Each of these correspond to Android CLI utilities, but each required special considerations: - Aapt2Client requires direct access to the Android SDK, but fortunately android_sdk_repository exposes this as a target so it's trivial to pass it in & call it. Some build information is needed, too (see next outer point). - ApkAnalyzerClient couldn't use the apkanalyzer CLI contained within the SDK since it's not exported by android_sdk_repository. Instead, we needed to depend on the CLI's internal implementation library (which I suspect is what Android Studio probably uses for its own APK Analyzer tool). This required some new implementation. - BundleToolClient fortunately can call right into the bundle tool library that we use when building AABs, but unfortunately that tool appears to not be designed to be called multiple times in the same process. Because Java doesn't support forking, we actually needed to fake a fork function by starting a new Java process using the current process's classpath in order to re-run bundle tool for each needed routine. Additionally, bundle tool required https://github.com/oppia/archive-patcher (which needed new BUILD files since it only supported Gradle building previously) and a non-Android version of Guava (see below for the changes this has caused). - A new build_vars.bzl was introduced to define the build SDK & build tools versions (this is done in a way where they can actually be passed to the new script's utilities since it needs to access aapt2). - rules_kotlin had a bug where resources wouldn't be pulled in properly for kt_jvm_library (see bazelbuild/rules_kotlin#281), but this was mitigated in a previous PR by upgrading rules_kotlin past alpha 2. - The new functionality required the JRE-compatible version of Guava (over the Android-constrained library used in the codebase today), but this introduces a one-version issue. The solution ended up being isolating the JRE-compatible Guava library to its own library with a slightly hacky direct reference to it in BundleToolClient. Some of the other attempts at solving this resulted in some Maven reference cleanups in existing script documentation. This functionality will be improved in downstream PRs, but other attempts that were originally made to isolate this cleanly were: - Introduce multiple maven_install files and isolate dependencies into: production, tests, scripts. This has a number of nice benefits (more correct licenses and faster Maven dependency fetches for production), but it results in very tricky one-version violations for test targets that cross dependencies between production and tests. - Isolated maven_install just for scripts. This is closer to the solution we'll want long-term, but it was too much complexity to fully introduce in this PR so it's been reworked into a downstream PR that can focus on cleaning up third-party dependency management across the whole codebase. This PR is introducing a few new dependencies that, in turn, pull in a *bunch* of transitive dependencies. These are all due to the new ``apkanalyzer`` dependency. While it will affect licenses for this specific PR, once third-party dependencies for scripts are cleaned up in a downstream PR they will be moved out (since they are script-only dependencies). Separately, also note that the AAPT2 utility requires stdout to be processed continuously in order for the process to finish. This was one of the primary reasons CommandExecutorImpl was reworked in oppia#4929. For testing: most of the changes in this PR have been extensively manually tested. However, the new utilities are lacking significant automated tests. Since this utility is a nice-to-have for the rest of the Bazel PR chain, it's being prioritized to be merged in spite of lacking code coverage. oppia#4971 has been filed to track adding these missing tests in the long-term. ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). ## For UI-specific PRs only N/A -- This only affects CI workflows & the build system. Technically, some dependency changes in the build system could have UI effects, but there should be no such changes in this PR. --------- Co-authored-by: Adhiambo Peres <[email protected]> Co-authored-by: Sean Lip <[email protected]>
1 parent 4c7f811 commit 23def1b

29 files changed

+4039
-74
lines changed

.github/workflows/stats.yml

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# Contains jobs corresponding to stats, including build stats due to changes in a PR.
2+
3+
name: Stats Checks & Reports
4+
5+
on:
6+
workflow_dispatch:
7+
schedule:
8+
- cron: "30 02 * * *"
9+
10+
permissions:
11+
pull-requests: write
12+
13+
jobs:
14+
find_open_pull_requests:
15+
name: Find open PRs
16+
runs-on: ubuntu-20.04
17+
outputs:
18+
matrix: ${{ steps.compute-pull-request-matrix.outputs.matrix }}
19+
env:
20+
GH_TOKEN: ${{ github.token }}
21+
steps:
22+
- uses: actions/checkout@v4
23+
24+
- name: Compute PR matrix
25+
id: compute-pull-request-matrix
26+
# Remove spaces to ensure the matrix output is on one line. Reference:
27+
# https://stackoverflow.com/a/3232433.
28+
run: |
29+
CURRENT_OPEN_PR_INFO="$(gh pr list --json number,baseRefName,headRefName,headRepository,headRepositoryOwner | tr -d '[:space:]')"
30+
echo "matrix={\"prInfo\": $CURRENT_OPEN_PR_INFO}" >> "$GITHUB_OUTPUT"
31+
32+
build_stats:
33+
name: Build Stats
34+
needs: find_open_pull_requests
35+
runs-on: ubuntu-20.04
36+
# Reduce parallelization due to high build times, and allow individual PRs to fail.
37+
strategy:
38+
fail-fast: false
39+
max-parallel: 5
40+
matrix: ${{ fromJson(needs.find_open_pull_requests.outputs.matrix) }}
41+
env:
42+
ENABLE_CACHING: false
43+
CACHE_DIRECTORY: ~/.bazel_cache
44+
steps:
45+
- name: Compute PR head owner/repo reference
46+
env:
47+
PR_HEAD_REPO: ${{ matrix.prInfo.headRepository.name }}
48+
PR_HEAD_REPO_OWNER: ${{ matrix.prInfo.headRepositoryOwner.login }}
49+
run: |
50+
echo "PR_HEAD=$PR_HEAD_REPO_OWNER/$PR_HEAD_REPO" >> "$GITHUB_ENV"
51+
- name: Print PR information for this run
52+
env:
53+
PR_BASE_REF_NAME: ${{ matrix.prInfo.baseRefName }}
54+
PR_HEAD_REF_NAME: ${{ matrix.prInfo.headRefName }}
55+
PR_NUMBER: ${{ matrix.prInfo.number }}
56+
run: |
57+
echo "PR $PR_NUMBER is merging into $PR_BASE_REF_NAME from https://github.com/$PR_HEAD branch $PR_HEAD_REF_NAME."
58+
59+
- name: Set up JDK 9
60+
uses: actions/setup-java@v1
61+
with:
62+
java-version: 9
63+
64+
- name: Set up Bazel
65+
uses: abhinavsingh/setup-bazel@v3
66+
with:
67+
version: 4.0.0
68+
69+
# For reference on this & the later cache actions, see:
70+
# https://github.com/actions/cache/issues/239#issuecomment-606950711 &
71+
# https://github.com/actions/cache/issues/109#issuecomment-558771281. Note that these work
72+
# with Bazel since Bazel can share the most recent cache from an unrelated build and still
73+
# benefit from incremental build performance (assuming that actions/cache aggressively removes
74+
# older caches due to the 5GB cache limit size & Bazel's large cache size).
75+
- uses: actions/cache@v2
76+
id: cache
77+
with:
78+
path: ${{ env.CACHE_DIRECTORY }}
79+
key: ${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-binary-${{ github.sha }}
80+
restore-keys: |
81+
${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-binary-
82+
${{ runner.os }}-${{ env.CACHE_DIRECTORY }}-bazel-
83+
84+
# This check is needed to ensure that Bazel's unbounded cache growth doesn't result in a
85+
# situation where the cache never updates (e.g. due to exceeding GitHub's cache size limit)
86+
# thereby only ever using the last successful cache version. This solution will result in a
87+
# few slower CI actions around the time cache is detected to be too large, but it should
88+
# incrementally improve thereafter.
89+
- name: Ensure cache size
90+
env:
91+
BAZEL_CACHE_DIR: ${{ env.CACHE_DIRECTORY }}
92+
run: |
93+
# See https://stackoverflow.com/a/27485157 for reference.
94+
EXPANDED_BAZEL_CACHE_PATH="${BAZEL_CACHE_DIR/#\~/$HOME}"
95+
CACHE_SIZE_MB=$(du -smc $EXPANDED_BAZEL_CACHE_PATH | grep total | cut -f1)
96+
echo "Total size of Bazel cache (rounded up to MBs): $CACHE_SIZE_MB"
97+
# Use a 4.5GB threshold since actions/cache compresses the results, and Bazel caches seem
98+
# to only increase by a few hundred megabytes across changes for unrelated branches. This
99+
# is also a reasonable upper-bound (local tests as of 2021-03-31 suggest that a full build
100+
# of the codebase (e.g. //...) from scratch only requires a ~2.1GB uncompressed/~900MB
101+
# compressed cache).
102+
if [[ "$CACHE_SIZE_MB" -gt 4500 ]]; then
103+
echo "Cache exceeds cut-off; resetting it (will result in a slow build)"
104+
rm -rf $EXPANDED_BAZEL_CACHE_PATH
105+
fi
106+
107+
- name: Configure Bazel to use a local cache
108+
env:
109+
BAZEL_CACHE_DIR: ${{ env.CACHE_DIRECTORY }}
110+
run: |
111+
EXPANDED_BAZEL_CACHE_PATH="${BAZEL_CACHE_DIR/#\~/$HOME}"
112+
echo "Using $EXPANDED_BAZEL_CACHE_PATH as Bazel's cache path"
113+
echo "build --disk_cache=$EXPANDED_BAZEL_CACHE_PATH" >> $HOME/.bazelrc
114+
shell: bash
115+
116+
# This checks out the actual true develop branch separately to ensure that the stats check is
117+
# run from the latest develop rather than the base branch (which might be different for
118+
# chained PRs).
119+
- name: Check out develop repository
120+
uses: actions/checkout@v4
121+
with:
122+
path: develop
123+
124+
- name: Set up build environment
125+
uses: ./develop/.github/actions/set-up-android-bazel-build-environment
126+
127+
- name: Check Bazel environment
128+
run: |
129+
cd develop
130+
bazel info
131+
132+
- name: Check out base repository and branch
133+
env:
134+
PR_BASE_REF_NAME: ${{ matrix.prInfo.baseRefName }}
135+
uses: actions/checkout@v4
136+
with:
137+
fetch-depth: 0
138+
ref: ${{ env.PR_BASE_REF_NAME }}
139+
path: base
140+
141+
- name: Check out head repository and branch
142+
env:
143+
PR_HEAD_REF_NAME: ${{ matrix.prInfo.headRefName }}
144+
uses: actions/checkout@v4
145+
with:
146+
fetch-depth: 0
147+
repository: ${{ env.PR_HEAD }}
148+
ref: ${{ env.PR_HEAD_REF_NAME }}
149+
path: head
150+
151+
# Note that Bazel is shutdown between builds since multiple Bazel servers will otherwise end
152+
# up being active (due to multiple repositories being used) and this can quickly overwhelm CI
153+
# worker resources.
154+
- name: Build Oppia dev, alpha, beta, and GA (feature branch)
155+
run: |
156+
cd head
157+
git log -n 1
158+
bazel build -- //:oppia_dev //:oppia_alpha //:oppia_beta //:oppia_ga
159+
cp bazel-bin/oppia_dev.aab ../develop/oppia_dev_with_changes.aab
160+
cp bazel-bin/oppia_alpha.aab ../develop/oppia_alpha_with_changes.aab
161+
cp bazel-bin/oppia_beta.aab ../develop/oppia_beta_with_changes.aab
162+
cp bazel-bin/oppia_ga.aab ../develop/oppia_ga_with_changes.aab
163+
bazel shutdown
164+
165+
- name: Build Oppia dev, alpha, beta, and GA (base branch)
166+
run: |
167+
cd base
168+
git log -n 1
169+
bazel build -- //:oppia_dev //:oppia_alpha //:oppia_beta //:oppia_ga
170+
cp bazel-bin/oppia_dev.aab ../develop/oppia_dev_without_changes.aab
171+
cp bazel-bin/oppia_alpha.aab ../develop/oppia_alpha_without_changes.aab
172+
cp bazel-bin/oppia_beta.aab ../develop/oppia_beta_without_changes.aab
173+
cp bazel-bin/oppia_ga.aab ../develop/oppia_ga_without_changes.aab
174+
bazel shutdown
175+
176+
- name: Run stats analysis tool (develop branch)
177+
run: |
178+
cd develop
179+
git log -n 1
180+
bazel run //scripts:compute_aab_differences -- \
181+
$(pwd)/brief_build_summary.log $(pwd)/full_build_summary.log \
182+
dev $(pwd)/oppia_dev_without_changes.aab $(pwd)/oppia_dev_with_changes.aab \
183+
alpha $(pwd)/oppia_alpha_without_changes.aab $(pwd)/oppia_alpha_with_changes.aab \
184+
beta $(pwd)/oppia_beta_without_changes.aab $(pwd)/oppia_beta_with_changes.aab \
185+
ga $(pwd)/oppia_ga_without_changes.aab $(pwd)/oppia_ga_with_changes.aab
186+
187+
# Reference: https://github.com/peter-evans/create-or-update-comment#setting-the-comment-body-from-a-file.
188+
# Also, for multi-line env values, see: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings.
189+
- name: Extract reports for uploading & commenting
190+
env:
191+
PR_NUMBER: ${{ matrix.prInfo.number }}
192+
id: compute-comment-body
193+
run: |
194+
{
195+
echo 'comment_body<<EOF'
196+
cat $GITHUB_WORKSPACE/develop/brief_build_summary.log
197+
echo EOF
198+
} >> "$GITHUB_OUTPUT"
199+
FULL_BUILD_SUMMARY_FILE_NAME="full_build_summary_pr_$PR_NUMBER.log"
200+
FULL_BUILD_SUMMARY_FILE_PATH="$GITHUB_WORKSPACE/develop/$FULL_BUILD_SUMMARY_FILE_NAME"
201+
echo "FULL_BUILD_SUMMARY_FILE_NAME=$FULL_BUILD_SUMMARY_FILE_NAME" >> "$GITHUB_ENV"
202+
echo "FULL_BUILD_SUMMARY_FILE_PATH=$FULL_BUILD_SUMMARY_FILE_PATH" >> "$GITHUB_ENV"
203+
cp "$GITHUB_WORKSPACE/develop/full_build_summary.log" "$FULL_BUILD_SUMMARY_FILE_PATH"
204+
205+
- name: Add build stats summary comment
206+
env:
207+
PR_NUMBER: ${{ matrix.prInfo.number }}
208+
uses: peter-evans/create-or-update-comment@v1
209+
with:
210+
issue-number: ${{ env.PR_NUMBER }}
211+
body: ${{ steps.compute-comment-body.outputs.comment_body }}
212+
213+
- uses: actions/upload-artifact@v2
214+
with:
215+
name: ${{ env.FULL_BUILD_SUMMARY_FILE_NAME }}
216+
path: ${{ env.FULL_BUILD_SUMMARY_FILE_PATH }}

WORKSPACE

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ This file lists and imports all external dependencies needed to build Oppia Andr
44

55
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
66
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_jar")
7+
load("//:build_vars.bzl", "BUILD_SDK_VERSION", "BUILD_TOOLS_VERSION")
78
load("//third_party:versions.bzl", "HTTP_DEPENDENCY_VERSIONS", "MAVEN_REPOSITORIES", "get_maven_dependencies")
89

910
# Android SDK configuration. For more details, see:
1011
# https://docs.bazel.build/versions/master/be/android.html#android_sdk_repository
1112
# TODO(#1542): Sync Android SDK version with the manifest.
1213
android_sdk_repository(
1314
name = "androidsdk",
14-
api_level = 33,
15-
build_tools_version = "29.0.2",
15+
api_level = BUILD_SDK_VERSION,
16+
build_tools_version = BUILD_TOOLS_VERSION,
1617
)
1718

1819
# Oppia's backend proto API definitions.
@@ -160,6 +161,13 @@ git_repository(
160161
shallow_since = "1679426649 -0700",
161162
)
162163

164+
git_repository(
165+
name = "archive_patcher",
166+
commit = "d1c18b0035d5f669ddaefadade49cae0748f9df2",
167+
remote = "https://github.com/oppia/archive-patcher",
168+
shallow_since = "1642022460 -0800",
169+
)
170+
163171
bind(
164172
name = "databinding_annotation_processor",
165173
actual = "//tools/android:compiler_annotation_processor",
@@ -214,17 +222,24 @@ load("@maven//:defs.bzl", "pinned_maven_install")
214222

215223
pinned_maven_install()
216224

217-
http_jar(
218-
name = "guava_android",
219-
sha256 = HTTP_DEPENDENCY_VERSIONS["guava_android"]["sha"],
220-
urls = [
221-
"{0}/com/google/guava/guava/{1}-android/guava-{1}-android.jar".format(
222-
url_base,
223-
HTTP_DEPENDENCY_VERSIONS["guava_android"]["version"],
224-
)
225-
for url_base in DAGGER_REPOSITORIES + MAVEN_REPOSITORIES
226-
],
227-
)
225+
[
226+
http_jar(
227+
name = "guava_%s" % guava_type,
228+
sha256 = HTTP_DEPENDENCY_VERSIONS["guava_%s" % guava_type]["sha"],
229+
urls = [
230+
"{0}/com/google/guava/guava/{1}-{2}/guava-{1}-{2}.jar".format(
231+
url_base,
232+
HTTP_DEPENDENCY_VERSIONS["guava_%s" % guava_type]["version"],
233+
guava_type,
234+
)
235+
for url_base in DAGGER_REPOSITORIES + MAVEN_REPOSITORIES
236+
],
237+
)
238+
for guava_type in [
239+
"android",
240+
"jre",
241+
]
242+
]
228243

229244
http_jar(
230245
name = "kotlinx-coroutines-core-jvm",

build_vars.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
BUILD_SDK_VERSION = 33
2+
BUILD_TOOLS_VERSION = "29.0.2"

oppia_android_application.bzl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ _bundle_module_zip_into_deployable_aab = rule(
280280
"_bundletool_tool": attr.label(
281281
executable = True,
282282
cfg = "host",
283-
default = "//third_party:android_bundletool",
283+
default = "//third_party:android_bundletool_binary",
284284
),
285285
},
286286
implementation = _bundle_module_zip_into_deployable_aab_impl,
@@ -316,7 +316,7 @@ _generate_apks_and_install = rule(
316316
"_bundletool_tool": attr.label(
317317
executable = True,
318318
cfg = "host",
319-
default = "//third_party:android_bundletool",
319+
default = "//third_party:android_bundletool_binary",
320320
),
321321
},
322322
executable = True,

scripts/BUILD.bazel

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ package_group(
4242
packages = ["//scripts/src/java/..."],
4343
)
4444

45+
kt_jvm_binary(
46+
name = "compute_aab_differences",
47+
testonly = True,
48+
data = ["@androidsdk//:aapt2_binary"],
49+
main_class = "org.oppia.android.scripts.apkstats.ComputeAabDifferencesKt",
50+
runtime_deps = [
51+
"//scripts/src/java/org/oppia/android/scripts/apkstats:compute_aab_differences_lib",
52+
],
53+
)
54+
4555
kt_jvm_binary(
4656
name = "compute_affected_tests",
4757
testonly = True,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.oppia.android.scripts.apkstats
2+
3+
import org.oppia.android.scripts.common.CommandExecutor
4+
import org.oppia.android.scripts.common.CommandExecutorImpl
5+
import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
6+
import java.io.File
7+
8+
/**
9+
* General utility for interfacing with AAPT2 in the local system at the specified working directory
10+
* path and contained within the specified Android SDK (per the given path).
11+
*
12+
* Note that in order for binary dependencies to utilize this client, they must add a 'data'
13+
* dependency on the AAPT2 binary included as part of the Android SDK, e.g.:
14+
*
15+
* ```bazel
16+
* data = ["@androidsdk//:aapt2_binary"]
17+
* ```
18+
*
19+
* @property workingDirectoryPath the path to the working directory in which instances of AAPT2
20+
* should be executed
21+
* @property buildToolsVersion the version of Android build tools installed & that should be used.
22+
* This value should be coordinated with the build system used by the APKs accessed by this
23+
* utility.
24+
* @param scriptBgDispatcher the [ScriptBackgroundCoroutineDispatcher] to be used for running the
25+
* AAPT2 command
26+
* @property commandExecutor the [CommandExecutor] to use when accessing AAPT2
27+
*/
28+
class Aapt2Client(
29+
private val workingDirectoryPath: String,
30+
private val buildToolsVersion: String,
31+
scriptBgDispatcher: ScriptBackgroundCoroutineDispatcher,
32+
private val commandExecutor: CommandExecutor = CommandExecutorImpl(scriptBgDispatcher)
33+
) {
34+
private val workingDirectory by lazy { File(workingDirectoryPath) }
35+
// Note that this pathing will not work by default on Windows (since executables end with '.exe').
36+
private val aapt2Path by lazy {
37+
File("external/androidsdk", "build-tools/$buildToolsVersion/aapt2").absolutePath
38+
}
39+
40+
// CLI reference: https://developer.android.com/studio/command-line/apkanalyzer.
41+
42+
/** Returns the permissions dump as reported by AAPT2 for the specified APK. */
43+
fun dumpPermissions(inputApkPath: String): List<String> {
44+
return executeApkAnalyzerCommand("dump", "permissions", inputApkPath)
45+
}
46+
47+
/** Returns the resources dump as reported by AAPT2 for the specified APK. */
48+
fun dumpResources(inputApkPath: String): List<String> {
49+
return executeApkAnalyzerCommand("dump", "resources", inputApkPath)
50+
}
51+
52+
/**
53+
* Returns badging information, that is, high-level details like supported locales and densities,
54+
* from the specified APK's manifest.
55+
*/
56+
fun dumpBadging(inputApkPath: String): List<String> {
57+
return executeApkAnalyzerCommand("dump", "badging", inputApkPath)
58+
}
59+
60+
private fun executeApkAnalyzerCommand(vararg arguments: String): List<String> {
61+
val result = commandExecutor.executeCommand(workingDirectory, aapt2Path, *arguments)
62+
check(result.exitCode == 0) {
63+
"Expected zero exit code (not ${result.exitCode}) for command: ${result.command}." +
64+
"\nStandard output:\n${result.output.joinToString("\n")}" +
65+
"\nError output:\n${result.errorOutput.joinToString("\n")}"
66+
}
67+
return result.output
68+
}
69+
}

0 commit comments

Comments
 (0)