/* * 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.msi; import groovy.lang.Closure; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map.Entry; import org.gradle.api.GradleException; import org.gradle.api.file.CopySpec; import org.gradle.api.internal.file.FileResolver; import com.inet.gradle.setup.SetupBuilder; import com.inet.gradle.setup.abstracts.AbstractBuilder; import com.inet.gradle.setup.abstracts.DesktopStarter; import com.inet.gradle.setup.util.ResourceUtils; /** * Build a MSI setup for Windows. * * @author Volker Berlin */ class MsiBuilder extends AbstractBuilder<Msi,SetupBuilder> { private SetupBuilder setup; /** * Create a new instance * * @param msi the calling task * @param setup the shared settings * @param fileResolver the file Resolver */ MsiBuilder( Msi msi, SetupBuilder setup, FileResolver fileResolver ) { super( msi, fileResolver ); this.setup = setup; } /** * Build the MSI installer. */ void build() { try { buildLauch4j(); File wxsFile = getWxsFile(); URL template = task.getWxsTemplate(); new WxsFileBuilder( task, setup, wxsFile, buildDir, template, false ).build(); template = wxsFile.toURI().toURL(); candle(); ResourceUtils.extract( getClass(), "sdk/MsiTran.exe", buildDir ); ResourceUtils.extract( getClass(), "sdk/wilangid.vbs", buildDir ); ResourceUtils.extract( getClass(), "sdk/wisubstg.vbs", buildDir ); List<MsiLanguages> languages = task.getLanguages(); File mui = light( languages.get( 0 ) ); HashMap<MsiLanguages, File> translations = new HashMap<>(); for( int i = 1; i < languages.size(); i++ ) { MsiLanguages language = languages.get( i ); File file = light( language ); patchLangID( file, language ); File mst = msitran( mui, file, language ); translations.put( language, mst ); } // Now create a msi with all files new WxsFileBuilder( task, setup, wxsFile, buildDir, template, true ).build(); candle(); mui = light( languages.get( 0 ) ); // Add the translations to the msi with all files StringBuilder langIDs = new StringBuilder( languages.get( 0 ).getLangID() ); for( Entry<MsiLanguages, File> entry : translations.entrySet() ) { MsiLanguages language = entry.getKey(); File mst = entry.getValue(); addTranslation( mui, mst, language ); langIDs.append( ',' ).append( language.getLangID() ); } patchLangID( mui, langIDs.toString() ); // signing and moving the final msi file signTool( mui ); Files.move( mui.toPath(), task.getSetupFile().toPath(), StandardCopyOption.REPLACE_EXISTING ); } catch( RuntimeException ex ) { throw ex; } catch( Exception ex ) { throw new RuntimeException( ex ); } } /** * Create the lauch4j starter if there was set some and add it to the sources. * * @throws Exception if any error occur */ private void buildLauch4j() throws Exception { if( task.getLaunch4js().size() > 0 ) { Launch4jCreator creator = new Launch4jCreator(); for( DesktopStarter launch : task.getLaunch4js() ) { File file = creator.create( launch, task, setup ); signTool( file ); CopySpec copySpec = task.getProject().copySpec( (Closure<CopySpec>)null ); task.with( copySpec ); copySpec.from( file ); String workDir = launch.getWorkDir(); if( workDir != null && !workDir.isEmpty() ) { copySpec.into( workDir ); } } creator.close(); } } /** * Call a program from the WIX installation. * * @param tool the program name * @param parameters the parameters */ private void callWixTool( String tool, ArrayList<String> parameters ) { parameters.add( 0, getToolPath( tool ) ); exec( parameters ); } /** * Call the candle.exe tool. */ private void candle() { ArrayList<String> parameters = new ArrayList<>(); parameters.add( "-nologo" ); parameters.add( "-arch" ); parameters.add( task.getArch() ); parameters.add( "-out" ); parameters.add( buildDir.getAbsolutePath() + '\\' ); parameters.add( getWxsFile().getAbsolutePath() ); parameters.add( "-ext" ); parameters.add( "WixUtilExtension" ); callWixTool( "candle.exe", parameters ); } /** * Call the light.exe tool. * * @param language the target language * @return the generated msi file */ private File light( MsiLanguages language ) { File out = new File( buildDir, setup.getArchiveName() + '_' + language.getCulture() + ".msi" ); ArrayList<String> parameters = new ArrayList<>(); parameters.add( "-nologo" ); parameters.add( "-sice:ICE60" ); // accept *.ttf files to install in the install directory parameters.add( "-ext" ); parameters.add( "WixUIExtension" ); parameters.add( "-ext" ); parameters.add( "WixUtilExtension" ); parameters.add( "-out" ); parameters.add( out.getAbsolutePath() ); parameters.add( "-spdb" ); parameters.add( "-cultures:" + language.getCulture() + ";neutral" ); parameters.add( "*.wixobj" ); callWixTool( "light.exe", parameters ); return out; } /** * Change the language ID of a *.msi file. * * @param file a msi file * @param language the target language */ private void patchLangID( File file, MsiLanguages language ) { ArrayList<String> parameters = new ArrayList<>(); parameters.add( "cscript" ); parameters.add( "//Nologo" ); parameters.add( new File( buildDir, "sdk/wilangid.vbs" ).getAbsolutePath() ); parameters.add( file.getAbsolutePath() ); parameters.add( "Product" ); parameters.add( language.getLangID() ); exec( parameters ); } /** * Set all languages IDs for which translations was added. * * @param mui the multilingual user interface (MUI) installer file * @param langIDs a comma separated list of languages IDs */ private void patchLangID( File mui, String langIDs ) { ArrayList<String> parameters = new ArrayList<>(); parameters.add( "cscript" ); parameters.add( "//Nologo" ); parameters.add( new File( buildDir, "sdk/wilangid.vbs" ).getAbsolutePath() ); parameters.add( mui.getAbsolutePath() ); parameters.add( "Package" ); parameters.add( langIDs ); exec( parameters ); } /** * Call the msitran.exe tool and create a transform file (*.mst). * * @param mui the multilingual user interface (MUI) installer file * @param file the current msi file * @param language current language * @return the *.mst file */ private File msitran( File mui, File file, MsiLanguages language ) { File mst = new File( buildDir, language.getCulture() + ".mst" ); ArrayList<String> parameters = new ArrayList<>(); parameters.add( new File( buildDir, "sdk/MsiTran.exe" ).getAbsolutePath() ); parameters.add( "-g" ); parameters.add( mui.getAbsolutePath() ); parameters.add( file.getAbsolutePath() ); parameters.add( mst.getAbsolutePath() ); exec( parameters ); file.delete(); // after creation of the mst file we does not need it anymore return mst; } /** * Add a transform file to the msi file * @param mui the multilingual user interface (MUI) installer file * @param mst the transform file * @param language current language */ private void addTranslation( File mui, File mst, MsiLanguages language ) { ArrayList<String> parameters = new ArrayList<>(); parameters.add( "cscript" ); parameters.add( "//Nologo" ); parameters.add( new File( buildDir, "sdk/wisubstg.vbs" ).getAbsolutePath() ); parameters.add( mui.getAbsolutePath() ); parameters.add( mst.getAbsolutePath() ); parameters.add( language.getLangID() ); exec( parameters ); mst.delete(); // after adding the mst file we does not need it anymore } /** * Sign a file if the needed information are set. * * @param file file to sign * @throws IOException If any I/O error occur on loading of the sign tool */ private void signTool( File file ) throws IOException { SignTool sign = task.getSignTool(); if( sign == null ) { return; // no sign information set } String tool = ResourceUtils.extract( getClass(), "sdk/signtool.exe", buildDir ).getAbsolutePath(); // signing the file ArrayList<String> parameters = new ArrayList<>(); parameters.add( tool ); parameters.add( "sign" ); if( sign.getCertificate() != null ) { parameters.add( "/f" ); parameters.add( task.getProject().file( sign.getCertificate() ).getAbsolutePath() ); } if( sign.getPassword() != null ) { parameters.add( "/p" ); parameters.add( sign.getPassword() ); } if( sign.getSha1() != null ) { parameters.add( "/sha1" ); parameters.add( sign.getSha1() ); } parameters.add( "/d" ); // http://stackoverflow.com/questions/4315840/the-uac-prompt-shows-a-temporary-random-program-name-for-msi-can-the-correct-na parameters.add( setup.getApplication() ); parameters.add( file.getAbsolutePath() ); exec( parameters ); // timestamp the signing List<String> servers = sign.getTimestamp(); if( servers != null ) { RuntimeException allEx = null; for( String server : servers ) { parameters = new ArrayList<>(); parameters.add( tool ); parameters.add( "timestamp" ); parameters.add( "/t" ); parameters.add( server ); parameters.add( file.getAbsolutePath() ); try { exec( parameters ); allEx = null; break; // timestamp is ok, if no exception occur } catch( RuntimeException ex ) { if( allEx == null ) { allEx = ex; } else { allEx.addSuppressed( ex ); } task.getProject().getLogger().lifecycle( "Timestamp failed: " + ex ); } } if( allEx != null ) { throw allEx; } } } /** * Get the name of the wxs file * * @return the xml file */ private File getWxsFile() { return new File( buildDir, setup.getArchiveName() + ".wxs" ); } /** * Get the calling path (include name) of a WIX tool * * @param tool the name of the tool file * @return the path */ private static String getToolPath( String tool ) { // first check the environ variable WIX String wix = System.getenv( "WIX" ); if( wix != null ) { File file = new File( wix ); file = new File( file, "bin\\" + tool ); if( file.exists() ) { return file.getAbsolutePath(); } } // search on well known folders String programFilesStr = System.getenv( "ProgramFiles(x86)" ); if( programFilesStr == null ) { programFilesStr = System.getenv( "ProgramW6432" ); } if( programFilesStr == null ) { throw new GradleException( "Environment variable ProgramFiles not found." ); } File programFiles = new File( programFilesStr ); String[] programs = programFiles.list(); // Searching the WiX Toolset for( String program : programs ) { if( program.toLowerCase().startsWith( "wix toolset" ) ) { File file = new File( programFiles, program + "\\bin\\" + tool ); if( file.exists() ) { return file.getAbsolutePath(); } } } // Searching the WixEdit for( String program : programs ) { if( program.equalsIgnoreCase( "WixEdit" ) ) { File wixEdit = new File( programFiles, program ); String[] wixEditFiles = wixEdit.list(); for( String wixEditFile : wixEditFiles ) { if( wixEditFile.toLowerCase().startsWith( "wix" ) ) { File file = new File( wixEdit, wixEditFile + "\\" + tool ); if( file.exists() ) { return file.getAbsolutePath(); } } } } } throw new GradleException( tool + " was not found. You need to install the WiX Toolset or set the environment variable WIX. You can download the WiX Toolset from http://wixtoolset.org/" ); } }