xref: /aosp_15_r20/external/cronet/third_party/jni_zero/jni_zero.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1#!/usr/bin/env python3
2# Copyright 2023 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"""A bindings generator for JNI on Android."""
6
7import argparse
8import os
9import shutil
10import sys
11
12import jni_generator
13import jni_registration_generator
14
15# jni_zero.py requires Python 3.8+.
16_MIN_PYTHON_MINOR = 8
17
18
19def _add_io_args(parser, *, is_final=False, is_javap=False):
20  inputs = parser.add_argument_group(title='Inputs')
21  outputs = parser.add_argument_group(title='Outputs')
22  if is_final:
23    inputs.add_argument(
24        '--java-sources-file',
25        required=True,
26        help='Newline-separated file containing paths to .java or .jni.pickle '
27        'files, taken from Java dependency tree.')
28    inputs.add_argument(
29        '--native-sources-file',
30        help='Newline-separated file containing paths to .java or .jni.pickle '
31        'files, taken from Native dependency tree.')
32  else:
33    if is_javap:
34      inputs.add_argument(
35          '--jar-file',
36          help='Extract the list of input files from a specified jar file. '
37          'Uses javap to extract the methods from a pre-compiled class.')
38
39      help_text = 'Paths within the .jar'
40    else:
41      help_text = 'Paths to .java files to parse.'
42    inputs.add_argument('--input-file',
43                        action='append',
44                        required=True,
45                        dest='input_files',
46                        help=help_text)
47    outputs.add_argument('--output-name',
48                         action='append',
49                         required=True,
50                         dest='output_names',
51                         help='Output filenames within output directory.')
52    outputs.add_argument('--output-dir',
53                         required=True,
54                         help='Output directory. '
55                         'Existing .h files in this directory will be assumed '
56                         'stale and removed.')
57    outputs.add_argument('--placeholder-srcjar-path',
58                         help='Path to output srcjar with placeholders for '
59                         'all referenced classes in |input_files|')
60
61  outputs.add_argument('--header-path', help='Path to output header file.')
62
63  if is_javap:
64    inputs.add_argument('--javap', help='The path to javap command.')
65  else:
66    outputs.add_argument(
67        '--srcjar-path',
68        help='Path to output srcjar for GEN_JNI.java (and J/N.java if proxy'
69        ' hash is enabled).')
70    outputs.add_argument('--jni-pickle',
71                         help='Path to write intermediate .jni.pickle file.')
72  if is_final:
73    outputs.add_argument(
74        '--depfile', help='Path to depfile (for use with ninja build system)')
75
76
77def _add_codegen_args(parser, *, is_final=False, is_javap=False):
78  group = parser.add_argument_group(title='Codegen Options')
79  group.add_argument(
80      '--module-name',
81      help='Only look at natives annotated with a specific module name.')
82  if is_final:
83    group.add_argument(
84        '--add-stubs-for-missing-native',
85        action='store_true',
86        help='Adds stub methods for any --java-sources-file which are missing '
87        'from --native-sources-files. If not passed, we will assert that none '
88        'of these exist.')
89    group.add_argument(
90        '--remove-uncalled-methods',
91        action='store_true',
92        help='Removes --native-sources-files which are not in '
93        '--java-sources-file. If not passed, we will assert that none of these '
94        'exist.')
95    group.add_argument(
96        '--namespace',
97        help='Native namespace to wrap the registration functions into.')
98    # TODO(crbug.com/898261) hook these flags up to the build config to enable
99    # mocking in instrumentation tests
100    group.add_argument(
101        '--enable-proxy-mocks',
102        default=False,
103        action='store_true',
104        help='Allows proxy native impls to be mocked through Java.')
105    group.add_argument(
106        '--require-mocks',
107        default=False,
108        action='store_true',
109        help='Requires all used native implementations to have a mock set when '
110        'called. Otherwise an exception will be thrown.')
111    group.add_argument('--manual-jni-registration',
112                       action='store_true',
113                       help='Generate a call to RegisterNatives()')
114    group.add_argument('--include-test-only',
115                       action='store_true',
116                       help='Whether to maintain ForTesting JNI methods.')
117  else:
118    group.add_argument('--extra-include',
119                       action='append',
120                       dest='extra_includes',
121                       help='Header file to #include in the generated header.')
122    group.add_argument(
123        '--split-name',
124        help='Split name that the Java classes should be loaded from.')
125    group.add_argument('--per-file-natives', action='store_true')
126
127  if is_javap:
128    group.add_argument('--unchecked-exceptions',
129                       action='store_true',
130                       help='Do not check that no exceptions were thrown.')
131  else:
132    group.add_argument(
133        '--use-proxy-hash',
134        action='store_true',
135        help='Enables hashing of the native declaration for methods in '
136        'a @NativeMethods interface')
137    group.add_argument('--enable-jni-multiplexing',
138                       action='store_true',
139                       help='Enables JNI multiplexing for Java native methods')
140    group.add_argument(
141        '--package-prefix',
142        help='Adds a prefix to the classes fully qualified-name. Effectively '
143        'changing a class name from foo.bar -> prefix.foo.bar')
144
145  if not is_final:
146    if is_javap:
147      instead_msg = 'instead of the javap class name.'
148    else:
149      instead_msg = 'when there is no @JNINamespace set'
150
151    group.add_argument('--namespace',
152                       help='Uses as a namespace in the generated header ' +
153                       instead_msg)
154
155
156def _maybe_relaunch_with_newer_python():
157  # If "python3" is < python3.8, but a newer version is available, then use
158  # that.
159  py_version = sys.version_info
160  if py_version < (3, _MIN_PYTHON_MINOR):
161    if os.environ.get('JNI_ZERO_RELAUNCHED'):
162      sys.stderr.write('JNI_ZERO_RELAUNCHED failure.\n')
163      sys.exit(1)
164    for i in range(_MIN_PYTHON_MINOR, 30):
165      name = f'python3.{i}'
166      if shutil.which(name):
167        cmd = [name] + sys.argv
168        env = os.environ.copy()
169        env['JNI_ZERO_RELAUNCHED'] = '1'
170        os.execvpe(cmd[0], cmd, env)
171    sys.stderr.write(
172        f'jni_zero requires Python 3.{_MIN_PYTHON_MINOR} or greater.\n')
173    sys.exit(1)
174
175
176def _add_args(parser, *, is_final=False, is_javap=False):
177  _add_io_args(parser, is_final=is_final, is_javap=is_javap)
178  _add_codegen_args(parser, is_final=is_final, is_javap=is_javap)
179
180
181def main():
182  parser = argparse.ArgumentParser(description=__doc__)
183  subparsers = parser.add_subparsers(required=True)
184
185  subp = subparsers.add_parser(
186      'from-source', help='Generates files for a set of .java sources.')
187  _add_args(subp)
188  subp.set_defaults(func=jni_generator.GenerateFromSource)
189
190  subp = subparsers.add_parser(
191      'from-jar', help='Generates files from a .jar of .class files.')
192  _add_args(subp, is_javap=True)
193  subp.set_defaults(func=jni_generator.GenerateFromJar)
194
195  subp = subparsers.add_parser(
196      'generate-final',
197      help='Generates files that require knowledge of all intermediates.')
198  _add_args(subp, is_final=True)
199  subp.set_defaults(func=jni_registration_generator.main)
200
201  # Default to showing full help text when no args are passed.
202  if len(sys.argv) == 1:
203    parser.print_help()
204  elif len(sys.argv) == 2 and sys.argv[1] in subparsers.choices:
205    parser.parse_args(sys.argv[1:] + ['-h'])
206  else:
207    args = parser.parse_args()
208    args.func(parser, args)
209
210
211if __name__ == '__main__':
212  _maybe_relaunch_with_newer_python()
213  main()
214