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