xref: /trunk/main/solenv/bin/patch_tool.pl (revision a9bc0cf5)
1#!/usr/bin/perl -w
2
3#**************************************************************
4#
5#  Licensed to the Apache Software Foundation (ASF) under one
6#  or more contributor license agreements.  See the NOTICE file
7#  distributed with this work for additional information
8#  regarding copyright ownership.  The ASF licenses this file
9#  to you under the Apache License, Version 2.0 (the
10#  "License"); you may not use this file except in compliance
11#  with the License.  You may obtain a copy of the License at
12#
13#    http://www.apache.org/licenses/LICENSE-2.0
14#
15#  Unless required by applicable law or agreed to in writing,
16#  software distributed under the License is distributed on an
17#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18#  KIND, either express or implied.  See the License for the
19#  specific language governing permissions and limitations
20#  under the License.
21#
22#**************************************************************
23
24use Getopt::Long;
25use Pod::Usage;
26use File::Path;
27use File::Spec;
28use File::Basename;
29use XML::LibXML;
30use Digest;
31use Archive::Zip;
32use Archive::Extract;
33
34use installer::ziplist;
35use installer::logger;
36use installer::windows::msiglobal;
37use installer::patch::Msi;
38use installer::patch::ReleasesList;
39use installer::patch::Version;
40
41#use Carp::Always;
42
43use strict;
44
45
46=head1 NAME
47
48    patch_tool.pl - Create Windows MSI patches.
49
50=head1 SYNOPSIS
51
52    patch_tool.pl command [options]
53
54    Commands:
55        create    create patches
56        apply     apply patches
57
58    Options:
59        -p|--product-name <product-name>
60             The product name, eg Apache_OpenOffice
61        -o|--output-path <path>
62             Path to the instsetoo_native platform output tree
63        -d|--data-path <path>
64             Path to the data directory that is expected to be under version control.
65        --source-version <major>.<minor>.<micro>
66             The version that is to be patched.
67        --target-version <major>.<minor>.<micro>
68             The version after the patch has been applied.
69        --language <language-code>
70             Language of the installation sets.
71        --package-format
72             Only the package format 'msi' is supported at the moment.
73
74=head1 DESCRIPTION
75
76    Creates windows MSP patch files, one for each relevant language.
77    Patches convert an installed OpenOffice to the target version.
78
79    Required data are:
80        Installation sets of the source versions
81            Taken from ext_sources/
82            Downloaded from archive.apache.org on demand
83
84        Installation set of the target version
85            This is expected to be the current version.
86
87=cut
88
89# The ImageFamily name has to have 1-8 alphanumeric characters.
90my $ImageFamily = "AOO";
91my $SourceImageName = "Source";
92my $TargetImageName = "Target";
93
94
95
96sub ProcessCommandline ()
97{
98    my $context = {
99        'product-name' => undef,
100        'output-path' => undef,
101        'data-path' => undef,
102        'lst-file' => undef,
103        'source-version' => undef,
104        'target-version' => undef,
105        'language' => undef,
106        'package-format' => undef
107    };
108
109    if ( ! GetOptions(
110               "product-name=s", \$context->{'product-name'},
111               "output-path=s", \$context->{'output-path'},
112               "data-path=s" => \$context->{'data-path'},
113               "lst-file=s" => \$context->{'lst-file'},
114               "source-version:s" => \$context->{'source-version'},
115               "target-version:s" => \$context->{'target-version'},
116               "language=s" => \$context->{'language'},
117               "package-format=s" => \$context->{'package-format'}
118        ))
119    {
120        pod2usage(2);
121    }
122
123    # Only the command should be left in @ARGV.
124    pod2usage(2) unless scalar @ARGV == 1;
125    $context->{'command'} = shift @ARGV;
126
127    return $context;
128}
129
130
131
132
133sub GetSourceMsiPath ($$)
134{
135    my ($context, $language) = @_;
136    my $unpacked_path = File::Spec->catfile(
137	$context->{'output-path'},
138	$context->{'product-name'},
139        $context->{'package-format'},
140	installer::patch::Version::ArrayToDirectoryName(
141	    installer::patch::Version::StringToNumberArray(
142		$context->{'source-version'})),
143	$language);
144}
145
146
147
148
149sub GetTargetMsiPath ($$)
150{
151    my ($context, $language) = @_;
152    return File::Spec->catfile(
153        $context->{'output-path'},
154        $context->{'product-name'},
155        $context->{'package-format'},
156        "install",
157        $language);
158}
159
160
161
162sub ProvideInstallationSets ($$)
163{
164    my ($context, $language) = @_;
165
166    # Assume that the target installation set is located in the output tree.
167    my $target_path = GetTargetMsiPath($context, $language);
168    if ( ! -d $target_path)
169    {
170        installer::logger::PrintError("can not find target installation set at '%s'\n", $target_path);
171        return 0;
172    }
173    my @target_version = installer::patch::Version::StringToNumberArray($context->{'target-version'});
174    my $target_msi_file = File::Spec->catfile(
175        $target_path,
176        sprintf("openoffice%d%d%d.msi", $target_version[0], $target_version[1], $target_version[2]));
177    if ( ! -f $target_msi_file)
178    {
179        installer::logger::PrintError("can not find target msi file at '%s'\n", $target_msi_file);
180        return 0;
181    }
182
183    return 1;
184}
185
186
187
188
189sub IsLanguageValid ($$$)
190{
191    my ($context, $release_data, $language) = @_;
192
193    my $normalized_language = installer::languages::get_normalized_language($language);
194
195    if ( ! ProvideInstallationSets($context, $language))
196    {
197        installer::logger::PrintError("    '%s' has no target installation set\n", $language);
198        return 0;
199    }
200    elsif ( ! defined $release_data->{$normalized_language})
201    {
202        installer::logger::PrintError("    '%s' is not a released language for version %s\n",
203            $language,
204            $context->{'source-version'});
205        return 0;
206    }
207    else
208    {
209        return 1;
210    }
211}
212
213
214
215
216sub ProvideSourceInstallationSet ($$$)
217{
218    my ($context, $language, $release_data) = @_;
219
220    my $url = $release_data->{$language}->{'URL'};
221    $url =~ /^(.*)\/([^\/]*)$/;
222    my ($location, $basename) = ($1,$2);
223
224    my $ext_sources_path = $ENV{'TARFILE_LOCATION'};
225    if ( ! -d $ext_sources_path)
226    {
227        installer::logger::PrintError("Can not determine the path to ext_sources/.\n");
228        installer::logger::PrintError("Maybe SOURCE_ROOT_DIR has not been correctly set in the environment?");
229        return 0;
230    }
231
232    # We need the unpacked installation set in <platform>/<product>/<package>/<source-version>,
233    # eg wntmsci12.pro/Apache_OpenOffice/msi/v-4-0-0.
234    my $unpacked_path = GetSourceMsiPath($context, $language);
235    if ( ! -d $unpacked_path)
236    {
237        # Make sure that the downloadable installation set (.exe) is present in ext_sources/.
238        my $filename = File::Spec->catfile($ext_sources_path, $basename);
239        if ( -f $filename)
240        {
241            PrintInfo("%s is already present in ext_sources/.  Nothing to do\n", $basename);
242        }
243        else
244        {
245            return 0 if ! installer::patch::InstallationSet::Download(
246                $language,
247                $release_data,
248                $filename);
249            return 0 if ! -f $filename;
250        }
251
252        # Unpack the installation set.
253        if ( -d $unpacked_path)
254        {
255            # Take the existence of the destination path as proof that the
256            # installation set was successfully unpacked before.
257        }
258        else
259        {
260            installer::patch::InstallationSet::Unpack($filename, $unpacked_path);
261        }
262    }
263}
264
265
266
267
268# Find the source and target version between which the patch will be
269# created.  Typically the target version is the current version and
270# the source version is the version of the previous release.
271sub DetermineVersions ($$)
272{
273    my ($context, $variables) = @_;
274
275    if (defined $context->{'source-version'} && defined $context->{'target-version'})
276    {
277        # Both source and target version have been specified on the
278        # command line. There remains nothing to be done.
279        return;
280    }
281
282    if ( ! defined $context->{'target-version'})
283    {
284        # Use the current version as target version.
285        $context->{'target-version'} = $variables->{PRODUCTVERSION};
286    }
287
288    my @target_version = installer::patch::Version::StringToNumberArray($context->{'target-version'});
289    shift @target_version;
290    my $is_target_version_major = 1;
291    foreach my $number (@target_version)
292    {
293        $is_target_version_major = 0 if ($number ne "0");
294    }
295    if ($is_target_version_major)
296    {
297        installer::logger::PrintError("can not create patch where target version is a new major version (%s)\n",
298            $context->{'target-version'});
299        die;
300    }
301
302    if ( ! defined $context->{'source-version'})
303    {
304        my $releases = installer::patch::ReleasesList::Instance();
305
306        # Search for target release in the list of previous releases.
307        # If it is found, use the previous version as source version.
308        # Otherwise use the last released version.
309        my $last_release = undef;
310        foreach my $release (@{$releases->{'releases'}})
311        {
312            last if ($release eq $context->{'target-version'});
313            $last_release = $release;
314        }
315        $context->{'source-version'} = $last_release;
316    }
317
318    if (defined $context->{'source-version'})
319    {
320        $context->{'source-version-dash'} = installer::patch::Version::ArrayToDirectoryName(
321            installer::patch::Version::StringToNumberArray(
322                $context->{'source-version'}));
323    }
324    if (defined $context->{'target-version'})
325    {
326        $context->{'target-version-dash'} = installer::patch::Version::ArrayToDirectoryName(
327            installer::patch::Version::StringToNumberArray(
328                $context->{'target-version'}));
329    }
330}
331
332
333
334
335=head2 CheckUpgradeCode($source_msi, $target_msi)
336
337    The 'UpgradeCode' values in the 'Property' table differs from source to target
338
339=cut
340sub CheckUpgradeCode($$)
341{
342    my ($source_msi, $target_msi) = @_;
343
344    my $source_upgrade_code = $source_msi->GetTable("Property")->GetValue("Property", "UpgradeCode", "Value");
345    my $target_upgrade_code = $target_msi->GetTable("Property")->GetValue("Property", "UpgradeCode", "Value");
346
347    if ($source_upgrade_code eq $target_upgrade_code)
348    {
349        $installer::logger::Info->printf("Error: The UpgradeCode properties have to differ but are both '%s'\n",
350            $source_upgrade_code);
351        return 0;
352    }
353    else
354    {
355        $installer::logger::Info->printf("OK: UpgradeCode values are different\n");
356        return 1;
357    }
358}
359
360
361
362
363=head2 CheckProductCode($source_msi, $target_msi)
364
365    The 'ProductCode' values in the 'Property' tables remain the same.
366
367=cut
368sub CheckProductCode($$)
369{
370    my ($source_msi, $target_msi) = @_;
371
372    my $source_product_code = $source_msi->GetTable("Property")->GetValue("Property", "ProductCode", "Value");
373    my $target_product_code = $target_msi->GetTable("Property")->GetValue("Property", "ProductCode", "Value");
374
375    if ($source_product_code ne $target_product_code)
376    {
377        $installer::logger::Info->printf("Error: The ProductCode properties have to remain the same but are\n");
378        $installer::logger::Info->printf("       '%s' and '%s'\n",
379            $source_product_code,
380            $target_product_code);
381        return 0;
382    }
383    else
384    {
385        $installer::logger::Info->printf("OK: ProductCodes are identical\n");
386        return 1;
387    }
388}
389
390
391
392
393=head2 CheckBuildIdCode($source_msi, $target_msi)
394
395    The 'PRODUCTBUILDID' values in the 'Property' tables (not the AOO build ids) differ and the
396    target value is higher than the source value.
397
398=cut
399sub CheckBuildIdCode($$)
400{
401    my ($source_msi, $target_msi) = @_;
402
403    my $source_build_id = $source_msi->GetTable("Property")->GetValue("Property", "PRODUCTBUILDID", "Value");
404    my $target_build_id = $target_msi->GetTable("Property")->GetValue("Property", "PRODUCTBUILDID", "Value");
405
406    if ($source_build_id >= $target_build_id)
407    {
408        $installer::logger::Info->printf(
409            "Error: The PRODUCTBUILDID properties have to increase but are '%s' and '%s'\n",
410            $source_build_id,
411            $target_build_id);
412        return 0;
413    }
414    else
415    {
416        $installer::logger::Info->printf("OK: source build id is lower than target build id\n");
417        return 1;
418    }
419}
420
421
422
423
424sub CheckProductName ($$)
425{
426    my ($source_msi, $target_msi) = @_;
427
428    my $source_product_name = $source_msi->GetTable("Property")->GetValue("Property", "DEFINEDPRODUCT", "Value");
429    my $target_product_name = $target_msi->GetTable("Property")->GetValue("Property", "DEFINEDPRODUCT", "Value");
430
431    if ($source_product_name ne $target_product_name)
432    {
433        $installer::logger::Info->printf("Error: product names of are not identical:\n");
434        $installer::logger::Info->printf("       %s != %s\n", $source_product_name, $target_product_name);
435        return 0;
436    }
437    else
438    {
439        $installer::logger::Info->printf("OK: product names are identical\n");
440        return 1;
441    }
442}
443
444
445
446
447=head2 CheckRemovedFiles($source_msi, $target_msi)
448
449    Files and components must not be deleted.
450
451=cut
452sub CheckRemovedFiles($$)
453{
454    my ($source_msi, $target_msi) = @_;
455
456    # Get the 'File' tables.
457    my $source_file_table = $source_msi->GetTable("File");
458    my $target_file_table = $target_msi->GetTable("File");
459
460    # Create data structures for fast lookup.
461    my @source_files = map {$_->GetValue("File")} @{$source_file_table->GetAllRows()};
462    my %target_file_map = map {$_->GetValue("File") => $_} @{$target_file_table->GetAllRows()};
463
464    # Search for removed files (files in source that are missing from target).
465    my $removed_file_count = 0;
466    foreach my $uniquename (@source_files)
467    {
468        if ( ! defined $target_file_map{$uniquename})
469        {
470            ++$removed_file_count;
471        }
472    }
473
474    if ($removed_file_count > 0)
475    {
476        $installer::logger::Info->printf("Error: %d files have been removed\n", $removed_file_count);
477        return 0;
478    }
479    else
480    {
481        $installer::logger::Info->printf("OK: no files have been removed\n");
482        return 1;
483    }
484}
485
486
487
488
489=head2 CheckNewFiles($source_msi, $target_msi)
490
491    New files have to be in new components.
492
493=cut
494sub CheckNewFiles($$)
495{
496    my ($source_msi, $target_msi) = @_;
497
498    # Get the 'File' tables.
499    my $source_file_table = $source_msi->GetTable("File");
500    my $target_file_table = $target_msi->GetTable("File");
501
502    # Create data structures for fast lookup.
503    my %source_file_map = map {$_->GetValue("File") => $_} @{$source_file_table->GetAllRows()};
504    my %target_files_map = map {$_->GetValue("File") => $_} @{$target_file_table->GetAllRows()};
505
506    # Search for added files (files in target that where not in source).
507    my @added_files = ();
508    foreach my $uniquename (keys %target_files_map)
509    {
510        if ( ! defined $source_file_map{$uniquename})
511        {
512            push @added_files, $target_files_map{$uniquename};
513        }
514    }
515
516    if (scalar @added_files > 0)
517    {
518        $installer::logger::Info->printf("Warning: %d files have been added\n", scalar @added_files);
519
520        # Prepare component tables and hashes.
521        my $source_component_table = $source_msi->GetTable("Component");
522        my $target_component_table = $target_msi->GetTable("Component");
523        die unless defined $source_component_table && defined $target_component_table;
524        my %source_component_map = map {$_->GetValue('Component') => $_} @{$source_component_table->GetAllRows()};
525        my %target_component_map = map {$_->GetValue('Component') => $_} @{$target_component_table->GetAllRows()};
526
527        my @new_files_with_existing_components = ();
528        foreach my $target_file_row (@added_files)
529        {
530	    $installer::logger::Info->printf("    %s (%s)\n",
531		$target_file_row->GetValue("FileName"),
532		$target_file_row->GetValue("File"));
533
534            # Get target component for target file.
535            my $target_component = $target_file_row->GetValue('Component_');
536
537            # Check that the component is not part of the source components.
538            if (defined $source_component_map{$target_component})
539            {
540                push @new_files_with_existing_components, $target_file_row;
541            }
542        }
543
544        if (scalar @new_files_with_existing_components > 0)
545        {
546            $installer::logger::Info->printf(
547                "Error: %d new files have existing components (which must also be new)\n",
548                scalar @new_files_with_existing_components);
549            return 0;
550        }
551        else
552        {
553            $installer::logger::Info->printf(
554                "OK: all %d new files also have new components\n",
555		scalar @added_files);
556            return 1;
557        }
558    }
559    else
560    {
561        $installer::logger::Info->printf("OK: no files have been added\n");
562        return 1;
563    }
564}
565
566
567
568
569=head2 CheckFeatureSets($source_msi, $target_msi)
570
571    Features must not be removed but can be added.
572    Parent features of new features also have to be new.
573
574=cut
575sub CheckFeatureSets($$)
576{
577    my ($source_msi, $target_msi) = @_;
578
579    # Get the 'Feature' tables.
580    my $source_feature_table = $source_msi->GetTable("Feature");
581    my $target_feature_table = $target_msi->GetTable("Feature");
582
583    # Create data structures for fast lookup.
584    my %source_feature_map = map {$_->GetValue("Feature") => $_} @{$source_feature_table->GetAllRows()};
585    my %target_feature_map = map {$_->GetValue("Feature") => $_} @{$target_feature_table->GetAllRows()};
586
587    # Check that no feature has been removed.
588    my @removed_features = ();
589    foreach my $feature_name (keys %source_feature_map)
590    {
591        if ( ! defined $target_feature_map{$feature_name})
592        {
593            push @removed_features, $feature_name;
594        }
595    }
596    if (scalar @removed_features > 0)
597    {
598        # There are removed features.
599        $installer::logger::Info->printf(
600            "Error: %d features have been removed:\n",
601            scalar @removed_features);
602        $installer::logger::Info->printf("       %s\n", join(", ", @removed_features));
603        return 0;
604    }
605
606    # Check that added features belong to new parent features.
607    my @added_features = ();
608    foreach my $feature_name (keys %target_feature_map)
609    {
610        if ( ! defined $source_feature_map{$feature_name})
611        {
612            push @added_features, $feature_name;
613        }
614    }
615
616    if (scalar @added_features > 0)
617    {
618        $installer::logger::Info->printf("Warning: %d features have been addded\n", scalar @added_features);
619
620        my @new_features_with_existing_parents = ();
621        foreach my $new_feature (@added_features)
622        {
623            my $target_feature = $target_feature_map{$new_feature};
624            if (defined $source_feature_map{$target_feature->{'Feature_Parent'}})
625            {
626                push @new_features_with_existing_parents, $target_feature;
627            }
628        }
629
630        if (scalar @new_features_with_existing_parents > 0)
631        {
632            $installer::logger::Info->printf(
633                "Error: %d new features have existing parents (which also must be new)\n",
634                scalar @new_features_with_existing_parents);
635            return 0;
636        }
637        else
638        {
639            $installer::logger::Info->printf(
640                "OK: parents of all new features are also new\n");
641            return 1;
642        }
643    }
644
645    $installer::logger::Info->printf("OK: feature sets in source and target are compatible\n");
646    return 1;
647}
648
649
650
651
652=head2 CheckRemovedComponents($source_msi, $target_msi)
653
654    Components must not be removed but can be added.
655    Features of added components have also to be new.
656
657=cut
658sub CheckRemovedComponents ($$)
659{
660    my ($source_msi, $target_msi) = @_;
661
662    # Get the 'Component' tables.
663    my $source_component_table = $source_msi->GetTable("Component");
664    my $target_component_table = $target_msi->GetTable("Component");
665
666    # Create data structures for fast lookup.
667    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
668    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
669
670    # Check that no component has been removed.
671    my @removed_components = ();
672    foreach my $componentname (keys %source_component_map)
673    {
674        if ( ! defined $target_component_map{$componentname})
675        {
676            push @removed_components, $componentname;
677        }
678    }
679    if (scalar @removed_components == 0)
680    {
681	$installer::logger::Info->printf("OK: no removed components\n");
682	return 1;
683    }
684    else
685    {
686        # There are removed components.
687
688        # Check if any of them is not a registry component.
689        my $is_file_component_removed = 0;
690        foreach my $componentname (@removed_components)
691        {
692            if ($componentname !~ /^registry/)
693            {
694                $is_file_component_removed = 1;
695            }
696        }
697        if ($is_file_component_removed)
698        {
699            $installer::logger::Info->printf(
700                "Error: %d components have been removed, some of them are file components:\n",
701                scalar @removed_components);
702            $installer::logger::Info->printf("       %s\n", join(", ", @removed_components));
703            return 0;
704        }
705        else
706        {
707            $installer::logger::Info->printf(
708                "Error: %d components have been removed, all of them are registry components:\n",
709                scalar @removed_components);
710            return 0;
711        }
712    }
713}
714
715
716
717
718sub GetTableAndMap ($$$)
719{
720    my ($msi, $table_name, $index_column) = @_;
721
722    my $table = $msi->GetTable($table_name);
723    my %map = map {$_->GetValue($index_column) => $_} @{$table->GetAllRows()};
724
725    return ($table, \%map);
726}
727
728
729=head2 CheckAddedComponents($source_msi, $target_msi)
730
731    Components can be added.
732    Features of added components have also to be new.
733
734=cut
735sub CheckAddedComponents ($$)
736{
737    my ($source_msi, $target_msi) = @_;
738
739    # Get the 'Component' tables and maps.
740    my ($source_component_table, $source_component_map)
741	= GetTableAndMap($source_msi, "Component", "Component");
742    my ($target_component_table, $target_component_map)
743	= GetTableAndMap($target_msi, "Component", "Component");
744
745    # Check that added components belong to new features.
746    my @added_components = ();
747    foreach my $componentname (keys %$target_component_map)
748    {
749        if ( ! defined $source_component_map->{$componentname})
750        {
751            push @added_components, $componentname;
752        }
753    }
754
755    if (scalar @added_components == 0)
756    {
757	$installer::logger::Info->printf("OK: no new components\n");
758	return 1;
759    }
760    else
761    {
762	$installer::logger::Info->printf(
763	    "Warning: %d components have been addded\n",
764	    scalar @added_components);
765
766        # Check that the referencing features are also new.
767	my $target_feature_component_table = $target_msi->GetTable("FeatureComponents");
768
769	my $error = 0;
770        foreach my $component_name (@added_components)
771        {
772	    my @feature_names = ();
773	    foreach my $feature_component_row (@{$target_feature_component_table->GetAllRows()})
774	    {
775		if ($feature_component_row->GetValue("Component_") eq $component_name)
776		{
777		    my $feature_name = $feature_component_row->GetValue("Feature_");
778		    push @feature_names, $feature_name;
779		}
780	    }
781	    if (scalar @feature_names == 0)
782	    {
783		$installer::logger::Info->printf("Error: no feature found for component '%s'\n", $component_name);
784		$error = 1;
785	    }
786	    else
787	    {
788		# Check that the referenced features are new and have new parents (if they have parents).
789		my ($source_feature_table, $source_feature_map)
790		    = GetTableAndMap($source_msi, "Feature", "Feature");
791		my ($target_feature_table, $target_feature_map)
792		    = GetTableAndMap($target_msi, "Feature", "Feature");
793		foreach my $feature_name (@feature_names)
794		{
795		    $installer::logger::Info->printf("    component '%s' -> feature '%s'\n",
796			$component_name,
797			$feature_name);
798		    my $source_feature_row = $source_feature_map->{$feature_name};
799		    if (defined $source_feature_row)
800		    {
801			$installer::logger::Info->printf("Warning(Error?): feature of new component is not new\n");
802			$error = 1;
803		    }
804		    else
805		    {
806			# Feature is new. Check that the parent feature is also new.
807			my $target_feature_row = $target_feature_map->{$feature_name};
808			my $parent_feature_name = $target_feature_row->GetValue("Feature_Parent");
809			if ($parent_feature_name ne "" && defined $source_feature_map->{$parent_feature_name})
810			{
811			    $installer::logger::Info->printf("Warning(Error?): parent feature of new component is not new\n");
812			    $error = 1;
813			}
814		    }
815		}
816	    }
817	}
818
819#	return !$error;
820	return 1;
821    }
822}
823
824
825
826
827=head2 CheckComponent($source_msi, $target_msi)
828
829    In the 'Component' table the 'ComponentId' and 'Component' values
830    for corresponding componts in the source and target release have
831    to be identical.
832
833=cut
834sub CheckComponentValues($$$)
835{
836    my ($source_msi, $target_msi, $variables) = @_;
837
838    # Get the 'Component' tables.
839    my $source_component_table = $source_msi->GetTable("Component");
840    my $target_component_table = $target_msi->GetTable("Component");
841
842    # Create data structures for fast lookup.
843    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
844    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
845
846    my @differences = ();
847    my $comparison_count = 0;
848    while (my ($componentname, $source_component_row) = each %source_component_map)
849    {
850        my $target_component_row = $target_component_map{$componentname};
851        if (defined $target_component_row)
852        {
853            ++$comparison_count;
854            if ($source_component_row->GetValue("ComponentId") ne $target_component_row->GetValue("ComponentId"))
855            {
856                push @differences, [
857                    $componentname,
858                    $source_component_row->GetValue("ComponentId"),
859                    $target_component_row->GetValue("ComponentId"),
860                    $target_component_row->GetValue("Component"),
861                ];
862            }
863        }
864    }
865
866    if (scalar @differences > 0)
867    {
868        $installer::logger::Info->printf(
869            "Error: there are %d components with different 'ComponentId' values after %d comparisons.\n",
870            scalar @differences,
871            $comparison_count);
872        foreach my $item (@differences)
873        {
874            $installer::logger::Info->printf("%s  %s\n", $item->[1], $item->[2]);
875        }
876        return 0;
877    }
878    else
879    {
880        $installer::logger::Info->printf("OK: components in source and target are identical\n");
881        return 1;
882    }
883}
884
885
886
887
888=head2 CheckFileSequence($source_msi, $target_msi)
889
890    In the 'File' table the 'Sequence' numbers for corresponding files has to be identical.
891
892=cut
893sub CheckFileSequence($$)
894{
895    my ($source_msi, $target_msi) = @_;
896
897    # Get the 'File' tables.
898    my $source_file_table = $source_msi->GetTable("File");
899    my $target_file_table = $target_msi->GetTable("File");
900
901    # Create temporary data structures for fast access.
902    my %source_file_map = map {$_->GetValue("File") => $_} @{$source_file_table->GetAllRows()};
903    my %target_file_map = map {$_->GetValue("File") => $_} @{$target_file_table->GetAllRows()};
904
905    # Search files with mismatching sequence numbers.
906    my @mismatching_files = ();
907    while (my ($uniquename,$source_file_row) = each %source_file_map)
908    {
909        my $target_file_row = $target_file_map{$uniquename};
910        if (defined $target_file_row)
911        {
912            if ($source_file_row->GetValue('Sequence') ne $target_file_row->GetValue('Sequence'))
913            {
914                push @mismatching_files, [
915                    $uniquename,
916                    $source_file_row,
917                    $target_file_row
918                ];
919            }
920        }
921    }
922
923    if (scalar @mismatching_files > 0)
924    {
925        $installer::logger::Info->printf("Error: there are %d files with mismatching 'Sequence' numbers\n",
926            scalar @mismatching_files);
927        foreach my $item (@mismatching_files)
928        {
929            $installer::logger::Info->printf("    %s: %d != %d\n",
930                $item->[0],
931                $item->[1]->GetValue("Sequence"),
932                $item->[2]->GetValue("Sequence"));
933        }
934        return 0;
935    }
936    else
937    {
938        $installer::logger::Info->printf("OK: all files have matching 'Sequence' numbers\n");
939        return 1;
940    }
941}
942
943
944
945
946=head2 CheckFileSequenceUnique($source_msi, $target_msi)
947
948    In the 'File' table the 'Sequence' values have to be unique.
949
950=cut
951sub CheckFileSequenceUnique($$)
952{
953    my ($source_msi, $target_msi) = @_;
954
955    # Get the 'File' tables.
956    my $target_file_table = $target_msi->GetTable("File");
957
958    my %sequence_numbers = ();
959    my $collision_count = 0;
960    foreach my $row (@{$target_file_table->GetAllRows()})
961    {
962        my $sequence_number = $row->GetValue("Sequence");
963        if (defined $sequence_numbers{$sequence_number})
964        {
965            ++$collision_count;
966        }
967        else
968        {
969            $sequence_numbers{$sequence_number} = 1;
970        }
971    }
972
973    if ($collision_count > 0)
974    {
975        $installer::logger::Info->printf("Error: there are %d collisions ofn the sequence numbers\n",
976            $collision_count);
977        return 0;
978    }
979    else
980    {
981        $installer::logger::Info->printf("OK: sequence numbers are unique\n");
982        return 1;
983    }
984}
985
986
987
988
989=head2 CheckFileSequenceHoles ($target_msi)
990
991    Check the sequence numbers of the target msi if the n files use numbers 1..n or if there are holes.
992    Holes are reported as warnings.
993
994=cut
995sub CheckFileSequenceHoles ($$)
996{
997    my ($source_msi, $target_msi) = @_;
998
999    my $target_file_table = $target_msi->GetTable("File");
1000    my %sequence_numbers = map {$_->GetValue("Sequence") => $_} @{$target_file_table->GetAllRows()};
1001    my @sorted_sequence_numbers = sort {$a <=> $b} keys %sequence_numbers;
1002    my $expected_next_sequence_number = 1;
1003    my @holes = ();
1004    foreach my $sequence_number (@sorted_sequence_numbers)
1005    {
1006        if ($sequence_number != $expected_next_sequence_number)
1007        {
1008            push @holes, [$expected_next_sequence_number, $sequence_number-1];
1009        }
1010        $expected_next_sequence_number = $sequence_number+1;
1011    }
1012    if (scalar @holes > 0)
1013    {
1014        $installer::logger::Info->printf("Warning: sequence numbers have %d holes\n");
1015        foreach my $hole (@holes)
1016        {
1017            if ($hole->[0] != $hole->[1])
1018            {
1019                $installer::logger::Info->printf("    %d\n", $hole->[0]);
1020            }
1021            else
1022            {
1023                $installer::logger::Info->printf("    %d -> %d\n", $hole->[0], $hole->[1]);
1024            }
1025        }
1026    }
1027    else
1028    {
1029        $installer::logger::Info->printf("OK: there are no holes in the sequence numbers\n");
1030    }
1031    return 1;
1032}
1033
1034
1035
1036
1037=head2 CheckRegistryItems($source_msi, $target_msi)
1038
1039    In the 'Registry' table the 'Component_' and 'Key' values must not
1040    depend on the version number (beyond the unchanging major
1041    version).
1042
1043    'Value' values must only depend on the major version number to
1044    avoid duplicate entries in the start menu.
1045
1046    Violations are reported as warnings for now.
1047
1048=cut
1049sub CheckRegistryItems($$$)
1050{
1051    my ($source_msi, $target_msi, $product_name) = @_;
1052
1053    # Get the registry tables.
1054    my $source_registry_table = $source_msi->GetTable("Registry");
1055    my $target_registry_table = $target_msi->GetTable("Registry");
1056
1057    my $registry_index = $target_registry_table->GetColumnIndex("Registry");
1058    my $component_index = $target_registry_table->GetColumnIndex("Component_");
1059
1060    # Create temporary data structures for fast access.
1061    my %source_registry_map = map {$_->GetValue($registry_index) => $_} @{$source_registry_table->GetAllRows()};
1062    my %target_registry_map = map {$_->GetValue($registry_index) => $_} @{$target_registry_table->GetAllRows()};
1063
1064    # Prepare version numbers to search.
1065    my $source_version_number = $source_msi->{'version'};
1066    my $source_version_nodots = installer::patch::Version::ArrayToNoDotName(
1067        installer::patch::Version::StringToNumberArray($source_version_number));
1068    my $source_component_pattern = lc($product_name).$source_version_nodots;
1069    my $target_version_number = $target_msi->{'version'};
1070    my $target_version_nodots = installer::patch::Version::ArrayToNoDotName(
1071        installer::patch::Version::StringToNumberArray($target_version_number));
1072    my $target_component_pattern = lc($product_name).$target_version_nodots;
1073
1074    foreach my $source_row (values %source_registry_map)
1075    {
1076        my $target_row = $target_registry_map{$source_row->GetValue($registry_index)};
1077        if ( ! defined $target_row)
1078        {
1079            $installer::logger::Info->printf("Error: sets of registry entries differs\n");
1080            return 1;
1081        }
1082
1083        my $source_component_name = $source_row->GetValue($component_index);
1084        my $target_component_name = $source_row->GetValue($component_index);
1085
1086    }
1087
1088    $installer::logger::Info->printf("OK: registry items are OK\n");
1089    return 1;
1090}
1091
1092
1093
1094
1095=head2
1096
1097    Component->KeyPath must not change. (see component.pm/get_component_keypath)
1098
1099=cut
1100sub CheckComponentKeyPath ($$)
1101{
1102    my ($source_msi, $target_msi) = @_;
1103
1104    # Get the registry tables.
1105    my $source_component_table = $source_msi->GetTable("Component");
1106    my $target_component_table = $target_msi->GetTable("Component");
1107
1108    # Create temporary data structures for fast access.
1109    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
1110    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
1111
1112    my @mismatches = ();
1113    while (my ($componentname, $source_component_row) = each %source_component_map)
1114    {
1115        my $target_component_row = $target_component_map{$componentname};
1116        if (defined $target_component_row)
1117        {
1118            my $source_keypath = $source_component_row->GetValue("KeyPath");
1119            my $target_keypath = $target_component_row->GetValue("KeyPath");
1120            if ($source_keypath ne $target_keypath)
1121            {
1122                push @mismatches, [$componentname, $source_keypath, $target_keypath];
1123            }
1124        }
1125    }
1126
1127    if (scalar @mismatches > 0)
1128    {
1129        $installer::logger::Info->printf(
1130            "Error: there are %d mismatches in the 'KeyPath' column of the 'Component' table\n",
1131            scalar @mismatches);
1132
1133        foreach my $item (@mismatches)
1134        {
1135            $installer::logger::Info->printf(
1136                "    %s: %s != %s\n",
1137                $item->[0],
1138                $item->[1],
1139                $item->[2]);
1140        }
1141
1142        return 0;
1143    }
1144    else
1145    {
1146        $installer::logger::Info->printf(
1147            "OK: no mismatches in the 'KeyPath' column of the 'Component' table\n");
1148        return 1;
1149    }
1150}
1151
1152
1153
1154
1155sub GetMissingReferences ($$$$$)
1156{
1157    my ($table, $key, $map, $what, $report_key) = @_;
1158
1159    my @missing_references = ();
1160
1161    foreach my $row (@{$table->GetAllRows()})
1162    {
1163        my $value = $row->GetValue($key);
1164        if ($value ne "" && ! defined $map->{$value})
1165        {
1166            push @missing_references, [$what, $row->GetValue($report_key), $value];
1167        }
1168    }
1169
1170    return @missing_references;
1171}
1172
1173
1174
1175
1176=head CheckAllReferences ($msi)
1177
1178    Check references from files and registry entries to components,
1179    from components to features, and between features.
1180
1181=cut
1182
1183sub CheckAllReferences ($)
1184{
1185    my ($msi) = @_;
1186
1187    # Set up tables and maps for easy iteration and fast lookups.
1188
1189    my $feature_table = $msi->GetTable("Feature");
1190    my $component_table = $msi->GetTable("Component");
1191    my $feature_component_table = $msi->GetTable("FeatureComponents");
1192    my $file_table = $msi->GetTable("File");
1193    my $registry_table = $msi->GetTable("Registry");
1194    my $directory_table = $msi->GetTable("Directory");
1195
1196    my %feature_map = map {$_->GetValue("Feature") => $_} @{$feature_table->GetAllRows()};
1197    my %component_map = map {$_->GetValue("Component") => $_} @{$component_table->GetAllRows()};
1198    my %directory_map = map {$_->GetValue("Directory") => $_} @{$directory_table->GetAllRows()};
1199
1200    my @missing_references = ();
1201
1202    # Check references from files and registry entries to components.
1203    push @missing_references, GetMissingReferences(
1204        $file_table,
1205        "Component_",
1206        \%component_map,
1207        "file->component",
1208        "File");
1209    push @missing_references, GetMissingReferences(
1210        $registry_table,
1211        "Component_",
1212        \%component_map,
1213        "registry->component",
1214        "Registry");
1215
1216    # Check references between features and components.
1217    push @missing_references, GetMissingReferences(
1218        $feature_component_table,
1219        "Feature_",
1220        \%feature_map,
1221        "component->feature",
1222        "Component_");
1223    push @missing_references, GetMissingReferences(
1224        $feature_component_table,
1225        "Component_",
1226        \%component_map,
1227        "feature->component",
1228        "Feature_");
1229
1230    # Check references between features.
1231    push @missing_references, GetMissingReferences(
1232        $feature_table,
1233        'Feature_Parent',
1234        \%feature_map,
1235        "feature->feature",
1236        'Feature');
1237
1238    # Check references between directories.
1239    push @missing_references, GetMissingReferences(
1240        $directory_table,
1241        'Directory_Parent',
1242        \%directory_map,
1243        "directory->directory",
1244        'Directory');
1245
1246    # Check references from components to directories.
1247    push @missing_references, GetMissingReferences(
1248        $component_table,
1249        'Directory_',
1250        \%directory_map,
1251        "component->directory",
1252        'Component');
1253
1254    # Check references from components to files (via the .
1255
1256    # Report the result.
1257    if (scalar @missing_references > 0)
1258    {
1259        $installer::logger::Info->printf("Error: there are %d missing references\n", scalar @missing_references);
1260        foreach my $reference (@missing_references)
1261        {
1262            $installer::logger::Info->printf("    %s : %s -> %s\n",
1263                $reference->[0],
1264                $reference->[1],
1265                $reference->[2]);
1266        }
1267        return 0;
1268    }
1269    else
1270    {
1271        $installer::logger::Info->printf("OK: all references are OK\n");
1272        return 1;
1273
1274    }
1275}
1276
1277
1278
1279
1280sub Check ($$$$)
1281{
1282    my ($source_msi, $target_msi, $variables, $product_name) = @_;
1283
1284    $installer::logger::Info->printf("checking if source and target releases are compatible\n");
1285    $installer::logger::Info->increase_indentation();
1286
1287    my $result = 1;
1288
1289    # Using &= below to avoid lazy evaluation.  Even if there are errors, all checks shall be run.
1290    $result &= CheckUpgradeCode($source_msi, $target_msi);
1291    $result &= CheckProductCode($source_msi, $target_msi);
1292    $result &= CheckBuildIdCode($source_msi, $target_msi);
1293    $result &= CheckProductName($source_msi, $target_msi);
1294    $result &= CheckRemovedFiles($source_msi, $target_msi);
1295    $result &= CheckNewFiles($source_msi, $target_msi);
1296    $result &= CheckFeatureSets($source_msi, $target_msi);
1297    $result &= CheckRemovedComponents($source_msi, $target_msi);
1298    $result &= CheckAddedComponents($source_msi, $target_msi);
1299    $result &= CheckComponentValues($source_msi, $target_msi, $variables);
1300    $result &= CheckFileSequence($source_msi, $target_msi);
1301    $result &= CheckFileSequenceUnique($source_msi, $target_msi);
1302    $result &= CheckFileSequenceHoles($source_msi, $target_msi);
1303    $result &= CheckRegistryItems($source_msi, $target_msi, $product_name);
1304    $result &= CheckComponentKeyPath($source_msi, $target_msi);
1305    $result &= CheckAllReferences($target_msi);
1306
1307    $installer::logger::Info->decrease_indentation();
1308
1309    if ($result)
1310    {
1311        $installer::logger::Info->printf("OK: Source and target releases are compatible.\n");
1312    }
1313    else
1314    {
1315        $installer::logger::Info->printf("Error: Source and target releases are not compatible.\n");
1316        $installer::logger::Info->printf("       => Can not create patch.\n");
1317        $installer::logger::Info->printf("       Did you create the target installation set with 'release=t' ?\n");
1318    }
1319
1320    return $result;
1321}
1322
1323
1324
1325
1326=head2 FindPcpTemplate ()
1327
1328    The template.pcp file is part of the Windows SDK.
1329
1330=cut
1331sub FindPcpTemplate ()
1332{
1333    my $psdk_home = $ENV{'PSDK_HOME'};
1334    if ( ! defined $psdk_home)
1335    {
1336        $installer::logger::Info->printf("Error: the PSDK_HOME environment variable is not set.\n");
1337        $installer::logger::Info->printf("       did you load the AOO build environment?\n");
1338        $installer::logger::Info->printf("       you may want to use the --with-psdk-home configure option\n");
1339        return undef;
1340    }
1341    if ( ! -d $psdk_home)
1342    {
1343        $installer::logger::Info->printf(
1344            "Error: the PSDK_HOME environment variable does not point to a valid directory: %s\n",
1345            $psdk_home);
1346        return undef;
1347    }
1348
1349    my $schema_path = File::Spec->catfile($psdk_home, "Bin", "msitools", "Schemas", "MSI");
1350    if (  ! -d $schema_path)
1351    {
1352        $installer::logger::Info->printf("Error: Can not locate the msi template folder in the Windows SDK\n");
1353        $installer::logger::Info->printf("       %s\n", $schema_path);
1354        $installer::logger::Info->printf("       Is the Windows SDK properly installed?\n");
1355        return undef;
1356    }
1357
1358    my $schema_filename = File::Spec->catfile($schema_path, "template.pcp");
1359    if (  ! -f $schema_filename)
1360    {
1361        $installer::logger::Info->printf("Error: Can not locate the pcp template at\n");
1362        $installer::logger::Info->printf("       %s\n", $schema_filename);
1363        $installer::logger::Info->printf("       Is the Windows SDK properly installed?\n");
1364        return undef;
1365    }
1366
1367    return $schema_filename;
1368}
1369
1370
1371
1372
1373sub SetupPcpPatchMetadataTable ($$$)
1374{
1375    my ($pcp, $source_msi, $target_msi) = @_;
1376
1377    # Determine values for eg product name and source and new version.
1378    my $source_version = $source_msi->{'version'};
1379    my $target_version = $target_msi->{'version'};
1380
1381    my $property_table = $target_msi->GetTable("Property");
1382    my $display_product_name = $property_table->GetValue("Property", "DEFINEDPRODUCT", "Value");
1383
1384    # Set table.
1385    my $table = $pcp->GetTable("PatchMetadata");
1386    $table->SetRow(
1387        "Company", "",
1388        "*Property", "Description",
1389        "Value", sprintf("Update of %s from %s to %s", $display_product_name, $source_version, $target_version)
1390        );
1391    $table->SetRow(
1392        "Company", "",
1393        "*Property", "DisplayName",
1394        "Value", sprintf("Update of %s from %s to %s", $display_product_name, $source_version, $target_version)
1395        );
1396    $table->SetRow(
1397        "Company", "",
1398        "*Property", "ManufacturerName",
1399        "Value", $property_table->GetValue("Property", "Manufacturer", "Value"),
1400        );
1401    $table->SetRow(
1402        "Company", "",
1403        "*Property", "MoreInfoURL",
1404        "Value", $property_table->GetValue("Property", "ARPURLINFOABOUT", "Value")
1405        );
1406    $table->SetRow(
1407        "Company", "",
1408        "*Property", "TargetProductName",
1409        "Value", $property_table->GetValue("Property", "ProductName", "Value")
1410        );
1411    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time);
1412
1413    $table->SetRow(
1414        "Company", "",
1415        "*Property", "CreationTimeUTC",
1416        "Value", sprintf("%d/%d/%d %d:%02d", $mon+1,$mday,$year+1900,$hour,$min)
1417        );
1418}
1419
1420
1421
1422
1423sub SetupPropertiesTable ($$)
1424{
1425    my ($pcp, $msp_filename) = @_;
1426
1427    my $table = $pcp->GetTable("Properties");
1428
1429    $table->SetRow(
1430        "*Name", "PatchOutputPath",
1431        "Value", installer::patch::Tools::ToWindowsPath($msp_filename)
1432        );
1433    # Request at least Windows installer 2.0.
1434    # Version 2.0 allows us to omit some values from ImageFamilies table.
1435    $table->SetRow(
1436        "*Name", "MinimumRequiredMsiVersion",
1437        "Value", 200
1438        );
1439    # Allow diffs for binary files.
1440    $table->SetRow(
1441        "*Name", "IncludeWholeFilesOnly",
1442        "Value", 0
1443        );
1444
1445    my $uuid = installer::windows::msiglobal::create_guid();
1446    my $uuid_string = "{" . $uuid . "}";
1447    $table->SetRow(
1448        "*Name", "PatchGUID",
1449        "Value", $uuid_string
1450        );
1451    $installer::logger::Info->printf("created new PatchGUID %s\n", $uuid_string);
1452
1453    # Prevent sequence table from being generated.
1454    $table->SetRow(
1455        "*Name", "SEQUENCE_DATA_GENERATION_DISABLED",
1456        "Value", 1);
1457
1458    # We don't provide file size and hash values.
1459    # This value is set to make this fact explicit (0 should be the default).
1460    $table->SetRow(
1461        "*Name", "TrustMsi",
1462        "Value", 0);
1463}
1464
1465
1466
1467
1468sub SetupImageFamiliesTable ($)
1469{
1470    my ($pcp) = @_;
1471
1472    $pcp->GetTable("ImageFamilies")->SetRow(
1473        "Family", $ImageFamily,
1474        "MediaSrcPropName", "",#"MNPSrcPropName",
1475        "MediaDiskId", "",
1476        "FileSequenceStart", "",
1477        "DiskPrompt", "",
1478        "VolumeLabel", "");
1479}
1480
1481
1482
1483
1484sub SetupUpgradedImagesTable ($$)
1485{
1486    my ($pcp, $target_msi_path) = @_;
1487
1488    my $msi_path = installer::patch::Tools::ToWindowsPath($target_msi_path);
1489    $pcp->GetTable("UpgradedImages")->SetRow(
1490        "Upgraded", $TargetImageName,
1491        "MsiPath", $msi_path,
1492        "PatchMsiPath", "",
1493        "SymbolPaths", "",
1494        "Family", $ImageFamily);
1495}
1496
1497
1498
1499
1500sub SetupTargetImagesTable ($$)
1501{
1502    my ($pcp, $source_msi_path) = @_;
1503
1504    $pcp->GetTable("TargetImages")->SetRow(
1505        "Target", $SourceImageName,
1506        "MsiPath", installer::patch::Tools::ToWindowsPath($source_msi_path),
1507        "SymbolPaths", "",
1508        "Upgraded", $TargetImageName,
1509        "Order", 1,
1510        "ProductValidateFlags", "",
1511        "IgnoreMissingSrcFiles", 0);
1512}
1513
1514
1515
1516
1517sub SetAdditionalValues ($%)
1518{
1519    my ($pcp, %data) = @_;
1520
1521    while (my ($key,$value) = each(%data))
1522    {
1523        $key =~ /^([^\/]+)\/([^:]+):(.+)$/
1524            || die("invalid key format");
1525        my ($table_name, $key_column,$key_value) = ($1,$2,$3);
1526        $value =~ /^([^:]+):(.*)$/
1527            || die("invalid value format");
1528        my ($value_column,$value_value) = ($1,$2);
1529
1530        my $table = $pcp->GetTable($table_name);
1531        $table->SetRow(
1532                "*".$key_column, $key_value,
1533                $value_column, $value_value);
1534    }
1535}
1536
1537
1538
1539
1540sub CreatePcp ($$$$$$%)
1541{
1542    my ($source_msi,
1543        $target_msi,
1544        $language,
1545        $context,
1546        $msp_path,
1547        $pcp_schema_filename,
1548        %additional_values) = @_;
1549
1550    # Create filenames.
1551    my $pcp_filename = File::Spec->catfile($msp_path, "openoffice.pcp");
1552    # Create basename to include product name and source and target version.
1553    # Hard code platform because that is the only platform supported at the moment.
1554    my $msp_basename = sprintf("%s_%s-%s_Win_x86_patch_%s.msp",
1555        $context->{'product-name'},
1556        $source_msi->{'version'},
1557        $target_msi->{'version'},
1558        $context->{'language'});
1559    my $msp_filename = File::Spec->catfile($msp_path, $msp_basename);
1560
1561    # Setup msp path and filename.
1562    unlink($pcp_filename) if -f $pcp_filename;
1563    if ( ! File::Copy::copy($pcp_schema_filename, $pcp_filename))
1564    {
1565        $installer::logger::Info->printf("Error: could not create openoffice.pcp as copy of pcp schema\n");
1566        $installer::logger::Info->printf("       %s\n", $pcp_schema_filename);
1567        $installer::logger::Info->printf("       %s\n", $pcp_filename);
1568        return undef;
1569    }
1570    my $pcp = installer::patch::Msi->new(
1571        $pcp_filename,
1572        $target_msi->{'version'},
1573        $target_msi->{'is_current_version'},
1574        $language,
1575        $context->{'product-name'});
1576
1577    # Store some values in the pcp for easy reference in the msp creation.
1578    $pcp->{'msp_filename'} = $msp_filename;
1579
1580    SetupPcpPatchMetadataTable($pcp, $source_msi, $target_msi);
1581    SetupPropertiesTable($pcp, $msp_filename);
1582    SetupImageFamiliesTable($pcp);
1583    SetupUpgradedImagesTable($pcp, $target_msi->{'filename'});
1584    SetupTargetImagesTable($pcp, $source_msi->{'filename'});
1585
1586    SetAdditionalValues(%additional_values);
1587
1588    $pcp->Commit();
1589
1590    # Remove the PatchSequence table to avoid MsiMsp error message:
1591    # "Since MSI 3.0 will block installation of major upgrade patches with
1592    #  sequencing information, creation of such patches is blocked."
1593    #$pcp->RemoveTable("PatchSequence");
1594    # TODO: alternatively add property SEQUENCE_DATA_GENERATION_DISABLED to pcp Properties table.
1595
1596
1597    $installer::logger::Info->printf("created pcp file at\n");
1598    $installer::logger::Info->printf("    %s\n", $pcp->{'filename'});
1599
1600    return $pcp;
1601}
1602
1603
1604
1605
1606sub ShowLog ($$$$)
1607{
1608    my ($log_path, $log_filename, $log_basename, $new_title) = @_;
1609
1610    if ( -f $log_filename)
1611    {
1612        my $destination_path = File::Spec->catfile($log_path, $log_basename);
1613        File::Path::make_path($destination_path) if ! -d $destination_path;
1614        my $command = join(" ",
1615            "wilogutl.exe",
1616            "/q",
1617            "/l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1618            "/o", "'".installer::patch::Tools::ToWindowsPath($destination_path)."'");
1619        printf("running command $command\n");
1620        my $response = qx($command);
1621        my @candidates = glob($destination_path . "/Details*");
1622        foreach my $candidate (@candidates)
1623        {
1624            next unless -f $candidate;
1625            my $new_name = $candidate;
1626            $new_name =~ s/Details.*$/$log_basename.html/;
1627
1628            # Rename the top-level html file and replace the title.
1629            open my $in, "<", $candidate;
1630            open my $out, ">", $new_name;
1631            while (<$in>)
1632            {
1633                if (/^(.*\<title\>)([^<]+)(.*)$/)
1634                {
1635                    print $out $1.$new_title.$3;
1636                }
1637                else
1638                {
1639                    print $out $_;
1640                }
1641            }
1642            close $in;
1643            close $out;
1644
1645            my $URL = File::Spec->rel2abs($new_name);
1646            $URL =~ s/\/cygdrive\/(.)\//$1|\//;
1647            $URL =~ s/^(.):/$1|/;
1648            $URL = "file:///". $URL;
1649            $installer::logger::Info->printf("open %s in your browser to see the log messages\n", $URL);
1650        }
1651    }
1652    else
1653    {
1654        $installer::logger::Info->printf("Error: log file not found at %s\n", $log_filename);
1655    }
1656}
1657
1658
1659
1660
1661sub CreateMsp ($)
1662{
1663    my ($pcp) = @_;
1664
1665    # Prepare log files.
1666    my $log_path = File::Spec->catfile($pcp->{'path'}, "log");
1667    my $log_basename = "msp";
1668    my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
1669    my $performance_log_basename = "performance";
1670    my $performance_log_filename = File::Spec->catfile($log_path, $performance_log_basename.".log");
1671    File::Path::make_path($log_path) if ! -d $log_path;
1672    unlink($log_filename) if -f $log_filename;
1673    unlink($performance_log_filename) if -f $performance_log_filename;
1674
1675    # Create the .msp patch file.
1676    my $temporary_msimsp_path = File::Spec->catfile($pcp->{'path'}, "tmp");
1677    if ( ! -d $temporary_msimsp_path)
1678    {
1679        File::Path::make_path($temporary_msimsp_path)
1680            || die ("can not create temporary path ".$temporary_msimsp_path);
1681    }
1682    $installer::logger::Info->printf("running msimsp.exe, that will take a while\n");
1683    my $create_performance_log = 0;
1684    my $command = join(" ",
1685        "msimsp.exe",
1686        "-s", "'".installer::patch::Tools::ToWindowsPath($pcp->{'filename'})."'",
1687        "-p", "'".installer::patch::Tools::ToWindowsPath($pcp->{'msp_filename'})."'",
1688        "-l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1689        "-f", "'".installer::patch::Tools::ToWindowsPath($temporary_msimsp_path)."'");
1690    if ($create_performance_log)
1691    {
1692        $command .= " -lp " . MsiTools::ToEscapedWindowsPath($performance_log_filename);
1693    }
1694    $installer::logger::Info->printf("running command %s\n", $command);
1695    my $response = qx($command);
1696    $installer::logger::Info->printf("response of msimsp is %s\n", $response);
1697    if ( ! -d $temporary_msimsp_path)
1698    {
1699        die("msimsp failed and deleted temporary path ".$temporary_msimsp_path);
1700    }
1701
1702    # Show the log file that was created by the msimsp.exe command.
1703    ShowLog($log_path, $log_filename, $log_basename, "msp creation");
1704    if ($create_performance_log)
1705    {
1706        ShowLog($log_path, $performance_log_filename, $performance_log_basename, "msp creation perf");
1707    }
1708}
1709
1710
1711sub ProvideMsis ($$$)
1712{
1713    my ($context, $variables, $language) = @_;
1714
1715    # 2a. Provide .msi and .cab files and unpack .cab for the source release.
1716    $installer::logger::Info->printf("locating source package (%s)\n", $context->{'source-version'});
1717    $installer::logger::Info->increase_indentation();
1718    if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
1719	       $context->{'source-version'},
1720	       0,
1721	       $language,
1722	       "msi",
1723	       $context->{'product-name'}))
1724    {
1725        die "could not provide unpacked .cab file";
1726    }
1727    my $source_msi = installer::patch::Msi->FindAndCreate(
1728        $context->{'source-version'},
1729        0,
1730        $language,
1731        $context->{'product-name'});
1732    die unless defined $source_msi;
1733    die unless $source_msi->IsValid();
1734    $installer::logger::Info->decrease_indentation();
1735
1736    # 2b. Provide .msi and .cab files and unpacked .cab for the target release.
1737    $installer::logger::Info->printf("locating target package (%s)\n", $context->{'target-version'});
1738    $installer::logger::Info->increase_indentation();
1739    if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
1740               $context->{'target-version'},
1741               1,
1742               $language,
1743               "msi",
1744               $context->{'product-name'}))
1745    {
1746        die;
1747    }
1748    my $target_msi = installer::patch::Msi->FindAndCreate(
1749        $context->{'target-version'},
1750        0,
1751        $language,
1752        $context->{'product-name'});
1753    die unless defined $target_msi;
1754    die unless $target_msi->IsValid();
1755    $installer::logger::Info->decrease_indentation();
1756
1757    return ($source_msi, $target_msi);
1758}
1759
1760
1761
1762
1763=head CreatePatch($context, $variables)
1764
1765    Create MSP patch files for all relevant languages.
1766    The different steps are:
1767    1. Determine the set of languages for which both the source and target installation sets are present.
1768    Per language:
1769        2. Unpack CAB files (for source and target).
1770        3. Check if source and target releases are compatible.
1771        4. Create the PCP driver file.
1772        5. Create the MSP patch file.
1773
1774=cut
1775sub CreatePatch ($$)
1776{
1777    my ($context, $variables) = @_;
1778
1779    $installer::logger::Info->printf("patch will update product %s from %s to %s\n",
1780        $context->{'product-name'},
1781        $context->{'source-version'},
1782        $context->{'target-version'});
1783
1784    # Locate the Pcp schema file early on to report any errors before the lengthy operations that follow.
1785    my $pcp_schema_filename = FindPcpTemplate();
1786    if ( ! defined $pcp_schema_filename)
1787    {
1788        exit(1);
1789    }
1790
1791    my $release_data = installer::patch::ReleasesList::Instance()
1792        ->{$context->{'source-version'}}
1793        ->{$context->{'package-format'}};
1794
1795    # 1. Determine the set of languages for which we can create patches.
1796    my $language = $context->{'language'};
1797    my %no_ms_lang_locale_map = map {$_=>1} @installer::globals::noMSLocaleLangs;
1798    if (defined $no_ms_lang_locale_map{$language})
1799    {
1800        $language = "en-US_".$language;
1801    }
1802
1803    if ( ! IsLanguageValid($context, $release_data, $language))
1804    {
1805        $installer::logger::Info->printf("can not create patch for language '%s'\n", $language);
1806    }
1807    else
1808    {
1809        $installer::logger::Info->printf("processing language '%s'\n", $language);
1810        $installer::logger::Info->increase_indentation();
1811
1812        my ($source_msi, $target_msi) = ProvideMsis($context, $variables, $language);
1813
1814        # Trigger reading of tables.
1815        foreach my $table_name (("File", "Component", "Registry"))
1816        {
1817            $source_msi->GetTable($table_name);
1818            $target_msi->GetTable($table_name);
1819            $installer::logger::Info->printf("read %s table (source and target\n", $table_name);
1820        }
1821
1822        # 3. Check if the source and target msis fullfil all necessary requirements.
1823        if ( ! Check($source_msi, $target_msi, $variables, $context->{'product-name'}))
1824        {
1825            exit(1);
1826        }
1827
1828        # Provide the base path for creating .pcp and .mcp file.
1829        my $msp_path = File::Spec->catfile(
1830            $context->{'output-path'},
1831            $context->{'product-name'},
1832            "msp",
1833            sprintf("%s_%s",
1834                installer::patch::Version::ArrayToDirectoryName(
1835                    installer::patch::Version::StringToNumberArray(
1836                        $source_msi->{'version'})),
1837                installer::patch::Version::ArrayToDirectoryName(
1838                    installer::patch::Version::StringToNumberArray(
1839                        $target_msi->{'version'}))),
1840            $language
1841            );
1842        File::Path::make_path($msp_path) unless -d $msp_path;
1843
1844        # 4. Create the .pcp file that drives the msimsp.exe command.
1845        my $pcp = CreatePcp(
1846            $source_msi,
1847            $target_msi,
1848            $language,
1849            $context,
1850            $msp_path,
1851            $pcp_schema_filename,
1852            "Properties/Name:DontRemoveTempFolderWhenFinished" => "Value:1");
1853
1854        # 5. Finally create the msp.
1855        CreateMsp($pcp);
1856
1857        $installer::logger::Info->decrease_indentation();
1858    }
1859}
1860
1861
1862
1863
1864sub CheckPatchCompatability ($$)
1865{
1866    my ($context, $variables) = @_;
1867
1868    $installer::logger::Info->printf("patch will update product %s from %s to %s\n",
1869        $context->{'product-name'},
1870        $context->{'source-version'},
1871        $context->{'target-version'});
1872
1873    my $release_data = installer::patch::ReleasesList::Instance()
1874        ->{$context->{'source-version'}}
1875        ->{$context->{'package-format'}};
1876
1877    # 1. Determine the set of languages for which we can create patches.
1878    my $language = $context->{'language'};
1879    my %no_ms_lang_locale_map = map {$_=>1} @installer::globals::noMSLocaleLangs;
1880    if (defined $no_ms_lang_locale_map{$language})
1881    {
1882        $language = "en-US_".$language;
1883    }
1884
1885    if ( ! IsLanguageValid($context, $release_data, $language))
1886    {
1887        $installer::logger::Info->printf("can not create patch for language '%s'\n", $language);
1888    }
1889    else
1890    {
1891        $installer::logger::Info->printf("processing language '%s'\n", $language);
1892        $installer::logger::Info->increase_indentation();
1893
1894        my ($source_msi, $target_msi) = ProvideMsis($context, $variables, $language);
1895
1896        # Trigger reading of tables.
1897        foreach my $table_name (("File", "Component", "Registry"))
1898        {
1899            $source_msi->GetTable($table_name);
1900            $target_msi->GetTable($table_name);
1901            $installer::logger::Info->printf("read %s table (source and target\n", $table_name);
1902        }
1903
1904        # 3. Check if the source and target msis fulfill all necessary requirements.
1905        if ( ! Check($source_msi, $target_msi, $variables, $context->{'product-name'}))
1906        {
1907            exit(1);
1908        }
1909    }
1910}
1911
1912
1913
1914
1915=cut ApplyPatch ($context, $variables)
1916
1917    This is for testing only.
1918    The patch is applied and (extensive) log information is created and transformed into HTML format.
1919
1920=cut
1921sub ApplyPatch ($$)
1922{
1923    my ($context, $variables) = @_;
1924
1925    $installer::logger::Info->printf("will apply patches that update product %s from %s to %s\n",
1926        $context->{'product-name'},
1927        $context->{'source-version'},
1928        $context->{'target-version'});
1929
1930    my $source_version_dirname = installer::patch::Version::ArrayToDirectoryName(
1931      installer::patch::Version::StringToNumberArray(
1932          $context->{'source-version'}));
1933    my $target_version_dirname = installer::patch::Version::ArrayToDirectoryName(
1934      installer::patch::Version::StringToNumberArray(
1935          $context->{'target-version'}));
1936
1937    my $language = $context->{'language'};
1938    my %no_ms_lang_locale_map = map {$_=>1} @installer::globals::noMSLocaleLangs;
1939    if (defined $no_ms_lang_locale_map{$language})
1940    {
1941        $language = "en-US_".$language;
1942    }
1943
1944    my $msp_filename = File::Spec->catfile(
1945        $context->{'output-path'},
1946        $context->{'product-name'},
1947        "msp",
1948        $source_version_dirname . "_" . $target_version_dirname,
1949        $language,
1950        "openoffice.msp");
1951    if ( ! -f $msp_filename)
1952    {
1953        $installer::logger::Info->printf("%s does not point to a valid file\n", $msp_filename);
1954        next;
1955    }
1956
1957    my $log_path = File::Spec->catfile(dirname($msp_filename), "log");
1958    my $log_basename = "apply-msp";
1959    my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
1960
1961    my $command = join(" ",
1962        "msiexec.exe",
1963        "/update", "'".installer::patch::Tools::ToWindowsPath($msp_filename)."'",
1964        "/L*xv!", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1965        "REINSTALL=ALL",
1966#            "REINSTALLMODE=vomus",
1967        "REINSTALLMODE=omus",
1968        "MSIENFORCEUPGRADECOMPONENTRULES=1");
1969
1970    printf("executing command %s\n", $command);
1971    my $response = qx($command);
1972    Encode::from_to($response, "UTF16LE", "UTF8");
1973    printf("response was '%s'\n", $response);
1974
1975    ShowLog($log_path, $log_filename, $log_basename, "msp application");
1976}
1977
1978
1979
1980
1981=head2 DownloadFile ($url)
1982
1983    A simpler version of InstallationSet::Download().  It is simple because it is used to
1984    setup the $release_data structure that is used by InstallationSet::Download().
1985
1986=cut
1987sub DownloadFile ($)
1988{
1989    my ($url) = shift;
1990
1991    my $agent = LWP::UserAgent->new();
1992    $agent->timeout(120);
1993    $agent->show_progress(0);
1994
1995    my $file_content = "";
1996    my $last_was_redirect = 0;
1997    my $bytes_read = 0;
1998    $agent->add_handler('response_redirect'
1999        => sub{
2000            $last_was_redirect = 1;
2001            return;
2002        });
2003    $agent->add_handler('response_data'
2004        => sub{
2005            if ($last_was_redirect)
2006            {
2007                $last_was_redirect = 0;
2008                # Throw away the data we got so far.
2009		$file_content = "";
2010            }
2011            my($response,$agent,$h,$data)=@_;
2012	    $file_content .= $data;
2013        });
2014    $agent->get($url);
2015
2016    return $file_content;
2017}
2018
2019
2020
2021
2022sub CreateReleaseItem ($$$)
2023{
2024    my ($language, $exe_filename, $msi) = @_;
2025
2026    die "can not open installation set at ".$exe_filename unless -f $exe_filename;
2027
2028    open my $in, "<", $exe_filename;
2029    my $sha256_checksum = new Digest("SHA-256")->addfile($in)->hexdigest();
2030    close $in;
2031
2032    my $filesize = -s $exe_filename;
2033
2034    # Get the product code property from the msi and strip the enclosing braces.
2035    my $product_code = $msi->GetTable("Property")->GetValue("Property", "ProductCode", "Value");
2036    $product_code =~ s/(^{|}$)//g;
2037    my $upgrade_code = $msi->GetTable("Property")->GetValue("Property", "UpgradeCode", "Value");
2038    $upgrade_code =~ s/(^{|}$)//g;
2039    my $build_id = $msi->GetTable("Property")->GetValue("Property", "PRODUCTBUILDID", "Value");
2040
2041    return {
2042        'language' => $language,
2043        'checksum-type' => "sha256",
2044        'checksum-value' => $sha256_checksum,
2045        'file-size' => $filesize,
2046        'product-code' => $product_code,
2047        'upgrade-code' => $upgrade_code,
2048        'build-id' => $build_id
2049    };
2050}
2051
2052
2053
2054
2055sub GetReleaseItemForCurrentBuild ($$$)
2056{
2057    my ($context, $language, $exe_basename) = @_;
2058
2059    # Target version is the current version.
2060    # Search instsetoo_native for the installation set.
2061    my $filename = File::Spec->catfile(
2062        $context->{'output-path'},
2063        $context->{'product-name'},
2064        $context->{'package-format'},
2065        "install",
2066        $language."_download",
2067        $exe_basename);
2068
2069    printf("        current : %s\n", $filename);
2070    if ( ! -f $filename)
2071    {
2072        printf("ERROR: can not find %s\n", $filename);
2073        return undef;
2074    }
2075    else
2076    {
2077        my $msi = installer::patch::Msi->FindAndCreate(
2078            $context->{'target-version'},
2079            1,
2080            $language,
2081            $context->{'product-name'});
2082        return CreateReleaseItem($language, $filename, $msi);
2083    }
2084}
2085
2086
2087
2088sub GetReleaseItemForOldBuild ($$$$)
2089{
2090    my ($context, $language, $exe_basename, $url_template) = @_;
2091
2092    # Use ext_sources/ as local cache for archive.apache.org
2093    # and search these for the installation set.
2094
2095    my $version = $context->{'target-version'};
2096    my $package_format =  $context->{'package-format'};
2097    my $releases_list = installer::patch::ReleasesList::Instance();
2098
2099    my $url = $url_template;
2100    $url =~ s/%L/$language/g;
2101    $releases_list->{$version}->{$package_format}->{$language}->{'URL'} = $url;
2102
2103    if ( ! installer::patch::InstallationSet::ProvideUnpackedExe(
2104               $version,
2105               0,
2106               $language,
2107               $package_format,
2108               $context->{'product-name'}))
2109    {
2110        # Can not provide unpacked EXE.
2111        return undef;
2112    }
2113    else
2114    {
2115        my $exe_filename = File::Spec->catfile(
2116            $ENV{'TARFILE_LOCATION'},
2117            $exe_basename);
2118        my $msi = installer::patch::Msi->FindAndCreate(
2119            $version,
2120            0,
2121            $language,
2122            $context->{'product-name'});
2123        return CreateReleaseItem($language, $exe_filename, $msi);
2124    }
2125}
2126
2127
2128
2129
2130sub UpdateReleasesXML($$)
2131{
2132    my ($context, $variables) = @_;
2133
2134    my $releases_list = installer::patch::ReleasesList::Instance();
2135    my $output_filename = File::Spec->catfile(
2136        $context->{'output-path'},
2137        "misc",
2138        "releases.xml");
2139
2140    my $target_version = $context->{'target-version'};
2141    my %version_hash = map {$_=>1} @{$releases_list->{'releases'}};
2142    my $item_hash = undef;
2143    if ( ! defined $version_hash{$context->{'target-version'}})
2144    {
2145        # Target version is not yet present.  Add it and print message that asks caller to check order.
2146        push @{$releases_list->{'releases'}}, $target_version;
2147        printf("adding data for new version %s to list of released versions.\n", $target_version);
2148        printf("please check order of releases in $output_filename\n");
2149        $item_hash = {};
2150    }
2151    else
2152    {
2153        printf("adding data for existing version %s to releases.xml\n", $target_version);
2154        $item_hash = $releases_list->{$target_version}->{$context->{'package-format'}};
2155    }
2156    $releases_list->{$target_version} = {$context->{'package-format'} => $item_hash};
2157
2158    my @languages = GetLanguages();
2159    my %language_items = ();
2160    foreach my $language (@languages)
2161    {
2162        # There are three different sources where to find the downloadable installation sets.
2163        # 1. archive.apache.org for previously released versions.
2164        # 2. A local cache or repository directory that conceptually is a local copy of archive.apache.org
2165        # 3. The downloadable installation sets built in instsetoo_native/.
2166
2167        my $exe_basename = sprintf(
2168            "%s_%s_Win_x86_install_%s.exe",
2169            $context->{'product-name'},
2170            $target_version,
2171            $language);
2172        my $url_template = sprintf(
2173            "http://archive.apache.org/dist/openoffice/%s/binaries/%%L/%s_%s_Win_x86_install_%%L.exe",
2174            $target_version,
2175            $context->{'product-name'},
2176            $target_version);
2177
2178        my $item = undef;
2179        if ($target_version eq $variables->{PRODUCTVERSION})
2180        {
2181            $item = GetReleaseItemForCurrentBuild($context, $language, $exe_basename);
2182        }
2183        else
2184        {
2185            $item = GetReleaseItemForOldBuild($context, $language, $exe_basename, $url_template);
2186        }
2187
2188        next unless defined $item;
2189
2190        $language_items{$language} = $item;
2191        $item_hash->{$language} = $item;
2192        $item_hash->{'upgrade-code'} = $item->{'upgrade-code'};
2193        $item_hash->{'build-id'} = $item->{'build-id'};
2194        $item_hash->{'url-template'} = $url_template;
2195    }
2196
2197    my @valid_languages = sort keys %language_items;
2198    $item_hash->{'languages'} = \@valid_languages;
2199
2200    $releases_list->Write($output_filename);
2201
2202    printf("\n\n");
2203    printf("please copy '%s' to main/instsetoo_native/data\n", $output_filename);
2204    printf("and check in the modified file to the version control system\n");
2205}
2206
2207
2208
2209
2210sub main ()
2211{
2212    my $context = ProcessCommandline();
2213#    installer::logger::starttime();
2214#    $installer::logger::Global->add_timestamp("starting logging");
2215    installer::logger::SetupSimpleLogging(undef);
2216
2217    die "ERROR: list file is not defined, please use --lst-file option"
2218        unless defined $context->{'lst-file'};
2219    die "ERROR: product name is not defined, please use --product-name option"
2220        unless defined $context->{'product-name'};
2221    die sprintf("ERROR: package format %s is not supported", $context->{'package-format'})
2222        unless defined $context->{'package-format'} ne "msi";
2223
2224    my ($variables, undef, undef) = installer::ziplist::read_openoffice_lst_file(
2225        $context->{'lst-file'},
2226        $context->{'product-name'},
2227        undef);
2228    DetermineVersions($context, $variables);
2229
2230    if ($context->{'command'} =~ /create|check/)
2231    {
2232        my $filename = File::Spec->catfile(
2233            $context->{'output-path'},
2234            $context->{'product-name'},
2235            "msp",
2236            $context->{'source-version-dash'} . "_" . $context->{'target-version-dash'},
2237            $context->{'language'},
2238            "log",
2239            "patch-creation.log");
2240        my $dirname = dirname($filename);
2241        File::Path::make_path($dirname) unless -d $dirname;
2242        printf("directing output to $filename\n");
2243
2244        $installer::logger::Lang->set_filename($filename);
2245        $installer::logger::Lang->copy_lines_from($installer::logger::Global);
2246        $installer::logger::Lang->set_forward(undef);
2247        $installer::logger::Info->set_forward($installer::logger::Lang);
2248    }
2249
2250    if ($context->{'command'} eq "create")
2251    {
2252        CreatePatch($context, $variables);
2253    }
2254    elsif ($context->{'command'} eq "apply")
2255    {
2256        ApplyPatch($context, $variables);
2257    }
2258    elsif ($context->{'command'} eq "update-releases-xml")
2259    {
2260        UpdateReleasesXML($context, $variables);
2261    }
2262    elsif ($context->{'command'} eq "check")
2263    {
2264        CheckPatchCompatability($context, $variables);
2265    }
2266}
2267
2268
2269main();
2270