#!/usr/bin/env python3 # Copyright 2023 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """A bindings generator for JNI on Android.""" import argparse import os import shutil import sys import jni_generator import jni_registration_generator # jni_zero.py requires Python 3.8+. _MIN_PYTHON_MINOR = 8 def _add_io_args(parser, *, is_final=False, is_javap=False): inputs = parser.add_argument_group(title='Inputs') outputs = parser.add_argument_group(title='Outputs') if is_final: inputs.add_argument( '--java-sources-file', required=True, help='Newline-separated file containing paths to .java or .jni.pickle ' 'files, taken from Java dependency tree.') inputs.add_argument( '--native-sources-file', help='Newline-separated file containing paths to .java or .jni.pickle ' 'files, taken from Native dependency tree.') else: if is_javap: inputs.add_argument( '--jar-file', help='Extract the list of input files from a specified jar file. ' 'Uses javap to extract the methods from a pre-compiled class.') help_text = 'Paths within the .jar' else: help_text = 'Paths to .java files to parse.' inputs.add_argument('--input-file', action='append', required=True, dest='input_files', help=help_text) outputs.add_argument('--output-name', action='append', required=True, dest='output_names', help='Output filenames within output directory.') outputs.add_argument('--output-dir', required=True, help='Output directory. ' 'Existing .h files in this directory will be assumed ' 'stale and removed.') outputs.add_argument('--placeholder-srcjar-path', help='Path to output srcjar with placeholders for ' 'all referenced classes in |input_files|') outputs.add_argument('--header-path', help='Path to output header file.') if is_javap: inputs.add_argument('--javap', help='The path to javap command.') else: outputs.add_argument( '--srcjar-path', help='Path to output srcjar for GEN_JNI.java (and J/N.java if proxy' ' hash is enabled).') outputs.add_argument('--jni-pickle', help='Path to write intermediate .jni.pickle file.') if is_final: outputs.add_argument( '--depfile', help='Path to depfile (for use with ninja build system)') def _add_codegen_args(parser, *, is_final=False, is_javap=False): group = parser.add_argument_group(title='Codegen Options') group.add_argument( '--module-name', help='Only look at natives annotated with a specific module name.') if is_final: group.add_argument( '--add-stubs-for-missing-native', action='store_true', help='Adds stub methods for any --java-sources-file which are missing ' 'from --native-sources-files. If not passed, we will assert that none ' 'of these exist.') group.add_argument( '--remove-uncalled-methods', action='store_true', help='Removes --native-sources-files which are not in ' '--java-sources-file. If not passed, we will assert that none of these ' 'exist.') group.add_argument( '--namespace', help='Native namespace to wrap the registration functions into.') # TODO(crbug.com/898261) hook these flags up to the build config to enable # mocking in instrumentation tests group.add_argument( '--enable-proxy-mocks', default=False, action='store_true', help='Allows proxy native impls to be mocked through Java.') group.add_argument( '--require-mocks', default=False, action='store_true', help='Requires all used native implementations to have a mock set when ' 'called. Otherwise an exception will be thrown.') group.add_argument('--manual-jni-registration', action='store_true', help='Generate a call to RegisterNatives()') group.add_argument('--include-test-only', action='store_true', help='Whether to maintain ForTesting JNI methods.') else: group.add_argument('--extra-include', action='append', dest='extra_includes', help='Header file to #include in the generated header.') group.add_argument( '--split-name', help='Split name that the Java classes should be loaded from.') group.add_argument('--per-file-natives', action='store_true') if is_javap: group.add_argument('--unchecked-exceptions', action='store_true', help='Do not check that no exceptions were thrown.') else: group.add_argument( '--use-proxy-hash', action='store_true', help='Enables hashing of the native declaration for methods in ' 'a @NativeMethods interface') group.add_argument('--enable-jni-multiplexing', action='store_true', help='Enables JNI multiplexing for Java native methods') group.add_argument( '--package-prefix', help='Adds a prefix to the classes fully qualified-name. Effectively ' 'changing a class name from foo.bar -> prefix.foo.bar') if not is_final: if is_javap: instead_msg = 'instead of the javap class name.' else: instead_msg = 'when there is no @JNINamespace set' group.add_argument('--namespace', help='Uses as a namespace in the generated header ' + instead_msg) def _maybe_relaunch_with_newer_python(): # If "python3" is < python3.8, but a newer version is available, then use # that. py_version = sys.version_info if py_version < (3, _MIN_PYTHON_MINOR): if os.environ.get('JNI_ZERO_RELAUNCHED'): sys.stderr.write('JNI_ZERO_RELAUNCHED failure.\n') sys.exit(1) for i in range(_MIN_PYTHON_MINOR, 30): name = f'python3.{i}' if shutil.which(name): cmd = [name] + sys.argv env = os.environ.copy() env['JNI_ZERO_RELAUNCHED'] = '1' os.execvpe(cmd[0], cmd, env) sys.stderr.write( f'jni_zero requires Python 3.{_MIN_PYTHON_MINOR} or greater.\n') sys.exit(1) def _add_args(parser, *, is_final=False, is_javap=False): _add_io_args(parser, is_final=is_final, is_javap=is_javap) _add_codegen_args(parser, is_final=is_final, is_javap=is_javap) def main(): parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers(required=True) subp = subparsers.add_parser( 'from-source', help='Generates files for a set of .java sources.') _add_args(subp) subp.set_defaults(func=jni_generator.GenerateFromSource) subp = subparsers.add_parser( 'from-jar', help='Generates files from a .jar of .class files.') _add_args(subp, is_javap=True) subp.set_defaults(func=jni_generator.GenerateFromJar) subp = subparsers.add_parser( 'generate-final', help='Generates files that require knowledge of all intermediates.') _add_args(subp, is_final=True) subp.set_defaults(func=jni_registration_generator.main) # Default to showing full help text when no args are passed. if len(sys.argv) == 1: parser.print_help() elif len(sys.argv) == 2 and sys.argv[1] in subparsers.choices: parser.parse_args(sys.argv[1:] + ['-h']) else: args = parser.parse_args() args.func(parser, args) if __name__ == '__main__': _maybe_relaunch_with_newer_python() main()