From 4eee262ca1b68238a4d9e3b26cc4bcde81471a5c Mon Sep 17 00:00:00 2001 From: Advay Mengle Date: Tue, 25 Aug 2015 17:00:29 -0700 Subject: [PATCH] Automatic dependency resolution. Dependency configuration happens in 2 phases: - Dependency conversion: This converts your compile and testCompile dependencies into equivalent j2objcTranslate and j2objcLink dependencies. Namely local jars are copied to j2objcTranslate, external Maven jars are converted into their 'sources' form and copied to j2objcTranslate, and projects are copied to j2objcLink (they don't need translation). This phase is optional and controlled by j2objcConfig.autoConfigureDeps - Dependency resolution: This phase converts j2objcTranslate and j2objcLink deps into actual j2objc commands. Any source jar on j2objcTranslate is added to translateSourcepaths with --build-closure. Any project on j2objcLink is added to translateClasspaths and has its final j2objc static library linked in to this project's objective c code. This phase always runs. If your dependencies are too complicated for the plugin to figure out in the first phase, keep autoConfigureDeps=false, and just add the appropriate projets, jars, and libraries here. Also adds system tests for both project and external Maven dependencies. #180; Fixes #41; Fixes #372 TESTED=yes --- .../j2objcgradle/DependencyConverter.groovy | 93 ++++++++++++++++ .../j2objcgradle/DependencyResolver.groovy | 96 ++++++++++++++++ .../j2objcgradle/J2objcConfig.groovy | 104 ++++++++++++------ .../j2objcgradle/J2objcPlugin.groovy | 15 +++ systemTests/multiProject1/sub2/build.gradle | 3 +- .../src/main/java/com/example/AntiCube.java | 6 +- .../test/java/com/example/AntiCubeTest.java | 2 +- 7 files changed, 283 insertions(+), 36 deletions(-) create mode 100644 src/main/groovy/com/github/j2objccontrib/j2objcgradle/DependencyConverter.groovy create mode 100644 src/main/groovy/com/github/j2objccontrib/j2objcgradle/DependencyResolver.groovy diff --git a/src/main/groovy/com/github/j2objccontrib/j2objcgradle/DependencyConverter.groovy b/src/main/groovy/com/github/j2objccontrib/j2objcgradle/DependencyConverter.groovy new file mode 100644 index 00000000..6d39ffcc --- /dev/null +++ b/src/main/groovy/com/github/j2objccontrib/j2objcgradle/DependencyConverter.groovy @@ -0,0 +1,93 @@ +package com.github.j2objccontrib.j2objcgradle + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.ExternalModuleDependency +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.artifacts.SelfResolvingDependency + +/** + * Converts `compile` and `testCompile` dependencies to their + * `j2objcTranslate` and `j2objcLink` equivalents. They will be resolved + * to appropriate `j2objc` constructs using DependencyResolver. + */ +@PackageScope +@CompileStatic +class DependencyConverter { + + final Project project + final J2objcConfig j2objcConfig + + // List of `group:name` + // TODO: Handle versioning. + static final List distLibDeps = [ + 'com.google.guava:guava', + 'junit:junit', + 'org.mockito:mockito-core', + 'com.google.j2objc:j2objc-annotations'] + + DependencyConverter(Project project, J2objcConfig j2objcConfig) { + this.project = project + this.j2objcConfig = j2objcConfig + } + + void configureAll() { + project.configurations.getByName('compile').dependencies.each { + visit(it) + } + project.configurations.getByName('testCompile').dependencies.each { + visit(it) + } + } + + protected void visit(Dependency dep) { + if (dep instanceof ProjectDependency) { + visitProjectDependency(dep as ProjectDependency) + } else if (dep instanceof SelfResolvingDependency) { + // File collections (ex. libs/*.jar) are one kind of SelfResolvingDependency. + visitSelfResolvingDependency(dep as SelfResolvingDependency) + } else if (dep instanceof ExternalModuleDependency) { + visitExternalModuleDependency(dep as ExternalModuleDependency) + } else { + visitGenericDependency(dep) + } + } + + protected void visitSelfResolvingDependency( + SelfResolvingDependency dep) { + project.logger.debug("j2objc dependency converter: Translating file dep: $dep") + project.configurations.getByName('j2objcTranslate').dependencies.add( + dep.copy()) + } + + protected void visitProjectDependency(ProjectDependency dep) { + project.logger.debug("j2objc dependency converter: Linking Project: $dep") + project.configurations.getByName('j2objcLink').dependencies.add( + dep.copy()) + } + + protected void visitExternalModuleDependency(ExternalModuleDependency dep) { + project.logger.debug("j2objc dependency converter: External module dep: $dep") + // If the dep is already in the j2objc dist, ignore it. + if (distLibDeps.contains("${dep.group}:${dep.name}".toString())) { + // TODO: A more correct method might be converting these into our own + // form of SelfResolvingDependency that specifies which j2objc dist lib + // to use. + project.logger.debug("-- Skipped: $dep") + return + } + project.logger.debug("-- Copied as source: $dep") + String group = dep.group == null ? '' : dep.group + String version = dep.version == null ? '' : dep.version + // TODO: Make this less fragile. What if sources don't exist for this artifact? + project.dependencies.add('j2objcTranslate', "${group}:${dep.name}:${version}:sources") + } + + protected void visitGenericDependency(Dependency dep) { + project.logger.debug("j2objc dependency converter: Generic dep: $dep") + project.configurations.getByName('j2objcTranslate').dependencies.add( + dep.copy()) + } +} diff --git a/src/main/groovy/com/github/j2objccontrib/j2objcgradle/DependencyResolver.groovy b/src/main/groovy/com/github/j2objccontrib/j2objcgradle/DependencyResolver.groovy new file mode 100644 index 00000000..d2d9eb0a --- /dev/null +++ b/src/main/groovy/com/github/j2objccontrib/j2objcgradle/DependencyResolver.groovy @@ -0,0 +1,96 @@ +package com.github.j2objccontrib.j2objcgradle +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.gradle.api.InvalidUserDataException +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.artifacts.SelfResolvingDependency +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.tasks.bundling.AbstractArchiveTask + +/** + * Resolves `j2objcTranslate` and 'j2objcLink' dependencies into their `j2objc` constructs. + */ +@PackageScope +@CompileStatic +class DependencyResolver { + + final Project project + final J2objcConfig j2objcConfig + + DependencyResolver(Project project, J2objcConfig j2objcConfig) { + this.project = project + this.j2objcConfig = j2objcConfig + } + + void configureAll() { + project.configurations.getByName('j2objcTranslate').each { File it -> + // These are the resolved files, NOT the dependencies themselves. + visitTranslateFile(it) + } + project.configurations.getByName('j2objcLink').dependencies.each { + visitLink(it) + } + } + + protected void visitTranslateFile(File depFile) { + j2objcConfig.translateSourcepaths(depFile.absolutePath) + j2objcConfig.enableBuildClosure() + } + + protected void visitLink(Dependency dep) { + if (dep instanceof ProjectDependency) { + visitLinkProjectDependency((ProjectDependency) dep) + } else if (dep instanceof SelfResolvingDependency) { + visitLinkSelfResolvingDependency((SelfResolvingDependency) dep) + } else { + visitLinkGenericDependency(dep) + } + } + + protected void visitLinkSelfResolvingDependency( + SelfResolvingDependency dep) { + // TODO: handle native prebuilt libraries as files. + throw new UnsupportedOperationException("Cannot automatically link J2ObjC dependency: $dep") + } + + protected void visitLinkProjectDependency(ProjectDependency dep) { + Project beforeProject = dep.dependencyProject + // We need to have j2objcConfig on the beforeProject configured first. + project.evaluationDependsOn beforeProject.path + + if (!beforeProject.plugins.hasPlugin(JavaPlugin)) { + String message = "$beforeProject is not a Java project.\n" + + "dependsOnJ2ObjcLib can only automatically resolve a\n" + + "dependency on a Java project also converted using the\n" + + "J2ObjC Gradle Plugin." + throw new InvalidUserDataException(message) + } + + if (!beforeProject.plugins.hasPlugin(J2objcPlugin)) { + String message = "$beforeProject does not use the J2ObjC Gradle Plugin.\n" + + "dependsOnJ2objcLib can be used only with another project that\n" + + "itself uses the J2ObjC Gradle Plugin." + throw new InvalidUserDataException(message) + } + + // Build and test the java/objc libraries and the objc headers of + // the other project first. + // Since we assert the presence of the J2objcPlugin above, + // we are guaranteed that the java plugin, which creates the jar task, + // is also present. + project.tasks.getByName('j2objcPreBuild').dependsOn { + return [beforeProject.tasks.getByName('j2objcBuild'), + beforeProject.tasks.getByName('jar')] + } + AbstractArchiveTask jarTask = beforeProject.tasks.getByName('jar') as AbstractArchiveTask + project.logger.debug("$project:j2objcTranslate must use ${jarTask.archivePath}") + j2objcConfig.translateClasspaths += jarTask.archivePath.absolutePath + j2objcConfig.nativeCompilation.dependsOnJ2objcLib(beforeProject) + } + + protected void visitLinkGenericDependency(Dependency dep) { + throw new UnsupportedOperationException("Cannot automatically link J2ObjC dependency: $dep") + } +} diff --git a/src/main/groovy/com/github/j2objccontrib/j2objcgradle/J2objcConfig.groovy b/src/main/groovy/com/github/j2objccontrib/j2objcgradle/J2objcConfig.groovy index 8ac80e4b..0e0c9751 100644 --- a/src/main/groovy/com/github/j2objccontrib/j2objcgradle/J2objcConfig.groovy +++ b/src/main/groovy/com/github/j2objccontrib/j2objcgradle/J2objcConfig.groovy @@ -15,17 +15,15 @@ */ package com.github.j2objccontrib.j2objcgradle - import com.github.j2objccontrib.j2objcgradle.tasks.Utils import com.google.common.annotations.VisibleForTesting import groovy.transform.CompileStatic +import groovy.transform.TypeCheckingMode import org.gradle.api.InvalidUserDataException import org.gradle.api.Project import org.gradle.api.Task -import org.gradle.api.tasks.bundling.AbstractArchiveTask import org.gradle.api.tasks.util.PatternSet import org.gradle.util.ConfigureUtil - /** * j2objcConfig is used to configure the plugin with the project's build.gradle. * @@ -184,6 +182,17 @@ class J2objcConfig { appendArgs(this.translateArgs, 'translateArgs', translateArgs) } + /** + * Enables --build-closure, which translates classes referenced from the + * list of files passed for translation, using the + * {@link #translateSourcepaths}. + */ + void enableBuildClosure() { + if (!translateArgs.contains('--build-closure')) { + translateArgs('--build-closure') + } + } + /** * Local jars for translation e.g.: "lib/json-20140107.jar", "lib/somelib.jar". * This will be added to j2objc as a '-classpath' argument. @@ -227,6 +236,29 @@ class J2objcConfig { // the build breaks, you need to do a clean build. boolean UNSAFE_incrementalBuildClosure = false + /** + * Experimental functionality to automatically configure dependencies. + * Consider you have dependencies like: + *
+     * dependencies {
+     *     compile project(':peer1')                  // type (1)
+     *     compile 'com.google.code.gson:gson:2.3.1'  // type (3)
+     *     compile 'com.google.guava:guava:18.0'      // type (2)
+     *     testCompile 'junit:junit:4.11'             // type (2)
+     * }
+     * 
+ * Dependencies of type (1) will be handled with {@link #dependsOnJ2objcLib(org.gradle.api.Project)}. + * Dependencies of type (2) are already linked to with {@link #translateJ2objcLibs} and ignored. + * Dependencies of type (3) will be added to a special `j2objcSource` configuration, downloaded + * with their source where possible, and translated using `--build-closure`. + * Because test and production code are not separated, only testCompile dependencies of type (2) + * will be handled - the plugin will not link new libraries in to your production code. + * Dependencies must be fully specified before you call finalConfigure(). + *

+ * This will become the default when stable in future releases. + */ + boolean autoConfigureDeps = false + /** * Additional libraries that are part of the j2objc distribution. *

@@ -312,32 +344,13 @@ class J2objcConfig { * translateSourcepaths or translateClasspaths, respectively. Calling this method * is preferable and sufficient. */ - // TODO: Do this automatically based on project dependencies. + // TODO: Phase this API out, and have J2ObjC-applied project dependencies controlled + // solely via `j2objcLink` configuration. + @CompileStatic(TypeCheckingMode.SKIP) void dependsOnJ2objcLib(Project beforeProject) { - // We need to have j2objcConfig on the beforeProject configured first. - project.evaluationDependsOn beforeProject.path - - if (!beforeProject.plugins.hasPlugin(J2objcPlugin)) { - String message = "$beforeProject does not use the J2ObjC Gradle Plugin.\n" + - "dependsOnJ2objcLib can be used only with another project that\n" + - "itself uses the J2ObjC Gradle Plugin." - throw new InvalidUserDataException(message) + project.dependencies { + j2objcLink beforeProject } - - // Build and test the java/objc libraries and the objc headers of - // the other project first. - // Since we assert the presence of the J2objcPlugin above, - // we are guaranteed that the java plugin, which creates the jar task, - // is also present. - project.tasks.getByName('j2objcPreBuild').dependsOn { - return [beforeProject.tasks.getByName('j2objcBuild'), - beforeProject.tasks.getByName('jar')] - } - AbstractArchiveTask jarTask = beforeProject.tasks.getByName('jar') as AbstractArchiveTask - project.logger.debug("$project:j2objcTranslate must use ${jarTask.archivePath}") - translateClasspaths += jarTask.archivePath.absolutePath - - nativeCompilation.dependsOnJ2objcLib(beforeProject) } /** @@ -538,22 +551,47 @@ class J2objcConfig { protected boolean finalConfigured = false /** - * Configures the native build using. Must be called at the very + * Configures the j2objc build. Must be called at the very * end of your j2objcConfig block. */ - // TODO: When Gradle makes it possible to modify a native build config - // after initial creation, we can remove this, and have methods on this object - // mutate the existing native model { } block. See: - // https://discuss.gradle.org/t/problem-with-model-block-when-switching-from-2-2-1-to-2-4/9937 @VisibleForTesting void finalConfigure() { - nativeCompilation.apply(project.file("${project.buildDir}/j2objcSrcGen")) + validateConfiguration() + // Conversion of compile and testCompile dependencies occurs optionally. + if (autoConfigureDeps) { + convertDeps() + } + // Resolution of j2objcTranslateSource dependencies occurs always. + // This lets users turn off autoConfigureDeps but manually set j2objcTranslateSource. + resolveDeps() + configureNativeCompilation() + configureTaskState() finalConfigured = true + } + protected void validateConfiguration() { assert destLibDir != null assert destSrcMainDir != null assert destSrcTestDir != null + } + + protected void configureNativeCompilation() { + // TODO: When Gradle makes it possible to modify a native build config + // after initial creation, we can remove this, and have methods on this object + // mutate the existing native model { } block. See: + // https://discuss.gradle.org/t/problem-with-model-block-when-switching-from-2-2-1-to-2-4/9937 + nativeCompilation.apply(project.file("${project.buildDir}/j2objcSrcGen")) + } + + protected void convertDeps() { + new DependencyConverter(project, this).configureAll() + } + + protected void resolveDeps() { + new DependencyResolver(project, this).configureAll() + } + protected void configureTaskState() { // Disable only if explicitly present and not true. boolean debugEnabled = Boolean.parseBoolean(Utils.getLocalProperty(project, 'debug.enabled', 'true')) boolean releaseEnabled = Boolean.parseBoolean(Utils.getLocalProperty(project, 'release.enabled', 'true')) diff --git a/src/main/groovy/com/github/j2objccontrib/j2objcgradle/J2objcPlugin.groovy b/src/main/groovy/com/github/j2objccontrib/j2objcgradle/J2objcPlugin.groovy index bfa22600..46a652d4 100644 --- a/src/main/groovy/com/github/j2objccontrib/j2objcgradle/J2objcPlugin.groovy +++ b/src/main/groovy/com/github/j2objccontrib/j2objcgradle/J2objcPlugin.groovy @@ -85,6 +85,21 @@ class J2objcPlugin implements Plugin { // specified in j2objcConfig (or associated defaults in J2objcConfig). File j2objcSrcGenDir = file("${buildDir}/j2objcSrcGen") + configurations { + // When j2objcConfig.autoConfigureDeps is true, this configuration + // will have source paths automatically added to it. You can add + // *source* JARs/directories yourself as well. + j2objcTranslate { + description = 'J2ObjC dependencies that need to be ' + + 'transitively translated via --build-closure' + } + // Currently, we can only handle Project dependencies here. + j2objcLink { + description = 'J2ObjC dependencies that need to be ' + + 'linked into the final library, and do not need translation' + } + } + // Produces a modest amount of output logging.captureStandardOutput LogLevel.INFO diff --git a/systemTests/multiProject1/sub2/build.gradle b/systemTests/multiProject1/sub2/build.gradle index e0336495..b188fa0b 100644 --- a/systemTests/multiProject1/sub2/build.gradle +++ b/systemTests/multiProject1/sub2/build.gradle @@ -24,12 +24,13 @@ repositories { dependencies { compile project(':sub1') compile 'com.google.guava:guava:17.0' + compile 'com.google.code.gson:gson:2.3.1' testCompile 'junit:junit:4.12' testCompile "org.mockito:mockito-core:1.9.5" } j2objcConfig { - dependsOnJ2objcLib project(':sub1') + autoConfigureDeps true finalConfigure() } diff --git a/systemTests/multiProject1/sub2/src/main/java/com/example/AntiCube.java b/systemTests/multiProject1/sub2/src/main/java/com/example/AntiCube.java index b94eee85..e3e4b356 100644 --- a/systemTests/multiProject1/sub2/src/main/java/com/example/AntiCube.java +++ b/systemTests/multiProject1/sub2/src/main/java/com/example/AntiCube.java @@ -16,6 +16,9 @@ package com.example; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + public class AntiCube extends Cube { public AntiCube(int dimension) { @@ -24,7 +27,8 @@ public AntiCube(int dimension) { @Override public String toString() { - return String.format("[AntiCube %d]", getDimension()); + Gson gson = new GsonBuilder().create(); + return gson.toJson(this); } @Override diff --git a/systemTests/multiProject1/sub2/src/test/java/com/example/AntiCubeTest.java b/systemTests/multiProject1/sub2/src/test/java/com/example/AntiCubeTest.java index b8feb129..66d17b38 100644 --- a/systemTests/multiProject1/sub2/src/test/java/com/example/AntiCubeTest.java +++ b/systemTests/multiProject1/sub2/src/test/java/com/example/AntiCubeTest.java @@ -23,7 +23,7 @@ public class AntiCubeTest { @Test public void testToString() { - Assert.assertEquals("[AntiCube 7]", new AntiCube(7).toString()); + Assert.assertEquals("{\"dimension\":7}", new AntiCube(7).toString()); } @Test