Skip to content

Commit 3dcf4b4

Browse files
cwd-googV8 LUCI CQ
authored andcommitted
codegen scripts for android_protoc
Script to check out Android frameworks base and proto_logging to get all the proto definitions needed for the --proto output from adb shell dumpsys. Change-Id: Id41161587effa13bcbcc2343113efbc5635829d3 Reviewed-on: https://chromium-review.googlesource.com/c/crossbench/+/6638793 Reviewed-by: Camillo Bruni <[email protected]> Commit-Queue: Charles Dick <[email protected]>
1 parent 6a043b4 commit 3dcf4b4

File tree

6 files changed

+208
-2
lines changed

6 files changed

+208
-2
lines changed

.yapfignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
android_protoc/frameworks/**
2+
android_protoc/__init__.py

PRESUBMIT.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def CheckChange(input_api, output_api, on_commit):
4747
input_api,
4848
output_api,
4949
files_to_check=pylint_file_patterns_to_check,
50+
files_to_skip=[r"^android_protoc/frameworks.*", r"^third_party/.*"],
5051
pylintrc=".pylintrc",
5152
version="3.2")
5253

@@ -164,8 +165,11 @@ def MypyFilesToCheck(input_api, on_commit, modified_py_files) -> list[str]:
164165
# TODO: enable mypy on all tests
165166
result = []
166167
for file in mypy_files_to_check:
167-
if not file.startswith("tests/"):
168-
result.append(file)
168+
if file.startswith("tests/"):
169+
continue
170+
if file.startswith("android_protoc/"):
171+
continue
172+
result.append(file)
169173
return result
170174

171175

android_protoc/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Android Proto
2+
This directory contains compiled proto files from Android frameworks.
3+
4+
To update the list of checked in proto files, add the module you want to
5+
PROTO_MODULES in codegen/gen.py and then run it. You will need git and protoc in
6+
your environment.
7+
8+
To use generated protos, import directly from android_proto, e.g.:
9+
```
10+
from android_protoc import activitymanagerservice_pb2
11+
```

android_protoc/codegen/gen.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 The Chromium Authors
3+
# Use of this source code is governed by a BSD-style license that can be
4+
# found in the LICENSE file.
5+
6+
# Script to generate Android proto parsing files.
7+
#
8+
# Does a sparse checkout of the required Android repos, and call protoc on the
9+
# minimum required proto files to have working implementations of the types
10+
# listed in PROTO_MODULES below.
11+
#
12+
# Also generates __init__.py that exports the types listed in PROTO_MODULES, for
13+
# easier importing into Crossbench.
14+
15+
from contextlib import contextmanager
16+
import importlib
17+
import os
18+
import re
19+
import shutil
20+
import subprocess
21+
22+
from pathlib import Path
23+
import sys
24+
25+
# List of modules we want to depend on. We will protoc this module and all its
26+
# dependencies.
27+
PROTO_MODULES = [
28+
"frameworks.base.core.proto.android.server.activitymanagerservice_pb2"
29+
]
30+
31+
# Map of repo name to optional list of folders to sparse checkout
32+
FRAMEWORKS_REPOS = {
33+
"base": ["core/proto"],
34+
"proto_logging": None,
35+
}
36+
37+
38+
@contextmanager
39+
def change_cwd(cwd):
40+
previous_cwd = os.getcwd()
41+
try:
42+
os.chdir(str(cwd))
43+
yield
44+
finally:
45+
os.chdir(previous_cwd)
46+
47+
48+
def run_and_log(args):
49+
print("Running: ", " ".join(args))
50+
subprocess.run(args, check=True)
51+
52+
53+
def checkout_frameworks_repo(root_dir, repo_name, sparse_dirs):
54+
repo_dir = root_dir / ".protos" / "frameworks" / repo_name
55+
repo_dir.mkdir(parents=True)
56+
with change_cwd(repo_dir):
57+
run_and_log(["git", "init"])
58+
url = f"https://android.googlesource.com/platform/frameworks/{repo_name}"
59+
run_and_log(["git", "remote", "add", "origin", url])
60+
if sparse_dirs:
61+
run_and_log(["git", "config", "core.sparsecheckout", "true"])
62+
with (repo_dir / ".git" / "info" / "sparse-checkout").open(
63+
"a", encoding="utf8") as sparse_file:
64+
sparse_file.writelines(sparse_dirs)
65+
run_and_log([
66+
"git", "fetch", "--filter=blob:none", "--depth", "1", "origin", "main"
67+
])
68+
run_and_log(["git", "checkout", "main"])
69+
70+
71+
# Patterns to parse import errors when a module has not been compiled yet.
72+
MISSING_MODULE_RE = re.compile("^No module named '([^']*)'")
73+
MISSING_FILE_RE = re.compile("^cannot import name '([^']*)_pb2' from '([^']*)'")
74+
75+
76+
def compile_proto(protos_dir, proto_file):
77+
# TODO: check that the proto file is inside one of the repos.
78+
print(f"compiling '{proto_file}'")
79+
run_and_log(
80+
["protoc", "--python_out=.", f"--proto_path={protos_dir}", proto_file])
81+
82+
83+
def compile_imports(root_dir, protos_dir, module_name):
84+
with change_cwd(root_dir):
85+
while True:
86+
try:
87+
importlib.invalidate_caches()
88+
importlib.import_module(module_name)
89+
break
90+
except ImportError as err:
91+
module_missing = MISSING_MODULE_RE.match(str(err))
92+
if module_missing:
93+
dir_name = module_missing[1].replace(".", "/")
94+
if dir_name.endswith("_pb2"):
95+
compile_proto(protos_dir, dir_name[:-4] + ".proto")
96+
continue
97+
print(f"making folder '{dir_name}'")
98+
99+
dir_path = root_dir / dir_name
100+
dir_path.mkdir()
101+
# this folder is a module
102+
with (dir_path / "__init__.py").open("x", encoding="utf8"):
103+
pass
104+
continue
105+
106+
file_missing = MISSING_FILE_RE.match(str(err))
107+
if file_missing:
108+
file_name = f"{file_missing[1]}.proto"
109+
dir_name = file_missing[2].replace(".", "/")
110+
proto_file = os.path.join(dir_name, file_name)
111+
compile_proto(protos_dir, proto_file)
112+
continue
113+
raise err
114+
115+
116+
ROOT_MODULE_PROLOGUE = '''# Copyright 2025 The Chromium Authors
117+
# Use of this source code is governed by a BSD-style license that can be
118+
# found in the LICENSE file.
119+
120+
# This file is generated by android_protoc/codegen/gen.py
121+
122+
from android_protoc.sys_path import android_protoc_in_sys_path
123+
124+
with android_protoc_in_sys_path():
125+
'''
126+
127+
128+
def write_root_module(root_dir):
129+
with (root_dir / "__init__.py").open("w", encoding="utf8") as root_module:
130+
root_module.write(ROOT_MODULE_PROLOGUE)
131+
for module_name in PROTO_MODULES:
132+
module_parts = module_name.split(".")
133+
from_name = ".".join(module_parts[0:-1])
134+
import_name = module_parts[-1]
135+
root_module.write(f" from {from_name} import {import_name}\n")
136+
137+
138+
def main():
139+
root_dir = Path(__file__).parent.parent.resolve()
140+
protos_dir = root_dir / ".protos"
141+
frameworks_dir = root_dir / "frameworks"
142+
143+
# Delete all checked out and generated code.
144+
if protos_dir.exists():
145+
shutil.rmtree(protos_dir)
146+
if frameworks_dir.exists():
147+
shutil.rmtree(frameworks_dir)
148+
149+
# Check out a fresh copy of all repos.
150+
for repo, sparse in FRAMEWORKS_REPOS.items():
151+
checkout_frameworks_repo(root_dir, repo, sparse)
152+
153+
# Put python_out dir in the python search path, so that once we protoc a file,
154+
# python will find it.
155+
sys.path.insert(0, str(root_dir))
156+
157+
for module in PROTO_MODULES:
158+
print("Compiling all dependencies for ", module)
159+
compile_imports(root_dir, protos_dir, module)
160+
161+
write_root_module(root_dir)
162+
163+
# Delete the checked out Android code.
164+
if protos_dir.exists():
165+
shutil.rmtree(protos_dir)
166+
167+
168+
if not __name__ == "__main__":
169+
raise RuntimeError("protoc should not be imported. Execute it as a script")
170+
171+
main()

android_protoc/sys_path.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2025 The Chromium Authors
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
from contextlib import contextmanager
6+
from pathlib import Path
7+
import sys
8+
9+
10+
@contextmanager
11+
def android_protoc_in_sys_path():
12+
prev_path = sys.path
13+
sys.path = [str(Path(__file__).parent.resolve())] + prev_path
14+
try:
15+
yield None
16+
finally:
17+
sys.path = prev_path

mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[mypy]
22
disable_error_code = import
3+
exclude = '/android_protoc/frameworks/'
34

45
[google.auth.*]
56
follow_untyped_imports = True

0 commit comments

Comments
 (0)