Need help trying to invoke an external ant script


(Doug Lethin) #1

As part of a gradle build I’m developing, one of my tasks needs to invoke an external ant script.

It is a legacy and script I don’t have much control over changing, and unfortunately it has dependencies on extensions provided by 3rd parties. So inside the build.xml, there are the following lines:

<!-- Task defs go here -->
 <taskdef name="xmltask" classname="com.oopsconsultancy.xmltask.ant.XmlTask" />
 <taskdef resource="net/sf/antcontrib/antlib.xml" />

The expectation in the past was that you needed to include the jars containing these custom tasks inside your ANT_HOME/lib prior to invoking the build.

Now I’m trying to automate this in an gradle task, as such:

task installActivityLogging(dependsOn: "unpackActivityLogging") << {
     ant.ant(antfile:"${jbossHome}/install/whp-activity-logging/build.xml",
     target:"install",
     dir:"${jbossHome}/install/whp-activity-logging")
}

However, this fails, with the following error:

Cause: The following error occurred while executing this line:.../tgt/whp-jboss-jee-4.3/install/whp-activity-logging/build.xml:9: taskdef class com.oopsconsultancy.xmltask.ant.XmlTask cannot be found
 using the classloader AntClassLoader[]

I thought that I could follow the advice in section 43.6 in the User Guide to add a buildScript section and then put on the classpath the jar containing this task, but that didn’t seem to work. Here is what I had:

buildscript {
  repositories {
    mavenRepo url: "<My internal nexus repo URL here>"
  }
  dependencies {
    classpath "com.oopsconsultancy:xmltask:1.15.1"
    }
}

However, I still get the same error.

Am I doing something wrong? Is there some other gradle magic that can get this file on the classpath.

The only other think I can think of is that I actually create a task that downloads an ant installation in to the buildDirectory, copies the xmltask jar into that ant distributions lib directory, and then invoke the ant proces externally pointing to that build.xml file. That is uglyness I’m trying to avoid.

Any suggests greatly appreciated


(Doug Lethin) #2

I’ve come up with a managable workaround to this problem in the meantime. Rather have the gradle build download a copy of ant from somewhere and install the required libraries into its lib folder, I will just require the users to have ant already installed and have ANT_HOME env specified.

Then I can copy the required ant libraries into a directory and use an Exec task to invoke ant, pointing to the lib directory containing my required jars as arguments to the command.

It looks something like this –

configurations {
   antlibs
}
  dependencies {
  antlibs "com.oopsconsultancy:xmltask:1.15.1@jar"
  antlibs "ant-contrib:ant-contrib:1.0b3@jar"
}
  task configureComponentA(type:Exec,
   dependsOn: ['unpackComponent', 'copyAntLibs']) {
   tsFile= new File("${buildDir}/.${name}")
  outputs.upToDateWhen { tsFile.exists() }
    antHome = System.getenv()['ANT_HOME']
   workingDir "${componentHome}"
  executable = "${antHome}/bin/ant"
  args = ['-lib', "${buildDir}/antlibs", 'config']
    doLast {
     ant.touch(file: tsFile)
  }
}
  task copyAntLibs(type: Copy) {
  description = 'copy antlibs required to execute legacy ant builds'
  into "$buildDir/antlibs"
  from configurations.antlibs
}

It would be nice to not have to externally invoke ant, but this is a reasonable compromise that works. We’ve got other fish to fry to worry about optimizing this.


(Matias Bjarland) #3

Assuming I understand your problem correctly, here is an alternative solution.

Assume we have the following build.xml ant build file:

<project>
      <target name="test">
      <foreach list="Alice, why, is, a, raven, like, a, writing, desk?" param="x" target="igetcalled" />
    </target>
          <target name="igetcalled">
      <echo message="${x}" />
      </target>
  </project>

this file uses the ant-contrib custom ant task foreach and executing ant on it as it stands results in the following familiar error:

nadurra:anttest mbjarland$ ant test
  Buildfile: /Users/mbjarland/tmp/anttest/build.xml
      test:
      BUILD FAILED
  /Users/mbjarland/tmp/anttest/build.xml:3: Problem: failed to create task or type foreach
  Cause: The name is undefined.
  Action: Check the spelling.
  Action: Check that any custom tasks/types have been declared.
  Action: Check that any <presetdef>/<macrodef> declarations have taken place.
      Total time: 0 seconds

To make it possible to call this ant file from gradle we can create the following build.gradle:

repositories {
    mavenCentral()
  }
      configurations {
    antcp {
       description = 'ant classpath'
      transitive = true
      exclude module: 'ant'
    }
  }
      dependencies {
    antcp "ant-contrib:ant-contrib:1.0b3"
  }
      task callAnt << {
    println "PATH: ${configurations.antcp.asPath}"
          ant.taskdef resource: "net/sf/antcontrib/antcontrib.properties",
                 classpath: configurations.antcp.asPath
    ant.ant antfile: "build.xml", target: "test", dir: "."
  }

so what is going on here? Well we start by creating a separate configuration to contain whatever custom classes your ant script needs to have access to. I chose to call this configuration ‘antcp’.

We then proceed to add the ant-contrib library to the antcp configuration using the dependencies closure. Note that we also exclude ant itself from the dependency chain as gradle already ships with an ant version and I chose not to have two versions of ant on the classpath at the same time (ant-contrib downloads ant-1.5 I think…old school). Not sure if this would or would not be a problem, but it feels cleaner to stick with the ant version gradle already uses.

We then do two things:

  1. We do a taskdef in the gradle file using ant.taskdef. This adds the foreach task to ant using the classpath we built using the antcp configuration.

  2. We then call the ant build.xml file from gradle with the assumption that the foreach task will be available.

The resulting gradle run looks as follows:

nadurra:anttest mbjarland$ gradle callAnt
  :callAnt
  PATH: /Users/mbjarland/.gradle/caches/artifacts-3/ant-contrib/ant-contrib/c12498cf18507aa6433a94eb7d3e77d5/jars/ant-contrib-1.0b3.jar
  [ant:echo] Alice
  [ant:echo]
why
  [ant:echo]
is
  [ant:echo]
a
  [ant:echo]
raven
  [ant:echo]
like
  [ant:echo]
a
  [ant:echo]
writing
  [ant:echo]
desk?
      BUILD SUCCESSFUL
      Total time: 4.784 secs

the above is working under the assumption that you can download whatever dependencies you need from a maven repository. If you have the jars on disk somewhere, you can do something like this:

dependencies {
    antcp files('libs/a.jar', 'libs/b.jar')
  }

Let me know if that helps you out. Gave me an excuse to teach myself a bit about classloading when calling ant : )


(Matias Bjarland) #4

dlethin, did the above solution work for your scenario?


(Doug Lethin) #5

Thanks you for taking the time to try to help resolve my issue, especially in the way you helped me walk through the steps.

Unfortunately, this solution did not work for me. Your demo ant build is a bit different from mine. If you modify your demo ant build as follows, you will probably get the same error as me:

<project>
      <!-- Add this line here -->
    <taskdef resource="net/sf/antcontrib/antlib.xml" />
      <target name="test">
      <foreach list="Alice, why, is, a, raven, like, a, writing, desk?" param="x" target="igetcalled" />
    </target>
    <target name="igetcalled">
      <echo message="${x}" />
      </target>
  </project>

The problem is that if if we tell gradle the classpath to resolve these custom tasks defs, When the script gets run, its going to resolve the path to the script itself, and it is not going to have access to the same classpath gradle was using.

I thought I could solve this by potentially using buildScript and adding these custom jars to the classpath configuration, but that didn’t work. Of course, its possible I was doing some wrong or stupid, so if that is something you actually do get to work, I would be very curious to know how.

Thanks again.


(Matias Bjarland) #6

So I’m assuming from your post that you will not be able to change the ant build files at all.

Adding the taskdef line to my ant build.xml:

<project>
      <taskdef resource="net/sf/antcontrib/antlib.xml" />
          <target name="test">
      <foreach list="Alice, why, is, a, raven, like, a, writing, desk?" param="x" target="igetcalled" />
    </target>
          <target name="igetcalled">
      <echo message="${x}" />
      </target>
  </project>

and executing it via gradle with the same build.gradle as above gives the following:

nadurra:anttest mbjarland$ gradle callAnt
  :callAnt
  PATH: /Users/mbjarland/.gradle/caches/artifacts-3/ant-contrib/ant-contrib/c12498cf18507aa6433a94eb7d3e77d5/jars/ant-contrib-1.0b3.jar:/Users/mbjarland/.gradle/caches/artifacts-3/ant/ant/c12498cf18507aa6433a94eb7d3e77d5/jars/ant-1.5.jar
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:echo] Alice
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:echo]
why
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:echo]
is
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:echo]
a
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:echo]
raven
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:echo]
like
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:echo]
a
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:echo]
writing
  [ant:taskdef] Could not load definitions from resource net/sf/antcontrib/antlib.xml. It could not be found.
  [ant:echo]
desk?
      BUILD SUCCESSFUL
      Total time: 4.427 secs
  nadurra:anttest mbjarland$

in oher words, the build works as the custom tasks are already defined from gradle, but the ant execution prints out warnings because it can not execute the taskdef calls. So this is now only a question of polluting the execution output.

There is a slightly hacky way to get around this…and this is getting ugly. In ant it is possible to override an existing task definition using, for example, a macrodef. So as it is the ‘taskdef’ task printing out all those warnings above, we can redefine it.

Resulting files:

build.gradle

repositories {
  mavenCentral()
}
  configurations {
  antcp {
     description = 'ant classpath'
    transitive = true
    //exclude module: 'ant'
  }
}
  dependencies {
  antcp "ant-contrib:ant-contrib:1.0b3"
}
  task callAnt << {
  println "PATH: ${configurations.antcp.asPath}"
      ant.taskdef resource: "net/sf/antcontrib/antcontrib.properties",
               classpath: configurations.antcp.asPath
               println "- Overriding task 'taskdef' from gradle, you can ignore the warning on the line below -"
  ant.macrodef(name: 'taskdef') {
    attribute name: "resource",
default: "foo"
    attribute name: "name",
    default: "foo"
    attribute name: "classname", default: "foo"
    sequential {
      //do nothing, a no-op 'taskdef'
      //echo message: "Ignoring taskdef as call is coming from gradle"
    }
  }
              ant.ant antfile: "build.xml", target: "test", dir: "."
}

build.xml

<project>
    <taskdef resource="net/sf/antcontrib/antlib.xml" />
      <target name="test">
    <foreach list="Alice, why, is, a, raven, like, a, writing, desk?" param="x" target="igetcalled" />
  </target>
      <target name="igetcalled">
    <echo message="${x}" />
    </target>
</project>

resulting execution log:


$ gradle callAnt  :callAnt  PATH: /Users/mbjarland/.gradle/caches/artifacts-3/ant-contrib/ant-contrib/c12498cf18507aa6433a94eb7d3e77d5/jars/ant-contrib-1.0b3.jar:/Users/mbjarland/.gradle/caches/artifacts-3/ant/ant/c12498cf18507aa6433a94eb7d3e77d5/jars/ant-1.5.jar  - Overriding task 'taskdef' from gradle, you can ignore the warning on the line below -  Trying to override old definition of task taskdef  [ant:echo] Alice  [ant:echo]  why  [ant:echo]  is  [ant:echo]  a  [ant:echo]  raven  [ant:echo]  like  [ant:echo]  a  [ant:echo]  writing  [ant:echo]  desk?

 BUILD SUCCESSFUL

 Total time: 5.081 secs  

…not saying this is pretty…but it does work. Let me know if we managed to solve your issue this time : )


(Doug Lethin) #7

Thanks again for the possibilities. In my particular case, the ant build I was trying to execute was actually failing, not just producing ugly output. As mentioned in my original post, my external build.xml file I’m invoking (and yes, I don’t have the luxury of changing it which is why I’m going down this path…) has two taskdef lines:

<taskdef name="xmltask" classname="com.oopsconsultancy.xmltask.ant.XmlTask" />
<taskdef resource="net/sf/antcontrib/antlib.xml" />

The latter one is what you are using in your example, but I think its the first taskdef that is more problematic, because the build will actually fail if it can’t find the classname, which is the experience I’m getting.

However, your last suggestion about using a macro to redefine the taskdef task DID work for me.

my task now looks like this:

task installActivityLogging << {
   println "PATH: ${configurations.antlibs.asPath}"
    ant.taskdef(name:"xmltask", classname:"com.oopsconsultancy.xmltask.ant.XmlTask",
       classpath: configurations.antlibs.asPath)
  ant.taskdef(resource:"net/sf/antcontrib/antlib.xml",
       classpath: configurations.antlibs.asPath)
  ant.macrodef(name: 'taskdef') {
    attribute name: "resource",
default: "foo"
    attribute name: "name",
    default: "foo"
    attribute name: "classname", default: "foo"
    sequential {
      //do nothing, a no-op 'taskdef'
      //echo message: "Ignoring taskdef as call is coming from gradle"
    }
  }
    ant.ant(antfile: "build.xml", target: "config",
   dir: "${pathToAntBuild}")
}

It is indeed not pretty, but it works. Its probably a better approach that what I have now because it removes the need to have ant installed with ANT_HOME env variable and calling it externally. Either way is a hack because in both cases I need to know in advance what custom tasks defs need to be defined and present in the build file I’m calling.

I think I will take your approach. Thanks for presenting the idea. I never really played around much with macros in ant before so its nice to see an example of how they can be used.

Thanks again.

Doug