diff --git a/Usage.md b/Usage.md index 5a04e7ba6..89dd4e034 100644 --- a/Usage.md +++ b/Usage.md @@ -51,6 +51,23 @@ okbuck { remove = ['.buckconfig.local', "**/BUCK"] keep = [".okbuck/**/BUCK"] } + + transform { + transforms = [ + 'appDebug' : [ + [transform : "", + configFile : ""] + ], + ] + } + + experimental { + transform = true + } +} + +dependencies { + transform "" } ``` diff --git a/build.gradle b/build.gradle index cb11b389e..7cadc4157 100644 --- a/build.gradle +++ b/build.gradle @@ -2,12 +2,14 @@ buildscript { apply from: rootProject.file('dependencies.gradle') repositories { jcenter() + maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath deps.build.androidPlugin classpath deps.build.butterKnifePlugin classpath deps.build.retrolambdaPlugin classpath deps.build.sqlDelightPlugin + classpath deps.build.shadowJar } } @@ -144,7 +146,7 @@ okbuck { 'libraries/common:paidRelease', ] ] - buckProjects = project.subprojects.findAll { it.name != "plugin" } + buckProjects = project.subprojects.findAll { it.name != "plugin" && it.name != "transform-cli" } intellij { sources = true diff --git a/buildSrc/src/main/groovy/com/uber/okbuck/composer/android/TrasformDependencyWriterRuleComposer.groovy b/buildSrc/src/main/groovy/com/uber/okbuck/composer/android/TrasformDependencyWriterRuleComposer.groovy index 3b1e1fe65..b5635bbab 100644 --- a/buildSrc/src/main/groovy/com/uber/okbuck/composer/android/TrasformDependencyWriterRuleComposer.groovy +++ b/buildSrc/src/main/groovy/com/uber/okbuck/composer/android/TrasformDependencyWriterRuleComposer.groovy @@ -7,13 +7,11 @@ import com.uber.okbuck.rule.base.GenRule import org.apache.commons.io.FileUtils import org.gradle.api.Project -import java.nio.file.Files - final class TrasformDependencyWriterRuleComposer extends AndroidBuckRuleComposer { - static final String OPT_TRANSFORM_PROVIDER_CLASS = "provider" static final String OPT_TRANSFORM_CLASS = "transform" static final String OPT_CONFIG_FILE = "configFile" + static final String RUNNER_MAIN_CLASS = "com.uber.transform.CliTransform" private TrasformDependencyWriterRuleComposer() {} @@ -24,8 +22,6 @@ final class TrasformDependencyWriterRuleComposer extends AndroidBuckRuleComposer } static GenRule compose(AndroidAppTarget target, Map options) { - String runnerMainClass = target.transformRunnerClass - String providerClass = options.get(OPT_TRANSFORM_PROVIDER_CLASS) String transformClass = options.get(OPT_TRANSFORM_CLASS) String configFile = options.get(OPT_CONFIG_FILE) @@ -37,27 +33,29 @@ final class TrasformDependencyWriterRuleComposer extends AndroidBuckRuleComposer String output = "\$OUT" List cmds = [ "echo \"#!/bin/bash\" > ${output};", - "echo \"set -e\" >> ${output};", + "echo \"set -ex\" >> ${output};", + + "echo \"java " + + + "-Dokbuck.inJarsDir=\"\\\$1\" " + + "-Dokbuck.outJarsDir=\"\\\$2\" " + + "-Dokbuck.androidBootClasspath=\"\\\$3\" " + - "echo \"export IN_JARS_DIR=\\\$1\" >> ${output};", - "echo \"export OUT_JARS_DIR=\\\$2\" >> ${output};", - "echo \"export ANDROID_BOOTCLASSPATH=\\\$3\" >> ${output};", + (configFile != null ? "-Dokbuck.configFile=\"\$SRCS\" " : "") + + (transformClass != null ? "-Dokbuck.transformClass=\"${transformClass}\" " : "") + - configFile != null ? "echo \"export CONFIG_FILE=\$SRCS\" >> ${output};" : "", - providerClass != null ? "echo \"export TRANSFORM_PROVIDER_CLASS=${providerClass}\" >> ${output};" : "", - transformClass != null ? "echo \"export TRANSFORM_CLASS=${transformClass}\" >> ${output};" : "", + " -cp \$(location ${TransformUtil.TRANSFORM_RULE}) ${RUNNER_MAIN_CLASS}\" >> ${output};", - "echo \"java -cp \$(location ${TransformUtil.TRANSFORM_RULE}) ${runnerMainClass}\" >> ${output};", "chmod +x ${output}"] + System.out.println("Generating rule for transform: ") + System.out.println(cmds) + return new GenRule(getTransformRuleName(target, options), input, cmds, true) } static getTransformRuleName(AndroidAppTarget target, Map options) { - String providerClass = options.get(OPT_TRANSFORM_PROVIDER_CLASS) - String transformClass = options.get(OPT_TRANSFORM_CLASS) - String name = providerClass != null ? providerClass : transformClass - return transform(name, target) + return transform(options.get(OPT_TRANSFORM_CLASS), target) } static String getTransformConfigRuleForFile(Project project, File config) { diff --git a/buildSrc/src/main/groovy/com/uber/okbuck/core/model/android/AndroidAppTarget.groovy b/buildSrc/src/main/groovy/com/uber/okbuck/core/model/android/AndroidAppTarget.groovy index de0cf58a1..2c69cfbf6 100644 --- a/buildSrc/src/main/groovy/com/uber/okbuck/core/model/android/AndroidAppTarget.groovy +++ b/buildSrc/src/main/groovy/com/uber/okbuck/core/model/android/AndroidAppTarget.groovy @@ -151,10 +151,6 @@ class AndroidAppTarget extends AndroidLibTarget { return (List>) getProp(okbuck.transform.transforms, []) } - String getTransformRunnerClass() { - return okbuck.transform.main - } - static String getPackedProguardConfig(File file) { ZipFile zipFile = new ZipFile(file) ZipEntry proguardEntry = zipFile.entries().find { diff --git a/buildSrc/src/main/java/com/uber/okbuck/core/util/TransformUtil.java b/buildSrc/src/main/java/com/uber/okbuck/core/util/TransformUtil.java index a1d6fd94f..82c21c0d8 100644 --- a/buildSrc/src/main/java/com/uber/okbuck/core/util/TransformUtil.java +++ b/buildSrc/src/main/java/com/uber/okbuck/core/util/TransformUtil.java @@ -5,23 +5,32 @@ import org.gradle.api.Project; +import java.io.File; import java.util.Collections; public final class TransformUtil { - private static final String CONFIGURATION_TRANSFORM = "transform"; public static final String TRANSFORM_CACHE = OkBuckGradlePlugin.DEFAULT_CACHE_PATH + "/transform"; - private static final String TRANSFORM_BUCK_FILE = "transform/BUCK_FILE"; + + private static final String CONFIGURATION_TRANSFORM = "transform"; + private static final String TRANSFORM_FOLDER = "transform/"; + private static final String TRANSFORM_BUCK_FILE = "BUCK_FILE"; + private static final String TRANSFORM_JAR = "transform-cli.jar"; public static final String TRANSFORM_RULE = "//" + TRANSFORM_CACHE + ":okbuck_transform"; private TransformUtil() { } public static void fetchTransformDeps(Project project) { - new DependencyCache("transform", + DependencyCache dependencyCache = new DependencyCache("transform", project.getRootProject(), TRANSFORM_CACHE, Collections.singleton(project.getConfigurations().getByName(CONFIGURATION_TRANSFORM)), - TRANSFORM_BUCK_FILE); + TRANSFORM_FOLDER + TRANSFORM_BUCK_FILE); + + FileUtil.copyResourceToProject( + TRANSFORM_FOLDER + TRANSFORM_BUCK_FILE, new File(dependencyCache.getCacheDir(), "BUCK")); + FileUtil.copyResourceToProject( + TRANSFORM_FOLDER + TRANSFORM_JAR, new File(dependencyCache.getCacheDir(), TRANSFORM_JAR)); } } diff --git a/buildSrc/src/main/java/com/uber/okbuck/extension/TransformExtension.java b/buildSrc/src/main/java/com/uber/okbuck/extension/TransformExtension.java index 9710f757e..4c6de0c6a 100644 --- a/buildSrc/src/main/java/com/uber/okbuck/extension/TransformExtension.java +++ b/buildSrc/src/main/java/com/uber/okbuck/extension/TransformExtension.java @@ -13,9 +13,4 @@ public class TransformExtension { * Stores the configuration per transform. Mapping is stored as target-[transforms]. */ public Map> transforms = new HashMap<>(); - - /** - * Transform runner class - */ - public String main; } diff --git a/buildSrc/src/main/resources/com/uber/okbuck/core/util/transform/transform-cli.jar b/buildSrc/src/main/resources/com/uber/okbuck/core/util/transform/transform-cli.jar new file mode 100644 index 000000000..df1bcae53 Binary files /dev/null and b/buildSrc/src/main/resources/com/uber/okbuck/core/util/transform/transform-cli.jar differ diff --git a/dependencies.gradle b/dependencies.gradle index e13ef134c..87eaf91fd 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,18 +1,21 @@ def versions = [ - butterKnifeVersion: '8.4.0', - daggerVersion : '2.8', - leakCanaryVersion : '1.5', - supportVersion : '25.0.0', + butterKnifeVersion : '8.4.0', + daggerVersion : '2.8', + leakCanaryVersion : '1.5', + supportVersion : '25.0.1', + androidPluginVersion : '2.3.0-beta3', ] def build = [ - androidPlugin : 'com.android.tools.build:gradle:2.3.0-beta3', + androidPlugin : "com.android.tools.build:gradle:${versions.androidPluginVersion}", + androidPluginApi : "com.android.tools.build:gradle-api:${versions.androidPluginVersion}", butterKnifePlugin: "com.jakewharton:butterknife-gradle-plugin:${versions.butterKnifeVersion}", commonsIo : 'commons-io:commons-io:2.5', commonsLang : 'commons-lang:commons-lang:2.6', mavenArtifact : 'org.apache.maven:maven-artifact:3.3.9', retrolambdaPlugin: 'me.tatarka:gradle-retrolambda:3.5.0', sqlDelightPlugin : 'com.squareup.sqldelight:gradle-plugin:0.5.1', + shadowJar : "com.github.jengelman.gradle.plugins:shadow:1.2.4", ] def buildConfig = [ @@ -64,6 +67,7 @@ def test = [ junit : 'junit:junit:4.12', mockito : 'org.mockito:mockito-core:1.10.19', robolectric : 'org.robolectric:robolectric:3.0', + assertj : 'org.assertj:assertj-core:3.6.1' ] ext.config = [ diff --git a/settings.gradle b/settings.gradle index 9d571e9c1..0c382177e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,3 +10,4 @@ include 'libraries:lintErrorLibrary' include 'libraries:parcelable' include 'libraries:robolectric-base' include 'plugin' +include 'transform-cli' diff --git a/transform-cli/build.gradle b/transform-cli/build.gradle new file mode 100644 index 000000000..dbcc9e35d --- /dev/null +++ b/transform-cli/build.gradle @@ -0,0 +1,34 @@ +apply plugin: "application" +apply plugin: "com.github.johnrengelman.shadow" + +jar.manifest.attributes( + 'Implementation-Title': 'Cli Transform', + 'Implementation-Version': '1.0', + 'Main-Class': 'com.ubercab.transform.CliTransform', + 'X-Compile-Source-JDK': '1.7', + 'X-Compile-Target-JDK': '1.7') + +mainClassName = "com.ubercab.transform.CliTransform" + +repositories { + jcenter() +} + +dependencies { + compileOnly gradleApi() + compile deps.support.annotations + compile deps.build.androidPluginApi + compile deps.build.commonsIo + testCompile deps.test.junit + testCompile deps.test.mockito + testCompile deps.test.assertj +} + +shadowJar { + baseName = 'transform-cli' + classifier = null + version = null + dependencies { + exclude(dependency("org.gradle:gradle:+")) + } +} \ No newline at end of file diff --git a/transform-cli/src/main/java/com/uber/transform/CliTransform.java b/transform-cli/src/main/java/com/uber/transform/CliTransform.java new file mode 100644 index 000000000..30e0b3b32 --- /dev/null +++ b/transform-cli/src/main/java/com/uber/transform/CliTransform.java @@ -0,0 +1,158 @@ +package com.uber.transform; + +import com.android.annotations.NonNull; +import com.android.build.api.transform.Transform; +import com.uber.transform.loader.SystemClassLoader; +import com.uber.transform.runner.TransformRunner; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +/** + * Main entry point for the cli application to apply the transform. + */ +public class CliTransform { + + @NonNull private static final String PROPERTY_IN_JARS_DIR = "okbuck.inJarsDir"; + @NonNull private static final String PROPERTY_OUT_JARS_DIR = "okbuck.outJarsDir"; + @NonNull private static final String PROPERTY_CONFIG_FILE = "okbuck.configFile"; + @NonNull private static final String PROPERTY_ANDROID_BOOTCLASSPATH = "okbuck.androidBootClasspath"; + @NonNull private static final String PROPERTY_TRANSFORM_DEPENDENCIES_FILE = "okbuck.transformJarsFile"; + @NonNull private static final String PROPERTY_TRANSFORM_CLASS = "okbuck.transformClass"; + @NonNull private static final String DEPENDENCIES_SEPARATOR = ":"; + + private CliTransform() { } + + /** + * Main. + * + * @param args arguments. + */ + public static void main(@NonNull String[] args) { + if (args.length != 0) { + StringBuilder sb = new StringBuilder(); + sb.append("No argument is expected. All parameters should be passed through java system properties.\n"); + sb.append(PROPERTY_IN_JARS_DIR + " : jars input directory\n"); + sb.append(PROPERTY_OUT_JARS_DIR + " : jars output directory\n"); + sb.append(PROPERTY_CONFIG_FILE + " : configuration file\n"); + sb.append(PROPERTY_ANDROID_BOOTCLASSPATH + " : android classpath\n"); + sb.append(PROPERTY_TRANSFORM_DEPENDENCIES_FILE + " : transform dependencies [optional]\n"); + sb.append(PROPERTY_TRANSFORM_CLASS + " : full qualified name for transform class\n"); + throw new IllegalArgumentException(sb.toString()); + } + + //Loading transform dependencies in this class loader to ensure this application to work properly + final String[] postProcessDependencies = + readDependenciesFileFromSystemPropertyVar(PROPERTY_TRANSFORM_DEPENDENCIES_FILE); + if (postProcessDependencies.length > 0) { + new SystemClassLoader().loadJarFiles(postProcessDependencies); + } + + //Passing the dependencies to the transform runner. + main(new TransformRunnerProvider() { + @Override + public TransformRunner provide() { + + //Reading config file. + String configFilePath = System.getProperty(PROPERTY_CONFIG_FILE); + + //Reading input jar dir + String inJarsDir = System.getProperty(PROPERTY_IN_JARS_DIR); + + //Reading output jar dir + String outJarsDir = System.getProperty(PROPERTY_OUT_JARS_DIR); + + //Reading android classpaths + String[] androidClasspaths = readDependenciesFromSystemPropertyVar(PROPERTY_ANDROID_BOOTCLASSPATH); + + //Creating TransformRunner class + try { + + Class transformClass; + String transformClassName = System.getProperty(PROPERTY_TRANSFORM_CLASS); + if (transformClassName != null) { + transformClass = (Class) Class.forName(transformClassName); + } else { + throw new IllegalArgumentException("No transform class defined."); + } + + TransformRunner runner = new TransformRunner( + configFilePath, inJarsDir, outJarsDir, androidClasspaths, transformClass); + + return runner; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + + /** + * Main method for testing. + * + * @param provider a provider for the transform runner. + */ + static void main(@NonNull TransformRunnerProvider provider) { + try { + provider.provide().runTransform(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Reads a dependency file from the path contained in a java system property. + * Dependency file contains jar file paths all colon separated. + * + * @param property the property with the file path to read. + * @return an array with all the dependency jars. + */ + @NonNull + static String[] readDependenciesFileFromSystemPropertyVar(String property) { + String envVarFilePath = System.getProperty(property); + if (envVarFilePath != null && envVarFilePath.length() > 0) { + try { + //This part needs to be written in vanilla java, since no dependency is available here. + BufferedReader reader = new BufferedReader(new FileReader(new File(envVarFilePath))); + String line = reader.readLine(); + reader.close(); + String[] dependencies = line.split(DEPENDENCIES_SEPARATOR); + return dependencies; + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + return new String[0]; + } + } + + /** + * Reads the dependencies from a java system property. + * Dependency file paths should all be colon separated. + * + * @param property the property with the dependencies. + * @return an array with all the dependency jars. + */ + @NonNull + static String[] readDependenciesFromSystemPropertyVar(@NonNull String property) { + String depsEnvVar = System.getProperty(property); + return depsEnvVar != null && depsEnvVar.length() > 0 + ? depsEnvVar.split(DEPENDENCIES_SEPARATOR) + : new String[0]; + } + + /** + * A provider for transforms, used for mainly for testing. + */ + interface TransformRunnerProvider { + + /** + * Returns the transform runner. + * + * @return the transform runner. + */ + TransformRunner provide(); + } +} diff --git a/transform-cli/src/main/java/com/uber/transform/builder/JarsTransformOutputProvider.java b/transform-cli/src/main/java/com/uber/transform/builder/JarsTransformOutputProvider.java new file mode 100644 index 000000000..bd100278b --- /dev/null +++ b/transform-cli/src/main/java/com/uber/transform/builder/JarsTransformOutputProvider.java @@ -0,0 +1,86 @@ +package com.uber.transform.builder; + +import com.android.annotations.NonNull; +import com.android.build.api.transform.Format; +import com.android.build.api.transform.QualifiedContent; +import com.android.build.api.transform.TransformOutputProvider; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Set; + +/** + * A {@link TransformOutputProvider} providing jar outputs based on a given output folder. + */ +public class JarsTransformOutputProvider implements TransformOutputProvider { + + @NonNull private final File outputFolder; + @NonNull private final String[] outputFolderParts; + + /** + * Constructor. + */ + public JarsTransformOutputProvider(@NonNull File outputFolder) { + this.outputFolder = outputFolder; + this.outputFolderParts = outputFolder.getAbsolutePath().split(File.separator); + } + + @Override + public void deleteAll() throws IOException { + FileUtils.deleteDirectory(outputFolder); + outputFolder.mkdirs(); + } + + @Override + @NonNull + public File getContentLocation( + @NonNull String name, + @NonNull Set types, + @NonNull Set scopes, + @NonNull Format format) { + + //Just a temp directory not to be used, to make the transform happy. + if (format == Format.DIRECTORY) { + return FileUtils.getTempDirectory(); + } + + /** + * For jars, the name is actually the absolute path. + * The goal here is to calculate the absolute output path starting from the input one. Since The output path + * will be different only in the last folder (replaced from the base output folder), it's possible to exclude + * from the input path the parts in common plus the last folder (i.e. all the parts of the output folder). + * + * Example of input path: + * ...mobile/okbuck/buck-out/bin/app + * /java_classes_preprocess_in_bin_prodDebug/buck-out/gen/.okbuck/cache/__app.rxscreenshotdetector-release + * .aar#aar_prebuilt_jar__/classes.jar + * + * Example of output base folder: + * ...mobile/okbuck/buck-out/bin/app/ + * java_classes_preprocess_out_bin_prodDebug/ + * + * Example of output path: + * ...mobile/okbuck/buck-out/bin/app + * /java_classes_preprocess_out_bin_prodDebug/buck-out/gen/.okbuck/cache/__app.rxscreenshotdetector-release + * .aar#aar_prebuilt_jar__/classes.jar + */ + + String[] nameParts = name.split(File.separator); + LinkedList baseFolderParts = new LinkedList(Arrays.asList(nameParts)); + for (int i = 0; i < outputFolderParts.length; i++) { + baseFolderParts.removeFirst(); + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < baseFolderParts.size(); i++) { + sb.append(baseFolderParts.get(i)); + if (i != baseFolderParts.size() - 1) { + sb.append(File.separator); + } + } + return new File(outputFolder, sb.toString()); + } +} diff --git a/transform-cli/src/main/java/com/uber/transform/builder/TransformInputBuilder.java b/transform-cli/src/main/java/com/uber/transform/builder/TransformInputBuilder.java new file mode 100644 index 000000000..0179097c4 --- /dev/null +++ b/transform-cli/src/main/java/com/uber/transform/builder/TransformInputBuilder.java @@ -0,0 +1,212 @@ +package com.uber.transform.builder; + +import android.support.annotation.NonNull; + +import com.android.build.api.transform.DirectoryInput; +import com.android.build.api.transform.JarInput; +import com.android.build.api.transform.Status; +import com.android.build.api.transform.TransformInput; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A builder for {@link TransformInput}. + */ +public class TransformInputBuilder { + + @NonNull private static final String DOT_JAR = ".jar"; + + @NonNull private final LinkedList jarInputs; + @NonNull private final LinkedList directoryInputs; + + /** + * Constructor. + */ + public TransformInputBuilder() { + this.jarInputs = new LinkedList<>(); + this.directoryInputs = new LinkedList<>(); + } + + /** + * Adds a jar input for this transform input. + * + * @param file the file of the jar input. + * @return this instance of the builder. + */ + @NonNull + public TransformInputBuilder addJarInput(@NonNull File file) { + if (file.exists()) { + this.jarInputs.add(new FileJarInput(file)); + System.out.println("Adding dependency jar: " + file.getAbsolutePath()); + } else { + System.out.println("Specified jar input doesn't exist: " + file.getAbsolutePath()); + } + return this; + } + + /** + * Adds a jar input folder for this transform input. All the jars found will be added to the class path. + * + * @param folder the folder of the jars input. + * @return this instance of the builder. + */ + @NonNull + public TransformInputBuilder addJarInputFolder(@NonNull File folder) { + File[] listFiles = folder.listFiles(); + if (listFiles != null) { + for (File file : listFiles) { + if (file.isDirectory()) { + addJarInputFolder(file); + } else { + if (!file.getAbsolutePath().endsWith(DOT_JAR)) { + continue; + } + addJarInput(file); + } + } + } + return this; + } + + /** + * Adds multiple jar inputs for this transform input. + * + * @param filePaths the paths of the jars. + * @return this instance of the builder. + */ + @NonNull + public TransformInputBuilder addJarInput(@NonNull String... filePaths) { + return addJarInput(Arrays.asList(filePaths)); + } + + /** + * Adds multiple jar inputs for this transform input. + * + * @param filePaths the paths of the jars. + * @return this instance of the builder. + */ + @NonNull + public TransformInputBuilder addJarInput(@NonNull List filePaths) { + for (String filePath : filePaths) { + addJarInput(new File(filePath)); + } + return this; + } + + /** + * Adds a directory input for this transform input. + * + * @param file the file of the directory input. + * @return this instance of the builder. + */ + @NonNull + public TransformInputBuilder addDirectoryInput(@NonNull File file) { + if (!file.exists()) { + throw new IllegalArgumentException("Specified directory input doesn't exist: " + file.getAbsolutePath()); + } + this.directoryInputs.add(new FileDirectoryInput(file)); + return this; + } + + /** + * Builds the {@link TransformInput}. + * + * @return a new {@link TransformInput} with the specified jar and directories. + */ + @NonNull + public TransformInput build() { + return new TransformInput() { + @Override + public Collection getJarInputs() { + return jarInputs; + } + + @Override + public Collection getDirectoryInputs() { + return directoryInputs; + } + }; + } + + /** + * A {@link JarInput} with specified file. + */ + private static class FileJarInput implements JarInput { + + @NonNull private final File file; + + public FileJarInput(@NonNull File file) { + this.file = file; + } + + @Override + @NonNull + public File getFile() { + return file; + } + + @Override + public Status getStatus() { + throw new UnsupportedOperationException(); + } + + @Override + public String getName() { + return file.getAbsolutePath(); + } + + @Override + public Set getContentTypes() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getScopes() { + throw new UnsupportedOperationException(); + } + } + + /** + * A {@link DirectoryInput} with specified file. + */ + private static class FileDirectoryInput implements DirectoryInput { + + @NonNull private final File file; + + public FileDirectoryInput(@NonNull File file) { + this.file = file; + } + + @Override + @NonNull + public File getFile() { + return file; + } + + @Override + public String getName() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getContentTypes() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getScopes() { + throw new UnsupportedOperationException(); + } + + @Override + public Map getChangedFiles() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/transform-cli/src/main/java/com/uber/transform/builder/TransformInvocationBuilder.java b/transform-cli/src/main/java/com/uber/transform/builder/TransformInvocationBuilder.java new file mode 100644 index 000000000..ee41bf14b --- /dev/null +++ b/transform-cli/src/main/java/com/uber/transform/builder/TransformInvocationBuilder.java @@ -0,0 +1,154 @@ +package com.uber.transform.builder; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.android.build.api.transform.Context; +import com.android.build.api.transform.SecondaryInput; +import com.android.build.api.transform.TransformInput; +import com.android.build.api.transform.TransformInvocation; +import com.android.build.api.transform.TransformOutputProvider; + +import org.gradle.api.logging.LoggingManager; + +import java.io.File; +import java.util.Collection; +import java.util.LinkedList; + +/** + * A builder for {@link TransformInvocation}. + */ +public class TransformInvocationBuilder { + + @NonNull private final LinkedList inputs; + @NonNull private final LinkedList referencedInputs; + @Nullable private TransformOutputProvider outputProvider; + + /** + * Constructor. + */ + public TransformInvocationBuilder() { + this.inputs = new LinkedList<>(); + this.referencedInputs = new LinkedList<>(); + } + + /** + * Adds a new input. + * + * @param transformInput the input to add. + * @return this instance of the builder + */ + @NonNull + public TransformInvocationBuilder addInput(@NonNull TransformInput transformInput) { + this.inputs.add(transformInput); + return this; + } + + /** + * Adds a new input. + * + * @param transformInput the input to add. + * @return this instance of the builder + */ + @NonNull + public TransformInvocationBuilder addReferencedInput(@NonNull TransformInput transformInput) { + this.referencedInputs.add(transformInput); + return this; + } + + /** + * Sets the output provider. + * + * @param outputProvider the output provider. + * @return this instance of the builder + */ + @NonNull + public TransformInvocationBuilder setOutputProvider(@NonNull TransformOutputProvider outputProvider) { + this.outputProvider = outputProvider; + return this; + } + + /** + * Builds the {@link TransformInvocation}. + * + * @return a new {@link TransformInvocation} with the specified inputs. + */ + @NonNull + public TransformInvocation build() { + if (outputProvider == null) { + throw new IllegalArgumentException("Output provider needs to be specified."); + } + return new CustomTransformInvocation(inputs, referencedInputs, outputProvider); + } + + /** + * The {@link TransformInvocation} built by the builder. + */ + private static class CustomTransformInvocation implements TransformInvocation, Context { + + @NonNull + private final Collection inputs; + @NonNull + private final Collection referencedInputs; + @NonNull + private final TransformOutputProvider transformOutputProvider; + + public CustomTransformInvocation( + @NonNull Collection inputs, + @NonNull Collection referencedInputs, + @NonNull TransformOutputProvider transformOutputProvider) { + this.inputs = inputs; + this.referencedInputs = referencedInputs; + this.transformOutputProvider = transformOutputProvider; + } + + @Override + @NonNull + public Collection getInputs() { + return inputs; + } + + @Override + @NonNull + public Collection getReferencedInputs() { + return referencedInputs; + } + + @Override + @NonNull + public TransformOutputProvider getOutputProvider() { + return transformOutputProvider; + } + + @Override + public boolean isIncremental() { + return false; + } + + @Override + @NonNull + public Context getContext() { + return this; + } + + @Override + public Collection getSecondaryInputs() { + throw new UnsupportedOperationException(); + } + + @Override + public LoggingManager getLogging() { + throw new UnsupportedOperationException(); + } + + @Override + public File getTemporaryDir() { + throw new UnsupportedOperationException(); + } + + @Override + public String getPath() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/transform-cli/src/main/java/com/uber/transform/loader/SystemClassLoader.java b/transform-cli/src/main/java/com/uber/transform/loader/SystemClassLoader.java new file mode 100644 index 000000000..f0682a0e7 --- /dev/null +++ b/transform-cli/src/main/java/com/uber/transform/loader/SystemClassLoader.java @@ -0,0 +1,62 @@ +package com.uber.transform.loader; + +import com.android.annotations.NonNull; + +import java.io.File; +import java.io.FileNotFoundException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Extend the support for the system class loader to load additional dependencies at runtime. Note that internally + * it works using {@link ClassLoader#getSystemClassLoader()}, making accessible the `addURL` method through reflection. + */ +public class SystemClassLoader { + + @NonNull private final URLClassLoader systemClassLoader; + @NonNull private final Method addUrlMethod; + + /** + * Constructor. + */ + public SystemClassLoader() { + try { + this.systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); + this.addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + this.addUrlMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * Loads a new jar in the system class loader using the given path. + * + * @param jarFilePath the path of a jar file. + */ + public void loadJarFile(@NonNull String jarFilePath) { + try { + File file = new File(jarFilePath); + if (!file.exists()) { + throw new FileNotFoundException(file.getAbsolutePath()); + } + URL jarFileUrl = file.toURI().toURL(); + this.addUrlMethod.invoke(systemClassLoader, jarFileUrl); + System.out.println("Added dependency: " + jarFileUrl.toString()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Loads new jars in the system class loader using the given paths. + * + * @param jarFilePaths the paths of a jar file. + */ + public void loadJarFiles(@NonNull String[] jarFilePaths) { + for (String jarFilePath : jarFilePaths) { + this.loadJarFile(jarFilePath); + } + } +} diff --git a/transform-cli/src/main/java/com/uber/transform/runner/TransformRunner.java b/transform-cli/src/main/java/com/uber/transform/runner/TransformRunner.java new file mode 100644 index 000000000..b18f69b3a --- /dev/null +++ b/transform-cli/src/main/java/com/uber/transform/runner/TransformRunner.java @@ -0,0 +1,123 @@ +package com.uber.transform.runner; + +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import com.android.annotations.NonNull; +import com.android.build.api.transform.Transform; +import com.android.build.api.transform.TransformInput; +import com.android.build.api.transform.TransformInvocation; +import com.android.build.api.transform.TransformOutputProvider; +import com.uber.transform.builder.JarsTransformOutputProvider; +import com.uber.transform.builder.TransformInputBuilder; +import com.uber.transform.builder.TransformInvocationBuilder; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; + +/** + * Transform runner that instantiates the transform class and starts the transform invocation. + */ +public class TransformRunner { + + @NonNull private final File inputJarsDir; + @NonNull private final File outputJarsDir; + @Nullable private final File configFile; + @NonNull private final String[] androidClassPath; + @NonNull private final Class transformClass; + + /** + * Constructor. + * + * @param configFilePath the path to the config file. + * @param inJarsDir input jars folder. + * @param outJarsDir output jars folder. + * @param androidClassPath android classpath. + * @param transformClass class for the transform. + */ + public TransformRunner( + @Nullable String configFilePath, + @NonNull String inJarsDir, + @NonNull String outJarsDir, + @NonNull String[] androidClassPath, + @NonNull Class transformClass) { + + this.androidClassPath = androidClassPath; + this.transformClass = transformClass; + + //Check input class path. + this.inputJarsDir = getFile(inJarsDir, "Input jars dir not existing or invalid.", true); + System.out.println("Input jars dir: " + inputJarsDir.getAbsolutePath()); + + //Check output class path. + this.outputJarsDir = getFile(outJarsDir, "Output jars dir not existing or invalid.", true); + System.out.println("Output jars dir: " + outputJarsDir.getAbsolutePath()); + + //Reading config file. + if (configFilePath != null) { + this.configFile = getFile(configFilePath, "Config file not existing or invalid.", false); + System.out.println("Config file: " + configFile.getAbsolutePath()); + } else { + this.configFile = null; + } + } + + @NonNull + private static File getFile(String path, String errorMsg, boolean isFolder) { + File file = new File(path); + if (!file.exists() || isFolder != file.isDirectory()) { + throw new IllegalArgumentException(errorMsg); + } + return file; + } + + /** + * Starts the transform. Here the transform is instantiated through reflection. + * If the config file is specified, a constructor with a single parameter accepting a + * {@link File} is used, otherwise an empty constructor. + * + * @throws Exception for any exception happened during the transform process. + */ + public void runTransform() throws Exception { + + Transform transform = configFile != null + ? transformClass.getConstructor(File.class).newInstance(configFile) + : transformClass.newInstance(); + + TransformOutputProvider transformOutputProvider = + new JarsTransformOutputProvider(outputJarsDir); + + runTransform(transform, transformOutputProvider); + } + + @VisibleForTesting + final void runTransform( + @NonNull Transform transform, + @NonNull TransformOutputProvider outputProvider) throws Exception { + + //Cleaning output directory. + try { + FileUtils.deleteDirectory(outputJarsDir); + } catch (IOException ignored) { + //Do nothing. + } + outputJarsDir.mkdirs(); + + //Preparing Transform invocation. + TransformInput input = + new TransformInputBuilder().addJarInputFolder(inputJarsDir).build(); + TransformInput referencedInput = + new TransformInputBuilder().addJarInput(androidClassPath).build(); + + TransformInvocation invocation = new TransformInvocationBuilder() + .addInput(input) + .addReferencedInput(referencedInput) + .setOutputProvider(outputProvider) + .build(); + + //Running the transform invocation. + transform.transform(invocation); + } +} diff --git a/transform-cli/src/test/java/com/uber/transform/CliTransformTest.java b/transform-cli/src/test/java/com/uber/transform/CliTransformTest.java new file mode 100644 index 000000000..952208931 --- /dev/null +++ b/transform-cli/src/test/java/com/uber/transform/CliTransformTest.java @@ -0,0 +1,34 @@ +package com.uber.transform; + +import com.uber.transform.runner.TransformRunner; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class CliTransformTest { + + @Before + public void setup() throws Exception { + + } + + @Test(expected = IllegalArgumentException.class) + public void whenStarting_withOneArgument_shouldThrowException() throws Exception { + CliTransform.main(new String[] {"any"}); + } + + @Test + public void whenStarting_shouldRunTransform() throws Exception { + final TransformRunner runner = mock(TransformRunner.class); + CliTransform.main(new CliTransform.TransformRunnerProvider() { + @Override + public TransformRunner provide() { + return runner; + } + }); + verify(runner).runTransform(); + } +} diff --git a/transform-cli/src/test/java/com/uber/transform/builder/JarsTransformOutputProviderTest.java b/transform-cli/src/test/java/com/uber/transform/builder/JarsTransformOutputProviderTest.java new file mode 100644 index 000000000..a4bdeb264 --- /dev/null +++ b/transform-cli/src/test/java/com/uber/transform/builder/JarsTransformOutputProviderTest.java @@ -0,0 +1,45 @@ +package com.uber.transform.builder; + +import com.android.build.api.transform.Format; + +import org.assertj.core.util.Files; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JarsTransformOutputProviderTest { + + private static final String NAME = + "buck-out/bin/app/java_classes_preprocess_in_bin_prodDebug/buck-out/gen/.okbuck/cache/" + + "__app.rxscreenshotdetector-release.aar#aar_prebuilt_jar__/classes.jar"; + + private static final String OUTPUT = + "buck-out/bin/app/java_classes_preprocess_out_bin_prodDebug/"; + + private static final String EXPECTED_OUTPUT_JAR = + "buck-out/bin/app/java_classes_preprocess_out_bin_prodDebug/buck-out/gen/.okbuck/cache/" + + "__app.rxscreenshotdetector-release.aar#aar_prebuilt_jar__/classes.jar"; + + private File inputJarFile; + private File outputJarFile; + private File outputFolder; + private JarsTransformOutputProvider provider; + + @Before + public void setUp() throws Exception { + File baseFolder = Files.newTemporaryFolder(); + this.inputJarFile = new File(baseFolder, NAME); + this.outputJarFile = new File(baseFolder, EXPECTED_OUTPUT_JAR); + this.outputFolder = new File(baseFolder, OUTPUT); + this.provider = new JarsTransformOutputProvider(outputFolder); + } + + @Test + public void getContentLocation() throws Exception { + File output = provider.getContentLocation(inputJarFile.getAbsolutePath(), null, null, Format.JAR); + assertThat(output.getAbsolutePath()).isEqualTo(outputJarFile.getAbsolutePath()); + } +} diff --git a/transform-cli/src/test/java/com/uber/transform/builder/TransformInputBuilderTest.java b/transform-cli/src/test/java/com/uber/transform/builder/TransformInputBuilderTest.java new file mode 100644 index 000000000..28d2ade29 --- /dev/null +++ b/transform-cli/src/test/java/com/uber/transform/builder/TransformInputBuilderTest.java @@ -0,0 +1,61 @@ +package com.uber.transform.builder; + +import com.android.build.api.transform.DirectoryInput; +import com.android.build.api.transform.JarInput; +import com.android.build.api.transform.TransformInput; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TransformInputBuilderTest { + + private TransformInputBuilder builder; + + @Before + public void setup() throws Exception { + builder = new TransformInputBuilder(); + } + + @Test + public void addJarInput_whenAddFile_builtShouldReturnFile() throws Exception { + File file = getTmpFile("tmp"); + + TransformInput input = builder.addJarInput(file).build(); + assertThat(input.getJarInputs()).hasSize(1); + assertThat(input.getJarInputs().toArray(new JarInput[1])[0].getFile()).isEqualTo(file); + } + + @Test + public void addJarInput_whenAddPaths_builtShouldReturnCorrectPaths() throws Exception { + File file1 = getTmpFile("tmp1"); + File file2 = getTmpFile("tmp2"); + + TransformInput input = builder.addJarInput(file1.getAbsolutePath(), file2.getAbsolutePath()).build(); + assertThat(input.getJarInputs()).hasSize(2); + + JarInput[] jarInputs = input.getJarInputs().toArray(new JarInput[2]); + assertThat(jarInputs[0].getFile().getAbsolutePath()).isEqualTo(file1.getAbsolutePath()); + assertThat(jarInputs[1].getFile().getAbsolutePath()).isEqualTo(file2.getAbsolutePath()); + } + + @Test + public void addDirectoryInput_builtShouldReturnCorrectFile() throws Exception { + File file = getTmpFile("tmp"); + + TransformInput input = builder.addDirectoryInput(file).build(); + assertThat(input.getDirectoryInputs()).hasSize(1); + assertThat(input.getDirectoryInputs().toArray(new DirectoryInput[1])[0].getFile()).isEqualTo(file); + } + + private File getTmpFile(String prefix) throws IOException { + File file = File.createTempFile(prefix, null); + file.deleteOnExit(); + return file; + } + +} diff --git a/transform-cli/src/test/java/com/uber/transform/builder/TransformInvocationBuilderTest.java b/transform-cli/src/test/java/com/uber/transform/builder/TransformInvocationBuilderTest.java new file mode 100644 index 000000000..74b3633ca --- /dev/null +++ b/transform-cli/src/test/java/com/uber/transform/builder/TransformInvocationBuilderTest.java @@ -0,0 +1,53 @@ +package com.uber.transform.builder; + +import com.android.build.api.transform.TransformInput; +import com.android.build.api.transform.TransformInvocation; +import com.android.build.api.transform.TransformOutputProvider; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TransformInvocationBuilderTest { + + @Mock private TransformInput transformInput; + @Mock private TransformOutputProvider outputProvider; + + private TransformInvocationBuilder builder; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + builder = new TransformInvocationBuilder(); + } + + @Test + public void addInput_shouldAddAnInput() throws Exception { + TransformInvocation invocation = builder + .addInput(transformInput) + .setOutputProvider(outputProvider) + .build(); + assertThat(invocation.getInputs()).containsExactly(transformInput); + assertThat(invocation.getReferencedInputs()).isEmpty(); + assertThat(invocation.getOutputProvider()).isEqualTo(outputProvider); + } + + @Test + public void addReferencedInput_shouldAddReferencedInput() throws Exception { + TransformInvocation invocation = builder + .addReferencedInput(transformInput) + .setOutputProvider(outputProvider) + .build(); + assertThat(invocation.getInputs()).isEmpty(); + assertThat(invocation.getReferencedInputs()).containsExactly(transformInput); + assertThat(invocation.getOutputProvider()).isEqualTo(outputProvider); + } + + @Test(expected = IllegalArgumentException.class) + public void whenOutputProviderNotSet_shouldThrowException() throws Exception { + builder.build(); + } +} diff --git a/transform-cli/src/test/java/com/uber/transform/utils/TestUtils.java b/transform-cli/src/test/java/com/uber/transform/utils/TestUtils.java new file mode 100644 index 000000000..2e16ce530 --- /dev/null +++ b/transform-cli/src/test/java/com/uber/transform/utils/TestUtils.java @@ -0,0 +1,41 @@ +package com.uber.transform.utils; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +/** + * Utility methods for testing. + */ +public class TestUtils { + + private TestUtils() { } + + /** + * Returns a temporary file to be deleted at the end of the test. + * + * @param directory true whether the {@link File} is a directory, false otherwise. + * @param exists true whether the {@link File} should exist, false otherwise. If true an empty file will be + * created. + * @return the newly created file. + */ + public static File getTmpFile(boolean directory, boolean exists) { + File file; + try { + file = new File(FileUtils.getTempDirectory(), UUID.randomUUID().toString()); + if (exists) { + if (directory) { + file.mkdirs(); + } else { + file.createNewFile(); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + file.deleteOnExit(); + return file; + } +} diff --git a/update-transform-cli.sh b/update-transform-cli.sh new file mode 100755 index 000000000..71b2c851c --- /dev/null +++ b/update-transform-cli.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +./gradlew transform-cli:shadowJar && cp ./transform-cli/build/libs/transform-cli.jar ./plugin/src/main/resources/com/uber/okbuck/core/util/transform/transform-cli.jar