package de.masters_of_disaster.ant.tasks.ar; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Enumeration; import java.util.Vector; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.MatchingTask; import org.apache.tools.ant.types.EnumeratedAttribute; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.util.FileUtils; import org.apache.tools.ant.util.MergingMapper; import org.apache.tools.ant.util.SourceFileScanner; import org.apache.tools.zip.UnixStat; /** * Creates an ar archive. * * @ant.task category="packaging" */ public class Ar extends MatchingTask { File destFile; File baseDir; private ArLongFileMode longFileMode = new ArLongFileMode(); Vector filesets = new Vector(); /** * Indicates whether the user has been warned about long files already. */ private boolean longWarningGiven = false; /** * Add a new fileset with the option to specify permissions * @return the ar fileset to be used as the nested element. */ public ArFileSet createArFileSet() { ArFileSet fileset = new ArFileSet(); filesets.addElement(fileset); return fileset; } /** * Set the name/location of where to create the ar file. * @param destFile The output of the tar */ public void setDestFile(File destFile) { this.destFile = destFile; } /** * This is the base directory to look in for things to ar. * @param baseDir the base directory. */ public void setBasedir(File baseDir) { this.baseDir = baseDir; } /** * Set how to handle long files, those with a name>16 chars or containing spaces. * Optional, default=warn. * <p> * Allowable values are * <ul> * <li> truncate - names are truncated to the maximum length, spaces are replaced by '_' * <li> fail - names greater than the maximum cause a build exception * <li> warn - names greater than the maximum cause a warning and TRUNCATE is used * <li> bsd - BSD variant is used if any names are greater than the maximum. * <li> gnu - GNU variant is used if any names are greater than the maximum. * <li> omit - files with a name greater than the maximum are omitted from the archive * </ul> * @param mode the mode to handle long file names. */ public void setLongfile(ArLongFileMode mode) { this.longFileMode = mode; } /** * do the business * @throws BuildException on error */ public void execute() throws BuildException { if (destFile == null) { throw new BuildException("destFile attribute must be set!", getLocation()); } if (destFile.exists() && destFile.isDirectory()) { throw new BuildException("destFile is a directory!", getLocation()); } if (destFile.exists() && !destFile.canWrite()) { throw new BuildException("Can not write to the specified destFile!", getLocation()); } Vector savedFileSets = (Vector) filesets.clone(); try { if (baseDir != null) { if (!baseDir.exists()) { throw new BuildException("basedir does not exist!", getLocation()); } // add the main fileset to the list of filesets to process. ArFileSet mainFileSet = new ArFileSet(fileset); mainFileSet.setDir(baseDir); filesets.addElement(mainFileSet); } if (filesets.size() == 0) { throw new BuildException("You must supply either a basedir " + "attribute or some nested filesets.", getLocation()); } // check if ar is out of date with respect to each // fileset boolean upToDate = true; for (Enumeration e = filesets.elements(); e.hasMoreElements();) { ArFileSet fs = (ArFileSet) e.nextElement(); String[] files = fs.getFiles(getProject()); if (!archiveIsUpToDate(files, fs.getDir(getProject()))) { upToDate = false; } for (int i = 0; i < files.length; ++i) { if (destFile.equals(new File(fs.getDir(getProject()), files[i]))) { throw new BuildException("An ar file cannot include " + "itself", getLocation()); } } } if (upToDate) { log("Nothing to do: " + destFile.getAbsolutePath() + " is up to date.", Project.MSG_INFO); return; } log("Building ar: " + destFile.getAbsolutePath(), Project.MSG_INFO); ArOutputStream aOut = null; try { aOut = new ArOutputStream( new BufferedOutputStream( new FileOutputStream(destFile))); if (longFileMode.isTruncateMode() || longFileMode.isWarnMode()) { aOut.setLongFileMode(ArOutputStream.LONGFILE_TRUNCATE); } else if (longFileMode.isFailMode() || longFileMode.isOmitMode()) { aOut.setLongFileMode(ArOutputStream.LONGFILE_ERROR); } else if (longFileMode.isBsdMode()) { aOut.setLongFileMode(ArOutputStream.LONGFILE_BSD); } else { // GNU aOut.setLongFileMode(ArOutputStream.LONGFILE_GNU); } longWarningGiven = false; for (Enumeration e = filesets.elements(); e.hasMoreElements();) { ArFileSet fs = (ArFileSet) e.nextElement(); String[] files = fs.getFiles(getProject()); if (files.length > 1 && fs.getFullpath().length() > 0) { throw new BuildException("fullpath attribute may only " + "be specified for " + "filesets that specify a " + "single file."); } for (int i = 0; i < files.length; i++) { File f = new File(fs.getDir(getProject()), files[i]); arFile(f, aOut, fs); } } } catch (IOException ioe) { String msg = "Problem creating AR: " + ioe.getMessage(); throw new BuildException(msg, ioe, getLocation()); } finally { FileUtils.close(aOut); } } finally { filesets = savedFileSets; } } /** * ar a file * @param file the file to ar * @param aOut the output stream * @param arFileSet the fileset that the file came from. * @throws IOException on error */ protected void arFile(File file, ArOutputStream aOut, ArFileSet arFileSet) throws IOException { FileInputStream fIn = null; if (file.isDirectory()) { return; } String fileName = file.getName(); String fullpath = arFileSet.getFullpath(); if (fullpath.length() > 0) { fileName = fullpath.substring(fullpath.lastIndexOf('/')); } // don't add "" to the archive if (fileName.length() <= 0) { return; } try { if ((fileName.length() >= ArConstants.NAMELEN) || (-1 != fileName.indexOf(' '))) { if (longFileMode.isOmitMode()) { log("Omitting: " + fileName, Project.MSG_INFO); return; } else if (longFileMode.isWarnMode()) { if (!longWarningGiven) { log("Resulting ar file contains truncated or space converted filenames", Project.MSG_WARN); longWarningGiven = true; } log("Entry: \"" + fileName + "\" longer than " + ArConstants.NAMELEN + " characters or containing spaces.", Project.MSG_WARN); } else if (longFileMode.isFailMode()) { throw new BuildException("Entry: \"" + fileName + "\" longer than " + ArConstants.NAMELEN + "characters or containting spaces.", getLocation()); } } ArEntry ae = new ArEntry(fileName); ae.setFileDate(file.lastModified()); ae.setUserId(arFileSet.getUid()); ae.setGroupId(arFileSet.getGid()); ae.setMode(arFileSet.getMode()); ae.setSize(file.length()); aOut.putNextEntry(ae); fIn = new FileInputStream(file); byte[] buffer = new byte[8 * 1024]; int count = 0; do { aOut.write(buffer, 0, count); count = fIn.read(buffer, 0, buffer.length); } while (count != -1); aOut.closeEntry(); } finally { if (fIn != null) { fIn.close(); } } } /** * Is the archive up to date in relationship to a list of files. * @param files the files to check * @param dir the base directory for the files. * @return true if the archive is up to date. */ protected boolean archiveIsUpToDate(String[] files, File dir) { SourceFileScanner sfs = new SourceFileScanner(this); MergingMapper mm = new MergingMapper(); mm.setTo(destFile.getAbsolutePath()); return sfs.restrict(files, dir, null, mm).length == 0; } /** * This is a FileSet with the option to specify permissions * and other attributes. */ public static class ArFileSet extends FileSet { private String[] files = null; private int fileMode = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM; private int uid; private int gid; private String fullpath = ""; /** * Creates a new <code>ArFileSet</code> instance. * Using a fileset as a constructor argument. * * @param fileset a <code>FileSet</code> value */ public ArFileSet(FileSet fileset) { super(fileset); } /** * Creates a new <code>ArFileSet</code> instance. * */ public ArFileSet() { super(); } /** * Get a list of files and directories specified in the fileset. * @param p the current project. * @return a list of file and directory names, relative to * the baseDir for the project. */ public String[] getFiles(Project p) { if (files == null) { DirectoryScanner ds = getDirectoryScanner(p); files = ds.getIncludedFiles(); } return files; } /** * A 3 digit octal string, specify the user, group and * other modes in the standard Unix fashion; * optional, default=0644 * @param octalString a 3 digit octal string. */ public void setMode(String octalString) { this.fileMode = UnixStat.FILE_FLAG | Integer.parseInt(octalString, 8); } /** * @return the current mode. */ public int getMode() { return fileMode; } /** * The UID for the ar entry; optional, default="0" * @param uid the id of the user for the ar entry. */ public void setUid(int uid) { this.uid = uid; } /** * @return the UID for the ar entry */ public int getUid() { return uid; } /** * The GID for the ar entry; optional, default="0" * @param gid the group id. */ public void setGid(int gid) { this.gid = gid; } /** * @return the group identifier. */ public int getGid() { return gid; } /** * If the fullpath attribute is set, the file in the fileset * is written with the last part of the path in the archive. * If the fullpath ends in '/' the file is omitted from the archive. * It is an error to have more than one file specified in such a fileset. * @param fullpath the path to use for the file in a fileset. */ public void setFullpath(String fullpath) { this.fullpath = fullpath; } /** * @return the path to use for a single file fileset. */ public String getFullpath() { return fullpath; } } /** * Set of options for long file handling in the task. */ public static class ArLongFileMode extends EnumeratedAttribute { /** permissible values for longfile attribute */ public static final String WARN = "warn", FAIL = "fail", TRUNCATE = "truncate", GNU = "gnu", BSD = "bsd", OMIT = "omit"; private final String[] validModes = {WARN, FAIL, TRUNCATE, GNU, BSD, OMIT}; /** Constructor, defaults to "warn" */ public ArLongFileMode() { super(); setValue(WARN); } /** * @return the possible values for this enumerated type. */ public String[] getValues() { return validModes; } /** * @return true if value is "truncate". */ public boolean isTruncateMode() { return TRUNCATE.equalsIgnoreCase(getValue()); } /** * @return true if value is "warn". */ public boolean isWarnMode() { return WARN.equalsIgnoreCase(getValue()); } /** * @return true if value is "gnu". */ public boolean isGnuMode() { return GNU.equalsIgnoreCase(getValue()); } /** * @return true if value is "bsd". */ public boolean isBsdMode() { return BSD.equalsIgnoreCase(getValue()); } /** * @return true if value is "fail". */ public boolean isFailMode() { return FAIL.equalsIgnoreCase(getValue()); } /** * @return true if value is "omit". */ public boolean isOmitMode() { return OMIT.equalsIgnoreCase(getValue()); } } }