/*
* Copyright 2015 - 2016 i-net software
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.inet.gradle.setup.unix.deb;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.gradle.api.internal.file.FileResolver;
import com.inet.gradle.setup.SetupBuilder;
import com.inet.gradle.setup.Template;
import com.inet.gradle.setup.abstracts.AbstractBuilder;
import com.inet.gradle.setup.abstracts.DesktopStarter;
import com.inet.gradle.setup.abstracts.DocumentType;
import com.inet.gradle.setup.abstracts.Service;
import com.inet.gradle.setup.unix.deb.DebControlFileBuilder.Script;
public class DebBuilder extends AbstractBuilder<Deb, SetupBuilder> {
private DebControlFileBuilder controlBuilder;
private DebDocumentFileBuilder documentBuilder;
private SetupBuilder setup;
/**
* Create a new instance
*
* @param deb
* the calling task
* @param setup
* the shared settings
* @param fileResolver
* the file Resolver
*/
public DebBuilder( Deb deb, SetupBuilder setup, FileResolver fileResolver ) {
super( deb, fileResolver );
this.setup = setup;
}
/**
* executes all necessary steps from copying to building the Debian package
*/
public void build() {
try {
File filesPath = new File( buildDir, task.getInstallationRoot() );
task.copyTo( filesPath );
changeFilePermissionsTo644( filesPath );
// create the package config files in the DEBIAN subfolder
controlBuilder = new DebControlFileBuilder( super.task, setup, new File( buildDir, "DEBIAN" ) );
addScriptsToControlFiles();
for( Service service : setup.getServices() ) {
setupService( service );
}
for( DesktopStarter starter : setup.getDesktopStarters() ) {
setupStarter( starter );
}
// TODO: internationalize
if( setup.getLicenseFile( "en" ) != null ) {
setupEula();
}
// removes only the files in the installation path
List<String> del_files = setup.getDeleteFiles();
for( String file : del_files ) {
controlBuilder.addTailScriptFragment( Script.PREINST, "if [ -f \"" + task.getInstallationRoot() + "/" + file + "\" ]; then\n rm -f \"" + task.getInstallationRoot() + "/" + file + "\"\nfi\n" );
controlBuilder.addTailScriptFragment( Script.PRERM, "if [ -f \"" + task.getInstallationRoot() + "/" + file + "\" ]; then\n rm -f \"" + task.getInstallationRoot() + "/" + file + "\"\nfi\n" );
}
// removes only the folders in the installation path
List<String> del_folders = setup.getDeleteFolders();
for( String folder : del_folders ) {
controlBuilder.addTailScriptFragment( Script.PREINST, "rm -R -f \"" + task.getInstallationRoot() + "/" + folder + "\"\n" );
controlBuilder.addTailScriptFragment( Script.PRERM, "rm -R -f \"" + task.getInstallationRoot() + "/" + folder + "\"\n" );
}
DesktopStarter runAfterStarter = setup.getRunAfter();
String installationRoot = task.getInstallationRoot();
if( runAfterStarter != null ) {
String executable = runAfterStarter.getExecutable();
String mainClass = runAfterStarter.getMainClass();
String workingDir = installationRoot + (runAfterStarter.getWorkDir() != null ? "/" + runAfterStarter.getWorkDir() : "");
String mainJarPath = workingDir + "/" + runAfterStarter.getMainJar();
if( executable != null ) {
controlBuilder.addTailScriptFragment( Script.POSTINST, "( cd \"" + workingDir + "\" && " + executable + " " + runAfterStarter.getStartArguments() + " & )\n" );
} else if( mainClass != null ) {
controlBuilder.addTailScriptFragment( Script.POSTINST, "( cd \"" + workingDir + "\" && java -cp \"" + mainJarPath + "\" " + mainClass + " " + runAfterStarter.getStartArguments() + ")\n" );
}
}
controlBuilder.addTailScriptFragment( Script.POSTINST, "gtk-update-icon-cache /usr/share/icons/hicolor &>/dev/null || : \n" );
DesktopStarter runBeforeUninstall = setup.getRunBeforeUninstall();
if( runBeforeUninstall != null ) {
String executable = runBeforeUninstall.getExecutable();
String mainClass = runBeforeUninstall.getMainClass();
String workingDir = installationRoot + (runBeforeUninstall.getWorkDir() != null ? "/" + runBeforeUninstall.getWorkDir() : "");
String mainJarPath = workingDir + "/" + runBeforeUninstall.getMainJar();
controlBuilder.addTailScriptFragment( Script.PRERM, "case \"$1\" in remove|purge)" );
if( executable != null ) {
if( task.getDaemonUser().equalsIgnoreCase( "root" ) ) {
controlBuilder.addTailScriptFragment( Script.PRERM, "( cd \"" + workingDir + "\" && " + executable + " " + runBeforeUninstall.getStartArguments() + " )" );
} else {
controlBuilder.addTailScriptFragment( Script.PRERM, "(su " + task.getDaemonUser() + " -c 'cd \"" + workingDir + "\" && " + executable + " " + runBeforeUninstall.getStartArguments() + "' )" );
}
} else if( mainClass != null ) {
if( task.getDaemonUser().equalsIgnoreCase( "root" ) ) {
controlBuilder.addTailScriptFragment( Script.PRERM, "( cd \"" + workingDir + "\" && java -cp \"" + mainJarPath + "\" " + mainClass + " " + runBeforeUninstall.getStartArguments() + ")" );
} else {
controlBuilder.addTailScriptFragment( Script.PRERM, "(su " + task.getDaemonUser() + " -c 'cd \"" + workingDir + "\" && java -cp \"" + mainJarPath + "\" " + mainClass + " " + runBeforeUninstall.getStartArguments() + "' )" );
}
}
controlBuilder.addTailScriptFragment( Script.PRERM, " ;;\nesac" );
}
controlBuilder.build();
documentBuilder = new DebDocumentFileBuilder( super.task, setup, new File( buildDir, "/usr/share/doc/" + setup.getAppIdentifier() ) );
documentBuilder.build();
changeDirectoryPermissionsTo755( buildDir );
createDebianPackage();
checkDebianPackage();
} catch( RuntimeException ex ) {
throw ex;
} catch( Exception ex ) {
throw new RuntimeException( ex );
}
}
/**
* adds the pre and post step entries to the pre and post config files
*/
private void addScriptsToControlFiles() {
controlBuilder.addHeadScriptFragment( Script.PREINST, "# check for java. the service woll need it and other parts probably too"
+ "[ ! -x '/usr/bin/java' ] && echo \"The program 'java' does not exist but will be needed.\" && exit 1 || :"
+ "\n\n"
);
String daemonuser = task.getDaemonUser();
if( !daemonuser.equalsIgnoreCase( "root" ) ) {
controlBuilder.addTailScriptFragment( Script.POSTINST, "useradd -r -m -U " + daemonuser + " 2> /dev/null || true\n"
+ "[ \"$(id " + daemonuser + " 2> /dev/null 1>&2; echo $?)\" == \"0\" ]"
+ " && chown -R " + daemonuser + ":" + daemonuser + " '" + task.getInstallationRoot() + "'"
+ " && chmod -R g+w '" + task.getInstallationRoot() + "' || true \n\n"
);
}
ArrayList<String> preinsts = task.getPreinst();
for( String preinst : preinsts ) {
controlBuilder.addTailScriptFragment( Script.PREINST, preinst );
}
ArrayList<String> postinsts = task.getPostinst();
for( String postinst : postinsts ) {
controlBuilder.addTailScriptFragment( Script.POSTINST, postinst );
}
ArrayList<String> prerms = task.getPrerm();
for( String prerm : prerms ) {
controlBuilder.addTailScriptFragment( Script.PRERM, prerm );
}
ArrayList<String> postrms = task.getPostrm();
for( String postrm : postrms ) {
controlBuilder.addTailScriptFragment( Script.POSTRM, postrm );
}
if( !daemonuser.equalsIgnoreCase( "root" ) ) {
controlBuilder.addTailScriptFragment( Script.POSTRM, "if [ \"$1\" = \"purge\" ] ; then\n" + "userdel -r " + daemonuser + " 2> /dev/null || true \n" + "groupdel " + daemonuser + " 2> /dev/null || true \n" + "fi" );
}
}
private void setupEula() throws IOException {
String templateLicenseName = setup.getAppIdentifier() + "/license";
String templateAcceptName = setup.getAppIdentifier() + "/accept-license";
String templateErrorName = setup.getAppIdentifier() + "/error-license";
try (FileWriter fw = new FileWriter( createFile( "DEBIAN/templates", false ) );
// TODO: internationalize
BufferedReader fr = new BufferedReader( new FileReader( setup.getLicenseFile( "en" ) ) )) {
fw.write( "Template: " + templateLicenseName + "\n" );
fw.write( "Type: note\n" );
fw.write( "Description: License agreement\n" );
while( fr.ready() ) {
String line = fr.readLine().trim();
if( line.isEmpty() ) {
fw.write( " .\n" );
} else {
fw.write( ' ' );
fw.write( line );
fw.write( '\n' );
}
}
fw.write( '\n' );
fw.write( "Template: " + templateAcceptName + "\n" );
fw.write( "Type: boolean\n" );
fw.write( "Description: Do you accept the license agreement?\n" );
fw.write( "Description-de.UTF-8: Akzeptieren Sie die Lizenzvereinbarung?\n" );
fw.write( '\n' );
fw.write( "Template: " + templateErrorName + "\n" );
fw.write( "Type: error\n" );
fw.write( "Description: It is required to accept the license to install this package.\n" );
fw.write( "Description-de.UTF-8: Zur Installation dieser Anwendung müssen Sie die Lizenz akzeptieren.\n" );
fw.write( '\n' );
}
controlBuilder.addTailScriptFragment( Script.POSTRM, "if [ \"$1\" = \"remove\" ] || [ \"$1\" = \"purge\" ] ; then\n" + " db_purge\n" + "fi" );
controlBuilder.addHeadScriptFragment( Script.PREINST, "if [ \"$1\" = \"install\" ] ; then\n" + " db_get " + templateAcceptName + "\n" + " if [ \"$RET\" = \"true\" ]; then\n" + " echo \"License already accepted\"\n" + " else\n" + " db_input high " + templateLicenseName + " || true\n" + " db_go\n" + " db_input high " + templateAcceptName + " || true\n" + " db_go\n" + " db_get " + templateAcceptName + "\n" + " if [ \"$RET\" != \"true\" ]; then\n" + " echo \"License was not accepted by the user\"\n" + " db_input high " + templateErrorName + " || true\n" + " db_go\n" + " db_purge\n" + " exit 1\n" + " fi\n" + " fi\n" + "fi" );
}
/**
* Creates the files and the corresponding script section for the specified service.
*
* @param service
* the service
* @throws IOException
* on errors during creating or writing a file
*/
private void setupService( Service service ) throws IOException {
String serviceUnixName = service.getId();
String installationRoot = task.getInstallationRoot();
String workingDir = installationRoot + (service.getWorkDir() != null ? "/" + service.getWorkDir() : "");
String mainJarPath = workingDir + "/" + service.getMainJar();
Template initScript = new Template( "unix/init-service.sh" );
initScript.setPlaceholder( "name", serviceUnixName );
String version = setup.getVersion();
initScript.setPlaceholder( "majorversion", version.substring( 0, version.indexOf( '.' ) ) );
initScript.setPlaceholder( "displayName", setup.getApplication() );
initScript.setPlaceholder( "description", service.getDescription() );
initScript.setPlaceholder( "wait", "2" );
initScript.setPlaceholder( "workdir", workingDir );
initScript.setPlaceholder( "mainJar", mainJarPath );
initScript.setPlaceholder( "startArguments", (service.getStartArguments()).trim() );
initScript.setPlaceholder( "mainClass", service.getMainClass() );
initScript.setPlaceholder( "daemonUser", task.getDaemonUser() );
initScript.setPlaceholder( "additionalServiceScript", task.getAdditionalServiceScript() );
String initScriptFile = "etc/init.d/" + serviceUnixName;
initScript.writeTo( createFile( initScriptFile, true ) );
// copy a default service file if set
if( task.getDefaultServiceFile() != null ) {
File serviceDestFile = new File( buildDir.getAbsolutePath(), "/etc/default/" + serviceUnixName );
serviceDestFile.mkdirs();
Files.copy( task.getDefaultServiceFile().toPath(), serviceDestFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING );
}
controlBuilder.addTailScriptFragment( Script.POSTINST, "[ -f \"/etc/init.d/" + serviceUnixName + "\" ] && update-rc.d " + serviceUnixName + " defaults 91 09 >/dev/null || true" );
controlBuilder.addTailScriptFragment( Script.POSTINST, "[ -f \"/etc/init.d/" + serviceUnixName + "\" ] && service " + serviceUnixName + " start || true" );
controlBuilder.addTailScriptFragment( Script.PRERM, "[ -f \"/etc/init.d/" + serviceUnixName + "\" ] && service " + serviceUnixName + " stop || true" );
controlBuilder.addTailScriptFragment( Script.POSTRM, "[ \"$1\" = \"purge\" ] && update-rc.d " + serviceUnixName + " remove >/dev/null || true " );
}
/**
* Creates the files and the corresponding scripts for the specified desktop starter.
*
* @param starter
* the desktop starter
* @throws IOException
* on errors during creating or writing a file
*/
private void setupStarter( DesktopStarter starter ) throws IOException {
String unixName = starter.getDisplayName();
String consoleStarterPath = "usr/bin/" + unixName;
try (FileWriter fw = new FileWriter( createFile( consoleStarterPath, true ) )) {
fw.write( "#!/bin/bash\n" );
if( starter.getExecutable() != null ) {
fw.write( "\"" + task.getInstallationRoot() + "/" + starter.getExecutable() + "\" " + starter.getStartArguments() + " \"$@\"" );
} else {
fw.write( "java -cp \"" + task.getInstallationRoot() + "/" + starter.getMainJar() + "\" " + starter.getMainClass() + " " + starter.getStartArguments() + " \"$@\"" );
}
}
int[] iconSizes = { 16, 32, 48, 64, 128 };
String iconName = "";
if( starter.getIcons() != null ) {
iconName = starter.getIcons().toString();
int index = iconName.lastIndexOf( '/' );
if( index > -1 ) {
iconName = iconName.substring( index + 1 );
}
// icons must be png files and should named like that
if( !iconName.endsWith( ".png" ) ) {
index = iconName.lastIndexOf( '.' );
if( index > -1 ) {
iconName = iconName.substring( 0, index );
}
iconName = iconName + ".png";
}
}
for( int size : iconSizes ) {
File iconDir = new File( buildDir, "usr/share/icons/hicolor/" + size + "x" + size + "/apps/" );
iconDir.mkdirs();
File scaledFile = setup.getIconForType( iconDir, "png" + size );
if( scaledFile != null ) {
File iconFile;
if( starter.getIcons() != null ) {
iconFile = new File( iconDir, iconName );
} else {
iconFile = new File( iconDir, unixName + ".png" );
}
scaledFile.renameTo( iconFile );
DebUtils.setPermissions( iconFile, false );
}
}
try (FileWriter fw = new FileWriter( createFile( "usr/share/applications/" + unixName + ".desktop", false ) )) {
fw.write( "[Desktop Entry]\n" );
fw.write( "Name=" + starter.getDisplayName() + "\n" );
fw.write( "Comment=" + starter.getDescription().replace( '\n', ' ' ) + "\n" );
if( starter.getExecutable() != null ) {
fw.write( "Exec=\"" + task.getInstallationRoot() + "/" + starter.getExecutable() + "\"\n" );
} else {
fw.write( "Exec=\"/" + consoleStarterPath + "\" %F\n" );
}
if( starter.getIcons() != null ) {
int index = iconName.lastIndexOf( '.' );
if( index > -1 ) {
iconName = iconName.substring( 0, index ); // as of the Icon Theme Specification the icon name should be without extension
}
fw.write( "Icon=" + iconName + "\n" );
} else {
fw.write( "Icon=" + unixName + "\n" );
}
String cwd = starter.getWorkDir();
if( cwd != null ) {
if( cwd.isEmpty() || cwd.equals( "." )) {
fw.write( "Path=" + task.getInstallationRoot() + "\n" );
} else {
fw.write( "Path=" + cwd + "\n" );
}
}
fw.write( "Terminal=false\n" );
fw.write( "StartupNotify=true\n" );
fw.write( "Type=Application\n" );
if( starter.getMimeTypes() != null ) {
fw.write( "MimeType=" + starter.getMimeTypes() + "\n" );
}
if( starter.getCategories() != null ) {
fw.write( "Categories=" + starter.getCategories() + "\n" );
}
}
// register the mime type and the default app for the extensions
for( DocumentType docType : starter.getDocumentType() ) {
for( String extension : docType.getFileExtension() ) {
String simpleVendor = setup.getVendor();
simpleVendor = simpleVendor.replaceAll( "\\W", "" );
try (FileWriter fw = new FileWriter( createFile( task.getInstallationRoot() + "/" + simpleVendor + "-" + extension + ".xml", false ) )) {
fw.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" );
fw.write( "<mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n" );
// if there was a mime type for the starter it will override the mime types of the documentType
if( starter.getMimeTypes() != null ) {
fw.write( " <mime-type type=\"" + starter.getMimeTypes() + "\">\n" );
} else {
fw.write( " <mime-type type=\"" + docType.getMimetype() + "\">\n" );
}
fw.write( " <comment>" + setup.getApplication() + "</comment>\n" );
fw.write( " <glob-deleteall/>\n" );
fw.write( " <glob pattern=\"*." + extension + "\"/>\n" );
fw.write( " </mime-type>\n" );
fw.write( "</mime-info>\n" );
}
controlBuilder.addTailScriptFragment( Script.POSTINST, "xdg-mime install \"" + task.getInstallationRoot() + "/" + simpleVendor + "-" + extension + ".xml\" || true" );
controlBuilder.addTailScriptFragment( Script.PRERM, "xdg-mime uninstall \"" + task.getInstallationRoot() + "/" + simpleVendor + "-" + extension + ".xml\" || true" );
String iconame = unixName;
if( starter.getIcons() != null ) {
iconame = iconName;
}
controlBuilder.addTailScriptFragment( Script.POSTINST, "xdg-icon-resource install --context mimetypes --novendor --size 48 /usr/share/icons/hicolor/48x48/apps/" + iconame + ".png " + iconame + " || true" );
controlBuilder.addTailScriptFragment( Script.PRERM, "xdg-icon-resource uninstall --context mimetypes --size 48 " + iconame + " || true" );
// we don't want to overwrite the default application and it seems that doing it per hand is the proper way under unix.
// so we don't do it here.
// String mimetypes = docType.getMimetype();
// if( starter.getMimeTypes() != null ) {
// mimetypes = starter.getMimeTypes();
// }
// controlBuilder.addTailScriptFragment( Script.POSTINST, "if [ -z \"$SUDO_USER\" ]; then" );
// controlBuilder.addTailScriptFragment( Script.POSTINST, " su $LOGNAME -c \"xdg-mime default '" + unixName + ".desktop' " + mimetypes + " || true\";" );
// controlBuilder.addTailScriptFragment( Script.POSTINST, "else" );
// controlBuilder.addTailScriptFragment( Script.POSTINST, " su $SUDO_USER -c \"xdg-mime default '" + unixName + ".desktop' " + mimetypes + " || true\";" );
// controlBuilder.addTailScriptFragment( Script.POSTINST, "fi" );
}
}
}
/**
* Creates a file in the build path structure.
*
* @param path
* the path relative to the root of the build path
* @param executable
* if set to <tt>true</tt> the executable bit will be set in the permission flags
* @return the created file
* @throws IOException
* on errors during creating the file or setting the permissions
*/
private File createFile( String path, boolean executable ) throws IOException {
File file = new File( buildDir, path );
if( !file.getParentFile().exists() ) {
file.getParentFile().mkdirs();
}
file.createNewFile();
DebUtils.setPermissions( file, executable );
return file;
}
/**
* execute the lintian tool to check the Debian package This will only be executed if the task 'checkPackage'
* property is set to true
*/
private void checkDebianPackage() {
if( task.getCheckPackage() == null || task.getCheckPackage().equalsIgnoreCase( "true" ) ) {
ArrayList<String> command = new ArrayList<>();
command.add( "lintian" );
// command.add( "-d" );
command.add( task.getSetupFile().getPath() );
exec( command );
}
}
/**
* execute the command to generate the Debian package
*/
private void createDebianPackage() {
try {
ArrayList<String> command = new ArrayList<>();
command.add( "fakeroot" );
command.add( "dpkg-deb" );
command.add( "--build" );
command.add( buildDir.getAbsolutePath() );
command.add( task.getSetupFile().getPath() );
exec( command );
} catch( Throwable e ) {
System.out.println( "Error. Control File was:" );
try {
System.out.println( String.join( "\n", Files.readAllLines( new File(buildDir.getAbsolutePath(), "DEBIAN/control" ).toPath() ) ) );
} catch( IOException e1 ) {
e1.printStackTrace();
}
throw e;
}
}
/**
* Changes the permissions of all directories recursively inside the specified path to 755.
*
* @param path
* the path
* @throws IOException
* on I/O failures
*/
private void changeDirectoryPermissionsTo755( File path ) throws IOException {
DebUtils.setPermissions( path, true );
for( File file : path.listFiles() ) {
if( file.isDirectory() ) {
changeDirectoryPermissionsTo755( file );
}
}
}
/**
* Changes the permissions of all files recursively inside the specified path to 644.
*
* @param path
* the path
* @throws IOException
* on I/O failures
*/
private void changeFilePermissionsTo644( File path ) throws IOException {
for( File file : path.listFiles() ) {
if( file.isDirectory() ) {
changeFilePermissionsTo644( file );
} else {
if( file.getName().endsWith( ".sh" ) ) {
DebUtils.setPermissions( file, true );
} else {
DebUtils.setPermissions( file, false );
}
}
}
}
}