Loading ravenwood/scripts/ravenwood-stats-collector.sh +11 −4 Original line number Diff line number Diff line Loading @@ -39,13 +39,17 @@ dump() { local jar=$1 local file=$2 # 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' # 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" Loading @@ -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" Loading tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +6 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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") } } Loading tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt +4 −41 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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") Loading Loading @@ -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) } } } tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt +33 −5 Original line number Diff line number Diff line Loading @@ -76,17 +76,45 @@ fun findAnnotationValueAsString(an: AnnotationNode, propertyName: String): Strin return null } private val removeLastElement = """[./][^./]*$""".toRegex() val periodOrSlash = charArrayOf('.', '/') fun getPackageNameFromClassName(className: String): String { return className.replace(removeLastElement, "") fun getPackageNameFromFullClassName(fullClassName: String): String { val pos = fullClassName.lastIndexOfAny(periodOrSlash) if (pos == -1) { return "" } else { return fullClassName.substring(0, pos) } } fun resolveClassName(className: String, packageName: String): String { fun getClassNameFromFullClassName(fullClassName: String): String { val pos = fullClassName.lastIndexOfAny(periodOrSlash) if (pos == -1) { return fullClassName } else { return fullClassName.substring(pos + 1) } } 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) } } /** * 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 { Loading tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/dumper/ApiDumper.kt 0 → 100644 +202 −0 Original line number Diff line number Diff line /* * 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.") } } } Loading
ravenwood/scripts/ravenwood-stats-collector.sh +11 −4 Original line number Diff line number Diff line Loading @@ -39,13 +39,17 @@ dump() { local jar=$1 local file=$2 # 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' # 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" Loading @@ -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" Loading
tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +6 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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") } } Loading
tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt +4 −41 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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") Loading Loading @@ -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) } } }
tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt +33 −5 Original line number Diff line number Diff line Loading @@ -76,17 +76,45 @@ fun findAnnotationValueAsString(an: AnnotationNode, propertyName: String): Strin return null } private val removeLastElement = """[./][^./]*$""".toRegex() val periodOrSlash = charArrayOf('.', '/') fun getPackageNameFromClassName(className: String): String { return className.replace(removeLastElement, "") fun getPackageNameFromFullClassName(fullClassName: String): String { val pos = fullClassName.lastIndexOfAny(periodOrSlash) if (pos == -1) { return "" } else { return fullClassName.substring(0, pos) } } fun resolveClassName(className: String, packageName: String): String { fun getClassNameFromFullClassName(fullClassName: String): String { val pos = fullClassName.lastIndexOfAny(periodOrSlash) if (pos == -1) { return fullClassName } else { return fullClassName.substring(pos + 1) } } 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) } } /** * 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 { Loading
tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/dumper/ApiDumper.kt 0 → 100644 +202 −0 Original line number Diff line number Diff line /* * 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.") } } }