/* * Copyright 2015 - 2017 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.msi; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringReader; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Scanner; import java.util.Set; import java.util.UUID; import javax.swing.JEditorPane; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.EditorKit; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.xml.parsers.ParserConfigurationException; import org.gradle.api.GradleException; import org.gradle.api.internal.file.CopyActionProcessingStreamAction; import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; import org.w3c.dom.Element; import com.inet.gradle.setup.SetupBuilder; 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.util.ResourceUtils; import com.inet.gradle.setup.util.XmlFileBuilder; /** * Builder for a *.wsx file. A *.wsx file is a XML that described MSI setup and is needed for the Wix tool. * * @author Volker Berlin */ class WxsFileBuilder extends XmlFileBuilder<Msi> { private static final String ICON_ID = "icon.ico"; private Set<String> components = new LinkedHashSet<>(); private HashMap<String, String> ids = new HashMap<>(); private String jvmDll; private String javaDir; private boolean isAddFiles; /** * The product node in the XML. */ private Element product; /** * Reference to INSTALLDIR */ private Element installDir; /** * Create a new instance. * @param msi the MSI task * @param setup the SetupBuilder extension * @param wxsFile the file name * @param buildDir the temporary directory of the task * @param template a template file * @param addFiles if files should be added in this phase * @throws Exception if any error occur */ WxsFileBuilder( Msi msi, SetupBuilder setup, File wxsFile, File buildDir, URL template, boolean addFiles ) throws Exception { super( msi, setup, wxsFile, buildDir, template ); this.isAddFiles = addFiles; } /** * Create *.wxs file based on the settings in the task. * * @throws ParserConfigurationException * @throws Exception if any error occur */ void build() throws Exception { // Wix node Element wix = (Element)doc.getFirstChild(); if( !"Wix".equals( wix.getTagName() ) ) { throw new IllegalArgumentException( "Template does not contains a Wix root: " + wix.getTagName() ); } wix.setAttributeNS( "http://www.w3.org/2000/xmlns/", "xmlns", "http://schemas.microsoft.com/wix/2006/wi" ); wix.setAttributeNS( "http://www.w3.org/2000/xmlns/", "xmlns:util", "http://schemas.microsoft.com/wix/UtilExtension" ); // Product node product = getOrCreateChild( wix, "Product" ); addAttributeIfNotExists( product, "Id", UUID.randomUUID().toString() ); // for multiple instances, every language transformation must have the same ProductCode addAttributeIfNotExists( product, "Language", "1033" ); addAttributeIfNotExists( product, "Manufacturer", setup.getVendor() ); addAttributeIfNotExists( product, "Name", setup.getApplication() ); addAttributeIfNotExists( product, "Version", setup.getVersion() ); addAttributeIfNotExists( product, "UpgradeCode", getGuid( "UpgradeCode" ) ); // Package node Element media = getOrCreateChildById( product, "Media", "1", false ); // must be the second in Product addAttributeIfNotExists( media, "Cabinet", "media1.cab" ); addAttributeIfNotExists( media, "EmbedCab", "yes" ); Element packge = getOrCreateChild( product, "Package", false ); // must be the first in Product if( product.getFirstChild() != packge ) { product.insertBefore( packge, product.getFirstChild() ); } addAttributeIfNotExists( packge, "Compressed", "yes" ); if( !setup.getDescription().isEmpty() ) { addAttributeIfNotExists( packge, "Comments", setup.getDescription() ); } addAttributeIfNotExists( packge, "InstallScope", task.getInstallScope().name() ); // MajorUpgrade if( task.getMultiInstanceCount() <= 1 ) { Element update = getOrCreateChild( product, "MajorUpgrade" ); addAttributeIfNotExists( update, "AllowDowngrades", "yes" ); } // Directory Element directory = getOrCreateChildById( product, "Directory", "TARGETDIR" ); addAttributeIfNotExists( directory, "Name", "SourceDir" ); Element programFiles = getOrCreateChildById( directory, "Directory", task.is64Bit() ? "ProgramFiles64Folder" : "ProgramFilesFolder" ); Element appDirectory = getOrCreateChildById( programFiles, "Directory", "INSTALLDIR" ); addAttributeIfNotExists( appDirectory, "Name", setup.getApplication() ); //Files installDir = getOrCreateChildById( product, "DirectoryRef", "INSTALLDIR" ); task.processFiles( new CopyActionProcessingStreamAction() { @Override public void processFile( FileCopyDetailsInternal details ) { try { if( !details.isDirectory() ) { String[] segments = details.getRelativePath().getSegments(); File file; try { file = details.getFile(); } catch( UnsupportedOperationException ex ) { // if there is set an filter then we need to copy it File buildDir = task.getTemporaryDir(); file = new File( buildDir, details.getRelativePath().getPathString() ); details.copyTo( file ); } addFile( file, segments ); } } catch( Exception ex ) { throw new GradleException( "Can't add file: " + details, ex ); } } } ); setMinimumOsVersion(); setOnly32BitCondition(); saveLoadLastInstallDir(); addMultiInstanceTransforms(); addBundleJre(); addGUI(); addIcon(); addServices(); addShortcuts(); addRunBeforeUninstall(); addRunAfter(); addDeleteFiles(); addPreAndPostScripts(); //Feature Element feature = getOrCreateChildById( product, "Feature", "MainApplication" ); for( String compID : components ) { getOrCreateChildById( feature, "ComponentRef", compID ); } save(); } /** * Add a condition for the minimum OS version if needed */ private void setMinimumOsVersion() { double version = task.getMinOS(); if( version == 0 ) { return; } int intVersion = 100 * (int)version + (int)((version * 10) % 10); String os; switch( intVersion ) { case 1000: os = "Windows 10, Windows Server 2016"; break; case 603: os = "Windows 8.1, Windows Server 2012 R2"; break; case 602: os = "Windows 8, Windows Server 2012"; break; case 601: os = "Windows 7, Windows Server 2008 R2"; break; case 600: os = "Windows Vista, Windows Server 2008"; break; case 502: os = "Windows Server 2003"; break; case 501: os = "Windows XP"; break; case 500: os = "Windows 2000"; break; default: throw new RuntimeException("Unsupported minimum OS version: " + version + ", " + intVersion ); } String msg = java.text.MessageFormat.format( "{0} is only supported on {1}, or higher.", setup.getApplication(), os ); Element condition = getOrCreateChildByKeyValue( product, "Condition", "Message", msg ); condition.setTextContent( "Installed OR (VersionNT >= " + intVersion + ")" ); } /** * Add condition for 32 bit only if needed. */ private void setOnly32BitCondition() { if( !task.isOnly32Bit() ) { return; } String msg = "You are attempting to run the 32-bit installer on a 64-bit version of Windows."; Element condition = getOrCreateChildByKeyValue( product, "Condition", "Message", msg ); condition.setTextContent( "NOT Msix64" ); } /** * Save the directory of the last installation and load it on an update as default install dir. */ private void saveLoadLastInstallDir() { String key = "Software\\" + setup.getVendor() + '\\' + setup.getApplication() + '\\' + "LastInstallDir"; // save the INSTALLDIR to the registry Element component = getComponent( installDir, "install_path" ); Element regkey = addRegistryKey( component, "HKLM", "install_path_reg", key ); addRegistryValue( regkey, null, "string", "[INSTALLDIR]" ); // load the INSTALLDIR from the registry Element lastInstalldir = getOrCreateChildById( product, "Property", "INSTALLDIR" ); addAttributeIfNotExists( lastInstalldir, "Secure", "yes" ); Element search = getOrCreateChildById( lastInstalldir, "RegistrySearch", "SearchInstallDir" ); addAttributeIfNotExists( search, "Root", "HKLM" ); addAttributeIfNotExists( search, "Key", key ); addAttributeIfNotExists( search, "Type", "directory" ); } /** * Get or create a directory node. * * @param segments the segments of the path in the target. The last segment contains the file name. * @return the directory node */ private Element getDirectory( String[] segments ) { return getDirectory( segments, segments.length - 1 ); } /** * Get or create a directory node. * * @param segments the segments of the path in the target. The last segment contains the file name. * @param length the used length from the segments * @return the directory node */ private Element getDirectory( String[] segments, int length ) { Element parent = installDir; for( int i = 0; i < length; i++ ) { String seg = segments[i]; parent = getOrCreateChildById( parent, "Directory", id( segments, i + 1 ) ); addAttributeIfNotExists( parent, "Name", seg ); } return parent; } /** * Get or Create a component node * * @param dir the parent directory node. * @param compID the ID of the component * @return the component node */ private Element getComponent( Element dir, String compID ) { components.add( compID ); Element component = getOrCreateChildById( dir, "Component", compID ); addAttributeIfNotExists( component, "Guid", getGuid( compID ) ); if( task.getMultiInstanceCount() > 1 ) { addAttributeIfNotExists( component, "MultiInstance", "yes" ); } return component; } /** * Add a file to the setup * * @param file the file to add. * @param segments the segments of the path in the target. The last segment contains the file name. */ private String addFile( File file, String[] segments ) { Element parent = getDirectory( segments ); String pathID = id( segments, segments.length - 1 ); String compID = id( pathID + "_Comp"); Element component = getComponent( parent, compID ); String name = segments[segments.length - 1]; String id = addFile( component, file, pathID, name ); // save the jvm.dll position if( name.equals( "jvm.dll" ) ) { StringBuilder jvm = new StringBuilder(); for( String segment : segments ) { if( jvm.length() > 0 ) { jvm.append( '\\' ); } jvm.append( segment ); } jvmDll = jvm.toString(); } return id; } /** * Add a file. * * @param component the parent component node * @param file the source file * @param pathID an ID of the parent path * @param name the target file name */ private String addFile( Element component, File file, String pathID, String name ) { return addFile( component, file, pathID, name, isAddFiles ); } /** * Add a file. * * @param component the parent component node * @param file the source file * @param pathID an ID of the parent path * @param name the target file name * @param isAddFiles if the file should added or not. On creating the multi language translations the files will not * added to improve the performance. */ private String addFile( Element component, File file, String pathID, String name, boolean isAddFiles ) { String id = id( pathID + '_' + name ); if( isAddFiles ) { Element fileEl = getOrCreateChildById( component, "File", id ); addAttributeIfNotExists( fileEl, "Source", file.getAbsolutePath() ); addAttributeIfNotExists( fileEl, "Name", name ); } else { getOrCreateChild( component, "CreateFolder" ); } return id; } /** * Add all files in a directory. * * @param dir the source directory * @param baseLength the base length of the directory. This length will be cut from the absolute path. * @param target the target directory */ private void addDirectory( File dir, int baseLength, String target ) { for( File file : dir.listFiles() ) { if( file.isDirectory() ) { addDirectory( file, baseLength, target ); } else { String name = file.getAbsolutePath().substring( baseLength ); addFile( file, segments( (target + name) ) ); } } } /** * Bundle a JRE if setup. */ private void addBundleJre() { Object jre = setup.getBundleJre(); if( jre == null ) { return; } File jreDir; try { jreDir = task.getProject().file( jre ); } catch( Exception e ) { jreDir = null; } if( jreDir == null || !jreDir.isDirectory() ) { // bundleJRE is not a directory, we interpret it as a version number String programFiles = System.getenv( task.is64Bit() ? "ProgramW6432" : "ProgramFiles(x86)" ); if( programFiles == null ) { throw new GradleException( "Environment ProgramFiles not found." ); } File java = new File( new File( programFiles ), "Java" ); if( !java.isDirectory() ) { throw new GradleException( "No installed Java VMs found: " + java ); } List<File> versions = new ArrayList<File>(); String [] s = { "jre", "jdk" }; Arrays.asList( s ).forEach( prefix -> { versions.addAll( getDirectories( java, prefix + jre ) ); if( versions.size() == 0 ) { // if the java version is like "1.8" then search also for "jre8" String jreStr = jre.toString(); if( jreStr.length() > 2 && jreStr.startsWith( "1." ) ) { versions.addAll( getDirectories( java, "jre" + jreStr.substring( 2 ) ) ); } } }); if( versions.size() == 0 ) { throw new GradleException( "bundleJre version " + jre + " can not be found in: '" + java + "' Its search for an folder that starts with: jre" + jre ); } Collections.sort( versions ); jreDir = versions.get( versions.size() - 1 ); if ( jreDir.getName().startsWith( "jdk" ) ) { // if this was a jdk, we have to use the jre subdirectory jreDir = new File( jreDir, "jre" ); } } task.getProject().getLogger().lifecycle( "\tbundle jre: " + jreDir ); int baseLength = jreDir.getAbsolutePath().length(); javaDir = setup.getBundleJreTarget().replace( '/', '\\' ); if( javaDir.endsWith( "\\" ) ) { baseLength++; } addDirectory( jreDir, baseLength, javaDir ); } /** * Get all directories from the directory that start with the prefix. * * @param parent parent directory * @param prefix the searching prefix * @return list of directories */ private static List<File> getDirectories( File parent, String prefix ) { ArrayList<File> files = new ArrayList<File>(); for( File file : parent.listFiles() ) { if( file.isDirectory() && file.getName().startsWith( prefix ) ) { files.add( file ); } } return files; } /** * Add the GUI to the Setup. * * @throws Exception if any exception occur */ private void addGUI() throws Exception { Element wixUiInstalldir = getOrCreateChildById( product, "Property", "WIXUI_INSTALLDIR" ); addAttributeIfNotExists( wixUiInstalldir, "Value", "INSTALLDIR" ); Element uiRef = getOrCreateChild( product, "UIRef" ); addAttributeIfNotExists( uiRef, "Id", "WixUI_InstallDir" ); boolean isLicense = addLicense( product ); if( !isLicense ) { // skip license dialog because we does no hat a license text Element ui = getOrCreateChild( product, "UI" ); Element child = getOrCreateChildByKeyValue( ui, "Publish", "Dialog", "WelcomeDlg" ); child.setAttribute( "Control", "Next" ); child.setAttribute( "Event", "NewDialog" ); child.setAttribute( "Value", "InstallDirDlg" ); child.setAttribute( "Order", "2" ); child.setTextContent( "1" ); child = getOrCreateChildByKeyValue( ui, "Publish", "Dialog", "InstallDirDlg" ); child.setAttribute( "Control", "Back" ); child.setAttribute( "Event", "NewDialog" ); child.setAttribute( "Value", "WelcomeDlg" ); child.setAttribute( "Order", "2" ); child.setTextContent( "1" ); } File file = task.getBannerBmp(); if( file != null ) { Element licenseNode = getOrCreateChildById( product, "WixVariable", "WixUIBannerBmp" ); addAttributeIfNotExists( licenseNode, "Value", file.getAbsolutePath() ); } file = task.getDialogBmp(); if( file != null ) { Element licenseNode = getOrCreateChildById( product, "WixVariable", "WixUIDialogBmp" ); addAttributeIfNotExists( licenseNode, "Value", file.getAbsolutePath() ); } } /** * Add an icon in Add/Remove Programs * * @param appDirRef * @throws IOException if an error occur on reading the image files */ private void addIcon() throws IOException { File iconFile = setup.getIconForType( buildDir, "ico" ); if( iconFile == null ) { // no icon was set return; } Element icon = getOrCreateChildById( product, "Icon", ICON_ID ); addAttributeIfNotExists( icon, "SourceFile", iconFile.getAbsolutePath() ); Element appProduction = getOrCreateChildById( product, "Property", "ARPPRODUCTICON" ); addAttributeIfNotExists( appProduction, "Value", ICON_ID ); } /** * Add a license file to the setup. If the license file is not an RTF file then it convert it to RTF. * * @param product the product node in the XML. * @return true, if license was added; false if license was not added. * @throws Exception if any exception occur */ private boolean addLicense( Element product ) throws Exception { // TODO: Internationalize File license = setup.getLicenseFile( "en" ); if( license == null ) { return false; } boolean isRtf; try( FileInputStream fis = new FileInputStream( license ) ) { byte[] bytes = new byte[5]; fis.read( bytes ); isRtf = "{\rtf".equals( new String( bytes ) ); } if( !isRtf ) { // Convert a txt file in a rtf file JEditorPane p = new JEditorPane(); EditorKit kit = p.getEditorKitForContentType( "text/plain" ); p.setContentType( "text/rtf" ); DefaultStyledDocument doc = (DefaultStyledDocument)p.getDocument(); try( FileInputStream fis = new FileInputStream( license ) ) { kit.read( fis, doc, 0 ); } SimpleAttributeSet attrs = new SimpleAttributeSet(); StyleConstants.setFontSize( attrs, 9 ); StyleConstants.setFontFamily( attrs, "Courier New" ); doc.setCharacterAttributes( 0, doc.getLength(), attrs, false ); kit = p.getEditorKitForContentType( "text/rtf" ); license = new File( buildDir, "license.rtf" ); try( FileOutputStream output = new FileOutputStream( license ) ) { kit.write( output, doc, 0, doc.getLength() ); } } Element licenseNode = getOrCreateChildById( product, "WixVariable", "WixUILicenseRtf" ); addAttributeIfNotExists( licenseNode, "Value", license.getAbsolutePath() ); return true; } /** * Add the windows services. * * @throws IOException if an I/O error occurs when reading or writing */ private void addServices() throws IOException { List<Service> services = setup.getServices(); if( services == null || services.isEmpty() ) { return; } File prunsrv = ResourceUtils.extract( getClass(), task.getArch() + "/prunsrv.exe", buildDir ); File prunmgr = ResourceUtils.extract( getClass(), "x86/prunmgr.exe", buildDir ); for( Service service : services ) { String name = service.getId(); String id = id( name.replace( '-', '_' ) ) + "_service"; String exe = service.getWrapper().replace( '\\', '/' ) + ".exe"; String subdir = service.getWorkDir(); if ( subdir != null && !subdir.isEmpty() ) { exe = new File( new File( subdir ), exe ).getPath(); if( !subdir.endsWith( "\\" ) ) { subdir += '\\'; } } else { subdir = ""; } // add the service file String[] segments = segments( exe ); Element directory = getDirectory( segments ); Element component = getComponent( directory, id ); addFile( component, prunsrv, id, segments[segments.length - 1], true ); // install the windows service Element install = getOrCreateChildById( component, "ServiceInstall", id + "_install" ); addAttributeIfNotExists( install, "Name", name ); addAttributeIfNotExists( install, "DisplayName", service.getDisplayName() ); addAttributeIfNotExists( install, "Description", service.getDescription() ); addAttributeIfNotExists( install, "Start", service.isStartOnBoot() ? "auto" : "demand" ); addAttributeIfNotExists( install, "Type", "ownProcess" ); addAttributeIfNotExists( install, "ErrorControl", "normal" ); addAttributeIfNotExists( install, "Arguments", " \"//RS//" + name + "\""); // add an empty parameters registry key Element regkey = getOrCreateChildById( component, "RegistryKey", id + "_RegParameters" ); addAttributeIfNotExists( regkey, "Root", "HKLM" ); addAttributeIfNotExists( regkey, "Key", "SYSTEM\\CurrentControlSet\\Services\\" + name + "\\Parameters" ); addAttributeIfNotExists( regkey, "ForceDeleteOnUninstall", "yes" ); addAttributeIfNotExists( regkey, "ForceCreateOnInstall", "yes" ); // Java parameter of the service String baseKey = task.is64Bit() ? "SOFTWARE\\Wow6432Node\\Apache Software Foundation\\ProcRun 2.0\\" : "SOFTWARE\\Apache Software Foundation\\ProcRun 2.0\\"; regkey = addRegistryKey( component, "HKLM", id + "_RegJava", baseKey + name + "\\Parameters\\Java" ); addRegistryValue( regkey, "Classpath", "string", service.getMainJar() ); if( setup.getBundleJre() != null ) { addRegistryValue( regkey, "JavaHome", "string", "[INSTALLDIR]" + setup.getBundleJreTarget() ); addRegistryValue( regkey, "Jvm", "string", "[INSTALLDIR]" + jvmDll ); } regkey = addRegistryKey( component, "HKLM", id + "_RegStart", baseKey + name + "\\Parameters\\Start" ); addRegistryValue( regkey, "Class", "string", service.getMainClass() ); addRegistryValue( regkey, "Mode", "string", "jvm" ); addRegistryValue( regkey, "WorkingPath", "string", "[INSTALLDIR]" + subdir ); regkey = addRegistryKey( component, "HKLM", id + "_RegLog", baseKey + name + "\\Parameters\\Log" ); addRegistryValue( regkey, "Path", "string", "[INSTALLDIR]" + subdir ); addRegistryValue( regkey, "Prefix", "string", "service" ); regkey = addRegistryKey( component, "HKLM", id + "_RegStop", baseKey + name + "\\Parameters\\Stop" ); addRegistryValue( regkey, "Class", "string", "java.lang.System" ); // call System.exit() on stop to support Runtime.getRuntime().addShutdownHook() addRegistryValue( regkey, "Mode", "string", "jvm" ); // start the service if( service.isStartOnBoot() ) { Element start = getOrCreateChildById( component, "ServiceControl", id + "_start" ); addAttributeIfNotExists( start, "Name", name ); addAttributeIfNotExists( start, "Start", "install" ); addAttributeIfNotExists( start, "Stop", "both" ); addAttributeIfNotExists( start, "Remove", "uninstall" ); addAttributeIfNotExists( start, "Wait", "yes" ); // MSI does not restart a service if it was stopped before the setup DesktopStarter run = new DesktopStarter( setup ); run.setExecutable( "net" ); run.setStartArguments( "start \"" + name + "\"" ); addRun( run, id + "Restart", "ignore", null ); addCustomActionToSequence( id + "Restart", true, "InstallFiles", true, "NOT REMOVE" ); } // Add the prunmgr.exe and change it name dynamically to the service name. Dynamically is important for multiple instances. String target = name.replace( '[', '_' ).replace( ']', '_' ); addFile( component, prunmgr, id + "GUI", target + ".exe", true ); renameFileIfDynamic( id, subdir, target + ".exe", name + ".exe" ); // delete log files on uninstall addDeleteFiles( subdir + "service.*.log" ); } } /** * Rename a file after it was copy. * * @param id the base ID for the actions * @param directory the directory of the file * @param sourceName the sourcename. This should not contain any placeholder in brackets [] * @param targetName the targetname, this can contain place holder */ private void renameFileIfDynamic( String id, String directory, String sourceName, String targetName ) { int idx = targetName.indexOf( '[' ); if( idx < 0 || targetName.indexOf( ']' ) < idx ) { // not a dynamic name return; } DesktopStarter run = new DesktopStarter( setup ); run.setExecutable( "ren" ); run.setStartArguments( "\"" + directory + sourceName + "\" \"" + targetName + "\"" ); addRun( run, id + "Rename", "ignore", null ); addCustomActionToSequence( id + "Rename", true, "CreateShortcuts", true, null ); run = new DesktopStarter( setup ); run.setExecutable( "del" ); run.setStartArguments( "/Q /F \"" + directory + targetName + "\"" ); addRun( run, id + "Delete", "ignore", null ); addCustomActionToSequence( id + "Delete", true, "InstallFiles", false, "NOT Installed OR REINSTALL OR REMOVE" ); } /** * Get the parent Component for a shortcut. * * @param starter the shortcut description * @param product the node of the product * @return the component node */ private Element getShortcutComponent( DesktopStarter starter, Element product ) { Element targetDir = getOrCreateChildById( product, "Directory", "TARGETDIR" ); String refDirID; boolean removeOnUninstall = false; switch( starter.getLocation() ) { default: case StartMenu: refDirID = "ProgramMenuFolder"; getOrCreateChildById( targetDir, "Directory", refDirID ); getOrCreateChildById( product, "DirectoryRef", refDirID ); break; case ApplicationMenu: refDirID = "ApplicationProgramsFolder"; Element menuFolders = getOrCreateChildById( targetDir, "Directory", "ProgramMenuFolder" ); Element appProgrammsFolder = getOrCreateChildById( menuFolders, "Directory", "ApplicationProgramsFolder" ); addAttributeIfNotExists( appProgrammsFolder, "Name", setup.getApplication() ); removeOnUninstall = true; break; case InstallDir: refDirID = "INSTALLDIR"; break; } Element dirRef = getOrCreateChildById( product, "DirectoryRef", refDirID ); Element component = getComponent( dirRef, "shortcuts_" + refDirID ); if( removeOnUninstall ) { Element removeFolder = getOrCreateChildById( component, "RemoveFolder", "ApplicationProgramsFolder" ); addAttributeIfNotExists( removeFolder, "On", "uninstall" ); } Element reg = addRegistryKey( component, "HKCU", "shortcuts_reg_" + refDirID, "Software\\" + setup.getVendor() + "\\" + setup.getApplication() ); reg = addRegistryValue( reg, "shortcut_" + refDirID, "string", "" ); addAttributeIfNotExists( reg, "KeyPath", "yes" ); return component; } /** * Add the shortcuts if which was define. * * @throws IOException If any I/O exception occur on icon loading */ private void addShortcuts() throws IOException { List<DesktopStarter> starters = setup.getDesktopStarters(); if( starters.isEmpty() ) { return; } for( DesktopStarter starter : starters ) { Element component = getShortcutComponent( starter, product ); String id = id( starter.getLocation() + "_" + starter.getDisplayName() ); Element shortcut = getOrCreateChildById( component, "Shortcut", id ); String name = starter.getDisplayName().replace( '[', '_' ).replace( ']', '_' ); addAttributeIfNotExists( shortcut, "Name", name ); addAttributeIfNotExists( shortcut, "Description", starter.getDescription() ); addAttributeIfNotExists( shortcut, "WorkingDirectory", getWoringDirID( starter ) ); // find the optional id for an icon String target = starter.getExecutable(); String iconID; if( starter.getIcons() != null ) { if( starter.getIcons().equals( setup.getIcons() ) ) { iconID = ICON_ID; } else { File iconFile = starter.getIconForType( buildDir, "ico" ); iconID = id( starter.getDisplayName() + ".ico" ); Element icon = getOrCreateChildById( product, "Icon", iconID ); addAttributeIfNotExists( icon, "SourceFile", iconFile.getAbsolutePath() ); } } else { iconID = null; } if( target == null || target.isEmpty() ) { // if target is empty then it must be a Java application if( iconID == null ) { iconID = ICON_ID; } } CommandLine cmd = new CommandLine( starter, javaDir ); addAttributeIfNotExists( shortcut, "Target", cmd.target ); if( !cmd.arguments.isEmpty() ) { addAttributeIfNotExists( shortcut, "Arguments", cmd.arguments ); } if( iconID != null ) { addAttributeIfNotExists( shortcut, "Icon", iconID ); } String linkLocation; switch( starter.getLocation() ) { default: case StartMenu: linkLocation = "[ProgramMenuFolder]"; break; case ApplicationMenu: linkLocation = "[ApplicationProgramsFolder]"; break; case InstallDir: linkLocation = "[INSTALLDIR]"; break; } if( starter.getWorkDir() != null ) { linkLocation += starter.getWorkDir(); } renameFileIfDynamic( id, linkLocation, name + ".lnk", starter.getDisplayName() + ".lnk" ); registerFileExtension( starter, cmd ); } } /** * Register file extension. * * @param starter desktop starter * @param cmd the command line to execute * @throws IOException if any IOException occur */ private void registerFileExtension( DesktopStarter starter, CommandLine cmd ) throws IOException { if( isAddFiles ) { for( DocumentType docType : starter.getDocumentType() ) { for( String fileExtension : docType.getFileExtension() ) { if( fileExtension.startsWith( "." ) ) { fileExtension = fileExtension.substring( 1 ); } String pID = id( setup.getAppIdentifier() + "." + fileExtension ); Element component = getComponent( installDir, "_file_extension" ); getOrCreateChild( component, "CreateFolder" ); Element progID = getOrCreateChildById( component, "ProgId", pID ); addAttributeIfNotExists( progID, "Description", docType.getName() ); File iconFile = starter.getIconForType( buildDir, "ico" ); if( iconFile != null ) { String iconID = addFile( iconFile, new String[] { iconFile.getName() } ); addAttributeIfNotExists( progID, "Icon", iconID ); } Element extension = getOrCreateChildById( progID, "Extension", fileExtension ); addAttributeIfNotExists( extension, "ContentType", docType.getMimetype() ); Element verb = getOrCreateChildById( extension, "Verb", "open" ); Element reg = addRegistryKey( component, "HKCR", id(pID + "\\shell\\open"), pID + "\\shell\\open" ); addRegistryValue( reg, "FriendlyAppName", "string", setup.getApplication() ); String targetFile = cmd.relativTarget; if( targetFile.startsWith( "[INSTALLDIR]" ) ) { targetFile = targetFile.substring( "[INSTALLDIR]".length() ); // für id we need to cut the [INSTALLDIR]. For java command there is ever a directory } String[] segments = segments( targetFile ); addAttributeIfNotExists( verb, "TargetFile", id( segments, segments.length ) ); addAttributeIfNotExists( verb, "Argument", cmd.arguments + "\"%1\"" ); } } } } /** * Get the ID of the working directory. * * @param starter the shortcut definition * @return the id */ private String getWoringDirID( DesktopStarter starter ) { String workDir = starter.getWorkDir(); if( workDir != null && !workDir.isEmpty() ) { String[] segments = segments( workDir ); Element dir = getDirectory( segments, segments.length ); return dir.getAttribute( "Id" ); } else { return "INSTALLDIR"; } } /** * Add a runner without command line window. * * @param run the runner * @param id the id that should be used * @param Return the Return attribute, can be null (default "check"), possible values: "asyncNoWait", "asyncWait", "check" or "ignore" * @param execute the Execute attribute */ private void addRun( DesktopStarter run, String id, String Return, String execute ) { CommandLine cmd = new CommandLine( run, javaDir ); Element action = getOrCreateChildById( product, "CustomAction", id ); // run with elevated privileges addAttributeIfNotExists( action, "Execute", execute == null ? "deferred" : execute ); addAttributeIfNotExists( action, "Impersonate", "no" ); if( Return != null ) { addAttributeIfNotExists( action, "Return", Return ); } String targetID; String dllEntry; String command; if( !"asyncNoWait".equals( Return ) ) { if( cmd.arguments.isEmpty() ) { targetID = "WixShellExecTarget"; dllEntry = "WixShellExec"; command = cmd.full; } else { targetID = id; dllEntry = "CAQuietExec"; command = "\"[SystemFolder]cmd.exe\" /C \"cd /D \"[INSTALLDIR]" + cmd.workDir + "\" & " + cmd.relativFull + '\"'; } } else { addAttributeIfNotExists( action, "Directory", getWoringDirID( run ) ); addAttributeIfNotExists( action, "ExeCommand", '\"' + cmd.target + "\" " + cmd.arguments ); // full quoted path + arguments return; } Element target = getOrCreateChildByKeyValue( product, "SetProperty", "Action", id + "_target" ); addAttributeIfNotExists( target, "Id", targetID ); addAttributeIfNotExists( target, "Before", id ); addAttributeIfNotExists( target, "Sequence", "execute" ); addAttributeIfNotExists( target, "Value", command ); addAttributeIfNotExists( action, "BinaryKey", "WixCA" ); addAttributeIfNotExists( action, "DllEntry", dllEntry ); } /** * Add an action that is executed before uninstall. */ private void addRunBeforeUninstall() { DesktopStarter runBeforeUninstall = setup.getRunBeforeUninstall(); if( runBeforeUninstall == null ) { return; } String id = "runBeforeUninstall"; addRun( runBeforeUninstall, id, "ignore", null ); // http://stackoverflow.com/questions/320921/how-to-add-a-wix-custom-action-that-happens-only-on-uninstall-via-msi addCustomActionToSequence( id, true, "StopServices", true, "REMOVE=\"ALL\" AND NOT UPGRADINGPRODUCTCODE" ); } /** * Add an action that is executed after the setup. */ private void addRunAfter() { DesktopStarter runAfter = setup.getRunAfter(); if( runAfter == null ) { return; } String id = "runAfter"; addRun( runAfter, id, "asyncNoWait", "immediate" ); Element ui = getOrCreateChild( product, "UI" ); Element exitDialog = getOrCreateChildByKeyValue( ui, "Publish", "Dialog", "ExitDialog" ); addAttributeIfNotExists( exitDialog, "Control", "Finish" ); addAttributeIfNotExists( exitDialog, "Event", "DoAction" ); addAttributeIfNotExists( exitDialog, "Value", id ); // http://stackoverflow.com/questions/320921/how-to-add-a-wix-custom-action-that-happens-only-on-uninstall-via-msi exitDialog.setTextContent( "NOT Installed OR REINSTALL OR UPGRADINGPRODUCTCODE" ); } /** * Add a CustomAction to one of the sequences. * * @param id the ID of the action * @param execute true, InstallExecuteSequence; false, InstallUISequence * @param sequenceAction the name of an existing action in sequence after which it should be added. * https://msdn.microsoft.com/en-us/library/aa372038(v=vs.85).aspx * https://msdn.microsoft.com/en-us/library/aa372039(v=vs.85).aspx * @param after true, After the action; false, Before * @param condition the condition under which it should run * @return the Custom element node */ private Element addCustomActionToSequence( String id, boolean execute, String sequenceAction, boolean after, String condition ) { Element executeSequence = getOrCreateChild( product, execute ? "InstallExecuteSequence" : "InstallUISequence" ); Element custom = getOrCreateChildByKeyValue( executeSequence, "Custom", "Action", id ); addAttributeIfNotExists( custom, after ? "After" : "Before", sequenceAction ); if( condition != null ) { custom.setTextContent( condition ); } return custom; } /** * Add files to delete if there any define in SetupBuilder. */ private void addDeleteFiles() { for( String pattern : setup.getDeleteFiles() ) { addDeleteFiles( pattern ); } for( String folder : setup.getDeleteFolders() ) { folder = folder.replace( '/', '\\' ); String id = id( "rmdir_" + folder ); if( folder.endsWith( "\\" ) ) { folder = folder.substring( 0, folder.length()-1 ); } DesktopStarter run = new DesktopStarter( setup ); run.setExecutable( "rmdir" ); run.setStartArguments( "/S /Q \"[INSTALLDIR]" + folder + '\"' ); addRun( run, id, "ignore", null ); addCustomActionToSequence( id, true, "RemoveFolders", true, null ); } } /** * Add files to delete. * * @param pattern the pattern to delete * @return the component */ private Element addDeleteFiles( String pattern ) { String[] segments = segments( pattern ); Element dir = getDirectory( segments ); String id = id( pattern ); Element component = getComponent( dir, "deleteFiles" + id ); Element remove = getOrCreateChildById( component, "RemoveFile", "removeFile" + id ); addAttributeIfNotExists( remove, "On", "both" ); addAttributeIfNotExists( remove, "Name", segments[segments.length - 1] ); return component; } /** * Add a registry key. * * @param component parent component * @param root The root of the key like HKLM, HKCU, HKMU * @param id the id for the key * @param key the key * @return the element to add values */ private Element addRegistryKey( Element component, String root, String id, String key ) { Element regkey = getOrCreateChildById( component, "RegistryKey", id ); addAttributeIfNotExists( regkey, "Root", root ); addAttributeIfNotExists( regkey, "Key", key ); addAttributeIfNotExists( regkey, "ForceDeleteOnUninstall", "yes" ); return regkey; } /** * Add a registry value. * * @param regkey the parent registry key * @param name the value name, null use the default value of a key * @param type the type * @param value the value * @return the value node */ private Element addRegistryValue( Element regkey, String name, String type, String value ) { Element regValue = getOrCreateChildByKeyValue( regkey, "RegistryValue", "Name", name ); addAttributeIfNotExists( regValue, "Type", type ); addAttributeIfNotExists( regValue, "Value", value ); return regValue; } /** * Split a path in segments. * * @param path the path to split * @return the segments of the path */ String[] segments( String path ) { return path.split( "[/\\\\]" ); } /** * Create a valid id for a directory from path segments. * * @param segments the segments of the path in the target. The last segment contains the file name. * @param length the length of the segments that should be used for the id * @return a valid id */ private String id( String[] segments, int length ) { if( length <= 0 ) { return ""; } else if( length == 1 ) { return id( segments[0] ); } else { StringBuilder builder = new StringBuilder(); for( int i = 0; i < length; i++ ) { if( i > 0 ) { builder.append( '_' ); } builder.append( segments[i] ); } return id( builder.toString() ); } } /** * Create a valid id from string for the wxs file. * * @param str possible id but with possible invalid characters * @return a valid id */ private String id( String str ) { StringBuilder builder = null; boolean needUnderscoreStart = false; for( int i = 0; i < str.length(); i++ ) { char ch = str.charAt( i ); if( (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch == '_') ) { continue; } if( (ch >= '0' && ch <= '9') || (ch == '.') ) { if( i > 0 ) { continue; } // id must begin with either a letter or an underscore. needUnderscoreStart = true; builder = new StringBuilder(); continue; } if( builder == null ) { builder = new StringBuilder(); } builder.append( str.substring( builder.length(), i ) ); builder.append( '_' ); } if( builder == null ) { if( str.length() <= 72 ) { if( !ids.containsKey( str ) ) { ids.put( str, str ); return str; } if( str.equals( ids.get( str ) ) ) { return str; } } builder = new StringBuilder(); } builder.append( str.substring( builder.length() ) ); if( builder.length() > 62 ) { // 72 is the max id length, we remove the starting part because this occurs only with files and there is the max important part at the end. builder.delete( 0, builder.length() - 62 ); char ch = builder.charAt( 0 ); needUnderscoreStart = (ch >= '0' && ch <= '9') || (ch == '.'); } if( needUnderscoreStart ) { builder.insert( 0, '_' ); } String id = builder.toString(); if( !ids.containsKey( id ) ) { ids.put( id, str ); return id; // new pair } if( str.equals( ids.get( id ) ) ) { return id; // identical pair already exists } // we have a collision, add a hashcode to prevent name collision builder.append( '_' ); builder.append( Integer.toHexString( str.hashCode() ) ); return builder.toString(); } /** * Create a reproducible GUID * * @param id a parameter as random input * @return the GUID */ private String getGuid( String id ) { return UUID.nameUUIDFromBytes( (setup.getVendor() + setup.getApplication() + id).getBytes() ).toString(); } /** * Add the settings for multiple instances * * @throws IOException If an I/O error occur on loading resources */ private void addMultiInstanceTransforms() throws IOException { int instanceCount = task.getMultiInstanceCount(); if( instanceCount <= 1 ) { return; } // http://windows-installer-xml-wix-toolset.687559.n2.nabble.com/Multiple-Instance-Transforms-Walkthrough-Proposed-Simple-Addition-to-WiX-to-Make-Them-Easier-td708828.html // define the property "INSTANCE_ID" Element property = getOrCreateChildById( product, "Property", "INSTANCE_ID" ); addAttributeIfNotExists( property, "Value", "NotSet" ); // define the property "InstancesCount" property = getOrCreateChildById( product, "Property", "InstancesCount" ); addAttributeIfNotExists( property, "Value", Integer.toString( instanceCount ) ); // add instance count InstanceTransforms and UpgradeVersion Element transforms = getOrCreateChildByKeyValue( product, "InstanceTransforms", "Property", "INSTANCE_ID" ); for( int i = 0; i < instanceCount; i++ ) { String id = "Instance_" + i; String guid = getGuid( id ); Element instance = getOrCreateChildById( transforms, "Instance", id ); addAttributeIfNotExists( instance, "ProductCode", "*" ); addAttributeIfNotExists( instance, "UpgradeCode", guid ); Element upgrade = getOrCreateChildById( product, "Upgrade", guid ); Element upgradeVersion = getOrCreateChildByKeyValue( upgrade, "UpgradeVersion", "Property", "WIX_UPGRADE_DETECTED_" + i ); addAttributeIfNotExists( upgradeVersion, "Minimum", "0.0.0.0" ); addAttributeIfNotExists( upgradeVersion, "MigrateFeatures", "yes" ); } Element executeSequence = getOrCreateChild( product, "InstallExecuteSequence" ); Element removeExistingProducts = getOrCreateChild( executeSequence, "RemoveExistingProducts" ); addAttributeIfNotExists( removeExistingProducts, "After", "InstallValidate" ); Element action; // add a vbscript action to set the instance names action = getOrCreateChildById( product, "CustomAction", "SetInstanceID" ); addAttributeIfNotExists( action, "Script", "vbscript" ); try( Scanner scanner = new Scanner( task.getMultiInstanceScript().openStream(), "UTF8" ) ) { String vbscript = scanner.useDelimiter( "\\A" ).next(); // read the completely file into a string vbscript = vbscript.replace( "\r\n", "\n" ); // \n will be replaced with platform default characters. https://bugs.openjdk.java.net/browse/JDK-8133452 action.setTextContent( vbscript ); } addCustomActionToSequence( "SetInstanceID", false, "ExecuteAction", false, null ); addCustomActionToSequence( "SetInstanceID", true, "ValidateProductID", false, null ); // add a action to set the property TRANSFORM action = getOrCreateChildById( product, "CustomAction", "SetTransforms" ); addAttributeIfNotExists( action, "Property", "TRANSFORMS" ); addAttributeIfNotExists( action, "Value", ":[INSTANCE_ID]" ); addCustomActionToSequence( "SetTransforms", false, "SetInstanceID", true, "ACTION = \"INSTALL\"" ); // set the ProductName properties from the property PRODUCT_NAME of the vbscript action = getOrCreateChildById( product, "CustomAction", "SetProductName" ); addAttributeIfNotExists( action, "Property", "ProductName" ); addAttributeIfNotExists( action, "Value", "[PRODUCT_NAME]" ); addCustomActionToSequence( "SetProductName", true, "SetInstanceID", true, "PRODUCT_NAME" ); // add the registry key with instance path Element component = getComponent( installDir, "instance_path" ); Element key = addRegistryKey( component, "HKLM", "instance_path_reg", "Software\\" + setup.getVendor() + "\\" + setup.getApplication() + "\\Instances\\[INSTANCE_NUMBER]" ); addRegistryValue( key, null, "string", "[INSTALLDIR]" ); addRegistryValue( key, "PackageCode", "string", "[PackageCode]" ); } /** * Add pre and post scripts if any set. * * @throws IOException can not occur */ private void addPreAndPostScripts() throws IOException { addPreAndPostScripts( "PreGUI_Script", task.getPreGUI(), false, "PrepareDlg", false, "NOT Installed OR REINSTALL OR UPGRADINGPRODUCTCODE" ); addPreAndPostScripts( "Preinst_Script", task.getPreinst(), true, "InstallInitialize", true, "NOT Installed OR REINSTALL OR UPGRADINGPRODUCTCODE" ); addPreAndPostScripts( "Postinst_Script", task.getPostinst(), true, "InstallFinalize", false, "NOT Installed OR REINSTALL OR UPGRADINGPRODUCTCODE" ); addPreAndPostScripts( "Prerm_Script", task.getPrerm(), true, "InstallInitialize", true, "REMOVE" ); addPreAndPostScripts( "Postrm_Script", task.getPostrm(), true, "InstallFinalize", false, "REMOVE" ); } /** * Add pre and post scripts if any set. * * @param actionId the unique ID * @param scripts the scripts content * @param execute true, InstallExecuteSequence; false, InstallUISequence * @param sequenceAction the name of an existing action in sequence after which it should be added. * @param after true, After the action; false, Before * @param condition the condition under which it should run * @throws IOException can not occur */ private void addPreAndPostScripts( String actionId, List<String> scripts, boolean execute, String sequenceAction, boolean after, String condition ) throws IOException { if( scripts == null || scripts.isEmpty() ) { return; } for( int i = 0; i < scripts.size(); i++ ) { String script = scripts.get( i ); addPreAndPostScripts( actionId + i, script, execute, sequenceAction, after, condition ); } } /** * Add pre and post scripts if any set. * * @param actionId the unique ID * @param script the script content * @param execute true, InstallExecuteSequence; false, InstallUISequence * @param sequenceAction the name of an existing action in sequence after which it should be added. * @param after true, After the action; false, Before * @param condition the condition under which it should run * @throws IOException can not occur */ private void addPreAndPostScripts( String actionId, String script, boolean execute, String sequenceAction, boolean after, String condition ) throws IOException { if( script == null || script.trim().isEmpty() ) { return; } Element action = getOrCreateChildById( product, "CustomAction", actionId ); addAttributeIfNotExists( action, "Script", getScriptLanguage( script ) ); script = script.replace( "\r\n", "\n" ); // \n will be replaced with platform default characters. https://bugs.openjdk.java.net/browse/JDK-8133452 action.setTextContent( script ); addCustomActionToSequence( actionId, execute, sequenceAction, after, condition ); if( execute ) { addAttributeIfNotExists( action, "Execute", "deferred" ); addAttributeIfNotExists( action, "Impersonate", "no" ); //http://blogs.technet.com/b/alexshev/archive/2008/03/25/property-does-not-exist-or-empty-when-accessed-from-deferred-custom-action.aspx action = getOrCreateChildById( product, "CustomAction", "SetProperties" + actionId ); addAttributeIfNotExists( action, "Property", actionId ); addAttributeIfNotExists( action, "Value", "INSTALLDIR='[INSTALLDIR]';ProductCode='[ProductCode]';INSTANCE_ID='[INSTANCE_ID]'" ); addCustomActionToSequence( "SetProperties" + actionId, execute, actionId, false, null ); } } /** * Detect the script language. * * @param script current script * @return "vbscript" or "jsscript" * @throws IOException can not occur */ private static String getScriptLanguage( String script ) throws IOException { BufferedReader reader = new BufferedReader( new StringReader( script ) ); do { String line = reader.readLine(); if( line == null ) { return "vbscript"; } if( line.trim().endsWith( ";" ) ) { return "jscript"; } } while( true ); } }