concat as resource collection in zip not working? - ant

I'm trying to use <concat> as a resource collection in a <zip> task and, according to the documentation, this should work. I'd like to do this because some of the files I want to include in the zip need to have some properties expanded, so I will also add a <filterchain> to the <concat> to do this. I'd prefer to do it directly rather than copying to a temp location (with property substitution) and including the copy in the zip file.
However, I can't seem to get <zip> to correctly use the <concat> element.
A simplified example of what I have so far:
<zip destfile="target/dist.zip">
<concat>
<fileset file="CHANGES.txt" />
</concat>
</zip>
This creates a zip file containing several directories all named concat (C: (obviously this is on a Windows machine).
What am I missing?

A colleague and I came up with the answer by looking through the <zip> and <concat> source. There are really two answers:
<concat>'s implementation of the ResourceCollection interface is odd, but we understand why.
There's a way around that.
For #1, while <concat> is a ResourceCollection (like FileSet), under the hood it returns as the name of single Resource it contains a hard-coded value:
"concat (" + String.valueOf(c) + ")";
Look familiar?
The name of resources is normally ignored--except by <zip> and its related tasks, which uses the resource name as the ZipEntry name. Since <concat> returns the odd-looking name, that's what we get in the zip file.
I haven't quite figured out why I get multiple entries, but it doesn't matter: the observation leads to a convoluted solution.
Since I know the name of the ZipEntry I want to create, I can use a <mapper> to give the <concat> resource a name. Here's what I came up with in all its glory:
<zip destfile="target/distribution.zip">
<fileset dir=".">
<exclude name="target/**" />
<exclude name="CHANGES.txt" />
</fileset>
<mappedresources>
<concat>
<fileset file="CHANGES.txt" />
<filterchain>
<expandproperties />
</filterchain>
</concat>
<mergemapper to="CHANGES.txt" />
</mappedresources>
</zip>
As my colleague says "In Ant every problem can be solved using a mapper."
This only works for Ant 1.8+ because <mappedresources> was added in that release.
I'm going to post some comments to the Ant mailing list and suggest a couple of enhancements:
Allow a resource name to be specified as an attribute on <concat> when it's being used as a ResourceCollection.
Throw an exception (and don't create a synthetic value) if getName() is called without having a value specified.
Finally, though not directly related, I do wish <expandproperties> could take a <propertyset> so I can control which properties get substituted.

Do you want the final zip to contain a single file or multiple files? As far as I can see, using concat (when done successfully, which isn't done above) would produce a single file, the result of concatenation of all files in the resource collection.
If you want multiple files rather than concatenation, I think intermediate copy is what you'll need.
From Ant manual for the concat task:
Since Apache Ant 1.7.1, this task can
be used as a Resource Collection that
will return exactly one resource.

Related

What are the usage restrictions on Ant's if/unless conditional attributes?

So we're using basic Ant 1.9.4, with a little use of 1.9.1's new if/unless attributes. For example,
<project xmlns:if="ant:if" xmlns:unless="ant:unless">
.... miles and miles of XML ....
<jar ....>
<service ....>
<provider classname="a.b.c.X" if:true="${some.flag}"/>
<provider ..../>
</service>
<fileset>
....the stuff in here will matter shortly....
</fileset>
</jar>
with some.flag always set to true or false, never left unset, and never set to any other value (assigned inside a property file read in earlier, if that matters), and it works wonderfully, giving us exactly the behavior we need. Joy!
More recently, we tried to make some of the jar task's fileset entries a little smarter, such as
<fileset dir="somedir">
<include name="optional_file" if:true="${some.flag}"/> <!-- same property name as before -->
</fileset>
With this syntax, we get an error "A zip file cannot include itself", with a line number pointing to the start of the jar task. This is obviously bogus syntax. However, changing this second if:true to simply if out of desperation -- and making no other changes -- avoids the error and gives us correct flag-based optional inclusion behavior.
What's going on here? Is the new syntax simply unavailable in <fileset> and/or fileset's nested <include> blocks?
As an experiment, I tried using an if:true or if:set attribute as appropriate in a few other useful places. Some places it worked perfectly. Some places I got some bizarre nonsense error, clearly the kind of thing that a parser prints when it's gone off the rails. Each time, reworking if:set="$(foo}" into if="foo" and if:true="${foo}" into if="${foo}" got back to the desired if-then behavior. So it's not a blocking problem, but we'd rather have the self-documenting :condition if we could.
I couldn't find mention of any such restriction in the Ant manual, but the manual describes the if/unless syntax in at least two different places using different descriptions. (I'm not sure where they are due to the manual's use of HTML frames; every URL shows up as index.html. Anytime I refer to the manual it feels like I'm browsing like it's 1999, baby! *does MC Hammer slide out of the room*)
Since Ant 1.4, <include> and <exclude> elements nested under <patternset> elements have supported if and else attributes. Each <fileset> has an implicit <patternset> nested under it, so the if and else attributes are available to it...
<condition property="some.flag.istrue">
<istrue value="${some.flag}"/>
</condition>
<fileset dir="somedir">
<!-- The "if" below is different than "if:true". -->
<include name="optional_file" if="some.flag.istrue"/>
</fileset>
In the above example, the if in <include> is an ordinary Ant attribute in the "default" Ant XML namespace. On the other hand, if:true is in the ant:if XML namespace.
The namespaced if:true doesn't work well with <include> elements. If the value provided to if:true doesn't evaluate to true, then Ant behaves as if the entire <include> element never existed. This is bad because Ant takes empty patternsets to mean "match every file". This is likely why you received the "A zip file cannot include itself" error; the <fileset> was likely containing the destination JAR file.
Stick with the plain if and else attributes for <include> and <exclude> elements and things should work.

How do I make a fileset from a comma-separated list of directories in Ant?

In an Ant target I get a property, containing the list of directories to be included in further action (copying, filtering, etc.). It looks like this:
directories=dir1, dir2, dir3
I need a way to convert this list to a fileset or patternset that selects all the files in these directories.
I know I can use a script to generate pattern strings and then use it in the "include" or "exclude", but is there are a way to avoid scripts?
Note that as of Ant 1.9.4, there is a new construct <multirootfileset> that provides that functionality, even if the dirs are not siblings:
<multirootfileset basedirs="${directories}" includes="**/*">
How about using the antcontrib propertyregex task to convert the comma-separated list into wildcards suitable for a fileset?
<property name="directories" value="dir1, dir2, dir3" />
<property name="wildcard" value="${file.separator}**${file.separator}*" />
<propertyregex property="my_pattern"
input="${directories}"
regexp=", "
replace="${wildcard}," />
At this point we now have:
my_pattern=dir1/**/*,dir2/**/*,dir3
That can be used with a further suffixed wildcard to get the full fileset:
<fileset dir="." id="my_fileset" includes="${my_pattern}${wildcard}" />
(The fiddly ${wildcard} is to ensure portability between unix and windows filesystems, you could use /**/* if you're pure unix.)
Something like this should work:
<dirset includes="${directories}"/>
Yes, dirset isn't fileset. However, it may be enough, or else you can probably use a for or foreach from ant-contrib to iterate over the directories in your target. You might also be able to define a ResourceCollection based around the dirset. It might help to know what the "further action" is expected to be.
However, this feels like too much work ...

Ant 1.8 include or import with nested resource collection

I'd like to have Ant automatically include or import resources matching a particular pattern, but I'm really struggling with the syntax. Here's what I've tried:
<import>
<fileset dir="${basedir}" includes="*-graph.xml" />
</import>
However, I just get the error message
import requires file attribute or at least one nested resource
The documentation for import (and include) both say that you can use a nested resource collection, and the documentation for resource collections says <fileset> is a resource collection. I've Googled and can't find any useful examples at all.
I'm using Ant 1.8.1 (verified with ant -version)
EDIT
Found the problem. Firstly, SOMETHING has to match the expression. With no files that match, it blows up - even with optional=true, which is odd! Secondly, the matching files have be valid Ant file (even if they just contain <project/> - simply creating an empty file wasn't good enough).
Better error messages, please Apache! :-)
Try:
<foreach target="-import" param="file.name">
<fileset dir="${basedir}" includes="*-graph.xml" />
</foreach>
<target name="-import">
<import file="${file.name}" />
</target>
is in the ant-contrib tasks (http://ant-contrib.sourceforge.net/tasks/tasks/index.html) I'm not sure if this will work, I just came up with it on the fly.
Here is a simple way to deal with this problem:
<project name="the-name-of-the-project">
<import>
<fileset dir="${basedir}" includes="*-graph.xml" erroronmissingdir="false"/>
<fileset file="${ant.file.the-name-of-the-project}"/>
</import>
</project>
This way, the set of imported files is never empty since it includes the file containing this declaration. This works for <import> directives only, since it omits already imported files; <include> directives would trivially lead to a recursion.
Note the erroronmissingdir="false" is optional if basedir always exists.

How can I iterate over properties from a file?

All my projects and their versions are defined in a properties file like this:
ProjectNameA=0.0.1
ProjectNameB=1.4.2
I'd like to iterate over all the projects, and use their names and versions in an Ant script.
At present I read the entire file using the property task, then iterate over a given list in a for loop like this:
<for list="ProjectNameA,ProjectNameB" param="project">
<sequential>
<echo message="#{project} has version ${#{project}}" />
</sequential>
</for>
How can I avoid the hard-coding of the project names in the for loop?
Basically iterate over each line and extract the name and the version of a project as I go.
Seeing as you're already using antcontrib for, how about making use of the propertyselector task:
<property file="properties.txt" prefix="projects."/>
<propertyselector property="projects" match="projects\.(.*)" select="\1"/>
<property file="properties.txt" />
<for list="${projects}" param="project">
...
</for>
The idea here is to read the properties once with the projects prefix, and use the resulting set of properties to build a comma-separated list of projects with the propertyselector task. Then the properties are re-read without the prefix, so that your for loop can proceed as before.
Something you want to keep in mind, if you are reading additional .property files (besides build.properties) is scoping. If you read an additional file (via the property file="foo.property") tag, ant will show that the file was read, and the properties loaded. However, when you goto reference them, they come up un-defined.

Ant: Create directory containing file if it doesn't already exist?

Basically, I get a path like "C:\test\subfolder1\subfolder2\subfolder3\myfile.txt", but it's possible that subfolders 1-3 don't exist already, which means I'd get an exception if I try to write to the file.
Is there a way to create the directory structure the target file is in, either by using some task that creates the structure when it outputs to the file and then deleting the file, or by parsing the directory part of the path and using the mkdir task first?
Ant will create the full tree of directories for you when you use the <mkdir> task. So you just need to use the <dirname> task to get the directory name from the file name.
<dirname property="directoryProperty" file="${filePathProperty}"/>
<mkdir dir="${directoryProperty}" />
The first line extracts the directory portion of your file path and stores it in the directoryProperty property. The second line creates the directory (and any parent directories that don't exist).
This task works well
<mkdir dir="${file}/../"/>
Sometimes we could have an alternate choice, using touch task
<touch file="${file}" mkdirs="true" verbose="true"/>
This task should do the job but would have a side effect to create the file with zero size
Just make failonerror=false to avoid the error to stop the whole logic.
<delete includeemptydirs="true" failonerror="false">
<fileset dir="${builder-base.dir}" includes="**/*"/>
</delete>
Using the
<mkdir dir="${dir}"/ >
inside your <target> tag should work, but I am not sure what else you want to do along with mkdir?
I'm not 100% sure it'll work but you might be able to do something like the following to make the parent directory you're after:
<mkdir dir="${file}/../"/>
If that doesn't work straight off then it might be worth defining a property using the location syntax before creating a directory with the new property:
<property name="dir" location="${file}/../" />
<mkdir dir="${dir}" />
Well-behaved Ant tasks are generally expected to create any necessary directory structures unless there is a good reason not to.
Are you writing a task? If so you should add the directory creation logic to your task. If you are getting the task from a third party you should point this fact out to them and have them fix their task. Failing that Dan's solution should work.

Resources