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