j2objc plugin - Java to Objective-C transpiler - Feedback Requested

Hi All, I’d like to solicit feedback on a Gradle plugin that I’m building for the j2objc project, which transpiles (translates) code from Java to Objective-C. The technology was used recently in “Inbox”, the new app from Google. It allowed them to share 70% of the code across Android, iPhone and the web: http://arstechnica.com/information-technology/2014/11/how-google-inbox-shares-70-of-its-code-across-android-ios-and-the-web/

Gist Link on GitHub if you’d like to suggest changes: https://gist.github.com/brunobowden/58d6e311ab96760fc371

The plugin is meant to make it easier for myself and others to use the system. It’s still a work in progress but I would appreciate feedback on style, Gradle conventions and anything else you feel would improve the plugin. At the moment it is only a single script but I want to migrate it to a jar, so that it can include some necessary binaries but can be setup with only a few lines in your build.gradle file. I’ve already done extensive refactoring myself, from dealing with << (broke my code and breaks the forum if surrounded by “”) and discovering “project.afterEvaluate” which greatly simplified the code (thanks Peter for posting about this). In general I’ve been enjoying the technology a great deal, though learning Gradle and Groovy at the same time, is quite a steep learning curve. When writing new code, I do it as if I was writing Python then consult the docs to figure out the Java syntax I need :wink:

My questions are scattered through the code as TODOs but here I’ve highlighted the main questions:

Main Questions: 1) Naming conventions on settings and task names? Currently this is “j2objcConfig” for the extension object and for the tasks: “j2objcTranslate”, “j2objcCompile” & “j2objcTest” 2) What’s the best way to package a plugin along with binaries? Pointer to documentation would be sufficient. 3) Android Studio IDE better handles “task Named(type:Exec)” rather than "tasks.create(“Named”, type:Exec). Can I do that here? 4) Should I be using the java plugin’s sourceSets to locate all the project java files? How would I do that? 5) How can I find if the project is using junit and if so, then what version? 6) Anything else you think I should know.

Thanks for a wonderful tool and looking forward to your feedback.

j2objc.gradle:

// TODO(bruno): 'apply' should be done in build.gradle, move there as this becomes a proper plugin
apply plugin: j2objc
    class J2objcPluginExtension {
    boolean noPackageDirectories = false
    String j2objcHome = null
    String outputDir = null
}
    class j2objc implements Plugin<Project> {
      void apply(Project project) {
          project.extensions.create("j2objcConfig", J2objcPluginExtension)
          // TODO(bruno): split out separate 'main' and 'test' tasks ??
          // Wait so that project.j2objcConfig settings have been applied
        project.afterEvaluate {
              // Setup basic paths
            def j2objcHome = System.getenv()['J2OBJC_HOME']
            if (project.j2objcConfig.j2objcHome) {
                j2objcHome = project.j2objcConfig.j2objcHome
            }
            def j2objcBin = project.file(j2objcHome + '/j2objc').path
            def j2objccBin = project.file(j2objcHome + '/j2objcc').path
              def outputDir = project.file(project.buildDir.path + '/j2objc').path
            if (project.j2objcConfig.outputDir) {
                outputDir = project.file(project.j2objcConfig.outputDir).path
            }
              def testRunnerFile = project.file(outputDir + '/testrunner')
              // TODO(bruno): possible to do??: "task j2objcTranslate(type:Exec) {"
            project.tasks.create(name: 'j2objcTranslate', type:Exec) {
                  description 'Translates all the java source files in to Objective-C using j2objc'
                  // TODO(bruno): can / should this use sourceSets produced by java plugin?
                inputs.files project.files(project.fileTree(
                        dir: project.projectDir, includes: ['**/*.java']))
                  outputs.files project.files(inputs.files.collect { file ->
                    def replaceRegex = project.j2objcConfig.noPackageDirectories ?
// REGEX's REMOVED - they were interfering with the forum layout
                    def path = project.relativePath(file).replaceFirst(
                            replaceRegex, project.j2objcConfig.outputDir + '/')
                    return [path.replace('.java', '.h'), path.replace('.java', '.m')]
                }.flatten())
                    executable = j2objcBin
                  args '-d', outputDir
                  // Source path is the same for main and test targets
                args '-sourcepath', project.file('src/main/java').path
                  // TODO(bruno): use junit version specified in j2objc plugin jar
                // TODO(bruno): warn if different version than testCompile
                args '-classpath', project.file(j2objcHome + '/lib/junit-4.10.jar').path
                  if (project.j2objcConfig.noPackageDirectories) {
                    args '--no-package-directories'
                }
                //
  TODO(bruno): add these as options?
                //
  args ('--prefixes', project.file('src/main/resources/prefixes.properties').path)
                //
  args '--mapping', project.file('mapping.properties').path
                //
  args '-use-arc'
                inputs.files.each { file ->
                    args file.path
                }
            }
                project.tasks.create(name: 'j2objcCompile', type:Exec, dependsOn: 'j2objcTranslate') {
                  description 'Compiles the j2objc generated Objective-C code in to testrunner binary'
                  inputs.files project.files(project.fileTree(
                        dir: outputDir, includes: ['**/*.h', '**/*.m']))
                outputs.file testRunnerFile
                  executable j2objccBin
                  //workingDir buildDir
                args ('-I' + outputDir)
                args '-ObjC', '-ljunit'
                args '-o ', testRunnerFile.path
                project.fileTree(dir: outputDir, include: '**/*.m').files.each { i ->
                    args i.path
                }
            }
                // TODO(bruno): name compileTestJ2objc to match compileTestJava convention?
            project.tasks.create(name: 'j2objcTest', type:Exec, dependsOn: 'j2objcCompile') {
                  description 'Runs all tests in the generated Objective-C code'
                  inputs.file testRunnerFile
// weirdly this was causing a break in the post
                  // TODO(bruno): what is the output of tests? How does Gradle track successful tests?
                  // Generates: com.example.dir1.dir2.ClassTest
                def javaTests = project.files(project.fileTree(
                        dir: project.projectDir, includes: ['**/*Test.java']))
                def tests = javaTests.collect { file ->
                    return [project.relativePath(file)
                                    .replace('src/test/java/', '')
                                    .replace('/', '.')
                                    .replace('.java', '')]
                }.flatten()
                  executable testRunnerFile.path
                  args 'org.junit.runner.JUnitCore'
                args tests
            }
        }
    }
}

ad 1. Names sound right. I’d probably name the extension ‘j2objc’. ad 2 Not sure I understand the question. ad 3. ‘task foo’ syntax will work in a build script, but not in a plugin class. ad 4. e.g. ‘sourceSets.main.java.srcDirs’, ‘sourceSets.test.java.srcDirs’. ad 5. You could iterate over ‘configurations.testRuntime’ looking for a JUnit Jar, then somehow detect the version (e.g. infer from filename). Alternatively you could iterate over the dependency graph. This simplifies getting the version, but won’t work if JUnit is provided as a file (rather than repository) dependency. ad 6. Only evaluation of code that accesses mutable parts of the build model needs/should be deferred (e.g. using ‘project.afterEvaluate {}’). In particular, it’s important to create tasks immediately, as otherwise it won’t be possible to (re)configure them in build scripts.

Thanks Peter. I rewrote it substantially based on bigguy’s advice… stopped using afterEvaluate and implemented my own custom functions.

For no. 2, I’d like to do a plugin as a jar, where it incorporates the plugin script AND a binary that’s used for the translation. So the script would reach in to it’s own Jar, execute the binary it found and use that for one of the build steps. Is that possible?

It should be possible. I guess you’d have to read the binary using ‘ClassLoader.getResourceAsStream()’, copy it to a temporary location, make it executable, and execute it.

I’ve reworked the plugin to a much more gradle-like version. See the latest version here:

https://gist.github.com/brunobowden/58d6e311ab96760fc371

Thanks for reminding me to set my real name on Stackoverflow. :}

I haven’t tried any of this yet, but I’m wondering if there’s a way to tie it into the existing obj-c native support (override the compiler toolchain with the j2obcc script, I think it’s just a passthrough). Would anyone ever want to mix handwritten obj-c with the generated j2objc code in one project?

Hi Sterling and thanks for your help. It’s likely that people will do this on a periodic rather than continuous basis, e.g. they get something working in the Android build over multiple checkins… then when it’s stable, someone translates it to the objective-c code and the Xcode person will do the work to hook up the new code. As sometimes this will break the iOS build, you’d want to have manually control over it. Though for those occasions where it works automatically without breaking the build, you might as well translate it immediately. It’s not seamless enough to make it part of the regular compile cycle.

Sorry, to answer your question… absolutely they’ll be mixing j2objc code as the UI needs to be written in Objective-C (or in my case Swift). The idea is to share as much code as possible and then write a great UI in the native system of each platform.