xref: /trunk/main/solenv/bin/patch_tool.pl (revision 677600b0)
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 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 compatable\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    my $msp_filename = File::Spec->catfile($msp_path, "openoffice.msp");
1553
1554    # Setup msp path and filename.
1555    unlink($pcp_filename) if -f $pcp_filename;
1556    if ( ! File::Copy::copy($pcp_schema_filename, $pcp_filename))
1557    {
1558        $installer::logger::Info->printf("Error: could not create openoffice.pcp as copy of pcp schema\n");
1559        $installer::logger::Info->printf("       %s\n", $pcp_schema_filename);
1560        $installer::logger::Info->printf("       %s\n", $pcp_filename);
1561        return undef;
1562    }
1563    my $pcp = installer::patch::Msi->new(
1564        $pcp_filename,
1565        $target_msi->{'version'},
1566        $target_msi->{'is_current_version'},
1567        $language,
1568        $context->{'product-name'});
1569
1570    # Store some values in the pcp for easy reference in the msp creation.
1571    $pcp->{'msp_filename'} = $msp_filename;
1572
1573    SetupPcpPatchMetadataTable($pcp, $source_msi, $target_msi);
1574    SetupPropertiesTable($pcp, $msp_filename);
1575    SetupImageFamiliesTable($pcp);
1576    SetupUpgradedImagesTable($pcp, $target_msi->{'filename'});
1577    SetupTargetImagesTable($pcp, $source_msi->{'filename'});
1578
1579    SetAdditionalValues(%additional_values);
1580
1581    $pcp->Commit();
1582
1583    # Remove the PatchSequence table to avoid MsiMsp error message:
1584    # "Since MSI 3.0 will block installation of major upgrade patches with
1585    #  sequencing information, creation of such patches is blocked."
1586    #$pcp->RemoveTable("PatchSequence");
1587    # TODO: alternatively add property SEQUENCE_DATA_GENERATION_DISABLED to pcp Properties table.
1588
1589
1590    $installer::logger::Info->printf("created pcp file at\n");
1591    $installer::logger::Info->printf("    %s\n", $pcp->{'filename'});
1592
1593    return $pcp;
1594}
1595
1596
1597
1598
1599sub ShowLog ($$$$)
1600{
1601    my ($log_path, $log_filename, $log_basename, $new_title) = @_;
1602
1603    if ( -f $log_filename)
1604    {
1605        my $destination_path = File::Spec->catfile($log_path, $log_basename);
1606        File::Path::make_path($destination_path) if ! -d $destination_path;
1607        my $command = join(" ",
1608            "wilogutl.exe",
1609            "/q",
1610            "/l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1611            "/o", "'".installer::patch::Tools::ToWindowsPath($destination_path)."'");
1612        printf("running command $command\n");
1613        my $response = qx($command);
1614        my @candidates = glob($destination_path . "/Details*");
1615        foreach my $candidate (@candidates)
1616        {
1617            next unless -f $candidate;
1618            my $new_name = $candidate;
1619            $new_name =~ s/Details.*$/$log_basename.html/;
1620
1621            # Rename the top-level html file and replace the title.
1622            open my $in, "<", $candidate;
1623            open my $out, ">", $new_name;
1624            while (<$in>)
1625            {
1626                if (/^(.*\<title\>)([^<]+)(.*)$/)
1627                {
1628                    print $out $1.$new_title.$3;
1629                }
1630                else
1631                {
1632                    print $out $_;
1633                }
1634            }
1635            close $in;
1636            close $out;
1637
1638            my $URL = File::Spec->rel2abs($new_name);
1639            $URL =~ s/\/cygdrive\/(.)\//$1|\//;
1640            $URL =~ s/^(.):/$1|/;
1641            $URL = "file:///". $URL;
1642            $installer::logger::Info->printf("open %s in your browser to see the log messages\n", $URL);
1643        }
1644    }
1645    else
1646    {
1647        $installer::logger::Info->printf("Error: log file not found at %s\n", $log_filename);
1648    }
1649}
1650
1651
1652
1653
1654sub CreateMsp ($)
1655{
1656    my ($pcp) = @_;
1657
1658    # Prepare log files.
1659    my $log_path = File::Spec->catfile($pcp->{'path'}, "log");
1660    my $log_basename = "msp";
1661    my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
1662    my $performance_log_basename = "performance";
1663    my $performance_log_filename = File::Spec->catfile($log_path, $performance_log_basename.".log");
1664    File::Path::make_path($log_path) if ! -d $log_path;
1665    unlink($log_filename) if -f $log_filename;
1666    unlink($performance_log_filename) if -f $performance_log_filename;
1667
1668    # Create the .msp patch file.
1669    my $temporary_msimsp_path = File::Spec->catfile($pcp->{'path'}, "tmp");
1670    if ( ! -d $temporary_msimsp_path)
1671    {
1672        File::Path::make_path($temporary_msimsp_path)
1673            || die ("can not create temporary path ".$temporary_msimsp_path);
1674    }
1675    $installer::logger::Info->printf("running msimsp.exe, that will take a while\n");
1676    my $create_performance_log = 0;
1677    my $command = join(" ",
1678        "msimsp.exe",
1679        "-s", "'".installer::patch::Tools::ToWindowsPath($pcp->{'filename'})."'",
1680        "-p", "'".installer::patch::Tools::ToWindowsPath($pcp->{'msp_filename'})."'",
1681        "-l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1682        "-f", "'".installer::patch::Tools::ToWindowsPath($temporary_msimsp_path)."'");
1683    if ($create_performance_log)
1684    {
1685        $command .= " -lp " . MsiTools::ToEscapedWindowsPath($performance_log_filename);
1686    }
1687    $installer::logger::Info->printf("running command %s\n", $command);
1688    my $response = qx($command);
1689    $installer::logger::Info->printf("response of msimsp is %s\n", $response);
1690    if ( ! -d $temporary_msimsp_path)
1691    {
1692        die("msimsp failed and deleted temporary path ".$temporary_msimsp_path);
1693    }
1694
1695    # Show the log file that was created by the msimsp.exe command.
1696    ShowLog($log_path, $log_filename, $log_basename, "msp creation");
1697    if ($create_performance_log)
1698    {
1699        ShowLog($log_path, $performance_log_filename, $performance_log_basename, "msp creation perf");
1700    }
1701}
1702
1703
1704sub ProvideMsis ($$$)
1705{
1706    my ($context, $variables, $language) = @_;
1707
1708    # 2a. Provide .msi and .cab files and unpack .cab for the source release.
1709    $installer::logger::Info->printf("locating source package (%s)\n", $context->{'source-version'});
1710    $installer::logger::Info->increase_indentation();
1711    if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
1712	       $context->{'source-version'},
1713	       0,
1714	       $language,
1715	       "msi",
1716	       $context->{'product-name'}))
1717    {
1718        die "could not provide unpacked .cab file";
1719    }
1720    my $source_msi = installer::patch::Msi->FindAndCreate(
1721        $context->{'source-version'},
1722        0,
1723        $language,
1724        $context->{'product-name'});
1725    die unless defined $source_msi;
1726    die unless $source_msi->IsValid();
1727    $installer::logger::Info->decrease_indentation();
1728
1729    # 2b. Provide .msi and .cab files and unpacked .cab for the target release.
1730    $installer::logger::Info->printf("locating target package (%s)\n", $context->{'target-version'});
1731    $installer::logger::Info->increase_indentation();
1732    if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
1733               $context->{'target-version'},
1734               1,
1735               $language,
1736               "msi",
1737               $context->{'product-name'}))
1738    {
1739        die;
1740    }
1741    my $target_msi = installer::patch::Msi->FindAndCreate(
1742        $context->{'target-version'},
1743        0,
1744        $language,
1745        $context->{'product-name'});
1746    die unless defined $target_msi;
1747    die unless $target_msi->IsValid();
1748    $installer::logger::Info->decrease_indentation();
1749
1750    return ($source_msi, $target_msi);
1751}
1752
1753
1754
1755
1756=head CreatePatch($context, $variables)
1757
1758    Create MSP patch files for all relevant languages.
1759    The different steps are:
1760    1. Determine the set of languages for which both the source and target installation sets are present.
1761    Per language:
1762        2. Unpack CAB files (for source and target).
1763        3. Check if source and target releases are compatible.
1764        4. Create the PCP driver file.
1765        5. Create the MSP patch file.
1766
1767=cut
1768sub CreatePatch ($$)
1769{
1770    my ($context, $variables) = @_;
1771
1772    $installer::logger::Info->printf("patch will update product %s from %s to %s\n",
1773        $context->{'product-name'},
1774        $context->{'source-version'},
1775        $context->{'target-version'});
1776
1777    # Locate the Pcp schema file early on to report any errors before the lengthy operations that follow.
1778    my $pcp_schema_filename = FindPcpTemplate();
1779    if ( ! defined $pcp_schema_filename)
1780    {
1781        exit(1);
1782    }
1783
1784    my $release_data = installer::patch::ReleasesList::Instance()
1785        ->{$context->{'source-version'}}
1786        ->{$context->{'package-format'}};
1787
1788    # 1. Determine the set of languages for which we can create patches.
1789    my $language = $context->{'language'};
1790    my %no_ms_lang_locale_map = map {$_=>1} @installer::globals::noMSLocaleLangs;
1791    if (defined $no_ms_lang_locale_map{$language})
1792    {
1793        $language = "en-US_".$language;
1794    }
1795
1796    if ( ! IsLanguageValid($context, $release_data, $language))
1797    {
1798        $installer::logger::Info->printf("can not create patch for language '%s'\n", $language);
1799    }
1800    else
1801    {
1802        $installer::logger::Info->printf("processing language '%s'\n", $language);
1803        $installer::logger::Info->increase_indentation();
1804
1805        my ($source_msi, $target_msi) = ProvideMsis($context, $variables, $language);
1806
1807        # Trigger reading of tables.
1808        foreach my $table_name (("File", "Component", "Registry"))
1809        {
1810            $source_msi->GetTable($table_name);
1811            $target_msi->GetTable($table_name);
1812            $installer::logger::Info->printf("read %s table (source and target\n", $table_name);
1813        }
1814
1815        # 3. Check if the source and target msis fullfil all necessary requirements.
1816        if ( ! Check($source_msi, $target_msi, $variables, $context->{'product-name'}))
1817        {
1818            exit(1);
1819        }
1820
1821        # Provide the base path for creating .pcp and .mcp file.
1822        my $msp_path = File::Spec->catfile(
1823            $context->{'output-path'},
1824            $context->{'product-name'},
1825            "msp",
1826            sprintf("%s_%s",
1827                installer::patch::Version::ArrayToDirectoryName(
1828                    installer::patch::Version::StringToNumberArray(
1829                        $source_msi->{'version'})),
1830                installer::patch::Version::ArrayToDirectoryName(
1831                    installer::patch::Version::StringToNumberArray(
1832                        $target_msi->{'version'}))),
1833            $language
1834            );
1835        File::Path::make_path($msp_path) unless -d $msp_path;
1836
1837        # 4. Create the .pcp file that drives the msimsp.exe command.
1838        my $pcp = CreatePcp(
1839            $source_msi,
1840            $target_msi,
1841            $language,
1842            $context,
1843            $msp_path,
1844            $pcp_schema_filename,
1845            "Properties/Name:DontRemoveTempFolderWhenFinished" => "Value:1");
1846
1847        # 5. Finally create the msp.
1848        CreateMsp($pcp);
1849
1850        $installer::logger::Info->decrease_indentation();
1851    }
1852}
1853
1854
1855
1856
1857sub CheckPatchCompatability ($$)
1858{
1859    my ($context, $variables) = @_;
1860
1861    $installer::logger::Info->printf("patch will update product %s from %s to %s\n",
1862        $context->{'product-name'},
1863        $context->{'source-version'},
1864        $context->{'target-version'});
1865
1866    my $release_data = installer::patch::ReleasesList::Instance()
1867        ->{$context->{'source-version'}}
1868        ->{$context->{'package-format'}};
1869
1870    # 1. Determine the set of languages for which we can create patches.
1871    my $language = $context->{'language'};
1872    my %no_ms_lang_locale_map = map {$_=>1} @installer::globals::noMSLocaleLangs;
1873    if (defined $no_ms_lang_locale_map{$language})
1874    {
1875        $language = "en-US_".$language;
1876    }
1877
1878    if ( ! IsLanguageValid($context, $release_data, $language))
1879    {
1880        $installer::logger::Info->printf("can not create patch for language '%s'\n", $language);
1881    }
1882    else
1883    {
1884        $installer::logger::Info->printf("processing language '%s'\n", $language);
1885        $installer::logger::Info->increase_indentation();
1886
1887        my ($source_msi, $target_msi) = ProvideMsis($context, $variables, $language);
1888
1889        # Trigger reading of tables.
1890        foreach my $table_name (("File", "Component", "Registry"))
1891        {
1892            $source_msi->GetTable($table_name);
1893            $target_msi->GetTable($table_name);
1894            $installer::logger::Info->printf("read %s table (source and target\n", $table_name);
1895        }
1896
1897        # 3. Check if the source and target msis fullfil all necessary requirements.
1898        if ( ! Check($source_msi, $target_msi, $variables, $context->{'product-name'}))
1899        {
1900            exit(1);
1901        }
1902    }
1903}
1904
1905
1906
1907
1908=cut ApplyPatch ($context, $variables)
1909
1910    This is for testing only.
1911    The patch is applied and (extensive) log information is created and transformed into HTML format.
1912
1913=cut
1914sub ApplyPatch ($$)
1915{
1916    my ($context, $variables) = @_;
1917
1918    $installer::logger::Info->printf("will apply patches that update product %s from %s to %s\n",
1919        $context->{'product-name'},
1920        $context->{'source-version'},
1921        $context->{'target-version'});
1922
1923    my $source_version_dirname = installer::patch::Version::ArrayToDirectoryName(
1924      installer::patch::Version::StringToNumberArray(
1925          $context->{'source-version'}));
1926    my $target_version_dirname = installer::patch::Version::ArrayToDirectoryName(
1927      installer::patch::Version::StringToNumberArray(
1928          $context->{'target-version'}));
1929
1930    my $language = $context->{'language'};
1931    my %no_ms_lang_locale_map = map {$_=>1} @installer::globals::noMSLocaleLangs;
1932    if (defined $no_ms_lang_locale_map{$language})
1933    {
1934        $language = "en-US_".$language;
1935    }
1936
1937    my $msp_filename = File::Spec->catfile(
1938        $context->{'output-path'},
1939        $context->{'product-name'},
1940        "msp",
1941        $source_version_dirname . "_" . $target_version_dirname,
1942        $language,
1943        "openoffice.msp");
1944    if ( ! -f $msp_filename)
1945    {
1946        $installer::logger::Info->printf("%s does not point to a valid file\n", $msp_filename);
1947        next;
1948    }
1949
1950    my $log_path = File::Spec->catfile(dirname($msp_filename), "log");
1951    my $log_basename = "apply-msp";
1952    my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
1953
1954    my $command = join(" ",
1955        "msiexec.exe",
1956        "/update", "'".installer::patch::Tools::ToWindowsPath($msp_filename)."'",
1957        "/L*xv!", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1958        "REINSTALL=ALL",
1959#            "REINSTALLMODE=vomus",
1960        "REINSTALLMODE=omus",
1961        "MSIENFORCEUPGRADECOMPONENTRULES=1");
1962
1963    printf("executing command %s\n", $command);
1964    my $response = qx($command);
1965    Encode::from_to($response, "UTF16LE", "UTF8");
1966    printf("response was '%s'\n", $response);
1967
1968    ShowLog($log_path, $log_filename, $log_basename, "msp application");
1969}
1970
1971
1972
1973
1974=head2 DownloadFile ($url)
1975
1976    A simpler version of InstallationSet::Download().  It is simple because it is used to
1977    setup the $release_data structure that is used by InstallationSet::Download().
1978
1979=cut
1980sub DownloadFile ($)
1981{
1982    my ($url) = shift;
1983
1984    my $agent = LWP::UserAgent->new();
1985    $agent->timeout(120);
1986    $agent->show_progress(0);
1987
1988    my $file_content = "";
1989    my $last_was_redirect = 0;
1990    my $bytes_read = 0;
1991    $agent->add_handler('response_redirect'
1992        => sub{
1993            $last_was_redirect = 1;
1994            return;
1995        });
1996    $agent->add_handler('response_data'
1997        => sub{
1998            if ($last_was_redirect)
1999            {
2000                $last_was_redirect = 0;
2001                # Throw away the data we got so far.
2002		$file_content = "";
2003            }
2004            my($response,$agent,$h,$data)=@_;
2005	    $file_content .= $data;
2006        });
2007    $agent->get($url);
2008
2009    return $file_content;
2010}
2011
2012
2013
2014
2015sub CreateReleaseItem ($$$)
2016{
2017    my ($language, $exe_filename, $msi) = @_;
2018
2019    die "can not open installation set at ".$exe_filename unless -f $exe_filename;
2020
2021    open my $in, "<", $exe_filename;
2022    my $sha256_checksum = new Digest("SHA-256")->addfile($in)->hexdigest();
2023    close $in;
2024
2025    my $filesize = -s $exe_filename;
2026
2027    # Get the product code property from the msi and strip the enclosing braces.
2028    my $product_code = $msi->GetTable("Property")->GetValue("Property", "ProductCode", "Value");
2029    $product_code =~ s/(^{|}$)//g;
2030    my $upgrade_code = $msi->GetTable("Property")->GetValue("Property", "UpgradeCode", "Value");
2031    $upgrade_code =~ s/(^{|}$)//g;
2032    my $build_id = $msi->GetTable("Property")->GetValue("Property", "PRODUCTBUILDID", "Value");
2033
2034    return {
2035        'language' => $language,
2036        'checksum-type' => "sha256",
2037        'checksum-value' => $sha256_checksum,
2038        'file-size' => $filesize,
2039        'product-code' => $product_code,
2040        'upgrade-code' => $upgrade_code,
2041        'build-id' => $build_id
2042    };
2043}
2044
2045
2046
2047
2048sub GetReleaseItemForCurrentBuild ($$$)
2049{
2050    my ($context, $language, $exe_basename) = @_;
2051
2052    # Target version is the current version.
2053    # Search instsetoo_native for the installation set.
2054    my $filename = File::Spec->catfile(
2055        $context->{'output-path'},
2056        $context->{'product-name'},
2057        $context->{'package-format'},
2058        "install",
2059        $language."_download",
2060        $exe_basename);
2061
2062    printf("        current : %s\n", $filename);
2063    if ( ! -f $filename)
2064    {
2065        printf("ERROR: can not find %s\n", $filename);
2066        return undef;
2067    }
2068    else
2069    {
2070        my $msi = installer::patch::Msi->FindAndCreate(
2071            $context->{'target-version'},
2072            1,
2073            $language,
2074            $context->{'product-name'});
2075        return CreateReleaseItem($language, $filename, $msi);
2076    }
2077}
2078
2079
2080
2081sub GetReleaseItemForOldBuild ($$$$)
2082{
2083    my ($context, $language, $exe_basename, $url_template) = @_;
2084
2085    # Use ext_sources/ as local cache for archive.apache.org
2086    # and search these for the installation set.
2087
2088    my $version = $context->{'target-version'};
2089    my $package_format =  $context->{'package-format'};
2090    my $releases_list = installer::patch::ReleasesList::Instance();
2091
2092    my $url = $url_template;
2093    $url =~ s/%L/$language/g;
2094    $releases_list->{$version}->{$package_format}->{$language}->{'URL'} = $url;
2095
2096    if ( ! installer::patch::InstallationSet::ProvideUnpackedExe(
2097               $version,
2098               0,
2099               $language,
2100               $package_format,
2101               $context->{'product-name'}))
2102    {
2103        # Can not provide unpacked EXE.
2104        return undef;
2105    }
2106    else
2107    {
2108        my $exe_filename = File::Spec->catfile(
2109            $ENV{'TARFILE_LOCATION'},
2110            $exe_basename);
2111        my $msi = installer::patch::Msi->FindAndCreate(
2112            $version,
2113            0,
2114            $language,
2115            $context->{'product-name'});
2116        return CreateReleaseItem($language, $exe_filename, $msi);
2117    }
2118}
2119
2120
2121
2122
2123sub UpdateReleasesXML($$)
2124{
2125    my ($context, $variables) = @_;
2126
2127    my $releases_list = installer::patch::ReleasesList::Instance();
2128    my $output_filename = File::Spec->catfile(
2129        $context->{'output-path'},
2130        "misc",
2131        "releases.xml");
2132
2133    my $target_version = $context->{'target-version'};
2134    my %version_hash = map {$_=>1} @{$releases_list->{'releases'}};
2135    my $item_hash = undef;
2136    if ( ! defined $version_hash{$context->{'target-version'}})
2137    {
2138        # Target version is not yet present.  Add it and print message that asks caller to check order.
2139        push @{$releases_list->{'releases'}}, $target_version;
2140        printf("adding data for new version %s to list of released versions.\n", $target_version);
2141        printf("please check order of releases in $output_filename\n");
2142        $item_hash = {};
2143    }
2144    else
2145    {
2146        printf("adding data for existing version %s to releases.xml\n", $target_version);
2147        $item_hash = $releases_list->{$target_version}->{$context->{'package-format'}};
2148    }
2149    $releases_list->{$target_version} = {$context->{'package-format'} => $item_hash};
2150
2151    my @languages = GetLanguages();
2152    my %language_items = ();
2153    foreach my $language (@languages)
2154    {
2155        # There are three different sources where to find the downloadable installation sets.
2156        # 1. archive.apache.org for previously released versions.
2157        # 2. A local cache or repository directory that conceptually is a local copy of archive.apache.org
2158        # 3. The downloadable installation sets built in instsetoo_native/.
2159
2160        my $exe_basename = sprintf(
2161            "%s_%s_Win_x86_install_%s.exe",
2162            $context->{'product-name'},
2163            $target_version,
2164            $language);
2165        my $url_template = sprintf(
2166            "http://archive.apache.org/dist/openoffice/%s/binaries/%%L/%s_%s_Win_x86_install_%%L.exe",
2167            $target_version,
2168            $context->{'product-name'},
2169            $target_version);
2170
2171        my $item = undef;
2172        if ($target_version eq $variables->{PRODUCTVERSION})
2173        {
2174            $item = GetReleaseItemForCurrentBuild($context, $language, $exe_basename);
2175        }
2176        else
2177        {
2178            $item = GetReleaseItemForOldBuild($context, $language, $exe_basename, $url_template);
2179        }
2180
2181        next unless defined $item;
2182
2183        $language_items{$language} = $item;
2184        $item_hash->{$language} = $item;
2185        $item_hash->{'upgrade-code'} = $item->{'upgrade-code'};
2186        $item_hash->{'build-id'} = $item->{'build-id'};
2187        $item_hash->{'url-template'} = $url_template;
2188    }
2189
2190    my @valid_languages = sort keys %language_items;
2191    $item_hash->{'languages'} = \@valid_languages;
2192
2193    $releases_list->Write($output_filename);
2194
2195    printf("\n\n");
2196    printf("please copy '%s' to main/instsetoo_native/data\n", $output_filename);
2197    printf("and check in the modified file to the version control system\n");
2198}
2199
2200
2201
2202
2203sub main ()
2204{
2205    my $context = ProcessCommandline();
2206    installer::logger::starttime();
2207    $installer::logger::Global->add_timestamp("starting logging");
2208#    installer::logger::SetupSimpleLogging(undef);
2209
2210    die "ERROR: list file is not defined, please use --lst-file option"
2211        unless defined $context->{'lst-file'};
2212    die "ERROR: product name is not defined, please use --product-name option"
2213        unless defined $context->{'product-name'};
2214    die sprintf("ERROR: package format %s is not supported", $context->{'package-format'})
2215        unless defined $context->{'package-format'} ne "msi";
2216
2217    my ($variables, undef, undef) = installer::ziplist::read_openoffice_lst_file(
2218        $context->{'lst-file'},
2219        $context->{'product-name'},
2220        undef);
2221    DetermineVersions($context, $variables);
2222
2223    if ($context->{'command'} =~ /create|check/)
2224    {
2225        $installer::logger::Lang->set_filename(
2226            File::Spec->catfile(
2227                $context->{'output-path'},
2228                $context->{'product-name'},
2229                "msp",
2230                $context->{'source-version-dash'} . "_" . $context->{'target-version-dash'},
2231                $context->{'language'},
2232                "log",
2233                "patch-creation.log"));
2234        $installer::logger::Lang->copy_lines_from($installer::logger::Global);
2235        $installer::logger::Lang->set_forward(undef);
2236        $installer::logger::Info->set_forward($installer::logger::Lang);
2237    }
2238
2239    if ($context->{'command'} eq "create")
2240    {
2241        CreatePatch($context, $variables);
2242    }
2243    elsif ($context->{'command'} eq "apply")
2244    {
2245        ApplyPatch($context, $variables);
2246    }
2247    elsif ($context->{'command'} eq "update-releases-xml")
2248    {
2249        UpdateReleasesXML($context, $variables);
2250    }
2251    elsif ($context->{'command'} eq "check")
2252    {
2253        CheckPatchCompatability($context, $variables);
2254    }
2255}
2256
2257
2258main();
2259