You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openoffice.apache.org by af...@apache.org on 2013/12/04 09:51:31 UTC

svn commit: r1547732 [4/4] - in /openoffice/trunk/main: instsetoo_native/data/ instsetoo_native/util/ solenv/bin/ solenv/bin/modules/installer/ solenv/bin/modules/installer/patch/ solenv/bin/modules/installer/windows/

Added: openoffice/trunk/main/solenv/bin/patch_tool.pl
URL: http://svn.apache.org/viewvc/openoffice/trunk/main/solenv/bin/patch_tool.pl?rev=1547732&view=auto
==============================================================================
--- openoffice/trunk/main/solenv/bin/patch_tool.pl (added)
+++ openoffice/trunk/main/solenv/bin/patch_tool.pl Wed Dec  4 08:51:30 2013
@@ -0,0 +1,1571 @@
+#!/usr/bin/perl -w
+
+#**************************************************************
+#  
+#  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
+#  
+#  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.
+#  
+#**************************************************************
+
+use Getopt::Long;
+use Pod::Usage;
+use File::Path;
+use File::Spec;
+use File::Basename;
+use XML::LibXML;
+use Digest;
+use Archive::Zip;
+use Archive::Extract;
+
+use installer::ziplist;
+use installer::logger;
+use installer::windows::msiglobal;
+use installer::patch::Msi;
+use installer::patch::ReleasesList;
+use installer::patch::Version;
+
+use strict;
+
+
+=head1 NAME
+
+    patch_tool.pl - Create Windows MSI patches.
+
+=head1 SYNOPSIS
+
+    patch_tool.pl command [options]
+
+    Commands:
+        create    create patches
+        apply     apply patches
+
+    Options:
+        -p|--product-name <product-name>
+             The product name, eg Apache_OpenOffice
+        -o|--output-path <path>
+             Path to the instsetoo_native platform output tree
+        -d|--data-path <path>
+             Path to the data directory that is expected to be under version control.
+        --source-version <major>.<minor>.<micro>
+             The version that is to be patched.
+        --target-version <major>.<minor>.<micro>
+             The version after the patch has been applied.
+
+=head1 DESCRIPTION
+
+    Creates windows MSP patch files, one for each relevant language.
+    Patches convert an installed OpenOffice to the target version.
+
+    Required data are:
+        Installation sets of the source versions
+            Taken from ext_sources/
+            Downloaded from archive.apache.org on demand
+
+        Installation set of the target version
+            This is expected to be the current version.
+
+=cut
+
+#    my $ImageFamily = "MNPapps";
+# The ImageFamily name has to have 1-8 alphanumeric characters.
+my $ImageFamily = "AOO"; 
+my $SourceImageName = "Source";
+my $TargetImageName = "Target";
+
+
+
+sub ProcessCommandline ()
+{
+    my $arguments = {
+        'product-name' => undef,
+        'output-path' => undef,
+        'data-path' => undef,
+        'lst-file' => undef,
+        'source-version' => undef,
+        'target-version' => undef};
+
+    if ( ! GetOptions(
+               "product-name=s", \$arguments->{'product-name'},
+               "output-path=s", \$arguments->{'output-path'},
+               "data-path=s" => \$arguments->{'data-path'},
+               "lst-file=s" => \$arguments->{'lst-file'},
+               "source-version:s" => \$arguments->{'source-version'},
+               "target-version:s" => \$arguments->{'target-version'}
+        ))
+    {
+        pod2usage(2);
+    }
+
+    # Only the command should be left in @ARGV.
+    pod2usage(2) unless scalar @ARGV == 1;
+    $arguments->{'command'} = shift @ARGV;
+    
+    # At the moment we only support patches on windows.  When this
+    # is extended in the future we need the package format as an
+    # argument.
+    $arguments->{'package-format'} = "msi";
+
+    return $arguments;
+}
+
+
+
+
+sub GetSourceMsiPath ($$)
+{
+    my ($context, $language) = @_;
+    my $unpacked_path = File::Spec->catfile(
+	$context->{'output-path'},
+	$context->{'product-name'},
+        $context->{'package-format'},
+	installer::patch::Version::ArrayToDirectoryName(
+	    installer::patch::Version::StringToNumberArray(
+		$context->{'source-version'})),
+	$language);
+}
+
+
+
+
+sub GetTargetMsiPath ($$)
+{
+    my ($context, $language) = @_;
+    return File::Spec->catfile(
+        $context->{'output-path'},
+        $context->{'product-name'},
+        $context->{'package-format'},
+        "install",
+        $language);
+}
+
+
+
+sub ProvideInstallationSets ($$)
+{
+    my ($context, $language) = @_;
+
+    # Assume that the target installation set is located in the output tree.
+    my $target_path = GetTargetMsiPath($context, $language);
+    if ( ! -d $target_path)
+    {
+        installer::logger::PrintError("can not find target installation set at '%s'\n", $target_path);
+        return 0;
+    }
+    my @target_version = installer::patch::Version::StringToNumberArray($context->{'target-version'});
+    my $target_msi_file = File::Spec->catfile(
+        $target_path,
+        sprintf("openoffice%d%d%d.msi", $target_version[0], $target_version[1], $target_version[2]));
+    if ( ! -f $target_msi_file)
+    {
+        installer::logger::PrintError("can not find target msi file at '%s'\n", $target_msi_file);
+        return 0;
+    }
+
+    return 1;
+}
+
+
+
+
+sub GetLanguages ()
+{
+    # The set of languages is taken from the WITH_LANG environment variable.
+    # If that is missing or is empty then the default 'en-US' is used instead.
+    my @languages = ("en-US");
+    my $with_lang = $ENV{'WITH_LANG'};
+    if (defined $with_lang && $with_lang ne "")
+    {
+        @languages = split(/\s+/, $with_lang);
+    }
+    return @languages;
+}
+
+
+
+
+sub FindValidLanguages ($$$)
+{
+    my ($context, $release_data, $languages) = @_;
+
+    my @valid_languages = ();
+    foreach my $language (@$languages)
+    {
+        if ( ! ProvideInstallationSets($context, $language))
+        {
+            installer::logger::PrintError("    '%s' has no target installation set\n", $language);
+        }
+        elsif ( ! defined $release_data->{$language})
+        {
+            installer::logger::PrintError("    '%s' is not a released language for version %s\n",
+                $language,
+                $context->{'source-version'});
+        }
+        else
+        {
+            push @valid_languages, $language;
+        }
+    }
+
+    return @valid_languages;
+}
+
+
+
+
+sub ProvideSourceInstallationSet ($$$)
+{
+    my ($context, $language, $release_data) = @_;
+
+    my $url = $release_data->{$language}->{'URL'};
+    $url =~ /^(.*)\/([^\/]*)$/;
+    my ($location, $basename) = ($1,$2);
+
+    my $ext_sources_path = $ENV{'TARFILE_LOCATION'};
+    if ( ! -d $ext_sources_path)
+    {
+        installer::logger::PrintError("Can not determine the path to ext_sources/.\n");
+        installer::logger::PrintError("Maybe SOURCE_ROOT_DIR has not been correctly set in the environment?");
+        return 0;
+    }
+
+    # We need the unpacked installation set in <platform>/<product>/<package>/<source-version>,
+    # eg wntmsci12.pro/Apache_OpenOffice/msi/v-4-0-0.
+    my $unpacked_path = GetSourceMsiPath($context, $language);
+    if ( ! -d $unpacked_path)
+    {
+        # Make sure that the downloadable installation set (.exe) is present in ext_sources/.
+        my $filename = File::Spec->catfile($ext_sources_path, $basename);
+        if ( -f $filename)
+        {
+            PrintInfo("%s is already present in ext_sources/.  Nothing to do\n", $basename);
+        }
+        else
+        {
+            return 0 if ! installer::patch::Download($language, $release_data, $location, $basename, $filename);
+            return 0 if ! -f $filename;
+        }
+
+        # Unpack the installation set.
+        if ( -d $unpacked_path)
+        {
+            # Take the existence of the destination path as proof that the
+            # installation set was successfully unpacked before.
+        }
+        else
+        {
+            installer::patch::InstallationSet::Unpack($filename, $unpacked_path);
+        }
+    }
+}
+
+
+
+
+# Find the source and target version between which the patch will be
+# created.  Typically the target version is the current version and
+# the source version is the version of the previous release.
+sub DetermineVersions ($$)
+{
+    my ($context, $variables) = @_;
+
+    if (defined $context->{'source-version'} && defined $context->{'target-version'})
+    {
+        # Both source and target version have been specified on the
+        # command line.  There remains nothing to be be done.
+        return;
+    }
+
+    if ( ! defined $context->{'target-version'})
+    {
+        # Use the current version as target version.
+        $context->{'target-version'} = $variables->{PRODUCTVERSION};
+    }
+
+    my @target_version = installer::patch::Version::StringToNumberArray($context->{'target-version'});
+    shift @target_version;
+    my $is_target_version_major = 1;
+    foreach my $number (@target_version)
+    {
+        $is_target_version_major = 0 if ($number ne "0");
+    }
+    if ($is_target_version_major)
+    {
+        installer::logger::PrintError("can not create patch where target version is a new major version (%s)\n",
+            $context->{'target-version'});
+        die;
+    }
+    
+    if ( ! defined $context->{'source-version'})
+    {
+        my $releases = installer::patch::ReleasesList::Instance();
+
+        # Search for target release in the list of previous releases.
+        # If it is found, use the previous version as source version.
+        # Otherwise use the last released version.
+        my $last_release = undef;
+        foreach my $release (@{$releases->{'releases'}})
+        {
+            last if ($release eq $context->{'target-version'});
+            $last_release = $release;
+        }
+        $context->{'source-version'} = $last_release;
+    }
+}
+
+
+
+
+=head2 CheckUpgradeCode($source_msi, $target_msi)
+
+    The 'UpgradeCode' values in the 'Property' table differs from source to target
+    
+=cut
+sub CheckUpgradeCode($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    my $source_upgrade_code = $source_msi->GetTable("Property")->GetValue("Property", "UpgradeCode", "Value");
+    my $target_upgrade_code = $target_msi->GetTable("Property")->GetValue("Property", "UpgradeCode", "Value");
+
+    if ($source_upgrade_code eq $target_upgrade_code)
+    {
+        $installer::logger::Info->printf("Error: The UpgradeCode properties have to differ but are both '%s'\n",
+            $source_upgrade_code);
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: UpgradeCode values are identical\n");
+        return 1;
+    }
+}
+
+
+
+
+=head2 CheckProductCode($source_msi, $target_msi)
+
+    The 'ProductCode' values in the 'Property' tables remain the same.
+
+=cut
+sub CheckProductCode($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    my $source_product_code = $source_msi->GetTable("Property")->GetValue("Property", "ProductCode", "Value");
+    my $target_product_code = $target_msi->GetTable("Property")->GetValue("Property", "ProductCode", "Value");
+
+    if ($source_product_code ne $target_product_code)
+    {
+        $installer::logger::Info->printf("Error: The ProductCode properties have to remain the same but are\n");
+        $installer::logger::Info->printf("       '%s' and '%s'\n",
+            $source_product_code,
+            $target_product_code);
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: ProductCode properties differ\n");
+        return 1;
+    }
+}
+
+
+
+
+=head2 CheckBuildIdCode($source_msi, $target_msi)
+
+    The 'PRODUCTBUILDID' values in the 'Property' tables (not the AOO build ids) differ and the
+    target value is higher than the source value.
+
+=cut
+sub CheckBuildIdCode($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    my $source_build_id = $source_msi->GetTable("Property")->GetValue("Property", "PRODUCTBUILDID", "Value");
+    my $target_build_id = $target_msi->GetTable("Property")->GetValue("Property", "PRODUCTBUILDID", "Value");
+    
+    if ($source_build_id >= $target_build_id)
+    {
+        $installer::logger::Info->printf(
+            "Error: The PRODUCTBUILDID properties have to increase but are '%s' and '%s'\n",
+            $source_build_id,
+            $target_build_id);
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: source build id is lower than target build id\n");
+        return 1;
+    }
+}
+
+
+
+
+sub CheckProductName ($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    my $source_product_name = $source_msi->GetTable("Property")->GetValue("Property", "DEFINEDPRODUCT", "Value");
+    my $target_product_name = $target_msi->GetTable("Property")->GetValue("Property", "DEFINEDPRODUCT", "Value");
+
+    if ($source_product_name ne $target_product_name)
+    {
+        $installer::logger::Info->printf("Error: product names of are not identical:\n");
+        $installer::logger::Info->printf("       %s != %s\n", $source_product_name, $target_product_name);
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: product names are identical\n");
+        return 1;
+    }
+}
+
+
+
+
+=head2 CheckRemovedFiles($source_msi, $target_msi)
+
+    Files and components must not be deleted.
+
+=cut
+sub CheckRemovedFiles($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    # Get the 'File' tables.
+    my $source_file_table = $source_msi->GetTable("File");
+    my $target_file_table = $target_msi->GetTable("File");
+
+    # Create data structures for fast lookup.
+    my @source_files = map {$_->GetValue("File")} @{$source_file_table->GetAllRows()};
+    my %target_file_map = map {$_->GetValue("File") => $_} @{$target_file_table->GetAllRows()};
+
+    # Search for removed files (files in source that are missing from target).
+    my $removed_file_count = 0;
+    foreach my $uniquename (@source_files)
+    {
+        if ( ! defined $target_file_map{$uniquename})
+        {
+            ++$removed_file_count;
+        }
+    }
+
+    if ($removed_file_count > 0)
+    {
+        $installer::logger::Info->printf("Error: %d files have been removed\n", $removed_file_count);
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: no files have been removed\n");
+        return 1;
+    }
+}
+
+
+
+
+=head2 CheckNewFiles($source_msi, $target_msi)
+
+    New files have to be in new components.
+
+=cut
+sub CheckNewFiles($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    # Get the 'File' tables.
+    my $source_file_table = $source_msi->GetTable("File");
+    my $target_file_table = $target_msi->GetTable("File");
+
+    # Create data structures for fast lookup.
+    my %source_file_map = map {$_->GetValue("File") => $_} @{$source_file_table->GetAllRows()};
+    my @target_files = map {$_->GetValue("File")} @{$target_file_table->GetAllRows()};
+
+    # Search for added files (files in target that where not in source).
+    my $added_file_count = 0;
+    foreach my $uniquename (@target_files)
+    {
+        if ( ! defined $source_file_map{$uniquename})
+        {
+            ++$added_file_count;
+        }
+    }
+
+    if ($added_file_count > 0)
+    {
+        $installer::logger::Info->printf("Warning: %d files have been added\n", $added_file_count);
+
+        $installer::logger::Info->printf("Check for new files being part of new components is not yet implemented\n");
+        
+        return 1;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: no files have been added\n");
+        return 1;
+    }
+}
+
+
+
+
+=head2 CheckComponentSets($source_msi, $target_msi)
+
+    Components must not be removed but can be added.
+    Features of added components have also to be new.
+
+=cut
+sub CheckComponentSets($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    # Get the 'Component' tables.
+    my $source_component_table = $source_msi->GetTable("Component");
+    my $target_component_table = $target_msi->GetTable("Component");
+
+    # Create data structures for fast lookup.
+    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
+    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
+
+    # Check that no component has been removed.
+    my @removed_components = ();
+    foreach my $componentname (keys %source_component_map)
+    {
+        if ( ! defined $target_component_map{$componentname})
+        {
+            push @removed_components, $componentname;
+        }
+    }
+    if (scalar @removed_components > 0)
+    {
+        # There are removed components.
+
+        # Check if any of them is not a registry component.
+        my $is_file_component_removed = 0;
+        foreach my $componentname (@removed_components)
+        {
+            if ($componentname !~ /^registry/)
+            {
+                $is_file_component_removed = 1;
+            }
+        }
+        if ($is_file_component_removed)
+        {
+            $installer::logger::Info->printf(
+                "Error: %d components have been removed, some of them are file components:\n",
+                scalar @removed_components);
+            $installer::logger::Info->printf("       %s\n", join(", ", @removed_components));
+            return 0;
+        }
+        else
+        {
+            $installer::logger::Info->printf(
+                "Error: %d components have been removed, all of them are registry components:\n",
+                scalar @removed_components);
+            return 0;
+        }
+    }
+
+    # Check that added components belong to new features.
+    my @added_components = ();
+    foreach my $componentname (keys %target_component_map)
+    {
+        if ( ! defined $source_component_map{$componentname})
+        {
+            push @added_components, $componentname;
+        }
+    }
+
+    if (scalar @added_components > 0)
+    {
+        # Check if any of them is not a registry component.
+        my $is_file_component_removed = 0;
+        foreach my $componentname (@removed_components)
+        {
+            if ($componentname !~ /^registry/)
+            {
+                $is_file_component_removed = 1;
+            }
+        }
+
+        if ($is_file_component_removed)
+        {
+            $installer::logger::Info->printf(
+                "Warning: %d components have been addded\n",
+                scalar @added_components);
+            $installer::logger::Info->printf(
+                "Test for new components belonging to new features has not yet been implemented\n");
+            return 0;
+        }
+        else
+        {
+            $installer::logger::Info->printf(
+                "Warning: %d components have been addded, all of them registry components\n",
+                scalar @added_components);
+        }
+    }
+
+    $installer::logger::Info->printf("OK: component sets in source and target are compatible\n");
+    return 1;
+}
+
+
+
+
+=head2 CheckComponent($source_msi, $target_msi)
+
+    In the 'Component' table the 'ComponentId' and 'Component' values
+    for corresponding componts in the source and target release have
+    to be identical.
+
+=cut
+sub CheckComponentValues($$$)
+{
+    my ($source_msi, $target_msi, $variables) = @_;
+
+    # Get the 'Component' tables.
+    my $source_component_table = $source_msi->GetTable("Component");
+    my $target_component_table = $target_msi->GetTable("Component");
+
+    # Create data structures for fast lookup.
+    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
+    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
+
+    my @differences = ();
+    my $comparison_count = 0;
+    while (my ($componentname, $source_component_row) = each %source_component_map)
+    {
+        my $target_component_row = $target_component_map{$componentname};
+        if (defined $target_component_row)
+        {
+            ++$comparison_count;
+            if ($source_component_row->GetValue("ComponentId") ne $target_component_row->GetValue("ComponentId"))
+            {
+                push @differences, [
+                    $componentname,
+                    $source_component_row->GetValue("ComponentId"),
+                    $target_component_row->GetValue("ComponentId"),
+                    $target_component_row->GetValue("Component"),
+                ];
+            }
+        }
+    }
+
+    if (scalar @differences > 0)
+    {
+        $installer::logger::Info->printf(
+            "Error: there are %d components with different 'ComponentId' values after %d comparisons.\n",
+            scalar @differences,
+            $comparison_count);
+        foreach my $item (@differences)
+        {
+            $installer::logger::Info->printf("%s  %s\n", $item->[1], $item->[2]);
+        }
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: components in source and target are identical\n");
+        return 1;
+    }
+}
+
+
+
+
+=head2 CheckFileSequence($source_msi, $target_msi)
+
+    In the 'File' table the 'Sequence' numbers for corresponding files has to be identical.
+    
+=cut
+sub CheckFileSequence($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    # Get the 'File' tables.
+    my $source_file_table = $source_msi->GetTable("File");
+    my $target_file_table = $target_msi->GetTable("File");
+
+    # Create temporary data structures for fast access.
+    my %source_file_map = map {$_->GetValue("File") => $_} @{$source_file_table->GetAllRows()};
+    my %target_file_map = map {$_->GetValue("File") => $_} @{$target_file_table->GetAllRows()};
+
+    # Search files with mismatching sequence numbers.
+    my @mismatching_files = ();
+    while (my ($uniquename,$source_file_row) = each %source_file_map)
+    {
+        my $target_file_row = $target_file_map{$uniquename};
+        if (defined $target_file_row)
+        {
+            if ($source_file_row->GetValue('Sequence') ne $target_file_row->GetValue('Sequence'))
+            {
+                push @mismatching_files, [
+                    $uniquename,
+                    $source_file_row,
+                    $target_file_row
+                ];
+            }
+        }
+    }
+
+    if (scalar @mismatching_files > 0)
+    {
+        $installer::logger::Info->printf("Error: there are %d files with mismatching 'Sequence' numbers\n",
+            scalar @mismatching_files);
+        foreach my $item (@mismatching_files)
+        {
+            $installer::logger::Info->printf("    %s: %d != %d\n",
+                $item->[0],
+                $item->[1]->GetValue("Sequence"),
+                $item->[2]->GetValue("Sequence"));
+        }
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: all files have matching 'Sequence' numbers\n");
+        return 1;
+    }
+}
+
+
+
+
+=head2 CheckFileSequenceUnique($source_msi, $target_msi)
+
+    In the 'File' table the 'Sequence' values have to be unique.
+    
+=cut
+sub CheckFileSequenceUnique($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    # Get the 'File' tables.
+    my $target_file_table = $target_msi->GetTable("File");
+
+    my %sequence_numbers = ();
+    my $collision_count = 0;
+    foreach my $row (@{$target_file_table->GetAllRows()})
+    {
+        my $sequence_number = $row->GetValue("Sequence");
+        if (defined $sequence_numbers{$sequence_number})
+        {
+            ++$collision_count;
+        }
+        else
+        {
+            $sequence_numbers{$sequence_number} = 1;
+        }
+    }
+
+    if ($collision_count > 0)
+    {
+        $installer::logger::Info->printf("Error: there are %d collisions ofn the sequence numbers\n",
+            $collision_count);
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: sequence numbers are unique\n");
+        return 1;
+    }
+}
+
+
+
+
+=head2 CheckFileSequenceHoles ($target_msi)
+
+    Check the sequence numbers of the target msi if the n files use numbers 1..n or if there are holes.
+    Holes are reported as warnings.
+    
+=cut
+sub CheckFileSequenceHoles ($$)
+{
+    my ($source_msi, $target_msi) = @_;
+    
+    my $target_file_table = $target_msi->GetTable("File");
+    my %sequence_numbers = map {$_->GetValue("Sequence") => $_} @{$target_file_table->GetAllRows()};
+    my @sorted_sequence_numbers = sort {$a <=> $b} keys %sequence_numbers;
+    my $expected_next_sequence_number = 1;
+    my @holes = ();
+    foreach my $sequence_number (@sorted_sequence_numbers)
+    {
+        if ($sequence_number != $expected_next_sequence_number)
+        {
+            push @holes, [$expected_next_sequence_number, $sequence_number-1];
+        }
+        $expected_next_sequence_number = $sequence_number+1;
+    }
+    if (scalar @holes > 0)
+    {
+        $installer::logger::Info->printf("Warning: sequence numbers have %d holes\n");
+        foreach my $hole (@holes)
+        {
+            if ($hole->[0] != $hole->[1])
+            {
+                $installer::logger::Info->printf("    %d\n", $hole->[0]);
+            }
+            else
+            {
+                $installer::logger::Info->printf("    %d -> %d\n", $hole->[0], $hole->[1]);
+            }
+        }
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: there are no holes in the sequence numbers\n");
+    }
+    return 1;
+}
+
+
+
+
+=head2 CheckRegistryItems($source_msi, $target_msi)
+
+    In the 'Registry' table the 'Component_' and 'Key' values must not
+    depend on the version number (beyond the unchanging major
+    version).
+
+    'Value' values must only depend on the major version number to
+    avoid duplicate entries in the start menu.
+
+    Violations are reported as warnings for now.
+
+=cut
+sub CheckRegistryItems($$$)
+{
+    my ($source_msi, $target_msi, $product_name) = @_;
+
+    # Get the registry tables.
+    my $source_registry_table = $source_msi->GetTable("Registry");
+    my $target_registry_table = $target_msi->GetTable("Registry");
+
+    my $registry_index = $target_registry_table->GetColumnIndex("Registry");
+    my $component_index = $target_registry_table->GetColumnIndex("Component_");
+    
+    # Create temporary data structures for fast access.
+    my %source_registry_map = map {$_->GetValue($registry_index) => $_} @{$source_registry_table->GetAllRows()};
+    my %target_registry_map = map {$_->GetValue($registry_index) => $_} @{$target_registry_table->GetAllRows()};
+
+    # Prepare version numbers to search.
+    my $source_version_number = $source_msi->{'version'};
+    my $source_version_nodots = installer::patch::Version::ArrayToNoDotName(
+        installer::patch::Version::StringToNumberArray($source_version_number));
+    my $source_component_pattern = lc($product_name).$source_version_nodots;
+    my $target_version_number = $target_msi->{'version'};
+    my $target_version_nodots = installer::patch::Version::ArrayToNoDotName(
+        installer::patch::Version::StringToNumberArray($target_version_number));
+    my $target_component_pattern = lc($product_name).$target_version_nodots;
+
+    foreach my $source_row (values %source_registry_map)
+    {
+        my $target_row = $target_registry_map{$source_row->GetValue($registry_index)};
+        if ( ! defined $target_row)
+        {
+            $installer::logger::Info->printf("Error: sets of registry entries differs\n");
+            return 1;
+        }
+
+        my $source_component_name = $source_row->GetValue($component_index);
+        my $target_component_name = $source_row->GetValue($component_index);
+
+    }
+    
+    $installer::logger::Info->printf("OK: registry items are OK\n");
+    return 1;
+}
+
+
+
+
+=head2
+
+    Component->KeyPath must not change. (see component.pm/get_component_keypath)
+    
+=cut
+sub CheckComponentKeyPath ($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    # Get the registry tables.
+    my $source_component_table = $source_msi->GetTable("Component");
+    my $target_component_table = $target_msi->GetTable("Component");
+
+    # Create temporary data structures for fast access.
+    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
+    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
+
+    my @mismatches = ();
+    while (my ($componentname, $source_component_row) = each %source_component_map)
+    {
+        my $target_component_row = $target_component_map{$componentname};
+        if (defined $target_component_row)
+        {
+            my $source_keypath = $source_component_row->GetValue("KeyPath");
+            my $target_keypath = $target_component_row->GetValue("KeyPath");
+            if ($source_keypath ne $target_keypath)
+            {
+                push @mismatches, [$componentname, $source_keypath, $target_keypath];
+            }
+        }
+    }
+
+    if (scalar @mismatches > 0)
+    {
+        $installer::logger::Info->printf(
+            "Error: there are %d mismatches in the 'KeyPath' column of the 'Component' table\n",
+            scalar @mismatches);
+
+        foreach my $item (@mismatches)
+        {
+            $installer::logger::Info->printf(
+                "    %s: %s != %s\n",
+                $item->[0],
+                $item->[1],
+                $item->[2]);
+        }
+        
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf(
+            "OK: no mismatches in the 'KeyPath' column of the 'Component' table\n");
+        return 1;
+    }
+}
+
+
+
+
+sub Check ($$$$)
+{
+    my ($source_msi, $target_msi, $variables, $product_name) = @_;
+
+    $installer::logger::Info->printf("checking if source and target releases are compatable\n");
+    $installer::logger::Info->increase_indentation();
+
+    my $result = 1;
+
+    $result &&= CheckUpgradeCode($source_msi, $target_msi);
+    $result &&= CheckProductCode($source_msi, $target_msi);
+    $result &&= CheckBuildIdCode($source_msi, $target_msi);
+    $result &&= CheckProductName($source_msi, $target_msi);
+    $result &&= CheckRemovedFiles($source_msi, $target_msi);
+    $result &&= CheckNewFiles($source_msi, $target_msi);
+    $result &&= CheckComponentSets($source_msi, $target_msi);
+    $result &&= CheckComponentValues($source_msi, $target_msi, $variables);
+    $result &&= CheckFileSequence($source_msi, $target_msi);
+    $result &&= CheckFileSequenceUnique($source_msi, $target_msi);
+    $result &&= CheckFileSequenceHoles($source_msi, $target_msi);
+    $result &&= CheckRegistryItems($source_msi, $target_msi, $product_name);
+    $result &&= CheckComponentKeyPath($source_msi, $target_msi);
+
+    $installer::logger::Info->decrease_indentation();
+
+    return $result;
+}
+
+
+
+
+=head2 FindPcpTemplate ()
+
+    The template.pcp file is part of the Windows SDK.
+
+=cut
+sub FindPcpTemplate ()
+{
+    my $psdk_home = $ENV{'PSDK_HOME'};
+    if ( ! defined $psdk_home)
+    {
+        $installer::logger::Info->printf("Error: the PSDK_HOME environment variable is not set.\n");
+        $installer::logger::Info->printf("       did you load the AOO build environment?\n");
+        $installer::logger::Info->printf("       you may want to use the --with-psdk-home configure option\n");
+        return undef;
+    }
+    if ( ! -d $psdk_home)
+    {
+        $installer::logger::Info->printf(
+            "Error: the PSDK_HOME environment variable does not point to a valid directory: %s\n",
+            $psdk_home);
+        return undef;
+    }
+
+    my $schema_path = File::Spec->catfile($psdk_home, "Bin", "msitools", "Schemas", "MSI");
+    if (  ! -d $schema_path)
+    {
+        $installer::logger::Info->printf("Error: Can not locate the msi template folder in the Windows SDK\n");
+        $installer::logger::Info->printf("       %s\n", $schema_path);
+        $installer::logger::Info->printf("       Is the Windows SDK properly installed?\n");
+        return undef;
+    }
+
+    my $schema_filename = File::Spec->catfile($schema_path, "template.pcp");
+    if (  ! -f $schema_filename)
+    {
+        $installer::logger::Info->printf("Error: Can not locate the pcp template at\n");
+        $installer::logger::Info->printf("       %s\n", $schema_filename);
+        $installer::logger::Info->printf("       Is the Windows SDK properly installed?\n");
+        return undef;
+    }
+
+    return $schema_filename;
+}
+
+
+
+
+sub SetupPcpPatchMetadataTable ($$$)
+{
+    my ($pcp, $source_msi, $target_msi) = @_;
+
+    # Determine values for eg product name and source and new version.
+    my $source_version = $source_msi->{'version'};
+    my $target_version = $target_msi->{'version'};
+
+    my $property_table = $target_msi->GetTable("Property");
+    my $display_product_name = $property_table->GetValue("Property", "DEFINEDPRODUCT", "Value");
+
+    # Set table.
+    my $table = $pcp->GetTable("PatchMetadata");
+    $table->SetRow(
+        "Company", "",
+        "*Property", "Description",
+        "Value", sprintf("Update of %s from %s to %s", $display_product_name, $source_version, $target_version)
+        );
+    $table->SetRow(
+        "Company", "",
+        "*Property", "DisplayName",
+        "Value", sprintf("Update of %s from %s to %s", $display_product_name, $source_version, $target_version)
+        );
+    $table->SetRow(
+        "Company", "",
+        "*Property", "ManufacturerName",
+        "Value", $property_table->GetValue("Property", "Manufacturer", "Value"),
+        );
+    $table->SetRow(
+        "Company", "",
+        "*Property", "MoreInfoURL",
+        "Value", $property_table->GetValue("Property", "ARPURLINFOABOUT", "Value")
+        );
+    $table->SetRow(
+        "Company", "",
+        "*Property", "TargetProductName",
+        "Value", $property_table->GetValue("Property", "ProductName", "Value")
+        );
+    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time);
+
+    $table->SetRow(
+        "Company", "",
+        "*Property", "CreationTimeUTC",
+        "Value", sprintf("%d/%d/%d %d:%02d", $mon+1,$mday,$year+1900,$hour,$min)
+        );
+}
+
+
+
+
+sub SetupPropertiesTable ($$)
+{
+    my ($pcp, $msp_filename) = @_;
+
+    my $table = $pcp->GetTable("Properties");
+    
+    $table->SetRow(
+        "*Name", "PatchOutputPath",
+        "Value", installer::patch::Tools::ToWindowsPath($msp_filename)
+        );
+    # Request at least Windows installer 2.0.
+    # Version 2.0 allows us to omit some values from ImageFamilies table.
+    $table->SetRow(
+        "*Name", "MinimumRequiredMsiVersion",
+        "Value", 200
+        );
+    # Allow diffs for binary files.
+    $table->SetRow(
+        "*Name", "IncludeWholeFilesOnly",
+        "Value", 0
+        );
+
+    my $uuid = installer::windows::msiglobal::create_guid();
+    my $uuid_string = "{" . $uuid . "}";
+    $table->SetRow(
+        "*Name", "PatchGUID",
+        "Value", $uuid_string
+        );
+    $installer::logger::Info->printf("created new PatchGUID %s\n", $uuid_string);
+
+    # Prevent sequence table from being generated.
+    $table->SetRow(
+        "*Name", "SEQUENCE_DATA_GENERATION_DISABLED",
+        "Value", 1);
+}
+
+
+
+
+sub SetupImageFamiliesTable ($)
+{
+    my ($pcp) = @_;
+
+    $pcp->GetTable("ImageFamilies")->SetRow(
+        "Family", $ImageFamily,
+        "MediaSrcPropName", "",#"MNPSrcPropName",
+        "MediaDiskId", "",
+        "FileSequenceStart", "",
+        "DiskPrompt", "",
+        "VolumeLabel", "");
+}
+
+
+
+
+sub SetupUpgradedImagesTable ($$)
+{
+    my ($pcp, $target_msi_path) = @_;
+
+    my $msi_path = installer::patch::Tools::ToWindowsPath($target_msi_path);
+    $pcp->GetTable("UpgradedImages")->SetRow(
+        "Upgraded", $TargetImageName,
+        "MsiPath", $msi_path,
+        "PatchMsiPath", "",
+        "SymbolPaths", "",
+        "Family", $ImageFamily);
+}
+
+
+
+
+sub SetupTargetImagesTable ($$)
+{
+    my ($pcp, $source_msi_path) = @_;
+
+    $pcp->GetTable("TargetImages")->SetRow(
+        "Target", $SourceImageName,
+        "MsiPath", installer::patch::Tools::ToWindowsPath($source_msi_path),
+        "SymbolPaths", "",
+        "Upgraded", $TargetImageName,
+        "Order", 1,
+        "ProductValidateFlags", "",
+        "IgnoreMissingSrcFiles", 0); 
+}
+
+
+
+
+sub SetAdditionalValues ($%)
+{
+    my ($pcp, %data) = @_;
+
+    while (my ($key,$value) = each(%data))
+    {
+        $key =~ /^([^\/]+)\/([^:]+):(.+)$/
+            || die("invalid key format");
+        my ($table_name, $key_column,$key_value) = ($1,$2,$3);
+        $value =~ /^([^:]+):(.*)$/
+            || die("invalid value format");
+        my ($value_column,$value_value) = ($1,$2);
+        
+        my $table = $pcp->GetTable($table_name);
+        $table->SetRow(
+                "*".$key_column, $key_value,
+                $value_column, $value_value);
+    }
+}
+
+
+
+
+sub CreatePcp ($$$$$$%)
+{
+    my ($source_msi,
+        $target_msi,
+        $language,
+        $context,
+        $msp_path,
+        $pcp_schema_filename,
+        %additional_values) = @_;
+
+    # Create filenames.
+    my $pcp_filename = File::Spec->catfile($msp_path, "openoffice.pcp");
+    my $msp_filename = File::Spec->catfile($msp_path, "openoffice.msp");
+    
+    # Setup msp path and filename.
+    unlink($pcp_filename) if -f $pcp_filename;        
+    if ( ! File::Copy::copy($pcp_schema_filename, $pcp_filename))
+    {
+        $installer::logger::Info->printf("Error: could not create openoffice.pcp as copy of pcp schema\n");
+        $installer::logger::Info->printf("       %s\n", $pcp_schema_filename);
+        $installer::logger::Info->printf("       %s\n", $pcp_filename);
+        return undef;
+    }
+    my $pcp = installer::patch::Msi->new(
+        $pcp_filename,
+        undef,
+        undef,
+        $language,
+        $context->{'product-name'});
+
+    # Store some values in the pcp for easy reference in the msp creation.
+    $pcp->{'msp_filename'} = $msp_filename;
+
+    SetupPcpPatchMetadataTable($pcp, $source_msi, $target_msi);
+    SetupPropertiesTable($pcp, $msp_filename);
+    SetupImageFamiliesTable($pcp);
+    SetupUpgradedImagesTable($pcp, $target_msi->{'filename'});
+    SetupTargetImagesTable($pcp, $source_msi->{'filename'});
+
+    SetAdditionalValues(%additional_values);
+
+    $pcp->Commit();
+
+    # Remove the PatchSequence table to avoid MsiMsp error message:
+    # "Since MSI 3.0 will block installation of major upgrade patches with
+    #  sequencing information, creation of such patches is blocked." 
+    #$pcp->RemoveTable("PatchSequence");
+    # TODO: alternatively add property SEQUENCE_DATA_GENERATION_DISABLED to pcp Properties table.
+
+
+    $installer::logger::Info->printf("created pcp file at\n");
+    $installer::logger::Info->printf("    %s\n", $pcp->{'filename'});
+    
+    return $pcp;
+}
+
+
+
+
+sub ShowLog ($$$$)
+{
+    my ($log_path, $log_filename, $log_basename, $new_title) = @_;
+
+    if ( -f $log_filename)
+    {
+        my $destination_path = File::Spec->catfile($log_path, $log_basename);
+        File::Path::make_path($destination_path) if ! -d $destination_path;
+        my $command = join(" ",
+            "wilogutl.exe",
+            "/q",
+            "/l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
+            "/o", "'".installer::patch::Tools::ToWindowsPath($destination_path)."'");
+        printf("running command $command\n");
+        my $response = qx($command);
+        printf("response is '%s'\n", $response);
+        my @candidates = glob($destination_path . "/Details*");
+        foreach my $candidate (@candidates)
+        {
+            next unless -f $candidate;
+            my $new_name = $candidate;
+            $new_name =~ s/Details.*$/$log_basename.html/;
+
+            # Rename the top-level html file and replace the title.
+            open my $in, "<", $candidate;
+            open my $out, ">", $new_name;
+            while (<$in>)
+            {
+                if (/^(.*\<title\>)([^<]+)(.*)$/)
+                {
+                    print $out $1.$new_title.$3;
+                }
+                else
+                {
+                    print $out $_;
+                }
+            }
+            close $in;
+            close $out;
+            
+            my $URL = $new_name;
+            $URL =~ s/\/c\//c|\//;
+            $URL =~ s/^(.):/$1|/;
+            $URL = "file:///". $URL;
+            $installer::logger::Info->printf("open %s in your browser to see the log messages\n", $URL);
+        }
+    }
+    else
+    {
+        $installer::logger::Info->printf("Error: log file not found at %s\n", $log_filename);
+    }
+}
+
+
+
+
+sub CreateMsp ($)
+{
+    my ($pcp) = @_;
+    
+    # Prepare log files.
+    my $log_path = File::Spec->catfile($pcp->{'path'}, "log");
+    my $log_basename = "msp";
+    my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
+    my $performance_log_basename = "performance";
+    my $performance_log_filename = File::Spec->catfile($log_path, $performance_log_basename.".log");
+    File::Path::make_path($log_path) if ! -d $log_path;
+    unlink($log_filename) if -f $log_filename;
+    unlink($performance_log_filename) if -f $performance_log_filename;
+
+    # Create the .msp patch file.
+    my $temporary_msimsp_path = File::Spec->catfile($pcp->{'path'}, "tmp");
+    if ( ! -d $temporary_msimsp_path)
+    {
+        File::Path::make_path($temporary_msimsp_path)
+            || die ("can not create temporary path ".$temporary_msimsp_path);
+    }
+    $installer::logger::Info->printf("running msimsp.exe, that will take a while\n");
+    my $command = join(" ",
+        "msimsp.exe",
+        "-s", "'".installer::patch::Tools::ToWindowsPath($pcp->{'filename'})."'",
+        "-p", "'".installer::patch::Tools::ToWindowsPath($pcp->{'msp_filename'})."'",
+        "-l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
+        "-f", "'".installer::patch::Tools::ToWindowsPath($temporary_msimsp_path)."'");
+#	    "-lp", MsiTools::ToEscapedWindowsPath($performance_log_filename),
+    $installer::logger::Info->printf("running command %s\n", $command);
+    my $response = qx($command);
+    $installer::logger::Info->printf("response of msimsp is %s\n", $response);
+    if ( ! -d $temporary_msimsp_path)
+    {
+        die("msimsp failed and deleted temporary path ".$temporary_msimsp_path);
+    }
+
+    # Show the log file that was created by the msimsp.exe command.
+    ShowLog($log_path, $log_filename, $log_basename, "msp creation");
+    ShowLog($log_path, $performance_log_filename, $performance_log_basename, "msp creation perf");
+}
+
+
+
+sub CreatePatch ($$)
+{
+    my ($context, $variables) = @_;
+    
+    $installer::logger::Info->printf("patch will update product %s from %s to %s\n",
+        $context->{'product-name'},
+        $context->{'source-version'},
+        $context->{'target-version'});
+
+    # Locate the Pcp schema file early on to report any errors before the lengthy operations that follow.
+    my $pcp_schema_filename = FindPcpTemplate();
+    if ( ! defined $pcp_schema_filename)
+    {
+        exit(1);
+    }
+        
+    my $release_data = installer::patch::ReleasesList::Instance()
+        ->{$context->{'source-version'}}
+        ->{$context->{'package-format'}};
+
+    # Create a patch for each language.
+    my @requested_languages = GetLanguages();
+    my @valid_languages = FindValidLanguages($context, $release_data, \@requested_languages);
+    $installer::logger::Info->printf("of the requested languages '%s' are valid: '%s'\n",
+        join("', '", @requested_languages),
+        join("', '", @valid_languages));
+    foreach my $language (@valid_languages)
+    {
+        $installer::logger::Info->printf("processing language '%s'\n", $language);
+        $installer::logger::Info->increase_indentation();
+
+        # Provide .msi and .cab files and unpacke .cab for the source release.
+        $installer::logger::Info->printf("locating source package (%s)\n", $context->{'source-version'});
+        $installer::logger::Info->increase_indentation();
+        if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
+            $context->{'source-version'},
+            0,
+            $language,
+            "msi",
+            $context->{'product-name'}))
+        {
+            die "could not provide unpacked .cab file";
+        }
+        my $source_msi = installer::patch::Msi->FindAndCreate(
+            $context->{'source-version'},
+            0,
+            $language,
+            $context->{'product-name'});
+        die unless $source_msi->IsValid();
+
+        $installer::logger::Info->decrease_indentation();
+
+        # Provide .msi and .cab files and unpacke .cab for the target release.
+        $installer::logger::Info->printf("locating target package (%s)\n", $context->{'target-version'});
+        $installer::logger::Info->increase_indentation();
+        if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
+            $context->{'target-version'},
+            1,
+            $language,
+            "msi",
+            $context->{'product-name'}))
+        {
+            die;
+        }
+        my $target_msi = installer::patch::Msi->FindAndCreate(
+            $context->{'target-version'},
+            0,
+            $language,
+            $context->{'product-name'});
+        die unless defined $target_msi;
+        die unless $target_msi->IsValid();
+        $installer::logger::Info->decrease_indentation();
+
+        # Trigger reading of tables.
+        foreach my $table_name (("File", "Component", "Registry"))
+        {
+            $source_msi->GetTable($table_name);
+            $target_msi->GetTable($table_name);
+            $installer::logger::Info->printf("read %s table (source and target\n", $table_name);
+        }
+        
+        # Check if the source and target msis fullfil all necessary requirements.
+        if ( ! Check($source_msi, $target_msi, $variables, $context->{'product-name'}))
+        {
+            $installer::logger::Info->printf("Error: Source and target releases are not compatible.\n");
+            $installer::logger::Info->printf("       => Can not create patch.\n");
+            $installer::logger::Info->printf("       Did you create the target installation set with 'release=t' ?\n");
+            exit(1);
+        }
+        else
+        {
+            $installer::logger::Info->printf("OK: Source and target releases are compatible.\n");
+        }
+
+        # Provide the base path for creating .pcp and .mcp file.
+        my $msp_path = File::Spec->catfile(
+            $context->{'output-path'},
+            $context->{'product-name'},
+            "msp",
+            sprintf("%s_%s",
+              installer::patch::Version::ArrayToDirectoryName(
+                installer::patch::Version::StringToNumberArray(
+                    $source_msi->{'version'})),
+              installer::patch::Version::ArrayToDirectoryName(
+                installer::patch::Version::StringToNumberArray(
+                    $target_msi->{'version'}))),
+            $language
+            );
+        File::Path::make_path($msp_path) unless -d $msp_path;
+
+        # Create the .pcp file that drives the msimsp.exe command.
+        my $pcp = CreatePcp(
+            $source_msi,
+            $target_msi,
+            $language,
+            $context,
+            $msp_path,
+            $pcp_schema_filename,
+            "Properties/Name:DontRemoveTempFolderWhenFinished" => "Value:1",
+            "Properties/Name:SEQUENCE_DATA_GENERATION_DISABLED" => "Value:1",
+            "Properties/Name:TrustMsi" => "Value:0",
+            "Properties/Name:ProductName" => "Value:OOO341");
+
+        # Finally create the msp.
+        CreateMsp($pcp);
+
+        $installer::logger::Info->decrease_indentation();
+    }
+}
+
+
+
+
+sub ApplyPatch ($$)
+{
+    my ($context, $variables) = @_;
+    
+    $installer::logger::Info->printf("will apply patches that update product %s from %s to %s\n",
+        $context->{'product-name'},
+        $context->{'source-version'},
+        $context->{'target-version'});
+    my @languages = GetLanguages();
+
+    my $source_version_dirname = installer::patch::Version::ArrayToDirectoryName(
+      installer::patch::Version::StringToNumberArray(
+          $context->{'source-version'}));
+    my $target_version_dirname = installer::patch::Version::ArrayToDirectoryName(
+      installer::patch::Version::StringToNumberArray(
+          $context->{'target-version'}));
+
+    foreach my $language (@languages)
+    {
+        my $msp_filename = File::Spec->catfile(
+            $context->{'output-path'},
+            $context->{'product-name'},
+            "msp",
+            $source_version_dirname . "_" . $target_version_dirname,
+            $language,
+            "openoffice.msp");
+        if ( ! -f $msp_filename)
+        {
+            $installer::logger::Info->printf("%s does not point to a valid file\n", $msp_filename);
+            next;
+        }
+        
+        my $log_path = File::Spec->catfile(dirname($msp_filename), "log");
+        my $log_basename = "apply-msp";
+        my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
+        
+        my $command = join(" ",
+            "msiexec.exe",
+            "/update", "'".installer::patch::Tools::ToWindowsPath($msp_filename)."'",
+            "/L*xv!", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
+            "REINSTALL=ALL",
+#            "REINSTALLMODE=vomus",
+            "REINSTALLMODE=omus",
+            "MSIENFORCEUPGRADECOMPONENTRULES=1");
+    
+        printf("executing command %s\n", $command);
+        my $response = qx($command);
+        Encode::from_to($response, "UTF16LE", "UTF8");
+        printf("response was '%s'\n", $response);
+    
+        ShowLog($log_path, $log_filename, $log_basename, "msp application");
+    }
+}
+
+
+
+
+sub main ()
+{
+    installer::logger::SetupSimpleLogging(undef);
+    my $context = ProcessCommandline();
+    my ($variables, undef, undef) = installer::ziplist::read_openoffice_lst_file(
+        $context->{'lst-file'},
+        $context->{'product-name'},
+        undef);
+    DetermineVersions($context, $variables);
+
+    if ($context->{'command'} eq "create")
+    {
+        CreatePatch($context, $variables);
+    }
+    elsif ($context->{'command'} eq "apply")
+    {
+        ApplyPatch($context, $variables);
+    }
+}
+
+
+main();

Modified: openoffice/trunk/main/solenv/bin/release_prepare.pl
URL: http://svn.apache.org/viewvc/openoffice/trunk/main/solenv/bin/release_prepare.pl?rev=1547732&r1=1547731&r2=1547732&view=diff
==============================================================================
--- openoffice/trunk/main/solenv/bin/release_prepare.pl (original)
+++ openoffice/trunk/main/solenv/bin/release_prepare.pl Wed Dec  4 08:51:30 2013
@@ -32,6 +32,8 @@ use Getopt::Long;
 use Pod::Usage;
 use Digest;
 
+use Carp::Always;
+
 use strict;
 
 =head1 NAME
@@ -106,9 +108,9 @@ sub ProcessCommandline ()
 
 
 
-sub ProcessLanguage ($$$$)
+sub ProcessLanguage ($$$$$)
 {
-    my ($source_version, $language, $package_format, $product_name) = @_;
+    my ($version, $is_current_version, $language, $package_format, $product_name) = @_;
     
     $installer::logger::Info->printf("%s\n", $language);
     $installer::logger::Info->increase_indentation();
@@ -118,77 +120,12 @@ sub ProcessLanguage ($$$$)
     # 2. unpack it to get access to .cab and .msi
     # 3. unpack .cab so that msimsp.exe can be run
 
-    # Create paths to unpacked contents of .exe and .cab and determine if they exist.
-    # The existence of these paths is taken as flag whether the unpacking has already taken place.
-    my $unpacked_exe_path = installer::patch::InstallationSet::GetUnpackedMsiPath(
-        $source_version,
+    installer::patch::InstallationSet::ProvideUnpackedCab(
+        $version,
+        $is_current_version,
         $language,
         $package_format,
         $product_name);
-    my $unpacked_cab_path = installer::patch::InstallationSet::GetUnpackedCabPath(
-        $source_version,
-        $language,
-        $package_format,
-        $product_name);
-    my $exe_is_unpacked = -d $unpacked_exe_path;
-    my $cab_is_unpacked = -d $unpacked_cab_path;
-
-    if ( ! $exe_is_unpacked)
-    {
-        # Interpret existence of path as proof that the installation
-        # set and the cab file have been successfully unpacked.
-        # Nothing to do.
-        my $filename = installer::patch::InstallationSet::ProvideDownloadSet(
-            $source_version,
-            $language,
-            $package_format);
-        if (defined $filename)
-        {
-            if ( ! -d $unpacked_exe_path)
-            {
-                installer::patch::InstallationSet::UnpackExe($filename, $unpacked_exe_path);
-            }
-        }
-        else
-        {
-            installer::logger::PrintError("could not provide .exe installation set at '%s'\n", $filename);
-        }
-    }
-    else
-    {
-        $installer::logger::Info->printf("downloadable installation set has already been unpacked to '%s'\n",
-            $unpacked_exe_path);
-    }
-
-    if ( ! $cab_is_unpacked)
-    {
-        my $cab_filename = File::Spec->catfile($unpacked_exe_path, "openoffice1.cab");
-        if ( ! -f $cab_filename)
-        {
-             # Cab file does not exist.
-            installer::logger::PrintError(
-                "could not find .cab file at '%s'.  Extraction of .exe seems to have failed.\n",
-                $cab_filename);
-        }
-
-        # Unpack the cab file.
-        my $msi = new installer::patch::Msi(
-            $source_version,
-            $language,
-            $product_name);
-
-        $installer::logger::Info->printf("unpacking cab file '%s' to '%s'\n",
-            $cab_filename, $unpacked_cab_path);
-        installer::patch::InstallationSet::UnpackCab(
-            $cab_filename,
-            $msi,
-            $unpacked_cab_path);
-    }
-    else
-    {
-        $installer::logger::Info->printf("cab has already been unpacked to\n");
-        $installer::logger::Info->printf("    %s\n", $unpacked_cab_path);
-    }
 
     $installer::logger::Info->decrease_indentation();
 }
@@ -196,31 +133,53 @@ sub ProcessLanguage ($$$$)
 
 
 
-installer::logger::SetupSimpleLogging("c:/tmp/log");
+sub main ()
+{
+    installer::logger::SetupSimpleLogging();
 
-my $arguments = ProcessCommandline();
-$arguments->{'package-format'} = 'msi';
+    my $arguments = ProcessCommandline();
+    $arguments->{'package-format'} = 'msi';
 
-print "preparing release build\n";
-my ($variables, undef, undef)
-    = installer::ziplist::read_openoffice_lst_file(
+    $installer::logger::Info->print("preparing release build\n");
+    my ($variables, undef, undef)
+        = installer::ziplist::read_openoffice_lst_file(
         $arguments->{'lst-file'},
         $arguments->{'product-name'},
         undef);
-if ( ! defined $arguments->{'source-version'})
-{
-    $arguments->{'source-version'} = $variables->{'PREVIOUS_VERSION'};
+    if ( ! defined $arguments->{'source-version'})
+    {
+        $arguments->{'source-version'} = $variables->{'PREVIOUS_VERSION'};
+        if ( ! defined $arguments->{'source-version'})
+        {
+            $arguments->{'source-version'} = installer::patch::ReleasesList::GetPreviousVersion(
+                $variables->{'PRODUCTVERSION'});
+            if ( ! defined $arguments->{'source-version'})
+            {
+                $installer::logger::Info->printf("ERROR: can not autodetect previous version\n");
+                $installer::logger::Info->printf("       please specify via 'PREVIOUS_VERSION' in %s\n",
+                    $arguments->{'lst-file'});
+                $installer::logger::Info->printf("       or the --source-version commandline option\n");
+                exit(1);
+            }
+        }
+    }
+    my $current_version = $variables->{'PRODUCTVERSION'};
+    $installer::logger::Info->printf("data from '%s'\n", $arguments->{'lst-file'});
+    $installer::logger::Info->printf("name is '%s'\n", $arguments->{'product-name'});
+    $installer::logger::Info->printf("path is '%s'\n", $arguments->{'output-path'});
+    $installer::logger::Info->printf("source version is '%s'\n", $arguments->{'source-version'});
+    $installer::logger::Info->printf("target version is '%s'\n", $current_version);
+
+    foreach my $language (@{$arguments->{'languages'}})
+    {
+        ProcessLanguage(
+            $arguments->{'source-version'},
+            $arguments->{'source-version'} eq $current_version,
+            $language,
+            $arguments->{'package-format'},
+            $arguments->{'product-name'});
+    }
 }
-$installer::logger::Info->printf("    reading data from '%s'\n", $arguments->{'lst-file'});
-$installer::logger::Info->printf("    product name is '%s'\n", $arguments->{'product-name'});
-$installer::logger::Info->printf("    output path is '%s'\n", $arguments->{'output-path'});
-$installer::logger::Info->printf("    source version is '%s'\n", $arguments->{'source-version'});
 
-foreach my $language (@{$arguments->{'languages'}})
-{
-    ProcessLanguage(
-        $arguments->{'source-version'},
-        $language,
-        $arguments->{'package-format'},
-        $arguments->{'product-name'});
-}
+
+main();