/* 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;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.freecompany.redline.Builder;
import org.freecompany.redline.header.Architecture;
import org.freecompany.redline.header.Os;
import org.freecompany.redline.header.RpmType;
import com.mobilesorcery.sdk.builder.linux.deb.BuilderUtil;
import com.mobilesorcery.sdk.builder.linux.deb.DebBuilder;
import com.mobilesorcery.sdk.builder.linux.deb.fields.ArchitectureHeader;
import com.mobilesorcery.sdk.builder.linux.deb.fields.DependsHeader;
import com.mobilesorcery.sdk.builder.linux.deb.fields.DescriptionHeader;
import com.mobilesorcery.sdk.builder.linux.deb.fields.MaintainerHeader;
import com.mobilesorcery.sdk.builder.linux.deb.fields.PriorityHeader;
import com.mobilesorcery.sdk.builder.linux.deb.fields.SectionHeader;
/**
* This class handles the details of building RPM and DEB packages
* from a template package.
*
* TODO:
* - Fix package signing
* - Fix application category
* - Proper package meta data, needs Eclipse support!
* -
*
* @author Ali Mosavian
*/
public class PackageBuilder
{
/**
* This Enum defines the X windows menu categories,
* obviously, not all of them are suitable in MoSync.
*
*
* @author Ali Mosavian
*/
public enum DesktopEntryCategory
{
AudioVideo, // A multimedia (audio/video) application
Audio, // An audio application Desktop entry must include AudioVideo as well
Video, // A video application Desktop entry must include AudioVideo as well
Development, // An application for development
Education, // Educational software
Game, // A game
Graphics, // Graphical application
Network, // Network application such as a web browser
Office, // An office type application
Settings, // Settings applications Entries may appear in a separate menu or as part of a "Control Center"
System, // System application, "System Tools" such as say a log viewer or network monitor
Utility // Small utility application, "Accessories"
};
private PrivateKey m_privKey;
private File m_tempDir;
private File m_template;
private PackageParser m_templateParser;
private Map<String,String> m_resourceMap;
/**
* Constructor
*
* @param t Sets the package template
*/
public PackageBuilder ( File t )
throws IOException
{
m_template = t;
m_templateParser = new PackageParser( );
m_resourceMap = new HashMap<String, String>( );
m_tempDir = File.createTempFile( "pkgbld", System.currentTimeMillis( ) + "" );
m_resourceMap.put( "version", "1.0" );
m_resourceMap.put( "vendor", "notset" );
}
public void setVersion ( String v )
{
m_resourceMap.put( "version", v );
}
public void setVendor ( String v )
{
m_resourceMap.put( "vendor", v );
}
/**
* Set the application name.
*
* @param s Name
*/
public void setAppName ( String s )
{
m_resourceMap.put( "appname", s );
m_templateParser.setAppName( s );
}
/**
* Sets the categories of the application
*
* @param c The category
*/
public void setCategory ( DesktopEntryCategory c )
{
m_templateParser.setAppCategories( c.toString( ) );
}
/**
* Sets the program file that is to be included.
* Note: This is not optional. there always has to be
* a program file!
*
* @param p Path to file
*/
public void setProgramFile ( String p )
{
m_resourceMap.put( "program", p );
}
/**
* Sets the resource file that is to be included.
*
* @param p Path to file
*/
public void setResorceFile ( String p )
{
m_resourceMap.put( "resource", p );
}
/**
* Set the path to an SVG icon.
*
* @param p Path to file
*/
public void setIconSVG ( String p )
{
m_resourceMap.put( "svg", p );
}
/**
* Set the path to png icons of different sizes.
* Note: At the very least, a 48x48 png icon always has
* to be included.
*
* @param s Size, (as in size x size), valid size are
* [16, 32, 48, 64, 128, 256]
*
* @param p Path to file
*/
public void setIconPNG ( int s,
String p )
{
m_resourceMap.put( "png"+s, p );
}
/**
* Sets the working directory where temporary
* unpacking and customization of a package is
* done.
*
* @param p
*/
public void setWorkDir ( String p )
{
m_tempDir.delete( );
m_tempDir = new File( p, "temp" );
}
/**
* This method sets a private RSA key in DER format that is used
* for signing packages. Currently the packages ARE NOT signed.
*
* @param k Path to key file
*
* @throws IOException
* @throws FileNotFoundException
* @throws InvalidKeySpecException
* @throws NoSuchAlgorithmException
*/
public void setDERPrivateKeyRSA ( String k )
throws IOException,
FileNotFoundException,
InvalidKeySpecException,
NoSuchAlgorithmException
{
// Read private key DER file
File f = new File( k );
DataInputStream dis = new DataInputStream( new FileInputStream( f ) );
byte[] privKeyBytes = new byte[(int)f.length( )];
dis.read( privKeyBytes );
dis.close( );
KeyFactory keyFactory = KeyFactory.getInstance( "RSA" );
// Decode private key
KeySpec privSpec = new PKCS8EncodedKeySpec( privKeyBytes );
m_privKey = keyFactory.generatePrivate( privSpec );
}
/**
* Attempts to create RPM and DEB packages and returns
* a list with absolute paths to them. If either of them
* is missing dependency list (This happens for instance
* when creating the template package on moblin, in which
* case there won't be a DEB dependency list as dpkg is
* missing), then that package won't be created. If neither
* of them has a dependency list, then neither is created
* and an exception is thrown.
*
* @param o The directory to which the packages should be
* output.
*
* @param pt Which kind of package to create, valid values
* are { "deb", "rpm", "all" }.
*
* @return List of (one or two) string(s) with the absolute
* path to the package(s)
*
* @throws Exception If neither package has a dependency list
* or if an exception occurred while building either
* package.
*/
public List<String> createPackages ( File o,
String pt )
throws Exception
{
List<String> l = new LinkedList<String>( );
// Parse and unpack template package
doParseTemplate( );
// Check which package types we need to create
if ( (pt.equals( "deb" ) ||
pt.equals( "rpm" ) ||
pt.equals( "all" )) == false )
throw new RuntimeException( "Unknown package type" );
// Create requested package(s)
if ( pt.equals( "deb" ) ||
pt.equals( "all" ) )
{
// Do we have dependencies for DEB ?
if ( m_templateParser.getDependsList( ).isEmpty( ) == false )
l.add( new File( o, createDEB( o ) ).getAbsolutePath( ) );
}
if ( pt.equals( "rpm" ) ||
pt.equals( "all" ) )
{
// Do we have dependencies for RPM ?
if ( m_templateParser.getRequiresList( ).isEmpty( ) == false )
l.add( new File( o, createRPM( o ) ).getAbsolutePath( ) );
}
// An empty list means that there hasn't been any attempt
// at creating a package because of missing dependency list.
if ( l.isEmpty( ) )
throw new Exception( "Package(s) not built - missing dependency list" );
return l;
}
/**
* This method creates a RPM package from the customized package
* that doParseTemplate( ) created.
*
* @param o Output directory
* @return Name of the package that was created
*
* @throws Exception If any kind of error occurs
*/
private String createRPM ( File o )
throws Exception
{
Builder rpmBuilder;
String appName = m_resourceMap.get( "appname" )
.toLowerCase( )
.replace( " ", "_" );
String version = m_resourceMap.get( "version" );
String vendorName = m_resourceMap.get( "vendor" );
//
// Set package parameters
//
rpmBuilder = new Builder( );
//rpmBuilder.addSignature( m_privKey );
rpmBuilder.setPackage( appName, version, "0" );
rpmBuilder.setVendor( vendorName );
rpmBuilder.setType( RpmType.BINARY );
// FIXME: Doesn't have to be i386
rpmBuilder.setPlatform( Architecture.I386, Os.LINUX );
rpmBuilder.setSummary( m_resourceMap.get( "appname" ) );
rpmBuilder.setDescription( m_resourceMap.get( "appname" ) );
rpmBuilder.setPackager( "MoSync" );
rpmBuilder.setProvides( m_resourceMap.get( "appname" ) );
addFilesToRpm( rpmBuilder, "/", new File( m_tempDir, "." ) );
// Package dependencies
for ( String r : m_templateParser.getRequiresList( ) )
{
// The format of an rpm dependency is
// - name
// - name(ver)
String n = "";
String v = "";
StringTokenizer tok = new StringTokenizer( r, "()" );
n = tok.nextToken( );
if ( tok.hasMoreTokens( ) == true )
v = tok.nextToken( );
rpmBuilder.addDependencyMore( n, v );
}
//
// Build and return path to package
//
return rpmBuilder.build( o );
}
/**
* This method creates a DEB package from the customised package
* that doParseTemplate( ) created.
*
* @param o Output directory
* @return Name of the package that was created
*
* @throws Exception If any kind of error occurs
*/
private String createDEB ( File o )
throws Exception
{
DebBuilder debBuilder;
String appName = m_resourceMap.get( "appname" );
String version = m_resourceMap.get( "version" );
String vendorName = m_resourceMap.get( "vendor" );
String appSummary = appName;
String appDescription = appName;
//
// Set package parameters
//
debBuilder = new DebBuilder( appName, version, "0" );
// FIXME: Doesn't have to be i386
debBuilder.addHeader( new ArchitectureHeader( ArchitectureHeader.CpuArch.I386 ) );
debBuilder.addHeader( new MaintainerHeader( vendorName, "" ) );
debBuilder.addHeader( new SectionHeader( SectionHeader.DebianSections.Utils ) );
debBuilder.addHeader( new PriorityHeader( PriorityHeader.Priorities.Optional ) );
debBuilder.addHeader( new DescriptionHeader( appSummary, appDescription ) );
// Package dependencies
for ( String r : m_templateParser.getDependsList( ) )
debBuilder.addHeader( new DependsHeader( r ) );
// Add files and directories
addFilesToDeb( debBuilder, "/", new File( m_tempDir, "." ) );
//
// Build and return path to package
//
return debBuilder.build( o );
}
/**
* This method extracts and parses a template package so that it
* becomes customized to this application.
*
* @throws Exception If there was a file error, or missing values
* in the meta data of the template.
*/
private void doParseTemplate ( )
throws Exception
{
File fin;
File fot;
// Parse and unpack template
m_tempDir.delete( );
m_tempDir.mkdirs( );
m_templateParser.doProcessTarGZip( m_tempDir, m_template );
// Copy program file
if ( m_resourceMap.containsKey( "program" ) == false )
throw new Exception( "Program file has not been set" );
fin = new File( m_resourceMap.get( "program" ) );
fot = new File( m_tempDir, m_templateParser.getProgramFilePath( ) );
BuilderUtil.getInstance( ).copyFile( fot, fin );
// Copy resource file
if ( m_resourceMap.containsKey( "resource" ) == true )
{
fin = new File( m_resourceMap.get( "resource" ) );
fot = new File( m_tempDir, m_templateParser.getResourceFilePath( ) );
BuilderUtil.getInstance( ).copyFile( fot, fin );
}
// Set SVG icon
if ( m_resourceMap.containsKey( "svg" ) == true )
{
fin = new File( m_resourceMap.get( "svg" ) );
fot = new File( m_tempDir, m_templateParser.getSVGIconDir( ) );
BuilderUtil.getInstance( ).copyFile( fot, fin );
}
// Set PNG icons
int iconSizeList[] = { 16, 32, 48, 64, 128, 256 };
for ( int s : iconSizeList )
{
if ( m_resourceMap.containsKey( "png"+s ) == true )
{
fin = new File( m_resourceMap.get( "png"+s ) );
fot = new File( m_tempDir, m_templateParser.getPNGIconDir( s ) );
BuilderUtil.getInstance( ).copyFile( fot, fin );
}
}
}
/**
* This method recursively adds file to an RPM while making sure
* that the file permissions are correct.
*
* @param b Instance of the rpm builder class
* @param r Relative path to base everything on (in the rpm)
* @param c File to recursively process
*
* @throws IOException Error reading the file.
* @throws NoSuchAlgorithmException This happens when the md5 and sha1
* hashing that the rpm builder does internally fails.
*/
private void addFilesToRpm ( Builder b,
String r,
File c )
throws IOException,
NoSuchAlgorithmException
{
// Update relative path
if ( c.getName( ).equals( "." ) == false )
r += (r.isEmpty( ) ? "" : (r.endsWith( "/") ? "" : "/")) + c.getName( );
// Is it a file?
if ( c.isFile( ) == true )
{
b.addFile( r, c, m_templateParser.getFileMode( r ) );
return;
}
// It's a directory
for ( File f : c.listFiles( ) )
addFilesToRpm( b, r, f );
}
/**
* This method recursively adds file to a DEB while making sure
* that the file permissions are correct.
*
* @param b Instance of the deb builder class
* @param r Relative path to base everything on (in the deb)
* @param c File to recursively process
*
* @throws IOException Error reading the file.
* @throws NoSuchAlgorithmException This happens when the md5 and sha1
* hashing that the rpm builder does internally fails.
*/
public void addFilesToDeb ( DebBuilder b,
String r,
File c )
throws IOException,
NoSuchAlgorithmException
{
// Update relative path
if ( c.getName( ).equals( "." ) == false )
r += (r.isEmpty( ) ? "" : (r.endsWith( "/") ? "" : "/")) + c.getName( );
// Is it a file?
if ( c.isFile( ) == true )
{
b.addFile( r, c, m_templateParser.getFileMode( r ) );
return;
}
else
b.addFile( r, c );
// It's a directory
for ( File f : c.listFiles( ) )
addFilesToDeb( b, r, f );
}
}