# # This file is part of the LibreOffice project. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # This file incorporates work covered by the following license notice: # # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed # with this work for additional information regarding copyright # ownership. The ASF licenses this file to you 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 . # package installer::windows::msp; use File::Copy; use installer::control; use installer::converter; use installer::exiter; use installer::files; use installer::globals; use installer::logger; use installer::pathanalyzer; use installer::systemactions; use installer::windows::admin; use installer::windows::idtglobal; use installer::windows::update; ################################################################################# # Making all required administrative installations ################################################################################# sub install_installation_sets { my ($installationdir) = @_; # Finding the msi database in the new installation set, that is located in $installationdir my $msifiles = installer::systemactions::find_file_with_file_extension("msi", $installationdir); if ( $#{$msifiles} < 0 ) { installer::exiter::exit_program("ERROR: Did not find msi database in directory $installationdir", "create_msp_patch"); } if ( $#{$msifiles} > 0 ) { installer::exiter::exit_program("ERROR: Did find more than one msi database in directory $installationdir", "create_msp_patch"); } my $newinstallsetdatabasepath = $installationdir . $installer::globals::separator . ${$msifiles}[0]; my $oldinstallsetdatabasepath = $installer::globals::updatedatabasepath; # Creating temp directory again installer::systemactions::create_directory_structure($installer::globals::temppath); # Creating old installation directory my $dirname = "admin"; my $installpath = $installer::globals::temppath . $installer::globals::separator . $dirname; if ( ! -d $installpath) { installer::systemactions::create_directory($installpath); } my $oldinstallpath = $installpath . $installer::globals::separator . "old"; my $newinstallpath = $installpath . $installer::globals::separator . "new"; if ( ! -d $oldinstallpath) { installer::systemactions::create_directory($oldinstallpath); } if ( ! -d $newinstallpath) { installer::systemactions::create_directory($newinstallpath); } my $olddatabase = installer::windows::admin::make_admin_install($oldinstallsetdatabasepath, $oldinstallpath); my $newdatabase = installer::windows::admin::make_admin_install($newinstallsetdatabasepath, $newinstallpath); if ( $^O =~ /cygwin/i ) { $olddatabase = qx{cygpath -w "$olddatabase"}; $olddatabase =~ s/\s*$//g; $newdatabase = qx{cygpath -w "$newdatabase"}; $newdatabase =~ s/\s*$//g; } return ($olddatabase, $newdatabase); } ################################################################################# # Extracting all tables from a pcp file ################################################################################# sub extract_all_tables_from_pcpfile { my ($fullpcpfilepath, $workdir) = @_; my $msidb = "msidb.exe"; # Has to be in the path my $infoline = ""; my $systemcall = ""; my $returnvalue = ""; my $extraslash = ""; # Has to be set for non-ActiveState perl my $localfullpcpfile = $fullpcpfilepath; my $localworkdir = $workdir; if ( $^O =~ /cygwin/i ) { # msidb.exe really wants backslashes. (And double escaping because system() expands the string.) $localfullpcpfile =~ s/\//\\\\/g; $localworkdir =~ s/\//\\\\/g; $extraslash = "\\"; } if ( $^O =~ /linux/i ) { $extraslash = "\\"; } # Export of all tables by using "*" $systemcall = $msidb . " -d " . $localfullpcpfile . " -f " . $localworkdir . " -e " . $extraslash . "*"; $returnvalue = system($systemcall); $infoline = "Systemcall: $systemcall\n"; push( @installer::globals::logfileinfo, $infoline); if ($returnvalue) { $infoline = "ERROR: Could not execute $systemcall !\n"; push( @installer::globals::logfileinfo, $infoline); installer::exiter::exit_program("ERROR: Could not exclude tables from pcp file: $fullpcpfilepath !", "extract_all_tables_from_msidatabase"); } else { $infoline = "Success: Executed $systemcall successfully!\n"; push( @installer::globals::logfileinfo, $infoline); } } ################################################################################# # Include tables into a pcp file ################################################################################# sub include_tables_into_pcpfile { my ($fullpcpfilepath, $workdir, $tables) = @_; my $msidb = "msidb.exe"; # Has to be in the path my $infoline = ""; my $systemcall = ""; my $returnvalue = ""; # Make all table 8+3 conform my $alltables = installer::converter::convert_stringlist_into_array(\$tables, " "); for ( my $i = 0; $i <= $#{$alltables}; $i++ ) { my $tablename = ${$alltables}[$i]; $tablename =~ s/\s*$//; my $namelength = length($tablename); if ( $namelength > 8 ) { my $newtablename = substr($tablename, 0, 8); # name, offset, length my $oldfile = $workdir . $installer::globals::separator . $tablename . ".idt"; my $newfile = $workdir . $installer::globals::separator . $newtablename . ".idt"; if ( -f $newfile ) { unlink $newfile; } installer::systemactions::copy_one_file($oldfile, $newfile); } } # Import of tables my $localworkdir = $workdir; my $localfullpcpfilepath = $fullpcpfilepath; if ( $^O =~ /cygwin/i ) { # msidb.exe really wants backslashes. (And double escaping because system() expands the string.) $localfullpcpfilepath =~ s/\//\\\\/g; $localworkdir =~ s/\//\\\\/g; } my @tables = split(' ', $tables); # I found that msidb from Windows SDK 7.1 did not accept more than one table. foreach my $table (@tables) { $systemcall = $msidb . " -d " . $localfullpcpfilepath . " -f " . $localworkdir . " -i " . $table; $returnvalue = system($systemcall); $infoline = "Systemcall: $systemcall\n"; push( @installer::globals::logfileinfo, $infoline); if ($returnvalue) { $infoline = "ERROR: Could not execute $systemcall !\n"; push( @installer::globals::logfileinfo, $infoline); installer::exiter::exit_program("ERROR: Could not include tables into pcp file: $fullpcpfilepath !", "include_tables_into_pcpfile"); } else { $infoline = "Success: Executed $systemcall successfully!\n"; push( @installer::globals::logfileinfo, $infoline); } } } ################################################################################# # Calling msimsp.exe ################################################################################# sub execute_msimsp { my ($fullpcpfilename, $mspfilename, $localmspdir) = @_; my $msimsp = "msimsp.exe"; # Has to be in the path my $infoline = ""; my $systemcall = ""; my $returnvalue = ""; my $logfilename = $localmspdir . $installer::globals::separator . "msimsp.log"; # Using a specific temp for each msimsp.exe process # Creating temp directory again (should already have happened) installer::systemactions::create_directory_structure($installer::globals::temppath); # Creating old installation directory my $dirname = "msimsptemp"; my $msimsptemppath = $installer::globals::temppath . $installer::globals::separator . $dirname; if ( ! -d $msimsptemppath) { installer::systemactions::create_directory($msimsptemppath); } # r:\msvc9p\PlatformSDK\v6.1\bin\msimsp.exe -s c:\patch\hotfix_qfe1.pcp -p c:\patch\patch_ooo3_m2_m3.msp -l c:\patch\patch_ooo3_m2_m3.log if ( -f $logfilename ) { unlink $logfilename; } my $localfullpcpfilename = $fullpcpfilename; my $localmspfilename = $mspfilename; my $locallogfilename = $logfilename; my $localmsimsptemppath = $msimsptemppath; if ( $^O =~ /cygwin/i ) { # msimsp.exe really wants backslashes. (And double escaping because system() expands the string.) $localfullpcpfilename =~ s/\//\\\\/g; $locallogfilename =~ s/\//\\\\/g; $localmspfilename =~ s/\\/\\\\/g; # path already contains backslash $localmsimsptemppath = qx{cygpath -w "$localmsimsptemppath"}; $localmsimsptemppath =~ s/\\/\\\\/g; $localmsimsptemppath =~ s/\s*$//g; } $systemcall = $msimsp . " -s " . $localfullpcpfilename . " -p " . $localmspfilename . " -l " . $locallogfilename . " -f " . $localmsimsptemppath; installer::logger::print_message( "... $systemcall ...\n" ); $returnvalue = system($systemcall); $infoline = "Systemcall: $systemcall\n"; push( @installer::globals::logfileinfo, $infoline); if ($returnvalue) { $infoline = "ERROR: Could not execute $systemcall !\n"; push( @installer::globals::logfileinfo, $infoline); installer::exiter::exit_program("ERROR: Could not execute $systemcall !", "execute_msimsp"); } else { $infoline = "Success: Executed $systemcall successfully!\n"; push( @installer::globals::logfileinfo, $infoline); } return $logfilename; } #################################################################### # Checking existence and saving all tables, that need to be edited #################################################################### sub check_and_save_tables { my ($tablelist, $workdir) = @_; my $tables = installer::converter::convert_stringlist_into_array(\$tablelist, " "); for ( my $i = 0; $i <= $#{$tables}; $i++ ) { my $filename = ${$tables}[$i]; $filename =~ s/\s*$//; my $fullfilename = $workdir . $installer::globals::separator . $filename . ".idt"; if ( ! -f $fullfilename ) { installer::exiter::exit_program("ERROR: Required idt file could not be found: \"$fullfilename\"!", "check_and_save_tables"); } my $savfilename = $fullfilename . ".sav"; installer::systemactions::copy_one_file($fullfilename, $savfilename); } } #################################################################### # Setting the name of the msp database #################################################################### sub set_mspfilename { my ($allvariables, $mspdir, $languagesarrayref) = @_; my $databasename = $allvariables->{'PRODUCTNAME'} . "-" . $allvariables->{'PRODUCTVERSION'} . "-" . $allvariables->{'WINDOWSPATCHLEVEL'} . ".msp"; my $fullmspname = $mspdir . $installer::globals::separator . $databasename; if ( $^O =~ /cygwin/i ) { $fullmspname =~ s/\//\\/g; } return $fullmspname; } #################################################################### # Editing table Properties #################################################################### sub change_properties_table { my ($localmspdir, $mspfilename) = @_; my $infoline = "Changing content of table \"Properties\"\n"; push( @installer::globals::logfileinfo, $infoline); my $filename = $localmspdir . $installer::globals::separator . "Properties.idt"; if ( ! -f $filename ) { installer::exiter::exit_program("ERROR: Could not find file \"$filename\" !", "change_properties_table"); } my $filecontent = installer::files::read_file($filename); my $guidref = installer::windows::msiglobal::get_guid_list(1, 1); ${$guidref}[0] =~ s/\s*$//; # removing ending spaces my $patchcode = "\{" . ${$guidref}[0] . "\}"; # Setting "PatchOutputPath" my $found_patchoutputpath = 0; my $found_patchguid = 0; for ( my $i = 0; $i <= $#{$filecontent}; $i++ ) { if ( ${$filecontent}[$i] =~ /^\s*PatchOutputPath\t(.*?)\s*$/ ) { my $oldvalue = $1; ${$filecontent}[$i] =~ s/\Q$oldvalue\E/$mspfilename/; $found_patchoutputpath = 1; } if ( ${$filecontent}[$i] =~ /^\s*PatchGUID\t(.*?)\s*$/ ) { my $oldvalue = $1; ${$filecontent}[$i] =~ s/\Q$oldvalue\E/$patchcode/; $found_patchguid = 1; } } if ( ! $found_patchoutputpath ) { my $newline = "PatchOutputPath\t$mspfilename\n"; push(@{$filecontent}, $newline); } if ( ! $found_patchguid ) { my $newline = "PatchGUID\t$patchcode\n"; push(@{$filecontent}, $newline); } # saving file installer::files::save_file($filename, $filecontent); } #################################################################### # Editing table TargetImages #################################################################### sub change_targetimages_table { my ($localmspdir, $olddatabase) = @_; my $infoline = "Changing content of table \"TargetImages\"\n"; push( @installer::globals::logfileinfo, $infoline); my $filename = $localmspdir . $installer::globals::separator . "TargetImages.idt"; if ( ! -f $filename ) { installer::exiter::exit_program("ERROR: Could not find file \"$filename\" !", "change_targetimages_table"); } my $filecontent = installer::files::read_file($filename); my @newcontent = (); # Copying the header for ( my $i = 0; $i <= $#{$filecontent}; $i++ ) { if ( $i < 3 ) { push(@newcontent, ${$filecontent}[$i]); } } #Adding all targets my $newline = "T1\t$olddatabase\t\tU1\t1\t0x00000922\t1\n"; push(@newcontent, $newline); # saving file installer::files::save_file($filename, \@newcontent); } #################################################################### # Editing table UpgradedImages #################################################################### sub change_upgradedimages_table { my ($localmspdir, $newdatabase) = @_; my $infoline = "Changing content of table \"UpgradedImages\"\n"; push( @installer::globals::logfileinfo, $infoline); my $filename = $localmspdir . $installer::globals::separator . "UpgradedImages.idt"; if ( ! -f $filename ) { installer::exiter::exit_program("ERROR: Could not find file \"$filename\" !", "change_upgradedimages_table"); } my $filecontent = installer::files::read_file($filename); my @newcontent = (); # Copying the header for ( my $i = 0; $i <= $#{$filecontent}; $i++ ) { if ( $i < 3 ) { push(@newcontent, ${$filecontent}[$i]); } } # Syntax: Upgraded MsiPath PatchMsiPath SymbolPaths Family # default values my $upgraded = "U1"; my $msipath = $newdatabase; my $patchmsipath = ""; my $symbolpaths = ""; my $family = "22334455"; if ( $#{$filecontent} >= 3 ) { my $line = ${$filecontent}[3]; if ( $line =~ /^\s*(.*?)\t(.*?)\t(.*?)\t(.*?)\t(.*?)\s*$/ ) { $upgraded = $1; $patchmsipath = $3; $symbolpaths = $4; $family = $5; } } #Adding sequence line, saving PatchFamily my $newline = "$upgraded\t$msipath\t$patchmsipath\t$symbolpaths\t$family\n"; push(@newcontent, $newline); # saving file installer::files::save_file($filename, \@newcontent); } #################################################################### # Editing table ImageFamilies #################################################################### sub change_imagefamilies_table { my ($localmspdir) = @_; my $infoline = "Changing content of table \"ImageFamilies\"\n"; push( @installer::globals::logfileinfo, $infoline); my $filename = $localmspdir . $installer::globals::separator . "ImageFamilies.idt"; if ( ! -f $filename ) { installer::exiter::exit_program("ERROR: Could not find file \"$filename\" !", "change_imagefamilies_table"); } my $filecontent = installer::files::read_file($filename); my @newcontent = (); # Copying the header for ( my $i = 0; $i <= $#{$filecontent}; $i++ ) { if ( $i < 3 ) { push(@newcontent, ${$filecontent}[$i]); } } # Syntax: Family MediaSrcPropName MediaDiskId FileSequenceStart DiskPrompt VolumeLabel # "FileSequenceStart has to be set # Default values: my $family = "22334455"; my $mediasrcpropname = "MediaSrcPropName"; my $mediadiskid = "2"; my $filesequencestart = get_filesequencestart(); my $diskprompt = ""; my $volumelabel = ""; if ( $#{$filecontent} >= 3 ) { my $line = ${$filecontent}[3]; if ( $line =~ /^\s*(.*?)\t(.*?)\t(.*?)\t(.*?)\t(.*?)\t(.*?)\s*$/ ) { $family = $1; $mediasrcpropname = $2; $mediadiskid = $3; $diskprompt = $5; $volumelabel = $6; } } #Adding sequence line my $newline = "$family\t$mediasrcpropname\t$mediadiskid\t$filesequencestart\t$diskprompt\t$volumelabel\n"; push(@newcontent, $newline); # saving file installer::files::save_file($filename, \@newcontent); } #################################################################### # Setting start sequence for patch #################################################################### sub get_filesequencestart { my $sequence = 1000; # default if ( $installer::globals::updatelastsequence ) { $sequence = $installer::globals::updatelastsequence + 500; } return $sequence; } #################################################################### # Setting time value into pcp file # Format mm/dd/yyyy hh:mm #################################################################### sub get_patchtime_value { # Syntax: 8/8/2008 11:55 my $minute = (localtime())[1]; my $hour = (localtime())[2]; my $day = (localtime())[3]; my $month = (localtime())[4]; my $year = 1900 + (localtime())[5]; $month++; # zero based month if ( $minute < 10 ) { $minute = "0" . $minute; } if ( $hour < 10 ) { $hour = "0" . $hour; } my $timestring = $month . "/" . $day . "/" . $year . " " . $hour . ":" . $minute; return $timestring; } ################################################################################# # Checking, if this is the correct database. ################################################################################# sub correct_langs { my ($langs, $languagestringref) = @_; my $correct_langs = 0; # Comparing $langs with $languagestringref my $langlisthash = installer::converter::convert_stringlist_into_hash(\$langs, ","); my $langstringhash = installer::converter::convert_stringlist_into_hash($languagestringref, "_"); my $not_included = 0; foreach my $onelang ( keys %{$langlisthash} ) { if ( ! exists($langstringhash->{$onelang}) ) { $not_included = 1; last; } } if ( ! $not_included ) { foreach my $onelanguage ( keys %{$langstringhash} ) { if ( ! exists($langlisthash->{$onelanguage}) ) { $not_included = 1; last; } } if ( ! $not_included ) { $correct_langs = 1; } } return $correct_langs; } ################################################################################# # Searching for the path to the reference database for this special product. ################################################################################# sub get_patchid_from_list { my ($filecontent, $languagestringref, $filename) = @_; my $patchid = ""; for ( my $i = 0; $i <= $#{$filecontent}; $i++ ) { my $line = ${$filecontent}[$i]; if ( $line =~ /^\s*$/ ) { next; } # empty line if ( $line =~ /^\s*\#/ ) { next; } # comment line if ( $line =~ /^\s*(.+?)\s*=\s*(.+?)\s*$/ ) { my $langs = $1; my $localpatchid = $2; if ( correct_langs($langs, $languagestringref) ) { $patchid = $localpatchid; last; } } else { installer::exiter::exit_program("ERROR: Wrong syntax in file: $filename! Line: \"$line\"", "get_patchid_from_list"); } } return $patchid; } #################################################################### # Editing table PatchMetadata #################################################################### sub change_patchmetadata_table { my ($localmspdir, $allvariables, $languagestringref) = @_; my $infoline = "Changing content of table \"PatchMetadata\"\n"; push( @installer::globals::logfileinfo, $infoline); my $filename = $localmspdir . $installer::globals::separator . "PatchMetadata.idt"; if ( ! -f $filename ) { installer::exiter::exit_program("ERROR: Could not find file \"$filename\" !", "change_patchmetadata_table"); } my $filecontent = installer::files::read_file($filename); my @newcontent = (); # Syntax: Company Property Value # Interesting properties: "Classification" and "CreationTimeUTC" my $classification_set = 0; my $creationtime_set = 0; my $targetproductname_set = 0; my $manufacturer_set = 0; my $displayname_set = 0; my $description_set = 0; my $allowremoval_set = 0; my $defaultcompany = ""; my $classificationstring = "Classification"; my $classificationvalue = "Hotfix"; if (( $allvariables->{'SERVICEPACK'} ) && ( $allvariables->{'SERVICEPACK'} == 1 )) { $classificationvalue = "ServicePack"; } my $allowremovalstring = "AllowRemoval"; my $allowremovalvalue = "1"; if (( exists($allvariables->{'MSPALLOWREMOVAL'}) ) && ( $allvariables->{'MSPALLOWREMOVAL'} == 0 )) { $allowremovalvalue = 0; } my $timestring = "CreationTimeUTC"; # Syntax: 8/8/2008 11:55 my $timevalue = get_patchtime_value(); my $targetproductnamestring = "TargetProductName"; my $targetproductnamevalue = $allvariables->{'PRODUCTNAME'}; if ( $allvariables->{'PROPERTYTABLEPRODUCTNAME'} ) { $targetproductnamevalue = $allvariables->{'PROPERTYTABLEPRODUCTNAME'}; } my $manufacturerstring = "ManufacturerName"; my $manufacturervalue = $ENV{'OOO_VENDOR'}; if ( $installer::globals::longmanufacturer ) { $manufacturervalue = $installer::globals::longmanufacturer; } my $displaynamestring = "DisplayName"; my $descriptionstring = "Description"; my $displaynamevalue = ""; my $descriptionvalue = ""; my $base = $allvariables->{'PRODUCTNAME'} . " " . $allvariables->{'PRODUCTVERSION'}; if ( $installer::globals::languagepack || $installer::globals::helppack ) { $base = $targetproductnamevalue; } my $windowspatchlevel = 0; if ( $allvariables->{'WINDOWSPATCHLEVEL'} ) { $windowspatchlevel = $allvariables->{'WINDOWSPATCHLEVEL'}; } my $displayaddon = ""; if ( $allvariables->{'PATCHDISPLAYADDON'} ) { $displayaddon = $allvariables->{'PATCHDISPLAYADDON'}; } my $patchsequence = get_patchsequence($allvariables); if (( $allvariables->{'SERVICEPACK'} ) && ( $allvariables->{'SERVICEPACK'} == 1 )) { $displaynamevalue = $base . " ServicePack " . $windowspatchlevel . " " . $patchsequence . " Build: " . $installer::globals::buildid; $descriptionvalue = $base . " ServicePack " . $windowspatchlevel . " " . $patchsequence . " Build: " . $installer::globals::buildid; } else { $displaynamevalue = $base . " Hotfix " . $displayaddon . " " . $patchsequence . " Build: " . $installer::globals::buildid; $descriptionvalue = $base . " Hotfix " . $displayaddon . " " . $patchsequence . " Build: " . $installer::globals::buildid; $displaynamevalue =~ s/ / /g; $descriptionvalue =~ s/ / /g; $displaynamevalue =~ s/ / /g; $descriptionvalue =~ s/ / /g; $displaynamevalue =~ s/ / /g; $descriptionvalue =~ s/ / /g; } if ( $allvariables->{'MSPPATCHNAMELIST'} ) { my $patchnamelistfile = $allvariables->{'MSPPATCHNAMELIST'}; $patchnamelistfile = $installer::globals::idttemplatepath . $installer::globals::separator . $patchnamelistfile; if ( ! -f $patchnamelistfile ) { installer::exiter::exit_program("ERROR: Could not find file \"$patchnamelistfile\".", "change_patchmetadata_table"); } my $filecontent = installer::files::read_file($patchnamelistfile); # Get name and path of reference database my $patchid = get_patchid_from_list($filecontent, $languagestringref, $patchnamelistfile); if ( $patchid eq "" ) { installer::exiter::exit_program("ERROR: Could not find file patchid in file \"$patchnamelistfile\" for language(s) \"$$languagestringref\".", "change_patchmetadata_table"); } # Setting language specific patch id } for ( my $i = 0; $i <= $#{$filecontent}; $i++ ) { if ( ${$filecontent}[$i] =~ /^\s*(.*?)\t(.*?)\t(.*?)\s*$/ ) { my $company = $1; my $property = $2; my $value = $3; if ( $property eq $classificationstring ) { ${$filecontent}[$i] = "$company\t$property\t$classificationvalue\n"; $classification_set = 1; } if ( $property eq $allowremovalstring ) { ${$filecontent}[$i] = "$company\t$property\t$allowremovalvalue\n"; $allowremoval_set = 1; } if ( $property eq $timestring ) { ${$filecontent}[$i] = "$company\t$property\t$timevalue\n"; $creationtime_set = 1; } if ( $property eq $targetproductnamestring ) { ${$filecontent}[$i] = "$company\t$property\t$targetproductnamevalue\n"; $targetproductname_set = 1; } if ( $property eq $manufacturerstring ) { ${$filecontent}[$i] = "$company\t$property\t$manufacturervalue\n"; $manufacturer_set = 1; } if ( $property eq $displaynamestring ) { ${$filecontent}[$i] = "$company\t$property\t$displaynamevalue\n"; $displayname_set = 1; } if ( $property eq $descriptionstring ) { ${$filecontent}[$i] = "$company\t$property\t$descriptionvalue\n"; $description_set = 1; } } push(@newcontent, ${$filecontent}[$i]); } if ( ! $classification_set ) { my $line = "$defaultcompany\t$classificationstring\t$classificationvalue\n"; push(@newcontent, $line); } if ( ! $allowremoval_set ) { my $line = "$defaultcompany\t$classificationstring\t$allowremovalvalue\n"; push(@newcontent, $line); } if ( ! $allowremoval_set ) { my $line = "$defaultcompany\t$classificationstring\t$allowremovalvalue\n"; push(@newcontent, $line); } if ( ! $creationtime_set ) { my $line = "$defaultcompany\t$timestring\t$timevalue\n"; push(@newcontent, $line); } if ( ! $targetproductname_set ) { my $line = "$defaultcompany\t$targetproductnamestring\t$targetproductnamevalue\n"; push(@newcontent, $line); } if ( ! $manufacturer_set ) { my $line = "$defaultcompany\t$manufacturerstring\t$manufacturervalue\n"; push(@newcontent, $line); } if ( ! $displayname_set ) { my $line = "$defaultcompany\t$displaynamestring\t$displaynamevalue\n"; push(@newcontent, $line); } if ( ! $description_set ) { my $line = "$defaultcompany\t$descriptionstring\t$descriptionvalue\n"; push(@newcontent, $line); } # saving file installer::files::save_file($filename, \@newcontent); } #################################################################### # Editing table PatchSequence #################################################################### sub change_patchsequence_table { my ($localmspdir, $allvariables) = @_; my $infoline = "Changing content of table \"PatchSequence\"\n"; push( @installer::globals::logfileinfo, $infoline); my $filename = $localmspdir . $installer::globals::separator . "PatchSequence.idt"; if ( ! -f $filename ) { installer::exiter::exit_program("ERROR: Could not find file \"$filename\" !", "change_patchsequence_table"); } my $filecontent = installer::files::read_file($filename); my @newcontent = (); # Copying the header for ( my $i = 0; $i <= $#{$filecontent}; $i++ ) { if ( $i < 3 ) { push(@newcontent, ${$filecontent}[$i]); } } # Syntax: PatchFamily Target Sequence Supersede my $patchfamily = "SO"; my $target = ""; my $patchsequence = get_patchsequence($allvariables); my $supersede = get_supersede($allvariables); if ( $#{$filecontent} >= 3 ) { my $line = ${$filecontent}[3]; if ( $line =~ /^\s*(.*?)\t(.*?)\t(.*?)\t(.*?)\s$/ ) { $patchfamily = $1; $target = $2; } } #Adding sequence line, saving PatchFamily my $newline = "$patchfamily\t$target\t$patchsequence\t$supersede\n"; push(@newcontent, $newline); # saving file installer::files::save_file($filename, \@newcontent); } #################################################################### # Setting supersede, "0" for Hotfixes, "1" for ServicePack #################################################################### sub get_supersede { my ( $allvariables ) = @_; my $supersede = 0; # if not defined, this is a Hotfix if (( $allvariables->{'SERVICEPACK'} ) && ( $allvariables->{'SERVICEPACK'} == 1 )) { $supersede = 1; } return $supersede; } #################################################################### # Setting the sequence of the patch #################################################################### sub get_patchsequence { my ( $allvariables ) = @_; my $patchsequence = "1.0"; if ( ! $allvariables->{'PACKAGEVERSION'} ) { installer::exiter::exit_program("ERROR: PACKAGEVERSION must be set for msp patch creation!", "get_patchsequence"); } my $packageversion = $allvariables->{'PACKAGEVERSION'}; if ( $packageversion =~ /^\s*(\d+)\.(\d+)\.(\d+)\.(\d+)\s*$/ ) { my $major = $1; my $minor = $2; my $micro = $3; my $patch = $4; $patchsequence = $major . "\." . $minor . "\." . $micro . "\." . $patch; } return $patchsequence; } #################################################################### # Editing all tables from pcp file, that need to be edited #################################################################### sub edit_tables { my ($tablelist, $localmspdir, $olddatabase, $newdatabase, $mspfilename, $allvariables, $languagestringref) = @_; # table list contains: my $tablelist = "Properties TargetImages UpgradedImages ImageFamilies PatchMetadata PatchSequence"; change_properties_table($localmspdir, $mspfilename); change_targetimages_table($localmspdir, $olddatabase); change_upgradedimages_table($localmspdir, $newdatabase); change_imagefamilies_table($localmspdir); change_patchmetadata_table($localmspdir, $allvariables, $languagestringref); change_patchsequence_table($localmspdir, $allvariables); } ################################################################################# # Checking, if this is the correct database. ################################################################################# sub correct_patch { my ($product, $pro, $langs, $languagestringref) = @_; my $correct_patch = 0; # Comparing $product with $installer::globals::product and # $pro with $installer::globals::pro and # $langs with $languagestringref my $product_is_good = 0; my $localproduct = $installer::globals::product; if ( $installer::globals::languagepack ) { $localproduct = $localproduct . "LanguagePack"; } elsif ( $installer::globals::helppack ) { $localproduct = $localproduct . "HelpPack"; } if ( $product eq $localproduct ) { $product_is_good = 1; } if ( $product_is_good ) { my $pro_is_good = 0; if ((( $pro eq "pro" ) && ( $installer::globals::pro )) || (( $pro eq "nonpro" ) && ( ! $installer::globals::pro ))) { $pro_is_good = 1; } if ( $pro_is_good ) { my $langlisthash = installer::converter::convert_stringlist_into_hash(\$langs, ","); my $langstringhash = installer::converter::convert_stringlist_into_hash($languagestringref, "_"); my $not_included = 0; foreach my $onelang ( keys %{$langlisthash} ) { if ( ! exists($langstringhash->{$onelang}) ) { $not_included = 1; last; } } if ( ! $not_included ) { foreach my $onelanguage ( keys %{$langstringhash} ) { if ( ! exists($langlisthash->{$onelanguage}) ) { $not_included = 1; last; } } if ( ! $not_included ) { $correct_patch = 1; } } } } return $correct_patch; } ################################################################################# # Searching for the path to the required patch for this special product. ################################################################################# sub get_requiredpatchfile_from_list { my ($filecontent, $languagestringref, $filename) = @_; my $patchpath = ""; for ( my $i = 0; $i <= $#{$filecontent}; $i++ ) { my $line = ${$filecontent}[$i]; if ( $line =~ /^\s*$/ ) { next; } # empty line if ( $line =~ /^\s*\#/ ) { next; } # comment line if ( $line =~ /^\s*(.+?)\s*\t+\s*(.+?)\s*\t+\s*(.+?)\s*\t+\s*(.+?)\s*$/ ) { my $product = $1; my $pro = $2; my $langs = $3; my $path = $4; if (( $pro ne "pro" ) && ( $pro ne "nonpro" )) { installer::exiter::exit_program("ERROR: Wrong syntax in file: $filename. Only \"pro\" or \"nonpro\" allowed in column 1! Line: \"$line\"", "get_databasename_from_list"); } if ( correct_patch($product, $pro, $langs, $languagestringref) ) { $patchpath = $path; last; } } else { installer::exiter::exit_program("ERROR: Wrong syntax in file: $filename! Line: \"$line\"", "get_requiredpatchfile_from_list"); } } return $patchpath; } ################################################################## # Converting unicode file to ascii # to be more precise: uft-16 little endian to ascii ################################################################## sub convert_unicode_to_ascii { my ( $filename ) = @_; my @localfile = (); my $savfilename = $filename . "_before.unicode"; installer::systemactions::copy_one_file($filename, $savfilename); open( IN, "<:encoding(UTF16-LE)", $filename ) || installer::exiter::exit_program("ERROR: Cannot open file $filename for reading", "convert_unicode_to_ascii"); while ( $line = ) { push @localfile, $line; } close( IN ); if ( open( OUT, ">", $filename ) ) { print OUT @localfile; close(OUT); } } #################################################################### # Analyzing the log file created by msimsp.exe to find all # files included into the patch. #################################################################### sub analyze_msimsp_logfile { my ($logfile, $filesarray) = @_; # Reading log file after converting from utf-16 (LE) to ascii convert_unicode_to_ascii($logfile); my $logfilecontent = installer::files::read_file($logfile); # Creating hash from $filesarray: unique file name -> destination of file my %filehash = (); my %destinationcollector = (); for ( my $i = 0; $i <= $#{$filesarray}; $i++ ) { my $onefile = ${$filesarray}[$i]; # Only collecting files with "uniquename" and "destination" if (( exists($onefile->{'uniquename'}) ) && ( exists($onefile->{'uniquename'}) )) { my $uniquefilename = $onefile->{'uniquename'}; my $destpath = $onefile->{'destination'}; $filehash{$uniquefilename} = $destpath; } } # Analyzing log file of msimsp.exe, finding all changed files # and searching all destinations of unique file names. # Content in log file: "INFO File Key: is modified" # Collecting content in @installer::globals::patchfilecollector for ( my $i = 0; $i <= $#{$logfilecontent}; $i++ ) { if ( ${$logfilecontent}[$i] =~ /Key\:\s*(.*?) is modified\s*$/ ) { my $filekey = $1; if ( exists($filehash{$filekey}) ) { $destinationcollector{$filehash{$filekey}} = 1; } else { installer::exiter::exit_program("ERROR: Could not find file key \"$filekey\" in file collector.", "analyze_msimsp_logfile"); } } } foreach my $onedest ( sort keys %destinationcollector ) { push(@installer::globals::patchfilecollector, "$onedest\n"); } } #################################################################### # Creating msp patch files for Windows #################################################################### sub create_msp_patch { my ($installationdir, $includepatharrayref, $allvariables, $languagestringref, $languagesarrayref, $filesarray) = @_; my $force = 1; # print this message even in 'quiet' mode installer::logger::print_message( "\n******************************************\n" ); installer::logger::print_message( "... creating msp installation set ...\n", $force ); installer::logger::print_message( "******************************************\n" ); $installer::globals::creating_windows_installer_patch = 1; my @needed_files = ("msimsp.exe"); # only required for patch creation process installer::control::check_needed_files_in_path(\@needed_files); installer::logger::include_header_into_logfile("Creating msp installation sets:"); my $firstdir = $installationdir; installer::pathanalyzer::get_path_from_fullqualifiedname(\$firstdir); my $lastdir = $installationdir; installer::pathanalyzer::make_absolute_filename_to_relative_filename(\$lastdir); if ( $lastdir =~ /\./ ) { $lastdir =~ s/\./_msp_inprogress\./ } else { $lastdir = $lastdir . "_msp_inprogress"; } # Removing existing directory "_native_packed_inprogress" and "_native_packed_witherror" and "_native_packed" my $mspdir = $firstdir . $lastdir; if ( -d $mspdir ) { installer::systemactions::remove_complete_directory($mspdir); } my $olddir = $mspdir; $olddir =~ s/_inprogress/_witherror/; if ( -d $olddir ) { installer::systemactions::remove_complete_directory($olddir); } $olddir = $mspdir; $olddir =~ s/_inprogress//; if ( -d $olddir ) { installer::systemactions::remove_complete_directory($olddir); } # Creating the new directory for new installation set installer::systemactions::create_directory($mspdir); $installer::globals::saveinstalldir = $mspdir; installer::logger::include_timestamp_into_logfile("\nPerformance Info: Starting product installation"); # Installing both installation sets installer::logger::print_message( "... installing products ...\n" ); my ($olddatabase, $newdatabase) = install_installation_sets($installationdir); installer::logger::include_timestamp_into_logfile("\nPerformance Info: Starting pcp file creation"); # Create pcp file installer::logger::print_message( "... creating pcp file ...\n" ); my $localmspdir = installer::systemactions::create_directories("msp", $languagestringref); if ( ! $allvariables->{'PCPFILENAME'} ) { installer::exiter::exit_program("ERROR: Property \"PCPFILENAME\" has to be defined.", "create_msp_patch"); } my $pcpfilename = $allvariables->{'PCPFILENAME'}; if ( $installer::globals::languagepack ) { $pcpfilename =~ s/.pcp\s*$/languagepack.pcp/; } elsif ( $installer::globals::helppack ) { $pcpfilename =~ s/.pcp\s*$/helppack.pcp/; } # Searching the pcp file in the include paths my $fullpcpfilenameref = installer::scriptitems::get_sourcepath_from_filename_and_includepath(\$pcpfilename, $includepatharrayref, 1); if ( $$fullpcpfilenameref eq "" ) { installer::exiter::exit_program("ERROR: pcp file not found: $pcpfilename !", "create_msp_patch"); } my $fullpcpfilenamesource = $$fullpcpfilenameref; # Copying pcp file my $fullpcpfilename = $localmspdir . $installer::globals::separator . $pcpfilename; installer::systemactions::copy_one_file($fullpcpfilenamesource, $fullpcpfilename); # a. Extracting tables from msi database: msidb.exe -d -f -e File Media, ... # b. Changing content of msi database in tables: File, Media, Directory, FeatureComponent # c. Including tables into msi database: msidb.exe -d -f -i File Media, ... # Unpacking tables from pcp file extract_all_tables_from_pcpfile($fullpcpfilename, $localmspdir); # Tables, that need to be edited my $tablelist = "Properties TargetImages UpgradedImages ImageFamilies PatchMetadata PatchSequence"; # required tables # Saving all tables check_and_save_tables($tablelist, $localmspdir); # Setting the name of the new msp file my $mspfilename = set_mspfilename($allvariables, $mspdir, $languagesarrayref); # Editing tables edit_tables($tablelist, $localmspdir, $olddatabase, $newdatabase, $mspfilename, $allvariables, $languagestringref); # Adding edited tables into pcp file include_tables_into_pcpfile($fullpcpfilename, $localmspdir, $tablelist); # Start msimsp.exe installer::logger::include_timestamp_into_logfile("\nPerformance Info: Starting msimsp.exe"); my $msimsplogfile = execute_msimsp($fullpcpfilename, $mspfilename, $localmspdir); # Sign .msp file if ( defined($ENV{'WINDOWS_BUILD_SIGNING'}) && ($ENV{'WINDOWS_BUILD_SIGNING'} eq 'TRUE') ) { my $localmspfilename = $mspfilename; $localmspfilename =~ s/\\/\\\\/g; my $systemcall = "signtool.exe sign "; if ( defined($ENV{'PFXFILE'}) ) { $systemcall .= "-f $ENV{'PFXFILE'} "; } if ( defined($ENV{'PFXPASSWORD'}) ) { $systemcall .= "-p $ENV{'PFXPASSWORD'} "; } if ( defined($ENV{'TIMESTAMPURL'}) ) { $systemcall .= "-t $ENV{'TIMESTAMPURL'} "; } else { $systemcall .= "-t http://timestamp.digicert.com/ "; } $systemcall .= "-d \"" . $allvariables->{'PRODUCTNAME'} . " " . $allvariables->{'PRODUCTVERSION'} . " Patch " . $allvariables->{'WINDOWSPATCHLEVEL'} . "\" "; $systemcall .= $localmspfilename; installer::logger::print_message( "... code signing and timestamping with signtool.exe ...\n" ); my $returnvalue = system($systemcall); # do not print password to log if ( defined($ENV{'PFXPASSWORD'}) ) { $systemcall =~ s/$ENV{'PFXPASSWORD'}/********/; } my $infoline = "Systemcall: $systemcall\n"; push( @installer::globals::logfileinfo, $infoline); if ($returnvalue) { $infoline = "ERROR: Could not execute \"$systemcall\"!\n"; push( @installer::globals::logfileinfo, $infoline); } else { $infoline = "Success: Executed \"$systemcall\" successfully!\n"; push( @installer::globals::logfileinfo, $infoline); } } # Copy final installation set next to msp file installer::logger::include_timestamp_into_logfile("\nPerformance Info: Copying installation set"); installer::logger::print_message( "... copying installation set ...\n" ); my $oldinstallationsetpath = $installer::globals::updatedatabasepath; if ( $^O =~ /cygwin/i ) { $oldinstallationsetpath =~ s/\\/\//g; } installer::pathanalyzer::get_path_from_fullqualifiedname(\$oldinstallationsetpath); installer::systemactions::copy_complete_directory($oldinstallationsetpath, $mspdir); # Copying additional patches into the installation set, if required if (( $allvariables->{'ADDITIONALREQUIREDPATCHES'} ) && ( $allvariables->{'ADDITIONALREQUIREDPATCHES'} ne "" ) && ( ! $installer::globals::languagepack ) && ( ! $installer::globals::helppack )) { my $filename = $allvariables->{'ADDITIONALREQUIREDPATCHES'}; my $fullfilenameref = installer::scriptitems::get_sourcepath_from_filename_and_includepath(\$filename, $includepatharrayref, 1); if ( $$fullfilenameref eq "" ) { installer::exiter::exit_program("ERROR: Could not find file with required patches, although it is defined: $filename !", "create_msp_patch"); } my $fullfilename = $$fullfilenameref; # Reading list file my $listfile = installer::files::read_file($fullfilename); # Get name and path of reference database my $requiredpatchfile = get_requiredpatchfile_from_list($listfile, $languagestringref, $fullfilename); if ( $requiredpatchfile eq "" ) { installer::exiter::exit_program("ERROR: Could not find path to required patch in file $fullfilename for language(s) $$languagestringref!", "create_msp_patch"); } # Copying patch file installer::systemactions::copy_one_file($requiredpatchfile, $mspdir); # my $infoline = "Copy $requiredpatchfile to $mspdir\n"; # push( @installer::globals::logfileinfo, $infoline); } # Find all files included into the patch # Analyzing the msimsp log file $msimsplogfile analyze_msimsp_logfile($msimsplogfile, $filesarray); # Done installer::logger::include_timestamp_into_logfile("\nPerformance Info: msp creation done"); return $mspdir; } 1;