package com.googlecode.ant_deb_task;
import org.apache.tools.ant.*;
import org.apache.tools.ant.taskdefs.Tar;
import org.apache.tools.ant.types.*;
import org.apache.tools.tar.TarOutputStream;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.util.zip.*;
import java.security.MessageDigest;
import java.net.URL;
import java.net.MalformedURLException;
/**
* Task that creates a Debian package.
*
* @antTaskName deb
*/
public class Deb extends Task
{
private static final Pattern PACKAGE_NAME_PATTERN = Pattern.compile("[a-z0-9][a-z0-9+\\-.]+");
public static class Description extends ProjectComponent
{
private String _synopsis;
private String _extended = "";
public String getSynopsis ()
{
return _synopsis;
}
public void setSynopsis (String synopsis)
{
_synopsis = synopsis.trim ();
}
public void addText (String text)
{
_extended += getProject ().replaceProperties (text);
}
public String getExtended ()
{
return _extended;
}
public String getExtendedFormatted ()
{
StringBuffer buffer = new StringBuffer (_extended.length ());
String lines[] = _extended.split ("\n");
int start = 0;
for (int i = 0; i < lines.length; i++)
{
String line = lines[i].trim ();
if (line.length () > 0)
break;
start++;
}
int end = lines.length;
for (int i = lines.length - 1; i >= 0; i--)
{
String line = lines[i].trim ();
if (line.length () > 0)
break;
end--;
}
for (int i = start; i < end; i++)
{
String line = lines[i].trim ();
buffer.append (' ');
buffer.append (line.length () == 0 ? "." : line);
buffer.append ('\n');
}
buffer.deleteCharAt (buffer.length () - 1);
return buffer.toString ();
}
}
public static class Version extends ProjectComponent
{
private static final Pattern UPSTREAM_VERSION_PATTERN = Pattern.compile("[0-9][A-Za-z0-9.+\\-:~]*");
private static final Pattern DEBIAN_VERSION_PATTERN = Pattern.compile("[A-Za-z0-9+.~]+");
private int _epoch = 0;
private String _upstream;
private String _debian = "1";
public void setEpoch(int epoch)
{
_epoch = epoch;
}
public void setUpstream(String upstream)
{
_upstream = upstream.trim ();
if (!UPSTREAM_VERSION_PATTERN.matcher (_upstream).matches ())
throw new BuildException("Invalid upstream version number!");
}
public void setDebian(String debian)
{
_debian = debian.trim ();
if (_debian.length() > 0 && !DEBIAN_VERSION_PATTERN.matcher (_debian).matches ())
throw new BuildException("Invalid debian version number!");
}
public String toString()
{
StringBuffer version = new StringBuffer();
if (_epoch > 0)
{
version.append(_epoch);
version.append(':');
}
else if (_upstream.indexOf(':') > -1)
throw new BuildException("Upstream version can contain colons only if epoch is specified!");
version.append(_upstream);
if (_debian.length() > 0)
{
version.append('-');
version.append(_debian);
}
else if (_upstream.indexOf('-') > -1)
throw new BuildException("Upstream version can contain hyphens only if debian version is specified!");
return version.toString();
}
}
public static class Maintainer extends ProjectComponent
{
private String _name;
private String _email;
public void setName (String name)
{
_name = name.trim ();
}
public void setEmail (String email)
{
_email = email.trim ();
}
public String toString()
{
if (_name == null || _name.length () == 0)
return _email;
StringBuffer buffer = new StringBuffer (_name);
buffer.append (" <");
buffer.append (_email);
buffer.append (">");
return buffer.toString ();
}
}
public static class Changelog extends ProjectComponent
{
public static class Format extends EnumeratedAttribute
{
public String[] getValues ()
{
// XML format will be added when supported
return new String[] {"plain" /* , "xml" */};
}
}
private static final String STANDARD_FILENAME = "changelog.gz";
private static final String DEBIAN_FILENAME = "changelog.Debian.gz";
private String _file;
private Changelog.Format _format;
private boolean _debian;
public Changelog()
{
_debian = false;
_format = new Changelog.Format();
_format.setValue("plain");
}
public void setFile (String file)
{
_file = file.trim ();
}
public String getFile()
{
return _file;
}
public void setFormat (Changelog.Format format)
{
_format = format;
}
public Changelog.Format getFormat()
{
return _format;
}
public void setDebian (boolean debian)
{
_debian = debian;
}
public boolean isDebian ()
{
return _debian;
}
public String getTargetFilename ()
{
return _debian ? DEBIAN_FILENAME : STANDARD_FILENAME;
}
}
public static class Section extends EnumeratedAttribute
{
private static final String[] PREFIXES = new String[] {"", "contrib/", "non-free/"};
private static final String[] BASIC_SECTIONS = new String[] {"admin", "base", "comm", "devel", "doc", "editors", "electronics", "embedded", "games", "gnome", "graphics", "hamradio", "interpreters", "kde", "libs", "libdevel", "mail", "math", "misc", "net", "news", "oldlibs", "otherosfs", "perl", "python", "science", "shells", "sound", "tex", "text", "utils", "web", "x11"};
private List sections = new ArrayList (PREFIXES.length * BASIC_SECTIONS.length);
public Section ()
{
for (int i = 0; i < PREFIXES.length; i++)
{
String prefix = PREFIXES[i];
for (int j = 0; j < BASIC_SECTIONS.length; j++)
{
String basicSection = BASIC_SECTIONS[j];
sections.add (prefix + basicSection);
}
}
}
public String[] getValues ()
{
return (String[]) sections.toArray (new String[sections.size()]);
}
}
public static class Priority extends EnumeratedAttribute
{
public String[] getValues ()
{
return new String[] {"required", "important", "standard", "optional", "extra"};
}
}
private File _toDir;
private String _debFilenameProperty = "";
private String _package;
private String _version;
private Deb.Version _versionObj;
private String _section;
private String _priority = "extra";
private String _architecture = "all";
private String _depends;
private String _preDepends;
private String _recommends;
private String _suggests;
private String _enhances;
private String _conflicts;
private String _provides;
private String _replaces;
private String _maintainer;
private URL _homepage;
private Deb.Maintainer _maintainerObj;
private Deb.Description _description;
private Set _conffiles = new HashSet ();
private Set _changelogs = new HashSet ();
private List _data = new ArrayList();
private File _preinst;
private File _postinst;
private File _prerm;
private File _postrm;
private File _config;
private File _templates;
private File _triggers;
private File _tempFolder;
private long _installedSize = 0;
private SortedSet _dataFolders;
private static final Tar.TarCompressionMethod GZIP_COMPRESSION_METHOD = new Tar.TarCompressionMethod ();
private static final Tar.TarLongFileMode GNU_LONGFILE_MODE = new Tar.TarLongFileMode ();
static
{
GZIP_COMPRESSION_METHOD.setValue ("gzip");
GNU_LONGFILE_MODE.setValue(Tar.TarLongFileMode.GNU);
}
public void setToDir (File toDir)
{
_toDir = toDir;
}
public void setDebFilenameProperty(String debFilenameProperty)
{
_debFilenameProperty = debFilenameProperty.trim();
}
public void setPackage (String packageName)
{
if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches())
throw new BuildException("Invalid package name!");
_package = packageName;
}
public void setVersion (String version)
{
_version = version;
}
public void setSection (Section section)
{
_section = section.getValue();
}
public void setPriority (Priority priority)
{
_priority = priority.getValue();
}
public void setArchitecture (String architecture)
{
_architecture = sanitize(architecture, _architecture);
}
public void setDepends (String depends)
{
_depends = sanitize(depends);
}
public void setPreDepends (String preDepends)
{
_preDepends = sanitize(preDepends);
}
public void setRecommends (String recommends)
{
_recommends = sanitize(recommends);
}
public void setSuggests (String suggests)
{
_suggests = sanitize(suggests);
}
public void setEnhances (String enhances)
{
_enhances = sanitize(enhances);
}
public void setConflicts (String conflicts)
{
_conflicts = sanitize(conflicts);
}
public void setProvides (String provides)
{
_provides = sanitize(provides);
}
public void setReplaces(String replaces)
{
_replaces = sanitize(replaces);
}
public void setMaintainer (String maintainer)
{
_maintainer = sanitize(maintainer);
}
public void setHomepage (String homepage)
{
try
{
_homepage = new URL (homepage);
}
catch (MalformedURLException e)
{
throw new BuildException ("Invalid homepage, must be a URL: " + homepage, e);
}
}
public void setPreinst (File preinst)
{
_preinst = preinst;
}
public void setPostinst (File postinst)
{
_postinst = postinst;
}
public void setPrerm (File prerm)
{
_prerm = prerm;
}
public void setPostrm (File postrm)
{
_postrm = postrm;
}
public void setConfig (File config)
{
_config = config;
}
public void setTemplates (File templates)
{
_templates = templates;
}
public void setTriggers(File triggers)
{
_triggers = triggers;
}
public void addConfFiles (TarFileSet conffiles)
{
_conffiles.add (conffiles);
_data.add (conffiles);
}
public void addChangelog(Deb.Changelog changelog)
{
_changelogs.add (changelog);
}
public void addDescription (Deb.Description description)
{
_description = description;
}
public void add (TarFileSet resourceCollection)
{
_data.add(resourceCollection);
}
public void addVersion(Deb.Version version)
{
_versionObj = version;
}
public void addMaintainer(Deb.Maintainer maintainer)
{
_maintainerObj = maintainer;
}
private void writeControlFile (File controlFile, long installedSize) throws FileNotFoundException
{
log ("Generating control file to: " + controlFile.getAbsolutePath (), Project.MSG_VERBOSE);
PrintWriter control = new UnixPrintWriter (controlFile);
control.print ("Package: ");
control.println (_package);
control.print ("Version: ");
control.println (_version);
if (_section != null)
{
control.print ("Section: ");
control.println (_section);
}
if (_priority != null)
{
control.print ("Priority: ");
control.println (_priority);
}
control.print ("Architecture: ");
control.println (_architecture);
if (_depends != null)
{
control.print ("Depends: ");
control.println (_depends);
}
if (_preDepends != null)
{
control.print ("Pre-Depends: ");
control.println (_preDepends);
}
if (_recommends != null)
{
control.print ("Recommends: ");
control.println (_recommends);
}
if (_suggests != null)
{
control.print ("Suggests: ");
control.println (_suggests);
}
if (_enhances != null)
{
control.print ("Enhances: ");
control.println (_enhances);
}
if (_conflicts != null)
{
control.print ("Conflicts: ");
control.println (_conflicts);
}
if (_provides != null)
{
control.print ("Provides: ");
control.println (_provides);
}
if (_replaces != null)
{
control.print ("Replaces: ");
control.println (_replaces);
}
if (installedSize > 0)
{
control.print ("Installed-Size: ");
control.println (installedSize / 1024L);
}
control.print ("Maintainer: ");
control.println (_maintainer);
if (_homepage != null)
{
control.print ("Homepage: ");
control.println(_homepage.toExternalForm());
}
control.print ("Description: ");
control.println (_description.getSynopsis ());
control.println (_description.getExtendedFormatted ());
control.close ();
}
private File createMasterControlFile () throws IOException
{
File controlFile = new File (_tempFolder, "control");
writeControlFile (controlFile, _installedSize);
File md5sumsFile = new File (_tempFolder, "md5sums");
File conffilesFile = new File (_tempFolder, "conffiles");
File masterControlFile = new File (_tempFolder, "control.tar.gz");
Tar controlTar = new Tar ();
controlTar.setProject (getProject ());
controlTar.setTaskName (getTaskName ());
controlTar.setDestFile (masterControlFile);
controlTar.setCompression (GZIP_COMPRESSION_METHOD);
addFileToTar (controlTar, controlFile, "control", "644");
addFileToTar (controlTar, md5sumsFile, "md5sums", "644");
if (conffilesFile.length () > 0)
addFileToTar (controlTar, conffilesFile, "conffiles", "644");
if (_preinst != null)
addFileToTar (controlTar, _preinst, "preinst", "755");
if (_postinst != null)
addFileToTar (controlTar, _postinst, "postinst", "755");
if (_prerm != null)
addFileToTar (controlTar, _prerm, "prerm", "755");
if (_postrm != null)
addFileToTar (controlTar, _postrm, "postrm", "755");
if (_config != null)
addFileToTar (controlTar, _config, "config", "755");
if (_templates != null)
addFileToTar (controlTar, _templates, "templates", "644");
if (_triggers != null)
addFileToTar (controlTar, _triggers, "triggers", "644");
controlTar.perform ();
deleteFileCheck(controlFile);
return masterControlFile;
}
private void addFileToTar(Tar tar, File file, String fullpath, String fileMode)
{
TarFileSet controlFileSet = tar.createTarFileSet ();
controlFileSet.setFile (file);
controlFileSet.setFullpath ("./" + fullpath);
controlFileSet.setFileMode (fileMode);
controlFileSet.setUserName ("root");
controlFileSet.setGroup ("root");
}
public void execute () throws BuildException
{
try
{
if (_versionObj != null)
_version = _versionObj.toString ();
if (_maintainerObj != null)
_maintainer = _maintainerObj.toString ();
_tempFolder = createTempFolder();
processChangelogs ();
scanData ();
File debFile = new File (_toDir, _package + "_" + _version + "_" + _architecture + ".deb");
File dataFile = createDataFile ();
File masterControlFile = createMasterControlFile ();
log ("Writing deb file to: " + debFile.getAbsolutePath());
BuildDeb.buildDeb (debFile, masterControlFile, dataFile);
if (_debFilenameProperty.length() > 0)
getProject().setProperty(_debFilenameProperty, debFile.getAbsolutePath());
deleteFileCheck(masterControlFile);
deleteFileCheck(dataFile);
deleteFolderCheck(_tempFolder);
}
catch (IOException e)
{
throw new BuildException (e);
}
}
private File createDataFile () throws IOException
{
File dataFile = new File (_tempFolder, "data.tar.gz");
Tar dataTar = new Tar ();
dataTar.setProject (getProject ());
dataTar.setTaskName (getTaskName ());
dataTar.setDestFile (dataFile);
dataTar.setCompression (GZIP_COMPRESSION_METHOD);
dataTar.setLongfile(GNU_LONGFILE_MODE);
if ( _data.size () > 0 )
{
// add folders
for (Iterator dataFoldersIter = _dataFolders.iterator (); dataFoldersIter.hasNext ();)
{
String targetFolder = (String) dataFoldersIter.next ();
TarFileSet targetFolderSet = dataTar.createTarFileSet ();
targetFolderSet.setFile (_tempFolder);
targetFolderSet.setFullpath (targetFolder);
targetFolderSet.setUserName ("root");
targetFolderSet.setGroup ("root");
}
// add actual data
for (int i = 0; i < _data.size (); i++)
{
TarFileSet data = (TarFileSet) _data.get (i);
if (data.getUserName() == null || data.getUserName().trim().length() == 0)
data.setUserName ("root");
if (data.getGroup() == null || data.getGroup().trim().length() == 0)
data.setGroup ("root");
dataTar.add (data);
}
dataTar.execute ();
}
else
{
// create an empty data.tar.gz file which is still a valid tar
TarOutputStream tarStream = new TarOutputStream(
new GZipOutputStream(
new BufferedOutputStream(new FileOutputStream(dataFile)),
Deflater.BEST_COMPRESSION
)
);
tarStream.close();
}
return dataFile;
}
private File createTempFolder() throws IOException
{
File tempFile = File.createTempFile ("deb", ".dir");
String tempFolderName = tempFile.getAbsolutePath ();
deleteFileCheck(tempFile);
tempFile = new File (tempFolderName, "removeme");
if (!tempFile.mkdirs ())
throw new IOException("Cannot create folder(s): " + tempFile.getAbsolutePath());
deleteFileCheck(tempFile);
log ("Temp folder: " + tempFolderName, Project.MSG_VERBOSE);
return new File (tempFolderName);
}
private void scanData()
{
try
{
Set existingDirs = new HashSet ();
_installedSize = 0;
PrintWriter md5sums = new UnixPrintWriter (new File (_tempFolder, "md5sums"));
PrintWriter conffiles = new UnixPrintWriter (new File (_tempFolder, "conffiles"));
_dataFolders = new TreeSet ();
Iterator filesets = _data.iterator();
while (filesets.hasNext())
{
TarFileSet fileset = (TarFileSet) filesets.next();
normalizeTargetFolder(fileset);
String fullPath = fileset.getFullpath (getProject ());
String prefix = fileset.getPrefix (getProject ());
String [] fileNames = getFileNames(fileset);
for (int i = 0; i < fileNames.length; i++)
{
String targetName;
String fileName = fileNames[i];
File file = new File (fileset.getDir (getProject ()), fileName);
if (fullPath.length () > 0)
targetName = fullPath;
else
targetName = prefix + fileName;
if (file.isDirectory ())
{
log ("existing dir: " + targetName, Project.MSG_DEBUG);
existingDirs.add (targetName);
}
else
{
// calculate installed size in bytes
_installedSize += file.length ();
// calculate and collect md5 sums
md5sums.print (getFileMd5 (file));
md5sums.print (' ');
md5sums.println (targetName.substring(2));
// get target folder names, and collect them (to be added to _data)
File targetFile = new File(targetName);
File parentFolder = targetFile.getParentFile ();
while (parentFolder != null)
{
String parentFolderPath = parentFolder.getPath ();
if (".".equals(parentFolderPath))
parentFolderPath = "./";
parentFolderPath = parentFolderPath.replace('\\', '/');
if (!existingDirs.contains (parentFolderPath) && !_dataFolders.contains (parentFolderPath))
{
log ("adding dir: " + parentFolderPath + " for " + targetName, Project.MSG_DEBUG);
_dataFolders.add (parentFolderPath);
}
parentFolder = parentFolder.getParentFile ();
}
// write conffiles
if (_conffiles.contains (fileset))
{
// NOTE: targetName is "./path/file" so substring(1) removes the leading '.'
conffiles.println (targetName.substring(1));
}
}
}
}
for (Iterator iterator = existingDirs.iterator (); iterator.hasNext ();)
{
String existingDir = (String) iterator.next ();
if (_dataFolders.contains (existingDir))
{
log ("removing existing dir " + existingDir, Project.MSG_DEBUG);
_dataFolders.remove (existingDir);
}
}
md5sums.close ();
conffiles.close ();
}
catch (Exception e)
{
throw new BuildException (e);
}
}
private void normalizeTargetFolder(TarFileSet fileset)
{
String fullPath = fileset.getFullpath (getProject ());
String prefix = fileset.getPrefix (getProject ());
if (fullPath.length() > 0)
{
if (fullPath.startsWith("/"))
fullPath = "." + fullPath;
else if (!fullPath.startsWith("./"))
fullPath = "./" + fullPath;
fileset.setFullpath(fullPath.replace('\\', '/'));
}
if (prefix.length() > 0)
{
if (!prefix.endsWith ("/"))
prefix += '/';
if (prefix.startsWith("/"))
prefix = "." + prefix;
else if (!prefix.startsWith("./"))
prefix = "./" + prefix;
fileset.setPrefix(prefix.replace('\\', '/'));
}
}
private String[] getFileNames(FileSet fs)
{
DirectoryScanner ds = fs.getDirectoryScanner(fs.getProject());
String[] directories = ds.getIncludedDirectories();
String[] filesPerSe = ds.getIncludedFiles();
String[] files = new String [directories.length + filesPerSe.length];
System.arraycopy(directories, 0, files, 0, directories.length);
System.arraycopy(filesPerSe, 0, files, directories.length, filesPerSe.length);
return files;
}
private String getFileMd5(File file)
{
try
{
MessageDigest md5 = MessageDigest.getInstance ("MD5");
FileInputStream inputStream = new FileInputStream (file);
byte[] buffer = new byte[1024];
while (true)
{
int read = inputStream.read (buffer);
if (read == -1)
break;
md5.update (buffer, 0, read);
}
inputStream.close();
byte[] md5Bytes = md5.digest ();
StringBuffer md5Buffer = new StringBuffer (md5Bytes.length * 2);
for (int i = 0; i < md5Bytes.length; i++)
{
String hex = Integer.toHexString (md5Bytes[i] & 0x00ff);
if (hex.length () == 1)
md5Buffer.append ('0');
md5Buffer.append (hex);
}
return md5Buffer.toString ();
}
catch (Exception e)
{
throw new BuildException(e);
}
}
private void processChangelogs() throws IOException
{
for (Iterator iter = _changelogs.iterator (); iter.hasNext (); )
{
processChangelog ((Deb.Changelog) iter.next ());
}
}
private void processChangelog (Deb.Changelog changelog) throws IOException
{
// Compress file
File file = new File(changelog.getFile ());
File temp = File.createTempFile ("changelog", ".gz");
gzip(file, temp, Deflater.BEST_COMPRESSION, GZipOutputStream.FS_UNIX);
// Determine path
StringBuffer path = new StringBuffer ("usr/share/doc/");
path.append (_package).append ('/');
path.append (changelog.getTargetFilename ());
// Add file to data
TarFileSet fileSet = new TarFileSet ();
fileSet.setProject (getProject ());
fileSet.setFullpath (path.toString ());
fileSet.setFile (temp);
fileSet.setFileMode ("0644");
_data.add (fileSet);
}
private static void gzip (File input, File output, int level, byte fileSystem)
throws IOException
{
GZipOutputStream out = null;
InputStream in = null;
try
{
out = new GZipOutputStream (new FileOutputStream (output), level);
out.setFileSystem (fileSystem);
in = new FileInputStream(input);
byte[] buffer = new byte[8 * 1024];
int len;
while ((len = in.read (buffer, 0, buffer.length)) > 0)
{
out.write(buffer, 0, len);
}
out.finish ();
}
finally
{
if (in != null)
{
in.close ();
}
if (out != null)
{
out.close ();
}
}
}
private boolean deleteFolder(File folder)
{
if (folder.isDirectory())
{
File[] children = folder.listFiles();
for (int i = 0; i < children.length; i++)
{
if (!deleteFolder(children[i]))
return false;
}
}
return folder.delete();
}
private void deleteFolderCheck(File folder) throws IOException
{
if (!deleteFolder(folder))
throw new IOException("Cannot delete file: " + folder.getAbsolutePath());
}
private void deleteFileCheck(File file) throws IOException
{
if (!file.delete ())
throw new IOException("Cannot delete file: " + file.getAbsolutePath());
}
private String sanitize(String value) {
return sanitize(value, null);
}
private String sanitize(String value, String defaultValue) {
if (value == null) {
return defaultValue;
}
value = value.trim();
return value.length() == 0 ? defaultValue : value;
}
}