1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tools.metalava.apilevels; 18 19 import com.android.tools.metalava.model.Codebase; 20 import com.android.tools.metalava.SdkIdentifier; 21 22 import org.jetbrains.annotations.NotNull; 23 import org.jetbrains.annotations.Nullable; 24 25 import java.io.File; 26 import java.io.IOException; 27 import java.io.PrintStream; 28 import java.nio.file.Files; 29 import java.util.ArrayList; 30 import java.util.Collections; 31 import java.util.HashMap; 32 import java.util.Iterator; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Set; 36 import java.util.function.Function; 37 38 /** 39 * Main class for command line command to convert the existing API XML/TXT files into diff-based 40 * simple text files. 41 */ 42 public class ApiGenerator { generate(@otNull File[] apiLevels, int firstApiLevel, int currentApiLevel, boolean isDeveloperPreviewBuild, @NotNull File outputFile, @NotNull Codebase codebase, @Nullable File sdkJarRoot, @Nullable File sdkFilterFile, boolean removeMissingClasses)43 public static boolean generate(@NotNull File[] apiLevels, 44 int firstApiLevel, 45 int currentApiLevel, 46 boolean isDeveloperPreviewBuild, 47 @NotNull File outputFile, 48 @NotNull Codebase codebase, 49 @Nullable File sdkJarRoot, 50 @Nullable File sdkFilterFile, 51 boolean removeMissingClasses) throws IOException, IllegalArgumentException { 52 if ((sdkJarRoot == null) != (sdkFilterFile == null)) { 53 throw new IllegalArgumentException("sdkJarRoot and sdkFilterFile must both be null, or non-null"); 54 } 55 56 int notFinalizedApiLevel = currentApiLevel + 1; 57 Api api = readAndroidJars(apiLevels, firstApiLevel); 58 if (isDeveloperPreviewBuild || apiLevels.length - 1 < currentApiLevel) { 59 // Only include codebase if we don't have a prebuilt, finalized jar for it. 60 int apiLevel = isDeveloperPreviewBuild ? notFinalizedApiLevel : currentApiLevel; 61 AddApisFromCodebaseKt.addApisFromCodebase(api, apiLevel, codebase); 62 } 63 api.backfillHistoricalFixes(); 64 65 Set<SdkIdentifier> sdkIdentifiers = Collections.emptySet(); 66 if (sdkJarRoot != null && sdkFilterFile != null) { 67 sdkIdentifiers = processExtensionSdkApis(api, notFinalizedApiLevel, sdkJarRoot, sdkFilterFile); 68 } 69 api.inlineFromHiddenSuperClasses(); 70 api.removeImplicitInterfaces(); 71 api.removeOverridingMethods(); 72 api.prunePackagePrivateClasses(); 73 if (removeMissingClasses) { 74 api.removeMissingClasses(); 75 } else { 76 api.verifyNoMissingClasses(); 77 } 78 return createApiFile(outputFile, api, sdkIdentifiers); 79 } 80 readAndroidJars(File[] apiLevels, int firstApiLevel)81 private static Api readAndroidJars(File[] apiLevels, int firstApiLevel) { 82 Api api = new Api(firstApiLevel); 83 for (int apiLevel = firstApiLevel; apiLevel < apiLevels.length; apiLevel++) { 84 File jar = apiLevels[apiLevel]; 85 JarReaderUtilsKt.readAndroidJar(api, apiLevel, jar); 86 } 87 return api; 88 } 89 90 /** 91 * Modify the extension SDK API parts of an API as dictated by a filter. 92 * 93 * - remove APIs not listed in the filter 94 * - assign APIs listed in the filter their corresponding extensions 95 * 96 * Some APIs only exist in extension SDKs and not in the Android SDK, but for backwards 97 * compatibility with tools that expect the Android SDK to be the only SDK, metalava needs to 98 * assign such APIs some Android SDK API level. The recommended value is current-api-level + 1, 99 * which is what non-finalized APIs use. 100 * 101 * @param api the api to modify 102 * @param apiLevelNotInAndroidSdk fallback API level for APIs not in the Android SDK 103 * @param sdkJarRoot path to directory containing extension SDK jars (usually $ANDROID_ROOT/prebuilts/sdk/extensions) 104 * @param filterPath: path to the filter file. @see ApiToExtensionsMap 105 * @throws IOException if the filter file can not be read 106 * @throws IllegalArgumentException if an error is detected in the filter file, or if no jar files were found 107 */ processExtensionSdkApis( @otNull Api api, int apiLevelNotInAndroidSdk, @NotNull File sdkJarRoot, @NotNull File filterPath)108 private static Set<SdkIdentifier> processExtensionSdkApis( 109 @NotNull Api api, 110 int apiLevelNotInAndroidSdk, 111 @NotNull File sdkJarRoot, 112 @NotNull File filterPath) throws IOException, IllegalArgumentException { 113 String rules = new String(Files.readAllBytes(filterPath.toPath())); 114 115 Map<String, List<VersionAndPath>> map = ExtensionSdkJarReader.Companion.findExtensionSdkJarFiles(sdkJarRoot); 116 if (map.isEmpty()) { 117 throw new IllegalArgumentException("no extension sdk jar files found in " + sdkJarRoot); 118 } 119 Map<String, ApiToExtensionsMap> moduleMaps = new HashMap<>(); 120 for (Map.Entry<String, List<VersionAndPath>> entry : map.entrySet()) { 121 String mainlineModule = entry.getKey(); 122 ApiToExtensionsMap moduleMap = ApiToExtensionsMap.Companion.fromXml(mainlineModule, rules); 123 if (moduleMap.isEmpty()) continue; // TODO(b/259115852): remove this (though it is an optimization too). 124 125 moduleMaps.put(mainlineModule, moduleMap); 126 for (VersionAndPath f : entry.getValue()) { 127 JarReaderUtilsKt.readExtensionJar(api, f.version, mainlineModule, f.path, apiLevelNotInAndroidSdk); 128 } 129 } 130 for (ApiClass clazz : api.getClasses()) { 131 String module = clazz.getMainlineModule(); 132 if (module == null) continue; 133 ApiToExtensionsMap extensionsMap = moduleMaps.get(module); 134 String sdks = extensionsMap.calculateSdksAttr(clazz.getSince(), apiLevelNotInAndroidSdk, 135 extensionsMap.getExtensions(clazz), clazz.getSinceExtension()); 136 clazz.updateSdks(sdks); 137 138 Iterator<ApiElement> iter = clazz.getFieldIterator(); 139 while (iter.hasNext()) { 140 ApiElement field = iter.next(); 141 sdks = extensionsMap.calculateSdksAttr(field.getSince(), apiLevelNotInAndroidSdk, 142 extensionsMap.getExtensions(clazz, field), field.getSinceExtension()); 143 field.updateSdks(sdks); 144 } 145 146 iter = clazz.getMethodIterator(); 147 while (iter.hasNext()) { 148 ApiElement method = iter.next(); 149 sdks = extensionsMap.calculateSdksAttr(method.getSince(), apiLevelNotInAndroidSdk, 150 extensionsMap.getExtensions(clazz, method), method.getSinceExtension()); 151 method.updateSdks(sdks); 152 } 153 } 154 return ApiToExtensionsMap.Companion.fromXml("", rules).getSdkIdentifiers(); 155 } 156 157 /** 158 * Creates the simplified diff-based API level. 159 * 160 * @param outFile the output file 161 * @param api the api to write 162 * @param sdkIdentifiers SDKs referenced by the api 163 */ createApiFile(@otNull File outFile, @NotNull Api api, @NotNull Set<SdkIdentifier> sdkIdentifiers)164 private static boolean createApiFile(@NotNull File outFile, @NotNull Api api, @NotNull Set<SdkIdentifier> sdkIdentifiers) { 165 File parentFile = outFile.getParentFile(); 166 if (!parentFile.exists()) { 167 boolean ok = parentFile.mkdirs(); 168 if (!ok) { 169 System.err.println("Could not create directory " + parentFile); 170 return false; 171 } 172 } 173 try (PrintStream stream = new PrintStream(outFile, "UTF-8")) { 174 stream.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); 175 api.print(stream, sdkIdentifiers); 176 } catch (Exception e) { 177 e.printStackTrace(); 178 return false; 179 } 180 181 return true; 182 } 183 } 184