#!/usr/bin/env python # # Copyright 2013 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import distutils.spawn import optparse import os import shutil import re import sys import textwrap from util import build_utils from util import md5_check import jar sys.path.append(build_utils.COLORAMA_ROOT) import colorama def ColorJavacOutput(output): fileline_prefix = r'(?P(?P[-.\w/\\]+.java):(?P[0-9]+):)' warning_re = re.compile( fileline_prefix + r'(?P warning: (?P.*))$') error_re = re.compile( fileline_prefix + r'(?P (?P.*))$') marker_re = re.compile(r'\s*(?P\^)\s*$') warning_color = ['full_message', colorama.Fore.YELLOW + colorama.Style.DIM] error_color = ['full_message', colorama.Fore.MAGENTA + colorama.Style.BRIGHT] marker_color = ['marker', colorama.Fore.BLUE + colorama.Style.BRIGHT] def Colorize(line, regex, color): match = regex.match(line) start = match.start(color[0]) end = match.end(color[0]) return (line[:start] + color[1] + line[start:end] + colorama.Fore.RESET + colorama.Style.RESET_ALL + line[end:]) def ApplyColor(line): if warning_re.match(line): line = Colorize(line, warning_re, warning_color) elif error_re.match(line): line = Colorize(line, error_re, error_color) elif marker_re.match(line): line = Colorize(line, marker_re, marker_color) return line return '\n'.join(map(ApplyColor, output.split('\n'))) def _FilterJavaFiles(paths, filters): return [f for f in paths if not filters or build_utils.MatchesGlob(f, filters)] _MAX_MANIFEST_LINE_LEN = 72 def _ExtractClassFiles(jar_path, dest_dir, java_files): """Extracts all .class files not corresponding to |java_files|.""" # Two challenges exist here: # 1. |java_files| have prefixes that are not represented in the the jar paths. # 2. A single .java file results in multiple .class files when it contains # nested classes. # Here's an example: # source path: ../../base/android/java/src/org/chromium/Foo.java # jar paths: org/chromium/Foo.class, org/chromium/Foo$Inner.class # To extract only .class files not related to the given .java files, we strip # off ".class" and "$*.class" and use a substring match against java_files. def extract_predicate(path): if not path.endswith('.class'): return False path_without_suffix = re.sub(r'(?:\$|\.)[^/]*class$', '', path) partial_java_path = path_without_suffix + '.java' return not any(p.endswith(partial_java_path) for p in java_files) build_utils.ExtractAll(jar_path, path=dest_dir, predicate=extract_predicate) for path in build_utils.FindInDirectory(dest_dir, '*.class'): shutil.copystat(jar_path, path) def _ConvertToJMakeArgs(javac_cmd, pdb_path): new_args = ['bin/jmake', '-pdb', pdb_path, '-jcexec', javac_cmd[0]] if md5_check.PRINT_EXPLANATIONS: new_args.append('-Xtiming') do_not_prefix = ('-classpath', '-bootclasspath') skip_next = False for arg in javac_cmd[1:]: if not skip_next and arg not in do_not_prefix: arg = '-C' + arg new_args.append(arg) skip_next = arg in do_not_prefix return new_args def _FixTempPathsInIncrementalMetadata(pdb_path, temp_dir): # The .pdb records absolute paths. Fix up paths within /tmp (srcjars). if os.path.exists(pdb_path): # Although its a binary file, search/replace still seems to work fine. with open(pdb_path) as fileobj: pdb_data = fileobj.read() with open(pdb_path, 'w') as fileobj: fileobj.write(re.sub(r'/tmp/[^/]*', temp_dir, pdb_data)) def _CheckPathMatchesClassName(java_file): package_name = '' class_name = None with open(java_file) as f: for l in f: # Strip unindented comments. # Considers a leading * as a continuation of a multi-line comment (our # linter doesn't enforce a space before it like there should be). l = re.sub(r'^(?://.*|/?\*.*?(?:\*/\s*|$))', '', l) m = re.match(r'package\s+(.*?);', l) if m and not package_name: package_name = m.group(1) # Not exactly a proper parser, but works for sources that Chrome uses. # In order to not match nested classes, it just checks for lack of indent. m = re.match(r'(?:\S.*?)?(?:class|@?interface|enum)\s+(.+?)\b', l) if m: if class_name: raise Exception(('File defines multiple top-level classes:\n %s\n' 'This confuses compiles with ' 'enable_incremental_javac=true.\n' 'classes=%s,%s\n') % (java_file, class_name, m.groups(1))) class_name = m.group(1) if class_name is None: raise Exception('Unable to find a class within %s' % java_file) parts = package_name.split('.') + [class_name + '.java'] expected_path_suffix = os.path.sep.join(parts) if not java_file.endswith(expected_path_suffix): raise Exception(('Java package+class name do not match its path.\n' 'Actual path: %s\nExpected path: %s') % (java_file, expected_path_suffix)) def _OnStaleMd5(changes, options, javac_cmd, java_files, classpath_inputs): incremental = options.incremental # Don't bother enabling incremental compilation for third_party code, since # _CheckPathMatchesClassName() fails on some of it, and it's not really much # benefit. for java_file in java_files: if 'third_party' in java_file: incremental = False else: _CheckPathMatchesClassName(java_file) with build_utils.TempDir() as temp_dir: srcjars = options.java_srcjars # The .excluded.jar contains .class files excluded from the main jar. # It is used for incremental compiles. excluded_jar_path = options.jar_path.replace('.jar', '.excluded.jar') classes_dir = os.path.join(temp_dir, 'classes') os.makedirs(classes_dir) changed_paths = None # jmake can handle deleted files, but it's a rare case and it would # complicate this script's logic. if incremental and changes.AddedOrModifiedOnly(): changed_paths = set(changes.IterChangedPaths()) # Do a full compile if classpath has changed. # jmake doesn't seem to do this on its own... Might be that ijars mess up # its change-detection logic. if any(p in changed_paths for p in classpath_inputs): changed_paths = None if options.incremental: pdb_path = options.jar_path + '.pdb' if incremental: # jmake is a compiler wrapper that figures out the minimal set of .java # files that need to be rebuilt given a set of .java files that have # changed. # jmake determines what files are stale based on timestamps between .java # and .class files. Since we use .jars, .srcjars, and md5 checks, # timestamp info isn't accurate for this purpose. Rather than use jmake's # programatic interface (like we eventually should), we ensure that all # .class files are newer than their .java files, and convey to jmake which # sources are stale by having their .class files be missing entirely # (by not extracting them). javac_cmd = _ConvertToJMakeArgs(javac_cmd, pdb_path) if srcjars: _FixTempPathsInIncrementalMetadata(pdb_path, temp_dir) if srcjars: java_dir = os.path.join(temp_dir, 'java') os.makedirs(java_dir) for srcjar in options.java_srcjars: if changed_paths: changed_paths.update(os.path.join(java_dir, f) for f in changes.IterChangedSubpaths(srcjar)) build_utils.ExtractAll(srcjar, path=java_dir, pattern='*.java') jar_srcs = build_utils.FindInDirectory(java_dir, '*.java') jar_srcs = _FilterJavaFiles(jar_srcs, options.javac_includes) java_files.extend(jar_srcs) if changed_paths: # Set the mtime of all sources to 0 since we use the absence of .class # files to tell jmake which files are stale. for path in jar_srcs: os.utime(path, (0, 0)) if java_files: if changed_paths: changed_java_files = [p for p in java_files if p in changed_paths] if os.path.exists(options.jar_path): _ExtractClassFiles(options.jar_path, classes_dir, changed_java_files) if os.path.exists(excluded_jar_path): _ExtractClassFiles(excluded_jar_path, classes_dir, changed_java_files) # Add the extracted files to the classpath. This is required because # when compiling only a subset of files, classes that haven't changed # need to be findable. classpath_idx = javac_cmd.index('-classpath') javac_cmd[classpath_idx + 1] += ':' + classes_dir # Can happen when a target goes from having no sources, to having sources. # It's created by the call to build_utils.Touch() below. if incremental: if os.path.exists(pdb_path) and not os.path.getsize(pdb_path): os.unlink(pdb_path) # Don't include the output directory in the initial set of args since it # being in a temp dir makes it unstable (breaks md5 stamping). cmd = javac_cmd + ['-d', classes_dir] + java_files # JMake prints out some diagnostic logs that we want to ignore. # This assumes that all compiler output goes through stderr. stdout_filter = lambda s: '' if md5_check.PRINT_EXPLANATIONS: stdout_filter = None attempt_build = lambda: build_utils.CheckOutput( cmd, print_stdout=options.chromium_code, stdout_filter=stdout_filter, stderr_filter=ColorJavacOutput) try: attempt_build() except build_utils.CalledProcessError as e: # Work-around for a bug in jmake (http://crbug.com/551449). if 'project database corrupted' not in e.output: raise print ('Applying work-around for jmake project database corrupted ' '(http://crbug.com/551449).') os.unlink(pdb_path) attempt_build() if options.incremental and (not java_files or not incremental): # Make sure output exists. build_utils.Touch(pdb_path) glob = options.jar_excluded_classes inclusion_predicate = lambda f: not build_utils.MatchesGlob(f, glob) exclusion_predicate = lambda f: not inclusion_predicate(f) jar.JarDirectory(classes_dir, options.jar_path, predicate=inclusion_predicate, provider_configurations=options.provider_configurations, additional_files=options.additional_jar_files) jar.JarDirectory(classes_dir, excluded_jar_path, predicate=exclusion_predicate, provider_configurations=options.provider_configurations, additional_files=options.additional_jar_files) def _ParseOptions(argv): parser = optparse.OptionParser() build_utils.AddDepfileOption(parser) parser.add_option( '--src-gendirs', help='Directories containing generated java files.') parser.add_option( '--java-srcjars', action='append', default=[], help='List of srcjars to include in compilation.') parser.add_option( '--bootclasspath', action='append', default=[], help='Boot classpath for javac. If this is specified multiple times, ' 'they will all be appended to construct the classpath.') parser.add_option( '--java-version', help='Java language version to use in -source and -target args to javac.') parser.add_option( '--classpath', action='append', help='Classpath for javac. If this is specified multiple times, they ' 'will all be appended to construct the classpath.') parser.add_option( '--incremental', action='store_true', help='Whether to re-use .class files rather than recompiling them ' '(when possible).') parser.add_option( '--javac-includes', default='', help='A list of file patterns. If provided, only java files that match' 'one of the patterns will be compiled.') parser.add_option( '--jar-excluded-classes', default='', help='List of .class file patterns to exclude from the jar.') parser.add_option( '--processor', dest='processors', action='append', help='Annotation processor to use.') parser.add_option( '--processorpath', help='Where javac should look for annotation processors.') parser.add_option( '--processor-arg', dest='processor_args', action='append', help='key=value arguments for the annotation processors.') parser.add_option( '--provider-configuration', dest='provider_configurations', action='append', help='File to specify a service provider. Will be included ' 'in the jar under META-INF/services.') parser.add_option( '--additional-jar-file', dest='additional_jar_files', action='append', help='Additional files to package into jar. By default, only Java .class ' 'files are packaged into the jar. Files should be specified in ' 'format :.') parser.add_option( '--chromium-code', type='int', help='Whether code being compiled should be built with stricter ' 'warnings for chromium code.') parser.add_option( '--use-errorprone-path', help='Use the Errorprone compiler at this path.') parser.add_option('--jar-path', help='Jar output path.') parser.add_option('--stamp', help='Path to touch on success.') parser.add_option( '--javac-arg', action='append', default=[], help='Additional arguments to pass to javac.') options, args = parser.parse_args(argv) build_utils.CheckOptions(options, parser, required=('jar_path',)) bootclasspath = [] for arg in options.bootclasspath: bootclasspath += build_utils.ParseGnList(arg) options.bootclasspath = bootclasspath if options.java_version == '1.8' and options.bootclasspath: # Android's boot jar doesn't contain all java 8 classes. # See: https://github.com/evant/gradle-retrolambda/issues/23. # Get the path of the jdk folder by searching for the 'jar' executable. We # cannot search for the 'javac' executable because goma provides a custom # version of 'javac'. jar_path = os.path.realpath(distutils.spawn.find_executable('jar')) jdk_dir = os.path.dirname(os.path.dirname(jar_path)) rt_jar = os.path.join(jdk_dir, 'jre', 'lib', 'rt.jar') options.bootclasspath.append(rt_jar) classpath = [] for arg in options.classpath: classpath += build_utils.ParseGnList(arg) options.classpath = classpath java_srcjars = [] for arg in options.java_srcjars: java_srcjars += build_utils.ParseGnList(arg) options.java_srcjars = java_srcjars additional_jar_files = [] for arg in options.additional_jar_files or []: filepath, jar_filepath = arg.split(':') additional_jar_files.append((filepath, jar_filepath)) options.additional_jar_files = additional_jar_files if options.src_gendirs: options.src_gendirs = build_utils.ParseGnList(options.src_gendirs) options.javac_includes = build_utils.ParseGnList(options.javac_includes) options.jar_excluded_classes = ( build_utils.ParseGnList(options.jar_excluded_classes)) java_files = [] for arg in args: # Interpret a path prefixed with @ as a file containing a list of sources. if arg.startswith('@'): java_files.extend(build_utils.ReadSourcesList(arg[1:])) else: java_files.append(arg) return options, java_files def main(argv): colorama.init() argv = build_utils.ExpandFileArgs(argv) options, java_files = _ParseOptions(argv) if options.src_gendirs: java_files += build_utils.FindInDirectories(options.src_gendirs, '*.java') java_files = _FilterJavaFiles(java_files, options.javac_includes) if options.use_errorprone_path: javac_path = options.use_errorprone_path else: javac_path = distutils.spawn.find_executable('javac') javac_cmd = [javac_path] javac_cmd.extend(( '-g', # Chromium only allows UTF8 source files. Being explicit avoids # javac pulling a default encoding from the user's environment. '-encoding', 'UTF-8', # Make sure we do not pass an empty string to -classpath and -sourcepath. '-classpath', ':'.join(options.classpath) or ':', # Prevent compiler from compiling .java files not listed as inputs. # See: http://blog.ltgt.net/most-build-tools-misuse-javac/ '-sourcepath', ':', )) if options.bootclasspath: javac_cmd.extend([ '-bootclasspath', ':'.join(options.bootclasspath) ]) if options.java_version: javac_cmd.extend([ '-source', options.java_version, '-target', options.java_version, ]) if options.chromium_code: javac_cmd.extend(['-Xlint:unchecked', '-Xlint:deprecation']) else: # XDignore.symbol.file makes javac compile against rt.jar instead of # ct.sym. This means that using a java internal package/class will not # trigger a compile warning or error. javac_cmd.extend(['-XDignore.symbol.file']) if options.processors: javac_cmd.extend(['-processor', ','.join(options.processors)]) if options.processorpath: javac_cmd.extend(['-processorpath', options.processorpath]) if options.processor_args: for arg in options.processor_args: javac_cmd.extend(['-A%s' % arg]) javac_cmd.extend(options.javac_arg) classpath_inputs = options.bootclasspath if options.classpath: if options.classpath[0].endswith('.interface.jar'): classpath_inputs.extend(options.classpath) else: # TODO(agrieve): Remove this .TOC heuristic once GYP is no more. for path in options.classpath: if os.path.exists(path + '.TOC'): classpath_inputs.append(path + '.TOC') else: classpath_inputs.append(path) # GN already knows of java_files, so listing them just make things worse when # they change. depfile_deps = [javac_path] + classpath_inputs + options.java_srcjars input_paths = depfile_deps + java_files output_paths = [ options.jar_path, options.jar_path.replace('.jar', '.excluded.jar'), ] if options.incremental: output_paths.append(options.jar_path + '.pdb') # An escape hatch to be able to check if incremental compiles are causing # problems. force = int(os.environ.get('DISABLE_INCREMENTAL_JAVAC', 0)) # List python deps in input_strings rather than input_paths since the contents # of them does not change what gets written to the depsfile. build_utils.CallAndWriteDepfileIfStale( lambda changes: _OnStaleMd5(changes, options, javac_cmd, java_files, classpath_inputs), options, depfile_deps=depfile_deps, input_paths=input_paths, input_strings=javac_cmd, output_paths=output_paths, force=force, pass_changes=True) if __name__ == '__main__': sys.exit(main(sys.argv[1:]))