[Libreoffice-commits] core.git: Branch 'aoo/trunk' - instsetoo_native/util solenv/bin

Andre Fischer af at apache.org
Fri Dec 13 06:08:10 PST 2013


 instsetoo_native/util/makefile.mk                     |   41 
 solenv/bin/make_installer.pl                          |   17 
 solenv/bin/modules/installer/globals.pm               |    2 
 solenv/bin/modules/installer/languages.pm             |   13 
 solenv/bin/modules/installer/patch/InstallationSet.pm |  117 ++
 solenv/bin/modules/installer/patch/Msi.pm             |  127 +-
 solenv/bin/modules/installer/patch/Tools.pm           |   14 
 solenv/bin/modules/installer/windows/component.pm     |   21 
 solenv/bin/modules/installer/windows/directory.pm     |    2 
 solenv/bin/modules/installer/windows/feature.pm       |  164 ++-
 solenv/bin/modules/installer/windows/file.pm          |   57 -
 solenv/bin/modules/installer/windows/msiglobal.pm     |  225 +---
 solenv/bin/modules/installer/windows/property.pm      |   10 
 solenv/bin/modules/installer/windows/registry.pm      |   52 -
 solenv/bin/patch_tool.pl                              |  819 +++++++++++++-----
 solenv/bin/release_prepare.pl                         |    2 
 16 files changed, 1122 insertions(+), 561 deletions(-)

New commits:
commit 20fcc36e1c2f22d824d478d39bb37d5162317526
Author: Andre Fischer <af at apache.org>
Date:   Fri Dec 13 12:05:15 2013 +0000

    123531: Fixes for the patch creation.

diff --git a/instsetoo_native/util/makefile.mk b/instsetoo_native/util/makefile.mk
index 0988ebd0..3a9e3f5 100644
--- a/instsetoo_native/util/makefile.mk
+++ b/instsetoo_native/util/makefile.mk
@@ -87,6 +87,7 @@ help .PHONY :
     @echo "    patch-create           create a patch for updating an installed office (Windows only)"
     @echo "    patch-apply            apply a previously created patch"
     @echo "    patch-update-releases-xml"
+    @echo "    patch-check            check if patch can be created (part of patch-create)"
     @echo 
     @echo "Most targets (all except aoo_srcrelease and updatepack) accept suffixes"
     @echo "    add _<language> to build a target for one language only"
@@ -162,12 +163,17 @@ updatepack:
     $(PERL) -w $(SOLARENV)$/bin$/packager.pl
 
 
-# The naming schema of targets is this: target_language.package
-# where 'target' is the target base name (as openoffice or sdkoo)
-#       'language' is the language name (like en-US or fr)
-#       'package' is the package format (like msi or deb)
 
-.IF "$(alllangiso)"!=""
+.IF "$(alllangiso)"==""
+openoffice:
+    @echo no languages specified => aborting packing
+
+.ELSE	# "$(alllangiso)"==""
+
+# The naming schema of targets is this: <target>_<language>.<package>
+# where <target> is the target base name (like openoffice or sdkoo)
+#       <language> is the language name (like en-US or fr)
+#       <package> is the package format (like archive, msi, deb, rpm, dmg)
 
 # Add dependencies of basic targets on language specific targets.
 openoffice: $(foreach,i,$(alllangiso) openoffice_$i)
@@ -259,15 +265,6 @@ $(foreach,P,$(PACKAGE_FORMATS) $(foreach,L,$(alllangiso) openoffice_$L.$P)) .PHO
         $(PRJ)$/util$/update.xml	\
         > $(MISC)/$(@:b)_$(RTL_OS)_$(RTL_ARCH)$(@:e).update.xml
 
-#$(foreach,L,$(alllangiso) openoffice_$L.archive) :
-#	$(MAKE_INSTALLER_COMMAND) 		\
-#		-p Apache_OpenOffice		\
-#		-msitemplate $(MSIOFFICETEMPLATEDIR)
-#	$(GEN_UPDATE_INFO_COMMAND)		\
-#		--product Apache_OpenOffice	\
-#		$(PRJ)$/util$/update.xml	\
-#		> $(MISC)/$(@:b)_$(RTL_OS)_$(RTL_ARCH)$(@:e).update.xml
-
 #openofficewithjre_%{$(PKGFORMAT:^".")} :
 $(foreach,P,$(PACKAGE_FORMATS) $(foreach,L,$(alllangiso) openofficewithjre_$L.$P)) .PHONY :
     $(MAKE_INSTALLER_COMMAND) -p Apache_OpenOffice_wJRE -msitemplate $(MSIOFFICETEMPLATEDIR)
@@ -301,11 +298,7 @@ $(foreach,P,$(PACKAGE_FORMATS) $(foreach,L,$(alllangiso) sdkoo_$L.$P)) .PHONY :
 $(foreach,P,$(PACKAGE_FORMATS) $(foreach,L,$(alllangiso) sdkoodev_$L.$P)) .PHONY :
     $(MAKE_INSTALLER_COMMAND) -p Apache_OpenOffice_Dev_SDK -msitemplate $(MSISDKOOTEMPLATEDIR) -dontstrip
 
-.ELSE			# "$(alllangiso)"!=""
-openoffice:
-    @echo cannot pack nothing...
-
-.ENDIF			# "$(alllangiso)"!=""
+.ENDIF	# "$(alllangiso)"==""
 
 $(BIN)$/%.py : $(SOLARSHAREDBIN)$/pyuno$/%.py
     $(COPY) $< $@
@@ -344,6 +337,16 @@ patch-update-releases-xml .PHONY:
         --output-path $(OUT)						\
         --lst-file $(PRJ)$/util$/openoffice.lst\
         --target-version 4.0.1
+$(foreach,P,$(PACKAGE_FORMATS) $(foreach,L,$(alllangiso) patch-check_$L.$P)) .PHONY :
+    @echo building $@
+    perl -I $(SOLARENV)$/bin/modules $(SOLARENV)$/bin$/patch_tool.pl	\
+        check								\
+        --product-name Apache_OpenOffice				\
+        --output-path $(OUT)						\
+        --data-path $(PRJ)$/data					\
+        --lst-file $(PRJ)$/util$/openoffice.lst				\
+        --language $(subst,$(@:s/_/ /:1)_, $(@:b))			\
+        --package-format $(@:e:s/.//)
 
 $(PRJ)$/data :
     mkdir $@
diff --git a/solenv/bin/make_installer.pl b/solenv/bin/make_installer.pl
index 8aa7449..f73c0b6 100644
--- a/solenv/bin/make_installer.pl
+++ b/solenv/bin/make_installer.pl
@@ -303,11 +303,18 @@ sub MakeWindowsBuild ($$$$$$$$$$$$$$$$$$$$)
     $modulesinproductlanguageresolvedarrayref = installer::windows::feature::sort_feature(
         $modulesinproductlanguageresolvedarrayref);
 
-    installer::windows::feature::create_feature_table(
-        $modulesinproductlanguageresolvedarrayref,
-        $newidtdir,
-        $languagesarrayref,
-        $allvariableshashref);
+    foreach my $onelanguage (@$languagesarrayref)
+    {
+        my $features = installer::windows::feature::prepare_feature_table(
+            $modulesinproductlanguageresolvedarrayref,
+            $onelanguage,
+            $allvariableshashref);
+        $features = installer::windows::feature::add_missing_features($features);
+        installer::windows::feature::create_feature_table(
+            $newidtdir,
+            $onelanguage,
+            $features);
+    }
 
     installer::windows::featurecomponent::create_featurecomponent_table(
         $filesinproductlanguageresolvedarrayref,
diff --git a/solenv/bin/modules/installer/globals.pm b/solenv/bin/modules/installer/globals.pm
index 603991a..0afa9af 100644
--- a/solenv/bin/modules/installer/globals.pm
+++ b/solenv/bin/modules/installer/globals.pm
@@ -441,7 +441,7 @@ BEGIN
     @environmentvariables = ( "SOLARVERSION", "GUI", "WORK_STAMP", "OUTPATH", "LOCAL_OUT", "LOCAL_COMMON_OUT" );
     @packagelistitems = ("module", "solarispackagename", "packagename", "copyright", "vendor", "description" );
     @languagepackfeature =();
-    @featurecollector =();
+    %featurecollector =();
     $msiassemblyfiles = "";
     $nsisfilename = "Nsis";
     $macinstallfilename = "macinstall.ulf";
diff --git a/solenv/bin/modules/installer/languages.pm b/solenv/bin/modules/installer/languages.pm
index 13b0736..5b83ce0 100644
--- a/solenv/bin/modules/installer/languages.pm
+++ b/solenv/bin/modules/installer/languages.pm
@@ -479,13 +479,20 @@ sub get_normalized_language ($)
 
     if (ref($language) eq "ARRAY")
     {
-        if (scalar @$language > 1 && $language->[0] eq "en-US")
+        if (scalar @$language > 1)
         {
-            return $language->[1];
+            if ($language->[0] eq "en-US")
+            {
+                return $language->[1];
+            }
+            else
+            {
+                return $language->[0];
+            }
         }
         else
         {
-            return $language;
+            return join("_", @$language);
         }
     }
     elsif ($language =~ /^.*?_(.*)$/)
diff --git a/solenv/bin/modules/installer/patch/InstallationSet.pm b/solenv/bin/modules/installer/patch/InstallationSet.pm
index 876bfb8..8ea4f15 100644
--- a/solenv/bin/modules/installer/patch/InstallationSet.pm
+++ b/solenv/bin/modules/installer/patch/InstallationSet.pm
@@ -27,34 +27,85 @@ use installer::logger;
 
 use strict;
 
-# TODO: Detect the location of 7z.exe
-my $Unpacker = "/c/Program\\ Files/7-Zip/7z.exe";
+# Call Get7Zip() to get access to the filename of the 7z executable.
+my $SevenZip = undef;
 
 
+=head1 NAME
+
+    package installer::patch::InstallationSet  -  Functions for handling installation sets
+
+=head1 DESCRIPTION
+
+    This package contains functions for unpacking the .exe files that
+    are created by the NSIS installer creator and the .cab files in
+    the installation sets.
+
+=cut
 
-# TODO: Is there a touch in a standard library?
-sub touch ($)
+
+
+
+=head2 Detect7ZipOnWindows ()
+
+    7Zip seems to be the only program able to unpack an NSIS installer.
+    Search for it.
+
+=cut
+
+sub Detect7ZipOnWindows ()
 {
-    my ($filename) = @_;
+    # Use 'reg query' to read registry entry from Windows registry.
+    my $registry_key = "HKEY_CURRENT_USER\\\\Software\\\\7-Zip";
+    my $registry_value_name = "Path";
+    my $command = sprintf("reg query %s /v %s", $registry_key, $registry_value_name);
+    my $response = qx($command);
+
+    # Process the response.
+    my $path_to_7zip = undef;
+    if ($response =~ /\s+REG_SZ\s+([^\r\n]*)/m)
+    {
+        $path_to_7zip = $1;
+    }
 
-    open my $out, ">", $filename;
-    close $out;
+    # If that failed, then make an educated guess.
+    if ( ! defined $path_to_7zip)
+    {
+        $path_to_7zip = "c:\\Program Files\\7-Zip\\";
+    }
+
+    # Check if the executable exists and is, well, executable.
+    return undef unless -d $path_to_7zip;
+    my $fullname = File::Spec->catfile($path_to_7zip, "7z.exe");
+    return undef unless -f $fullname;
+    return undef unless -x $fullname;
+
+    return $fullname;
 }
 
 
 
 
-=head1 NAME
+sub Get7Zip ()
+{
+    if ( ! defined $SevenZip)
+    {
+        if ($ENV{'OS'} eq "WNT")
+        {
+            $SevenZip = Detect7ZipOnWindows();
+        }
+        if ( ! defined $SevenZip)
+        {
+            # Use an empty string to avoid repeated (and failing) detections of a missing 7z.
+            $SevenZip = "";
+        }
+    }
 
-    package installer::patch::InstallationSet  -  Functions for handling installation sets
+    return $SevenZip eq "" ? undef : $SevenZip;
+}
 
-=head1 DESCRIPTION
 
-    This package contains functions for unpacking the .exe files that
-    are created by the NSIS installer creator and the .cab files in
-    the installation sets.
 
-=cut
 
 sub UnpackExe ($$)
 {
@@ -69,12 +120,18 @@ sub UnpackExe ($$)
     my $windows_filename = installer::patch::Tools::ToEscapedWindowsPath($filename);
     my $windows_destination_path = installer::patch::Tools::ToEscapedWindowsPath($destination_path);
     my $command = join(" ",
-        $Unpacker,
+        "\"".Get7Zip()."\"",
         "x",
         "-y",
         "-o".$windows_destination_path,
         $windows_filename);
     my $result = qx($command);
+    if ( ! $result)
+    {
+        installer::exiter::exit_program(
+            "ERROR: can not unpack downloadable installation set: ".$!,
+            "installer::patch::InstallationSet::UnpackExe");
+    }
 
     # Check the existence of the .cab files.
     my $cab_filename = File::Spec->catfile($destination_path, "openoffice1.cab");
@@ -119,6 +176,7 @@ sub UnpackCab ($$$)
     if ( -d $temporary_destination_path)
     {
         # Temporary directory already exists => cab file has already been unpacked (flat), nothing to do.
+        printf("%s exists\n", $temporary_destination_path);
         $installer::logger::Info->printf("cab file has already been unpacked to flat structure\n");
     }
     else
@@ -130,22 +188,30 @@ sub UnpackCab ($$$)
     # Move the files to their destinations.
     File::Path::make_path($destination_path);
     $installer::logger::Info->printf("moving files to their directories\n");
+    my $directory_map = $msi->GetDirectoryMap();
+    my $office_menu_folder_name = $directory_map->{'INSTALLLOCATION'}->{'target_long_name'};
     my $count = 0;
     foreach my $file_row (@{$file_table->GetAllRows()})
     {
         my $unique_name = $file_row->GetValue('File');
-        my $directory_item = $file_map->{$unique_name}->{'directory'};
-        my $source_full_name = $directory_item->{'full_source_long_name'};
-
+        my $file_item = $file_map->{$unique_name};
+        my $directory_item = $file_item->{'directory'};
+        my $long_file_name = $file_item->{'long_name'};
+        my $full_name = $directory_item->{'full_source_long_name'};
+        # Strip away the leading OfficeMenuFolder part.
+        $full_name =~ s/^$office_menu_folder_name\///;
         my $flat_filename = File::Spec->catfile($temporary_destination_path, $unique_name);
-        my $dir_path = File::Spec->catfile($destination_path, $source_full_name);
-        my $dir_filename = File::Spec->catfile($dir_path, $unique_name);
+        my $dir_path = File::Spec->catfile($destination_path, $full_name);
+        my $dir_filename = File::Spec->catfile($dir_path, $long_file_name);
 
         if ( ! -d $dir_path)
         {
             File::Path::make_path($dir_path);
         }
-        File::Copy::move($flat_filename, $dir_filename);
+
+        $installer::logger::Lang->printf("moving %s to %s\n", $flat_filename, $dir_filename);
+        File::Copy::move($flat_filename, $dir_filename)
+            || die("can not move file ".$flat_filename.":".$!);
 
         ++$count;
     }
@@ -180,7 +246,7 @@ sub UnpackCabFlat ($$$)
     my $windows_cab_filename = installer::patch::Tools::ToEscapedWindowsPath($cab_filename);
     my $windows_destination_path = installer::patch::Tools::ToEscapedWindowsPath($destination_path);
     my $command = join(" ",
-        $Unpacker,
+        "\"".Get7Zip()."\"",
         "x", "-o".$windows_destination_path,
         $windows_cab_filename,
         "-y");
@@ -449,6 +515,7 @@ sub ProvideDownloadSet ($$$)
     my ($version, $language, $package_format) = @_;
 
     my $release_item = installer::patch::ReleasesList::Instance()->{$version}->{$package_format}->{$language};
+    return undef unless defined $release_item;
 
     # Get basename of installation set from URL.
     $release_item->{'URL'} =~ /^(.*)\/([^\/]+)$/;
@@ -558,7 +625,7 @@ sub ProvideUnpackedExe ($$$$$)
         $file_count,
         $directory_count);
 
-        touch($unpacked_exe_flag_filename);
+        installer::patch::Tools::touch($unpacked_exe_flag_filename);
 
         return 1;
     }
@@ -580,7 +647,7 @@ sub ProvideUnpackedExe ($$$$$)
                 $installer::logger::Info->printf("downloadable installation set has been unpacked to\n");
                 $installer::logger::Info->printf("    %s\n", $unpacked_exe_path);
 
-                touch($unpacked_exe_flag_filename);
+                installer::patch::Tools::touch($unpacked_exe_flag_filename);
 
                 return 1;
             }
@@ -774,7 +841,7 @@ sub ProvideUnpackedCab ($$$$$)
             $installer::logger::Info->printf("unpacked cab file '%s'\n", $cab_filename);
             $installer::logger::Info->printf("    to '%s'\n", $unpacked_cab_path);
 
-            touch($unpacked_cab_flag_filename);
+            installer::patch::Tools::touch($unpacked_cab_flag_filename);
 
             return 1;
         }
diff --git a/solenv/bin/modules/installer/patch/Msi.pm b/solenv/bin/modules/installer/patch/Msi.pm
index e5b47f6..f6d4497 100644
--- a/solenv/bin/modules/installer/patch/Msi.pm
+++ b/solenv/bin/modules/installer/patch/Msi.pm
@@ -76,7 +76,7 @@ sub FindAndCreate($$$$$)
 
 =cut
 
-sub new ($$$$$$)
+sub new ($$;$$$$)
 {
     my ($class, $filename, $version, $is_current_version, $language, $product_name) = @_;
 
@@ -102,6 +102,22 @@ sub new ($$$$$$)
     };
     bless($self, $class);
 
+    # Fill in some missing values from the 'Properties' table.
+    if ( ! (defined $version && defined $language && defined $product_name))
+    {
+        my $property_table = $self->GetTable("Property");
+
+        $self->{'version'} = $property_table->GetValue("Property", "DEFINEDVERSION", "Value")
+            unless defined $self->{'version'};
+        $self->{'product_name'} = $property_table->GetValue("Property", "DEFINEDPRODUCT", "Value")
+            unless defined $self->{'product_name'};
+
+        my $language = $property_table->GetValue("Property", "ProductLanguage", "Value");
+        # TODO: Convert numerical language id to language name.
+        $self->{'language'} = $language
+            unless defined $self->{'language'};
+    }
+
     return $self;
 }
 
@@ -181,7 +197,6 @@ sub GetTable ($$)
                 "-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}),
                 "-e", $table_name);
             my $result = qx($command);
-            print $result;
         }
 
         # Read table into memory.
@@ -323,6 +338,48 @@ sub SplitTargetSourceLongShortName ($)
 }
 
 
+
+
+sub SetupFullNames ($$);
+sub SetupFullNames ($$)
+{
+    my ($item, $directory_map) = @_;
+
+    # Don't process any item twice.
+    return if defined $item->{'full_source_name'};
+
+    my $parent = $item->{'parent'};
+    if (defined $parent)
+    {
+        # Process the parent first.
+        if ( ! defined $parent->{'full_source_long_name'})
+        {
+            SetupFullNames($parent, $directory_map);
+        }
+
+        # Prepend the full names of the parent to our names.
+        $item->{'full_source_long_name'}
+            = $parent->{'full_source_long_name'} . "/" . $item->{'source_long_name'};
+        $item->{'full_source_short_name'}
+            = $parent->{'full_source_short_name'} . "/" . $item->{'source_short_name'};
+        $item->{'full_target_long_name'}
+            = $parent->{'full_target_long_name'} . "/" . $item->{'target_long_name'};
+        $item->{'full_target_short_name'}
+            = $parent->{'full_target_short_name'} . "/" . $item->{'target_short_name'};
+    }
+    else
+    {
+        # Directory has no parent => full names are the same as the name.
+        $item->{'full_source_long_name'} = $item->{'source_long_name'};
+        $item->{'full_source_short_name'} = $item->{'source_short_name'};
+        $item->{'full_target_long_name'} = $item->{'target_long_name'};
+        $item->{'full_target_short_name'} = $item->{'target_short_name'};
+    }
+}
+
+
+
+
 =head2 GetDirectoryMap($self)
 
     Return a map that maps directory unique names (column 'Directory' in table 'Directory')
@@ -339,17 +396,18 @@ sub GetDirectoryMap ($)
         return $self->{'DirectoryMap'};
     }
 
+    # Initialize the directory map.
     my $directory_table = $self->GetTable("Directory");
-    my %dir_map = ();
+    my $directory_map = ();
     foreach my $row (@{$directory_table->GetAllRows()})
     {
         my ($target_long_name, $target_short_name, $source_long_name, $source_short_name)
             = installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir"));
         my $unique_name = $row->GetValue("Directory");
-        $dir_map{$unique_name} =
+        $directory_map->{$unique_name} =
         {
             'unique_name' => $unique_name,
-            'parent' => $row->GetValue("Directory_Parent"),
+            'parent_name' => $row->GetValue("Directory_Parent"),
             'default_dir' => $row->GetValue("DefaultDir"),
             'source_long_name' => $source_long_name,
             'source_short_name' => $source_short_name,
@@ -358,49 +416,20 @@ sub GetDirectoryMap ($)
         };
     }
 
-    # Set up full names for all directories.
-    my @todo = map {$_} (keys %dir_map);
-    while (scalar @todo > 0)
+    # Add references to parent directories.
+    foreach my $item (values %$directory_map)
     {
-        my $key = shift @todo;
-        my $item = $dir_map{$key};
-        next if defined $item->{'full_source_name'};
+        $item->{'parent'} = $directory_map->{$item->{'parent_name'}};
+    }
 
-        if ($item->{'parent'} eq "")
-        {
-            # Directory has no parent => full names are the same as the name.
-            $item->{'full_source_long_name'} = $item->{'source_long_name'};
-            $item->{'full_source_short_name'} = $item->{'source_short_name'};
-            $item->{'full_target_long_name'} = $item->{'target_long_name'};
-            $item->{'full_target_short_name'} = $item->{'target_short_name'};
-        }
-        else
-        {
-            my $parent = $dir_map{$item->{'parent'}};
-            if ( defined $parent->{'full_source_long_name'})
-            {
-                # Parent aleady has full names => we can create the full name of the current item.
-                $item->{'full_source_long_name'}
-                    = $parent->{'full_source_long_name'} . "/" . $item->{'source_long_name'};
-                $item->{'full_source_short_name'}
-                    = $parent->{'full_source_short_name'} . "/" . $item->{'source_short_name'};
-                $item->{'full_target_long_name'}
-                    = $parent->{'full_target_long_name'} . "/" . $item->{'target_long_name'};
-                $item->{'full_target_short_name'}
-                    = $parent->{'full_target_short_name'} . "/" . $item->{'target_short_name'};
-            }
-            else
-            {
-                # Parent has to be processed before the current item can be processed.
-                # Push both to the head of the list.
-                unshift @todo, $key;
-                unshift @todo, $item->{'parent'};
-            }
-        }
+    # Set up full names for all directories.
+    foreach my $item (values %$directory_map)
+    {
+        SetupFullNames($item, $directory_map);
     }
 
-    # Postprocess the path names for cleanup.
-    foreach my $item (values %dir_map)
+    # Cleanup the names.
+    foreach my $item (values %$directory_map)
     {
         foreach my $id (
             'full_source_long_name',
@@ -414,7 +443,7 @@ sub GetDirectoryMap ($)
         }
     }
 
-    $self->{'DirectoryMap'} = \%dir_map;
+    $self->{'DirectoryMap'} = $directory_map;
     return $self->{'DirectoryMap'};
 }
 
@@ -455,14 +484,20 @@ sub GetFileMap ($)
     my $file_map = {};
     my $file_component_index = $file_table->GetColumnIndex("Component_");
     my $file_file_index = $file_table->GetColumnIndex("File");
+    my $file_filename_index = $file_table->GetColumnIndex("FileName");
     foreach my $file_row (@{$file_table->GetAllRows()})
     {
         my $component_name = $file_row->GetValue($file_component_index);
         my $directory_name = $component_to_directory_map{$component_name};
         my $unique_name = $file_row->GetValue($file_file_index);
+        my $file_name = $file_row->GetValue($file_filename_index);
+        my ($long_name, $short_name) = SplitLongShortName($file_name);
         $file_map->{$unique_name} = {
             'directory' => $dir_map->{$directory_name},
-            'component_name' => $component_name
+            'component_name' => $component_name,
+            'file_name' => $file_name,
+            'long_name' => $long_name,
+            'short_name' => $short_name
         };
     }
 
diff --git a/solenv/bin/modules/installer/patch/Tools.pm b/solenv/bin/modules/installer/patch/Tools.pm
index 06035a3..6dd45e9 100644
--- a/solenv/bin/modules/installer/patch/Tools.pm
+++ b/solenv/bin/modules/installer/patch/Tools.pm
@@ -59,4 +59,18 @@ sub ToWindowsPath ($)
     return $windows_path;
 }
 
+
+# TODO: Is there a touch in a standard library?
+sub touch ($)
+{
+    my ($filename) = @_;
+
+    open my $out, ">", $filename;
+    close $out;
+}
+
+
+
+
+
 1;
diff --git a/solenv/bin/modules/installer/windows/component.pm b/solenv/bin/modules/installer/windows/component.pm
index 8ccaed7..0383852 100644
--- a/solenv/bin/modules/installer/windows/component.pm
+++ b/solenv/bin/modules/installer/windows/component.pm
@@ -513,7 +513,7 @@ sub get_component_data ($$$$)
         }
         else
         {
-            $guid = installer::windows::msiglobal::create_guid();
+            $guid = "{" . installer::windows::msiglobal::create_guid() . "}";
             $installer::logger::Lang->printf("    creating new guid %s\n", $guid);
         }
         $target_data{$name}->{'component_id'} = $guid;
@@ -521,28 +521,33 @@ sub get_component_data ($$$$)
 
     # Add values for the KeyPath column.
     $installer::logger::Lang->printf("preparing Component->KeyPath values\n");
-    foreach my $name (@$file_component_names,@$registry_component_names)
+    foreach my $component_name (@$file_component_names,@$registry_component_names)
     {
         # Determine the key path.
         my $key_path = $installer::globals::is_release
-            ? $source_data{$name}->{'key_path'}
+            ? $source_data{$component_name}->{'key_path'}
             : undef;
         if (defined $key_path)
         {
-            $installer::logger::Lang->printf("    reusing key path %s\n", $key_path);
+            $installer::logger::Lang->printf("    reusing key path %s for component %s\n",
+                $key_path,
+                $component_name);
         }
         else
         {
-            if ($target_data{$name}->{'is_file'})
+            if ($target_data{$component_name}->{'is_file'})
             {
-                $key_path = get_component_keypath($name, $files);
+                $key_path = get_component_keypath($component_name, $files);
             }
             else
             {
-                $key_path = get_component_keypath($name, $registry_entries);
+                $key_path = get_component_keypath($component_name, $registry_entries);
             }
+            $installer::logger::Lang->printf("    created key path %s for component %s\n",
+                $key_path,
+                $component_name);
         }
-        $target_data{$name}->{'key_path'} = $key_path;
+        $target_data{$component_name}->{'key_path'} = $key_path;
     }
 
     return \%target_data;
diff --git a/solenv/bin/modules/installer/windows/directory.pm b/solenv/bin/modules/installer/windows/directory.pm
index 5560260..c62f50c 100644
--- a/solenv/bin/modules/installer/windows/directory.pm
+++ b/solenv/bin/modules/installer/windows/directory.pm
@@ -750,7 +750,7 @@ sub find_missing_directories ($$)
     {
         my $new_directory_item = {
             'uniquename' => $source_directory_item->{'unique_name'},
-            'uniqueparentname' => $source_directory_item->{'parent'},
+            'uniqueparentname' => $source_directory_item->{'parent_name'},
             'defaultdir' => $source_directory_item->{'default_dir'},
             'HostName' => $source_directory_item->{'full_target_long_name'},
             'componentname' => $source_directory_item->{'component_name'},
diff --git a/solenv/bin/modules/installer/windows/feature.pm b/solenv/bin/modules/installer/windows/feature.pm
index 20cba14..d2a1aa4 100644
--- a/solenv/bin/modules/installer/windows/feature.pm
+++ b/solenv/bin/modules/installer/windows/feature.pm
@@ -363,78 +363,140 @@ sub add_uniquekey
 # Feature Feature_Parent Title Description Display Level Directory_ Attributes
 #################################################################################
 
-sub create_feature_table
+sub prepare_feature_table ($$$)
 {
-    my ($modulesref, $basedir, $languagesarrayref, $allvariableshashref) = @_;
+    my ($modules, $language, $variables) = @_;
 
-    for ( my $m = 0; $m <= $#{$languagesarrayref}; $m++ )
+    my $features = [];
+
+    foreach my $onefeature (@$modules)
     {
-        my $onelanguage = ${$languagesarrayref}[$m];
+        # Java and Ada only, if the correct settings are set
+        my $styles = $onefeature->{'Styles'} // "";
+        if (( $styles =~ /\bJAVAMODULE\b/ ) && ( ! ($variables->{'JAVAPRODUCT'} ))) { next; }
+
+        # Controlling the language!
+        # Only language independent feature or feature with the correct language will be included into the table
+
+        next if $onefeature->{'ismultilingual'} && ($onefeature->{'specificlanguage'} ne $language);
+
+        my $feature_gid =get_feature_gid($onefeature);
+
+        my $feature = {
+            'Feature' => $feature_gid,
+            'Feature_Parent' => get_feature_parent($onefeature),
+            'Title' => $onefeature->{'Name'},
+            'Description' => $onefeature->{'Description'},
+            'Display' => get_feature_display($onefeature),
+            'Level' => get_feature_level($onefeature),
+            'Directory_' => get_feature_directory($onefeature),
+            'Attributes' => get_feature_attributes($onefeature)
+        };
+        push @$features, $feature;
+
+        # collecting all feature in global feature collector (so that properties can be set in property table)
+        $installer::globals::featurecollector{$feature_gid} = 1;
+
+        # collecting all language feature in feature collector for check of language selection
+        if (( $styles =~ /\bSHOW_MULTILINGUAL_ONLY\b/ ) && $onefeature->{'ParentID'} ne $installer::globals::rootmodulegid)
+        {
+            $installer::globals::multilingual_only_modules{$feature_gid} = 1;
+        }
 
-        my $infoline;
+        # collecting all application feature in global feature collector for check of application selection
+        if ( $styles =~ /\bAPPLICATIONMODULE\b/ )
+        {
+            $installer::globals::application_modules{$feature_gid} = 1;
+        }
+    }
 
-        my @featuretable = ();
+    return $features;
+}
 
-        installer::windows::idtglobal::write_idt_header(\@featuretable, "feature");
 
-        for ( my $i = 0; $i <= $#{$modulesref}; $i++ )
-        {
-            my $onefeature = ${$modulesref}[$i];
 
-            # Java and Ada only, if the correct settings are set
-            my $styles = "";
-            if ( $onefeature->{'Styles'} ) { $styles = $onefeature->{'Styles'}; }
-            if (( $styles =~ /\bJAVAMODULE\b/ ) && ( ! ($allvariableshashref->{'JAVAPRODUCT'} ))) { next; }
-            if (( $styles =~ /\bADAMODULE\b/ ) && ( ! ($allvariableshashref->{'ADAPRODUCT'} ))) { next; }
 
-            # Controlling the language!
-            # Only language independent feature or feature with the correct language will be included into the table
+=head add_missing_features($features)
 
-            if (! (!(( $onefeature->{'ismultilingual'} )) || ( $onefeature->{'specificlanguage'} eq $onelanguage )) )  { next; }
+    When we are building a release, then there may be features missing
+    that where present in the source release.  As missing features
+    would prevent patches from being created, we add the missing
+    features.
 
-            my %feature = ();
+    The returned feature hash is either identical to the given
+    $features or is a copy with the missing features added.
 
-            $feature{'feature'} = get_feature_gid($onefeature);
-            $feature{'feature_parent'} = get_feature_parent($onefeature);
-            # if ( $onefeature->{'ParentID'} eq "" ) { $feature{'feature_parent'} = ""; }   # Root has no parent
-            $feature{'Title'} = $onefeature->{'Name'};
-            $feature{'Description'} = $onefeature->{'Description'};
-            $feature{'Display'} = get_feature_display($onefeature);
-            $feature{'Level'} = get_feature_level($onefeature);
-            $feature{'Directory_'} = get_feature_directory($onefeature);
-            $feature{'Attributes'} = get_feature_attributes($onefeature);
+=cut
 
-            my $oneline = $feature{'feature'} . "\t" . $feature{'feature_parent'} . "\t" . $feature{'Title'} . "\t"
-                    . $feature{'Description'} . "\t" . $feature{'Display'} . "\t" . $feature{'Level'} . "\t"
-                    . $feature{'Directory_'} . "\t" . $feature{'Attributes'} . "\n";
+sub add_missing_features ($)
+{
+    my ($features) = @_;
 
-            push(@featuretable, $oneline);
+    return $features if ! $installer::globals::is_release;
 
-            # collecting all feature in global feature collector (so that properties can be set in property table)
-            if ( ! installer::existence::exists_in_array($feature{'feature'}, \@installer::globals::featurecollector) )
-            {
-                push(@installer::globals::featurecollector, $feature{'feature'});
-            }
+    # Aquire the feature list of the source release.
+    my $source_feature_table = $installer::globals::source_msi->GetTable("Feature");
+    my $feature_column_index = $source_feature_table->GetColumnIndex("Feature");
 
-            # collecting all language feature in feature collector for check of language selection
-            if (( $styles =~ /\bSHOW_MULTILINGUAL_ONLY\b/ ) && ( $onefeature->{'ParentID'} ne $installer::globals::rootmodulegid ))
-            {
-                $installer::globals::multilingual_only_modules{$feature{'feature'}} = 1;
-            }
+    # Prepare fast lookup of the target features.
+    my %target_feature_map = map {$_->{'Feature'} => $_} @$features;
 
-            # collecting all application feature in global feature collector for check of application selection
-            if ( $styles =~ /\bAPPLICATIONMODULE\b/ )
-            {
-                $installer::globals::application_modules{$feature{'feature'}} = 1;
-            }
+    # Find missing features.
+    my @missing_features = ();
+    foreach my $source_feature_row (@{$source_feature_table->GetAllRows()})
+    {
+        my $feature_gid = $source_feature_row->GetValue($feature_column_index);
+        if ( ! defined $target_feature_map{$feature_gid})
+        {
+            push @missing_features, $source_feature_row;
         }
+    }
 
-        # Saving the file
+    # Return when there are no missing features.
+    return $features if scalar @missing_features==0;
+
+    # Process the missing features.
+    my $extended_features = [@$features];
+    foreach my $missing_feature_row (@missing_features)
+    {
+        my %feature = map
+            {$_ => $missing_feature_row->GetValue($_)}
+            ('Feature', 'Feature_Parent', 'Title', 'Description', 'Display', 'Level', 'Directory_', 'Attributes');
+        push @$extended_features, \%feature;
 
-        my $featuretablename = $basedir . $installer::globals::separator . "Feature.idt" . "." . $onelanguage;
-        installer::files::save_file($featuretablename ,\@featuretable);
-        $installer::logger::Lang->printf("Created idt file: %s\n", $featuretablename);
+        $installer::logger::Lang->printf("added missing feature %s\n", $feature->{'Feature'});
     }
+    return $extended_features;
+}
+
+
+
+
+sub create_feature_table ($$$)
+{
+    my ($basedir, $language, $features) = @_;
+
+    my @feature_table = ();
+    installer::windows::idtglobal::write_idt_header(\@feature_table, "feature");
+
+    foreach my $feature (@$features)
+    {
+        my $line = join("\t",
+            $feature->{'Feature'},
+            $feature->{'Feature_Parent'},
+            $feature->{'Title'},
+            $feature->{'Description'},
+            $feature->{'Display'},
+            $feature->{'Level'},
+            $feature->{'Directory_'},
+            $feature->{'Attributes'}) . "\n";
+
+        push(@feature_table, $line);
+    }
+
+    my $filename = $basedir . $installer::globals::separator . "Feature.idt" . "." . $language;
+    installer::files::save_file($filename ,\@feature_table);
+    $installer::logger::Lang->printf("Created idt file: %s\n", $filename);
 }
 
 1;
diff --git a/solenv/bin/modules/installer/windows/file.pm b/solenv/bin/modules/installer/windows/file.pm
index e9b5d76..339cf4a 100644
--- a/solenv/bin/modules/installer/windows/file.pm
+++ b/solenv/bin/modules/installer/windows/file.pm
@@ -635,19 +635,19 @@ sub assign_missing_sequence_numbers ($)
 
 sub create_items_for_missing_files ($$$)
 {
-    my ($missing_items, $msi, $directory_list) = @_;
+    my ($missing_items, $source_msi, $directory_list) = @_;
 
     # For creation of the FeatureComponent table (in a later step) we
     # have to provide references from the file to component and
     # modules (ie features).  Note that Each file belongs to exactly
     # one component but one component can belong to multiple features.
-    my $component_to_features_map = create_feature_component_map($msi);
+    my $component_to_features_map = create_feature_component_map($source_msi);
 
     my @new_files = ();
     foreach my $row (@$missing_items)
     {
         $installer::logger::Info->printf("creating new file item for '%s'\n", $row->GetValue('File'));
-        my $file_item = create_script_item_for_deleted_file($row, $msi, $component_to_features_map);
+        my $file_item = create_script_item_for_deleted_file($row, $source_msi, $component_to_features_map);
         push @new_files, $file_item;
     }
 
@@ -657,26 +657,53 @@ sub create_items_for_missing_files ($$$)
 
 
 
+=head2 create_script_item_for_deleted_file (($file_row, $source_msi, $component_to_features_map)
+
+    Create a new script item for a file that was present in the
+    previous release but isn't anymore.  Most of the necessary
+    information is taken from the 'File' table of the source release.
+
+    The values of 'sourcepath' and 'cyg_sourcepath' will point to the
+    respective file in the unpacked source release.  An alternative
+    would be to let them point to an empty file.  That, however, might
+    make the patch bigger (diff between identical file contents is
+    (almost) empty, diff between file and empty file is the 'inverse'
+    of the file).
+
+=cut
+
+my $use_source_files_for_missing_files = 1;
+
 sub create_script_item_for_deleted_file ($$$)
 {
-    my ($file_row, $msi, $component_to_features_map) = @_;
+    my ($file_row, $source_msi, $component_to_features_map) = @_;
 
     my $uniquename = $file_row->GetValue('File');
 
-    my $file_map = $msi->GetFileMap();
+    my $file_map = $source_msi->GetFileMap();
 
-    my $directory_item = $file_map->{$uniquename}->{'directory'};
+    my $file_item = $file_map->{$uniquename};
+    my $directory_item = $file_item->{'directory'};
     my $source_path = $directory_item->{'full_source_long_name'};
     my $target_path = $directory_item->{'full_target_long_name'};
-    my $full_source_name = File::Spec->catfile(
-        installer::patch::InstallationSet::GetUnpackedCabPath(
-            $msi->{'version'},
-            $msi->{'is_current_version'},
-            $msi->{'language'},
-            $msi->{'package_format'},
-            $msi->{'product_name'}),
-        $source_path,
-        $uniquename);
+    my $full_source_name = undef;
+    if ($use_source_files_for_missing_files)
+    {
+        $full_source_name = File::Spec->catfile(
+            installer::patch::InstallationSet::GetUnpackedCabPath(
+                $source_msi->{'version'},
+                $source_msi->{'is_current_version'},
+                $source_msi->{'language'},
+                $source_msi->{'package_format'},
+                $source_msi->{'product_name'}),
+            $source_path,
+            $file_item->{'long_name'});
+    }
+    else
+    {
+        $full_source_name = "/c/tmp/missing/".$uniquename;
+        installer::patch::Tools::touch($full_source_name);
+    }
     my ($long_name, undef) = installer::patch::Msi::SplitLongShortName($file_row->GetValue("FileName"));
     my $target_name = File::Spec->catfile($target_path, $long_name);
     if ( ! -f $full_source_name)
diff --git a/solenv/bin/modules/installer/windows/msiglobal.pm b/solenv/bin/modules/installer/windows/msiglobal.pm
index 81c0b31..57c06fc 100644
--- a/solenv/bin/modules/installer/windows/msiglobal.pm
+++ b/solenv/bin/modules/installer/windows/msiglobal.pm
@@ -131,25 +131,6 @@ sub make_relative_ddf_path
     return $sourcepath;
 }
 
-##########################################################################
-# Returning the order of the sequences in the files array.
-##########################################################################
-
-sub get_sequenceorder
-{
-    my ($filesref) = @_;
-
-    my %order = ();
-
-    for ( my $i = 0; $i <= $#{$filesref}; $i++ )
-    {
-        my $onefile = ${$filesref}[$i];
-        if ( ! $onefile->{'assignedsequencenumber'} ) { installer::exiter::exit_program("ERROR: No sequence number assigned to $onefile->{'gid'} ($onefile->{'uniquename'})!", "get_sequenceorder"); }
-        $order{$onefile->{'assignedsequencenumber'}} = $i;
-    }
-
-    return \%order;
-}
 
 ##########################################################################
 # Generation the list, in which the source of the files is connected
@@ -157,185 +138,87 @@ sub get_sequenceorder
 # to be included into a cab file, this has to be done via ddf files.
 ##########################################################################
 
-sub generate_cab_file_list
+sub generate_cab_file_list ($$$$)
 {
     my ($filesref, $installdir, $ddfdir, $allvariables) = @_;
 
-    my @cabfilelist = ();
-
     installer::logger::include_header_into_logfile("Generating ddf files");
 
-    $installer::logger::Lang->add_timestamp("Performance Info: ddf file generation start");
-
-    if ( $^O =~ /cygwin/i ) { installer::worker::generate_cygwin_pathes($filesref); }
+    if ( $^O =~ /cygwin/i )
+    {
+        installer::worker::generate_cygwin_pathes($filesref);
+    }
 
-    if ( $installer::globals::fix_number_of_cab_files )
+    # Make sure that all files point to the same cabinet file.
+    # Multiple cabinet files are not supported anymore.
+    my $cabinetfile = $filesref->[0]->{'cabinet'};
+    foreach my $onefile (@$filesref)
     {
-        for ( my $i = 0; $i <= $#{$filesref}; $i++ )
+        if ($onefile->{'cabinet'} ne $cabinetfile)
         {
-            my $onefile = ${$filesref}[$i];
-            my $cabinetfile = $onefile->{'cabinet'};
-            my $sourcepath =  $onefile->{'sourcepath'};
-            if ( $^O =~ /cygwin/i ) { $sourcepath = $onefile->{'cyg_sourcepath'}; }
-            my $uniquename =  $onefile->{'uniquename'};
-
-            my $styles = "";
-            my $doinclude = 1;
-            if ( $onefile->{'Styles'} ) { $styles = $onefile->{'Styles'}; };
-            if ( $styles =~ /\bDONT_PACK\b/ ) { $doinclude = 0; }
-
-
-            # to avoid lines with more than 256 characters, it can be useful to use relative pathes
-            if ( $allvariables->{'RELATIVE_PATHES_IN_DDF'} ) { $sourcepath = make_relative_ddf_path($sourcepath); }
-
-            # all files with the same cabinetfile are directly behind each other in the files collector
-
-            my @ddffile = ();
-
-            write_ddf_file_header(\@ddffile, $cabinetfile, $installdir);
-
-            my $ddfline = "\"" . $sourcepath . "\"" . " " . $uniquename . "\n";
-            if ( $doinclude ) { push(@ddffile, $ddfline); }
-
-            my $nextfile = ${$filesref}[$i+1];
-            my $nextcabinetfile = "";
-
-            if ( $nextfile->{'cabinet'} ) { $nextcabinetfile = $nextfile->{'cabinet'}; }
-
-            while ( $nextcabinetfile eq $cabinetfile )
-            {
-                $sourcepath =  $nextfile->{'sourcepath'};
-                if ( $^O =~ /cygwin/i ) { $sourcepath = $nextfile->{'cyg_sourcepath'}; }
-                # to avoid lines with more than 256 characters, it can be useful to use relative pathes
-                if ( $allvariables->{'RELATIVE_PATHES_IN_DDF'} ) { $sourcepath = make_relative_ddf_path($sourcepath); }
-                $uniquename =  $nextfile->{'uniquename'};
-                my $localdoinclude = 1;
-                my $nextfilestyles = "";
-                if ( $nextfile->{'Styles'} ) { $nextfilestyles = $nextfile->{'Styles'}; }
-                if ( $nextfilestyles =~ /\bDONT_PACK\b/ ) { $localdoinclude = 0; }
-                $ddfline = "\"" . $sourcepath . "\"" . " " . $uniquename . "\n";
-                if ( $localdoinclude ) { push(@ddffile, $ddfline); }
-                $i++;                                           # increasing the counter!
-                $nextfile = ${$filesref}[$i+1];
-                if ( $nextfile ) { $nextcabinetfile = $nextfile->{'cabinet'}; }
-                else { $nextcabinetfile = "_lastfile_"; }
-            }
-
-            # creating the DDF file
-
-            my $ddffilename = $cabinetfile;
-            $ddffilename =~ s/.cab/.ddf/;
-            $ddfdir =~ s/\Q$installer::globals::separator\E\s*$//;
-            $ddffilename = $ddfdir . $installer::globals::separator . $ddffilename;
-
-            installer::files::save_file($ddffilename ,\@ddffile);
-            my $infoline = "Created ddf file: $ddffilename\n";
-            $installer::logger::Lang->print($infoline);
-
-            # lines in ddf files must not be longer than 256 characters
-            check_ddf_file(\@ddffile, $ddffilename);
-
-            # Writing the makecab system call
-
-            my $oneline = "makecab.exe /V3 /F " . $ddffilename . " 2\>\&1 |" . "\n";
-
-            push(@cabfilelist, $oneline);
-
-            # collecting all ddf files
-            push(@installer::globals::allddffiles, $ddffilename);
+            installer::exiter::exit_program(
+                "ERROR: multiple cabinet files are not supported",
+                "generate_cab_file_list");
         }
     }
-    elsif ( $installer::globals::one_cab_file )
-    {
-        my @ddffile = ();
 
-        my $cabinetfile = "";
+    # Sort files on the sequence number.
+    my @sorted_files = sort {$a->{'sequencenumber'} <=> $b->{'sequencenumber'}} @$filesref;
 
-        for ( my $i = 0; $i <= $#{$filesref}; $i++ )
+    my @ddffile = ();
+    write_ddf_file_header(\@ddffile, $cabinetfile, $installdir);
+    foreach my $onefile (@sorted_files)
+    {
+        my $styles = $onefile->{'Styles'} // "";
+        if ($styles =~ /\bDONT_PACK\b/)
         {
-            my $onefile = ${$filesref}[$i];
-            $cabinetfile = $onefile->{'cabinet'};
-            my $sourcepath =  $onefile->{'sourcepath'};
-            if ( $^O =~ /cygwin/i ) { $sourcepath = $onefile->{'cyg_sourcepath'}; }
-            my $uniquename =  $onefile->{'uniquename'};
-
-            # to avoid lines with more than 256 characters, it can be useful to use relative pathes
-            if ( $allvariables->{'RELATIVE_PATHES_IN_DDF'} ) { $sourcepath = make_relative_ddf_path($sourcepath); }
-
-            if ( $i == 0 ) { write_ddf_file_header(\@ddffile, $cabinetfile, $installdir); }
-
-            my $styles = "";
-            my $doinclude = 1;
-            if ( $onefile->{'Styles'} ) { $styles = $onefile->{'Styles'}; };
-            if ( $styles =~ /\bDONT_PACK\b/ ) { $doinclude = 0; }
-
-            my $ddfline = "\"" . $sourcepath . "\"" . " " . $uniquename . "\n";
-            if ( $doinclude ) { push(@ddffile, $ddfline); }
+            $installer::logger::Lang->printf("    excluding '%s' from ddf\n", $onefile->{'uniquename'});
         }
 
-        # creating the DDF file
-
-        my $ddffilename = $cabinetfile;
-        $ddffilename =~ s/.cab/.ddf/;
-        $ddfdir =~ s/[\/\\]\s*$//;
-        $ddffilename = $ddfdir . $installer::globals::separator . $ddffilename;
-
-        installer::files::save_file($ddffilename ,\@ddffile);
-        my $infoline = "Created ddf file: $ddffilename\n";
-        $installer::logger::Lang->print($infoline);
-
-        # lines in ddf files must not be longer than 256 characters
-        check_ddf_file(\@ddffile, $ddffilename);
-
-        # Writing the makecab system call
+        my $uniquename = $onefile->{'uniquename'};
+        my $sourcepath = $onefile->{'sourcepath'};
+        if ( $^O =~ /cygwin/i )
+        {
+            $sourcepath = $onefile->{'cyg_sourcepath'};
+        }
 
-        my $oneline = "makecab.exe /F " . $ddffilename . "\n";
+        # to avoid lines with more than 256 characters, it can be useful to use relative pathes
+        if ($allvariables->{'RELATIVE_PATHES_IN_DDF'})
+        {
+            $sourcepath = make_relative_ddf_path($sourcepath);
+        }
 
-        push(@cabfilelist, $oneline);
+        my $ddfline = "\"" . $sourcepath . "\"" . " " . $uniquename . "\n";
+        push(@ddffile, $ddfline);
 
-        # collecting all ddf files
-        push(@installer::globals::allddffiles, $ddffilename);
+        $installer::logger::Lang->printf("    adding '%s' with sequence %d to ddf\n",
+            $onefile->{'uniquename'},
+            $onefile->{'sequencenumber'});
     }
-    else
-    {
-        installer::exiter::exit_program("ERROR: No cab file specification in globals.pm !", "create_media_table");
-    }
-
-    $installer::logger::Lang->add_timestamp("Performance Info: ddf file generation end");
-
-    return \@cabfilelist;   # contains all system calls for packaging process
-}
-
-########################################################################
-# Returning the file sequence of a specified file.
-########################################################################
-
-sub get_file_sequence
-{
-    my ($filesref, $uniquefilename) = @_;
+    # creating the DDF file
 
-    my $sequence = "";
-    my $found_sequence = 0;
+    my $ddffilename = $cabinetfile;
+    $ddffilename =~ s/.cab/.ddf/;
+    $ddfdir =~ s/\Q$installer::globals::separator\E\s*$//;
+    $ddffilename = $ddfdir . $installer::globals::separator . $ddffilename;
 
-    for ( my $i = 0; $i <= $#{$filesref}; $i++ )
-    {
-        my $onefile = ${$filesref}[$i];
-        my $uniquename = $onefile->{'uniquename'};
+    installer::files::save_file($ddffilename ,\@ddffile);
+    $installer::logger::Lang->print("Created ddf file: %s\n", $ddffilename);
 
-        if ( $uniquename eq $uniquefilename )
-        {
-            $sequence = $onefile->{'sequencenumber'};
-            $found_sequence = 1;
-            last;
-        }
-    }
+    # lines in ddf files must not be longer than 256 characters
+    check_ddf_file(\@ddffile, $ddffilename);
 
-    if ( ! $found_sequence ) { installer::exiter::exit_program("ERROR: No sequence found for $uniquefilename !", "get_file_sequence"); }
+    # collecting all ddf files
+    push(@installer::globals::allddffiles, $ddffilename);
 
-    return $sequence;
+    # Writing the makecab system call
+    # Return a list with all system calls for packaging process.
+    my @cabfilelist = ("makecab.exe /V3 /F " . $ddffilename . " 2\>\&1 |" . "\n");
+    return \@cabfilelist;
 }
 
 
+
 #################################################################
 # Returning the name of the msi database
 #################################################################
diff --git a/solenv/bin/modules/installer/windows/property.pm b/solenv/bin/modules/installer/windows/property.pm
index e93ad04..e6b0d9b 100644
--- a/solenv/bin/modules/installer/windows/property.pm
+++ b/solenv/bin/modules/installer/windows/property.pm
@@ -208,16 +208,14 @@ sub get_productversion_for_property_table
 # required for the Windows patch process.
 #######################################################
 
-sub set_featurename_properties_for_patch
+sub set_featurename_properties_for_patch ($)
 {
-    ($propertyfile) = @_;
+    my ($propertyfile) = @_;
 
-    for ( my $i = 0; $i <= $#installer::globals::featurecollector; $i++ )
+    foreach my $feature_gid (keys %installer::globals::featurecollector)
     {
-        my $onepropertyline =  $installer::globals::featurecollector[$i] . "\t" . "1" . "\n";
-        push(@{$propertyfile}, $onepropertyline);
+        push @$propertyfile, $feature_gid . "\t" . "1" . "\n";
     }
-
 }
 
 #######################################################
diff --git a/solenv/bin/modules/installer/windows/registry.pm b/solenv/bin/modules/installer/windows/registry.pm
index f277e47..1352d28 100644
--- a/solenv/bin/modules/installer/windows/registry.pm
+++ b/solenv/bin/modules/installer/windows/registry.pm
@@ -47,7 +47,10 @@ sub get_registry_component_name
     my $componentname = "";
     my $isrootmodule = 0;
 
-    if ( $registryref->{'ModuleID'} ) { $componentname = $registryref->{'ModuleID'}; }
+    if ($registryref->{'ModuleID'})
+    {
+        $componentname = $registryref->{'ModuleID'};
+    }
 
     $componentname =~ s/\\/\_/g;
     $componentname =~ s/\//\_/g;
@@ -56,7 +59,10 @@ sub get_registry_component_name
 
     $componentname = lc($componentname);    # componentnames always lowercase
 
-    if ( $componentname eq "gid_module_root" ) { $isrootmodule = 1; }
+    if ( $componentname eq "gid_module_root" )
+    {
+        $isrootmodule = 1;
+    }
 
     # Attention: Maximum length for the componentname is 72
 
@@ -69,20 +75,36 @@ sub get_registry_component_name
 
     # This componentname must be more specific
     my $addon = "_";
-    if ( $allvariables->{'PRODUCTNAME'} ) { $addon = $addon . $allvariables->{'PRODUCTNAME'}; }
-    if ( $allvariables->{'PRODUCTVERSION'} ) { $addon = $addon . $allvariables->{'PRODUCTVERSION'}; }
+    if ($allvariables->{'PRODUCTNAME'})
+    {
+        $addon .= $allvariables->{'PRODUCTNAME'};
+    }
+
+    # Append the version number.
+    # Previously that was the full version number as provided by 'PRODUCTVERSION'.
+    # But MSI patches introduce the restriction that component names must not change.
+    # Use just the major version number.
+    my $version = $allvariables->{"BRANDPACKAGEVERSION"} // "";
+    $addon .= $version;
     $addon = lc($addon);
     $addon =~ s/ //g;
     $addon =~ s/-//g;
     $addon =~ s/\.//g;
 
-    my $styles = "";
-    if ( $registryref->{'Styles'} ) { $styles = $registryref->{'Styles'}; }
-
     $componentname = $componentname . $addon;
 
-    if (( $styles =~ /\bLANGUAGEPACK\b/ ) && ( $installer::globals::languagepack )) { $componentname = $componentname . "_lang"; }
-    if ( $styles =~ /\bALWAYS_REQUIRED\b/ ) { $componentname = $componentname . "_forced"; }
+    my $styles = $registryref->{'Styles'};
+    if (defined $styles)
+    {
+        if (($styles =~ /\bLANGUAGEPACK\b/) && $installer::globals::languagepack)
+        {
+            $componentname .= "_lang";
+        }
+        if ($styles =~ /\bALWAYS_REQUIRED\b/)
+        {
+            $componentname .= "_forced";
+        }
+    }
 
     # Attention: Maximum length for the componentname is 72
     # %installer::globals::allregistrycomponents_in_this_database_ : resetted for each database
@@ -92,10 +114,13 @@ sub get_registry_component_name
 
     my $fullname = $componentname;  # This can be longer than 72
 
-    if (( exists($installer::globals::allregistrycomponents_{$fullname}) ) && ( ! exists($installer::globals::allregistrycomponents_in_this_database_{$fullname}) ))
+    if (exists($installer::globals::allregistrycomponents_{$fullname})
+        && ! exists($installer::globals::allregistrycomponents_in_this_database_{$fullname}))
     {
         # This is not allowed: One component cannot be installed with different packages.
-        installer::exiter::exit_program("ERROR: Windows registry component \"$fullname\" is already included into another package. This is not allowed.", "get_registry_component_name");
+        installer::exiter::exit_program(
+            "ERROR: Windows registry component \"$fullname\" is already included into another package. This is not allowed.",
+            "get_registry_component_name");
     }
 
     if ( exists($installer::globals::allregistrycomponents_{$fullname}) )
@@ -113,7 +138,10 @@ sub get_registry_component_name
         $installer::globals::allregistrycomponents_in_this_database_{$fullname} = 1;
     }
 
-    if ( $isrootmodule ) { $installer::globals::registryrootcomponent = $componentname; }
+    if ( $isrootmodule )
+    {
+        $installer::globals::registryrootcomponent = $componentname;
+    }
 
     return $componentname;
 }
diff --git a/solenv/bin/patch_tool.pl b/solenv/bin/patch_tool.pl
index e490df5..c82806c 100644
--- a/solenv/bin/patch_tool.pl
+++ b/solenv/bin/patch_tool.pl
@@ -38,6 +38,8 @@ use installer::patch::Msi;
 use installer::patch::ReleasesList;
 use installer::patch::Version;
 
+#use Carp::Always;
+
 use strict;
 
 
@@ -64,6 +66,10 @@ use strict;
              The version that is to be patched.
         --target-version <major>.<minor>.<micro>
              The version after the patch has been applied.
+        --language <language-code>
+             Language of the installation sets.
+        --package-format
+             Only the package format 'msi' is supported at the moment.
 
 =head1 DESCRIPTION
 
@@ -80,7 +86,6 @@ use strict;
 
 =cut
 
-#    my $ImageFamily = "MNPapps";
 # The ImageFamily name has to have 1-8 alphanumeric characters.
 my $ImageFamily = "AOO";
 my $SourceImageName = "Source";
@@ -90,21 +95,26 @@ my $TargetImageName = "Target";
 
 sub ProcessCommandline ()
 {
-    my $arguments = {
+    my $context = {
         'product-name' => undef,
         'output-path' => undef,
         'data-path' => undef,
         'lst-file' => undef,
         'source-version' => undef,
-        'target-version' => undef};
+        'target-version' => undef,
+        'language' => undef,
+        'package-format' => 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'}
+               "product-name=s", \$context->{'product-name'},
+               "output-path=s", \$context->{'output-path'},
+               "data-path=s" => \$context->{'data-path'},
+               "lst-file=s" => \$context->{'lst-file'},
+               "source-version:s" => \$context->{'source-version'},
+               "target-version:s" => \$context->{'target-version'},
+               "language=s" => \$context->{'language'},
+               "package-format=s" => \$context->{'package-format'}
         ))
     {
         pod2usage(2);
@@ -112,14 +122,9 @@ sub ProcessCommandline ()
 
     # 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";
+    $context->{'command'} = shift @ARGV;
 
-    return $arguments;
+    return $context;
 }
 
 
@@ -181,46 +186,28 @@ sub ProvideInstallationSets ($$)
 
 
 
-sub GetLanguages ()
+sub IsLanguageValid ($$$)
 {
-    # 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;
-}
-
-
+    my ($context, $release_data, $language) = @_;
 
+    my $normalized_language = installer::languages::get_normalized_language($language);
 
-sub FindValidLanguages ($$$)
-{
-    my ($context, $release_data, $languages) = @_;
-
-    my @valid_languages = ();
-    foreach my $language (@$languages)
+    if ( ! ProvideInstallationSets($context, $language))
     {
-        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;
-        }
+        installer::logger::PrintError("    '%s' has no target installation set\n", $language);
+        return 0;
+    }
+    elsif ( ! defined $release_data->{$normalized_language})
+    {
+        installer::logger::PrintError("    '%s' is not a released language for version %s\n",
+            $language,
+            $context->{'source-version'});
+        return 0;
+    }
+    else
+    {
+        return 1;
     }
-
-    return @valid_languages;
 }
 
 
@@ -327,6 +314,19 @@ sub DetermineVersions ($$)
         }
         $context->{'source-version'} = $last_release;
     }
+
+    if (defined $context->{'source-version'})
+    {
+        $context->{'source-version-dash'} = installer::patch::Version::ArrayToDirectoryName(
+            installer::patch::Version::StringToNumberArray(
+                $context->{'source-version'}));
+    }
+    if (defined $context->{'target-version'})
+    {
+        $context->{'target-version-dash'} = installer::patch::Version::ArrayToDirectoryName(
+            installer::patch::Version::StringToNumberArray(
+                $context->{'target-version'}));
+    }
 }
 
 
@@ -352,7 +352,7 @@ sub CheckUpgradeCode($$)
     }
     else
     {
-        $installer::logger::Info->printf("OK: UpgradeCode values are identical\n");
+        $installer::logger::Info->printf("OK: UpgradeCode values are different\n");
         return 1;
     }
 }
@@ -382,7 +382,7 @@ sub CheckProductCode($$)
     }
     else
     {
-        $installer::logger::Info->printf("OK: ProductCode properties differ\n");
+        $installer::logger::Info->printf("OK: ProductCodes are identical\n");
         return 1;
     }
 }
@@ -501,25 +501,60 @@ sub CheckNewFiles($$)
 
     # 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()};
+    my %target_files_map = 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)
+    my @added_files = ();
+    foreach my $uniquename (keys %target_files_map)
     {
         if ( ! defined $source_file_map{$uniquename})
         {
-            ++$added_file_count;
+            push @added_files, $target_files_map{$uniquename};
         }
     }
 
-    if ($added_file_count > 0)
+    if (scalar @added_files > 0)
     {
-        $installer::logger::Info->printf("Warning: %d files have been added\n", $added_file_count);
+        $installer::logger::Info->printf("Warning: %d files have been added\n", scalar @added_files);
 
-        $installer::logger::Info->printf("Check for new files being part of new components is not yet implemented\n");
+        # Prepare component tables and hashes.
+        my $source_component_table = $source_msi->GetTable("Component");
+        my $target_component_table = $target_msi->GetTable("Component");
+        die unless defined $source_component_table && defined $target_component_table;
+        my %source_component_map = map {$_->GetValue('Component') => $_} @{$source_component_table->GetAllRows()};
+        my %target_component_map = map {$_->GetValue('Component') => $_} @{$target_component_table->GetAllRows()};
 
-        return 1;
+        my @new_files_with_existing_components = ();
+        foreach my $target_file_row (@added_files)
+        {
+        $installer::logger::Info->printf("    %s (%s)\n",
+        $target_file_row->GetValue("FileName"),
+        $target_file_row->GetValue("File"));
+
+            # Get target component for target file.
+            my $target_component = $target_file_row->GetValue('Component_');
+
+            # Check that the component is not part of the source components.
+            if (defined $source_component_map{$target_component})
+            {
+                push @new_files_with_existing_components, $target_file_row;
+            }
+        }
+
+        if (scalar @new_files_with_existing_components > 0)
+        {
+            $installer::logger::Info->printf(
+                "Error: %d new files have existing components (which must also be new)\n",
+                scalar @new_files_with_existing_components);
+            return 0;
+        }
+        else
+        {
+            $installer::logger::Info->printf(
+                "OK: all %d new files also have new components\n",
+        scalar @added_files);
+            return 1;
+        }
     }
     else
     {
@@ -531,13 +566,96 @@ sub CheckNewFiles($$)
 
 
 
-=head2 CheckComponentSets($source_msi, $target_msi)
+=head2 CheckFeatureSets($source_msi, $target_msi)
+
+    Features must not be removed but can be added.
+    Parent features of new features also have to be new.
+
+=cut
+sub CheckFeatureSets($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    # Get the 'Feature' tables.
+    my $source_feature_table = $source_msi->GetTable("Feature");
+    my $target_feature_table = $target_msi->GetTable("Feature");
+
+    # Create data structures for fast lookup.
+    my %source_feature_map = map {$_->GetValue("Feature") => $_} @{$source_feature_table->GetAllRows()};
+    my %target_feature_map = map {$_->GetValue("Feature") => $_} @{$target_feature_table->GetAllRows()};
+
+    # Check that no feature has been removed.
+    my @removed_features = ();
+    foreach my $feature_name (keys %source_feature_map)
+    {
+        if ( ! defined $target_feature_map{$feature_name})
+        {
+            push @removed_features, $feature_name;
+        }
+    }
+    if (scalar @removed_features > 0)
+    {
+        # There are removed features.
+        $installer::logger::Info->printf(
+            "Error: %d features have been removed:\n",
+            scalar @removed_features);
+        $installer::logger::Info->printf("       %s\n", join(", ", @removed_features));
+        return 0;
+    }
+
+    # Check that added features belong to new parent features.
+    my @added_features = ();
+    foreach my $feature_name (keys %target_feature_map)
+    {
+        if ( ! defined $source_feature_map{$feature_name})
+        {
+            push @added_features, $feature_name;
+        }
+    }
+
+    if (scalar @added_features > 0)
+    {
+        $installer::logger::Info->printf("Warning: %d features have been addded\n", scalar @added_features);
+
+        my @new_features_with_existing_parents = ();
+        foreach my $new_feature (@added_features)
+        {
+            my $target_feature = $target_feature_map{$new_feature};
+            if (defined $source_feature_map{$target_feature->{'Feature_Parent'}})
+            {
+                push @new_features_with_existing_parents, $target_feature;
+            }
+        }
+
+        if (scalar @new_features_with_existing_parents > 0)
+        {
+            $installer::logger::Info->printf(
+                "Error: %d new features have existing parents (which also must be new)\n",
+                scalar @new_features_with_existing_parents);
+            return 0;
+        }
+        else
+        {
+            $installer::logger::Info->printf(
+                "OK: parents of all new features are also new\n");
+            return 1;
+        }
+    }
+
+    $installer::logger::Info->printf("OK: feature sets in source and target are compatible\n");
+    return 1;
+}
+
+
+
+
+=head2 CheckRemovedComponents($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($$)
+sub CheckRemovedComponents ($$)
 {
     my ($source_msi, $target_msi) = @_;
 
@@ -558,7 +676,12 @@ sub CheckComponentSets($$)
             push @removed_components, $componentname;
         }
     }
-    if (scalar @removed_components > 0)
+    if (scalar @removed_components == 0)
+    {
+    $installer::logger::Info->printf("OK: no removed components\n");
+    return 1;
+    }
+    else
     {
         # There are removed components.
 
@@ -587,48 +710,115 @@ sub CheckComponentSets($$)
             return 0;
         }
     }
+}
+
+
+
+
+sub GetTableAndMap ($$$)
+{
+    my ($msi, $table_name, $index_column) = @_;
+
+    my $table = $msi->GetTable($table_name);
+    my %map = map {$_->GetValue($index_column) => $_} @{$table->GetAllRows()};
+
+    return ($table, \%map);
+}
+
+
+=head2 CheckAddedComponents($source_msi, $target_msi)
+
+    Components can be added.
+    Features of added components have also to be new.
+
+=cut
+sub CheckAddedComponents ($$)
+{
+    my ($source_msi, $target_msi) = @_;
+
+    # Get the 'Component' tables and maps.
+    my ($source_component_table, $source_component_map)
+    = GetTableAndMap($source_msi, "Component", "Component");
+    my ($target_component_table, $target_component_map)
+    = GetTableAndMap($target_msi, "Component", "Component");
 
     # Check that added components belong to new features.
     my @added_components = ();
-    foreach my $componentname (keys %target_component_map)
+    foreach my $componentname (keys %$target_component_map)
     {
-        if ( ! defined $source_component_map{$componentname})
+        if ( ! defined $source_component_map->{$componentname})
         {
             push @added_components, $componentname;
         }
     }
 
-    if (scalar @added_components > 0)
+    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)
+    $installer::logger::Info->printf("OK: no new components\n");
+    return 1;
+    }
+    else
+    {
+    $installer::logger::Info->printf(
+        "Warning: %d components have been addded\n",
+        scalar @added_components);
+
+        # Check that the referencing features are also new.
+    my $target_feature_component_table = $target_msi->GetTable("FeatureComponents");
+
+    my $error = 0;
+        foreach my $component_name (@added_components)
         {
-            if ($componentname !~ /^registry/)
-            {
-                $is_file_component_removed = 1;
-            }
+        my @feature_names = ();
+        foreach my $feature_component_row (@{$target_feature_component_table->GetAllRows()})
+        {
+        if ($feature_component_row->GetValue("Component_") eq $component_name)
+        {
+            my $feature_name = $feature_component_row->GetValue("Feature_");
+            push @feature_names, $feature_name;
         }
-
-        if ($is_file_component_removed)
+        }
+        if (scalar @feature_names == 0)
         {
-            $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;
+        $installer::logger::Info->printf("Error: no feature found for component '%s'\n", $component_name);
+        $error = 1;
         }
         else
         {
-            $installer::logger::Info->printf(
-                "Warning: %d components have been addded, all of them registry components\n",
-                scalar @added_components);
+        # Check that the referenced features are new and have new parents (if they have parents).
+        my ($source_feature_table, $source_feature_map)
+            = GetTableAndMap($source_msi, "Feature", "Feature");
+        my ($target_feature_table, $target_feature_map)
+            = GetTableAndMap($target_msi, "Feature", "Feature");
+        foreach my $feature_name (@feature_names)
+        {
+            $installer::logger::Info->printf("    component '%s' -> feature '%s'\n",
+            $component_name,
+            $feature_name);
+            my $source_feature_row = $source_feature_map->{$feature_name};
+            if (defined $source_feature_row)
+            {
+            $installer::logger::Info->printf("Warning(Error?): feature of new component is not new\n");
+            $error = 1;
+            }
+            else
+            {
+            # Feature is new. Check that the parent feature is also new.
+            my $target_feature_row = $target_feature_map->{$feature_name};
+            my $parent_feature_name = $target_feature_row->GetValue("Feature_Parent");
+            if ($parent_feature_name ne "" && defined $source_feature_map->{$parent_feature_name})
+            {
+                $installer::logger::Info->printf("Warning(Error?): parent feature of new component is not new\n");
+                $error = 1;
+            }
+            }
+        }
         }
     }
 
-    $installer::logger::Info->printf("OK: component sets in source and target are compatible\n");
+#   return !$error;
     return 1;
+    }
 }
 
 
@@ -962,6 +1152,131 @@ sub CheckComponentKeyPath ($$)
 
 
 
+sub GetMissingReferences ($$$$$)
+{
+    my ($table, $key, $map, $what, $report_key) = @_;
+
+    my @missing_references = ();
+
+    foreach my $row (@{$table->GetAllRows()})
+    {
+        my $value = $row->GetValue($key);
+        if ($value ne "" && ! defined $map->{$value})
+        {
+            push @missing_references, [$what, $row->GetValue($report_key), $value];
+        }
+    }
+
+    return @missing_references;
+}
+
+
+
+
+=head CheckAllReferences ($msi)
+
+    Check references from files and registry entries to components,
+    from components to features, and between features.
+
+=cut
+
+sub CheckAllReferences ($)
+{
+    my ($msi) = @_;
+
+    # Set up tables and maps for easy iteration and fast lookups.
+
+    my $feature_table = $msi->GetTable("Feature");
+    my $component_table = $msi->GetTable("Component");
+    my $feature_component_table = $msi->GetTable("FeatureComponents");
+    my $file_table = $msi->GetTable("File");
+    my $registry_table = $msi->GetTable("Registry");
+    my $directory_table = $msi->GetTable("Directory");
+
+    my %feature_map = map {$_->GetValue("Feature") => $_} @{$feature_table->GetAllRows()};
+    my %component_map = map {$_->GetValue("Component") => $_} @{$component_table->GetAllRows()};
+    my %directory_map = map {$_->GetValue("Directory") => $_} @{$directory_table->GetAllRows()};
+
+    my @missing_references = ();
+
+    # Check references from files and registry entries to components.
+    push @missing_references, GetMissingReferences(
+        $file_table,
+        "Component_",
+        \%component_map,
+        "file->component",
+        "File");
+    push @missing_references, GetMissingReferences(
+        $registry_table,
+        "Component_",
+        \%component_map,
+        "registry->component",
+        "Registry");
+
+    # Check references between features and components.
+    push @missing_references, GetMissingReferences(
+        $feature_component_table,
+        "Feature_",
+        \%feature_map,
+        "component->feature",
+        "Component_");
+    push @missing_references, GetMissingReferences(
+        $feature_component_table,
+        "Component_",
+        \%component_map,
+        "feature->component",
+        "Feature_");
+
+    # Check references between features.
+    push @missing_references, GetMissingReferences(
+        $feature_table,
+        'Feature_Parent',
+        \%feature_map,
+        "feature->feature",
+        'Feature');
+
+    # Check references between directories.
+    push @missing_references, GetMissingReferences(
+        $directory_table,
+        'Directory_Parent',
+        \%directory_map,
+        "directory->directory",
+        'Directory');
+
+    # Check references from components to directories.
+    push @missing_references, GetMissingReferences(
+        $component_table,
+        'Directory_',
+        \%directory_map,
+        "component->directory",
+        'Component');
+
+    # Check references from components to files (via the .
+
+    # Report the result.
+    if (scalar @missing_references > 0)
+    {
+        $installer::logger::Info->printf("Error: there are %d missing references\n", scalar @missing_references);
+        foreach my $reference (@missing_references)
+        {
+            $installer::logger::Info->printf("    %s : %s -> %s\n",
+                $reference->[0],
+                $reference->[1],
+                $reference->[2]);
+        }
+        return 0;
+    }
+    else
+    {
+        $installer::logger::Info->printf("OK: all references are OK\n");
+        return 1;
+
+    }
+}
+
+
+
+
 sub Check ($$$$)
 {
     my ($source_msi, $target_msi, $variables, $product_name) = @_;
@@ -971,22 +1286,37 @@ sub Check ($$$$)
 
     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);
+    # Using &= below to avoid lazy evaluation.  Even if there are errors, all checks shall be run.
+    $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 &= CheckFeatureSets($source_msi, $target_msi);
+    $result &= CheckRemovedComponents($source_msi, $target_msi);
+    $result &= CheckAddedComponents($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);
+    $result &= CheckAllReferences($target_msi);
 
     $installer::logger::Info->decrease_indentation();
 
+    if ($result)
+    {
+        $installer::logger::Info->printf("OK: Source and target releases are compatible.\n");
+    }
+    else
+    {
+        $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");
+    }
+
     return $result;
 }
 
@@ -1232,8 +1562,8 @@ sub CreatePcp ($$$$$$%)
     }
     my $pcp = installer::patch::Msi->new(
         $pcp_filename,
-        undef,
-        undef,
+        $target_msi->{'version'},
+        $target_msi->{'is_current_version'},
         $language,
         $context->{'product-name'});
 
@@ -1281,7 +1611,6 @@ sub ShowLog ($$$$)
             "/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)
         {
@@ -1306,8 +1635,8 @@ sub ShowLog ($$$$)
             close $in;
             close $out;
 
-            my $URL = $new_name;
-            $URL =~ s/\/c\//c|\//;
+            my $URL = File::Spec->rel2abs($new_name);
+            $URL =~ s/\/cygdrive\/(.)\//$1|\//;
             $URL =~ s/^(.):/$1|/;
             $URL = "file:///". $URL;
             $installer::logger::Info->printf("open %s in your browser to see the log messages\n", $URL);
@@ -1344,13 +1673,17 @@ sub CreateMsp ($)
             || die ("can not create temporary path ".$temporary_msimsp_path);
     }
     $installer::logger::Info->printf("running msimsp.exe, that will take a while\n");
+    my $create_performance_log = 0;
     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),
+    if ($create_performance_log)
+    {
+        $command .= " -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);
@@ -1361,7 +1694,60 @@ sub CreateMsp ($)
 
     # 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");
+    if ($create_performance_log)
+    {
+        ShowLog($log_path, $performance_log_filename, $performance_log_basename, "msp creation perf");
+    }
+}
+
+
+sub ProvideMsis ($$$)
+{
+    my ($context, $variables, $language) = @_;
+
+    # 2a. Provide .msi and .cab files and unpack .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 defined $source_msi;
+    die unless $source_msi->IsValid();
+    $installer::logger::Info->decrease_indentation();
+
+    # 2b. Provide .msi and .cab files and unpacked .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();
+
+    return ($source_msi, $target_msi);
 }
 
 
@@ -1400,57 +1786,23 @@ sub CreatePatch ($$)
         ->{$context->{'package-format'}};
 
     # 1. Determine the set of languages for which we can create patches.
-    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)
+    my $language = $context->{'language'};
+    my %no_ms_lang_locale_map = map {$_=>1} @installer::globals::noMSLocaleLangs;
+    if (defined $no_ms_lang_locale_map{$language})
     {
-        $installer::logger::Info->printf("processing language '%s'\n", $language);
-        $installer::logger::Info->increase_indentation();
+        $language = "en-US_".$language;
+    }
 
-        # 2a. Provide .msi and .cab files and unpacke .cab for the source release.
-        $installer::logger::Info->printf("locating source package (%s)\n", $context->{'source-version'});
+    if ( ! IsLanguageValid($context, $release_data, $language))
+    {
+        $installer::logger::Info->printf("can not create patch for language '%s'\n", $language);
+    }
+    else
+    {
+        $installer::logger::Info->printf("processing language '%s'\n", $language);
         $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();
-
-        # 2b. 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();
+        my ($source_msi, $target_msi) = ProvideMsis($context, $variables, $language);
 
         # Trigger reading of tables.
         foreach my $table_name (("File", "Component", "Registry"))
@@ -1463,15 +1815,8 @@ sub CreatePatch ($$)
         # 3. 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(
@@ -1479,12 +1824,12 @@ sub CreatePatch ($$)
             $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'}))),
+                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;
@@ -1508,6 +1853,58 @@ sub CreatePatch ($$)
 
 
 
+
+sub CheckPatchCompatability ($$)
+{
+    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'});
+
+    my $release_data = installer::patch::ReleasesList::Instance()
+        ->{$context->{'source-version'}}
+        ->{$context->{'package-format'}};
+
+    # 1. Determine the set of languages for which we can create patches.
+    my $language = $context->{'language'};
+    my %no_ms_lang_locale_map = map {$_=>1} @installer::globals::noMSLocaleLangs;
+    if (defined $no_ms_lang_locale_map{$language})
+    {
+        $language = "en-US_".$language;
+    }
+
+    if ( ! IsLanguageValid($context, $release_data, $language))
+    {
+        $installer::logger::Info->printf("can not create patch for language '%s'\n", $language);
+    }
+    else
+    {
+        $installer::logger::Info->printf("processing language '%s'\n", $language);
+        $installer::logger::Info->increase_indentation();
+
+        my ($source_msi, $target_msi) = ProvideMsis($context, $variables, $language);
+
+        # 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);
+        }
+
+        # 3. Check if the source and target msis fullfil all necessary requirements.
+        if ( ! Check($source_msi, $target_msi, $variables, $context->{'product-name'}))
+        {
+            exit(1);
+        }
+    }
+}
+
+
+
+
 =cut ApplyPatch ($context, $variables)
 
     This is for testing only.
@@ -1522,7 +1919,6 @@ sub ApplyPatch ($$)
         $context->{'product-name'},
         $context->{'source-version'},
         $context->{'target-version'});
-    my @languages = GetLanguages();
 
     my $source_version_dirname = installer::patch::Version::ArrayToDirectoryName(
       installer::patch::Version::StringToNumberArray(
@@ -1531,41 +1927,45 @@ sub ApplyPatch ($$)
       installer::patch::Version::StringToNumberArray(
           $context->{'target-version'}));
 
-    foreach my $language (@languages)
+    my $language = $context->{'language'};
+    my %no_ms_lang_locale_map = map {$_=>1} @installer::globals::noMSLocaleLangs;
+    if (defined $no_ms_lang_locale_map{$language})
     {
-        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;
-        }
+        $language = "en-US_".$language;
+    }
+
+    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 $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",
+    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");
+        "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);
+    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");
-    }
+    ShowLog($log_path, $log_filename, $log_basename, "msp application");
 }
 
 
@@ -1802,12 +2202,17 @@ sub UpdateReleasesXML($$)
 
 sub main ()
 {
-    installer::logger::SetupSimpleLogging(undef);
     my $context = ProcessCommandline();
+    installer::logger::starttime();
+    $installer::logger::Global->add_timestamp("starting logging");
+#    installer::logger::SetupSimpleLogging(undef);
+
     die "ERROR: list file is not defined, please use --lst-file option"
         unless defined $context->{'lst-file'};
     die "ERROR: product name is not defined, please use --product-name option"
         unless defined $context->{'product-name'};
+    die sprintf("ERROR: package format %s is not supported", $context->{'package-format'})
+        unless defined $context->{'package-format'} ne "msi";
 
     my ($variables, undef, undef) = installer::ziplist::read_openoffice_lst_file(
         $context->{'lst-file'},
@@ -1815,6 +2220,22 @@ sub main ()
         undef);
     DetermineVersions($context, $variables);
 
+    if ($context->{'command'} =~ /create|check/)
+    {
+        $installer::logger::Lang->set_filename(
+            File::Spec->catfile(
+                $context->{'output-path'},
+                $context->{'product-name'},
+                "msp",
+                $context->{'source-version-dash'} . "_" . $context->{'target-version-dash'},
+                $context->{'language'},
+                "log",
+                "patch-creation.log"));
+        $installer::logger::Lang->copy_lines_from($installer::logger::Global);
+        $installer::logger::Lang->set_forward(undef);
+        $installer::logger::Info->set_forward($installer::logger::Lang);
+    }
+
     if ($context->{'command'} eq "create")
     {
         CreatePatch($context, $variables);
@@ -1827,6 +2248,10 @@ sub main ()
     {
         UpdateReleasesXML($context, $variables);
     }
+    elsif ($context->{'command'} eq "check")
+    {
+        CheckPatchCompatability($context, $variables);
+    }
 }
 
 
diff --git a/solenv/bin/release_prepare.pl b/solenv/bin/release_prepare.pl
index 1d22ad7..432adf2 100644
--- a/solenv/bin/release_prepare.pl
+++ b/solenv/bin/release_prepare.pl
@@ -32,7 +32,7 @@ use Getopt::Long;
 use Pod::Usage;
 use Digest;
 
-use Carp::Always;
+#use Carp::Always;
 
 use strict;
 


More information about the Libreoffice-commits mailing list