Mercurial > cgi-bin > hgweb.cgi > JpegWasher
changeset 33:3d86f0391168
Work on improving the build system.
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Building.html Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- Skeleton or template web page, in the standard style. --> +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <title>Building JpegWasher</title> + <style> +html { font-family: "TeX Gyre Schola", serif; } +h1, h2, h3, h4, h5, h6 { font-family: "Avenir Next", sans-serif; } +pre, code, kbd, samp { font-family: "Menlo", monospace; ; font-size: 85%; } + </style> + </head> + <body> + <h1>Building JpegWasher</h1> + <h2>Prerequisites</h2> + <ul> + <li><a href="https://ant.apache.org/">Apache Ant</a>, with the following + extensions (note that the extensions are already present in the lib + subdirectory, but you will need to install Ant).</li> + <ul> + <li><a href="http://ant-contrib.sourceforge.net/">Ant-Contrib</a></li> + <li><a href="https://github.com/UltraMixer/JarBundler">JarBundler</a> + (if building on a Mac)</li> + <li><a href="http://launch4j.sourceforge.net/">launch4j</a> (if building + on Windows)</li> + </ul> + <li>Java JDK 1.8 or better (see notes).</li> + <li>Kotlin</li> + <li>Exiv2</li> + <li>A C++ Compiler</li> + <li>Make (Nmake on Windows)</li> + </ul> + <h2>JpegWasher Is Not Pure Java</h2> + <p>This means two things: </p> + <ol> + <li>You need a C++ compiler in addition to a Kotlin compiler (and a Java + one) to build JpegWasher.</li> + <li>The result of a build will run only on the architecture you built it + on.</li> + </ol> + <p>It <em>is</em> possible to create a semi-portable JAR that runs on both + Linux and Windows (it contains both <code>*.so</code> libraries and <code>*.dll</code> + ones, and loads the correct ones at run time, see <code>linwin.jar</code>), + but the process for doing so is not totally automated. It is alas not + possible to support the Macintosh in the same JAR as well, because a Mac + app in Java 1.8 requires making a few nonportable, Apple-only API calls, + whose presence will cause <code>ClassNotFoundException</code> to be + thrown at runtime on non-Macintosh systems.</p> + <p>As to why, the answer is simple: Try as I could, I could not find any + pure Java libraries that could read <em>and write</em> image metadata. + Therefore I had to use a C++ library.</p> + <h2>Which Version of Java to Use?</h2> + <p>In short, Java 1.8. Most systems don't yet have OpenJDK 11 or greater + installed, so using a compiler newer than 1.8 is asking for trouble. All + code <em>should</em> build on OpenJDK 11 or greater, with the exception + of the OS-dependent code for the Macintosh (which will have to be recoded + to use the <code>java.awt.Desktop</code> class). The latter would be a + net win, as it is portable, and would spell the death of the only bit of + OS-dependent Kotlin code in this application.</p> + <p>In another year or two, I will probably make OpenJDK 11 or greater the + preferred version.</p> + </body> +</html>
--- a/Makefile.doc Fri Apr 24 14:01:03 2020 -0700 +++ b/Makefile.doc Fri Apr 24 19:45:57 2020 -0700 @@ -1,6 +1,6 @@ .PHONY: clean -all: Readme.rst Readme.pdf +all: Readme.rst Readme.pdf Building.rst Building.pdf clean: -rm *.rst *.pdf *.ps *.nrf
--- a/build.xml Fri Apr 24 14:01:03 2020 -0700 +++ b/build.xml Fri Apr 24 19:45:57 2020 -0700 @@ -1,5 +1,9 @@ <?xml version="1.0" encoding="UTF-8"?> -<project name="JpegWasher" default="help" basedir="." xmlns:fx="javafx:com.sun.javafx.tools.ant"> +<project name="JpegWasher" default="help" basedir="." + xmlns:contrib="antlib:net.sf.antcontrib" + xmlns:jarbundler="antlib:com.ultramixer.jarbundler" + xmlns:launch4j="antlib:net.sf.launch4j.ant" + xmlns:mod="antlib:de.masters_of_disaster.ant.tasks"> <!-- import all environment variables as env.* --> <property environment="env"/> @@ -17,15 +21,8 @@ <env-require name="JRE_HOME"/> <env-require name="KOTLIN_HOME"/> - <!-- define the kotlin task --> - <property name="kotlin.lib" value="${env.KOTLIN_HOME}/lib"/> - <typedef resource="org/jetbrains/kotlin/ant/antlib.xml" - classpath="${kotlin.lib}/kotlin-ant.jar"/> - - <!-- define the package-building tasks --> - <taskdef resource="com/sun/javafx/tools/ant/antlib.xml" - uri="javafx:com.sun.javafx.tools.ant" - classpath="${env.JRE_HOME}/lib/ant-javafx.jar"/> + <!-- load launch4j (Windows app builder) --> + <!-- load m-o-d (Linux Gnome app builder) --> <!-- cribbed from https://stackoverflow.com/questions/7129672/uppercase-lowercase-capitalize-an-ant-property --> <scriptdef language="javascript" name="toLowerCase"> @@ -49,6 +46,66 @@ <property name="nat.dir" value="${src.home}/name/blackcap/exifwasher/exiv2"/> <property name="bin.dir" value="${src.home}/name/blackcap/exifwasher/binaries"/> + <!-- load the ant-contrib tasks --> + <taskdef resource="net/sf/antcontrib/antlib.xml" + uri="antlib:net.sf.antcontrib"> + <classpath> + <fileset dir="${lib.home}/ant-contrib" includes="*.jar"/> + </classpath> + </taskdef> + + <!-- load jarbundler (Mac app bundler) tasks --> + <contrib:if> + <os family="mac"/> + <contrib:then> + <taskdef name="create" + classname="com.ultramixer.jarbundler.JarBundler" + classpath="${lib.home}/jarbundler-core-3.3.0.jar" + uri="antlib:com.ultramixer.jarbundler"> + </taskdef> + </contrib:then> + </contrib:if> + + <!-- load launch4j (Windows app bundler) tasks --> + <contrib:if> + <os family="windows"/> + <contrib:then> + <taskdef name="create" + classname="net.sf.launch4j.ant.Launch4jTask" + uri="antlib:net.sf.launch4j.ant"> + <classpath> + <fileset dir="${lib.dir}/launch4j" includes="*.jar"/> + </classpath> + </taskdef> + </contrib:then> + </contrib:if> + + <!-- load mod (Gnome app bundler) tasks --> + <contrib:if> + <os family="unix"/> + <contrib:then> + <taskdef name="calculatesize" + classname="de.masters_of_disaster.ant.tasks.calculatesize.CalculateSize" + uri="antlib:de.masters_of_disaster.ant.tasks"> + <classpath> + <fileset dir="${lib.dir}/mod" includes="*.jar"/> + </classpath> + </taskdef> + <taskdef name="deb" + classname="de.masters_of_disaster.ant.tasks.deb.Deb" + uri="antlib:de.masters_of_disaster.ant.tasks"/> + <classpath> + <fileset dir="${lib.dir}/mod" includes="*.jar"/> + </classpath> + </taskdef> + </contrib:then> + </contrib:if> + + <!-- define the kotlin task --> + <property name="kotlin.lib" value="${env.KOTLIN_HOME}/lib"/> + <typedef resource="org/jetbrains/kotlin/ant/antlib.xml" + classpath="${kotlin.lib}/kotlin-ant.jar"/> + <!-- help message --> <target name="help"> <echo>You can use the following targets:</echo> @@ -63,26 +120,40 @@ <echo>prompt> ant all </echo> </target> + <!-- Determine OS type --> + <condition property="os.type" value="mac"> + <os family="mac"/> + </condition> + <condition property="os.type" value="windows"> + <os family="windows"/> + </condition> + <condition property="os.type" value="linux"> + <!-- notes: 1) XXX not all non-mac UNIX systems are Linux, but this assumes + so; 2) this clause MUST appear AFTER the "mac" one --> + <os family="unix"/> + </condition> + <property name="os.type" value="unknown"/> + <!-- Define the CLASSPATH --> <target name="classpath"> - <path id="std.classpath"> - <fileset dir="${lib.home}"> - <include name="*.jar"/> - </fileset> - </path> <path id="compile.classpath"> - <path refid="std.classpath"/> <pathelement location="${src.home}"/> </path> - <path id="test.classpath"> - <path refid="std.classpath"/> - <pathelement location="${work.home}"/> - </path> </target> <!-- make needed directories --> <target name="mkdirs"> <mkdir dir="${lib.home}"/> + <mkdir dir="${src.home}/name/blackcap/exifwasher/binaries/${os.type}"/> + </target> + + <!-- rename files containing OS-dependant code --> + <target name="osdep"> + <exec executable="${env.JRE_HOME}/bin/java"> + <arg value="-jar"/> + <arg value="../Osdep/osdep.jar"/> + <arg value="${src.home}"/> + </exec> </target> <!-- do everything but install --> @@ -90,7 +161,7 @@ description="Clean work dirs, compile, make JAR."/> <!-- compile *.kt to *.class --> - <target name="compile" depends="mkdirs,classpath" + <target name="compile" depends="mkdirs,osdep,classpath" description="Compile Java sources to ${work.home}"> <kotlinc src="${src.home}" output="${work.jar}" classpathref="compile.classpath">
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/build_mod.xml Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project name="mod" default="help" basedir="."> + <!-- import all environment variables as env.* --> + <property environment="env"/> + + <!-- ensure required environment variables are set --> + <macrodef name="env-require"> + <attribute name="name"/> + <sequential> + <fail message="Environment variable @{name} not set!"> + <condition> + <not><isset property="env.@{name}"/></not> + </condition> + </fail> + </sequential> + </macrodef> + <env-require name="JRE_HOME"/> + + <!-- load launch4j (Windows app builder) --> + <!-- load m-o-d (Linux Gnome app builder) --> + + <!-- cribbed from https://stackoverflow.com/questions/7129672/uppercase-lowercase-capitalize-an-ant-property --> + <scriptdef language="javascript" name="toLowerCase"> + <attribute name="value" /> + <attribute name="target" /> + <![CDATA[ + project.setProperty( attributes.get( "target" ), + attributes.get( "value" ).toLowerCase() ); + ]]> + </scriptdef> + + <!-- Define the properties used by the build --> + <property name="app.name" value="${ant.project.name}"/> + <toLowerCase target="lc.app.name" value="${app.name}"/> + <property name="jar.name" value="${basedir}/${lc.app.name}.jar"/> + <property name="src.home" value="${basedir}/src"/> + <property name="work.home" value="${basedir}/work"/> + + <!-- help message --> + <target name="help"> + <echo>You can use the following targets:</echo> + <echo> </echo> + <echo> help : (default) Prints this message </echo> + <echo> clean : Deletes work directories</echo> + <echo> compile : Compiles source into class files</echo> + <echo> jar : Make JAR file.</echo> + <echo> </echo> + <echo>For example, to clean, compile, and package all at once, run:</echo> + <echo>prompt> ant -f build_mod.xml all </echo> + </target> + + <!-- do everything --> + <target name="all" depends="clean,jar" + description="Clean work dirs, compile, make JAR."/> + + <!-- clean old cruft out of our way --> + <target name="clean" + description="Delete old work and dist directories."> + <delete dir="${work.home}"/> + </target> + + <!-- make new dist and work trees --> + <target name="mkdirs" description="Create working dirs"> + <mkdir dir="${work.home}"/> + </target> + + <!-- compile *.java to *.class --> + <target name="compile" depends="mkdirs" + description="Compile Java sources to ${work.home}"> + <javac srcdir="${src.home}" destdir="${work.home}" debug="true" + includeAntRuntime="true"> + </javac> + </target> + + <!-- make .jar file --> + <target name="jar" depends="compile" description="Create JAR file."> + <jar destfile="${jar.name}"> + <fileset dir="${work.home}"/> + </jar> + </target> + +</project>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/Ar.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,468 @@ +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()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArConstants.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,62 @@ +package de.masters_of_disaster.ant.tasks.ar; + +/** + * This interface contains all the definitions used in the package. + */ + +public interface ArConstants { + /** + * The length of the name field in a file header. + */ + int NAMELEN = 16; + + /** + * The length of the file date field in a file header. + */ + int FILEDATELEN = 12; + + /** + * The length of the user id field in a file header. + */ + int UIDLEN = 6; + + /** + * The length of the group id field in a file header. + */ + int GIDLEN = 6; + + /** + * The length of the mode field in a file header. + */ + int MODELEN = 8; + + /** + * The length of the size field in a file header. + */ + int SIZELEN = 10; + + /** + * The length of the magic field in a file header. + */ + int MAGICLEN = 2; + + /** + * The magic tag put at the end of a file header. + */ + String HEADERMAGIC = "`\n"; + + /** + * The headerlength of a file header. + */ + int HEADERLENGTH = NAMELEN + FILEDATELEN + UIDLEN + GIDLEN + MODELEN + SIZELEN + MAGICLEN; + + /** + * The length of the magic field in a file header. + */ + byte[] PADDING = { '\n' }; + + /** + * The magic tag representing an ar archive. + */ + byte[] ARMAGIC = { '!', '<', 'a', 'r', 'c', 'h', '>', '\n' }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArEntry.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,355 @@ +package de.masters_of_disaster.ant.tasks.ar; + +import java.io.File; +import java.util.Date; + +/** + * This class represents an entry in an Ar archive. It consists + * of the entry's header, as well as the entry's File. Entries + * can be instantiated in one of three ways, depending on how + * they are to be used. + * <p> + * ArEntries that are created from the header bytes read from + * an archive are instantiated with the ArEntry( byte[] ) + * constructor. These entries will be used when extracting from + * or listing the contents of an archive. These entries have their + * header filled in using the header bytes. They also set the File + * to null, since they reference an archive entry not a file. + * <p> + * ArEntries that are created from Files that are to be written + * into an archive are instantiated with the ArEntry( File ) + * constructor. These entries have their header filled in using + * the File's information. They also keep a reference to the File + * for convenience when writing entries. + * <p> + * Finally, ArEntries can be constructed from nothing but a name. + * This allows the programmer to construct the entry by hand, for + * instance when only an InputStream is available for writing to + * the archive, and the header information is constructed from + * other information. In this case the header fields are set to + * defaults and the File is set to null. + * + * <p> + * The C structure for an Ar Entry's header is: + * <pre> + * struct header { + * char filename[16]; + * char filedate[12]; + * char uid[6]; + * char gid[6]; + * char mode[8]; + * char size[10]; + * char magic[2]; + * } header; + * </pre> + * + */ + +public class ArEntry implements ArConstants { + /** The entry's filename. */ + private StringBuffer filename; + + /** The entry's file date. */ + private long fileDate; + + /** The entry's user id. */ + private int userId; + + /** The entry's group id. */ + private int groupId; + + /** The entry's permission mode. */ + private int mode; + + /** The entry's size. */ + private long size; + + /** The entry's magic tag. */ + private StringBuffer magic; + + /** The entry's file reference */ + private File file; + + /** Default permissions bits for files */ + public static final int DEFAULT_FILE_MODE = 0100644; + + /** Convert millis to seconds */ + public static final int MILLIS_PER_SECOND = 1000; + + /** + * Construct an empty entry and prepares the header values. + */ + private ArEntry () { + this.magic = new StringBuffer(HEADERMAGIC); + this.filename = new StringBuffer(); + this.userId = 0; + this.groupId = 0; + this.file = null; + } + + /** + * Construct an entry with only a name. This allows the programmer + * to construct the entry's header "by hand". File is set to null. + * + * @param name the entry name + */ + public ArEntry(String name) { + this(); + if (name.endsWith("/")) { + throw new IllegalArgumentException("ar archives can only contain files"); + } + this.filename = new StringBuffer(name); + this.mode = DEFAULT_FILE_MODE; + this.userId = 0; + this.groupId = 0; + this.size = 0; + this.fileDate = (new Date()).getTime() / MILLIS_PER_SECOND; + } + + /** + * Construct an entry for a file. File is set to file, and the + * header is constructed from information from the file. + * + * @param file The file that the entry represents. + */ + public ArEntry(File file) { + this(); + if (file.isDirectory()) { + throw new IllegalArgumentException("ar archives can only contain files"); + } + this.file = file; + this.filename = new StringBuffer(file.getName()); + this.fileDate = file.lastModified() / MILLIS_PER_SECOND; + this.mode = DEFAULT_FILE_MODE; + this.size = file.length(); + } + + /** + * Construct an entry from an archive's header bytes. File is set + * to null. + * + * @param headerBuf The header bytes from an ar archive entry. + */ + public ArEntry(byte[] headerBuf) { + this(); + this.parseArHeader(headerBuf); + } + + /** + * Determine if the two entries are equal. Equality is determined + * by the header names being equal. + * + * @param it Entry to be checked for equality. + * @return True if the entries are equal. + */ + public boolean equals(ArEntry it) { + return this.getFilename().equals(it.getFilename()); + } + + /** + * Determine if the two entries are equal. Equality is determined + * by the header names being equal. + * + * @param it Entry to be checked for equality. + * @return True if the entries are equal. + */ + public boolean equals(Object it) { + if (it == null || getClass() != it.getClass()) { + return false; + } + return equals((ArEntry) it); + } + + /** + * Hashcodes are based on entry names. + * + * @return the entry hashcode + */ + public int hashCode() { + return getFilename().hashCode(); + } + + /** + * Get this entry's name. + * + * @return This entry's name. + */ + public String getFilename() { + return this.filename.toString(); + } + + /** + * Set this entry's name. + * + * @param name This entry's new name. + */ + public void setFilename(String filename) { + this.filename = new StringBuffer(filename); + } + + /** + * Set the mode for this entry + * + * @param mode the mode for this entry + */ + public void setMode(int mode) { + this.mode = mode; + } + + /** + * Get this entry's user id. + * + * @return This entry's user id. + */ + public int getUserId() { + return this.userId; + } + + /** + * Set this entry's user id. + * + * @param userId This entry's new user id. + */ + public void setUserId(int userId) { + this.userId = userId; + } + + /** + * Get this entry's group id. + * + * @return This entry's group id. + */ + public int getGroupId() { + return this.groupId; + } + + /** + * Set this entry's group id. + * + * @param groupId This entry's new group id. + */ + public void setGroupId(int groupId) { + this.groupId = groupId; + } + + /** + * Convenience method to set this entry's group and user ids. + * + * @param userId This entry's new user id. + * @param groupId This entry's new group id. + */ + public void setIds(int userId, int groupId) { + this.setUserId(userId); + this.setGroupId(groupId); + } + + /** + * Set this entry's modification time. The parameter passed + * to this method is in "Java time". + * + * @param time This entry's new modification time. + */ + public void setFileDate(long time) { + this.fileDate = time / MILLIS_PER_SECOND; + } + + /** + * Set this entry's modification time. + * + * @param time This entry's new modification time. + */ + public void setFileDate(Date time) { + this.fileDate = time.getTime() / MILLIS_PER_SECOND; + } + + /** + * Get this entry's modification time. + * + * @return time This entry's new modification time. + */ + public Date getFileDate() { + return new Date(this.fileDate * MILLIS_PER_SECOND); + } + + /** + * Get this entry's file. + * + * @return This entry's file. + */ + public File getFile() { + return this.file; + } + + /** + * Get this entry's mode. + * + * @return This entry's mode. + */ + public int getMode() { + return this.mode; + } + + /** + * Get this entry's file size. + * + * @return This entry's file size. + */ + public long getSize() { + return this.size; + } + + /** + * Set this entry's file size. + * + * @param size This entry's new file size. + */ + public void setSize(long size) { + this.size = size; + } + + /** + * Write an entry's header information to a header buffer. + * + * @param outbuf The tar entry header buffer to fill in. + */ + public void writeEntryHeader(byte[] outbuf) { + int offset = 0; + + offset = ArUtils.getNameBytes(this.filename, outbuf, offset, NAMELEN); + offset = ArUtils.getLongBytes(this.fileDate, outbuf, offset, FILEDATELEN); + offset = ArUtils.getIntegerBytes(this.userId, outbuf, offset, UIDLEN); + offset = ArUtils.getIntegerBytes(this.groupId, outbuf, offset, GIDLEN); + offset = ArUtils.getOctalBytes(this.mode, outbuf, offset, MODELEN); + offset = ArUtils.getLongBytes(this.size, outbuf, offset, SIZELEN); + offset = ArUtils.getNameBytes(this.magic, outbuf, offset, MAGICLEN); + + while (offset < outbuf.length) { + outbuf[offset++] = 0; + } + } + + /** + * Parse an entry's header information from a header buffer. + * + * @param header The ar entry header buffer to get information from. + */ + public void parseArHeader(byte[] header) { + throw new UnsupportedOperationException("parseArHeader(byte[]) not yet implmented"); +// int offset = 0; +// +// this.filename = TarUtils.parseName(header, offset, NAMELEN); +// offset += NAMELEN; +// this.fileDate = TarUtils.parseOctal(header, offset, FILEDATELEN); +// offset += FILEDATELEN; +// this.userId = (int) TarUtils.parseOctal(header, offset, UIDLEN); +// offset += UIDLEN; +// this.groupId = (int) TarUtils.parseOctal(header, offset, GIDLEN); +// offset += GIDLEN; +// this.mode = (int) TarUtils.parseOctal(header, offset, MODELEN); +// offset += MODELEN; +// this.size = TarUtils.parseOctal(header, offset, SIZELEN); +// offset += SIZELEN; +// this.magic = TarUtils.parseName(header, offset, MAGICLEN); +// offset += MAGICLEN; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArOutputStream.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,167 @@ +package de.masters_of_disaster.ant.tasks.ar; + +import java.io.FilterOutputStream; +import java.io.OutputStream; +import java.io.IOException; + +/** + * The ArOutputStream writes an ar archive as an OutputStream. + * Methods are provided to put entries, and then write their contents + * by writing to this stream using write(). + */ +public class ArOutputStream extends FilterOutputStream { + /** Fail if a long file name is required in the archive or the name contains spaces. */ + public static final int LONGFILE_ERROR = 0; + + /** Long paths will be truncated in the archive. Spaces are replaced by '_' */ + public static final int LONGFILE_TRUNCATE = 1; + + /** GNU ar variant is used to store long file names and file names with spaced in the archive. */ + public static final int LONGFILE_GNU = 2; + + /** BSD ar variant is used to store long file names and file names with spaced in the archive. */ + public static final int LONGFILE_BSD = 3; + + protected int currSize; + protected int currBytes; + protected byte[] oneBuf; + protected int longFileMode = LONGFILE_ERROR; + protected boolean writingStarted = false; + protected boolean inEntry = false; + + public ArOutputStream(OutputStream os) throws IOException { + super(os); + if (null == os) { + throw new NullPointerException("os must not be null"); + } + this.out.write(ArConstants.ARMAGIC,0,ArConstants.ARMAGIC.length); + this.oneBuf = new byte[1]; + } + + public void setLongFileMode(int longFileMode) { + if (writingStarted) { + throw new IllegalStateException("longFileMode cannot be changed after writing to the archive has begun"); + } + if (LONGFILE_GNU == longFileMode) { + throw new UnsupportedOperationException("GNU variant isn't implemented yet"); + } + if (LONGFILE_BSD == longFileMode) { + throw new UnsupportedOperationException("BSD variant isn't implemented yet"); + } + this.longFileMode = longFileMode; + } + + /** + * Put an entry on the output stream. This writes the entry's + * header record and positions the output stream for writing + * the contents of the entry. Once this method is called, the + * stream is ready for calls to write() to write the entry's + * contents. Once the contents are written, closeEntry() + * <B>MUST</B> be called to ensure that all buffered data + * is completely written to the output stream. + * + * @param entry The ArEntry to be written to the archive. + */ + public void putNextEntry(ArEntry entry) throws IOException { + writingStarted = true; + if (inEntry) { + throw new IOException("the current entry has to be closed before starting a new one"); + } + String filename = entry.getFilename(); + if ((filename.length() >= ArConstants.NAMELEN) + && (longFileMode != LONGFILE_TRUNCATE)) { + throw new RuntimeException("file name \"" + entry.getFilename() + + "\" is too long ( > " + + ArConstants.NAMELEN + " bytes )"); + } + if (-1 != filename.indexOf(' ')) { + if (longFileMode == LONGFILE_TRUNCATE) { + entry.setFilename(filename.replace(' ','_')); + } else { + throw new RuntimeException("file name \"" + entry.getFilename() + + "\" contains spaces"); + } + } + + byte[] headerBuf = new byte[ArConstants.HEADERLENGTH]; + entry.writeEntryHeader(headerBuf); + this.out.write(headerBuf,0,ArConstants.HEADERLENGTH); + + this.currBytes = 0; + this.currSize = (int) entry.getSize(); + inEntry = true; + } + + /** + * Close an entry. This method MUST be called for all file + * entries that contain data. The reason is that we must + * pad an entries data if it is of odd size. + */ + public void closeEntry() throws IOException { + if (!inEntry) { + throw new IOException("we are not in an entry currently"); + } + + if (this.currBytes < this.currSize) { + throw new IOException("entry closed at '" + this.currBytes + + "' before the '" + this.currSize + + "' bytes specified in the header were written"); + } + + if (1 == (this.currSize & 1)) { + this.out.write(ArConstants.PADDING,0,1); + } + + inEntry = false; + } + + /** + * Writes a byte to the current ar archive entry. + * + * This method simply calls write( byte[], int, int ). + * + * @param b The byte to write to the archive. + */ + public void write(int b) throws IOException { + this.oneBuf[0] = (byte) b; + this.write(this.oneBuf, 0, 1); + } + + /** + * Writes bytes to the current ar archive entry. + * + * This method simply calls write( byte[], int, int ). + * + * @param wBuf The buffer to write to the archive. + */ + public void write(byte[] wBuf) throws IOException { + this.write(wBuf, 0, wBuf.length); + } + + /** + * Writes bytes to the current ar archive entry. This method + * is aware of the current entry and will throw an exception if + * you attempt to write bytes past the length specified for the + * current entry. + * + * @param wBuf The buffer to write to the archive. + * @param wOffset The offset in the buffer from which to get bytes. + * @param numToWrite The number of bytes to write. + */ + public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { + if (!inEntry) { + throw new IOException("we are not in an entry currently"); + } + + if ((this.currBytes + numToWrite) > this.currSize) { + throw new IOException("request to write '" + numToWrite + + "' bytes exceeds size in header of '" + + this.currSize + "' bytes"); + } + + if (numToWrite > 0) { + this.out.write(wBuf,wOffset,numToWrite); + this.currBytes += numToWrite; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/ar/ArUtils.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,155 @@ +package de.masters_of_disaster.ant.tasks.ar; + +/** + * This class provides static utility methods to work with byte streams. + */ +public class ArUtils { + /** + * Parse an octal string from a header buffer. This is used for the + * file permission mode value. + * + * @param header The header buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @param length The number of header bytes to parse. + * @return The long value of the octal string. + */ + public static long parseOctal(byte[] header, int offset, int length) { + long result = 0; + int end = offset + length; + + for (int i=offset ; i<end ; i++) { + if (header[i] == (byte) ' ') { + break; + } + result = (result << 3) + (header[i] - '0'); + } + + return result; + } + + /** + * Parse an entry name from a header buffer. + * + * @param header The header buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @param length The number of header bytes to parse. + * @return The header's entry name. + */ + public static StringBuffer parseName(byte[] header, int offset, int length) { + StringBuffer result = new StringBuffer(length); + int end = offset + length; + + for (int i=offset ; i<end ; i++) { + if (header[i] == ' ') { + break; + } + + result.append((char) header[i]); + } + + return result; + } + + /** + * Write a name into a byte array. + * + * @param name The name to write. + * @param buf The byte array into which to write. + * @param offset The offset into the buffer from which to write. + * @param length The number of header bytes to write. + * @return The number of bytes written to the buffer. + */ + public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) { + int i; + int c = name.length(); + + for (i=0 ; i<length && i<c ; i++) { + buf[offset+i] = (byte) name.charAt(i); + } + + while (i<length) { + buf[offset+i] = (byte) ' '; + i++; + } + + return offset + length; + } + + /** + * Write a long value into a byte array. + * + * @param value The value to write. + * @param buf The byte array into which to write. + * @param offset The offset into the buffer from which to write. + * @param length The number of header bytes to write. + * @return The number of bytes written to the buffer. + */ + public static int getLongBytes(long value, byte[] buf, int offset, int length) { + int i; + String tmp = Long.toString(value); + int c = tmp.length(); + + for (i=0 ; i<length && i<c ; i++) { + buf[offset+i] = (byte) tmp.charAt(i); + } + + while (i<length) { + buf[offset+i] = (byte) ' '; + i++; + } + + return offset + length; + } + + /** + * Write an int value into a byte array. + * + * @param value The value to write. + * @param buf The byte array into which to write. + * @param offset The offset into the buffer from which to write. + * @param length The number of header bytes to write. + * @return The number of bytes written to the buffer. + */ + public static int getIntegerBytes(int value, byte[] buf, int offset, int length) { + int i; + String tmp = Integer.toString(value); + int c = tmp.length(); + + for (i=0 ; i<length && i<c ; i++) { + buf[offset+i] = (byte) tmp.charAt(i); + } + + while (i<length) { + buf[offset+i] = (byte) ' '; + i++; + } + + return offset + length; + } + + /** + * Write an octal value into a byte array. + * + * @param value The value to write. + * @param buf The byte array into which to write. + * @param offset The offset into the buffer from which to write. + * @param length The number of header bytes to write. + * @return The number of bytes written to the buffer. + */ + public static int getOctalBytes(long value, byte[] buf, int offset, int length) { + int i; + String tmp = Long.toOctalString(value); + int c = tmp.length(); + + for (i=0 ; i<length && i<c ; i++) { + buf[offset+i] = (byte) tmp.charAt(i); + } + + while (i<length) { + buf[offset+i] = (byte) ' '; + i++; + } + + return offset + length; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/calculatesize/CalculateSize.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,94 @@ +package de.masters_of_disaster.ant.tasks.calculatesize; + +import java.io.File; +import java.util.Enumeration; +import java.util.Vector; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.taskdefs.MatchingTask; +import org.apache.tools.ant.types.FileSet; + +/** + * Calculates the "Installed-Size" of a deb package for the "control"-file. + * + * @ant.task category="packaging" + */ +public class CalculateSize extends MatchingTask { + String realSizeProperty = null; + String diskSizeProperty = null; + Vector fileSets = new Vector(); + File baseDir; + + /** + * Add a new fileset + * + * @return the fileset to be used as the nested element. + */ + public FileSet createFileSet() { + FileSet fileSet = new FileSet(); + fileSets.addElement(fileSet); + return fileSet; + } + + /** + * This is the base directory to look in for things to include. + * + * @param baseDir the base directory. + */ + public void setBaseDir(File baseDir) { + this.baseDir = baseDir; + fileset.setDir(baseDir); + } + + /** + * This is the property to set to the real size. + * + * @param realSizeProperty The property to set to the real size + */ + public void setRealSizeProperty(String realSizeProperty) { + this.realSizeProperty = realSizeProperty; + } + + /** + * This is the property to set to the disk size. + * + * @param diskSizeProperty The property to set to the disk size + */ + public void setDiskSizeProperty(String diskSizeProperty) { + this.diskSizeProperty = diskSizeProperty; + } + + /** + * do the business + * + * @throws BuildException on error + */ + public void execute() throws BuildException { + if ((null == realSizeProperty) && (null == diskSizeProperty)) { + throw new BuildException("realSizeProperty or diskSizeProperty must be set for <CalculateSize>"); + } + + if (null != baseDir) { + // add the main fileset to the list of filesets to process. + fileSets.addElement(fileset); + } + + long realSize = 0; + long diskSize = 0; + for (Enumeration e=fileSets.elements() ; e.hasMoreElements() ; ) { + FileSet fileSet = (FileSet)e.nextElement(); + String[] files = fileSet.getDirectoryScanner(getProject()).getIncludedFiles(); + File fileSetDir = fileSet.getDir(getProject()); + for (int i=0, c=files.length ; i<c ; i++) { + long fileLength = new File(fileSetDir,files[i]).length(); + realSize += fileLength / 1024; + diskSize += (fileLength / 4096 + 1) * 4; + } + } + if (null != realSizeProperty) { + getProject().setNewProperty(realSizeProperty,Long.toString(realSize)); + } + if (null != diskSizeProperty) { + getProject().setNewProperty(diskSizeProperty,Long.toString(diskSize)); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/mod/src/de/masters_of_disaster/ant/tasks/deb/Deb.java Fri Apr 24 19:45:57 2020 -0700 @@ -0,0 +1,354 @@ +package de.masters_of_disaster.ant.tasks.deb; + +import de.masters_of_disaster.ant.tasks.ar.Ar; +import de.masters_of_disaster.ant.tasks.ar.Ar.ArFileSet; +import java.io.File; +import java.util.Enumeration; +import java.util.Vector; +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.taskdefs.Checksum; +import org.apache.tools.ant.taskdefs.Echo; +import org.apache.tools.ant.taskdefs.Echo.EchoLevel; +import org.apache.tools.ant.taskdefs.Mkdir; +import org.apache.tools.ant.taskdefs.MatchingTask; +import org.apache.tools.ant.taskdefs.Tar; +import org.apache.tools.ant.taskdefs.Tar.TarCompressionMethod; +import org.apache.tools.ant.taskdefs.Tar.TarFileSet; +import org.apache.tools.ant.util.FileUtils; +import org.apache.tools.ant.util.MergingMapper; +import org.apache.tools.ant.util.SourceFileScanner; + +/** + * Creates a deb package. + * + * @ant.task category="packaging" + */ +public class Deb extends MatchingTask { + Vector controlFileSets = new Vector(); + Vector dataFileSets = new Vector(); + File baseDir; + File destFile; + File tempDir; + boolean deleteTempFiles = true; + boolean includeMd5sums = false; + Tar controlTarGz = new Tar(); + Tar dataTarGz = new Tar(); + Ar debPackage = new Ar(); + + { + fileset = dataTarGz.createTarFileSet(); + } + + /** + * Add a new fileset for the control files with the option to specify permissions + * + * @return the tar fileset to be used as the nested element. + */ + public TarFileSet createControlFileSet() { + TarFileSet fileSet = controlTarGz.createTarFileSet(); + controlFileSets.addElement(fileSet); + return fileSet; + } + + /** + * Add a new fileset for the data files with the option to specify permissions + * + * @return the tar fileset to be used as the nested element. + */ + public TarFileSet createDataFileSet() { + TarFileSet fileSet = dataTarGz.createTarFileSet(); + dataFileSets.addElement(fileSet); + return fileSet; + } + + /** + * Set the name/location of where to create the deb file. + * + * @param destFile The output of the deb + */ + public void setDestFile(File destFile) { + this.destFile = destFile; + debPackage.setDestFile(destFile); + } + + /** + * This is the base directory to look in for things to include in the data files. + * + * @param baseDir the base directory. + */ + public void setBaseDir(File baseDir) { + this.baseDir = baseDir; + fileset.setDir(baseDir); + } + + /** + * This is the temp directory where to create the temporary files. + * If not set, the current projects baseDir is used. + * + * @param tempDir the temp directory. + */ + public void setTempDir(File tempDir) { + this.tempDir = tempDir; + } + + /** + * This specifies if the temporary files should get deleted. + * + * @param deleteTempFiles whether to delete the temporary files or not. + */ + public void setDeleteTempFiles(boolean deleteTempFiles) { + this.deleteTempFiles = deleteTempFiles; + } + + /** + * This specifies if the MD5 sums of the files in the data section should be + * included in the file "md5sums" in the control section. + * + * @param includeMd5sums whether to include MD5 sums in the control section or not. + */ + public void setIncludeMd5sums(boolean includeMd5sums) { + this.includeMd5sums = includeMd5sums; + } + + /** + * do the business + * + * @throws BuildException on error + */ + public void execute() throws BuildException { + prepareTask(controlTarGz); + prepareTask(dataTarGz); + prepareTask(debPackage); + TarFileSet tarFileSet = controlTarGz.createTarFileSet(); + tarFileSet.setFile(new File(System.getProperty("user.dir"))); + tarFileSet.setUserName("root"); + tarFileSet.setGroup("root"); + tarFileSet.setFullpath("./"); + tarFileSet = dataTarGz.createTarFileSet(); + tarFileSet.setFile(new File(System.getProperty("user.dir"))); + tarFileSet.setUserName("root"); + tarFileSet.setGroup("root"); + tarFileSet.setFullpath("./"); + + if (null == tempDir) { + tempDir = getProject().getBaseDir(); + } + + if (null != baseDir) { + // add the main fileset to the list of filesets to process. + dataFileSets.addElement(fileset); + } else { + fileset.setDir(new File(System.getProperty("user.dir"))); + fileset.setExcludes("**"); + } + + boolean controlFound = false; + for (Enumeration e=controlFileSets.elements() ; e.hasMoreElements() ; ) { + TarFileSet fileSet = (TarFileSet)e.nextElement(); + String[] files = fileSet.getFiles(getProject()); + int i = 0; + int c; + + for (c=files.length ; i<c && !controlFound ; i++) { + if (files[i].endsWith("control") + && (new File(fileSet.getDir(getProject()),files[i])).isFile()) { + controlFound = true; + } + } + } + if (!controlFound) { + throw new BuildException("The control fileset must contain a file \"control\"", getLocation()); + } + + // check if deb is out of date with respect to each fileset + boolean upToDate = true; + for (Enumeration e=controlFileSets.elements() ; e.hasMoreElements() ; ) { + TarFileSet fileSet = (TarFileSet)e.nextElement(); + String[] files = fileSet.getFiles(getProject()); + + if (!packageIsUpToDate(files,fileSet.getDir(getProject()))) { + upToDate = false; + } + } + + for (Enumeration e=dataFileSets.elements() ; e.hasMoreElements() ; ) { + TarFileSet fileSet = (TarFileSet)e.nextElement(); + String[] files = fileSet.getFiles(getProject()); + + if (!packageIsUpToDate(files,fileSet.getDir(getProject()))) { + upToDate = false; + } + } + + if (upToDate) { + log("Nothing to do: " + destFile.getAbsolutePath() + + " is up to date.", Project.MSG_INFO); + return; + } + + log("Building deb: " + destFile.getAbsolutePath(), Project.MSG_INFO); + + Mkdir mkdir = new Mkdir(); + prepareTask(mkdir); + mkdir.setDir(tempDir); + mkdir.perform(); + + EchoLevel echoLevel = new EchoLevel(); + echoLevel.setValue("error"); + File debianBinaryFile = new File(tempDir,"debian-binary"); + Echo echo = new Echo(); + prepareTask(echo); + echo.setFile(debianBinaryFile); + echo.setLevel(echoLevel); + echo.setMessage("2.0\n"); + echo.perform(); + + for (Enumeration e=controlFileSets.elements() ; e.hasMoreElements() ; ) { + TarFileSet fileSet = (TarFileSet)e.nextElement(); + String prefix = fileSet.getPrefix(); + String fullpath = fileSet.getFullpath(); + if ("".equals(fullpath) && !prefix.startsWith("./")) { + if (prefix.startsWith("/")) { + fileSet.setPrefix("." + prefix); + } else { + fileSet.setPrefix("./" + prefix); + } + } + if ((fullpath.length() > 0) && !fullpath.startsWith("./")) { + fileSet.setPrefix(""); + if (fullpath.startsWith("/")) { + fileSet.setFullpath("." + fullpath); + } else { + fileSet.setFullpath("./" + fullpath); + } + } + if ((0 == fileSet.getUid()) && ("" == fileSet.getUserName())) { + fileSet.setUserName("root"); + } + if ((0 == fileSet.getGid()) && ("" == fileSet.getGroup())) { + fileSet.setGroup("root"); + } + } + + for (Enumeration e=dataFileSets.elements() ; e.hasMoreElements() ; ) { + TarFileSet fileSet = (TarFileSet)e.nextElement(); + String prefix = fileSet.getPrefix(); + String fullpath = fileSet.getFullpath(); + if ("".equals(fullpath) && !prefix.startsWith("./")) { + if (prefix.startsWith("/")) { + fileSet.setPrefix("." + prefix); + } else { + fileSet.setPrefix("./" + prefix); + } + } + if ((fullpath.length() > 0) && !fullpath.startsWith("./")) { + fileSet.setPrefix(""); + if (fullpath.startsWith("/")) { + fileSet.setFullpath("." + fullpath); + } else { + fileSet.setFullpath("./" + fullpath); + } + } + if ((0 == fileSet.getUid()) && ("" == fileSet.getUserName())) { + fileSet.setUserName("root"); + } + if ((0 == fileSet.getGid()) && ("" == fileSet.getGroup())) { + fileSet.setGroup("root"); + } + } + + File md5sumsFile = new File(tempDir,"md5sums"); + if (includeMd5sums) { + Checksum md5 = new Checksum(); + prepareTask(md5); + int md5Count = 0; + StringBuffer md5sums = new StringBuffer(); + for (Enumeration e=dataFileSets.elements() ; e.hasMoreElements() ; ) { + TarFileSet fileSet = (TarFileSet)e.nextElement(); + String[] files = fileSet.getDirectoryScanner(getProject()).getIncludedFiles(); + File fileSetDir = fileSet.getDir(getProject()); + for (int i=0, c=files.length ; i<c ; i++) { + md5.setFile(new File(fileSetDir,files[i])); + md5.setProperty("md5_"+md5Count); + md5.perform(); + md5sums.append(getProject().getProperty("md5_"+md5Count)).append(" "); + String fullpath = fileSet.getFullpath(); + if (fullpath.length() > 0) { + md5sums.append(fullpath.substring(2)); + } else { + md5sums.append(fileSet.getPrefix().substring(2)).append(files[i].replace('\\','/')); + } + md5sums.append("\n"); + md5Count++; + } + } + echo = new Echo(); + prepareTask(echo); + echo.setFile(md5sumsFile); + echo.setLevel(echoLevel); + echo.setMessage(md5sums.toString()); + echo.perform(); + tarFileSet = controlTarGz.createTarFileSet(); + tarFileSet.setFile(md5sumsFile); + tarFileSet.setUserName("root"); + tarFileSet.setGroup("root"); + tarFileSet.setPrefix("./"); + } + + TarCompressionMethod tarCompressionMethod = new TarCompressionMethod(); + tarCompressionMethod.setValue("gzip"); + controlTarGz.setCompression(tarCompressionMethod); + File controlTarGzFile = new File(tempDir,"control.tar.gz"); + controlTarGz.setDestFile(controlTarGzFile); + controlTarGz.perform(); + + dataTarGz.setCompression(tarCompressionMethod); + File dataTarGzFile = new File(tempDir,"data.tar.gz"); + dataTarGz.setDestFile(dataTarGzFile); + dataTarGz.perform(); + + FileUtils.delete(destFile); + ArFileSet fileSet = debPackage.createArFileSet(); + fileSet.setFile(debianBinaryFile); + fileSet = debPackage.createArFileSet(); + fileSet.setFile(controlTarGzFile); + fileSet = debPackage.createArFileSet(); + fileSet.setFile(dataTarGzFile); + debPackage.perform(); + + if (deleteTempFiles) { + FileUtils.delete(debianBinaryFile); + FileUtils.delete(controlTarGzFile); + FileUtils.delete(dataTarGzFile); + FileUtils.delete(md5sumsFile); + } + } + + /** + * Checks whether the package is 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 packageIsUpToDate(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; + } + + /** + * Prepares a task for execution. + * + * @param task the task to prepare + */ + protected void prepareTask(Task task) { + task.setProject(getProject()); + task.setOwningTarget(getOwningTarget()); + task.setTaskName(getTaskName()); + task.setTaskType(getTaskType()); + } +}
--- a/src/name/blackcap/exifwasher/Test.kt Fri Apr 24 14:01:03 2020 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,24 +0,0 @@ -/* - * A basic test of the library: try to use it to print out the EXIF - * data. - */ -package name.blackcap.exifwasher - -import name.blackcap.exifwasher.exiv2.* - -/* entry point */ -fun main(args: Array<String>) { - println("java.class.path = " + System.getProperty("java.class.path")) - if (args.size != 1) { - System.err.println("expecting a file name") - System.exit(1) - } - val image = Image(args[0]) - val meta = image.metadata - val keys = meta.keys - keys.sort() - keys.forEach { - val v = meta[it] - println("${it}: ${v.type} = ${v.value}") - } -}
--- a/src/name/blackcap/exifwasher/Test2.kt Fri Apr 24 14:01:03 2020 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/* - * A basic test of the library: try to use it to print out the EXIF - * data. - */ -package name.blackcap.exifwasher - -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import name.blackcap.exifwasher.exiv2.* - -/* entry point */ -fun main(args: Array<String>) { - /* must have a file name */ - if (args.size != 1) { - System.err.println("expecting a file name") - System.exit(1) - } - - /* copy the file; we don't want to scribble on the original */ - val (name, ext) = splitext(args[0]) - val newName = "${name}_washed${ext}" - FileInputStream(args[0]).use { source -> - FileOutputStream(newName).use { target -> - source.copyTo(target) - } - } - - /* load the whitelist and image data */ - val white = Whitelist.parse(PROPERTIES.getProperty("whitelist")) - val image = Image(newName) - - /* do the washing */ - val meta = image.metadata - val keys = meta.keys - val keysin = keys.size - var deleted = 0 - keys.forEach { - if (!white.contains(it)) { - meta.erase(it) - deleted += 1 - } - } - - /* save and summarize */ - image.store() - println("${keysin} in - ${deleted} deleted = ${keysin - deleted} out") -} - -fun splitext(s: String): Pair<String, String> { - val pos = s.lastIndexOf('.') - if (pos == -1) { - return Pair(s, "") - } - return Pair(s.substring(0, pos), s.substring(pos)) -}