diff options
6 files changed, 261 insertions, 55 deletions
diff --git a/ravenwood/scripts/ravenwood-stats-collector.sh b/ravenwood/scripts/ravenwood-stats-collector.sh index b5843d0fa26b..beacde282d49 100755 --- a/ravenwood/scripts/ravenwood-stats-collector.sh +++ b/ravenwood/scripts/ravenwood-stats-collector.sh @@ -39,14 +39,18 @@ dump() { local jar=$1 local file=$2 - sed -e '1d' -e "s/^/$jar,/" $file + # Use sed to remove the header + prepend the jar filename. + sed -e '1d' -e "s/^/$jar,/" $file } collect_stats() { local out="$1" { - echo 'Jar,PackageName,ClassName,SupportedMethods,TotalMethods' - dump "framework-minus-apex" hoststubgen_framework-minus-apex_stats.csv + # Copy the header, with the first column appended. + echo -n "Jar," + head -n 1 hoststubgen_framework-minus-apex_stats.csv + + dump "framework-minus-apex" hoststubgen_framework-minus-apex_stats.csv dump "service.core" hoststubgen_services.core_stats.csv } > "$out" @@ -56,7 +60,10 @@ collect_stats() { collect_apis() { local out="$1" { - echo 'Jar,PackageName,ClassName,MethodName,Descriptor' + # Copy the header, with the first column appended. + echo -n "Jar," + head -n 1 hoststubgen_framework-minus-apex_apis.csv + dump "framework-minus-apex" hoststubgen_framework-minus-apex_apis.csv dump "service.core" hoststubgen_services.core_apis.csv } > "$out" diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 2f432cc7ac96..7212beb6ae4b 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -16,6 +16,7 @@ package com.android.hoststubgen import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.dumper.ApiDumper import com.android.hoststubgen.filters.AnnotationBasedFilter import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter import com.android.hoststubgen.filters.ConstantFilter @@ -89,7 +90,11 @@ class HostStubGen(val options: HostStubGenOptions) { log.i("Dump file created at $it") } options.apiListFile.ifSet { - PrintWriter(it).use { pw -> stats.dumpApis(pw) } + PrintWriter(it).use { pw -> + // TODO, when dumping a jar that's not framework-minus-apex.jar, we need to feed + // framework-minus-apex.jar so that we can dump inherited methods from it. + ApiDumper(pw, allClasses, null, filter).dump() + } log.i("API list file created at $it") } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt index da6146911a21..9045db210495 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt @@ -15,7 +15,8 @@ */ package com.android.hoststubgen -import com.android.hoststubgen.asm.toHumanReadableClassName +import com.android.hoststubgen.asm.getOuterClassNameFromFullClassName +import com.android.hoststubgen.asm.getPackageNameFromFullClassName import com.android.hoststubgen.filters.FilterPolicyWithReason import org.objectweb.asm.Opcodes import java.io.PrintWriter @@ -55,8 +56,8 @@ open class HostStubGenStats { // Ignore methods where policy isn't relevant if (policy.isIgnoredForStats) return - val packageName = resolvePackageName(fullClassName) - val className = resolveOuterClassName(fullClassName) + val packageName = getPackageNameFromFullClassName(fullClassName) + val className = getOuterClassNameFromFullClassName(fullClassName) // Ignore methods for certain generated code if (className.endsWith("Proto") @@ -88,42 +89,4 @@ open class HostStubGenStats { } } } - - fun dumpApis(pw: PrintWriter) { - pw.printf("PackageName,ClassName,MethodName,MethodDesc\n") - apis.sortedWith(compareBy({ it.fullClassName }, { it.methodName }, { it.methodDesc })) - .forEach { api -> - pw.printf( - "%s,%s,%s,%s\n", - csvEscape(resolvePackageName(api.fullClassName)), - csvEscape(resolveClassName(api.fullClassName)), - csvEscape(api.methodName), - csvEscape(api.methodDesc), - ) - } - } - - private fun resolvePackageName(fullClassName: String): String { - val start = fullClassName.lastIndexOf('/') - return fullClassName.substring(0, start).toHumanReadableClassName() - } - - private fun resolveOuterClassName(fullClassName: String): String { - val start = fullClassName.lastIndexOf('/') - val end = fullClassName.indexOf('$') - if (end == -1) { - return fullClassName.substring(start + 1) - } else { - return fullClassName.substring(start + 1, end) - } - } - - private fun resolveClassName(fullClassName: String): String { - val pos = fullClassName.lastIndexOf('/') - if (pos == -1) { - return fullClassName - } else { - return fullClassName.substring(pos + 1) - } - } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt index 83e122feeeb2..b8d18001f37b 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt @@ -76,17 +76,45 @@ fun findAnnotationValueAsString(an: AnnotationNode, propertyName: String): Strin return null } -private val removeLastElement = """[./][^./]*$""".toRegex() +val periodOrSlash = charArrayOf('.', '/') + +fun getPackageNameFromFullClassName(fullClassName: String): String { + val pos = fullClassName.lastIndexOfAny(periodOrSlash) + if (pos == -1) { + return "" + } else { + return fullClassName.substring(0, pos) + } +} + +fun getClassNameFromFullClassName(fullClassName: String): String { + val pos = fullClassName.lastIndexOfAny(periodOrSlash) + if (pos == -1) { + return fullClassName + } else { + return fullClassName.substring(pos + 1) + } +} -fun getPackageNameFromClassName(className: String): String { - return className.replace(removeLastElement, "") +fun getOuterClassNameFromFullClassName(fullClassName: String): String { + val start = fullClassName.lastIndexOfAny(periodOrSlash) + val end = fullClassName.indexOf('$') + if (end == -1) { + return fullClassName.substring(start + 1) + } else { + return fullClassName.substring(start + 1, end) + } } -fun resolveClassName(className: String, packageName: String): String { +/** + * If [className] is a fully qualified name, just return it. + * Otherwise, prepend [defaultPackageName]. + */ +fun resolveClassNameWithDefaultPackage(className: String, defaultPackageName: String): String { if (className.contains('.') || className.contains('/')) { return className } - return "$packageName.$className" + return "$defaultPackageName.$className" } fun String.toJvmClassName(): String { diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/dumper/ApiDumper.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/dumper/ApiDumper.kt new file mode 100644 index 000000000000..aaefee4f71e8 --- /dev/null +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/dumper/ApiDumper.kt @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2024 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.hoststubgen.dumper + +import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME +import com.android.hoststubgen.asm.CTOR_NAME +import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.asm.getClassNameFromFullClassName +import com.android.hoststubgen.asm.getPackageNameFromFullClassName +import com.android.hoststubgen.asm.toHumanReadableClassName +import com.android.hoststubgen.csvEscape +import com.android.hoststubgen.filters.FilterPolicy +import com.android.hoststubgen.filters.FilterPolicyWithReason +import com.android.hoststubgen.filters.OutputFilter +import com.android.hoststubgen.log +import org.objectweb.asm.Type +import org.objectweb.asm.tree.ClassNode +import java.io.PrintWriter + +/** + * Dump all the API methods in [classes], with inherited methods, with their policies. + */ +class ApiDumper( + val pw: PrintWriter, + val classes: ClassNodes, + val frameworkClasses: ClassNodes?, + val filter: OutputFilter, +) { + private data class MethodKey( + val name: String, + val descriptor: String, + ) + + val javaStandardApiPolicy = FilterPolicy.Stub.withReason("Java standard API") + + private val shownMethods = mutableSetOf<MethodKey>() + + /** + * Do the dump. + */ + fun dump() { + pw.printf("PackageName,ClassName,FromSubclass,DeclareClass,MethodName,MethodDesc" + + ",Supported,Policy,Reason\n") + + classes.forEach { classNode -> + shownMethods.clear() + dump(classNode, classNode) + } + } + + private fun dumpMethod( + classPackage: String, + className: String, + isSuperClass: Boolean, + methodClassName: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) { + pw.printf( + "%s,%s,%d,%s,%s,%s,%d,%s,%s\n", + csvEscape(classPackage), + csvEscape(className), + if (isSuperClass) { 1 } else { 0 }, + csvEscape(methodClassName), + csvEscape(methodName), + csvEscape(methodDesc), + if (policy.policy.isSupported) { 1 } else { 0 }, + policy.policy, + csvEscape(policy.reason), + ) + } + + private fun isDuplicate(methodName: String, methodDesc: String): Boolean { + val methodKey = MethodKey(methodName, methodDesc) + + if (shownMethods.contains(methodKey)) { + return true + } + shownMethods.add(methodKey) + return false + } + + private fun dump( + dumpClass: ClassNode, + methodClass: ClassNode, + ) { + val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName() + val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName() + + val isSuperClass = dumpClass != methodClass + + methodClass.methods?.sortedWith(compareBy({ it.name }, { it.desc }))?.forEach { method -> + + // Don't print ctor's from super classes. + if (isSuperClass) { + if (CTOR_NAME == method.name || CLASS_INITIALIZER_NAME == method.name) { + return@forEach + } + } + // If we already printed the method from a subclass, don't print it. + if (isDuplicate(method.name, method.desc)) { + return@forEach + } + + val policy = filter.getPolicyForMethod(methodClass.name, method.name, method.desc) + + // Let's skip "Remove" APIs. Ideally we want to print it, just to make the CSV + // complete, we still need to hide methods substituted (== @RavenwoodReplace) methods + // and for now we don't have an easy way to detect it. + if (policy.policy == FilterPolicy.Remove) { + return@forEach + } + + val renameTo = filter.getRenameTo(methodClass.name, method.name, method.desc) + + dumpMethod(pkg, cls, isSuperClass, methodClass.name.toHumanReadableClassName(), + renameTo ?: method.name, method.desc, policy) + } + + // Dump super class methods. + dumpSuper(dumpClass, methodClass.superName) + + // Dump interface methods (which may have default methods). + methodClass.interfaces?.sorted()?.forEach { interfaceName -> + dumpSuper(dumpClass, interfaceName) + } + } + + /** + * Dump a given super class / interface. + */ + private fun dumpSuper( + dumpClass: ClassNode, + methodClassName: String, + ) { + classes.findClass(methodClassName)?.let { methodClass -> + dump(dumpClass, methodClass) + return + } + frameworkClasses?.findClass(methodClassName)?.let { methodClass -> + dump(dumpClass, methodClass) + return + } + if (methodClassName.startsWith("java/") || + methodClassName.startsWith("javax/") + ) { + dumpStandardClass(dumpClass, methodClassName) + return + } + log.w("Super class or interface $methodClassName (used by ${dumpClass.name}) not found.") + } + + /** + * Dump methods from Java standard classes. + */ + private fun dumpStandardClass( + dumpClass: ClassNode, + methodClassName: String, + ) { + val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName() + val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName() + + val methodClassName = methodClassName.toHumanReadableClassName() + + try { + val clazz = Class.forName(methodClassName) + + // Method.getMethods() returns only public methods, but with inherited ones. + // Method.getDeclaredMethods() returns private methods too, but no inherited methods. + // + // Since we're only interested in public ones, just use getMethods(). + clazz.methods.forEach { method -> + val methodName = method.name + val methodDesc = Type.getMethodDescriptor(method) + + // If we already printed the method from a subclass, don't print it. + if (isDuplicate(methodName, methodDesc)) { + return@forEach + } + + dumpMethod(pkg, cls, true, methodClassName, + methodName, methodDesc, javaStandardApiPolicy) + } + } catch (e: ClassNotFoundException) { + log.w("JVM type $methodClassName (used by ${dumpClass.name}) not found.") + } + } +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt index 45e140c8e3ff..6643492a1394 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt @@ -20,8 +20,8 @@ import com.android.hoststubgen.HostStubGenStats import com.android.hoststubgen.LogLevel import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.UnifiedVisitor -import com.android.hoststubgen.asm.getPackageNameFromClassName -import com.android.hoststubgen.asm.resolveClassName +import com.android.hoststubgen.asm.getPackageNameFromFullClassName +import com.android.hoststubgen.asm.resolveClassNameWithDefaultPackage import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.filters.FilterPolicy import com.android.hoststubgen.filters.FilterPolicyWithReason @@ -89,7 +89,7 @@ abstract class BaseAdapter ( ) { super.visit(version, access, name, signature, superName, interfaces) currentClassName = name - currentPackageName = getPackageNameFromClassName(name) + currentPackageName = getPackageNameFromFullClassName(name) classPolicy = filter.getPolicyForClass(currentClassName) log.d("[%s] visit: %s (package: %s)", this.javaClass.simpleName, name, currentPackageName) @@ -98,7 +98,8 @@ abstract class BaseAdapter ( log.indent() filter.getNativeSubstitutionClass(currentClassName)?.let { className -> - val fullClassName = resolveClassName(className, currentPackageName).toJvmClassName() + val fullClassName = resolveClassNameWithDefaultPackage(className, currentPackageName) + .toJvmClassName() log.d(" NativeSubstitutionClass: $fullClassName") if (classes.findClass(fullClassName) == null) { log.w("Native substitution class $fullClassName not found. Class must be " + |