├── .gitignore ├── README.markdown ├── build.gradle ├── common ├── build.gradle └── src │ └── main │ ├── java │ └── org │ │ └── mariotaku │ │ └── patchlib │ │ └── common │ │ ├── asm │ │ ├── ChangeClassVisitor.java │ │ └── ExtendedClassWriter.java │ │ ├── model │ │ ├── ExceptionInfo.java │ │ ├── ModifierInfo.java │ │ ├── PatchClassInfo.java │ │ ├── ProcessingRules.java │ │ └── deserializer │ │ │ ├── ExceptionInfoDeserializer.java │ │ │ └── ModifierInfoDeserializer.java │ │ ├── processor │ │ ├── LibraryProcessor.java │ │ └── impl │ │ │ ├── AarLibraryProcessor.java │ │ │ └── JarLibraryProcessor.java │ │ └── util │ │ └── Utils.java │ └── resources │ └── META-INF │ └── gradle-plugins │ └── org.mariotaku.patchlib.properties ├── executable ├── build.gradle └── src │ └── main │ └── java │ └── org │ └── mariotaku │ └── patchlib │ ├── Main.java │ └── PatchLibApplication.java ├── gradle-plugin ├── build.gradle └── src │ └── main │ ├── groovy │ └── org │ │ └── mariotaku │ │ └── patchlib │ │ ├── PatchLibExtension.groovy │ │ ├── PatchLibPlugin.groovy │ │ └── task │ │ └── PatchLibProcessTask.groovy │ └── resources │ └── META-INF │ └── gradle-plugins │ └── org.mariotaku.patchlib.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | build 3 | 4 | # Local configuration file (sdk path, etc) 5 | local.properties 6 | 7 | # Gradle generated files 8 | .gradle/ 9 | 10 | # Signing files 11 | .signing/ 12 | 13 | # User-specific configurations 14 | /.idea 15 | *.iml 16 | 17 | # OS-specific files 18 | .DS_Store 19 | .DS_Store? 20 | ._* 21 | .Spotlight-V100 22 | .Trashes 23 | ehthumbs.db 24 | Thumbs.db 25 | 26 | # Private files 27 | /signing.properties -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | #PatchLib 2 | 3 | --- 4 | 5 | ## In very early stage of development! 6 | 7 | Goodbye, Java reflection! 8 | 9 | Let's pretend you are using a good library that fits almost all your need, except one thing - some useful fields or classes are private, so you can't access in normal ways. 10 | 11 | It's extremely painful to use reflections. 12 | 13 | ````java 14 | AClass aInstance = someMethod(); 15 | Class cls = Class.forName("com.example.ADefaultClass"); 16 | cls.setAccessible(true); 17 | Field field = cls.getDeclaredField("aPrivateField"); 18 | field.setAccessible(true); 19 | Object fieldObj = field.get(aInstance); 20 | Class fieldCls = Class.forName("com.example.APrivateFieldType"); 21 | Method classMethod = fieldCls.getMethod("privateMethod", Object.class); 22 | classMethod.invoke(fieldObj, "Parameter"); 23 | // ... Exceptions are even not included in this example! 24 | 25 | ```` 26 | Hey! since classes in those libraries will be shipped with our binary, why can't we modify it? 27 | 28 | With PatchLib, all you need to do is write a rule file in .yml format like this: 29 | 30 | 31 | ````yaml 32 | com/example/ADefaultClass: 33 | modifiers: +public # Make public 34 | fields: 35 | - aPrivateField: 36 | modifiers: +public # Make public 37 | com/example/APrivateFieldType: 38 | modifiers: +public # Make public 39 | methods: 40 | - privateMethod: 41 | modifiers: +public # Make public 42 | 43 | ```` 44 | 45 | ... and use your desired classes like this: 46 | 47 | 48 | ````java 49 | AClass aInstance = someMethod(); 50 | aInstance.aPrivateField.privateMethod("Parameter"); 51 | 52 | ```` 53 | 54 | That's it! 55 | 56 | --- 57 | 58 | There're more advanced usages: 59 | 60 | ````yaml 61 | # Let you throw custom exceptions in Retrofit 62 | /retrofit/Converter: # Will match all subclasses if name beginning with a slash 63 | methods: 64 | convert: 65 | exceptions: +com/example/CustomException 66 | /retrofit/RequestBuilderAction: 67 | methods: 68 | perform: 69 | exceptions: +com/example/CustomException 70 | /retrofit/RequestFactory: 71 | methods: 72 | create: 73 | exceptions: +com/example/CustomException 74 | retrofit/OkHttpCall: 75 | methods: 76 | parseResponse: 77 | exceptions: +com/example/CustomException 78 | createRawCall: 79 | exceptions: +com/example/CustomException 80 | execute: 81 | exceptions: +com/example/CustomException 82 | /retrofit/Call: 83 | methods: 84 | execute: 85 | exceptions: +com/example/CustomException 86 | /retrofit/CallAdapter: 87 | methods: 88 | adapt: 89 | exceptions: +com/example/CustomException 90 | retrofit/MethodHandler: 91 | methods: 92 | invoke: 93 | exceptions: +com/example/CustomException 94 | 95 | ```` 96 | 97 | The library not exactly matches your need? Patch it! No more reflections, no more waiting for pull request approval, no more meed to fork repos and merge code changes, just write rules and enjoy! -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | version = '0.1.1-SNAPSHOT' 3 | group 'com.github.mariotaku.PatchLib' 4 | } 5 | 6 | subprojects { 7 | apply plugin: 'java' 8 | apply plugin: 'maven' 9 | 10 | sourceCompatibility = JavaVersion.VERSION_1_7 11 | targetCompatibility = JavaVersion.VERSION_1_7 12 | 13 | task sourcesJar(type: Jar, dependsOn: classes) { 14 | classifier = 'sources' 15 | from sourceSets.main.allSource 16 | } 17 | 18 | task javadocJar(type: Jar, dependsOn: javadoc) { 19 | classifier = 'javadoc' 20 | } 21 | 22 | artifacts { 23 | archives sourcesJar 24 | archives javadocJar 25 | } 26 | } -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | compile 'org.ow2.asm:asm:5.0.3' 9 | compile 'com.fasterxml.jackson.core:jackson-databind:2.6.3' 10 | compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.5.4' 11 | compile 'org.xeustechnologies:jcl-core:2.7' 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/asm/ChangeClassVisitor.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.asm; 2 | 3 | import org.mariotaku.patchlib.common.model.PatchClassInfo; 4 | import org.mariotaku.patchlib.common.model.ProcessingRules; 5 | import org.mariotaku.patchlib.common.processor.LibraryProcessor; 6 | import org.objectweb.asm.*; 7 | 8 | public class ChangeClassVisitor extends ClassVisitor { 9 | 10 | private final ProcessingRules conf; 11 | private final LibraryProcessor.CommandLineOptions opts; 12 | private PatchClassInfo classInfo; 13 | 14 | public ChangeClassVisitor(ClassWriter cw, ProcessingRules conf, LibraryProcessor.CommandLineOptions opts) { 15 | super(Opcodes.ASM5, cw); 16 | this.conf = conf; 17 | this.opts = opts; 18 | } 19 | 20 | @Override 21 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 22 | classInfo = conf.getClassInfo(opts, name, signature, superName, interfaces); 23 | if (classInfo != null) { 24 | if (opts.isVerbose()) { 25 | System.out.printf("Processing class %s\n", name); 26 | } 27 | access = classInfo.processModifiers(access); 28 | } 29 | super.visit(version, access, name, signature, superName, interfaces); 30 | } 31 | 32 | @Override 33 | public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { 34 | if (classInfo != null) { 35 | PatchClassInfo.PatchMemberInfo fieldInfo = classInfo.getFieldInfo(name); 36 | if (fieldInfo != null) { 37 | if (opts.isVerbose()) { 38 | System.out.printf("Processing field %s\n", name); 39 | } 40 | access = fieldInfo.processModifiers(access); 41 | } 42 | } 43 | return super.visitField(access, name, desc, signature, value); 44 | } 45 | 46 | @Override 47 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 48 | if (classInfo != null) { 49 | PatchClassInfo.PatchMethodInfo fieldInfo = classInfo.getMethodInfo(name); 50 | if (fieldInfo != null) { 51 | if (opts.isVerbose()) { 52 | System.out.printf("Processing method %s\n", name); 53 | } 54 | access = fieldInfo.processModifiers(access); 55 | exceptions = fieldInfo.processExceptions(exceptions); 56 | } 57 | } 58 | return super.visitMethod(access, name, desc, signature, exceptions); 59 | } 60 | 61 | public boolean found() { 62 | return classInfo != null; 63 | } 64 | } -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/asm/ExtendedClassWriter.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.asm; 2 | 3 | import org.objectweb.asm.ClassReader; 4 | import org.objectweb.asm.ClassWriter; 5 | 6 | /** 7 | * Created by mariotaku on 15/11/30. 8 | */ 9 | public class ExtendedClassWriter extends ClassWriter { 10 | private final ClassLoader classLoader; 11 | 12 | public ExtendedClassWriter(int flags, ClassLoader classLoader) { 13 | super(flags); 14 | this.classLoader = classLoader; 15 | } 16 | 17 | @Override 18 | protected String getCommonSuperClass(String type1, String type2) { 19 | Class c, d; 20 | try { 21 | c = Class.forName(type1.replace('/', '.'), false, classLoader); 22 | d = Class.forName(type2.replace('/', '.'), false, classLoader); 23 | } catch (Exception e) { 24 | throw new RuntimeException(e); 25 | } 26 | if (c.isAssignableFrom(d)) { 27 | return type1; 28 | } 29 | if (d.isAssignableFrom(c)) { 30 | return type2; 31 | } 32 | if (c.isInterface() || d.isInterface()) { 33 | return "java/lang/Object"; 34 | } else { 35 | do { 36 | c = c.getSuperclass(); 37 | } while (!c.isAssignableFrom(d)); 38 | return c.getName().replace('.', '/'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/model/ExceptionInfo.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.model; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * Created by mariotaku on 15/11/29. 7 | */ 8 | 9 | public class ExceptionInfo { 10 | 11 | final List operators = new ArrayList<>(); 12 | IsSetMode setMode = IsSetMode.NONE; 13 | 14 | public static ExceptionInfo parse(String string) { 15 | final ExceptionInfo info = new ExceptionInfo(); 16 | Scanner scanner = new Scanner(string); 17 | while (scanner.hasNext()) { 18 | info.addOperator(scanner.next()); 19 | } 20 | return info; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "ExceptionInfo{" + 26 | "operators=" + operators + 27 | ", setMode=" + setMode + 28 | '}'; 29 | } 30 | 31 | private void addOperator(String token) { 32 | final Mode mode; 33 | final String modifier; 34 | if (token.startsWith("+")) { 35 | mode = Mode.ADD; 36 | modifier = token.substring(1).trim(); 37 | setSetMode(IsSetMode.FALSE); 38 | } else if (token.startsWith("-")) { 39 | mode = Mode.REMOVE; 40 | modifier = token.substring(1).trim(); 41 | setSetMode(IsSetMode.FALSE); 42 | } else { 43 | mode = Mode.SET; 44 | modifier = token.trim(); 45 | setSetMode(IsSetMode.TRUE); 46 | } 47 | operators.add(new Operator(mode, modifier)); 48 | } 49 | 50 | private void setSetMode(IsSetMode newMode) { 51 | if (setMode != IsSetMode.NONE && setMode != newMode) { 52 | throw new IllegalArgumentException("Illegal modifier mode combination " + setMode + " VS " + newMode); 53 | } 54 | setMode = newMode; 55 | } 56 | 57 | public String[] process(String[] exceptions) { 58 | Set result = new HashSet<>(); 59 | if (setMode == IsSetMode.TRUE) { 60 | for (Operator operator : operators) { 61 | result.add(operator.exception); 62 | } 63 | return result.toArray(new String[result.size()]); 64 | } else { 65 | if (exceptions != null) { 66 | Collections.addAll(result, exceptions); 67 | } 68 | for (Operator operator : operators) { 69 | if (operator.mode == Mode.ADD) { 70 | result.add(operator.exception); 71 | } else if (operator.mode == Mode.REMOVE) { 72 | result.remove(operator.exception); 73 | } 74 | } 75 | } 76 | return result.toArray(new String[result.size()]); 77 | } 78 | 79 | enum Mode { 80 | ADD, REMOVE, SET 81 | } 82 | 83 | enum IsSetMode { 84 | TRUE, 85 | FALSE, 86 | NONE 87 | } 88 | 89 | static class Operator { 90 | Mode mode; 91 | String exception; 92 | 93 | Operator(Mode mode, String exception) { 94 | this.mode = mode; 95 | this.exception = exception; 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | return "Operator{" + 101 | "mode=" + mode + 102 | ", exception='" + exception + '\'' + 103 | '}'; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/model/ModifierInfo.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.model; 2 | 3 | import org.objectweb.asm.Opcodes; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Scanner; 8 | 9 | /** 10 | * Created by mariotaku on 15/11/29. 11 | */ 12 | 13 | public class ModifierInfo { 14 | 15 | final List operators = new ArrayList<>(); 16 | IsSetMode setMode = IsSetMode.NONE; 17 | 18 | public static ModifierInfo parse(String string) { 19 | final ModifierInfo info = new ModifierInfo(); 20 | Scanner scanner = new Scanner(string); 21 | while (scanner.hasNext()) { 22 | info.addOperator(scanner.next()); 23 | } 24 | return info; 25 | } 26 | 27 | private void addOperator(String token) { 28 | final Mode mode; 29 | final String modifier; 30 | if (token.startsWith("+")) { 31 | mode = Mode.ADD; 32 | modifier = token.substring(1).trim(); 33 | setSetMode(IsSetMode.FALSE); 34 | } else if (token.startsWith("-")) { 35 | mode = Mode.REMOVE; 36 | modifier = token.substring(1).trim(); 37 | setSetMode(IsSetMode.FALSE); 38 | } else { 39 | mode = Mode.SET; 40 | modifier = token.trim(); 41 | setSetMode(IsSetMode.TRUE); 42 | } 43 | operators.add(new Operator(mode, modifier)); 44 | } 45 | 46 | private void setSetMode(IsSetMode newMode) { 47 | if (setMode != IsSetMode.NONE && setMode != newMode) { 48 | throw new IllegalArgumentException("Illegal modifier mode combination " + setMode + " VS " + newMode); 49 | } 50 | setMode = newMode; 51 | } 52 | 53 | public int process(int modifiers) { 54 | if (setMode == IsSetMode.TRUE) { 55 | int newModifiers = 0; 56 | for (Operator operator : operators) { 57 | newModifiers |= operator.modifier; 58 | } 59 | return newModifiers; 60 | } else { 61 | for (Operator operator : operators) { 62 | if (operator.mode == Mode.ADD) { 63 | modifiers = regulateModifiers(modifiers, operator.modifier); 64 | } else if (operator.mode == Mode.REMOVE) { 65 | modifiers &= ~operator.modifier; 66 | } 67 | } 68 | } 69 | return modifiers; 70 | } 71 | 72 | private int regulateModifiers(int modifiers, int modifier) { 73 | if (isVisibilityModifier(modifier)) { 74 | modifiers &= ~(Opcodes.ACC_PUBLIC | Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED); 75 | } 76 | return modifiers | modifier; 77 | } 78 | 79 | private boolean isVisibilityModifier(int modifier) { 80 | return (modifier & Opcodes.ACC_PUBLIC) != 0 || (modifier & Opcodes.ACC_PRIVATE) != 0 || 81 | (modifier & Opcodes.ACC_PROTECTED) != 0; 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | return "ModifierInfo{" + 87 | "operators=" + operators + 88 | ", setMode=" + setMode + 89 | '}'; 90 | } 91 | 92 | enum Mode { 93 | ADD, REMOVE, SET 94 | } 95 | 96 | enum IsSetMode { 97 | TRUE, 98 | FALSE, 99 | NONE 100 | } 101 | 102 | static class Operator { 103 | Mode mode; 104 | int modifier; 105 | 106 | Operator(Mode mode, String modifier) { 107 | this.mode = mode; 108 | this.modifier = parseModifier(modifier); 109 | } 110 | 111 | static int parseModifier(String string) { 112 | switch (string) { 113 | case "public": 114 | return Opcodes.ACC_PUBLIC; 115 | case "private": 116 | return Opcodes.ACC_PRIVATE; 117 | case "protected": 118 | return Opcodes.ACC_PROTECTED; 119 | case "static": 120 | return Opcodes.ACC_STATIC; 121 | case "final": 122 | return Opcodes.ACC_FINAL; 123 | } 124 | throw new IllegalArgumentException("Unsupported modifier " + string); 125 | } 126 | 127 | @Override 128 | public String toString() { 129 | return "Operator{" + 130 | "mode=" + mode + 131 | ", modifier=" + modifier + 132 | '}'; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/model/PatchClassInfo.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 6 | import org.mariotaku.patchlib.common.model.deserializer.ExceptionInfoDeserializer; 7 | import org.mariotaku.patchlib.common.model.deserializer.ModifierInfoDeserializer; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by mariotaku on 15/11/29. 13 | */ 14 | @JsonIgnoreProperties(ignoreUnknown = true) 15 | public class PatchClassInfo { 16 | 17 | Map fields; 18 | Map methods; 19 | ModifierInfo modifiers; 20 | 21 | @JsonProperty("modifiers") 22 | public ModifierInfo getModifiers() { 23 | return modifiers; 24 | } 25 | 26 | @JsonDeserialize(using = ModifierInfoDeserializer.class) 27 | public void setModifiers(ModifierInfo modifiers) { 28 | this.modifiers = modifiers; 29 | } 30 | 31 | @JsonProperty("fields") 32 | public Map getFields() { 33 | return fields; 34 | } 35 | 36 | public void setFields(Map fields) { 37 | this.fields = fields; 38 | } 39 | 40 | @JsonProperty("methods") 41 | public Map getMethods() { 42 | return methods; 43 | } 44 | 45 | public void setMethods(Map methods) { 46 | this.methods = methods; 47 | } 48 | 49 | public int processModifiers(int access) { 50 | if (modifiers == null) return access; 51 | return modifiers.process(access); 52 | } 53 | 54 | public PatchMemberInfo getFieldInfo(String name) { 55 | if (fields == null) return null; 56 | return fields.get(name); 57 | } 58 | 59 | public PatchMethodInfo getMethodInfo(String name) { 60 | if (methods == null) return null; 61 | return methods.get(name); 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return "PatchClassInfo{" + 67 | "fields=" + fields + 68 | ", methods=" + methods + 69 | ", modifiers=" + modifiers + 70 | '}'; 71 | } 72 | 73 | @JsonIgnoreProperties(ignoreUnknown = true) 74 | public static class PatchMemberInfo { 75 | ModifierInfo modifiers; 76 | 77 | @JsonProperty("modifiers") 78 | public ModifierInfo getModifiers() { 79 | return modifiers; 80 | } 81 | 82 | @JsonDeserialize(using = ModifierInfoDeserializer.class) 83 | public void setModifiers(ModifierInfo modifiers) { 84 | this.modifiers = modifiers; 85 | } 86 | 87 | public int processModifiers(int access) { 88 | if (modifiers == null) return access; 89 | return modifiers.process(access); 90 | } 91 | 92 | @Override 93 | public String toString() { 94 | return "PatchMemberInfo{" + 95 | "modifiers=" + modifiers + 96 | '}'; 97 | } 98 | } 99 | 100 | @JsonIgnoreProperties(ignoreUnknown = true) 101 | public static class PatchMethodInfo extends PatchMemberInfo { 102 | ExceptionInfo exceptions; 103 | 104 | @JsonProperty("exceptions") 105 | public ExceptionInfo getExceptions() { 106 | return exceptions; 107 | } 108 | 109 | @JsonDeserialize(using = ExceptionInfoDeserializer.class) 110 | public void setExceptions(ExceptionInfo exceptions) { 111 | this.exceptions = exceptions; 112 | } 113 | 114 | @Override 115 | public String toString() { 116 | return "PatchMethodInfo{" + 117 | "exceptions=" + exceptions + 118 | "} " + super.toString(); 119 | } 120 | 121 | public String[] processExceptions(String[] array) { 122 | if (exceptions == null) return array; 123 | return exceptions.process(array); 124 | } 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/model/ProcessingRules.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import org.mariotaku.patchlib.common.processor.LibraryProcessor; 6 | 7 | import java.io.IOException; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by mariotaku on 15/12/2. 13 | */ 14 | @JsonIgnoreProperties(ignoreUnknown = true) 15 | public class ProcessingRules { 16 | 17 | Map rules; 18 | 19 | @JsonProperty("rules") 20 | public Map getRules() { 21 | return rules; 22 | } 23 | 24 | public void setRules(Map rules) { 25 | this.rules = rules; 26 | } 27 | 28 | public PatchClassInfo getClassInfo(LibraryProcessor.CommandLineOptions opts, String name, String signature, 29 | String superName, String[] interfaces) { 30 | if (rules == null) return null; 31 | for (Map.Entry entry : rules.entrySet()) { 32 | if (matches(opts, entry.getKey(), name)) { 33 | return entry.getValue(); 34 | } 35 | } 36 | return null; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return "ConfigurationFile{" + 42 | "rules=" + rules + 43 | '}'; 44 | } 45 | 46 | private boolean matches(LibraryProcessor.CommandLineOptions opts, String ruleName, String className) { 47 | if (ruleName.equals(className)) return true; 48 | if (ruleName.startsWith("/")) { 49 | try { 50 | final ClassLoader loader = opts.createClassLoader(); 51 | final Class ruleCls = loader.loadClass(ruleName.substring(1).replace('/', '.')); 52 | return ruleCls.isAssignableFrom(loader.loadClass(className.replace('/', '.'))); 53 | } catch (ClassNotFoundException e) { 54 | return false; 55 | } catch (IOException e) { 56 | throw new RuntimeException(e); 57 | } 58 | } 59 | return false; 60 | } 61 | 62 | public void addRules(Map infoMap) { 63 | if (rules == null) rules = new HashMap<>(); 64 | rules.putAll(infoMap); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/model/deserializer/ExceptionInfoDeserializer.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.model.deserializer; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationContext; 5 | import com.fasterxml.jackson.databind.JsonDeserializer; 6 | import org.mariotaku.patchlib.common.model.ExceptionInfo; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * Created by mariotaku on 15/11/29. 12 | */ 13 | public class ExceptionInfoDeserializer extends JsonDeserializer { 14 | @Override 15 | public ExceptionInfo deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { 16 | return ExceptionInfo.parse(jsonParser.getValueAsString()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/model/deserializer/ModifierInfoDeserializer.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.model.deserializer; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationContext; 5 | import com.fasterxml.jackson.databind.JsonDeserializer; 6 | import org.mariotaku.patchlib.common.model.ModifierInfo; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * Created by mariotaku on 15/11/29. 12 | */ 13 | public class ModifierInfoDeserializer extends JsonDeserializer { 14 | @Override 15 | public ModifierInfo deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { 16 | return ModifierInfo.parse(jsonParser.getValueAsString()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/processor/LibraryProcessor.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.processor; 2 | 3 | import org.mariotaku.patchlib.common.model.ProcessingRules; 4 | import org.mariotaku.patchlib.common.processor.impl.AarLibraryProcessor; 5 | import org.mariotaku.patchlib.common.processor.impl.JarLibraryProcessor; 6 | import org.xeustechnologies.jcl.JarClassLoader; 7 | 8 | import java.io.*; 9 | import java.util.Collection; 10 | import java.util.Enumeration; 11 | import java.util.Map; 12 | import java.util.Set; 13 | import java.util.jar.*; 14 | 15 | /** 16 | * Created by mariotaku on 15/11/30. 17 | */ 18 | public abstract class LibraryProcessor { 19 | 20 | public final InputStream source; 21 | public final OutputStream target; 22 | public final ProcessingRules rules; 23 | public final CommandLineOptions opts; 24 | 25 | public LibraryProcessor(InputStream source, OutputStream target, ProcessingRules rules) { 26 | this(source, target, rules, new CommandLineOptions()); 27 | } 28 | 29 | public LibraryProcessor(InputStream source, OutputStream target, ProcessingRules rules, CommandLineOptions opts) { 30 | this.source = source; 31 | this.target = target; 32 | this.rules = rules; 33 | this.opts = opts; 34 | } 35 | 36 | public boolean process() throws IOException { 37 | boolean processed = false; 38 | final JarInputStream inputStream = new JarInputStream(source); 39 | 40 | Manifest manifest = inputStream.getManifest(); 41 | if (manifest == null) { 42 | manifest = new Manifest(); 43 | } 44 | if (opts.sourceFilePath != null) { 45 | final Map entries = manifest.getEntries(); 46 | Attributes attributes = new Attributes(); 47 | attributes.put(new Attributes.Name("Source-File-Path"), opts.sourceFilePath); 48 | entries.put("PatchLib", attributes); 49 | } 50 | final JarOutputStream outputStream = new JarOutputStream(target, manifest); 51 | JarEntry entry; 52 | while ((entry = inputStream.getNextJarEntry()) != null) { 53 | // Only process .class file 54 | processed |= processEntry(inputStream, outputStream, entry); 55 | } 56 | outputStream.finish(); 57 | return processed; 58 | } 59 | 60 | protected abstract boolean processEntry(JarInputStream inputArchive, JarOutputStream outputArchive, JarEntry entry) throws IOException; 61 | 62 | public static LibraryProcessor get(InputStream source, OutputStream target, String name, ProcessingRules conf, CommandLineOptions opts) { 63 | if (name.endsWith(".jar")) { 64 | return new JarLibraryProcessor(source, target, conf, opts); 65 | } else if (name.endsWith(".aar")) { 66 | return new AarLibraryProcessor(source, target, conf, opts); 67 | } 68 | return null; 69 | } 70 | 71 | public static class CommandLineOptions { 72 | public Set extraClasspath; 73 | private ClassLoader cachedClassLoader; 74 | private boolean verbose; 75 | private String sourceFilePath; 76 | 77 | public Set getExtraClasspath() { 78 | return extraClasspath; 79 | } 80 | 81 | public void setExtraClasspath(Set extraClasspath) { 82 | this.extraClasspath = extraClasspath; 83 | cachedClassLoader = null; 84 | } 85 | 86 | public ClassLoader createClassLoader() throws IOException { 87 | if (cachedClassLoader != null) return cachedClassLoader; 88 | if (extraClasspath == null) { 89 | return cachedClassLoader = LibraryProcessor.class.getClassLoader(); 90 | } 91 | return cachedClassLoader = createClassLoader(extraClasspath); 92 | } 93 | 94 | private static ClassLoader createClassLoader(Collection classpath) throws IOException { 95 | JarClassLoader jcl = new JarClassLoader(); 96 | for (File item : classpath) { 97 | if (item == null) continue; 98 | final String name = item.getName(); 99 | if (name.endsWith(".jar")) { 100 | jcl.add(new FileInputStream(item)); 101 | } else if (name.endsWith(".aar")) { 102 | try (JarFile zip = new JarFile(item)) { 103 | final Enumeration entries = zip.entries(); 104 | while (entries.hasMoreElements()) { 105 | JarEntry entry = entries.nextElement(); 106 | if (entry.getName().endsWith(".jar")) { 107 | jcl.add(zip.getInputStream(entry)); 108 | } 109 | } 110 | } 111 | } 112 | } 113 | return jcl; 114 | } 115 | 116 | public void setVerbose(boolean verbose) { 117 | this.verbose = verbose; 118 | } 119 | 120 | public boolean isVerbose() { 121 | return verbose; 122 | } 123 | 124 | public void setSourceFilePath(String comment) { 125 | this.sourceFilePath = comment; 126 | } 127 | 128 | public String getSourceFilePath() { 129 | return sourceFilePath; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/processor/impl/AarLibraryProcessor.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.processor.impl; 2 | 3 | import org.mariotaku.patchlib.common.model.ProcessingRules; 4 | import org.mariotaku.patchlib.common.processor.LibraryProcessor; 5 | import org.mariotaku.patchlib.common.util.Utils; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.util.jar.JarEntry; 11 | import java.util.jar.JarInputStream; 12 | import java.util.jar.JarOutputStream; 13 | 14 | /** 15 | * Created by mariotaku on 15/11/30. 16 | */ 17 | public class AarLibraryProcessor extends LibraryProcessor { 18 | 19 | public AarLibraryProcessor(InputStream source, OutputStream target, ProcessingRules conf) { 20 | super(source, target, conf); 21 | } 22 | 23 | public AarLibraryProcessor(InputStream source, OutputStream target, ProcessingRules conf, CommandLineOptions opts) { 24 | super(source, target, conf, opts); 25 | } 26 | 27 | @Override 28 | protected boolean processEntry(JarInputStream inputStream, JarOutputStream outputStream, JarEntry entry) throws IOException { 29 | boolean processed = false; 30 | final String entryName = entry.getName(); 31 | if (entry.isDirectory()) { 32 | JarLibraryProcessor.processDirectory(outputStream, entry); 33 | } else if (entryName.endsWith(".class")) { 34 | processed = Utils.processMatchedClass(inputStream, outputStream, entry, rules, opts); 35 | } else if (entryName.endsWith(".jar")) { 36 | final JarEntry newEntry = new JarEntry(entry.getName()); 37 | outputStream.putNextEntry(newEntry); 38 | JarLibraryProcessor processor = new JarLibraryProcessor(inputStream, outputStream, rules, opts); 39 | processed = processor.process(); 40 | outputStream.closeEntry(); 41 | } else { 42 | JarLibraryProcessor.processDirectFile(inputStream, outputStream, entry); 43 | } 44 | return processed; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/processor/impl/JarLibraryProcessor.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.processor.impl; 2 | 3 | import org.mariotaku.patchlib.common.model.ProcessingRules; 4 | import org.mariotaku.patchlib.common.processor.LibraryProcessor; 5 | import org.mariotaku.patchlib.common.util.Utils; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.util.jar.JarEntry; 11 | import java.util.jar.JarFile; 12 | import java.util.jar.JarInputStream; 13 | import java.util.jar.JarOutputStream; 14 | 15 | /** 16 | * Created by mariotaku on 15/11/29. 17 | */ 18 | public class JarLibraryProcessor extends LibraryProcessor { 19 | 20 | public JarLibraryProcessor(InputStream source, OutputStream target, ProcessingRules conf, CommandLineOptions opts) { 21 | super(source, target, conf, opts); 22 | } 23 | 24 | public JarLibraryProcessor(InputStream source, OutputStream target, ProcessingRules conf) { 25 | super(source, target, conf); 26 | } 27 | 28 | protected boolean processEntry(JarInputStream inputArchive, JarOutputStream outputArchive, JarEntry entry) throws IOException { 29 | boolean processed = false; 30 | final String entryName = entry.getName(); 31 | if (entry.isDirectory()) { 32 | processDirectory(outputArchive, entry); 33 | } else if (entryName.endsWith(".class")) { 34 | processed = Utils.processMatchedClass(inputArchive, outputArchive, entry, rules, opts); 35 | } else if (!entryName.equals(JarFile.MANIFEST_NAME)) { 36 | processDirectFile(inputArchive, outputArchive, entry); 37 | } 38 | return processed; 39 | } 40 | 41 | static void processDirectFile(JarInputStream inputArchive, JarOutputStream outputArchive, JarEntry entry) throws IOException { 42 | // Pass through 43 | final JarEntry newEntry = new JarEntry(entry.getName()); 44 | outputArchive.putNextEntry(newEntry); 45 | Utils.copy(inputArchive, outputArchive); 46 | outputArchive.closeEntry(); 47 | } 48 | 49 | static void processDirectory(JarOutputStream outputArchive, JarEntry entry) throws IOException { 50 | outputArchive.putNextEntry(new JarEntry(entry.getName())); 51 | outputArchive.closeEntry(); 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /common/src/main/java/org/mariotaku/patchlib/common/util/Utils.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.common.util; 2 | 3 | import org.mariotaku.patchlib.common.asm.ChangeClassVisitor; 4 | import org.mariotaku.patchlib.common.asm.ExtendedClassWriter; 5 | import org.mariotaku.patchlib.common.model.ProcessingRules; 6 | import org.mariotaku.patchlib.common.processor.LibraryProcessor; 7 | import org.objectweb.asm.ClassReader; 8 | import org.objectweb.asm.ClassWriter; 9 | 10 | import java.io.IOException; 11 | import java.io.OutputStream; 12 | import java.util.jar.JarEntry; 13 | import java.util.jar.JarInputStream; 14 | import java.util.jar.JarOutputStream; 15 | import java.util.jar.Manifest; 16 | 17 | /** 18 | * Created by mariotaku on 15/12/1. 19 | */ 20 | public class Utils { 21 | 22 | public static JarOutputStream openJarOutputStreamForCopy(JarInputStream sourceStream, OutputStream targetStream) throws IOException { 23 | final Manifest manifest = sourceStream.getManifest(); 24 | if (manifest != null) return new JarOutputStream(targetStream, manifest); 25 | return new JarOutputStream(targetStream); 26 | } 27 | 28 | public static boolean processMatchedClass(JarInputStream inputArchive, JarOutputStream outputArchive, JarEntry entry, 29 | ProcessingRules conf, LibraryProcessor.CommandLineOptions opts) throws IOException { 30 | final ClassReader cr = new ClassReader(inputArchive); 31 | final ClassWriter cw = new ExtendedClassWriter(ClassWriter.COMPUTE_FRAMES, opts.createClassLoader()); 32 | 33 | final ChangeClassVisitor cv = new ChangeClassVisitor(cw, conf, opts); 34 | cr.accept(cv, 0); 35 | 36 | final JarEntry newEntry = new JarEntry(entry.getName()); 37 | outputArchive.putNextEntry(newEntry); 38 | outputArchive.write(cw.toByteArray()); 39 | outputArchive.closeEntry(); 40 | return cv.found(); 41 | } 42 | 43 | public static void copy(JarInputStream is, JarOutputStream os) throws IOException { 44 | final byte[] buf = new byte[1024]; 45 | int len; 46 | while ((len = is.read(buf)) > 0) { 47 | os.write(buf, 0, len); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /common/src/main/resources/META-INF/gradle-plugins/org.mariotaku.patchlib.properties: -------------------------------------------------------------------------------- 1 | implementation-class=org.mariotaku.patchlib.PatchLibPlugin -------------------------------------------------------------------------------- /executable/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'com.github.johnrengelman.shadow' 3 | apply plugin: 'maven' 4 | 5 | buildscript { 6 | repositories { jcenter() } 7 | dependencies { 8 | classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2' 9 | } 10 | } 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | shadowJar { 17 | manifest { 18 | attributes 'Main-Class': 'org.mariotaku.patchlib.Main' 19 | } 20 | exclude 'META-INF/*.DSA' 21 | exclude 'META-INF/*.RSA' 22 | } 23 | 24 | artifacts { 25 | archives shadowJar 26 | } 27 | 28 | dependencies { 29 | compile 'commons-cli:commons-cli:1.3' 30 | compile project(':common') 31 | } 32 | 33 | -------------------------------------------------------------------------------- /executable/src/main/java/org/mariotaku/patchlib/Main.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib; 2 | 3 | import org.apache.commons.cli.ParseException; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * Created by mariotaku on 15/12/1. 9 | */ 10 | public class Main { 11 | 12 | public static void main(String[] args) throws ParseException, IOException { 13 | final PatchLibApplication application = new PatchLibApplication(); 14 | application.start(args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /executable/src/main/java/org/mariotaku/patchlib/PatchLibApplication.java: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 6 | import org.apache.commons.cli.*; 7 | import org.mariotaku.patchlib.common.model.PatchClassInfo; 8 | import org.mariotaku.patchlib.common.model.ProcessingRules; 9 | import org.mariotaku.patchlib.common.processor.LibraryProcessor; 10 | 11 | import java.io.*; 12 | import java.util.HashMap; 13 | import java.util.HashSet; 14 | import java.util.Map; 15 | import java.util.Set; 16 | import java.util.regex.Pattern; 17 | 18 | /** 19 | * Created by mariotaku on 15/12/1. 20 | */ 21 | public class PatchLibApplication { 22 | private final Options options; 23 | 24 | public PatchLibApplication() { 25 | // create Options object 26 | options = new Options(); 27 | options.addOption("i", "in", true, "Input file"); 28 | options.addOption("o", "out", true, "Output file"); 29 | options.addOption("r", "rules", true, "Rules files in .yml format"); 30 | options.addOption("c", "classpath", true, "Extra classpath"); 31 | options.addOption("v", "verbose", true, "Print detailed log"); 32 | } 33 | 34 | public void start(String[] args) throws ParseException, IOException { 35 | final CommandLineParser parser = new DefaultParser(); 36 | final CommandLine cmd = parser.parse(options, args); 37 | if (cmd.hasOption("in") && cmd.hasOption("out") && cmd.hasOption("rules")) { 38 | final File inFile = new File(cmd.getOptionValue("in")); 39 | final File outFile = new File(cmd.getOptionValue("out")); 40 | if (checkParamFiles(inFile, outFile)) { 41 | if (startProcess(inFile, outFile, cmd)) { 42 | System.out.println("Process finished!"); 43 | } else { 44 | System.out.println("Library left unchanged."); 45 | } 46 | } 47 | } else { 48 | showUsage(); 49 | } 50 | } 51 | 52 | private boolean startProcess(File inFile, File outFile, CommandLine cmdLine) throws IOException { 53 | ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); 54 | 55 | final ProcessingRules conf = new ProcessingRules(); 56 | conf.setRules(new HashMap()); 57 | for (File rulesFile : getFiles(cmdLine.getOptionValue("rules"))) { 58 | final Map rulesMap = mapper.readValue(rulesFile, new TypeReference>() { 59 | }); 60 | conf.addRules(rulesMap); 61 | } 62 | try (InputStream is = new FileInputStream(inFile); OutputStream os = new FileOutputStream(outFile)) { 63 | final LibraryProcessor.CommandLineOptions options = getOptions(cmdLine); 64 | options.setSourceFilePath(inFile.getAbsolutePath()); 65 | final LibraryProcessor processor = LibraryProcessor.get(is, os, inFile.getName(), conf, options); 66 | if (processor == null) { 67 | throw new UnsupportedOperationException("Unsupported library " + inFile); 68 | } 69 | return processor.process(); 70 | } 71 | } 72 | 73 | private boolean checkParamFiles(File inFile, File outFile) { 74 | if (!inFile.exists()) { 75 | System.err.print("Input file not found"); 76 | return false; 77 | } 78 | final File outDir = outFile.getParentFile(); 79 | if (outDir != null && !outDir.exists() && !outDir.mkdirs()) { 80 | System.err.print("Cannot open output dir"); 81 | return false; 82 | } 83 | return true; 84 | } 85 | 86 | private void showUsage() { 87 | final HelpFormatter formatter = new HelpFormatter(); 88 | formatter.printHelp("PatchLib", options); 89 | } 90 | 91 | private LibraryProcessor.CommandLineOptions getOptions(CommandLine commandLine) { 92 | final String classpath = commandLine.getOptionValue("classpath"); 93 | final Set files = getFiles(classpath); 94 | LibraryProcessor.CommandLineOptions opts = new LibraryProcessor.CommandLineOptions(); 95 | opts.setExtraClasspath(files); 96 | opts.setVerbose(Boolean.parseBoolean(commandLine.getOptionValue("verbose"))); 97 | return opts; 98 | } 99 | 100 | private Set getFiles(String paths) { 101 | Set files = new HashSet<>(); 102 | if (paths != null) { 103 | for (String item : paths.split(Pattern.quote(File.pathSeparator))) { 104 | if (item.isEmpty()) continue; 105 | files.add(new File(item)); 106 | } 107 | } 108 | return files; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /gradle-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: 'java' 3 | apply plugin: 'maven' 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | compile gradleApi() 11 | compile localGroovy() 12 | compile project(':executable') 13 | } 14 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/groovy/org/mariotaku/patchlib/PatchLibExtension.groovy: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib 2 | 3 | import org.gradle.api.file.FileCollection 4 | 5 | /** 6 | * Created by mariotaku on 15/11/29. 7 | */ 8 | class PatchLibExtension { 9 | FileCollection rules; 10 | boolean verbose = false; 11 | 12 | FileCollection getRules() { 13 | return rules 14 | } 15 | 16 | void setRules(FileCollection rules) { 17 | this.rules = rules 18 | } 19 | 20 | boolean getVerbose() { 21 | return verbose 22 | } 23 | 24 | void setVerbose(boolean verbose) { 25 | this.verbose = verbose 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/groovy/org/mariotaku/patchlib/PatchLibPlugin.groovy: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.api.artifacts.ProjectDependency 6 | import org.gradle.api.tasks.compile.JavaCompile 7 | import org.mariotaku.patchlib.task.PatchLibProcessTask 8 | 9 | import java.util.jar.JarFile 10 | import java.util.regex.Pattern 11 | 12 | /** 13 | * Created by mariotaku on 15/11/29. 14 | */ 15 | class PatchLibPlugin implements Plugin { 16 | @Override 17 | void apply(Project project) { 18 | 19 | project.extensions.create('patchLib', PatchLibExtension) 20 | project.configurations.create('patchCompile') 21 | 22 | def patchLibDir = new File(project.buildDir, 'patchLib') 23 | 24 | project.tasks.create('patchLibProcess', PatchLibProcessTask) << { PatchLibProcessTask process -> 25 | PatchLibExtension props = project.extensions.findByType(PatchLibExtension) 26 | if (!props) return; 27 | Set execClasspath = [] 28 | // Add compile classpath and build script classpath to java exec task 29 | project.tasks.withType(JavaCompile) { compile -> 30 | 31 | def bootClasspath = compile.options.bootClasspath 32 | if (bootClasspath != null) { 33 | execClasspath += project.files(splitBootClassPath(bootClasspath)) 34 | } 35 | } 36 | 37 | execClasspath += project.rootProject.buildscript.configurations.classpath 38 | execClasspath += project.buildscript.configurations.classpath 39 | 40 | Set alreadyPatched = [] 41 | addToAlreadyPatched(project, alreadyPatched) 42 | 43 | def dependencyLibs = [:] 44 | def librariesToRemove = [] 45 | project.configurations.patchCompile.each { File libFile -> 46 | def destinationArchive = new File(patchLibDir, libFile.name) 47 | if (alreadyPatched.contains(libFile)) { 48 | printf('Ignoring processed library %s\n', libFile) 49 | librariesToRemove.add(destinationArchive) 50 | return 51 | } 52 | project.javaexec { 53 | it.main = Main.class.name 54 | it.args = ['-i', libFile.absolutePath, 55 | '-o', destinationArchive.absolutePath, 56 | '-r', props.rules.getAsPath(), 57 | // In order to avoid library clash, all dependency libraries are loaded separately in 58 | // custom ClassLoader 59 | '-c', project.configurations.patchCompile.getAsPath(), 60 | '-v', String.valueOf(props.verbose) 61 | ] 62 | // 63 | it.classpath += project.files(execClasspath) 64 | } 65 | 66 | dependencyLibs.put(libFile, destinationArchive) 67 | } 68 | 69 | project.configurations.patchCompile.files.each { File libFile -> 70 | if (!dependencyLibs.containsKey(libFile)) { 71 | dependencyLibs.put(libFile, libFile) 72 | } else { 73 | librariesToRemove.add(libFile) 74 | } 75 | } 76 | 77 | project.tasks.withType(JavaCompile) { compile -> 78 | Set finalLibraries = [], addedLibs = []; 79 | dependencyLibs.each { 80 | def jar = new JarFile(it.value as File) 81 | 82 | if (jar.manifest?.getAttributes("PatchLib")?.containsKey("Source-File-Path")) { 83 | def sourceFile = new File(jar.manifest.getAttributes("PatchLib").get("Source-File-Path") as String) 84 | if (sourceFile.exists()) { 85 | // Remove deps in comment, and mark this library added 86 | if (!addedLibs.contains(sourceFile)) { 87 | finalLibraries.add(it.value) 88 | addedLibs.add(sourceFile) 89 | } else { 90 | librariesToRemove.add(it.value) 91 | } 92 | librariesToRemove.add(sourceFile) 93 | } else { 94 | finalLibraries.add(it.value) 95 | } 96 | } else { 97 | finalLibraries.add(it.value) 98 | } 99 | 100 | jar.close() 101 | } 102 | compile.classpath += project.files(finalLibraries) 103 | compile.classpath -= project.files(librariesToRemove) 104 | compile.classpath -= project.files(alreadyPatched) 105 | } 106 | } 107 | 108 | project.tasks.withType(JavaCompile) { compile -> 109 | compile.dependsOn += project.tasks.withType(PatchLibProcessTask) 110 | } 111 | project.dependencies.add('compile', project.fileTree(dir: patchLibDir, include: ['*.jar'])) 112 | 113 | } 114 | 115 | static def addToAlreadyPatched(Project project, Set set) { 116 | project.configurations.compile.dependencies.each { dependency -> 117 | if (dependency instanceof ProjectDependency) { 118 | def dependencyProject = dependency.dependencyProject 119 | if (dependencyProject.configurations.hasProperty('patchCompile')) { 120 | dependencyProject.configurations.patchCompile.each { libFile -> 121 | set.add(libFile) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | static def splitBootClassPath(String paths) { 129 | def result = [] 130 | paths.split(Pattern.quote(paths)).each { 131 | if (!it.empty) result.add(it) 132 | } 133 | return result.toArray() 134 | } 135 | 136 | static def getAarDependency(String name) { 137 | def lastDot = name.lastIndexOf('.'), lastDash = name.lastIndexOf('-') 138 | return name.substring(0, lastDash) + ':' + name.substring(lastDash + 1, lastDot) + '@' + 139 | name.substring(name.lastIndexOf('.') + 1) 140 | } 141 | 142 | static def getLibraryExtension(String name) { 143 | return name.substring(name.lastIndexOf('.') + 1) 144 | } 145 | 146 | static def isJavaProject(Project project) { 147 | project.plugins.findPlugin('java') 148 | } 149 | 150 | static def isAndroidProject(Project project) { 151 | project.plugins.findPlugin('com.android.application') || project.plugins.findPlugin('com.android.library') 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/groovy/org/mariotaku/patchlib/task/PatchLibProcessTask.groovy: -------------------------------------------------------------------------------- 1 | package org.mariotaku.patchlib.task 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.tasks.JavaExec 5 | import org.mariotaku.patchlib.Main 6 | 7 | /** 8 | * Created by mariotaku on 15/12/1. 9 | */ 10 | class PatchLibProcessTask extends DefaultTask { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.mariotaku.patchlib.properties: -------------------------------------------------------------------------------- 1 | implementation-class=org.mariotaku.patchlib.PatchLibPlugin -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariotaku/PatchLib/bdce29c427e692eb8383a6ab30b77bd7213fbffe/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Nov 22 23:26:08 CST 2015 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-2.9-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'PatchLib' 2 | 3 | include ':common' 4 | include ':executable' 5 | include ':gradle-plugin' --------------------------------------------------------------------------------