/*
* Copyright 2015 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.rpm;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiConsumer;
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.LocalizedResource;
import com.inet.gradle.setup.abstracts.Service;
import com.inet.gradle.setup.unix.rpm.RpmControlFileBuilder.Script;
public class RpmBuilder extends AbstractBuilder<Rpm, SetupBuilder> {
private RpmControlFileBuilder controlBuilder;
private SetupBuilder setup;
/**
* Create a new instance
*
* @param rpm the calling task
* @param setup the shared settings
* @param fileResolver the file Resolver
*/
public RpmBuilder( Rpm rpm, SetupBuilder setup, FileResolver fileResolver ) {
super( rpm, fileResolver );
this.setup = setup;
}
/**
* Build the RedHat package in different steps with the 'rpmbuild'.
*
* <dl>
* <dt>copy files</dt>
* <dd>copy the files specified in the gradle script to the BUILD/usr/share/archivesBaseName directory.</dd>
* <dd>The files must be in the BUILD directory because the 'prep' step will copy all the files from there to the BUILDROOT directory.</dd>
* <dd>The 'rpmbuild' deletes the BUILDROOT directory before building the package. Thats why we need to copy the files into it.</dd>
* <dt>SPEC file creation</dt>
* <dd>The 'rpmbuild' requires a configuration files ending with .spec.</dd>
* <dd>This spec file contains all required informations (like name, version, dependencies) and scripts that are executed during the creation and installing of the package.</dd>
* <dt>change file permissions</dt>
* <dd>Before the package is created the permissions of all files need to be set correctly.</dd>
* <dd>All directories and executables will be changed to 755 permission and other files to 644.</dd>
* <dt>create the package</dt>
* <dd>Creates the package with 'rpmbuild'</dd>
* </dl>
*/
public void build() {
try {
String release = task.getRelease();
if( release == null || release.length() == 0 ) {
release = "1";
}
File filesPath = new File( buildDir.getAbsolutePath() + "/BUILD" + task.getInstallationRoot() );
task.copyTo( filesPath );
changeFilePermissionsTo644( filesPath );
controlBuilder = new RpmControlFileBuilder( super.task, setup, new File( buildDir, "SPECS" ) );
controlBuilder.addScriptFragment( Script.PREINSTHEAD, "# 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.addScriptFragment( Script.POSTINSTHEAD, "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" );
}
for( Service service : setup.getServices() ) {
setupService( service );
}
for( DesktopStarter starter : setup.getDesktopStarters() ) {
setupStarter( starter );
}
if( !daemonuser.equalsIgnoreCase( "root" ) ) {
controlBuilder.addScriptFragment( Script.POSTRMTAIL, "userdel -r " + daemonuser + " 2> /dev/null || true \n" );
controlBuilder.addScriptFragment( Script.POSTRMTAIL, "groupdel " + daemonuser + " 2> /dev/null || true \n" );
}
// copy the license files
for( LocalizedResource license : setup.getLicenseFiles() ) {
File licensetarget = new File( buildDir.getAbsolutePath() + "/BUILD/usr/share/licenses/" + setup.getApplication() + "/" + license.getResource().getName() );
licensetarget.mkdirs();
Files.copy( license.getResource().toPath(), licensetarget.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING );
}
controlBuilder.build();
changeDirectoryPermissionsTo755( buildDir );
createRpmPackage();
} catch( RuntimeException ex ) {
throw ex;
} catch( Exception ex ) {
throw new RuntimeException( ex );
}
}
/**
* 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();
String initTemplate = "unix/init-service.sh";
Template initScript = new Template( initTemplate );
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 = "BUILD/etc/init.d/" + serviceUnixName;
initScript.writeTo( createFile( initScriptFile , true ) );
controlBuilder.addConfFile( initScriptFile );
controlBuilder.addScriptFragment( Script.PREINSTHEAD, "[ -f \"/etc/init.d/" + serviceUnixName + "\" ] && \"/etc/init.d/" + serviceUnixName + "\" stop || true" );
controlBuilder.addScriptFragment( Script.POSTINSTTAIL, "if [ -f \"/etc/init.d/" + serviceUnixName + "\" ] && [ \"" + installationRoot + "\" != \"$RPM_INSTALL_PREFIX\" ] ; then\n"
+ "echo replace path\n"
+ "sed -i 's|'" + installationRoot + "'|'$RPM_INSTALL_PREFIX'|g' /etc/init.d/" + serviceUnixName
+ "\nfi" );
// copy a default service file if set
if( task.getDefaultServiceFile() != null ) {
File serviceDestFile = createFile( "BUILD/etc/sysconfig/" + serviceUnixName, true );
Files.copy( task.getDefaultServiceFile().toPath(), serviceDestFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING );
}
controlBuilder.addScriptFragment( Script.POSTINSTTAIL, "[ -f \"/etc/init.d/" + serviceUnixName + "\" ] && chkconfig --add " + serviceUnixName + " || true" );
controlBuilder.addScriptFragment( Script.POSTINSTTAIL, "[ -f \"/etc/init.d/" + serviceUnixName + "\" ] && /etc/init.d/" + serviceUnixName + " start || true" );
controlBuilder.addScriptFragment( Script.PRERMHEAD, "[ -f \"/etc/init.d/" + serviceUnixName + "\" ] && /etc/init.d/" + serviceUnixName + " stop || true" );
controlBuilder.addScriptFragment( Script.PRERMHEAD, "[ -f \"/etc/init.d/" + serviceUnixName + "\" ] && chkconfig --del " + serviceUnixName + " || true" );
}
/**
* Changes the permissions of all directories recursively inside the specified path to 755.
*
* @param path the path
* @throws IOException on I/O failures
*/
// share
private void changeDirectoryPermissionsTo755( File path ) throws IOException {
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
*/
// share
private void changeFilePermissionsTo644( File path ) throws IOException {
for( File file : path.listFiles() ) {
if( file.isDirectory() ) {
changeFilePermissionsTo644( file );
} else {
if( file.getName().endsWith( ".sh" ) ) {
setPermissions( file, true );
} else {
setPermissions( file, false );
}
}
}
}
/**
* Sets the permissions of the specified file, either to 644 (non-executable) or 755 (executable).
*
* @param file the file
* @param executable if set to <tt>true</tt> the executable bit will be set
* @throws IOException on errors when setting the permissions
*/
// share
static void setPermissions( File file, boolean executable ) throws IOException {
Set<PosixFilePermission> perms = new HashSet<PosixFilePermission>();
perms.add( PosixFilePermission.OWNER_READ );
perms.add( PosixFilePermission.OWNER_WRITE );
perms.add( PosixFilePermission.GROUP_READ );
perms.add( PosixFilePermission.OTHERS_READ );
if( executable ) {
perms.add( PosixFilePermission.OWNER_EXECUTE );
perms.add( PosixFilePermission.GROUP_EXECUTE );
perms.add( PosixFilePermission.OTHERS_EXECUTE );
}
Files.setPosixFilePermissions( file.toPath(), perms );
}
/**
* 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
*/
// share
private void setupStarter( DesktopStarter starter ) throws IOException {
String unixName = starter.getDisplayName();
String consoleStarterPath = "/usr/bin/" + unixName;
try (FileWriter fw = new FileWriter( createFile( "BUILD" + 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, 256, 512 };
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, "BUILD/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 );
setPermissions( iconFile, false );
}
}
try (FileWriter fw = new FileWriter( createFile( "BUILD/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( "BUILD/" + 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.addScriptFragment( Script.POSTINSTTAIL, "xdg-mime install \"" + task.getInstallationRoot() + "/" + simpleVendor + "-" + extension + ".xml\" || true" );
controlBuilder.addScriptFragment( Script.PRERMHEAD, "xdg-mime uninstall \"" + task.getInstallationRoot() + "/" + simpleVendor + "-" + extension + ".xml\" || true" );
String iconame = unixName;
if( starter.getIcons() != null ) {
iconame = iconName;
}
controlBuilder.addScriptFragment( Script.POSTINSTTAIL, "xdg-icon-resource install --context mimetypes --novendor --size 48 /usr/share/icons/hicolor/48x48/apps/" + iconame + ".png " + iconame + " || true" );
controlBuilder.addScriptFragment( Script.PRERMHEAD, "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.addScriptFragment( Script.POSTINSTTAIL, "if [ -z \"$SUDO_USER\" ]; then" );
// controlBuilder.addScriptFragment( Script.POSTINSTTAIL, " su $LOGNAME -c \"xdg-mime default '" + unixName + ".desktop' " + mimetypes + " || true\";" );
// controlBuilder.addScriptFragment( Script.POSTINSTTAIL, "else" );
// controlBuilder.addScriptFragment( Script.POSTINSTTAIL, " su $SUDO_USER -c \"xdg-mime default '" + unixName + ".desktop' " + mimetypes + " || true\";" );
// controlBuilder.addScriptFragment( Script.POSTINSTTAIL, "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
*/
// share
private File createFile( String path, boolean executable ) throws IOException {
File file = new File( buildDir, path );
if( !file.getParentFile().exists() ) {
file.getParentFile().mkdirs();
}
file.createNewFile();
setPermissions( file, executable );
return file;
}
/**
* execute the command to generate the RPM package
*
* rpmbuild -ba -clean "--define=_topdir buildDir(rpm)" SPECS/basename.spec
*
*/
private void createRpmPackage() {
ArrayList<String> command = new ArrayList<>();
command.add( "rpmbuild" );
command.add( "-ba" );
command.add( "-v" );
command.add( "--clean" );
command.add( "--define=_topdir " + buildDir.getAbsolutePath() );
command.add( "SPECS/" + setup.getAppIdentifier() + ".spec" );
exec( command );
}
/**
* A consumer interface that can throw exceptions further up the chain.
* @author gamma
*
* @param <T> first element that will be accepted
* @param <P> second element that will be accepted
*/
@FunctionalInterface
private interface ThrowingBiConsumer<T,P> extends BiConsumer<T,P> {
@Override
default void accept(final T elem, final P mele) {
try {
acceptThrows(elem, mele);
} catch (final Exception e) {
/* Do whatever here ... */
System.out.println("handling an exception...");
throw new RuntimeException(e);
}
}
void acceptThrows(T elem, P mele) throws Exception;
}
}