├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── capt-api ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── coffeepartner │ └── capt │ └── plugin │ └── api │ ├── Arguments.java │ ├── Capt.java │ ├── CaptInternal.java │ ├── Context.java │ ├── OutputProvider.java │ ├── Plugin.java │ ├── annotations │ └── Def.java │ ├── asm │ └── CaptClassVisitor.java │ ├── graph │ ├── ClassGraph.java │ ├── ClassInfo.java │ ├── MethodInfo.java │ └── Status.java │ ├── hint │ ├── Thread.java │ └── Type.java │ ├── log │ ├── LogLevel.java │ └── Logger.java │ ├── process │ ├── AnnotationProcessor.java │ └── ClassConsumer.java │ ├── transform │ ├── ClassRequest.java │ ├── ClassTransformer.java │ └── TransformContext.java │ └── util │ └── RelativeDirectoryProvider.java ├── capt-plugin ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── coffeepartner │ └── capt │ └── plugin │ ├── CaptTransform.java │ ├── GradleCaptExtension.java │ ├── GradleCaptPlugin.java │ ├── api │ ├── asm │ │ └── ClassVisitorManager.java │ └── logger │ │ └── LoggerFactory.java │ ├── cache │ ├── FileManager.java │ ├── InternalCache.java │ ├── OutputProviderFactory.java │ ├── RelativeDirectoryProviderFactory.java │ └── RelativeDirectoryProviderFactoryImpl.java │ ├── dsl │ ├── CaptPluginExtension.java │ └── ConfigurableMap.java │ ├── graph │ ├── ApkClassGraph.java │ ├── ApkClassInfo.java │ ├── ClassBean.java │ └── MethodBean.java │ ├── process │ ├── CommonArgs.java │ ├── PluginBean.java │ ├── PluginManager.java │ ├── plugin │ │ ├── ForwardingCapt.java │ │ ├── GlobalCapt.java │ │ └── PluginWrapper.java │ └── visitors │ │ ├── AnnotationClassDispatcher.java │ │ ├── FirstRound.java │ │ └── ThirdRound.java │ ├── resource │ ├── GlobalResource.java │ ├── PluginResource.java │ ├── TransformResource.java │ └── VariantResource.java │ ├── util │ ├── CaptThreadFactory.java │ ├── ClassWalker.java │ ├── ConcurrentHashSet.java │ ├── Constants.java │ ├── Functions.java │ ├── Util.java │ ├── WaitableTasks.java │ └── asm │ │ └── AnnotationSniffer.java │ └── variant │ ├── VariantManager.java │ └── VariantScope.java ├── config └── checkstyle │ └── checkstyle.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── publish.gradle ├── samples └── SafeCatcher │ ├── .gitignore │ ├── README.md │ ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── coffeepartner │ │ │ └── capt │ │ │ └── example │ │ │ ├── ExampleInstrumentedTest.java │ │ │ ├── MatchesInAndroidTest.java │ │ │ └── ParseIntTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── coffeepartner │ │ │ │ └── capt │ │ │ │ └── example │ │ │ │ ├── MainActivity.java │ │ │ │ └── Matches.java │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ └── content_main.xml │ │ │ ├── menu │ │ │ └── menu_main.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── coffeepartner │ │ └── capt │ │ └── example │ │ └── ExampleUnitTest.java │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── safecatcher │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── coffeepartner │ │ │ └── capt │ │ │ └── sample │ │ │ └── safecatcher │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── coffeepartner │ │ │ │ └── capt │ │ │ │ └── sample │ │ │ │ └── safecatcher │ │ │ │ ├── MethodReplacer.java │ │ │ │ ├── SafePlugin.java │ │ │ │ └── TestPlugin.java │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ └── resources │ │ │ └── META-INF │ │ │ └── capt-plugins │ │ │ ├── com.example.safecatcher.properties │ │ │ └── com.example.test.properties │ │ └── test │ │ └── java │ │ └── coffeepartner │ │ └── capt │ │ └── sample │ │ └── safecatcher │ │ └── ExampleUnitTest.java │ ├── safecatcherrt │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── coffeepartner │ │ │ └── capt │ │ │ └── sample │ │ │ └── safecatcher │ │ │ └── rt │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── coffeepartner │ │ │ │ └── capt │ │ │ │ └── sample │ │ │ │ └── safecatcher │ │ │ │ └── rt │ │ │ │ └── Match.java │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── coffeepartner │ │ └── capt │ │ └── sample │ │ └── safecatcher │ │ └── rt │ │ └── ExampleUnitTest.java │ └── settings.gradle └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings 5 | eclipsebin 6 | .gradle 7 | bin 8 | gen 9 | build 10 | out 11 | lib 12 | target 13 | pom.xml.* 14 | # Idea 15 | .idea 16 | *.iml 17 | obj 18 | #MacOS 19 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | anguage: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | notifications: 7 | email: false 8 | 9 | sudo: false 10 | 11 | 12 | script: ./gradlew check 13 | 14 | after_success: 15 | 16 | branches: 17 | except: 18 | - gh-pages 19 | 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #Change Log 2 | 3 | 4 | ## Version 1.0.0-RC2 5 | 6 | **API changes** 7 | 8 | - [Pull 1](https://github.com/CoffeePartner/capt/pull/1): `RelativeDirectoryProvider` API & dispatch logic. 9 | 10 | ## Version 1.0.0-RC1 11 | *2019-01-03* 12 | - Initial release. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # capt 2 | 3 | [ ![Download](https://api.bintray.com/packages/dieyi/maven/capt-api/images/download.svg) ](https://bintray.com/dieyi/maven/capt-api/_latestVersion)[ ![Build Status](https://travis-ci.org/CoffeePartner/capt.svg?branch=master)](https://travis-ci.org/CoffeePartner/capt)[ ![codebeat badge](https://codebeat.co/badges/da0511cf-b82f-4f16-a8d3-69d3b005d61b)](https://codebeat.co/projects/github-com-coffeepartner-capt-master) 4 | 5 | Capt is short for Class Annotation Processor Tool on Android. 6 | 7 | Like apt, capt provide some mechanism to parse annotations at compile time. 8 | 9 | But capt can do more stuff than apt, because capt visit every class that will packing into APK, and apt only visit java sources. 10 | 11 | Further more, capt also provides the chance to update every original class. 12 | 13 | For more information please see the [wiki](https://github.com/CoffeePartner/capt/wiki). 14 | 15 | Hope you enjoy it! 16 | 17 | ## Getting Started 18 | 19 | ##### Add capt plugin to gradle script classpath: 20 | 21 | ```groovy 22 | classpath 'coffeepartner.capt:plugin:1.0.0-RC2' 23 | // Take care, 1.0.0-RC2 is incompatible with Android Gradle Plugin 3.3.0 because of the Guava API's break change, 24 | // 1.0.0 will fix it by using Gradle API instead. 25 | ``` 26 | 27 | ##### Apply capt plugin on Android application or library module: 28 | 29 | ```gradle 30 | apply plugin: 'com.android.application' 31 | // or apply plugin: 'com.android.library' 32 | apply plugin: 'capt' 33 | ``` 34 | 35 | ##### Add plugins for capt 36 | 37 | ```groovy 38 | dependencies { 39 | capt 'xxx:xxx:x.y.z' 40 | capt files('xxx') 41 | capt project(":xxx") // java library and android library both supported 42 | debugCapt 'xxx' 43 | androidTestCapt 'xxx' 44 | } 45 | 46 | capt { 47 | plugins { 48 | pluginOfCapt { 49 | // arguments for pluginOfCapt 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | ## Documentation 56 | 57 | * User guide: This guide contains examples on how to use capt in your gradle script. 58 | * [Writing Capt Plugins](https://github.com/CoffeePartner/capt/wiki): This guide introduce how to develop a plugin of capt. 59 | * Design document: This document discusses issues we faced while designing capt. 60 | 61 | ## Releases Plan 62 | 63 | - - **03/01/2019: Release Candidate 1: stabilized API and feature set** 64 | - - **17/01/2019: Release Candidate 2: addressing feedback from RC 1** 65 | - 31/01/2019: Stable Release: General availability 66 | 67 | ## License 68 | 69 | Licensed under the Apache License, Version 2.0 (the "License"); 70 | you may not use this file except in compliance with the License. 71 | You may obtain a copy of the License at 72 | 73 | http://www.apache.org/licenses/LICENSE-2.0 74 | 75 | Unless required by applicable law or agreed to in writing, software 76 | distributed under the License is distributed on an "AS IS" BASIS, 77 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 78 | See the License for the specific language governing permissions and 79 | limitations under the License. 80 | 81 | 82 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | capt_group = 'coffeepartner.capt' 4 | capt_version = '1.0.0-RC2' 5 | 6 | gson_version = '2.8.5' 7 | okio_version = '1.16.0' 8 | asm_version = '6.0' 9 | 10 | // minimal supported 11 | android_tools_version = '3.0.0' 12 | } 13 | repositories { 14 | jcenter() 15 | } 16 | dependencies { 17 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' 18 | } 19 | } 20 | 21 | subprojects { 22 | repositories { 23 | google() 24 | jcenter() 25 | mavenCentral() 26 | } 27 | 28 | apply plugin: 'checkstyle' 29 | } 30 | 31 | try { 32 | apply from: 'publish.gradle' 33 | } catch (e) { 34 | e.printStackTrace() 35 | } 36 | 37 | -------------------------------------------------------------------------------- /capt-api/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /capt-api/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | dependencies { 4 | compile fileTree(dir: 'libs', include: ['*.jar']) 5 | compileOnly "com.google.code.findbugs:jsr305:3.0.2" 6 | compile "com.squareup.okio:okio:$okio_version" 7 | compile "org.ow2.asm:asm:$asm_version" 8 | compile "org.ow2.asm:asm-tree:$asm_version" 9 | compile "com.google.code.gson:gson:$gson_version" 10 | compileOnly gradleApi() 11 | compileOnly "com.android.tools.build:gradle:$android_tools_version" 12 | } 13 | 14 | sourceCompatibility = "1.7" 15 | targetCompatibility = "1.7" 16 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/Arguments.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api; 2 | 3 | import java.util.Map; 4 | 5 | public interface Arguments { 6 | 7 | PluginArguments getArgumentsById(String id); 8 | 9 | PluginArguments getMyArguments(); 10 | 11 | interface PluginArguments { 12 | 13 | /** 14 | * @return priority of plugi 15 | */ 16 | int priority(); 17 | 18 | /** 19 | * @return build.gradle passed arguments for specific plugin 20 | */ 21 | Map arguments(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/Capt.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api; 2 | 3 | import coffeepartner.capt.plugin.api.graph.ClassGraph; 4 | import org.gradle.internal.HasInternalProtocol; 5 | 6 | 7 | @HasInternalProtocol 8 | public interface Capt { 9 | /** 10 | * @return true if this build is incremental 11 | */ 12 | boolean isIncremental(); 13 | 14 | /** 15 | * @return context cpat passed 16 | */ 17 | Context getContext(); 18 | 19 | /** 20 | * @return class graph in apk 21 | */ 22 | ClassGraph getClassGraph(); 23 | 24 | /** 25 | * @return arguments in build.gradle 26 | */ 27 | Arguments getArgs(); 28 | 29 | /** 30 | * @return output provider to store cache 31 | */ 32 | OutputProvider getOutputs(); 33 | } 34 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/CaptInternal.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api; 2 | 3 | import com.android.build.gradle.BaseExtension; 4 | import org.gradle.api.Project; 5 | 6 | import java.net.URLClassLoader; 7 | 8 | /** 9 | * You'd better use {@link Capt}. 10 | * If you create capt plugin by java/groovy plugin with Gradle runtime, you can use CaptInternal. 11 | */ 12 | public interface CaptInternal extends Capt { 13 | 14 | /** 15 | * @return this project 16 | */ 17 | Project getProject(); 18 | 19 | /** 20 | * @return android extension 21 | */ 22 | BaseExtension getAndroid(); 23 | 24 | /** 25 | * @return the class loader contains all cpat configuration files correspond to variant 26 | */ 27 | URLClassLoader captLoader(); 28 | } 29 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/Context.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api; 2 | 3 | import coffeepartner.capt.plugin.api.log.Logger; 4 | 5 | import java.util.concurrent.ExecutorService; 6 | import java.util.concurrent.ForkJoinPool; 7 | 8 | public interface Context { 9 | 10 | /** 11 | * @return variant name of this build 12 | */ 13 | String getVariantName(); 14 | 15 | /** 16 | * @param clazz class 17 | * @return the logger 18 | */ 19 | Logger getLogger(Class clazz); 20 | 21 | /** 22 | * @return thread pool to do computation jobs 23 | */ 24 | ForkJoinPool getComputation(); 25 | 26 | /** 27 | * @return thread pool to do io jobs 28 | */ 29 | ExecutorService getIo(); 30 | } 31 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/OutputProvider.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api; 2 | 3 | import coffeepartner.capt.plugin.api.util.RelativeDirectoryProvider; 4 | 5 | import java.io.IOException; 6 | 7 | public interface OutputProvider { 8 | 9 | /** 10 | * Ensure the directory exists, unique for every plugin. 11 | * 12 | * @param type type 13 | * @return dir 14 | */ 15 | RelativeDirectoryProvider getProvider(Type type); 16 | 17 | /** 18 | * Remove all content of this plugin. 19 | * 20 | * @throws IOException if deleting failed. 21 | */ 22 | void deleteAll() throws IOException; 23 | 24 | 25 | enum Type { 26 | /** 27 | * the generated byte-code in this dir will also be added to the transform pipeline as a 28 | * EXTERNAL_LIBRARIES stream. 29 | *

30 | * Write class file in sub directory according to its package! 31 | * MUST NOT write jar or other strange things! 32 | */ 33 | CLASS, 34 | /** 35 | * Any change in this dir after build will led next build to full mode. 36 | */ 37 | CACHE, 38 | /** 39 | * Don't depends on files on it from last build. 40 | */ 41 | TEMP 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/Plugin.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api; 2 | 3 | import coffeepartner.capt.plugin.api.annotations.Def; 4 | import coffeepartner.capt.plugin.api.hint.Thread; 5 | import coffeepartner.capt.plugin.api.hint.Type; 6 | import coffeepartner.capt.plugin.api.process.AnnotationProcessor; 7 | import coffeepartner.capt.plugin.api.transform.ClassTransformer; 8 | 9 | import javax.annotation.Nullable; 10 | import java.io.IOException; 11 | import java.util.Collections; 12 | import java.util.HashSet; 13 | import java.util.Objects; 14 | import java.util.Set; 15 | 16 | /** 17 | * Lifecycle: 18 | * 1. beforeCreate in priority order 19 | * 2. onCreate concurrently 20 | * 3. Parse classes by {@link AnnotationProcessor} 21 | * 4. Transform every class from inputs by {@link ClassTransformer} 22 | * 5. onDestroy concurrently 23 | */ 24 | public abstract class Plugin { 25 | 26 | public final Set getSupportedAnnotations() { 27 | return arrayToSet(getDef().supportedAnnotationTypes()); 28 | } 29 | 30 | private Def getDef() { 31 | return Objects.requireNonNull(getClass().getAnnotation(Def.class), "Require @Def on " + getClass().getName()); 32 | } 33 | 34 | public final int defaultPriority() { 35 | return getDef().defaultPriority(); 36 | } 37 | 38 | /** 39 | * Apply the plugin, don't do too much work(such as read / write file) in this function. 40 | * 41 | * @param capt Plugin context 42 | */ 43 | @Thread(Type.SINGLE) 44 | public void beforeCreate(T capt) { 45 | } 46 | 47 | /** 48 | * Do your time-consuming tasks in this function. 49 | * 50 | * @param capt Plugin context 51 | * @throws IOException io 52 | * @throws InterruptedException inter 53 | */ 54 | @Thread(Type.IO) 55 | public abstract void onCreate(T capt) throws IOException, InterruptedException ; 56 | 57 | /** 58 | * @return meta processor 59 | */ 60 | @Thread(Type.SINGLE) 61 | @Nullable 62 | public AnnotationProcessor onProcessAnnotations() { 63 | return null; 64 | } 65 | 66 | /** 67 | * @return transformer 68 | */ 69 | @Thread(Type.SINGLE) 70 | @Nullable 71 | public ClassTransformer onTransformClass() { 72 | return null; 73 | } 74 | 75 | /** 76 | * The last time to do your stuff. 77 | * 78 | * @param capt Plugin context 79 | * @throws IOException io 80 | * @throws InterruptedException inter 81 | */ 82 | @Thread(Type.IO) 83 | public void onDestroy(T capt) throws IOException, InterruptedException { 84 | } 85 | 86 | private static Set arrayToSet(String[] array) { 87 | assert array != null; 88 | Set set = new HashSet(array.length); 89 | for (String s : array) 90 | set.add(s.replace('.', '/')); 91 | return Collections.unmodifiableSet(set); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/annotations/Def.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Def { 11 | 12 | /** 13 | * Default priority, can be override by config. 14 | * 15 | * @return default priority 16 | */ 17 | int defaultPriority() default 0; 18 | 19 | /** 20 | * @return supported Annotation types 21 | */ 22 | String[] supportedAnnotationTypes() default {}; 23 | } 24 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/asm/CaptClassVisitor.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.asm; 2 | 3 | import coffeepartner.capt.plugin.api.transform.TransformContext; 4 | import org.objectweb.asm.ClassReader; 5 | import org.objectweb.asm.ClassVisitor; 6 | import org.objectweb.asm.ClassWriter; 7 | import org.objectweb.asm.Opcodes; 8 | 9 | import javax.annotation.Nullable; 10 | import java.util.Iterator; 11 | import java.util.Objects; 12 | 13 | public abstract class CaptClassVisitor extends ClassVisitor { 14 | 15 | private TransformContext context; 16 | 17 | public CaptClassVisitor() { 18 | this(null); 19 | } 20 | 21 | public CaptClassVisitor(@Nullable CaptClassVisitor next) { 22 | super(Opcodes.ASM5, next); 23 | } 24 | 25 | protected final TransformContext context() { 26 | return Objects.requireNonNull(context, "Don't use context() outside visit lifecycle."); 27 | } 28 | 29 | final void detach() { 30 | this.context = null; 31 | } 32 | 33 | final void link(ClassVisitor next) { 34 | if (cv != null) { 35 | if (cv instanceof CaptClassVisitor) { 36 | ((CaptClassVisitor) cv).link(next); 37 | } else { 38 | throw new IllegalStateException("Require CaptClassVisitor or subclass, but is " 39 | + cv.getClass().getName()); 40 | } 41 | } else { 42 | cv = next; 43 | } 44 | } 45 | 46 | final Iterator iterate() { 47 | return new Iterator() { 48 | CaptClassVisitor c = CaptClassVisitor.this; 49 | 50 | @Override 51 | public boolean hasNext() { 52 | return c != null; 53 | } 54 | 55 | @Override 56 | public CaptClassVisitor next() { 57 | CaptClassVisitor cur = c; 58 | c = c.cv instanceof CaptClassVisitor ? (CaptClassVisitor) c.cv : null; 59 | return cur; 60 | } 61 | 62 | @Override 63 | public void remove() { 64 | throw new UnsupportedOperationException(); 65 | } 66 | }; 67 | } 68 | 69 | final void attach(TransformContext context) { 70 | this.context = context; 71 | } 72 | 73 | /** 74 | * flags: 75 | * lower 16bit: 76 | * {@link ClassReader#SKIP_CODE} 77 | * {@link ClassReader#SKIP_DEBUG} 78 | * {@link ClassReader#SKIP_FRAMES} 79 | * {@link ClassReader#EXPAND_FRAMES} 80 | * higher 16bit: 81 | * {@link ClassWriter#COMPUTE_MAXS} 82 | * {@link ClassWriter#COMPUTE_FRAMES} 83 | *

84 | * example: {@code ClassWriter.COMPUTE_MAXS << 16 | ClassReader.EXPAND_FRAMES} 85 | * Take care, the flags affect every visitor on the chain. 86 | * 87 | * @return the required flag 88 | */ 89 | protected int beforeAttach() { 90 | return 0; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/graph/ClassGraph.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.graph; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public interface ClassGraph { 6 | 7 | /** 8 | * 9 | * @param name class name 10 | * @return class info 11 | */ 12 | @Nullable 13 | ClassInfo get(String name); 14 | } 15 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/graph/ClassInfo.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.graph; 2 | 3 | import javax.annotation.Nullable; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.List; 7 | 8 | /** 9 | * REMOVED and NOT_EXISTS are not in graph. 10 | */ 11 | public interface ClassInfo { 12 | 13 | /** 14 | * @return status from last build 15 | */ 16 | Status status(); 17 | 18 | /** 19 | * Alias for: status != REMOVED and status != NOT_EXISTS. 20 | * 21 | * @return true if exists in APK. 22 | */ 23 | boolean exists(); 24 | 25 | /** 26 | * class access with deprecated flag, see Opcodes.ACC_DEPRECATED. 27 | * 28 | * @return class access. 29 | */ 30 | int access(); 31 | 32 | /** 33 | * @return class name. 34 | */ 35 | String name(); 36 | 37 | /** 38 | * Use it carefully! This class MUST NOT depend on classes in Android Framework. 39 | * 40 | * @return class of this. 41 | * @throws ClassNotFoundException not found class 42 | */ 43 | Class loadClass() throws ClassNotFoundException; 44 | 45 | 46 | /** 47 | * if {@link #exists()} returns false, it will open failed. 48 | * 49 | * @return the input stream of class bytes 50 | * @throws IOException if open failed 51 | */ 52 | InputStream openStream() throws IOException; 53 | 54 | /** 55 | * @return the signature of this class. May be {@literal null} if the class is not a 56 | * generic one, and does not extend or implement generic classes or interfaces. 57 | */ 58 | @Nullable 59 | String signature(); 60 | 61 | /** 62 | * Returns null if this class is not exists in apk. 63 | * 64 | * @return super class 65 | */ 66 | @Nullable 67 | ClassInfo parent(); 68 | 69 | /** 70 | * @return The methods it own. 71 | */ 72 | List methods(); 73 | 74 | /** 75 | * If this is a interface, returns this extends interfaces. 76 | * Otherwise, returns this implements interfaces. 77 | * 78 | * @return interfaces. 79 | */ 80 | List interfaces(); 81 | 82 | /** 83 | * If this is not a interface, returns classes extends this. 84 | * Otherwise, returns empty list. 85 | * 86 | * @return classes extends this. 87 | */ 88 | List classChildren(); 89 | 90 | /** 91 | * If this is interface, returns interfaces extends this. 92 | * Otherwise, returns empty list. 93 | * 94 | * @return interfaces extends this. 95 | */ 96 | List interfaceChildren(); 97 | 98 | /** 99 | * If this is interface, returns classes implements this "directly". 100 | * Directly means literal implements on class definition. 101 | * 102 | * @return classes implements this. 103 | */ 104 | List implementedClasses(); 105 | 106 | } 107 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/graph/MethodInfo.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.graph; 2 | 3 | 4 | import javax.annotation.Nullable; 5 | 6 | public interface MethodInfo { 7 | 8 | /** 9 | * method access with deprecated flag, see Opcodes.ACC_DEPRECATED. 10 | * 11 | * @return method access 12 | */ 13 | int access(); 14 | 15 | /** 16 | * @return method name 17 | */ 18 | String name(); 19 | 20 | /** 21 | * @return method desc 22 | */ 23 | String desc(); 24 | 25 | /** 26 | * @return the method's signature. May be {@literal null} if the method parameters, 27 | * return type and exceptions do not use generic types. 28 | */ 29 | @Nullable 30 | String signature(); 31 | } 32 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/graph/Status.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.graph; 2 | 3 | public enum Status { 4 | /** 5 | * The file was not changed since the last build. 6 | */ 7 | NOT_CHANGED, 8 | /** 9 | * The file was added since the last build. 10 | */ 11 | ADDED, 12 | /** 13 | * The file was modified since the last build. 14 | */ 15 | CHANGED, 16 | /** 17 | * The file was removed since the last build. 18 | */ 19 | REMOVED, 20 | 21 | /** 22 | * The file is not exists in APK. 23 | */ 24 | NOT_EXISTS 25 | } 26 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/hint/Thread.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.hint; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Notify function on which thread type. 7 | */ 8 | @Documented 9 | @Retention(RetentionPolicy.SOURCE) 10 | @Target(ElementType.METHOD) 11 | public @interface Thread { 12 | 13 | Type value(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/hint/Type.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.hint; 2 | 3 | public enum Type { 4 | SINGLE, 5 | IO, 6 | COMPUTATION 7 | } 8 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/log/LogLevel.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.log; 2 | 3 | /** 4 | * The log levels supported by Capt. 5 | */ 6 | public enum LogLevel { 7 | DEBUG, 8 | INFO, 9 | LIFECYCLE, 10 | WARN, 11 | QUIET, 12 | ERROR 13 | } 14 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/log/Logger.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.log; 2 | 3 | public interface Logger { 4 | 5 | /** 6 | * @param level level 7 | * @return true if the given log level is enabled for this logger. 8 | */ 9 | boolean isEnabled(LogLevel level); 10 | 11 | /** 12 | * Logs the given message at debug log level. 13 | * 14 | * @param message the log message. 15 | */ 16 | void debug(String message); 17 | 18 | /** 19 | * Logs the given message at debug log level. 20 | * 21 | * @param message the log message. 22 | * @param objects the log message parameters. 23 | */ 24 | void debug(String message, Object... objects); 25 | 26 | /** 27 | * Logs the given message at debug log level. 28 | * 29 | * @param message the log message. 30 | * @param throwable the exception to log. 31 | */ 32 | void debug(String message, Throwable throwable); 33 | 34 | /** 35 | * Logs the given message at info log level. 36 | * 37 | * @param message the log message. 38 | */ 39 | void info(String message); 40 | 41 | /** 42 | * Logs the given message at info log level. 43 | * 44 | * @param message the log message. 45 | * @param objects the log message parameters. 46 | */ 47 | void info(String message, Object... objects); 48 | 49 | /** 50 | * Logs the given message at info log level. 51 | * 52 | * @param message the log message. 53 | * @param throwable the exception to log. 54 | */ 55 | void info(String message, Throwable throwable); 56 | 57 | /** 58 | * Logs the given message at lifecycle log level. 59 | * 60 | * @param message the log message. 61 | */ 62 | void lifecycle(String message); 63 | 64 | /** 65 | * Logs the given message at lifecycle log level. 66 | * 67 | * @param message the log message. 68 | * @param objects the log message parameters. 69 | */ 70 | void lifecycle(String message, Object... objects); 71 | 72 | /** 73 | * Logs the given message at lifecycle log level. 74 | * 75 | * @param message the log message. 76 | * @param throwable the exception to log. 77 | */ 78 | void lifecycle(String message, Throwable throwable); 79 | 80 | /** 81 | * Logs the given message at warn log level. 82 | * 83 | * @param message the log message. 84 | */ 85 | void warn(String message); 86 | 87 | /** 88 | * Logs the given message at warn log level. 89 | * 90 | * @param message the log message. 91 | * @param objects the log message parameters. 92 | */ 93 | void warn(String message, Object... objects); 94 | 95 | /** 96 | * Logs the given message at warn log level. 97 | * 98 | * @param message the log message. 99 | * @param throwable the exception to log. 100 | */ 101 | void warn(String message, Throwable throwable); 102 | 103 | /** 104 | * Logs the given message at quiet log level. 105 | * 106 | * @param message the log message. 107 | */ 108 | void quiet(String message); 109 | 110 | /** 111 | * Logs the given message at quiet log level. 112 | * 113 | * @param message the log message. 114 | * @param objects the log message parameters. 115 | */ 116 | void quiet(String message, Object... objects); 117 | 118 | /** 119 | * Logs the given message at quiet log level. 120 | * 121 | * @param message the log message. 122 | * @param throwable the exception to log. 123 | */ 124 | void quiet(String message, Throwable throwable); 125 | 126 | /** 127 | * Logs the given message at error log level. 128 | * 129 | * @param message the log message. 130 | */ 131 | void error(String message); 132 | 133 | /** 134 | * Logs the given message at error log level. 135 | * 136 | * @param message the log message. 137 | * @param objects the log message parameters. 138 | */ 139 | void error(String message, Object... objects); 140 | 141 | /** 142 | * Logs the given message at error log level. 143 | * 144 | * @param message the log message. 145 | * @param throwable the exception to log. 146 | */ 147 | void error(String message, Throwable throwable); 148 | } 149 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/process/AnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.process; 2 | 3 | import coffeepartner.capt.plugin.api.graph.ClassInfo; 4 | import coffeepartner.capt.plugin.api.hint.Thread; 5 | import coffeepartner.capt.plugin.api.hint.Type; 6 | 7 | import javax.annotation.Nullable; 8 | 9 | /** 10 | * Only class contains plugin interested annotations will pass to AnnotationProcessor. 11 | */ 12 | public class AnnotationProcessor { 13 | /** 14 | * Class changed, pre matched, but current mismatched, due to one of the following: 15 | * 1. No matched annotation in @Def. 16 | * 17 | * @param info info 18 | * @return class consumer 19 | */ 20 | @Thread(Type.COMPUTATION) 21 | @Nullable 22 | public ClassConsumer onAnnotationMismatch(ClassInfo info) { 23 | return null; 24 | } 25 | 26 | /** 27 | * Class changed, pre doesn't match, but current does. 28 | * 29 | * @param info info 30 | * @return class consumer 31 | */ 32 | @Thread(Type.COMPUTATION) 33 | @Nullable 34 | public ClassConsumer onAnnotationMatched(ClassInfo info) { 35 | return null; 36 | } 37 | 38 | /** 39 | * Class changed, both matched. 40 | * 41 | * @param info info 42 | * @return class consumer 43 | */ 44 | @Thread(Type.COMPUTATION) 45 | @Nullable 46 | public ClassConsumer onAnnotationChanged(ClassInfo info) { 47 | return null; 48 | } 49 | 50 | /** 51 | * Class removed, pre matched. 52 | * 53 | * @param info last info 54 | */ 55 | @Thread(Type.COMPUTATION) 56 | public void onAnnotationClassRemoved(ClassInfo info) { 57 | } 58 | 59 | /** 60 | * Class added, current matched. 61 | * 62 | * @param info info 63 | * @return class consumer 64 | */ 65 | @Thread(Type.COMPUTATION) 66 | @Nullable 67 | public ClassConsumer onAnnotationClassAdded(ClassInfo info) { 68 | return null; 69 | } 70 | 71 | /** 72 | * Class not changed, both matched. 73 | * If we not in incremental mode, we will call this method only. 74 | * 75 | * @param info info 76 | * @return class consumer 77 | */ 78 | @Thread(Type.COMPUTATION) 79 | @Nullable 80 | public ClassConsumer onAnnotationClassNotChanged(ClassInfo info) { 81 | return null; 82 | } 83 | 84 | @Thread(Type.COMPUTATION) 85 | public void onProcessEnd() { 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/process/ClassConsumer.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.process; 2 | 3 | import coffeepartner.capt.plugin.api.hint.Thread; 4 | import coffeepartner.capt.plugin.api.hint.Type; 5 | import org.objectweb.asm.tree.ClassNode; 6 | 7 | public interface ClassConsumer { 8 | 9 | @Thread(Type.COMPUTATION) 10 | void accept(ClassNode node); 11 | } 12 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/transform/ClassRequest.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.transform; 2 | 3 | 4 | import java.util.Collections; 5 | import java.util.Set; 6 | 7 | public class ClassRequest { 8 | 9 | public Scope scope() { 10 | return Scope.CHANGED; 11 | } 12 | 13 | /** 14 | * Regardless of scope(), extra specified classes. 15 | * 16 | * @return extra specified classes 17 | */ 18 | public Set extraSpecified() { 19 | return Collections.emptySet(); 20 | } 21 | 22 | public enum Scope { 23 | ALL, 24 | CHANGED, 25 | NONE, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/transform/ClassTransformer.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.transform; 2 | 3 | import coffeepartner.capt.plugin.api.asm.CaptClassVisitor; 4 | import coffeepartner.capt.plugin.api.graph.ClassInfo; 5 | import coffeepartner.capt.plugin.api.hint.Thread; 6 | import coffeepartner.capt.plugin.api.hint.Type; 7 | 8 | import javax.annotation.Nullable; 9 | import java.io.IOException; 10 | 11 | public abstract class ClassTransformer { 12 | /** 13 | * After parse every meta class, tell capt which classes require to transform. 14 | * 15 | * @return class request 16 | */ 17 | @Thread(Type.COMPUTATION) 18 | public ClassRequest beforeTransform() { 19 | return new ClassRequest(); 20 | } 21 | 22 | /** 23 | * @param classInfo the basic info of class 24 | * @param required true if the class in your ClassRequest 25 | * @return the class visitor to participate in class transform. 26 | */ 27 | @Thread(Type.COMPUTATION) 28 | @Nullable 29 | public abstract CaptClassVisitor onTransform(ClassInfo classInfo, boolean required); 30 | 31 | /** 32 | * Invoked after all class transform done. 33 | * @throws IOException io 34 | * @throws InterruptedException inter 35 | */ 36 | @Thread(Type.IO) 37 | public void afterTransform() throws IOException, InterruptedException { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/transform/TransformContext.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.transform; 2 | 3 | import org.objectweb.asm.ClassVisitor; 4 | 5 | /** 6 | * Context during single class transform. 7 | * TODO: more API 8 | */ 9 | public interface TransformContext { 10 | 11 | /** 12 | * This usually invoked on visitEnd(), tell capt if you changed the origin class content. 13 | */ 14 | void notifyChanged(); 15 | 16 | /** 17 | * @return The last ClassWriter on the chain. 18 | */ 19 | ClassVisitor getLastWriter(); 20 | } 21 | -------------------------------------------------------------------------------- /capt-api/src/main/java/coffeepartner/capt/plugin/api/util/RelativeDirectoryProvider.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.util; 2 | 3 | import okio.BufferedSink; 4 | import okio.BufferedSource; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | 9 | public interface RelativeDirectoryProvider { 10 | 11 | /** 12 | * @return get the root directory, may not exists 13 | */ 14 | File root(); 15 | 16 | /** 17 | * Use '/' as separator, even if windows. 18 | * 19 | * @param path the relative path 20 | * @return source 21 | * @throws IOException If create file failed. 22 | */ 23 | BufferedSource asSource(String path) throws IOException; 24 | 25 | 26 | /** 27 | * Use '/' as separator, even if windows. 28 | * 29 | * @param path the relative path 30 | * @throws IOException if delete failed 31 | */ 32 | void deleteIfExists(String path) throws IOException; 33 | 34 | /** 35 | * Use '/' as separator, even if windows. 36 | * 37 | * @param path the relative path 38 | * @return sink 39 | * @throws IOException If file not exists. 40 | */ 41 | BufferedSink asSink(String path) throws IOException; 42 | 43 | /** 44 | * Delete all content in {@link #root()}. This is useful when running in non-incremental mode 45 | * 46 | * @throws IOException if deleting the output failed. 47 | */ 48 | void deleteAll() throws IOException; 49 | } 50 | -------------------------------------------------------------------------------- /capt-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /capt-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-gradle-plugin' 2 | 3 | dependencies { 4 | implementation fileTree(dir: 'libs', include: ['*.jar']) 5 | compile "com.android.tools.build:gradle:$android_tools_version" 6 | compile project(":api") 7 | testImplementation 'junit:junit:4.12' 8 | } 9 | 10 | gradlePlugin { 11 | plugins { 12 | capt { 13 | id = 'capt' 14 | implementationClass = 'coffeepartner.capt.plugin.GradleCaptPlugin' 15 | } 16 | } 17 | } 18 | 19 | sourceCompatibility = "1.8" 20 | targetCompatibility = "1.8" 21 | 22 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/CaptTransform.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin; 2 | 3 | import com.android.build.api.transform.QualifiedContent; 4 | import com.android.build.api.transform.Transform; 5 | import com.android.build.api.transform.TransformException; 6 | import com.android.build.api.transform.TransformInvocation; 7 | import com.android.build.gradle.internal.pipeline.TransformManager; 8 | import coffeepartner.capt.plugin.util.Constants; 9 | import coffeepartner.capt.plugin.variant.VariantManager; 10 | import com.google.common.collect.ImmutableSet; 11 | import com.google.common.collect.Sets; 12 | import org.gradle.api.logging.Logger; 13 | import org.gradle.api.logging.Logging; 14 | 15 | import java.io.IOException; 16 | import java.util.Set; 17 | 18 | public class CaptTransform extends Transform implements Constants { 19 | 20 | private static final Logger LOGGER = Logging.getLogger(CaptTransform.class); 21 | private static final Set ALL = ImmutableSet.of( 22 | QualifiedContent.Scope.PROJECT, 23 | QualifiedContent.Scope.SUB_PROJECTS, 24 | QualifiedContent.Scope.EXTERNAL_LIBRARIES, 25 | QualifiedContent.Scope.PROVIDED_ONLY, 26 | QualifiedContent.Scope.TESTED_CODE); 27 | 28 | private final VariantManager variantManager; 29 | 30 | public CaptTransform(VariantManager variantManager) { 31 | this.variantManager = variantManager; 32 | } 33 | 34 | @Override 35 | public String getName() { 36 | return NAME; 37 | } 38 | 39 | @Override 40 | public Set getInputTypes() { 41 | return TransformManager.CONTENT_CLASS; 42 | } 43 | 44 | @Override 45 | public Set getScopes() { 46 | return variantManager.isApplication() ? TransformManager.SCOPE_FULL_PROJECT : TransformManager.PROJECT_ONLY; 47 | } 48 | 49 | /** 50 | * Needs other scopes to compute frame. 51 | */ 52 | @Override 53 | public Set getReferencedScopes() { 54 | return Sets.difference(ALL, getScopes()); 55 | } 56 | 57 | @Override 58 | public boolean isIncremental() { 59 | return true; 60 | } 61 | 62 | @Override 63 | public boolean isCacheable() { 64 | return true; 65 | } 66 | 67 | @Override 68 | public void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException { 69 | long startMs = System.currentTimeMillis(); 70 | LOGGER.lifecycle("Start capt transform for '{}', incremental: {}", invocation.getContext().getVariantName(), invocation.isIncremental()); 71 | variantManager.getVariantScope(invocation.getContext().getVariantName()) 72 | .doTransform(invocation); 73 | LOGGER.lifecycle("End capt transform, cost: {}ms", (System.currentTimeMillis() - startMs)); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/GradleCaptExtension.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin; 2 | 3 | import coffeepartner.capt.plugin.dsl.CaptPluginExtension; 4 | import org.gradle.api.Action; 5 | import org.gradle.api.NamedDomainObjectContainer; 6 | 7 | import javax.inject.Inject; 8 | 9 | public class GradleCaptExtension { 10 | 11 | private NamedDomainObjectContainer plugins; 12 | 13 | private boolean throwIfDuplicated = true; 14 | 15 | @Inject 16 | public GradleCaptExtension(NamedDomainObjectContainer plugins) { 17 | this.plugins = plugins; 18 | } 19 | 20 | public void plugins(Action> action) { 21 | action.execute(plugins); 22 | } 23 | 24 | public NamedDomainObjectContainer getPlugins() { 25 | return plugins; 26 | } 27 | 28 | public void setThrowIfDuplicated(boolean throwIfDuplicated) { 29 | this.throwIfDuplicated = throwIfDuplicated; 30 | } 31 | 32 | public void throwIfDuplicated(boolean throwIfDuplicated) { 33 | setThrowIfDuplicated(throwIfDuplicated); 34 | } 35 | 36 | public boolean getThrowIfDuplicated() { 37 | return throwIfDuplicated; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/GradleCaptPlugin.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin; 2 | 3 | import coffeepartner.capt.plugin.api.graph.ClassInfo; 4 | import coffeepartner.capt.plugin.dsl.CaptPluginExtension; 5 | import coffeepartner.capt.plugin.graph.ClassBean; 6 | import coffeepartner.capt.plugin.resource.GlobalResource; 7 | import coffeepartner.capt.plugin.util.CaptThreadFactory; 8 | import coffeepartner.capt.plugin.util.Constants; 9 | import coffeepartner.capt.plugin.variant.VariantManager; 10 | import com.android.build.gradle.AppPlugin; 11 | import com.android.build.gradle.BaseExtension; 12 | import com.android.build.gradle.LibraryPlugin; 13 | import com.google.common.reflect.TypeToken; 14 | import com.google.gson.Gson; 15 | import com.google.gson.GsonBuilder; 16 | import com.google.gson.InstanceCreator; 17 | import org.gradle.api.Plugin; 18 | import org.gradle.api.Project; 19 | import org.gradle.api.logging.Logger; 20 | import org.gradle.api.logging.Logging; 21 | 22 | import java.io.File; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | import java.util.concurrent.*; 26 | 27 | public class GradleCaptPlugin implements Plugin, Constants { 28 | 29 | private static final Logger LOGGER = Logging.getLogger(GradleCaptPlugin.class); 30 | 31 | @Override 32 | public void apply(Project project) { 33 | project.getPlugins().matching(p -> p instanceof AppPlugin || p instanceof LibraryPlugin) 34 | .all(c -> { 35 | 36 | BaseExtension baseExtension = (BaseExtension) project.getExtensions().getByName("android"); 37 | project.getExtensions().create(NAME, GradleCaptExtension.class, project.container(CaptPluginExtension.class)); 38 | 39 | VariantManager variantManager = new VariantManager(createGlobalResource(project, baseExtension), 40 | baseExtension, project); 41 | // callCreate configurations for separate variant 42 | variantManager.createConfigurationForVariant(); 43 | 44 | CaptTransform captTransform = new CaptTransform(variantManager); 45 | baseExtension.registerTransform(captTransform); 46 | }); 47 | } 48 | 49 | private static GlobalResource createGlobalResource(Project project, BaseExtension baseExtension) { 50 | 51 | int core = Runtime.getRuntime().availableProcessors(); 52 | // use 20s instead if 60s to opt memory 53 | // 3 x core threads at most 54 | // Use it combine with ForkJoinPool 55 | ExecutorService io = new ThreadPoolExecutor(0, core * 3, 56 | 20L, TimeUnit.SECONDS, 57 | new LinkedBlockingQueue<>(), 58 | new CaptThreadFactory()); 59 | 60 | // ForkJoinPool.common() just have core - 1, because it use the waiting thread, 61 | // But we just wait at IO threads, not computation, so we need core threads. 62 | ForkJoinPool computation = new ForkJoinPool(core); 63 | 64 | Gson gson = new GsonBuilder() 65 | .setPrettyPrinting() 66 | .disableHtmlEscaping() 67 | // optimize for List, reduce array copy 68 | .registerTypeAdapter(new TypeToken>() { 69 | }.getType(), (InstanceCreator) select -> new ArrayList(Constants.OPT_SIZE)) 70 | .create(); 71 | 72 | File root = new File(project.getBuildDir(), NAME); 73 | 74 | return new GlobalResource(project, root, computation, io, gson, (GradleCaptExtension) project.getExtensions().getByName(NAME), baseExtension); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/api/asm/ClassVisitorManager.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.asm; 2 | 3 | import coffeepartner.capt.plugin.api.transform.TransformContext; 4 | import com.google.common.collect.Streams; 5 | import org.objectweb.asm.ClassVisitor; 6 | 7 | import java.util.stream.Stream; 8 | 9 | public final class ClassVisitorManager { 10 | 11 | public void link(CaptClassVisitor pre, ClassVisitor next) { 12 | pre.link(next); 13 | } 14 | 15 | public void attach(CaptClassVisitor visitor, TransformContext context) { 16 | visitor.attach(context); 17 | } 18 | 19 | public Stream expand(CaptClassVisitor header) { 20 | return Streams.stream(header.iterate()); 21 | } 22 | 23 | public int beforeAttach(CaptClassVisitor visitor) { 24 | return visitor.beforeAttach(); 25 | } 26 | 27 | public void detach(CaptClassVisitor visitor) { 28 | visitor.detach(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/api/logger/LoggerFactory.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.api.logger; 2 | 3 | 4 | import coffeepartner.capt.plugin.api.log.LogLevel; 5 | import coffeepartner.capt.plugin.api.log.Logger; 6 | import org.gradle.api.logging.Logging; 7 | 8 | public class LoggerFactory { 9 | 10 | private LoggerFactory() { 11 | } 12 | 13 | public static Logger getLogger(Class clazz) { 14 | return new LoggerImpl(Logging.getLogger(clazz)); 15 | } 16 | 17 | static class LoggerImpl implements Logger { 18 | 19 | private final org.gradle.api.logging.Logger logger; 20 | 21 | LoggerImpl(org.gradle.api.logging.Logger logger) { 22 | this.logger = logger; 23 | } 24 | 25 | @Override 26 | public boolean isEnabled(LogLevel level) { 27 | org.gradle.api.logging.LogLevel logLevel; 28 | switch (level) { 29 | case DEBUG: 30 | logLevel = org.gradle.api.logging.LogLevel.DEBUG; 31 | break; 32 | case INFO: 33 | logLevel = org.gradle.api.logging.LogLevel.INFO; 34 | break; 35 | case LIFECYCLE: 36 | logLevel = org.gradle.api.logging.LogLevel.LIFECYCLE; 37 | break; 38 | case WARN: 39 | logLevel = org.gradle.api.logging.LogLevel.WARN; 40 | break; 41 | case QUIET: 42 | logLevel = org.gradle.api.logging.LogLevel.QUIET; 43 | break; 44 | default: 45 | logLevel = org.gradle.api.logging.LogLevel.ERROR; 46 | break; 47 | } 48 | return logger.isEnabled(logLevel); 49 | } 50 | 51 | @Override 52 | public void debug(String message) { 53 | logger.debug(message); 54 | } 55 | 56 | @Override 57 | public void debug(String message, Object... objects) { 58 | logger.debug(message, objects); 59 | } 60 | 61 | @Override 62 | public void debug(String message, Throwable throwable) { 63 | logger.debug(message, throwable); 64 | } 65 | 66 | @Override 67 | public void info(String message) { 68 | logger.info(message); 69 | } 70 | 71 | @Override 72 | public void info(String message, Object... objects) { 73 | logger.info(message, objects); 74 | } 75 | 76 | @Override 77 | public void info(String message, Throwable throwable) { 78 | logger.info(message, throwable); 79 | } 80 | 81 | @Override 82 | public void lifecycle(String message) { 83 | logger.lifecycle(message); 84 | } 85 | 86 | @Override 87 | public void lifecycle(String message, Object... objects) { 88 | logger.lifecycle(message, objects); 89 | } 90 | 91 | @Override 92 | public void lifecycle(String message, Throwable throwable) { 93 | logger.lifecycle(message, throwable); 94 | } 95 | 96 | @Override 97 | public void warn(String message) { 98 | logger.warn(message); 99 | } 100 | 101 | @Override 102 | public void warn(String message, Object... objects) { 103 | logger.warn(message, objects); 104 | } 105 | 106 | @Override 107 | public void warn(String message, Throwable throwable) { 108 | logger.warn(message, throwable); 109 | } 110 | 111 | @Override 112 | public void quiet(String message) { 113 | logger.quiet(message); 114 | } 115 | 116 | @Override 117 | public void quiet(String message, Object... objects) { 118 | logger.quiet(message, objects); 119 | } 120 | 121 | @Override 122 | public void quiet(String message, Throwable throwable) { 123 | logger.quiet(message, throwable); 124 | } 125 | 126 | @Override 127 | public void error(String message) { 128 | logger.error(message); 129 | } 130 | 131 | @Override 132 | public void error(String message, Object... objects) { 133 | logger.error(message, objects); 134 | } 135 | 136 | @Override 137 | public void error(String message, Throwable throwable) { 138 | logger.error(message, throwable); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/cache/FileManager.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.cache; 2 | 3 | import com.android.build.api.transform.Format; 4 | import com.android.build.api.transform.QualifiedContent; 5 | import com.android.build.api.transform.TransformInvocation; 6 | import org.apache.commons.io.FileUtils; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.util.Collections; 11 | 12 | public class FileManager { 13 | private final File variantRoot; 14 | private TransformInvocation invocation; 15 | 16 | public FileManager(File variantRoot) { 17 | this.variantRoot = variantRoot; 18 | } 19 | 20 | public void attachContext(TransformInvocation invocation) { 21 | this.invocation = invocation; 22 | } 23 | 24 | public OutputProviderFactory.RootSelector asSelector() { 25 | return (type, id) -> { 26 | switch (type) { 27 | case CLASS: 28 | return classRootFor(id); 29 | case TEMP: 30 | return tempRootFor(id); 31 | case CACHE: 32 | return cacheRootFor(id); 33 | } 34 | throw new AssertionError(); 35 | }; 36 | } 37 | 38 | public File variantRoot() { 39 | return variantRoot; 40 | } 41 | 42 | private File classRootFor(String id) { 43 | return invocation.getOutputProvider().getContentLocation("capt-generated-by:" + id, 44 | Collections.singleton(QualifiedContent.DefaultContentType.CLASSES), 45 | Collections.singleton(QualifiedContent.Scope.EXTERNAL_LIBRARIES), 46 | Format.DIRECTORY); 47 | } 48 | 49 | private File tempRootFor(String id) { 50 | return new File(invocation.getContext().getTemporaryDir(), id); 51 | } 52 | 53 | 54 | private File cacheRootFor(String id) { 55 | return new File(variantRoot, "plugins" + File.separator + id); 56 | } 57 | 58 | public void clearForFullMode() throws IOException { 59 | invocation.getOutputProvider().deleteAll(); 60 | FileUtils.deleteDirectory(variantRoot); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/cache/InternalCache.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.cache; 2 | 3 | import coffeepartner.capt.plugin.api.util.RelativeDirectoryProvider; 4 | import coffeepartner.capt.plugin.resource.GlobalResource; 5 | import coffeepartner.capt.plugin.util.Constants; 6 | import coffeepartner.capt.plugin.util.WaitableTasks; 7 | import com.android.build.api.transform.TransformException; 8 | import com.google.common.io.Closeables; 9 | import okio.BufferedSink; 10 | import okio.BufferedSource; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStreamReader; 16 | import java.io.OutputStreamWriter; 17 | import java.lang.reflect.ParameterizedType; 18 | import java.util.concurrent.Callable; 19 | import java.util.function.Consumer; 20 | import java.util.function.Supplier; 21 | 22 | public class InternalCache { 23 | 24 | private static final Logger LOGGER = LoggerFactory.getLogger(InternalCache.class); 25 | 26 | private final GlobalResource global; 27 | private final RelativeDirectoryProvider provider; 28 | private final WaitableTasks waitableTasks; 29 | 30 | public InternalCache(RelativeDirectoryProvider provider, GlobalResource global) { 31 | this.provider = provider; 32 | this.global = global; 33 | this.waitableTasks = WaitableTasks.get(global.io()); 34 | } 35 | 36 | public void loadSync(Consumer consumer) throws IOException, TransformException { 37 | try { 38 | new SingleReadTask<>(consumer).call(); 39 | } catch (Exception e) { 40 | if (e instanceof IOException) { 41 | throw (IOException) e; 42 | } else if (e instanceof RuntimeException) { 43 | throw (RuntimeException) e; 44 | } else { 45 | throw new TransformException(e); 46 | } 47 | } 48 | } 49 | 50 | public void loadAsync(Consumer consumer) { 51 | waitableTasks.submit(new SingleReadTask<>(consumer)); 52 | } 53 | 54 | public void storeAsync(Supplier supplier) { 55 | waitableTasks.submit(new SingleWriteTask<>(supplier)); 56 | } 57 | 58 | public void await() throws IOException, InterruptedException, TransformException { 59 | waitableTasks.await(); 60 | } 61 | 62 | @SuppressWarnings("unchecked") 63 | private static Class getTargetType(Object t) { 64 | return (Class) ((ParameterizedType) t.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0]; 65 | } 66 | 67 | class SingleReadTask implements Callable { 68 | 69 | private final Consumer consumer; 70 | 71 | SingleReadTask(Consumer consumer) { 72 | this.consumer = consumer; 73 | } 74 | 75 | @Override 76 | public Void call() throws Exception { 77 | Class targetType = getTargetType(consumer); 78 | String fileName = targetType.getSimpleName() + ".json"; 79 | BufferedSource bs = null; 80 | 81 | try { 82 | bs = provider.asSource(fileName); 83 | consumer.accept(global.gson().fromJson(new InputStreamReader(bs.inputStream(), Constants.UTF8), targetType)); 84 | } catch (IOException | RuntimeException e) { 85 | LOGGER.error("Read failed for {}", fileName); 86 | throw e; 87 | } finally { 88 | Closeables.close(bs, true); 89 | } 90 | return null; 91 | } 92 | } 93 | 94 | class SingleWriteTask implements Callable { 95 | private final Supplier supplier; 96 | 97 | SingleWriteTask(Supplier supplier) { 98 | this.supplier = supplier; 99 | } 100 | 101 | @Override 102 | public Void call() throws Exception { 103 | String fileName = getTargetType(supplier).getSimpleName() + ".json"; 104 | BufferedSink bs = null; 105 | try { 106 | bs = provider.asSink(fileName); 107 | OutputStreamWriter os = new OutputStreamWriter(bs.outputStream(), Constants.UTF8); 108 | global.gson().toJson(supplier.get(), os); 109 | os.flush(); 110 | } catch (IOException | RuntimeException e) { 111 | LOGGER.error("Write failed for {}", fileName); 112 | throw e; 113 | } finally { 114 | Closeables.close(bs, true); 115 | } 116 | return null; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/cache/OutputProviderFactory.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.cache; 2 | 3 | import coffeepartner.capt.plugin.api.OutputProvider; 4 | import coffeepartner.capt.plugin.api.util.RelativeDirectoryProvider; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | 9 | public class OutputProviderFactory { 10 | 11 | private final RelativeDirectoryProviderFactory provider; 12 | private final RootSelector selector; 13 | 14 | public OutputProviderFactory(RelativeDirectoryProviderFactory provider, RootSelector selector) { 15 | this.provider = provider; 16 | this.selector = selector; 17 | } 18 | 19 | public OutputProvider newProvider(String id) { 20 | return new OutputProviderImpl(id); 21 | } 22 | 23 | public interface RootSelector { 24 | File select(OutputProvider.Type type, String id); 25 | } 26 | 27 | 28 | class OutputProviderImpl implements OutputProvider { 29 | 30 | private final String id; 31 | 32 | OutputProviderImpl(String id) { 33 | this.id = id; 34 | } 35 | 36 | @Override 37 | public RelativeDirectoryProvider getProvider(Type type) { 38 | File root = selector.select(type, id); 39 | return provider.newProvider(root); 40 | } 41 | 42 | @Override 43 | public void deleteAll() throws IOException { 44 | for (Type type : Type.values()) { 45 | getProvider(type).deleteAll(); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/cache/RelativeDirectoryProviderFactory.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.cache; 2 | 3 | import coffeepartner.capt.plugin.api.util.RelativeDirectoryProvider; 4 | 5 | import java.io.File; 6 | 7 | public interface RelativeDirectoryProviderFactory { 8 | 9 | RelativeDirectoryProvider newProvider(File root); 10 | } 11 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/cache/RelativeDirectoryProviderFactoryImpl.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.cache; 2 | 3 | import coffeepartner.capt.plugin.api.util.RelativeDirectoryProvider; 4 | import coffeepartner.capt.plugin.util.Util; 5 | import com.google.common.io.Files; 6 | import okio.BufferedSink; 7 | import okio.BufferedSource; 8 | import okio.Okio; 9 | import org.apache.commons.io.FileUtils; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | 14 | public class RelativeDirectoryProviderFactoryImpl implements RelativeDirectoryProviderFactory { 15 | @Override 16 | public RelativeDirectoryProvider newProvider(File root) { 17 | return new RelativeDirectoryProviderImpl(root); 18 | } 19 | 20 | static class RelativeDirectoryProviderImpl implements RelativeDirectoryProvider { 21 | 22 | private final File root; 23 | 24 | RelativeDirectoryProviderImpl(File root) { 25 | this.root = root; 26 | } 27 | 28 | @Override 29 | public File root() { 30 | return root; 31 | } 32 | 33 | @Override 34 | public BufferedSource asSource(String path) throws IOException { 35 | File target = create(path); 36 | Files.createParentDirs(target); 37 | return Okio.buffer(Okio.source(target)); 38 | } 39 | 40 | @Override 41 | public void deleteIfExists(String path) throws IOException { 42 | Util.deleteIFExists(create(path)); 43 | } 44 | 45 | @Override 46 | public BufferedSink asSink(String path) throws IOException { 47 | File target = create(path); 48 | Files.createParentDirs(target); 49 | return Okio.buffer(Okio.sink(target)); 50 | } 51 | 52 | private File create(String path) { 53 | return new File(root, path.replace('/', File.separatorChar)); 54 | } 55 | 56 | @Override 57 | public void deleteAll() throws IOException { 58 | FileUtils.deleteDirectory(root()); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/dsl/CaptPluginExtension.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.dsl; 2 | 3 | import groovy.lang.GroovyObjectSupport; 4 | import org.gradle.api.Named; 5 | 6 | import java.util.Map; 7 | 8 | public class CaptPluginExtension extends GroovyObjectSupport implements Named { 9 | 10 | public static final int ANDROID_TEST = 1; 11 | public static final int ASSEMBLE = 2; 12 | 13 | private final String name; 14 | private Integer priority = null; 15 | private int scope = ANDROID_TEST | ASSEMBLE; 16 | private ConfigurableMap configurableMap = new ConfigurableMap(); 17 | 18 | public CaptPluginExtension(String name) { 19 | this.name = name; 20 | } 21 | 22 | @Override 23 | public Object invokeMethod(String name, Object args) { 24 | return configurableMap.invokeMethod(name, args); 25 | } 26 | 27 | @Override 28 | public void setProperty(String property, Object newValue) { 29 | configurableMap.setProperty(property, newValue); 30 | } 31 | 32 | @Override 33 | public Object getProperty(String property) { 34 | return configurableMap.getProperty(property); 35 | } 36 | 37 | public Integer getPriority() { 38 | return priority; 39 | } 40 | 41 | public void setPriority(int priority) { 42 | this.priority = priority; 43 | } 44 | 45 | public void priority(int priority) { 46 | setPriority(priority); 47 | } 48 | 49 | public void scope(int scope) { 50 | this.scope = scope; 51 | } 52 | 53 | public void setScope(int scope) { 54 | this.scope = scope; 55 | } 56 | 57 | public int getScope() { 58 | return scope; 59 | } 60 | 61 | public Map getPluginProperties() { 62 | return configurableMap.toRealMap(); 63 | } 64 | 65 | @Override 66 | public String getName() { 67 | return name; 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "CaptPluginExtension{" + 73 | "name='" + name + '\'' + 74 | ", priority=" + priority + 75 | ", configurableMap=" + configurableMap + 76 | '}'; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/dsl/ConfigurableMap.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.dsl; 2 | 3 | import groovy.lang.Closure; 4 | import groovy.lang.GroovyObjectSupport; 5 | import org.gradle.api.Action; 6 | import org.gradle.util.ConfigureUtil; 7 | 8 | import java.lang.reflect.Array; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public class ConfigurableMap extends GroovyObjectSupport { 13 | 14 | private Map properties = new HashMap<>(); 15 | 16 | @Override 17 | public Object invokeMethod(String name, Object args) { 18 | int length = Array.getLength(args); 19 | if (length != 1) { 20 | throw new UnsupportedOperationException("number of argument is illegal: " + length); 21 | } 22 | setProperty(name, Array.get(args, 0)); 23 | return null; 24 | } 25 | 26 | @SuppressWarnings("unchecked") 27 | @Override 28 | public void setProperty(String property, Object newValue) { 29 | Action action; 30 | if (newValue instanceof Closure) { 31 | action = ConfigureUtil.configureUsing((Closure) newValue); 32 | } else if (newValue instanceof Action) { 33 | action = (Action) newValue; 34 | } else { 35 | properties.put(property, newValue); 36 | return; 37 | } 38 | 39 | Object oldValue = properties.get(property); 40 | if (oldValue instanceof ConfigurableMap) { 41 | action.execute((ConfigurableMap) oldValue); 42 | } else { 43 | ConfigurableMap map = new ConfigurableMap(); 44 | action.execute(map); 45 | properties.put(property, map); 46 | } 47 | } 48 | 49 | @Override 50 | public Object getProperty(String property) { 51 | return properties.get(property); 52 | } 53 | 54 | public Map toRealMap() { 55 | Map map = new HashMap<>(properties.size()); 56 | for (Map.Entry entry : properties.entrySet()) { 57 | if (entry.getValue() instanceof ConfigurableMap) { 58 | map.put(entry.getKey(), ((ConfigurableMap) entry.getValue()).toRealMap()); 59 | } else { 60 | map.put(entry.getKey(), entry.getValue()); 61 | } 62 | } 63 | return map; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | return "ConfigurableMap{" + 69 | "properties=" + properties + 70 | '}'; 71 | } 72 | 73 | 74 | } 75 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/graph/ApkClassGraph.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.graph; 2 | 3 | import coffeepartner.capt.plugin.api.graph.ClassGraph; 4 | import coffeepartner.capt.plugin.api.graph.Status; 5 | import coffeepartner.capt.plugin.resource.VariantResource; 6 | import coffeepartner.capt.plugin.util.ConcurrentHashSet; 7 | import coffeepartner.capt.plugin.util.Constants; 8 | import org.gradle.api.logging.Logger; 9 | import org.gradle.api.logging.Logging; 10 | import org.objectweb.asm.Opcodes; 11 | 12 | import javax.annotation.Nullable; 13 | import java.util.*; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | import java.util.function.Consumer; 16 | import java.util.function.Supplier; 17 | import java.util.stream.Collectors; 18 | 19 | public class ApkClassGraph implements ClassGraph { 20 | 21 | private static final Logger LOGGER = Logging.getLogger(ApkClassGraph.class); 22 | 23 | private final Map classes = new ConcurrentHashMap<>(Constants.OPT_SIZE); 24 | private final VariantResource variantResource; 25 | private final boolean throwIfDuplicated; 26 | private Set removedJars = new ConcurrentHashSet<>(); 27 | 28 | public ApkClassGraph(VariantResource variantResource, boolean throwIfDuplicated) { 29 | this.variantResource = variantResource; 30 | this.throwIfDuplicated = throwIfDuplicated; 31 | } 32 | 33 | public Map getAll() { 34 | return classes; 35 | } 36 | 37 | @SuppressWarnings("Convert2Lambda") 38 | public Consumer readClasses() { 39 | // Don't use lambda here, or you will lose generic info 40 | return new Consumer() { 41 | @Override 42 | public void accept(Classes classes) { 43 | classes.classes.parallelStream() 44 | .forEach(c -> ApkClassGraph.this.add(c, Status.NOT_CHANGED)); 45 | } 46 | }; 47 | } 48 | 49 | @SuppressWarnings("Convert2Lambda") 50 | public Supplier writeClasses() { 51 | return new Supplier() { 52 | @Override 53 | public Classes get() { 54 | return new Classes(classes.values().parallelStream() 55 | .filter(ApkClassInfo::exists) 56 | .map(n -> n.clazz) 57 | .collect(Collectors.toCollection(() -> new ArrayList<>(classes.size() >> 3)))); 58 | } 59 | }; 60 | } 61 | 62 | 63 | public void onJarRemoved(String name) { 64 | removedJars.add(name); 65 | } 66 | 67 | 68 | public void markRemovedClassesAndBuildGraph() { 69 | Set removed = new HashSet<>(removedJars); 70 | this.getAll().values() 71 | .forEach(n -> { 72 | if (n.status() == Status.NOT_CHANGED && removed.contains(n.clazz.belongsTo)) { 73 | // make sure mark remove succeed, so pass null is OK 74 | n.markRemoved(throwIfDuplicated, null); 75 | } else if (n.exists()) { // exists means parent is not null. 76 | ApkClassInfo parent = n.parent; 77 | if (parent.classChildren == Collections.emptyList()) { 78 | // optimize for Object, I guess 1/32 classes directly extend Object 79 | if (parent.name().equals("java/lang/Object")) { 80 | parent.classChildren = new ArrayList<>(getAll().size() >> 6); 81 | } else { 82 | parent.classChildren = new ArrayList<>(); 83 | } 84 | } 85 | parent.classChildren.add(n); 86 | 87 | n.interfaces.forEach(i -> { 88 | if ((n.access() & Opcodes.ACC_INTERFACE) != 0) { 89 | if (i.interfaceChildren == Collections.emptyList()) { 90 | i.interfaceChildren = new ArrayList<>(); 91 | } 92 | i.interfaceChildren.add(n); 93 | } else { 94 | if (i.implementedClasses == Collections.emptyList()) { 95 | i.implementedClasses = new ArrayList<>(); 96 | } 97 | i.implementedClasses.add(n); 98 | } 99 | }); 100 | } 101 | }); 102 | } 103 | 104 | public void add(ClassBean clazz, Status status) { 105 | ApkClassInfo node = getOrCreate(clazz.name, (clazz.access & Opcodes.ACC_INTERFACE) != 0); 106 | node.update(clazz, status, throwIfDuplicated); 107 | if (clazz.superName != null) { 108 | node.parent = getOrCreate(clazz.superName, false); 109 | 110 | } 111 | if (!clazz.interfaces.isEmpty()) { 112 | node.interfaces = clazz.interfaces.stream().map(n -> getOrCreate(n, true)).collect(Collectors.toList()); 113 | } 114 | } 115 | 116 | private ApkClassInfo getOrCreate(String name, boolean isInterface) { 117 | return classes.computeIfAbsent(name, n -> ApkClassInfo.createStub(variantResource, name, isInterface)); 118 | } 119 | 120 | @Nullable 121 | @Override 122 | public ApkClassInfo get(String name) { 123 | return classes.get(name); 124 | } 125 | 126 | public void markRemoved(String className, String belongsTo) { 127 | classes.get(className).markRemoved(throwIfDuplicated, belongsTo); 128 | } 129 | 130 | public void clear() { 131 | classes.clear(); 132 | } 133 | 134 | 135 | public static class Classes { 136 | public List classes; 137 | 138 | public Classes(List classes) { 139 | this.classes = classes; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/graph/ApkClassInfo.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.graph; 2 | 3 | import coffeepartner.capt.plugin.api.graph.ClassInfo; 4 | import coffeepartner.capt.plugin.api.graph.MethodInfo; 5 | import coffeepartner.capt.plugin.api.graph.Status; 6 | import coffeepartner.capt.plugin.resource.VariantResource; 7 | import org.gradle.api.logging.Logger; 8 | import org.gradle.api.logging.Logging; 9 | 10 | import javax.annotation.Nullable; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.util.Collections; 14 | import java.util.List; 15 | import java.util.concurrent.atomic.AtomicReference; 16 | 17 | public class ApkClassInfo implements ClassInfo { 18 | 19 | private static final Logger LOGGER = Logging.getLogger(ApkClassInfo.class); 20 | 21 | public AtomicReference status; 22 | public ClassBean clazz; 23 | 24 | /** 25 | * null means removed or not exists. 26 | */ 27 | @Nullable 28 | public ApkClassInfo parent; 29 | public List interfaces = Collections.emptyList(); 30 | 31 | // rev direction 32 | public List classChildren = Collections.emptyList(); 33 | public List interfaceChildren = Collections.emptyList(); 34 | public List implementedClasses = Collections.emptyList(); 35 | 36 | private final VariantResource resource; 37 | 38 | public static ApkClassInfo createStub(VariantResource resource, String name, boolean isInterface) { 39 | return new ApkClassInfo(resource, new ClassBean(name, isInterface)); 40 | } 41 | 42 | private ApkClassInfo(VariantResource resource, ClassBean clazz) { 43 | this.resource = resource; 44 | this.clazz = clazz; 45 | this.status = new AtomicReference<>(Status.NOT_EXISTS); 46 | } 47 | 48 | public void markRemoved(boolean throwIfDuplicated, String belongsTo) { 49 | if (updateStatus(Status.REMOVED)) { 50 | if (status.get() == Status.REMOVED) { 51 | parent = null; 52 | interfaces = classChildren = interfaceChildren = implementedClasses = Collections.emptyList(); 53 | } 54 | } else { 55 | throwOrLog(throwIfDuplicated, belongsTo); 56 | } 57 | } 58 | 59 | void update(ClassBean bean, Status newStatus, boolean throwIfDuplicated) { 60 | if (updateStatus(newStatus)) { 61 | this.clazz = bean; 62 | } else { 63 | throwOrLog(throwIfDuplicated, bean.belongsTo); 64 | } 65 | } 66 | 67 | private void throwOrLog(boolean throwIfDuplicated, String belongsTo) { 68 | if (throwIfDuplicated) { 69 | throw new IllegalStateException("Found duplicated class: " + clazz.name + "in '" + clazz.belongsTo + "' and ' " + belongsTo + "'"); 70 | } 71 | LOGGER.error("Found duplicated class: {} in '{}' and '{}'", clazz.name, clazz.belongsTo, belongsTo); 72 | } 73 | 74 | private boolean updateStatus(Status newStatus) { 75 | // for not exists in incremental mode, we change the new status to ADDED 76 | if (newStatus == Status.CHANGED && status.get() == Status.NOT_EXISTS) { 77 | newStatus = Status.ADDED; 78 | } 79 | Status oldStatus = this.status.getAndSet(newStatus); 80 | if (oldStatus != Status.NOT_EXISTS && oldStatus != Status.NOT_CHANGED) { 81 | if (oldStatus == Status.ADDED && newStatus == Status.REMOVED 82 | || oldStatus == Status.REMOVED && newStatus == Status.ADDED) { 83 | return status.getAndSet(Status.CHANGED) == newStatus; 84 | } 85 | return false; 86 | } 87 | return true; 88 | } 89 | 90 | 91 | @Override 92 | public Status status() { 93 | Status res = status.get(); 94 | if (!resource.isIncremental()) { 95 | return res == Status.NOT_EXISTS || res == Status.REMOVED ? Status.NOT_EXISTS 96 | : Status.NOT_CHANGED; 97 | } 98 | return res; 99 | } 100 | 101 | @Override 102 | public int access() { 103 | return clazz.access; 104 | } 105 | 106 | @Override 107 | public String name() { 108 | return clazz.name; 109 | } 110 | 111 | @Override 112 | public Class loadClass() throws ClassNotFoundException { 113 | return resource.loadApkClass(name()); 114 | } 115 | 116 | @Override 117 | public InputStream openStream() throws IOException { 118 | return resource.openStream(name()); 119 | } 120 | 121 | @Nullable 122 | @Override 123 | public String signature() { 124 | return clazz.signature; 125 | } 126 | 127 | @Nullable 128 | @Override 129 | public ClassInfo parent() { 130 | return parent; 131 | } 132 | 133 | @Override 134 | public List methods() { 135 | return clazz.methods; 136 | } 137 | 138 | @Override 139 | public List interfaces() { 140 | return interfaces; 141 | } 142 | 143 | @Override 144 | public List classChildren() { 145 | return classChildren; 146 | } 147 | 148 | @Override 149 | public List interfaceChildren() { 150 | return interfaceChildren; 151 | } 152 | 153 | @Override 154 | public List implementedClasses() { 155 | return implementedClasses; 156 | } 157 | 158 | @Override 159 | public boolean exists() { 160 | Status s = status.get(); 161 | return s != Status.NOT_EXISTS && s != Status.REMOVED; 162 | } 163 | 164 | @Override 165 | public String toString() { 166 | return "ApkClassInfo{" + 167 | "status=" + status.get() + 168 | ", clazz=" + clazz + 169 | '}'; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/graph/ClassBean.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.graph; 2 | 3 | 4 | import org.objectweb.asm.Opcodes; 5 | 6 | import javax.annotation.Nullable; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class ClassBean { 13 | 14 | public String belongsTo; 15 | 16 | public int access; 17 | public String name; 18 | @Nullable 19 | public String signature; 20 | @Nullable 21 | public String superName; 22 | public List interfaces; 23 | 24 | // remove field because useless till now. 25 | //public List fields = new ArrayList<>(); 26 | 27 | public List methods = Collections.emptyList(); 28 | 29 | 30 | public ClassBean(String name, boolean isInterface) { 31 | this(isInterface ? Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT : 0, name, null, null, null); 32 | } 33 | 34 | 35 | public ClassBean(int access, String name, @Nullable String signature, @Nullable String superName, @Nullable String[] interfaces) { 36 | this.access = access; 37 | this.name = name; 38 | this.signature = signature; 39 | this.superName = superName; 40 | this.interfaces = interfaces == null ? Collections.emptyList() : Arrays.asList(interfaces); 41 | this.methods = Collections.emptyList(); 42 | } 43 | 44 | public void addMethod(MethodBean methodBean) { 45 | if (methods == Collections.emptyList()) { 46 | methods = new ArrayList<>(); 47 | } 48 | methods.add(methodBean); 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return "ClassBean{" + 54 | "access=" + access + 55 | ", name='" + name + '\'' + 56 | ", signature='" + signature + '\'' + 57 | ", superName='" + superName + '\'' + 58 | ", interfaces=" + interfaces + 59 | ", methods=" + methods + 60 | '}'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/graph/MethodBean.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.graph; 2 | 3 | import coffeepartner.capt.plugin.api.graph.MethodInfo; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | /** 8 | * Ignore exceptions, too large. 9 | */ 10 | public class MethodBean implements MethodInfo { 11 | 12 | public int access; 13 | public String name; 14 | public String desc; 15 | @Nullable 16 | public String signature; 17 | 18 | public MethodBean(int access, String name, String desc, @Nullable String signature) { 19 | this.access = access; 20 | this.name = name; 21 | this.desc = desc; 22 | this.signature = signature; 23 | } 24 | 25 | @Override 26 | public int access() { 27 | return access; 28 | } 29 | 30 | @Override 31 | public String name() { 32 | return name; 33 | } 34 | 35 | @Override 36 | public String desc() { 37 | return desc; 38 | } 39 | 40 | @Nullable 41 | @Override 42 | public String signature() { 43 | return signature; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return "MethodBean{" + 49 | "access=" + access + 50 | ", name='" + name + '\'' + 51 | ", desc='" + desc + '\'' + 52 | ", signature='" + signature + '\'' + 53 | '}'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/process/CommonArgs.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.process; 2 | 3 | import coffeepartner.capt.plugin.api.Arguments; 4 | import coffeepartner.capt.plugin.api.Plugin; 5 | import coffeepartner.capt.plugin.dsl.CaptPluginExtension; 6 | import coffeepartner.capt.plugin.GradleCaptExtension; 7 | 8 | import java.util.Map; 9 | import java.util.function.Function; 10 | import java.util.stream.Collectors; 11 | 12 | public class CommonArgs { 13 | 14 | private Map map; 15 | 16 | CommonArgs(Map map) { 17 | this.map = map; 18 | } 19 | 20 | public static CommonArgs createBy(GradleCaptExtension extension, int scope, Map plugins) { 21 | return new CommonArgs(extension.getPlugins().stream() 22 | .map(e -> { 23 | Plugin plugin = plugins.get(e.getName()); 24 | if (plugin == null) { 25 | return null; 26 | } 27 | return new SimpleArgs(e, plugins.get(e.getName()).defaultPriority()); 28 | }) 29 | .filter(a -> { 30 | if (a != null) { 31 | if (a.allow(scope)) { 32 | return true; 33 | } 34 | plugins.remove(a.name()); 35 | } 36 | return false; 37 | }) 38 | .collect(Collectors.toMap(SimpleArgs::name, Function.identity())) 39 | ); 40 | } 41 | 42 | public Arguments asArgumentsFor(String pluginId) { 43 | Arguments.PluginArguments my = map.get(pluginId); 44 | return new Arguments() { 45 | @Override 46 | public PluginArguments getArgumentsById(String id) { 47 | return map.get(id); 48 | } 49 | 50 | @Override 51 | public PluginArguments getMyArguments() { 52 | return my; 53 | } 54 | }; 55 | } 56 | 57 | 58 | static class SimpleArgs implements Arguments.PluginArguments { 59 | 60 | private final int priority; 61 | private final Map map; 62 | private final String name; 63 | private final int scope; 64 | 65 | SimpleArgs(CaptPluginExtension extension, int defaultPriority) { 66 | this.scope = extension.getScope(); 67 | this.name = extension.getName(); 68 | this.priority = (extension.getPriority() == null ? defaultPriority : extension.getPriority()); 69 | this.map = extension.getPluginProperties(); 70 | } 71 | 72 | boolean allow(int scope) { 73 | return (this.scope & scope) != 0; 74 | } 75 | 76 | public String name() { 77 | return name; 78 | } 79 | 80 | @Override 81 | public int priority() { 82 | return priority; 83 | } 84 | 85 | @Override 86 | public Map arguments() { 87 | return map; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/process/PluginBean.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.process; 2 | 3 | import java.util.Set; 4 | 5 | public class PluginBean { 6 | 7 | private String id; 8 | 9 | private Set affectedClasses; 10 | 11 | public PluginBean(String id, Set affectedClasses) { 12 | this.id = id; 13 | this.affectedClasses = affectedClasses; 14 | } 15 | 16 | public String getId() { 17 | return id; 18 | } 19 | 20 | public Set getAffectedClasses() { 21 | return affectedClasses; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return "PluginBean{" + 27 | "id='" + id + '\'' + 28 | ", affectedClasses=" + affectedClasses + 29 | '}'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/process/plugin/ForwardingCapt.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.process.plugin; 2 | 3 | import com.android.build.gradle.BaseExtension; 4 | import coffeepartner.capt.plugin.api.Arguments; 5 | import coffeepartner.capt.plugin.api.Context; 6 | import coffeepartner.capt.plugin.api.CaptInternal; 7 | import coffeepartner.capt.plugin.api.OutputProvider; 8 | import coffeepartner.capt.plugin.api.graph.ClassGraph; 9 | import org.gradle.api.Project; 10 | 11 | import java.net.URLClassLoader; 12 | 13 | public class ForwardingCapt implements CaptInternal { 14 | 15 | private final CaptInternal delegate; 16 | 17 | public ForwardingCapt(CaptInternal delegate) { 18 | this.delegate = delegate; 19 | } 20 | 21 | @Override 22 | public Project getProject() { 23 | return delegate.getProject(); 24 | } 25 | 26 | @Override 27 | public BaseExtension getAndroid() { 28 | return delegate.getAndroid(); 29 | } 30 | 31 | @Override 32 | public URLClassLoader captLoader() { 33 | return delegate.captLoader(); 34 | } 35 | 36 | @Override 37 | public boolean isIncremental() { 38 | return delegate.isIncremental(); 39 | } 40 | 41 | @Override 42 | public Context getContext() { 43 | return delegate.getContext(); 44 | } 45 | 46 | @Override 47 | public ClassGraph getClassGraph() { 48 | return delegate.getClassGraph(); 49 | } 50 | 51 | @Override 52 | public Arguments getArgs() { 53 | return delegate.getArgs(); 54 | } 55 | 56 | @Override 57 | public OutputProvider getOutputs() { 58 | return delegate.getOutputs(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/process/plugin/GlobalCapt.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.process.plugin; 2 | 3 | import com.android.build.gradle.BaseExtension; 4 | import coffeepartner.capt.plugin.api.Arguments; 5 | import coffeepartner.capt.plugin.api.Context; 6 | import coffeepartner.capt.plugin.api.CaptInternal; 7 | import coffeepartner.capt.plugin.api.OutputProvider; 8 | import coffeepartner.capt.plugin.api.graph.ClassGraph; 9 | import coffeepartner.capt.plugin.api.log.Logger; 10 | import coffeepartner.capt.plugin.api.logger.LoggerFactory; 11 | import coffeepartner.capt.plugin.resource.GlobalResource; 12 | import coffeepartner.capt.plugin.resource.VariantResource; 13 | import org.gradle.api.Project; 14 | 15 | import java.net.URLClassLoader; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.ForkJoinPool; 18 | 19 | public class GlobalCapt implements CaptInternal, Context { 20 | 21 | private final ClassGraph classGraph; 22 | private final GlobalResource global; 23 | private final VariantResource variantResource; 24 | 25 | public GlobalCapt(ClassGraph classGraph, GlobalResource global, VariantResource variantResource) { 26 | this.classGraph = classGraph; 27 | this.global = global; 28 | this.variantResource = variantResource; 29 | } 30 | 31 | @Override 32 | public Project getProject() { 33 | return global.project(); 34 | } 35 | 36 | @Override 37 | public BaseExtension getAndroid() { 38 | return global.android(); 39 | } 40 | 41 | @Override 42 | public URLClassLoader captLoader() { 43 | return variantResource.loader(); 44 | } 45 | 46 | @Override 47 | public boolean isIncremental() { 48 | throw new UnsupportedOperationException(); 49 | } 50 | 51 | @Override 52 | public Context getContext() { 53 | return this; 54 | } 55 | 56 | @Override 57 | public ClassGraph getClassGraph() { 58 | return classGraph; 59 | } 60 | 61 | @Override 62 | public Arguments getArgs() { 63 | throw new UnsupportedOperationException(); 64 | } 65 | 66 | @Override 67 | public OutputProvider getOutputs() { 68 | throw new UnsupportedOperationException(); 69 | } 70 | 71 | @Override 72 | public String getVariantName() { 73 | return variantResource.variant(); 74 | } 75 | 76 | @Override 77 | public Logger getLogger(Class clazz) { 78 | return LoggerFactory.getLogger(clazz); 79 | } 80 | 81 | @Override 82 | public ForkJoinPool getComputation() { 83 | return global.computation(); 84 | } 85 | 86 | @Override 87 | public ExecutorService getIo() { 88 | return global.io(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/process/plugin/PluginWrapper.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.process.plugin; 2 | 3 | import coffeepartner.capt.plugin.api.Arguments; 4 | import coffeepartner.capt.plugin.api.CaptInternal; 5 | import coffeepartner.capt.plugin.api.OutputProvider; 6 | import coffeepartner.capt.plugin.api.Plugin; 7 | import coffeepartner.capt.plugin.api.asm.CaptClassVisitor; 8 | import coffeepartner.capt.plugin.api.graph.ClassInfo; 9 | import coffeepartner.capt.plugin.api.process.AnnotationProcessor; 10 | import coffeepartner.capt.plugin.api.transform.ClassRequest; 11 | import coffeepartner.capt.plugin.api.transform.ClassTransformer; 12 | import coffeepartner.capt.plugin.process.PluginBean; 13 | import coffeepartner.capt.plugin.process.visitors.AnnotationClassDispatcher; 14 | import coffeepartner.capt.plugin.process.visitors.ThirdRound; 15 | import coffeepartner.capt.plugin.resource.VariantResource; 16 | import coffeepartner.capt.plugin.util.ConcurrentHashSet; 17 | import com.google.common.collect.Sets; 18 | 19 | import javax.annotation.Nullable; 20 | import java.io.IOException; 21 | import java.util.Collections; 22 | import java.util.Set; 23 | 24 | @SuppressWarnings("unchecked") 25 | public class PluginWrapper extends ForwardingCapt { 26 | 27 | private final boolean incremental; 28 | private final Plugin plugin; 29 | private final Arguments args; 30 | private final String id; 31 | private final VariantResource resource; 32 | private final Set affected = new ConcurrentHashSet<>(); 33 | private Set preAffected = Collections.emptySet(); 34 | 35 | public PluginWrapper(boolean incremental, Plugin plugin, 36 | Arguments args, 37 | String id, VariantResource resource, CaptInternal delegate) { 38 | super(delegate); 39 | this.incremental = incremental; 40 | this.plugin = plugin; 41 | this.args = args; 42 | this.id = id; 43 | this.resource = resource; 44 | } 45 | 46 | public String id() { 47 | return id; 48 | } 49 | 50 | public void callBeforeCreate() { 51 | plugin.beforeCreate(this); 52 | } 53 | 54 | public void callOnCreate() throws IOException, InterruptedException { 55 | plugin.onCreate(this); 56 | } 57 | 58 | public void callOnDestroy() throws IOException, InterruptedException { 59 | plugin.onDestroy(this); 60 | } 61 | 62 | public Set getSupportedAnnotations() { 63 | return plugin.getSupportedAnnotations(); 64 | } 65 | 66 | @Nullable 67 | public ThirdRound.TransformProvider newTransformProvider() { 68 | ClassTransformer transformer = new ClassTransformWrapper(plugin.onTransformClass()); 69 | return new ThirdRound.TransformProvider() { 70 | 71 | @Override 72 | public void onClassAffected(String className) { 73 | affected.add(className); 74 | } 75 | 76 | @Override 77 | public ClassTransformer transformer() { 78 | return transformer; 79 | } 80 | }; 81 | } 82 | 83 | @Nullable 84 | public AnnotationClassDispatcher.AnnotationProcessorProvider newAnnotationProvider() { 85 | AnnotationProcessor processor = plugin.onProcessAnnotations(); 86 | if (processor != null) { 87 | return new AnnotationClassDispatcher.AnnotationProcessorProvider() { 88 | Set supported = plugin.getSupportedAnnotations(); 89 | 90 | @Override 91 | public Set supports() { 92 | return supported; 93 | } 94 | 95 | @Override 96 | public AnnotationProcessor processor() { 97 | return processor; 98 | } 99 | }; 100 | } 101 | return null; 102 | } 103 | 104 | @Override 105 | public boolean isIncremental() { 106 | return incremental; 107 | } 108 | 109 | @Override 110 | public Arguments getArgs() { 111 | return args; 112 | } 113 | 114 | @Override 115 | public OutputProvider getOutputs() { 116 | return resource.provider(id); 117 | } 118 | 119 | public PluginBean toBean() { 120 | return new PluginBean(id, Sets.union(affected, preAffected)); 121 | } 122 | 123 | public void combinePre(PluginBean pre) { 124 | preAffected = pre.getAffectedClasses(); 125 | } 126 | 127 | private static final ClassTransformer NOOP = new ClassTransformer() { 128 | @Override 129 | public ClassRequest beforeTransform() { 130 | return new ClassRequest() { 131 | }; 132 | } 133 | 134 | @Nullable 135 | @Override 136 | public CaptClassVisitor onTransform(ClassInfo classInfo, boolean required) { 137 | return null; 138 | } 139 | }; 140 | 141 | class ClassTransformWrapper extends ClassTransformer { 142 | private final ClassTransformer classTransformer; 143 | 144 | ClassTransformWrapper(@Nullable ClassTransformer classTransformer) { 145 | this.classTransformer = classTransformer == null ? NOOP : classTransformer; 146 | } 147 | 148 | @Override 149 | public ClassRequest beforeTransform() { 150 | return classTransformer.beforeTransform(); 151 | } 152 | 153 | @Nullable 154 | @Override 155 | public CaptClassVisitor onTransform(ClassInfo classInfo, boolean required) { 156 | preAffected.remove(classInfo.name()); 157 | return classTransformer.onTransform(classInfo, required); 158 | } 159 | 160 | @Override 161 | public void afterTransform() throws IOException, InterruptedException { 162 | classTransformer.afterTransform(); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/process/visitors/FirstRound.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.process.visitors; 2 | 3 | import coffeepartner.capt.plugin.graph.ApkClassGraph; 4 | import coffeepartner.capt.plugin.graph.ClassBean; 5 | import coffeepartner.capt.plugin.graph.MethodBean; 6 | import coffeepartner.capt.plugin.util.ClassWalker; 7 | import coffeepartner.capt.plugin.util.asm.AnnotationSniffer; 8 | import com.android.build.api.transform.JarInput; 9 | import com.android.build.api.transform.Status; 10 | import com.android.build.api.transform.TransformException; 11 | import org.gradle.api.logging.Logger; 12 | import org.gradle.api.logging.Logging; 13 | import org.objectweb.asm.ClassReader; 14 | import org.objectweb.asm.MethodVisitor; 15 | 16 | import javax.annotation.Nullable; 17 | import java.io.IOException; 18 | import java.util.Set; 19 | import java.util.concurrent.ForkJoinPool; 20 | import java.util.concurrent.ForkJoinTask; 21 | 22 | public class FirstRound { 23 | 24 | private static final Logger LOGGER = Logging.getLogger(FirstRound.class); 25 | 26 | private final ApkClassGraph graph; 27 | 28 | public FirstRound(ApkClassGraph graph) { 29 | this.graph = graph; 30 | } 31 | 32 | public void accept(ClassWalker walker, boolean incremental, AnnotationCollector collector) throws IOException, InterruptedException, TransformException { 33 | if (!incremental) { 34 | graph.clear(); 35 | } 36 | walker.visit(false, incremental, false, asFactory(collector)); 37 | } 38 | 39 | private ClassWalker.Visitor.Factory asFactory(AnnotationCollector collector) { 40 | return (incremental, content) -> { 41 | if (incremental && content instanceof JarInput) { 42 | JarInput j = (JarInput) content; 43 | if (j.getStatus() == Status.REMOVED) { 44 | graph.onJarRemoved(j.getName()); 45 | return null; 46 | } else if (j.getStatus() == Status.CHANGED) { 47 | graph.onJarRemoved(j.getName()); 48 | } 49 | } 50 | return new NamedVisitor(content.getName(), collector); 51 | }; 52 | } 53 | 54 | public interface AnnotationCollector { 55 | void onAnnotation(String className, Set annotations); 56 | } 57 | 58 | class NamedVisitor implements ClassWalker.Visitor { 59 | 60 | private final String name; 61 | private final AnnotationCollector collector; 62 | 63 | NamedVisitor(String name, AnnotationCollector collector) { 64 | this.name = name; 65 | this.collector = collector; 66 | } 67 | 68 | @SuppressWarnings("unchecked") 69 | @Nullable 70 | @Override 71 | public ForkJoinTask onVisit(ForkJoinPool pool, @Nullable byte[] classBytes, String className, Status status) { 72 | if (status == Status.REMOVED) { 73 | graph.markRemoved(className, name); 74 | return null; 75 | } 76 | return pool.submit(() -> { 77 | try { 78 | ClassReader reader = new ClassReader(classBytes); 79 | reader.accept(new FirstRoundVisitor(className, status, name, collector), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); 80 | } catch (RuntimeException e) { // class maybe illegal, we catch and ignore it. 81 | LOGGER.warn("Class '" + className + "' in " + name + " parse failed, skip it", e); 82 | } 83 | return null; 84 | }); 85 | } 86 | } 87 | 88 | class FirstRoundVisitor extends AnnotationSniffer { 89 | 90 | private final String expectedName; 91 | private final Status status; 92 | private final String belongsTo; 93 | private final AnnotationCollector collector; 94 | private ClassBean bean; 95 | 96 | FirstRoundVisitor(String expectedName, Status status, String belongsTo, AnnotationCollector collector) { 97 | super(null); 98 | this.expectedName = expectedName; 99 | this.status = status; 100 | this.belongsTo = belongsTo; 101 | this.collector = collector; 102 | } 103 | 104 | @Override 105 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 106 | if (!expectedName.equals(name)) { 107 | throw new IllegalArgumentException("Class name '" + name + "' is not the same as expected '" + expectedName + "' in" + belongsTo); 108 | } 109 | bean = new ClassBean(access, name, signature, superName, interfaces); 110 | bean.belongsTo = belongsTo; 111 | } 112 | 113 | @Override 114 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 115 | bean.addMethod(new MethodBean(access, name, descriptor, signature)); 116 | return super.visitMethod(access, name, descriptor, signature, exceptions); 117 | } 118 | 119 | @Override 120 | public void visitEnd() { 121 | graph.add(bean, mapStatus(status)); 122 | collector.onAnnotation(expectedName, annotations()); 123 | } 124 | } 125 | 126 | static coffeepartner.capt.plugin.api.graph.Status mapStatus(Status status) { 127 | switch (status) { 128 | case ADDED: 129 | return coffeepartner.capt.plugin.api.graph.Status.ADDED; 130 | case CHANGED: 131 | return coffeepartner.capt.plugin.api.graph.Status.CHANGED; 132 | case REMOVED: 133 | return coffeepartner.capt.plugin.api.graph.Status.REMOVED; 134 | default: 135 | return coffeepartner.capt.plugin.api.graph.Status.NOT_CHANGED; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/resource/GlobalResource.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.resource; 2 | 3 | import com.android.build.gradle.BaseExtension; 4 | import coffeepartner.capt.plugin.GradleCaptExtension; 5 | import com.google.gson.Gson; 6 | import org.gradle.api.Project; 7 | 8 | import java.io.File; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.ForkJoinPool; 11 | 12 | public class GlobalResource { 13 | 14 | private final Project project; 15 | private final File root; 16 | private final ForkJoinPool computation; 17 | private final ExecutorService executor; 18 | private final Gson gson; 19 | 20 | private final GradleCaptExtension gradleCaptExtension; 21 | private final BaseExtension extension; 22 | 23 | public GlobalResource(Project project, 24 | File root, 25 | ForkJoinPool computation, 26 | ExecutorService executor, 27 | Gson gson, 28 | GradleCaptExtension gradleCaptExtension, 29 | BaseExtension extension) { 30 | this.project = project; 31 | this.root = root; 32 | this.computation = computation; 33 | this.executor = executor; 34 | this.gson = gson; 35 | this.gradleCaptExtension = gradleCaptExtension; 36 | this.extension = extension; 37 | } 38 | 39 | public Project project() { 40 | return project; 41 | } 42 | 43 | public ExecutorService io() { 44 | return executor; 45 | } 46 | 47 | public ForkJoinPool computation() { 48 | return computation; 49 | } 50 | 51 | public Gson gson() { 52 | return gson; 53 | } 54 | 55 | public File root() { 56 | return root; 57 | } 58 | 59 | public GradleCaptExtension gradleCaptExtension() { 60 | return gradleCaptExtension; 61 | } 62 | 63 | public BaseExtension android() { 64 | return extension; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/resource/PluginResource.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.resource; 2 | 3 | public class PluginResource { 4 | } 5 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/resource/TransformResource.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.resource; 2 | 3 | public class TransformResource { 4 | } 5 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/resource/VariantResource.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.resource; 2 | 3 | import coffeepartner.capt.plugin.api.OutputProvider; 4 | import coffeepartner.capt.plugin.api.Plugin; 5 | import coffeepartner.capt.plugin.cache.FileManager; 6 | import coffeepartner.capt.plugin.cache.OutputProviderFactory; 7 | import coffeepartner.capt.plugin.util.Constants; 8 | import com.android.build.api.transform.QualifiedContent; 9 | import com.android.build.api.transform.TransformInvocation; 10 | import com.google.common.collect.Streams; 11 | import org.gradle.api.artifacts.Configuration; 12 | import org.gradle.api.logging.Logger; 13 | import org.gradle.api.logging.Logging; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.net.MalformedURLException; 19 | import java.net.URL; 20 | import java.net.URLClassLoader; 21 | import java.net.URLConnection; 22 | import java.util.List; 23 | import java.util.stream.Stream; 24 | 25 | public class VariantResource implements Constants { 26 | 27 | private static final Logger LOGGER = Logging.getLogger(Loader.class); 28 | 29 | private final Loader loader = new Loader(); 30 | private final String variant; 31 | private final FileManager files; 32 | private final OutputProviderFactory factory; 33 | private boolean incremental; 34 | 35 | public VariantResource(String variant, FileManager files, OutputProviderFactory factory) { 36 | this.variant = variant; 37 | this.files = files; 38 | this.factory = factory; 39 | } 40 | 41 | public String variant() { 42 | return variant; 43 | } 44 | 45 | public void init(TransformInvocation invocation, List bootClasspath, Configuration target) { 46 | this.loader.initClassLoader(invocation, bootClasspath, target); 47 | this.files.attachContext(invocation); 48 | } 49 | 50 | public void setIncremental(boolean incremental) throws IOException { 51 | this.incremental = incremental; 52 | if (!incremental) { 53 | files.clearForFullMode(); 54 | } 55 | } 56 | 57 | public InputStream openStream(String className) throws IOException { 58 | URL is = loader.runtimeLoader.getResource(className + ".class"); 59 | if (is == null) { 60 | throw new IOException("open class failed: " + className); 61 | } 62 | URLConnection connection = is.openConnection(); 63 | connection.setUseCaches(false); // ignore jar entry cache, or else you will die 64 | return connection.getInputStream(); 65 | } 66 | 67 | public Class loadApkClass(String className) throws ClassNotFoundException { 68 | return loader.runtimeLoader.loadClass(className); 69 | } 70 | 71 | public URLClassLoader getFullAndroidLoader() { 72 | return loader.fullAndroidLoader; 73 | } 74 | 75 | public Class loadPluginClass(String className) throws ClassNotFoundException { 76 | return loader.runnerLoader.loadClass(className); 77 | } 78 | 79 | public URL loadPluginOnCapt(String pluginName) { 80 | return loader.loadPluginOnCapt(pluginName); 81 | } 82 | 83 | public boolean isIncremental() { 84 | return incremental; 85 | } 86 | 87 | public OutputProvider provider(String id) { 88 | return factory.newProvider(id); 89 | } 90 | 91 | public URLClassLoader loader() { 92 | return loader.runnerLoader; 93 | } 94 | 95 | static class Loader { 96 | 97 | private URLClassLoader runnerLoader; 98 | private URLClassLoader runtimeLoader; 99 | private URLClassLoader fullAndroidLoader; 100 | 101 | void initClassLoader(TransformInvocation invocation, List bootClasspath, Configuration target) { 102 | 103 | URL[] runnerUrls = target.resolve().stream() 104 | .map(f -> { 105 | try { 106 | return f.toURI().toURL(); 107 | } catch (MalformedURLException e) { 108 | throw new AssertionError(e); 109 | } 110 | }).toArray(URL[]::new); 111 | URLClassLoader captDependencies = new URLClassLoader(runnerUrls, Plugin.class.getClassLoader()); 112 | 113 | URL[] runtimeUrls = invocation.getInputs().stream() 114 | .flatMap(s -> Stream.concat(s.getDirectoryInputs().stream(), s.getJarInputs().stream())) 115 | .map(QualifiedContent::getFile) 116 | .map(f -> { 117 | try { 118 | return f.toURI().toURL(); 119 | } catch (MalformedURLException e) { 120 | throw new AssertionError(e); 121 | } 122 | }) 123 | .toArray(URL[]::new); 124 | 125 | URL[] fullAndroidUrls = Streams.concat( 126 | Streams.concat(invocation.getInputs().stream(), 127 | invocation.getReferencedInputs().stream()) 128 | .flatMap(s -> Stream.concat(s.getDirectoryInputs().stream(), s.getJarInputs().stream())) 129 | .map(QualifiedContent::getFile), 130 | bootClasspath.stream()) 131 | .map(f -> { 132 | try { 133 | return f.toURI().toURL(); 134 | } catch (MalformedURLException e) { 135 | throw new AssertionError(e); 136 | } 137 | }).toArray(URL[]::new); 138 | 139 | // runner doesn't have runtime 140 | this.runnerLoader = captDependencies; 141 | this.runtimeLoader = new URLClassLoader(runtimeUrls); 142 | this.fullAndroidLoader = new URLClassLoader(fullAndroidUrls); 143 | } 144 | 145 | public URL loadPluginOnCapt(String pluginName) { 146 | return runnerLoader.getResource(PLUGIN_PATH + pluginName + ".properties"); 147 | } 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/util/CaptThreadFactory.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.util; 2 | 3 | 4 | import java.util.concurrent.ThreadFactory; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | public class CaptThreadFactory implements ThreadFactory { 8 | private final AtomicInteger threadNumber = new AtomicInteger(1); 9 | private final String namePrefix; 10 | 11 | public CaptThreadFactory() { 12 | namePrefix = "capt-io-thread-"; 13 | } 14 | 15 | @Override 16 | public Thread newThread(Runnable r) { 17 | Thread thread = new Thread(r, 18 | namePrefix + threadNumber.getAndIncrement()); 19 | thread.setPriority(Thread.NORM_PRIORITY - 1); // io lower than normal 20 | 21 | if (thread.isDaemon()) { 22 | thread.setDaemon(false); 23 | } 24 | return thread; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/util/ConcurrentHashSet.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.util; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.*; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.function.Consumer; 8 | import java.util.function.Predicate; 9 | import java.util.stream.Stream; 10 | 11 | public class ConcurrentHashSet extends AbstractSet implements Set { 12 | 13 | private final ConcurrentHashMap> map; 14 | 15 | public ConcurrentHashSet() { 16 | map = new ConcurrentHashMap<>(); 17 | } 18 | 19 | public ConcurrentHashSet(int initialCapacity) { 20 | map = new ConcurrentHashMap<>(initialCapacity); 21 | } 22 | 23 | public ConcurrentHashSet(int initialCapacity, float loadFactor) { 24 | map = new ConcurrentHashMap<>(initialCapacity, loadFactor); 25 | } 26 | 27 | public ConcurrentHashSet(Collection c) { 28 | map = new ConcurrentHashMap<>(); 29 | addAll(c); 30 | } 31 | 32 | @Override 33 | public int size() { 34 | return map.size(); 35 | } 36 | 37 | @Override 38 | public boolean isEmpty() { 39 | return map.isEmpty(); 40 | } 41 | 42 | @Override 43 | public boolean contains(Object o) { 44 | return map.containsKey(o); 45 | } 46 | 47 | @NotNull 48 | @Override 49 | public Iterator iterator() { 50 | return map.keySet().iterator(); 51 | } 52 | 53 | @Override 54 | public void forEach(Consumer action) { 55 | map.keySet().forEach(action); 56 | } 57 | 58 | @NotNull 59 | @Override 60 | public Object[] toArray() { 61 | return map.keySet().toArray(); 62 | } 63 | 64 | @NotNull 65 | @Override 66 | public T[] toArray(@NotNull T[] a) { 67 | return map.keySet().toArray(a); 68 | } 69 | 70 | @Override 71 | public boolean add(E e) { 72 | return map.put(e, this) == null; 73 | } 74 | 75 | @Override 76 | public boolean remove(Object o) { 77 | return map.remove(o) != null; 78 | } 79 | 80 | @Override 81 | public boolean containsAll(@NotNull Collection c) { 82 | return map.keySet().containsAll(c); 83 | } 84 | 85 | @Override 86 | public boolean addAll(@NotNull Collection c) { 87 | return map.keySet(this).addAll(c); 88 | } 89 | 90 | @Override 91 | public boolean retainAll(@NotNull Collection c) { 92 | return map.keySet().retainAll(c); 93 | } 94 | 95 | @Override 96 | public boolean removeAll(@NotNull Collection c) { 97 | return map.keySet().removeAll(c); 98 | } 99 | 100 | @Override 101 | public void clear() { 102 | map.clear(); 103 | } 104 | 105 | @Override 106 | public boolean removeIf(Predicate filter) { 107 | return map.keySet().removeIf(filter); 108 | } 109 | 110 | @Override 111 | public Spliterator spliterator() { 112 | return map.keySet().spliterator(); 113 | } 114 | 115 | @Override 116 | public Stream stream() { 117 | return map.keySet().stream(); 118 | } 119 | 120 | @Override 121 | public Stream parallelStream() { 122 | return map.keySet().parallelStream(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/util/Constants.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.util; 2 | 3 | 4 | import java.nio.charset.Charset; 5 | 6 | public interface Constants { 7 | 8 | int OPT_SIZE = 8192; 9 | 10 | String NAME = "capt"; 11 | 12 | String CAPITALIZED_NAME = "Capt"; 13 | 14 | String PLUGIN_PATH = "META-INF/capt-plugins/"; 15 | 16 | String ANDROID_TEST = "AndroidTest"; 17 | 18 | String TEST = "test"; 19 | 20 | String PLUGIN_KEY = "implementation-class"; 21 | 22 | Charset UTF8 = Charset.forName("utf-8"); 23 | } 24 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/util/Functions.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.util; 2 | 3 | import java.util.concurrent.atomic.AtomicReference; 4 | import java.util.function.Supplier; 5 | 6 | public class Functions { 7 | 8 | private Functions() { 9 | 10 | } 11 | 12 | public static Supplier cache(Supplier supplier) { 13 | return new LazySupplier<>(supplier); 14 | } 15 | 16 | 17 | static class LazySupplier implements Supplier { 18 | 19 | private static Object holder = LazySupplier.class; 20 | private final AtomicReference> ref; 21 | private T t = (T) holder; 22 | 23 | LazySupplier(Supplier supplier) { 24 | this.ref = new AtomicReference<>(supplier); 25 | } 26 | 27 | @Override 28 | public T get() { 29 | while (t == holder) { 30 | Supplier supplier = ref.getAndSet(null); 31 | if (supplier != null) { 32 | t = supplier.get(); 33 | } 34 | } 35 | return t; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/util/Util.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.util; 2 | 3 | import com.android.build.api.transform.TransformException; 4 | import org.objectweb.asm.tree.ClassNode; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.util.concurrent.ExecutionException; 9 | import java.util.concurrent.Future; 10 | 11 | public class Util { 12 | 13 | private Util() { 14 | } 15 | 16 | public static T await(Future future) throws IOException, TransformException, InterruptedException { 17 | try { 18 | return future.get(); 19 | } catch (ExecutionException e) { 20 | if (e.getCause() instanceof IOException) { 21 | throw (IOException) e.getCause(); 22 | } else if (e.getCause() instanceof RuntimeException) { 23 | throw (RuntimeException) e.getCause(); 24 | } else if (e.getCause() instanceof Error) { 25 | throw (Error) e.getCause(); 26 | } 27 | throw new TransformException(e.getCause()); 28 | } 29 | } 30 | 31 | // Ljava/lang/Object; to java/lang/Object 32 | public static String objDescToInternalName(String desc) { 33 | return desc.substring(1, desc.length() - 1); 34 | } 35 | 36 | public static ClassNode clone(ClassNode node) { 37 | ClassNode cloned = new ClassNode(); 38 | node.accept(cloned); 39 | return cloned; 40 | } 41 | 42 | public static void deleteIFExists(File target) throws IOException { 43 | if (target.exists() && !target.delete()) { 44 | throw new IOException("delete file " + target + " failed"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/util/WaitableTasks.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.util; 2 | 3 | import com.android.build.api.transform.TransformException; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.Callable; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.Future; 11 | 12 | public class WaitableTasks { 13 | 14 | private final ExecutorService executor; 15 | private final List> futures = new ArrayList<>(); 16 | 17 | public static WaitableTasks get(ExecutorService executor) { 18 | return new WaitableTasks(executor); 19 | } 20 | 21 | public WaitableTasks(ExecutorService executor) { 22 | this.executor = executor; 23 | } 24 | 25 | public void execute(Runnable runnable) { 26 | futures.add(executor.submit(runnable)); 27 | } 28 | 29 | public Future submit(Callable callable) { 30 | Future future = executor.submit(callable); 31 | futures.add(future); 32 | return future; 33 | } 34 | 35 | public void await() throws IOException, InterruptedException, TransformException { 36 | for (Future future : futures) { 37 | Util.await(future); 38 | } 39 | futures.clear(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/util/asm/AnnotationSniffer.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.util.asm; 2 | 3 | import coffeepartner.capt.plugin.util.Util; 4 | import org.objectweb.asm.*; 5 | 6 | import java.util.HashSet; 7 | import java.util.Set; 8 | 9 | public class AnnotationSniffer extends ClassVisitor { 10 | 11 | private Set annotations = new HashSet<>(); 12 | 13 | public AnnotationSniffer(ClassVisitor next) { 14 | super(Opcodes.ASM5, next); 15 | } 16 | 17 | @Override 18 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { 19 | onAnnotationFound(descriptor, visible); 20 | return super.visitAnnotation(descriptor, visible); 21 | } 22 | 23 | protected void onAnnotationFound(String descriptor, boolean visible) { 24 | annotations.add(Util.objDescToInternalName(descriptor)); 25 | } 26 | 27 | @Override 28 | public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { 29 | return new FieldVisitor(Opcodes.ASM5, super.visitField(access, name, descriptor, signature, value)) { 30 | @Override 31 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { 32 | onAnnotationFound(descriptor, visible); 33 | return super.visitAnnotation(descriptor, visible); 34 | } 35 | }; 36 | } 37 | 38 | public Set annotations() { 39 | return annotations; 40 | } 41 | 42 | @Override 43 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 44 | return new MethodVisitor(Opcodes.ASM5, super.visitMethod(access, name, descriptor, signature, exceptions)) { 45 | @Override 46 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { 47 | onAnnotationFound(descriptor, visible); 48 | return super.visitAnnotation(descriptor, visible); 49 | } 50 | 51 | @Override 52 | public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { 53 | onAnnotationFound(descriptor, visible); 54 | return super.visitParameterAnnotation(parameter, descriptor, visible); 55 | } 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/variant/VariantManager.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.variant; 2 | 3 | import com.android.build.api.attributes.BuildTypeAttr; 4 | import com.android.build.api.attributes.ProductFlavorAttr; 5 | import com.android.build.gradle.AppExtension; 6 | import com.android.build.gradle.BaseExtension; 7 | import com.android.build.gradle.LibraryExtension; 8 | import com.android.build.gradle.api.ApplicationVariant; 9 | import com.android.build.gradle.api.BaseVariant; 10 | import com.android.build.gradle.api.LibraryVariant; 11 | import com.android.build.gradle.api.TestVariant; 12 | import com.android.build.gradle.internal.pipeline.TransformTask; 13 | import com.android.builder.model.SourceProvider; 14 | import coffeepartner.capt.plugin.resource.GlobalResource; 15 | import coffeepartner.capt.plugin.util.Constants; 16 | import org.gradle.api.DomainObjectCollection; 17 | import org.gradle.api.Project; 18 | import org.gradle.api.artifacts.Configuration; 19 | import org.gradle.api.artifacts.ConfigurationContainer; 20 | import org.gradle.api.artifacts.ResolutionStrategy; 21 | import org.gradle.api.artifacts.type.ArtifactTypeDefinition; 22 | import org.gradle.api.attributes.Attribute; 23 | import org.gradle.api.attributes.AttributeContainer; 24 | import org.gradle.api.attributes.Usage; 25 | import org.gradle.api.logging.Logger; 26 | import org.gradle.api.logging.Logging; 27 | import org.gradle.api.tasks.SourceSet; 28 | 29 | import java.util.HashMap; 30 | import java.util.Map; 31 | import java.util.Objects; 32 | 33 | /** 34 | * Cross multi variant tasks. 35 | */ 36 | public class VariantManager implements Constants { 37 | 38 | private static final Logger LOGGER = Logging.getLogger(VariantManager.class); 39 | private static final Attribute ARTIFACT_TYPE = Attribute.of("artifactType", String.class); 40 | 41 | private final Map dependencies = new HashMap<>(); 42 | private VariantScope.Factory factory; 43 | 44 | private final GlobalResource global; 45 | private final BaseExtension extension; 46 | private final Project project; 47 | 48 | public VariantManager(GlobalResource global, BaseExtension extension, Project project) { 49 | this.global = global; 50 | this.extension = extension; 51 | this.project = project; 52 | this.factory = new VariantDependenciesFactory(); 53 | } 54 | 55 | public boolean isApplication() { 56 | return extension instanceof AppExtension; 57 | } 58 | 59 | public void createConfigurationForVariant() { 60 | ConfigurationContainer configurations = project.getConfigurations(); 61 | extension.getSourceSets().all(androidSourceSet -> { 62 | if (!androidSourceSet.getName().startsWith(TEST)) { // don't support unit test 63 | Configuration configuration = configurations.maybeCreate(sourceSetToConfigurationName(androidSourceSet.getName())); 64 | // internal use, will be extended by the actual variant configuration 65 | configuration.setDescription("Classpath for the capt plugin for " + androidSourceSet.getName() + "."); 66 | configuration.setVisible(false); 67 | configuration.setCanBeConsumed(false); 68 | configuration.setCanBeResolved(false); 69 | } 70 | }); 71 | 72 | DomainObjectCollection collection = extension instanceof AppExtension ? 73 | ((AppExtension) extension).getApplicationVariants() 74 | : ((LibraryExtension) extension).getLibraryVariants(); 75 | 76 | collection.all(v -> { 77 | VariantScope variant = factory.create(v); 78 | dependencies.put(v.getName(), variant); 79 | 80 | TestVariant t; 81 | if (v instanceof ApplicationVariant) { 82 | t = ((ApplicationVariant) v).getTestVariant(); 83 | } else { 84 | t = ((LibraryVariant) v).getTestVariant(); 85 | } 86 | 87 | if (t != null) { 88 | VariantScope testVariant = factory.create(t, variant); 89 | dependencies.put(t.getName(), testVariant); 90 | } 91 | }); 92 | 93 | 94 | // The fucking transform API doesn't provide evaluating time variant! 95 | project.afterEvaluate(p -> p.getTasks() 96 | .withType(TransformTask.class, t -> { 97 | if (t.getTransform().getName().equals(NAME)) { 98 | t.dependsOn(getByVariant(t.getVariantName())); 99 | 100 | // register output 101 | t.getOutputs().dir(getVariantScope(t.getVariantName()).getRoot()); 102 | } 103 | })); 104 | } 105 | 106 | private Configuration getByVariant(String name) { 107 | return project.getConfigurations().maybeCreate(name + "CaptClasspath"); 108 | } 109 | 110 | public VariantScope getVariantScope(String name) { 111 | return Objects.requireNonNull(dependencies.get(name)); 112 | } 113 | 114 | private static String sourceSetToConfigurationName(String name) { 115 | if (SourceSet.MAIN_SOURCE_SET_NAME.equals(name)) { 116 | return NAME; 117 | } 118 | return name + CAPITALIZED_NAME; 119 | } 120 | 121 | private class VariantDependenciesFactory implements VariantScope.Factory { 122 | 123 | 124 | @Override 125 | public VariantScope create(BaseVariant v) { 126 | ConfigurationContainer configurations = project.getConfigurations(); 127 | 128 | // the actual configuration 129 | Configuration configuration = getByVariant(v.getName()); 130 | 131 | // attributes match 132 | AttributeContainer attributes = configuration.getAttributes(); 133 | attributes 134 | .attribute(ARTIFACT_TYPE, ArtifactTypeDefinition.JAR_TYPE) 135 | .attribute(BuildTypeAttr.ATTRIBUTE, project.getObjects().named(BuildTypeAttr.class, v.getBuildType().getName())) 136 | .attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_RUNTIME)); 137 | v.getProductFlavors().forEach(p -> attributes.attribute(Attribute.of(p.getDimension(), ProductFlavorAttr.class), 138 | project.getObjects().named(ProductFlavorAttr.class, p.getName()))); 139 | 140 | configuration.setDescription("Resolved configuration for capt for variant: " + v.getName()); 141 | configuration.setVisible(false); 142 | configuration.setCanBeConsumed(false); 143 | configuration.getResolutionStrategy().sortArtifacts(ResolutionStrategy.SortOrder.CONSUMER_FIRST); 144 | 145 | v.getSourceSets().stream() 146 | .map(SourceProvider::getName) 147 | .map(VariantManager::sourceSetToConfigurationName) 148 | .map(configurations::getByName) 149 | .forEach(configuration::extendsFrom); 150 | 151 | return new VariantScope(v.getName(), configuration, global); 152 | } 153 | 154 | @Override 155 | public VariantScope create(BaseVariant v, VariantScope parent) { 156 | VariantScope child = create(v); 157 | // TODO: should androidTest variant extendsFrom normal variant ? 158 | child.getCaptConfiguration().extendsFrom(parent.getCaptConfiguration()); 159 | return child; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /capt-plugin/src/main/java/coffeepartner/capt/plugin/variant/VariantScope.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.plugin.variant; 2 | 3 | import com.android.build.api.transform.TransformException; 4 | import com.android.build.api.transform.TransformInvocation; 5 | import com.android.build.gradle.api.BaseVariant; 6 | import coffeepartner.capt.plugin.cache.*; 7 | import coffeepartner.capt.plugin.dsl.CaptPluginExtension; 8 | import coffeepartner.capt.plugin.graph.ApkClassGraph; 9 | import coffeepartner.capt.plugin.process.PluginManager; 10 | import coffeepartner.capt.plugin.process.plugin.GlobalCapt; 11 | import coffeepartner.capt.plugin.process.visitors.AnnotationClassDispatcher; 12 | import coffeepartner.capt.plugin.process.visitors.FirstRound; 13 | import coffeepartner.capt.plugin.process.visitors.ThirdRound; 14 | import coffeepartner.capt.plugin.resource.GlobalResource; 15 | import coffeepartner.capt.plugin.resource.VariantResource; 16 | import coffeepartner.capt.plugin.util.ClassWalker; 17 | import coffeepartner.capt.plugin.util.Constants; 18 | import org.gradle.api.artifacts.Configuration; 19 | import org.gradle.api.logging.Logger; 20 | import org.gradle.api.logging.Logging; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | 25 | 26 | public class VariantScope implements Constants { 27 | private static final Logger LOGGER = Logging.getLogger(VariantScope.class); 28 | 29 | private final String variant; 30 | private Configuration captConfiguration; 31 | private final GlobalResource global; 32 | private final FileManager files; 33 | 34 | VariantScope(String variant, Configuration captConfiguration, GlobalResource global) { 35 | this.variant = variant; 36 | this.captConfiguration = captConfiguration; 37 | this.global = global; 38 | this.files = new FileManager(new File(global.root(), getVariant())); 39 | } 40 | 41 | public Configuration getCaptConfiguration() { 42 | return captConfiguration; 43 | } 44 | 45 | public File getRoot() { 46 | return files.variantRoot(); 47 | } 48 | 49 | public String getVariant() { 50 | return variant; 51 | } 52 | 53 | public void doTransform(TransformInvocation invocation) throws IOException, TransformException, InterruptedException { 54 | 55 | long pre = System.currentTimeMillis(); 56 | long cur; 57 | // load and prepare 58 | ClassWalker walker = new ClassWalker(global, invocation); 59 | AnnotationClassDispatcher annotationClassDispatcher = new AnnotationClassDispatcher(global); 60 | 61 | 62 | RelativeDirectoryProviderFactory singleFactory = new RelativeDirectoryProviderFactoryImpl(); 63 | OutputProviderFactory factory = new OutputProviderFactory(singleFactory, files.asSelector()); 64 | VariantResource variantResource = new VariantResource(getVariant(), 65 | files, factory); 66 | variantResource.init(invocation, global.android().getBootClasspath(), getCaptConfiguration()); 67 | InternalCache internalCache = new InternalCache(singleFactory.newProvider(new File(files.variantRoot(), "core")) 68 | , global); 69 | ApkClassGraph graph = new ApkClassGraph(variantResource, global.gradleCaptExtension().getThrowIfDuplicated()); 70 | GlobalCapt capt = new GlobalCapt(graph, global, variantResource); 71 | 72 | PluginManager manager = new PluginManager(global, variantResource, invocation); 73 | 74 | if (invocation.isIncremental()) { 75 | internalCache.loadSync(manager.readPrePlugins()); 76 | } 77 | 78 | int scope = variant.endsWith(ANDROID_TEST) ? CaptPluginExtension.ANDROID_TEST : CaptPluginExtension.ASSEMBLE; 79 | boolean incremental = manager.initPlugins(global.gradleCaptExtension(), scope, capt); 80 | variantResource.setIncremental(incremental); 81 | 82 | if (incremental) { 83 | internalCache.loadAsync(graph.readClasses()); 84 | internalCache.loadAsync(annotationClassDispatcher.readPreMatched()); 85 | internalCache.await(); 86 | } 87 | 88 | cur = System.currentTimeMillis(); 89 | LOGGER.lifecycle("Prepare, cost: {}ms", (cur - pre)); 90 | pre = cur; 91 | 92 | // Round 1: make class graph & collect metas 93 | new FirstRound(graph) 94 | .accept(walker, 95 | incremental, 96 | annotationClassDispatcher.toCollector(manager.getAllSupportedAnnotations())); 97 | graph.markRemovedClassesAndBuildGraph(); 98 | 99 | cur = System.currentTimeMillis(); 100 | LOGGER.lifecycle("Build class graph, cost: {}ms", (cur - pre)); 101 | pre = cur; 102 | 103 | // everything ready, call plugin lifecycle 104 | manager.callCreate(); 105 | 106 | cur = System.currentTimeMillis(); 107 | LOGGER.lifecycle("Call plugins create, cost: {}ms", (cur - pre)); 108 | pre = cur; 109 | 110 | // Round 2: visit Metas 111 | annotationClassDispatcher.dispatch( 112 | manager.hasPluginRemoved(), 113 | incremental, 114 | graph, 115 | variantResource, 116 | manager.forAnnotation()); 117 | 118 | cur = System.currentTimeMillis(); 119 | LOGGER.lifecycle("Dispatch annotations, cost: {}ms", (cur - pre)); 120 | pre = cur; 121 | 122 | // Round 3: transform classes 123 | new ThirdRound(variantResource, global, graph) 124 | .accept(incremental, 125 | walker, 126 | manager.forThird(), 127 | invocation); 128 | 129 | cur = System.currentTimeMillis(); 130 | LOGGER.lifecycle("Transform classes, cost: {}ms", (cur - pre)); 131 | pre = cur; 132 | // transform done, store cache 133 | internalCache.storeAsync(graph.writeClasses()); 134 | internalCache.storeAsync(manager.writePlugins()); 135 | internalCache.storeAsync(annotationClassDispatcher.writeMatched()); 136 | 137 | manager.callDestroy(); // call destroy after store to save time 138 | internalCache.await(); 139 | 140 | cur = System.currentTimeMillis(); 141 | LOGGER.lifecycle("Store cache and call plugins destroy, cost: {}ms", (cur - pre)); 142 | } 143 | 144 | public interface Factory { 145 | 146 | VariantScope create(BaseVariant v); 147 | 148 | VariantScope create(BaseVariant v, VariantScope parent); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Apr 10 15:23:45 CST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip 7 | 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /publish.gradle: -------------------------------------------------------------------------------- 1 | subprojects { pro -> 2 | 3 | if (!pro.projectDir.name.startsWith("capt-")) return 4 | 5 | version = capt_version 6 | group = capt_group 7 | 8 | apply plugin: 'maven' 9 | apply plugin: 'com.jfrog.bintray' 10 | afterEvaluate { 11 | task sourcesJar(type: Jar) { 12 | from sourceSets.main.java.srcDirs 13 | classifier = 'sources' 14 | } 15 | 16 | task javadocJar(type: Jar, dependsOn: javadoc) { 17 | classifier = 'javadoc' 18 | from javadoc.destinationDir 19 | } 20 | 21 | artifacts { 22 | archives sourcesJar 23 | archives javadocJar 24 | } 25 | tasks.install { 26 | repositories.mavenInstaller { 27 | pom { 28 | project { 29 | packaging 'jar' 30 | licenses { 31 | license { 32 | name 'The Apache Software License, Version 2.0' 33 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 34 | } 35 | } 36 | developers { 37 | developer { 38 | id 'dieyi' 39 | name '耿万鹏' 40 | email 'dieyidezui@gmail.com' 41 | } 42 | 43 | developer { 44 | id 'bxbxbai' 45 | name '白雪斌' 46 | email 'bxbxbai@gmail.com' 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | bintray { 55 | user = System.getenv("BINTRAY_USER") 56 | key = System.getenv("BINTRAY_KEY") 57 | configurations = ['archives'] 58 | pkg { 59 | repo = "maven" 60 | name = "capt-$pro.name" 61 | websiteUrl = "https://github.com/CoffeePartner/capt" 62 | vcsUrl = "https://github.com/CoffeePartner/capt.git" 63 | licenses = ["Apache-2.0"] 64 | publish = true 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /samples/SafeCatcher/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild -------------------------------------------------------------------------------- /samples/SafeCatcher/README.md: -------------------------------------------------------------------------------- 1 | ## SafeCatcher 2 | 3 | This example shows how to write a simple capt plugin. 4 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/build.gradle: -------------------------------------------------------------------------------- 1 | import coffeepartner.capt.plugin.dsl.CaptPluginExtension 2 | 3 | apply plugin: 'capt' 4 | apply plugin: 'com.android.application' 5 | 6 | android { 7 | compileSdkVersion 28 8 | defaultConfig { 9 | applicationId "coffeepartner.capt.example" 10 | minSdkVersion 15 11 | targetSdkVersion 28 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation fileTree(dir: 'libs', include: ['*.jar']) 26 | implementation 'com.android.support:appcompat-v7:28.0.0' 27 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 28 | implementation 'com.android.support:design:28.0.0' 29 | testImplementation 'junit:junit:4.12' 30 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 31 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 32 | implementation project(":safecatcherrt") 33 | capt project(":safecatcher") 34 | } 35 | 36 | capt { 37 | plugins { 38 | 'com.example.safecatcher' { 39 | scope CaptPluginExtension.ASSEMBLE | CaptPluginExtension.ANDROID_TEST 40 | 41 | plugin_defined_args1 121322 42 | 43 | plugin_defined_args2 { 44 | 45 | } 46 | } 47 | 'com.example.test' {} 48 | } 49 | } -------------------------------------------------------------------------------- /samples/SafeCatcher/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/androidTest/java/coffeepartner/capt/example/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.example; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("coffeepartner.capt.example", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/androidTest/java/coffeepartner/capt/example/MatchesInAndroidTest.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.example; 2 | 3 | import android.util.Log; 4 | 5 | import coffeepartner.capt.sample.safecatcher.rt.Match; 6 | 7 | public class MatchesInAndroidTest { 8 | @Match(owner = "java/lang/Integer", name = "parseInt", desc = "(Ljava/lang/String;)I") 9 | public static int parseInt(String s) { 10 | try { 11 | return Integer.parseInt(s); 12 | } catch (NumberFormatException e) { 13 | Log.e("catch", "parseInt : " + s, e); 14 | } 15 | return 0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/androidTest/java/coffeepartner/capt/example/ParseIntTest.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.example; 2 | 3 | import android.support.test.runner.AndroidJUnit4; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | @RunWith(AndroidJUnit4.class) 10 | public class ParseIntTest { 11 | 12 | @Test 13 | public void parseInt() { 14 | try { 15 | Integer.parseInt(null); 16 | } catch (NumberFormatException e) { 17 | throw new AssertionError(e); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/java/coffeepartner/capt/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.example; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.FloatingActionButton; 5 | import android.support.design.widget.Snackbar; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.support.v7.widget.Toolbar; 8 | import android.util.Log; 9 | import android.view.View; 10 | import android.view.Menu; 11 | import android.view.MenuItem; 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_main); 19 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 20 | setSupportActionBar(toolbar); 21 | 22 | FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); 23 | fab.setOnClickListener(new View.OnClickListener() { 24 | @Override 25 | public void onClick(View view) { 26 | Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) 27 | .setAction("Action", null).show(); 28 | } 29 | }); 30 | Log.e("debug", "int : " + Integer.parseInt(null)); 31 | } 32 | 33 | @Override 34 | public boolean onCreateOptionsMenu(Menu menu) { 35 | // Inflate the menu; this adds items to the action bar if it is present. 36 | getMenuInflater().inflate(R.menu.menu_main, menu); 37 | return true; 38 | } 39 | 40 | @Override 41 | public boolean onOptionsItemSelected(MenuItem item) { 42 | // Handle action bar item clicks here. The action bar will 43 | // automatically handle clicks on the Home/Up button, so long 44 | // as you specify a parent activity in AndroidManifest.xml. 45 | int id = item.getItemId(); 46 | 47 | //noinspection SimplifiableIfStatement 48 | if (id == R.id.action_settings) { 49 | return true; 50 | } 51 | 52 | return super.onOptionsItemSelected(item); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/java/coffeepartner/capt/example/Matches.java: -------------------------------------------------------------------------------- 1 | package coffeepartner.capt.example; 2 | 3 | import android.util.Log; 4 | 5 | import coffeepartner.capt.sample.safecatcher.rt.Match; 6 | 7 | public class Matches { 8 | 9 | @Match(owner = "java/lang/Integer", name = "parseInt", desc = "(Ljava/lang/String;)I") 10 | public static int parseInt(String s) { 11 | try { 12 | return Integer.parseInt(s); 13 | } catch (NumberFormatException e) { 14 | Log.e("catch", "parseInt : " + s, e); 15 | } 16 | return 0; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 20 | 21 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 |

5 | 9 | 10 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoffeePartner/capt/fd705b8f8786e4e2ff76145299503edeca09e086/samples/SafeCatcher/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 4 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | example 3 | Settings 4 | 5 | -------------------------------------------------------------------------------- /samples/SafeCatcher/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 14 |