[native] Run a task before *any* compilation task


(Hoobajoob) #1

I have a multiplatform build script that runs on windows and linux systems, and prior to any compilation activity, I want to run a task that generates a file that will be included in compilation.

I’m hoping that I can do this in a one-liner within the build script, but I can’t seem to find just one task on which to hang a dependsOn relationship. There are different binaries and variants that are produced, and I just want this task to run before any of these other tasks runs. The assemble task has these dependencies:

assemble - Assembles the outputs of this project.
[myComponentWindows_x86DebugStaticLibrary,
myComponentWindows_x86ReleaseStaticLibrary,
myComponentTestWindows_x86DebugExecutable,
myComponentTestWindows_x86ReleaseExecutable]

Can I do this by setting up task dependencies in build.gradle, or do I have to run the task explicitly before the build task on the gradle command line as below?

./gradlew myPrecompileTask build

build.dependsOn myPrecompileTask is insufficient, as a whole bunch of tasks get run before myPrecompileTask


(Hoobajoob) #2

Anyone have a recommendation?

@mark_vieira
@Daniel_L


(Daniel Lacasse) #3

What you are looking at is most likely a source set with a generatedBy task. The source set will be included as source to compiled to the component’s binaries. The IDL example will get you started in the right direction: https://github.com/gradle/gradle/blob/master/subprojects/docs/src/samples/native-binaries/idl/build.gradle.

You can simplify the IDL example a bit. For example, you don’t really need to define a custom class for your myPrecompileTask. You can use extended property to the headerDir and sourceDir property so the generatedBy mechanic works.

Caveat with this technique is you can easily forget that each source set are compiled independently. This means that if the generated source set depends on another library (needs additional include path) or your main source set depends on the generated source set (main files needs the generated includes), you need to set the dependency accordingly.

Bonus point: If I recall correctly and the generated files are not native source files, you can start looking into modeling your precompile task as it’s separate source type with it’s own build task. Another way to do it is to drill down the binaries and look at the build task list which will contains the compile, link and lifecycle tasks. You can filter them just like you do in any other Gradle container.

Don’t hesitate if you have any other question or you want me to expand on any solution.


(Hoobajoob) #4

This precompile step is pretty simple - I copy a source file like SomeClass.cpp.tokenized into the same directory, performing token substitution on the contents and rename the copied file to strip off the .tokenized suffix, so that directory has both files: SomeClass.cpp and SomeClass.cpp.tokenized.

The sources for the component then use an exclude rule to omit files ending with .tokenized from compilation.

The task looks like this:

substituteVersion(Copy) {
    def versionFileDir ='src/myComponent/cpp'
    def versionFile = "Version.cpp"
    def tokenizedFile = "${versionFile}.tokenized"

    def buildNumber = '0'

    if ( project.hasProperty("buildNumber") ) {
        buildNumber = project.buildNumber
    }

    from (versionFileDir) {
        include tokenizedFile
        rename tokenizedFile, versionFile
        filter { line ->
            line.replaceAll( '@build@', buildNumber );
        }
    }
    into (versionFileDir)
}

Can this simple operation be modeled as a source set?


(Hoobajoob) #5

Ah - light bulb just went on - I’ll try using this copy task as the generatedBy task for a source set and see where that gets me


(Hoobajoob) #6

@Daniel_L: well, this light bulb just turned off again.

There are several unexpected problems I keep running into trying to get this to work, so I have some questions:

  1. Do I really need a separate source set? This is really just the cpp source set for the component, with the same headerDir and sourceDir. Can I just add a generatedBy property to the existing cpp source set like so?
model {
    components {
        myComponent(NativeLibrarySpec) {
            sources {
                cpp {
                    source {
                        exclude "**/*.h"
                        exclude "**/*.tokenized"
                        generatedBy tasks.substituteVersion
                    }
                }
            }
        }
    }
    tasks {
        substituteVersion(Copy) {
            // copy config here
        }
    }
}
  1. Where does the generatedBy task have to be defined? The example shows it defined in the global script scope. Does it have to be defined there? Can I define it under model { tasks { } }? If I do define it under model.tasks, can I use the same syntax for referencing it (tasks.<taskName>)? I have tried this, and I get this error below:
Could not find property 'substituteVersion' on task set.
  1. It’s not clear to me how I am to add the properties required by the source set to the generatedBy task. When I try to add the extended properties as shown below, I get the error shown below:
model {
    tasks {
        substituteVersion(Copy) {
            @OutputDirectory File headerDir = file('src/myComponent/headers')
            @OutputDirectory File sourceDir = file('src/myComponent/cpp')
            // ...copy logic here
        }
    }
}
Could not compile build file 'C:\Users\hoobajoob\projects\project-x\build.gradle'.
> startup failed:
  build file 'C:\Users\hoobajoob\projects\project-x\build.gradle': 232: Annotation
@org.gradle.api.tasks.OutputDirectory is not allowed on element LOCAL_VARIABLE

(Daniel Lacasse) #7
  1. This looks like it’s working:

    apply plugin: 'cpp’
    task substituteVersion(type: Copy) {
    ext.headerDir = file(‘src/myComponent/headers’)
    ext.sourceDir = file(‘src/myComponent/cpp’)
    }
    model {
    components {
    myComponent(NativeLibrarySpec) {
    sources {
    cpp {
    source {
    exclude "/*.h"
    exclude "
    /*.tokenized"
    generatedBy tasks.substituteVersion
    }
    }
    }
    }
    }
    }

  2. I would guess you need to use the model way to specify you take the tasks model container as input for your component rule: $.tasks.substituteVersion. However, binding the tasks container as input for the components rules cause a cycle model rule dependencies. Probably a Gradle engineer could anwser this one off. You can, as a workaround, use the legacy tasks container.

  3. All you need to do is specify extended properties:

    model {
    tasks {
    substituteVersion(Copy) {
    ext.headerDir = file(‘src/myComponent/headers’)
    ext.sourceDir = file(‘src/myComponent/cpp’)
    // …copy logic here
    }
    }
    }

A small personnal best practice I would add is to put your generated source (header/source) in the buildDir. The reason is you don’t need to tweak the clean task to clean the generated source when needed. Also you don’t need to deal with .gitignore (or equivalent) since the build directory is, usually, already ignored.


(Hoobajoob) #8

ok - I’ll try these revisions and see if I can get the same results. Thanks!