/* Copyright (C) 2009 Mobile Sorcery AB
This program is free software; you can redistribute it and/or modify it
under the terms of the Eclipse Public License v1.0.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the Eclipse Public License v1.0 for
more details.
You should have received a copy of the Eclipse Public License v1.0 along
with this program. It is also available at http://www.eclipse.org/legal/epl-v10.html
*/
package com.mobilesorcery.sdk.builder.linux.deb;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import org.apache.commons.compress.archivers.ar.ArArchiveEntry;
import org.apache.commons.compress.archivers.ar.ArArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import com.mobilesorcery.sdk.builder.linux.deb.fields.ArchitectureHeader;
import com.mobilesorcery.sdk.builder.linux.deb.fields.Header;
import com.mobilesorcery.sdk.builder.linux.deb.fields.NameHeader;
import com.mobilesorcery.sdk.builder.linux.deb.fields.SizeHeader;
import com.mobilesorcery.sdk.builder.linux.deb.fields.VersionHeader;
/**
* This class is used for building .deb packages.
*
* @author Ali Mosavian
*/
public class DebBuilder
{
String m_packName;
StringBuilder m_md5sums;
private Map<String, Header> m_headerMap;
private Map<String, String> m_scriptMap;
private long m_installedSize;
private List<SimpleEntry<File, SimpleEntry<String, Integer>>> m_fileList;
/**
* Constructor
*
*/
public DebBuilder ( String n,
String v,
String r )
{
r = r.isEmpty( ) ? "0" : r;
m_installedSize = 0;
m_packName = n.toLowerCase( )
.replaceAll( " ", "_")
.concat( "_" )
.concat( v )
.concat( "-" )
.concat( r );
m_md5sums = new StringBuilder( );
m_headerMap = new HashMap<String, Header>( );
m_scriptMap = new HashMap<String, String>( );
m_fileList = new LinkedList<SimpleEntry<File, SimpleEntry<String, Integer>>>( );
// Set name and version
try
{
addHeader( new NameHeader( n ) );
addHeader( new VersionHeader( v+"-"+r ) );
} catch ( Exception e ) {
throw new RuntimeException( e );
}
}
/**
* Adds header to the control file
*
* @param h Header to add
*
* @throws Exception if multiple values of the same header type is
* added and the header type doesn't support multiple values
*/
public void addHeader ( Header h )
throws Exception
{
if ( m_headerMap.containsKey( h.getName( ) ) == false )
m_headerMap.put( h.getName( ), h );
else
m_headerMap.get( h.getName( ) ).addNext( h );
if ( h instanceof ArchitectureHeader )
m_packName += "_" + ((ArchitectureHeader)h).getValue( );
}
/**
* Adds a new file to package
*
* @param p Path to file
*/
public void addFile ( String p )
throws IOException,
FileNotFoundException,
NoSuchAlgorithmException
{
File f = new File( p );
int m = TarArchiveEntry.DEFAULT_FILE_MODE;
if ( f.isDirectory( ) == true )
m = TarArchiveEntry.DEFAULT_DIR_MODE;
addFile( p, f, m );
}
/**
* Adds a new file to package
*
* @param p Path to file
* @param m Standard unix file mode in octal
*/
public void addFile ( String p,
Integer m )
throws IOException,
FileNotFoundException,
NoSuchAlgorithmException
{
addFile( p, new File( p ), m );
}
/**
* Adds a new file to package
*
* @param f The actual file to add
* @param p Path to file
*/
public void addFile ( String p,
File f )
throws IOException,
FileNotFoundException,
NoSuchAlgorithmException
{
int m = TarArchiveEntry.DEFAULT_FILE_MODE;
if ( f.isDirectory( ) == true )
m = TarArchiveEntry.DEFAULT_DIR_MODE;
addFile( p, f, m );
}
/**
* Adds a new file to package
*
* @param path File path in debian package
* @param file The actual file to add
* @param mode Standard unix file mode in octal
*/
public void addFile ( String path,
File file,
Integer mode )
throws IOException,
FileNotFoundException,
NoSuchAlgorithmException
{
StringBuilder o = m_md5sums;
// These entries will corrupt the dpkg database
if ( path.equals( "/" ) || path.equals( "." ) || path.equals( "./" ) )
return;
// Add to file list
if ( path.startsWith( "./" ) == false )
{
if ( path.charAt( 0 ) == '/' )
path = "." + path;
else
path = "./" + path;
}
SimpleEntry<String, Integer> v = new SimpleEntry<String, Integer>( path, mode );
m_fileList.add( new SimpleEntry<File, SimpleEntry<String, Integer>>( file, v ) );
if ( file.isDirectory( ) == true )
return;
// Calculate file MD5
String md5 = BuilderUtil.getInstance( ).calcFileMD5Sum( file );
o.append( md5 );
for ( int i = 0; i < 1+(32-md5.length( )); i++ )
o.append( " " );
o.append( path ).append( "\n" );
// Add file size to total
m_installedSize += file.length( )/1024;
}
/**
* Sets the pre install script
*
* @param s The script
*/
public void setScriptPreInst ( String s )
{
m_scriptMap.put( "preinst", s );
}
/**
* Sets the post install script
*
* @param s The script
*/
public void setScriptPostInst ( String s )
{
m_scriptMap.put( "postinst", s );
}
/**
* Sets the pre remove script
*
* @param s The script
*/
public void setScriptPreRm ( String s )
{
m_scriptMap.put( "prerm", s );
}
/**
* Sets the post remove script
*
* @param s The script
*/
public void setScriptPostRm ( String s )
{
m_scriptMap.put( "postrm", s );
}
/**
* Create debian package
*
* @param p Path to write to
*/
public String build ( File p )
throws Exception,
IOException,
FileNotFoundException
{
File ftemp;
String fileName= m_packName + ".deb";
File filePack= new File( p, fileName );
long sysTime = System.currentTimeMillis( );
FileOutputStream fos = new FileOutputStream( filePack );
BufferedOutputStream bos = new BufferedOutputStream( fos );
ArArchiveOutputStream aros= new ArArchiveOutputStream( bos );
byte[] debBin = { (byte)'2', (byte)'.', (byte)'0', (byte)0x0a };
// Write the file 'debian-binary'
aros.putArchiveEntry( new ArArchiveEntry( "debian-binary",
4,
0,
0,
0x81a4,
sysTime ) );
aros.write( debBin );
aros.closeArchiveEntry( );
// Create control.tar.gz
ftemp = File.createTempFile( sysTime+"", "control.tar.gz" );
doCreateControlTarGZip( ftemp );
aros.putArchiveEntry( new ArArchiveEntry( "control.tar.gz",
ftemp.length( ),
0,
0,
0x81a4,
sysTime ) );
BuilderUtil.getInstance( ).copyFileToOutputStream( aros, ftemp );
aros.closeArchiveEntry( );
ftemp.delete( );
// Create data.tar.gz
ftemp = File.createTempFile( sysTime+"", "data.tar.gz" );
doAddFilesToTarGZip( ftemp );
aros.putArchiveEntry( new ArArchiveEntry( "data.tar.gz",
ftemp.length( ),
0,
0,
0x81a4,
sysTime ) );
BuilderUtil.getInstance( ).copyFileToOutputStream( aros, ftemp );
aros.closeArchiveEntry( );
ftemp.delete( );
// Done
aros.close( );
bos.close( );
fos.close( );
// Return absolute path
return filePack.getName( );
}
/**
*
* @param os
* @throws IOException
*/
private void doCreateControlTarGZip ( File f )
throws Exception
{
File ftemp;
FileOutputStream os = new FileOutputStream( f );
GzipCompressorOutputStream gzos = new GzipCompressorOutputStream( os );
TarArchiveOutputStream tos = new TarArchiveOutputStream( gzos );
// Write control file
ftemp = File.createTempFile( System.currentTimeMillis()+"", "control" );
doWriteControl( ftemp );
tos.putArchiveEntry( new TarArchiveEntry( ftemp, "./control" ) );
BuilderUtil.getInstance( ).copyFileToOutputStream( tos, ftemp );
tos.closeArchiveEntry( );
ftemp.delete( );
// Write md5sums
ftemp = File.createTempFile( System.currentTimeMillis()+"", "md5sums" );
doWriteMD5SumsToFile( ftemp );
tos.putArchiveEntry( new TarArchiveEntry( ftemp, "./md5sums" ) );
BuilderUtil.getInstance( ).copyFileToOutputStream( tos, ftemp );
tos.closeArchiveEntry( );
ftemp.delete( );
// Add prerm, postrm, preinst, postinst scripts
for ( Entry<String, String> s : m_scriptMap.entrySet( ) )
{
TarArchiveEntry e = new TarArchiveEntry( "./"+s.getKey( ) );
e.setSize( s.getValue( ).length( ) );
tos.putArchiveEntry( e );
BuilderUtil.getInstance( ).copyStringToOutputStream( tos, s.getValue( ) );
tos.closeArchiveEntry( );
}
// Done
tos.close( );
gzos.close( );
os.close( );
}
/**
*
* @param os
*/
private void doWriteControl ( File f )
throws Exception
{
Vector<Header> order = new Vector<Header>( 20 );
FileOutputStream fos = new FileOutputStream( f );
BufferedOutputStream bos = new BufferedOutputStream( fos );
// Add Installed-Size header
addHeader( new SizeHeader( m_installedSize ) );
// FIXME: Replace with priority queue
for ( int i = 0; i < 20; i++ )
order.add( null );
// Check if the mandatory headers are there
for ( String s : Header.getMandatory( ) )
if ( m_headerMap.containsKey( s ) == false )
throw new Exception( "Mandatory header '" + s + "' is missing" );
// Put the headers in a priority queue to get correct order
for ( Header h : m_headerMap.values( ) )
order.set( h.getPriority( ), h );
// Write them
for ( Header h : order )
{
if ( h == null )
continue;
bos.write( h.toString( ).getBytes( ) );
bos.write( 0x0a );
}
bos.close( );
fos.close( );
}
/**
* Adds the files in the file list in a tar+gz
*
* @param o Output file
*
* @throws IOException If error occurs during writing
* @throws FileNotFoundException If the output file could not be opened.
*/
private void doAddFilesToTarGZip ( File o )
throws IOException,
FileNotFoundException
{
FileOutputStream os = new FileOutputStream( o );
GzipCompressorOutputStream gzos = new GzipCompressorOutputStream( os );
TarArchiveOutputStream tos = new TarArchiveOutputStream( gzos );
// Add files
for ( SimpleEntry<File, SimpleEntry<String, Integer>> fileEntry : m_fileList )
{
File file = fileEntry.getKey( );
String name = fileEntry.getValue( ).getKey( );
int mode = fileEntry.getValue( ).getValue( );
TarArchiveEntry e = new TarArchiveEntry( file, name );
// Add to tar, user/group id 0 is always root
e.setMode( mode );
e.setUserId( 0 );
e.setUserName( "root" );
e.setGroupId( 0 );
e.setGroupName( "root" );
tos.putArchiveEntry( e );
// Write bytes
if ( file.isFile( ) )
BuilderUtil.getInstance( ).copyFileToOutputStream( tos, file );
tos.closeArchiveEntry( );
}
// Done
tos.close( );
gzos.close( );
os.close( );
}
/**
* Writes the md5 sums of the files in the package to a file.
*
* @param o Output file
* @throws IOException If there occurs an error during writing
* @throws FileNotFoundException If failed to open output file
*/
private void doWriteMD5SumsToFile ( File o )
throws IOException,
FileNotFoundException
{
FileOutputStream fos = new FileOutputStream( o );
fos.write( m_md5sums.toString( ).getBytes( ) );
fos.close( );
}
}