A couple of weeks ago I sent out a patch for <zip> and <jar> tasks which had several enhancements and bugfixes, but did not get any firm response and it was not applied. There was some disagreement as to handling of empty archives; so I have created a new patch, which I think resolves this disagreement by grandfathering the current behavior but making it easy to switch; and included several more improvements. Attached is a fresh patch against current CVS sources; noticeable changes from CVS version in no particular order are:
1. Behavior of Zip and Jar when no files match is more carefully regulated. (With current CVS sources, it will silently skip making the archive--not intentionally but due to an improperly swallowed exception.) There is now an optional attribute "whenempty" which is used to control what happens when you try to make an archive with no files. "create" causes it to be created as you asked, though a warning is printed. "skip" causes it to skip trying to make the archive (and a warning is printed). "fail" causes the build to halt with an exception. The default for Zip is "skip" to match current behavior (though personally I would prefer "fail" or "create" as defaults; it would be a one-line change to modify the default behavior). The default for Jar is "create" (since there is always the manifest, so the archive is not exactly empty). In the case of empty ZIP files, java.util.zip.* cannot handle this, so an empty ZIP archive (22 bytes, but zero file entries) is created when "create" is given. 2. Error-handling of both tasks is made more robust; some exceptions were being swallowed before, or some streams might not have been closed reliably, etc. 3. Entries for containing directories are added to the archive whenever files are added, since some tools might fail if these were not present, and for consistency with existing ZIP and JAR creation tools. 4. The default manifest for JARs says they were created by Ant and gives the Ant version number used. 5. JARs are recreated if their manifest is specified and is changed on disk (and of course if any contained files are changed or added too, as for ZIPs). 6. Documentation update for both tasks covering user-visible changes. 7. Both <zip> and <jar> tasks may take embedded filesets. Thus, for compatibility with current scripts and for simplicity of syntax, there is still an implicit fileset used when you specify basedir (which is now optional if there are embedded filesets). But you may add <fileset> or <filesetref> elements inside the <zip> or <jar> and these will also be included in the archive. If multiple filesets are employed, it works more or less like the JAR tool's -C option: each fileset has its own basedir, and any files acquired from that fileset are stored with a relative archive path according to that basedir. This makes it possible to efficiently and in one task include files from multiple disparate directories, while controlling the archive path (critical for e.g. resource paths/packages in JARs). 8. For Jar task, if you have a file META-INF/MANIFEST.MF present in one of your filesets, this was always ignored in deference to the (automatic or supplied) manifest. Now a warning is printed if this happens. Only tested on Linux with Sun JDK 1.3; I tried to not use any > 1.1 APIs but someone should check of course. Including ZIP of informal test set; tries to make ZIPs and JARs in various ways with different options. Just run the build.xml file, target "all" to create; "touchreal" to change timestamp of a content file, then try "all" again; "touchmani" for a manifest file; "clean" to delete archives; three other targets mentioned in it which are expected to fail. I think that's it. Let me know if anything is missing. Cheers, -Jesse -- Jesse Glick <mailto:[EMAIL PROTECTED]> NetBeans, Open APIs <http://www.netbeans.org/> tel (+4202) 3300-9161 Sun Micro x49161 Praha CR
Index: build.xml
===================================================================
RCS file: /home/cvspublic/jakarta-ant/build.xml,v
retrieving revision 1.62
diff -d -u -r1.62 build.xml
--- build.xml 2000/08/21 15:05:52 1.62
+++ build.xml 2000/08/30 13:41:10
@@ -90,7 +90,6 @@
</javac>
<copydir src="${src.dir}" dest="${build.classes}">
- <include name="**/defaultManifest.mf" />
<include name="**/*.properties" />
</copydir>
@@ -102,6 +101,7 @@
forceoverwrite="true"
filtering="on">
<include name="**/version.txt" />
+ <include name="**/defaultManifest.mf" />
</copydir>
</target>
Index: docs/index.html
===================================================================
RCS file: /home/cvspublic/jakarta-ant/docs/index.html,v
retrieving revision 1.82
diff -d -u -r1.82 index.html
--- docs/index.html 2000/08/21 14:41:11 1.82
+++ docs/index.html 2000/08/30 13:41:12
@@ -2022,6 +2022,15 @@
<code><include></code>, <code><exclude></code>,
<code><patternset></code> and <code><patternsetref></code>
elements.</p>
+<p>You can also use nested file sets for more flexibility, and specify
+multiple ones to merge together different trees of files into one JAR.
+See the <a href="#zip">Zip</a> task for more details and examples.</p>
+<p>If the manifest is omitted, a simple one will be supplied by Ant.
+You should not include <samp>META-INF/MANIFEST.MF</samp> in your set of files.
+<p>The <code>whenempty</code> parameter controls what happens when no files
match.
+If <code>create</code> (the default), the JAR is created anyway with only a
manifest.
+If <code>skip</code>, the JAR is not created and a warning is issued.
+If <code>fail</code>, the JAR is not created and the build is halted with an
error.
<h3>Parameters</h3>
<table border="1" cellpadding="2" cellspacing="0">
<tr>
@@ -2037,7 +2046,7 @@
<tr>
<td valign="top">basedir</td>
<td valign="top">the directory from which to jar the files.</td>
- <td valign="top" align="center">Yes</td>
+ <td valign="top" align="center">No</td>
</tr>
<tr>
<td valign="top">compress</td>
@@ -2079,6 +2088,11 @@
<td valign="top">the manifest file to use.</td>
<td valign="top" align="center">No</td>
</tr>
+ <tr>
+ <td valign="top">whenempty</td>
+ <td valign="top">Behavior to use if no files match.</td>
+ <td valign="top" align="center">No</td>
+ </tr>
</table>
<h3>Examples</h3>
<pre> <jar jarfile="${dist}/lib/app.jar"
basedir="${build}/classes" /></pre>
@@ -2100,6 +2114,21 @@
called <code>app.jar</code> in the <code>${dist}/lib</code> directory. Only
files under the directory <code>mypackage/test</code> are used, and files with
the name <code>Test.class</code> are excluded.</p>
+<pre> <jar jarfile="${dist}/lib/app.jar">
+ <fileset dir="${build}/classes"
+ excludes="**/Test.class"
+ />
+ <fileset dir="${src}/resources"/>
+ </jar></pre>
+<p>jars all files in the <code>${build}/classes</code> directory and also
+in the <code>${src}/resources</code> directory together in a file
+called <code>app.jar</code> in the <code>${dist}/lib</code> directory.
+Files with the name <code>Test.class</code> are excluded.
+If there are files such as
<code>${build}/classes/mypackage/MyClass.class</code>
+and <code>${src}/resources/mypackage/image.gif</code>, they will appear
+in the same directory in the JAR (and thus be considered in the same package
+by Java).</p>
+
<hr>
<h2><a name="java">Java</a></h2>
<h3>Description</h3>
@@ -3655,6 +3684,19 @@
<code><include></code>, <code><exclude></code>,
<code><patternset></code> and <code><patternsetref></code>
elements.</p>
+<p>Or, you may place within it nested file sets, or references to file sets.
+In this case <code>basedir</code> is optional; the implicit file set is
<em>only used</em>
+if <code>basedir</code> is set. You may use any mixture of the implicit file
set
+(with <code>basedir</code> set, and optional attributes like
<code>includes</code>
+and optional subelements like <code><include></code>); explicit nested
+<code><fileset></code> elements; and nested
<code><filesetref></code>
+elements; so long as at least one fileset total is specified. The ZIP file will
+only reflect the relative paths of files <em>within</em> each fileset.</p>
+<p>The <code>whenempty</code> parameter controls what happens when no files
match.
+If <code>skip</code> (the default), the ZIP is not created and a warning is
issued.
+If <code>fail</code>, the ZIP is not created and the build is halted with an
error.
+If <code>create</code>, an empty ZIP file (explicitly zero entries) is created,
+which should be recognized as such by compliant ZIP manipulation tools.</p>
<h3>Parameters</h3>
<table border="1" cellpadding="2" cellspacing="0">
<tr>
@@ -3670,7 +3712,7 @@
<tr>
<td valign="top">basedir</td>
<td valign="top">the directory from which to zip the files.</td>
- <td align="center" valign="top">Yes</td>
+ <td align="center" valign="top">No</td>
</tr>
<tr>
<td valign="top">compress</td>
@@ -3707,6 +3749,11 @@
("yes"/"no"). Default excludes are used when
omitted.</td>
<td valign="top" align="center">No</td>
</tr>
+ <tr>
+ <td valign="top">whenempty</td>
+ <td valign="top">Behavior when no files match.</td>
+ <td valign="top" align="center">No</td>
+ </tr>
</table>
<h3>Examples</h3>
<pre> <zip zipfile="${dist}/manual.zip"
@@ -3729,6 +3776,14 @@
<p>zips all files in the <code>htdocs/manual</code> directory in a file called
<code>manual.zip</code>
in the <code>${dist}</code> directory. Only html files under the directory
<code>api</code>
are zipped, and files with the name <code>todo.html</code> are excluded.</p>
+<pre> <zip zipfile="${dist}/manual.zip">
+ <fileset dir="htdocs/manual"/>
+ <fileset dir="." includes="ChangeLog.txt"/>
+ </zip></pre>
+<p>zips all files in the <code>htdocs/manual</code> directory in a file called
<code>manual.zip</code>
+in the <code>${dist}</code> directory, and also adds the file
<code>ChangeLog.txt</code> in the
+current directory. <code>ChangeLog.txt</code> will be added to the top of the
ZIP file, just as if
+it had been located at <code>htdocs/manual/ChangeLog.txt</code>.</p>
<hr>
<h2><a name="optionaltasks">Optional tasks</a></h2>
Index: src/main/org/apache/tools/ant/defaultManifest.mf
===================================================================
RCS file:
/home/cvspublic/jakarta-ant/src/main/org/apache/tools/ant/defaultManifest.mf,v
retrieving revision 1.1
diff -d -u -r1.1 defaultManifest.mf
--- src/main/org/apache/tools/ant/defaultManifest.mf 2000/01/13 10:41:40
1.1
+++ src/main/org/apache/tools/ant/defaultManifest.mf 2000/08/30 13:41:32
@@ -1 +1,3 @@
Manifest-Version: 1.0
+Created-By: Ant @VERSION@
+
Index: src/main/org/apache/tools/ant/taskdefs/Jar.java
===================================================================
RCS file:
/home/cvspublic/jakarta-ant/src/main/org/apache/tools/ant/taskdefs/Jar.java,v
retrieving revision 1.5
diff -d -u -r1.5 Jar.java
--- src/main/org/apache/tools/ant/taskdefs/Jar.java 2000/06/27 11:12:11
1.5
+++ src/main/org/apache/tools/ant/taskdefs/Jar.java 2000/08/30 13:41:33
@@ -86,38 +86,67 @@
super.zipDir(new File(manifest.getParent()), zOut, "META-INF/");
super.zipFile(manifest, zOut, "META-INF/MANIFEST.MF");
} else {
- /*
- * We don't store directories at all and this one will cause a lot
- * of problems with STORED Zip-Mode.
- *
- * That's why i've removed it -- Stefan Bodewig
- */
- // ZipEntry ze = new ZipEntry("META-INF/");
- // zOut.putNextEntry(ze);
String s = "/org/apache/tools/ant/defaultManifest.mf";
InputStream in = this.getClass().getResourceAsStream(s);
if ( in == null )
throw new BuildException ( "Could not find: " + s );
+ super.zipDir(null, zOut, "META-INF/");
zipFile(in, zOut, "META-INF/MANIFEST.MF",
System.currentTimeMillis());
}
}
+ protected boolean isUpToDate(FileScanner[] scanners, File zipFile)
+ {
+ File[] files = grabFiles(scanners);
+ if (emptyBehavior == null) emptyBehavior = "create";
+ if (files.length == 0) {
+ if (emptyBehavior.equals("skip")) {
+ log("Warning: skipping JAR archive " + zipFile +
+ " because no files were included.", Project.MSG_WARN);
+ return true;
+ } else if (emptyBehavior.equals("fail")) {
+ throw new BuildException("Cannot create JAR archive " +
zipFile +
+ ": no files were included.",
location);
+ } else {
+ // create
+ if (!zipFile.exists() ||
+ (manifest != null &&
+ manifest.lastModified() > zipFile.lastModified()))
+ log("Note: creating empty JAR archive " + zipFile,
Project.MSG_INFO);
+ // and continue below...
+ }
+ }
+ if (!zipFile.exists()) return false;
+ if (manifest != null && manifest.lastModified() >
zipFile.lastModified())
+ return false;
+ for (int i=0; i<files.length; i++) {
+ if (files[i].lastModified() > zipFile.lastModified()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
protected void zipDir(File dir, ZipOutputStream zOut, String vPath)
throws IOException
{
// First add directory to zip entry
- if(!vPath.equals("META-INF/")) {
+ if(!vPath.equalsIgnoreCase("META-INF/")) {
// we already added a META-INF
super.zipDir(dir, zOut, vPath);
}
+ // no warning if not, it is harmless in and of itself
}
protected void zipFile(File file, ZipOutputStream zOut, String vPath)
throws IOException
{
// We already added a META-INF/MANIFEST.MF
- if (!vPath.equals("META-INF/MANIFEST.MF")) {
+ if (!vPath.equalsIgnoreCase("META-INF/MANIFEST.MF")) {
super.zipFile(file, zOut, vPath);
+ } else {
+ log("Warning: selected JAR files include a META-INF/MANIFEST.MF
which will be ignored " +
+ "(please use manifest attribute to jar task)",
Project.MSG_WARN);
}
}
}
Index: src/main/org/apache/tools/ant/taskdefs/Zip.java
===================================================================
RCS file:
/home/cvspublic/jakarta-ant/src/main/org/apache/tools/ant/taskdefs/Zip.java,v
retrieving revision 1.10
diff -d -u -r1.10 Zip.java
--- src/main/org/apache/tools/ant/taskdefs/Zip.java 2000/08/03 09:13:18
1.10
+++ src/main/org/apache/tools/ant/taskdefs/Zip.java 2000/08/30 13:41:33
@@ -55,9 +55,11 @@
package org.apache.tools.ant.taskdefs;
import org.apache.tools.ant.*;
+import org.apache.tools.ant.types.*;
import java.io.*;
import java.util.Enumeration;
+import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.zip.*;
@@ -75,6 +77,10 @@
private File baseDir;
private boolean doCompress = true;
protected String archiveType = "zip";
+ // For directories:
+ private static long emptyCrc = new CRC32 ().getValue ();
+ protected String emptyBehavior = null;
+ private Vector filesets = new Vector ();
/**
* This is the name/location of where to
@@ -99,73 +105,113 @@
doCompress = Project.toBoolean(compress);
}
- public void execute() throws BuildException {
- if (baseDir == null) {
- throw new BuildException("basedir attribute must be set!");
- }
- if (!baseDir.exists()) {
- throw new BuildException("basedir does not exist!");
- }
+ /**
+ * Adds a set of files (nested fileset attribute).
+ */
+ public void addFileset(FileSet set) {
+ filesets.addElement(set);
+ }
- DirectoryScanner ds = super.getDirectoryScanner(baseDir);
+ /**
+ * Adds a reference to a set of files (nested filesetref element).
+ */
+ public void addFilesetref(Reference ref) {
+ filesets.addElement(ref);
+ }
- String[] files = ds.getIncludedFiles();
- String[] dirs = ds.getIncludedDirectories();
+ /**
+ * Sets behavior of the task when no files match.
+ * Possible values are: <code>fail</code> (throw an exception
+ * and halt the build); <code>skip</code> (do not create
+ * any archive, but issue a warning); <code>create</code>
+ * (make an archive with no entries).
+ * Default for zip tasks is <code>skip</code>;
+ * for jar tasks, <code>create</code>.
+ */
+ public void setWhenempty(String we) throws BuildException {
+ we = we.toLowerCase();
+ // XXX could instead be using EnumeratedAttribute, but this works
+ if (!"fail".equals(we) && !"skip".equals(we) && !"create".equals(we))
+ throw new BuildException("Unrecognized whenempty attribute: " +
we);
+ emptyBehavior = we;
+ }
+
+ public void execute() throws BuildException {
+ if (baseDir == null && filesets.size() == 0)
+ throw new BuildException("basedir attribute must be set, or at
least one fileset must be given!");
+
+ Vector dss = new Vector ();
+ if (baseDir != null)
+ dss.addElement(getDirectoryScanner(baseDir));
+ for (int i=0; i<filesets.size(); i++) {
+ Object o = filesets.elementAt(i);
+ FileSet fs;
+ if (o instanceof FileSet) {
+ fs = (FileSet) o;
+ } else {
+ Reference r = (Reference) o;
+ o = r.getReferencedObject(project);
+ if (o instanceof FileSet) {
+ fs = (FileSet) o;
+ } else {
+ throw new BuildException(r.getRefId() + " does not denote
a fileset", location);
+ }
+ }
+ dss.addElement (fs.getDirectoryScanner(project));
+ }
+ FileScanner[] scanners = new FileScanner[dss.size()];
+ dss.copyInto(scanners);
// quick exit if the target is up to date
- boolean upToDate = true;
- for (int i=0; i<files.length && upToDate; i++)
- if (new File(baseDir,files[i]).lastModified() >
- zipFile.lastModified())
- upToDate = false;
- if (upToDate) return;
+ // can also handle empty archives
+ if (isUpToDate(scanners, zipFile)) return;
log("Building "+ archiveType +": "+ zipFile.getAbsolutePath());
- ZipOutputStream zOut = null;
- try {
- zOut = new ZipOutputStream(new FileOutputStream(zipFile));
- if (doCompress) {
- zOut.setMethod(ZipOutputStream.DEFLATED);
- } else {
- zOut.setMethod(ZipOutputStream.STORED);
- }
- initZipOutputStream(zOut);
+ try {
+ ZipOutputStream zOut = new ZipOutputStream(new
FileOutputStream(zipFile));
+ try {
+ if (doCompress) {
+ zOut.setMethod(ZipOutputStream.DEFLATED);
+ } else {
+ zOut.setMethod(ZipOutputStream.STORED);
+ }
+ initZipOutputStream(zOut);
- for (int i = 0; i < dirs.length; i++) {
- File f = new File(baseDir,dirs[i]);
- String name = dirs[i].replace(File.separatorChar,'/')+"/";
- zipDir(f, zOut, name);
- }
+ // XXX ideally would also enter includedDirectories to the
archive
+ Hashtable parentDirs = new Hashtable();
- for (int i = 0; i < files.length; i++) {
- File f = new File(baseDir,files[i]);
- String name = files[i].replace(File.separatorChar,'/');
- zipFile(f, zOut, name);
- }
- } catch (IOException ioe) {
- String msg = "Problem creating " + archiveType + " " +
ioe.getMessage();
+ for (int j = 0; j < scanners.length; j++) {
+ String[] files = scanners[j].getIncludedFiles();
+ File thisBaseDir = scanners[j].getBasedir();
+ for (int i = 0; i < files.length; i++) {
+ File f = new File(thisBaseDir,files[i]);
+ String name = files[i].replace(File.separatorChar,'/');
+ // Look for & create parent dirs as needed.
+ int slashPos = -1;
+ while ((slashPos = name.indexOf((int)'/', slashPos +
1)) != -1) {
+ String dir = name.substring(0, slashPos);
+ if (!parentDirs.contains(dir)) {
+ parentDirs.put(dir, dir);
+ zipDir(new File(thisBaseDir, dir.replace('/',
File.separatorChar)),
+ zOut, dir + '/');
+ }
+ }
+ zipFile(f, zOut, name);
+ }
+ }
+ } finally {
+ zOut.close ();
+ }
+ } catch (IOException ioe) {
+ String msg = "Problem creating " + archiveType + ": " +
ioe.getMessage();
// delete a bogus ZIP file
- if (zOut != null) {
- try {
- zOut.close();
- zOut = null;
- } catch (IOException e) {}
- if (!zipFile.delete()) {
- msg = zipFile + " is probably corrupt but I could not
delete it";
- }
- }
+ if (!zipFile.delete()) {
+ msg += " (and the archive is probably corrupt but I could not
delete it)";
+ }
throw new BuildException(msg, ioe, location);
- } finally {
- if (zOut != null) {
- try {
- // close up
- zOut.close();
- }
- catch (IOException e) {}
- }
}
}
@@ -174,9 +220,87 @@
{
}
+ /**
+ * Check whether the archive is up-to-date; and handle behavior for empty
archives.
+ * @param scanners list of prepared scanners containing files to archive
+ * @param zipFile intended archive file (may or may not exist)
+ * @return true if nothing need be done (may have done something already);
false if
+ * archive creation should proceed
+ * @exception BuildException if it likes
+ */
+ protected boolean isUpToDate(FileScanner[] scanners, File zipFile) throws
BuildException
+ {
+ if (emptyBehavior == null) emptyBehavior = "skip";
+ File[] files = grabFiles(scanners);
+ if (files.length == 0) {
+ if (emptyBehavior.equals("skip")) {
+ log("Warning: skipping ZIP archive " + zipFile +
+ " because no files were included.", Project.MSG_WARN);
+ return true;
+ } else if (emptyBehavior.equals("fail")) {
+ throw new BuildException("Cannot create ZIP archive " +
zipFile +
+ ": no files were included.",
location);
+ } else {
+ // Create.
+ if (zipFile.exists()) return true;
+ // In this case using java.util.zip will not work
+ // because it does not permit a zero-entry archive.
+ // Must create it manually.
+ log("Note: creating empty ZIP archive " + zipFile,
Project.MSG_INFO);
+ try {
+ OutputStream os = new FileOutputStream(zipFile);
+ try {
+ // Cf. PKZIP specification.
+ byte[] empty = new byte[22];
+ empty[0] = 80; // P
+ empty[1] = 75; // K
+ empty[2] = 5;
+ empty[3] = 6;
+ // remainder zeros
+ os.write(empty);
+ } finally {
+ os.close();
+ }
+ } catch (IOException ioe) {
+ throw new BuildException("Could not create empty ZIP
archive", ioe, location);
+ }
+ return true;
+ }
+ } else {
+ // Probably unnecessary but just for clarity:
+ if (!zipFile.exists()) return false;
+ for (int i=0; i<files.length; i++) {
+ if (files[i].lastModified() > zipFile.lastModified()) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ protected static File[] grabFiles(FileScanner[] scanners) {
+ Vector files = new Vector ();
+ for (int i = 0; i < scanners.length; i++) {
+ File thisBaseDir = scanners[i].getBasedir();
+ String[] ifiles = scanners[i].getIncludedFiles();
+ for (int j = 0; j < ifiles.length; j++)
+ files.add(new File(thisBaseDir, ifiles[j]));
+ }
+ File[] toret = new File[files.size()];
+ files.copyInto(toret);
+ return toret;
+ }
+
protected void zipDir(File dir, ZipOutputStream zOut, String vPath)
throws IOException
{
+ ZipEntry ze = new ZipEntry (vPath);
+ if (dir != null) ze.setTime (dir.lastModified ());
+ ze.setSize (0);
+ ze.setMethod (ZipEntry.STORED);
+ // This is faintly ridiculous:
+ ze.setCrc (emptyCrc);
+ zOut.putNextEntry (ze);
}
protected void zipFile(InputStream in, ZipOutputStream zOut, String vPath,
<<attachment: zip-jar-test.zip>>
