Skip to content

Commit

Permalink
Automatic dependency resolution.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
advayDev1 committed Aug 26, 2015
1 parent 08ccb5a commit a9432a2
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -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<String> 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())
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
* <pre>
* 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)
* }
* </pre>
* 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().
* <p/>
* This will become the default when stable in future releases.
*/
boolean autoConfigureDeps = false

/**
* Additional libraries that are part of the j2objc distribution.
* <p/>
Expand Down Expand Up @@ -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)
}

// 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')]
project.dependencies {
j2objcLink beforeProject
}
AbstractArchiveTask jarTask = beforeProject.tasks.getByName('jar') as AbstractArchiveTask
project.logger.debug("$project:j2objcTranslate must use ${jarTask.archivePath}")
translateClasspaths += jarTask.archivePath.absolutePath

nativeCompilation.dependsOnJ2objcLib(beforeProject)
}

/**
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -623,4 +661,12 @@ class J2objcConfig {
}
}
}

@VisibleForTesting
void testingOnlyPrepConfigurations() {
// When testing we don't always want to apply the entire plugin
// before calling finalConfigure.
project.configurations.create('j2objcTranslate')
project.configurations.create('j2objcLink')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ class J2objcPlugin implements Plugin<Project> {
// 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

Expand Down
Loading

0 comments on commit a9432a2

Please sign in to comment.