/* 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( ); } }