/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.build.gradle.tasks.annotations; import static com.android.SdkConstants.DOT_JAVA; import static java.io.File.pathSeparator; import static java.io.File.pathSeparatorChar; import com.android.annotations.NonNull; import com.android.tools.lint.EcjParser; import com.android.utils.Pair; import com.google.common.base.Charsets; import com.google.common.base.Splitter; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.io.Files; import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; import org.eclipse.jdt.internal.compiler.batch.CompilationUnit; import org.eclipse.jdt.internal.compiler.env.ICompilationUnit; import org.eclipse.jdt.internal.compiler.env.INameEnvironment; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; import org.eclipse.jdt.internal.compiler.util.Util; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.util.Collection; import java.util.List; import java.util.Map; /** * The extract annotations driver is a command line interface to extracting annotations * from a source tree. It's similar to the gradle * {@link com.android.build.gradle.tasks.ExtractAnnotations} task, * but usable from the command line and outside Gradle, for example * to extract annotations from the Android framework itself (which is not built with * Gradle). It also allows other options only interesting for extracting * platform annotations, such as filtering all APIs and constants through an * API white-list (such that we for example can pull annotations from the master * branch which has the latest metadata, but only expose APIs that are actually in * a released platform), as well as translating android.annotation annotations into * android.support.annotations. */ public class ExtractAnnotationsDriver { public static void main(String[] args) { new ExtractAnnotationsDriver().run(args); } private static void usage(PrintStream output) { output.println("Usage: " + ExtractAnnotationsDriver.class.getSimpleName() + " <flags>"); output.println(" --sources <paths> : Source directories to extract annotations from. "); output.println(" Separate paths with " + pathSeparator + ", and you can use @ "); output.println(" as a filename prefix to have the filenames fed from a file"); output.println("--classpath <paths> : Directories and .jar files to resolve symbols from"); output.println("--output <zip path> : The .zip file to write the extracted annotations to, if any"); output.println("--proguard <path> : The proguard.cfg file to write the keep rules to, if any"); output.println(); output.println("Optional flags:"); output.println("--merge-zips <paths> : Existing external annotation files to merge in"); output.println("--quiet : Don't print summary information"); output.println("--rmtypedefs <folder> : Remove typedef classes found in the given folder"); output.println("--allow-missing-types : Don't fail even if some types can't be resolved"); output.println("--allow-errors : Don't fail even if there are some compiler errors"); output.println("--encoding <encoding> : Encoding (defaults to utf-8)"); output.println("--language-level <level> : Java source language level, typically 1.6 (default) or 1.7"); output.println("--api-filter <api.txt> : A framework API definition to restrict included APIs to"); output.println("--hide-filtered : If filtering out non-APIs, supply this flag to hide listing matches"); output.println("--skip-class-retention : Don't extract annotations that have class retention"); System.exit(-1); } @SuppressWarnings("MethodMayBeStatic") public void run(@NonNull String[] args) { List<String> classpath = Lists.newArrayList(); List<File> sources = Lists.newArrayList(); List<File> mergePaths = Lists.newArrayList(); List<File> apiFilters = null; File rmTypeDefs = null; boolean verbose = true; boolean allowMissingTypes = false; boolean allowErrors = false; boolean listFiltered = true; boolean skipClassRetention = false; String encoding = Charsets.UTF_8.name(); File output = null; File proguard = null; long languageLevel = EcjParser.getLanguageLevel(1, 7); if (args.length == 1 && "--help".equals(args[0])) { usage(System.out); } if (args.length < 2) { usage(System.err); } for (int i = 0, n = args.length; i < n; i++) { String flag = args[i]; if (flag.equals("--quiet")) { verbose = false; continue; } else if (flag.equals("--allow-missing-types")) { allowMissingTypes = true; continue; } else if (flag.equals("--allow-errors")) { allowErrors = true; continue; } else if (flag.equals("--hide-filtered")) { listFiltered = false; continue; } else if (flag.equals("--skip-class-retention")) { skipClassRetention = true; continue; } if (i == n - 1) { usage(System.err); } String value = args[i + 1]; i++; if (flag.equals("--sources")) { sources = getFiles(value); } else if (flag.equals("--classpath")) { classpath = getPaths(value); } else if (flag.equals("--merge-zips")) { mergePaths = getFiles(value); } else if (flag.equals("--output")) { output = new File(value); if (output.exists()) { if (output.isDirectory()) { abort(output + " is a directory"); } boolean deleted = output.delete(); if (!deleted) { abort("Could not delete previous version of " + output); } } else if (output.getParentFile() != null && !output.getParentFile().exists()) { abort(output.getParentFile() + " does not exist"); } } else if (flag.equals("--proguard")) { proguard = new File(value); if (proguard.exists()) { if (proguard.isDirectory()) { abort(proguard + " is a directory"); } boolean deleted = proguard.delete(); if (!deleted) { abort("Could not delete previous version of " + proguard); } } else if (proguard.getParentFile() != null && !proguard.getParentFile().exists()) { abort(proguard.getParentFile() + " does not exist"); } } else if (flag.equals("--encoding")) { encoding = value; } else if (flag.equals("--api-filter")) { if (apiFilters == null) { apiFilters = Lists.newArrayList(); } for (String path : Splitter.on(",").omitEmptyStrings().split(value)) { File apiFilter = new File(path); if (!apiFilter.isFile()) { String message = apiFilter + " does not exist or is not a file"; abort(message); } apiFilters.add(apiFilter); } } else if (flag.equals("--language-level")) { if ("1.6".equals(value)) { languageLevel = EcjParser.getLanguageLevel(1, 6); } else if ("1.7".equals(value)) { languageLevel = EcjParser.getLanguageLevel(1, 7); } else { abort("Unsupported language level " + value); } } else if (flag.equals("--rmtypedefs")) { rmTypeDefs = new File(value); if (!rmTypeDefs.isDirectory()) { abort(rmTypeDefs + " is not a directory"); } } else { System.err.println("Unknown flag " + flag + ": Use --help for usage information"); } } if (sources.isEmpty()) { abort("Must specify at least one source path"); } if (classpath.isEmpty()) { abort("Must specify classpath pointing to at least android.jar or the framework"); } if (output == null && proguard == null) { abort("Must specify output path with --output or a proguard path with --proguard"); } // API definition files ApiDatabase database = null; if (apiFilters != null && !apiFilters.isEmpty()) { try { List<String> lines = Lists.newArrayList(); for (File file : apiFilters) { lines.addAll(Files.readLines(file, Charsets.UTF_8)); } database = new ApiDatabase(lines); } catch (IOException e) { abort("Could not open API database " + apiFilters + ": " + e.getLocalizedMessage()); } } Extractor extractor = new Extractor(database, rmTypeDefs, verbose, !skipClassRetention, true); extractor.setListIgnored(listFiltered); try { Pair<Collection<CompilationUnitDeclaration>, INameEnvironment> pair = parseSources(sources, classpath, encoding, languageLevel); Collection<CompilationUnitDeclaration> units = pair.getFirst(); boolean abort = false; int errorCount = 0; for (CompilationUnitDeclaration unit : units) { // so maybe I don't need my map!! IProblem[] problems = unit.compilationResult().getAllProblems(); if (problems != null) { for (IProblem problem : problems) { if (problem.isError()) { errorCount++; String message = problem.getMessage(); if (allowMissingTypes) { if (message.contains("cannot be resolved")) { continue; } } System.out.println("Error: " + new String(problem.getOriginatingFileName()) + ":" + problem.getSourceLineNumber() + ": " + message); abort = !allowErrors; } } } } if (errorCount > 0) { System.err.println("Found " + errorCount + " errors"); } if (abort) { abort("Not extracting annotations (compilation problems encountered)"); } INameEnvironment environment = pair.getSecond(); extractor.extractFromProjectSource(units); if (mergePaths != null) { for (File jar : mergePaths) { extractor.mergeExisting(jar); } } extractor.export(output, proguard); // Remove typedefs? //noinspection VariableNotUsedInsideIf if (rmTypeDefs != null) { extractor.removeTypedefClasses(); } environment.cleanup(); } catch (IOException e) { e.printStackTrace(); } } private static void abort(@NonNull String message) { System.err.println(message); System.exit(-1); } private static List<File> getFiles(String value) { List<File> files = Lists.newArrayList(); Splitter splitter = Splitter.on(pathSeparatorChar).omitEmptyStrings().trimResults(); for (String path : splitter.split(value)) { if (path.startsWith("@")) { // Special syntax for providing files in a list File sourcePath = new File(path.substring(1)); if (!sourcePath.exists()) { abort(sourcePath + " does not exist"); } try { for (String line : Files.readLines(sourcePath, Charsets.UTF_8)) { line = line.trim(); if (!line.isEmpty()) { File file = new File(line); if (!file.exists()) { System.err.println("Warning: Could not find file " + line + " listed in " + sourcePath); } files.add(file); } } continue; } catch (IOException e) { e.printStackTrace(); System.exit(-1); } } File file = new File(path); if (!file.exists()) { abort(file + " does not exist"); } files.add(file); } return files; } private static List<String> getPaths(String value) { List<File> files = getFiles(value); List<String> paths = Lists.newArrayListWithExpectedSize(files.size()); for (File file : files) { paths.add(file.getPath()); } return paths; } private static void addJavaSources(List<File> list, File file) { if (file.isDirectory()) { File[] files = file.listFiles(); if (files != null) { for (File child : files) { addJavaSources(list, child); } } } else { if (file.isFile() && file.getName().endsWith(DOT_JAVA)) { list.add(file); } } } private static List<File> gatherJavaSources(List<File> sourcePath) { List<File> sources = Lists.newArrayList(); for (File file : sourcePath) { addJavaSources(sources, file); } return sources; } @NonNull private static Pair<Collection<CompilationUnitDeclaration>,INameEnvironment> parseSources( @NonNull List<File> sourcePaths, @NonNull List<String> classpath, @NonNull String encoding, long languageLevel) throws IOException { List<ICompilationUnit> sourceUnits = Lists.newArrayListWithExpectedSize(100); for (File source : gatherJavaSources(sourcePaths)) { char[] contents = Util.getFileCharContent(source, encoding); ICompilationUnit unit = new CompilationUnit(contents, source.getPath(), encoding); sourceUnits.add(unit); } Map<ICompilationUnit, CompilationUnitDeclaration> outputMap = Maps.newHashMapWithExpectedSize( sourceUnits.size()); CompilerOptions options = EcjParser.createCompilerOptions(); options.docCommentSupport = true; // So I can find @hide // Note: We can *not* set options.ignoreMethodBodies=true because it disables // type attribution! options.sourceLevel = languageLevel; options.complianceLevel = options.sourceLevel; // We don't generate code, but just in case the parser consults this flag // and makes sure that it's not greater than the source level: options.targetJDK = options.sourceLevel; options.originalComplianceLevel = options.sourceLevel; options.originalSourceLevel = options.sourceLevel; options.inlineJsrBytecode = true; // >= 1.5 INameEnvironment environment = EcjParser.parse(options, sourceUnits, classpath, outputMap, null); Collection<CompilationUnitDeclaration> parsedUnits = outputMap.values(); return Pair.of(parsedUnits, environment); } }