xref: /aoo41x/main/solenv/bin/patch_tool.pl (revision 9f91b7e3)
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
41use strict;
42
43
44=head1 NAME
45
46    patch_tool.pl - Create Windows MSI patches.
47
48=head1 SYNOPSIS
49
50    patch_tool.pl command [options]
51
52    Commands:
53        create    create patches
54        apply     apply patches
55
56    Options:
57        -p|--product-name <product-name>
58             The product name, eg Apache_OpenOffice
59        -o|--output-path <path>
60             Path to the instsetoo_native platform output tree
61        -d|--data-path <path>
62             Path to the data directory that is expected to be under version control.
63        --source-version <major>.<minor>.<micro>
64             The version that is to be patched.
65        --target-version <major>.<minor>.<micro>
66             The version after the patch has been applied.
67
68=head1 DESCRIPTION
69
70    Creates windows MSP patch files, one for each relevant language.
71    Patches convert an installed OpenOffice to the target version.
72
73    Required data are:
74        Installation sets of the source versions
75            Taken from ext_sources/
76            Downloaded from archive.apache.org on demand
77
78        Installation set of the target version
79            This is expected to be the current version.
80
81=cut
82
83#    my $ImageFamily = "MNPapps";
84# The ImageFamily name has to have 1-8 alphanumeric characters.
85my $ImageFamily = "AOO";
86my $SourceImageName = "Source";
87my $TargetImageName = "Target";
88
89
90
91sub ProcessCommandline ()
92{
93    my $arguments = {
94        'product-name' => undef,
95        'output-path' => undef,
96        'data-path' => undef,
97        'lst-file' => undef,
98        'source-version' => undef,
99        'target-version' => undef};
100
101    if ( ! GetOptions(
102               "product-name=s", \$arguments->{'product-name'},
103               "output-path=s", \$arguments->{'output-path'},
104               "data-path=s" => \$arguments->{'data-path'},
105               "lst-file=s" => \$arguments->{'lst-file'},
106               "source-version:s" => \$arguments->{'source-version'},
107               "target-version:s" => \$arguments->{'target-version'}
108        ))
109    {
110        pod2usage(2);
111    }
112
113    # Only the command should be left in @ARGV.
114    pod2usage(2) unless scalar @ARGV == 1;
115    $arguments->{'command'} = shift @ARGV;
116
117    # At the moment we only support patches on windows.  When this
118    # is extended in the future we need the package format as an
119    # argument.
120    $arguments->{'package-format'} = "msi";
121
122    return $arguments;
123}
124
125
126
127
128sub GetSourceMsiPath ($$)
129{
130    my ($context, $language) = @_;
131    my $unpacked_path = File::Spec->catfile(
132	$context->{'output-path'},
133	$context->{'product-name'},
134        $context->{'package-format'},
135	installer::patch::Version::ArrayToDirectoryName(
136	    installer::patch::Version::StringToNumberArray(
137		$context->{'source-version'})),
138	$language);
139}
140
141
142
143
144sub GetTargetMsiPath ($$)
145{
146    my ($context, $language) = @_;
147    return File::Spec->catfile(
148        $context->{'output-path'},
149        $context->{'product-name'},
150        $context->{'package-format'},
151        "install",
152        $language);
153}
154
155
156
157sub ProvideInstallationSets ($$)
158{
159    my ($context, $language) = @_;
160
161    # Assume that the target installation set is located in the output tree.
162    my $target_path = GetTargetMsiPath($context, $language);
163    if ( ! -d $target_path)
164    {
165        installer::logger::PrintError("can not find target installation set at '%s'\n", $target_path);
166        return 0;
167    }
168    my @target_version = installer::patch::Version::StringToNumberArray($context->{'target-version'});
169    my $target_msi_file = File::Spec->catfile(
170        $target_path,
171        sprintf("openoffice%d%d%d.msi", $target_version[0], $target_version[1], $target_version[2]));
172    if ( ! -f $target_msi_file)
173    {
174        installer::logger::PrintError("can not find target msi file at '%s'\n", $target_msi_file);
175        return 0;
176    }
177
178    return 1;
179}
180
181
182
183
184sub GetLanguages ()
185{
186    # The set of languages is taken from the WITH_LANG environment variable.
187    # If that is missing or is empty then the default 'en-US' is used instead.
188    my @languages = ("en-US");
189    my $with_lang = $ENV{'WITH_LANG'};
190    if (defined $with_lang && $with_lang ne "")
191    {
192        @languages = split(/\s+/, $with_lang);
193    }
194    return @languages;
195}
196
197
198
199
200sub FindValidLanguages ($$$)
201{
202    my ($context, $release_data, $languages) = @_;
203
204    my @valid_languages = ();
205    foreach my $language (@$languages)
206    {
207        if ( ! ProvideInstallationSets($context, $language))
208        {
209            installer::logger::PrintError("    '%s' has no target installation set\n", $language);
210        }
211        elsif ( ! defined $release_data->{$language})
212        {
213            installer::logger::PrintError("    '%s' is not a released language for version %s\n",
214                $language,
215                $context->{'source-version'});
216        }
217        else
218        {
219            push @valid_languages, $language;
220        }
221    }
222
223    return @valid_languages;
224}
225
226
227
228
229sub ProvideSourceInstallationSet ($$$)
230{
231    my ($context, $language, $release_data) = @_;
232
233    my $url = $release_data->{$language}->{'URL'};
234    $url =~ /^(.*)\/([^\/]*)$/;
235    my ($location, $basename) = ($1,$2);
236
237    my $ext_sources_path = $ENV{'TARFILE_LOCATION'};
238    if ( ! -d $ext_sources_path)
239    {
240        installer::logger::PrintError("Can not determine the path to ext_sources/.\n");
241        installer::logger::PrintError("Maybe SOURCE_ROOT_DIR has not been correctly set in the environment?");
242        return 0;
243    }
244
245    # We need the unpacked installation set in <platform>/<product>/<package>/<source-version>,
246    # eg wntmsci12.pro/Apache_OpenOffice/msi/v-4-0-0.
247    my $unpacked_path = GetSourceMsiPath($context, $language);
248    if ( ! -d $unpacked_path)
249    {
250        # Make sure that the downloadable installation set (.exe) is present in ext_sources/.
251        my $filename = File::Spec->catfile($ext_sources_path, $basename);
252        if ( -f $filename)
253        {
254            PrintInfo("%s is already present in ext_sources/.  Nothing to do\n", $basename);
255        }
256        else
257        {
258            return 0 if ! installer::patch::Download($language, $release_data, $location, $basename, $filename);
259            return 0 if ! -f $filename;
260        }
261
262        # Unpack the installation set.
263        if ( -d $unpacked_path)
264        {
265            # Take the existence of the destination path as proof that the
266            # installation set was successfully unpacked before.
267        }
268        else
269        {
270            installer::patch::InstallationSet::Unpack($filename, $unpacked_path);
271        }
272    }
273}
274
275
276
277
278# Find the source and target version between which the patch will be
279# created.  Typically the target version is the current version and
280# the source version is the version of the previous release.
281sub DetermineVersions ($$)
282{
283    my ($context, $variables) = @_;
284
285    if (defined $context->{'source-version'} && defined $context->{'target-version'})
286    {
287        # Both source and target version have been specified on the
288        # command line.  There remains nothing to be be done.
289        return;
290    }
291
292    if ( ! defined $context->{'target-version'})
293    {
294        # Use the current version as target version.
295        $context->{'target-version'} = $variables->{PRODUCTVERSION};
296    }
297
298    my @target_version = installer::patch::Version::StringToNumberArray($context->{'target-version'});
299    shift @target_version;
300    my $is_target_version_major = 1;
301    foreach my $number (@target_version)
302    {
303        $is_target_version_major = 0 if ($number ne "0");
304    }
305    if ($is_target_version_major)
306    {
307        installer::logger::PrintError("can not create patch where target version is a new major version (%s)\n",
308            $context->{'target-version'});
309        die;
310    }
311
312    if ( ! defined $context->{'source-version'})
313    {
314        my $releases = installer::patch::ReleasesList::Instance();
315
316        # Search for target release in the list of previous releases.
317        # If it is found, use the previous version as source version.
318        # Otherwise use the last released version.
319        my $last_release = undef;
320        foreach my $release (@{$releases->{'releases'}})
321        {
322            last if ($release eq $context->{'target-version'});
323            $last_release = $release;
324        }
325        $context->{'source-version'} = $last_release;
326    }
327}
328
329
330
331
332=head2 CheckUpgradeCode($source_msi, $target_msi)
333
334    The 'UpgradeCode' values in the 'Property' table differs from source to target
335
336=cut
337sub CheckUpgradeCode($$)
338{
339    my ($source_msi, $target_msi) = @_;
340
341    my $source_upgrade_code = $source_msi->GetTable("Property")->GetValue("Property", "UpgradeCode", "Value");
342    my $target_upgrade_code = $target_msi->GetTable("Property")->GetValue("Property", "UpgradeCode", "Value");
343
344    if ($source_upgrade_code eq $target_upgrade_code)
345    {
346        $installer::logger::Info->printf("Error: The UpgradeCode properties have to differ but are both '%s'\n",
347            $source_upgrade_code);
348        return 0;
349    }
350    else
351    {
352        $installer::logger::Info->printf("OK: UpgradeCode values are identical\n");
353        return 1;
354    }
355}
356
357
358
359
360=head2 CheckProductCode($source_msi, $target_msi)
361
362    The 'ProductCode' values in the 'Property' tables remain the same.
363
364=cut
365sub CheckProductCode($$)
366{
367    my ($source_msi, $target_msi) = @_;
368
369    my $source_product_code = $source_msi->GetTable("Property")->GetValue("Property", "ProductCode", "Value");
370    my $target_product_code = $target_msi->GetTable("Property")->GetValue("Property", "ProductCode", "Value");
371
372    if ($source_product_code ne $target_product_code)
373    {
374        $installer::logger::Info->printf("Error: The ProductCode properties have to remain the same but are\n");
375        $installer::logger::Info->printf("       '%s' and '%s'\n",
376            $source_product_code,
377            $target_product_code);
378        return 0;
379    }
380    else
381    {
382        $installer::logger::Info->printf("OK: ProductCode properties differ\n");
383        return 1;
384    }
385}
386
387
388
389
390=head2 CheckBuildIdCode($source_msi, $target_msi)
391
392    The 'PRODUCTBUILDID' values in the 'Property' tables (not the AOO build ids) differ and the
393    target value is higher than the source value.
394
395=cut
396sub CheckBuildIdCode($$)
397{
398    my ($source_msi, $target_msi) = @_;
399
400    my $source_build_id = $source_msi->GetTable("Property")->GetValue("Property", "PRODUCTBUILDID", "Value");
401    my $target_build_id = $target_msi->GetTable("Property")->GetValue("Property", "PRODUCTBUILDID", "Value");
402
403    if ($source_build_id >= $target_build_id)
404    {
405        $installer::logger::Info->printf(
406            "Error: The PRODUCTBUILDID properties have to increase but are '%s' and '%s'\n",
407            $source_build_id,
408            $target_build_id);
409        return 0;
410    }
411    else
412    {
413        $installer::logger::Info->printf("OK: source build id is lower than target build id\n");
414        return 1;
415    }
416}
417
418
419
420
421sub CheckProductName ($$)
422{
423    my ($source_msi, $target_msi) = @_;
424
425    my $source_product_name = $source_msi->GetTable("Property")->GetValue("Property", "DEFINEDPRODUCT", "Value");
426    my $target_product_name = $target_msi->GetTable("Property")->GetValue("Property", "DEFINEDPRODUCT", "Value");
427
428    if ($source_product_name ne $target_product_name)
429    {
430        $installer::logger::Info->printf("Error: product names of are not identical:\n");
431        $installer::logger::Info->printf("       %s != %s\n", $source_product_name, $target_product_name);
432        return 0;
433    }
434    else
435    {
436        $installer::logger::Info->printf("OK: product names are identical\n");
437        return 1;
438    }
439}
440
441
442
443
444=head2 CheckRemovedFiles($source_msi, $target_msi)
445
446    Files and components must not be deleted.
447
448=cut
449sub CheckRemovedFiles($$)
450{
451    my ($source_msi, $target_msi) = @_;
452
453    # Get the 'File' tables.
454    my $source_file_table = $source_msi->GetTable("File");
455    my $target_file_table = $target_msi->GetTable("File");
456
457    # Create data structures for fast lookup.
458    my @source_files = map {$_->GetValue("File")} @{$source_file_table->GetAllRows()};
459    my %target_file_map = map {$_->GetValue("File") => $_} @{$target_file_table->GetAllRows()};
460
461    # Search for removed files (files in source that are missing from target).
462    my $removed_file_count = 0;
463    foreach my $uniquename (@source_files)
464    {
465        if ( ! defined $target_file_map{$uniquename})
466        {
467            ++$removed_file_count;
468        }
469    }
470
471    if ($removed_file_count > 0)
472    {
473        $installer::logger::Info->printf("Error: %d files have been removed\n", $removed_file_count);
474        return 0;
475    }
476    else
477    {
478        $installer::logger::Info->printf("OK: no files have been removed\n");
479        return 1;
480    }
481}
482
483
484
485
486=head2 CheckNewFiles($source_msi, $target_msi)
487
488    New files have to be in new components.
489
490=cut
491sub CheckNewFiles($$)
492{
493    my ($source_msi, $target_msi) = @_;
494
495    # Get the 'File' tables.
496    my $source_file_table = $source_msi->GetTable("File");
497    my $target_file_table = $target_msi->GetTable("File");
498
499    # Create data structures for fast lookup.
500    my %source_file_map = map {$_->GetValue("File") => $_} @{$source_file_table->GetAllRows()};
501    my @target_files = map {$_->GetValue("File")} @{$target_file_table->GetAllRows()};
502
503    # Search for added files (files in target that where not in source).
504    my $added_file_count = 0;
505    foreach my $uniquename (@target_files)
506    {
507        if ( ! defined $source_file_map{$uniquename})
508        {
509            ++$added_file_count;
510        }
511    }
512
513    if ($added_file_count > 0)
514    {
515        $installer::logger::Info->printf("Warning: %d files have been added\n", $added_file_count);
516
517        $installer::logger::Info->printf("Check for new files being part of new components is not yet implemented\n");
518
519        return 1;
520    }
521    else
522    {
523        $installer::logger::Info->printf("OK: no files have been added\n");
524        return 1;
525    }
526}
527
528
529
530
531=head2 CheckComponentSets($source_msi, $target_msi)
532
533    Components must not be removed but can be added.
534    Features of added components have also to be new.
535
536=cut
537sub CheckComponentSets($$)
538{
539    my ($source_msi, $target_msi) = @_;
540
541    # Get the 'Component' tables.
542    my $source_component_table = $source_msi->GetTable("Component");
543    my $target_component_table = $target_msi->GetTable("Component");
544
545    # Create data structures for fast lookup.
546    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
547    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
548
549    # Check that no component has been removed.
550    my @removed_components = ();
551    foreach my $componentname (keys %source_component_map)
552    {
553        if ( ! defined $target_component_map{$componentname})
554        {
555            push @removed_components, $componentname;
556        }
557    }
558    if (scalar @removed_components > 0)
559    {
560        # There are removed components.
561
562        # Check if any of them is not a registry component.
563        my $is_file_component_removed = 0;
564        foreach my $componentname (@removed_components)
565        {
566            if ($componentname !~ /^registry/)
567            {
568                $is_file_component_removed = 1;
569            }
570        }
571        if ($is_file_component_removed)
572        {
573            $installer::logger::Info->printf(
574                "Error: %d components have been removed, some of them are file components:\n",
575                scalar @removed_components);
576            $installer::logger::Info->printf("       %s\n", join(", ", @removed_components));
577            return 0;
578        }
579        else
580        {
581            $installer::logger::Info->printf(
582                "Error: %d components have been removed, all of them are registry components:\n",
583                scalar @removed_components);
584            return 0;
585        }
586    }
587
588    # Check that added components belong to new features.
589    my @added_components = ();
590    foreach my $componentname (keys %target_component_map)
591    {
592        if ( ! defined $source_component_map{$componentname})
593        {
594            push @added_components, $componentname;
595        }
596    }
597
598    if (scalar @added_components > 0)
599    {
600        # Check if any of them is not a registry component.
601        my $is_file_component_removed = 0;
602        foreach my $componentname (@removed_components)
603        {
604            if ($componentname !~ /^registry/)
605            {
606                $is_file_component_removed = 1;
607            }
608        }
609
610        if ($is_file_component_removed)
611        {
612            $installer::logger::Info->printf(
613                "Warning: %d components have been addded\n",
614                scalar @added_components);
615            $installer::logger::Info->printf(
616                "Test for new components belonging to new features has not yet been implemented\n");
617            return 0;
618        }
619        else
620        {
621            $installer::logger::Info->printf(
622                "Warning: %d components have been addded, all of them registry components\n",
623                scalar @added_components);
624        }
625    }
626
627    $installer::logger::Info->printf("OK: component sets in source and target are compatible\n");
628    return 1;
629}
630
631
632
633
634=head2 CheckComponent($source_msi, $target_msi)
635
636    In the 'Component' table the 'ComponentId' and 'Component' values
637    for corresponding componts in the source and target release have
638    to be identical.
639
640=cut
641sub CheckComponentValues($$$)
642{
643    my ($source_msi, $target_msi, $variables) = @_;
644
645    # Get the 'Component' tables.
646    my $source_component_table = $source_msi->GetTable("Component");
647    my $target_component_table = $target_msi->GetTable("Component");
648
649    # Create data structures for fast lookup.
650    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
651    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
652
653    my @differences = ();
654    my $comparison_count = 0;
655    while (my ($componentname, $source_component_row) = each %source_component_map)
656    {
657        my $target_component_row = $target_component_map{$componentname};
658        if (defined $target_component_row)
659        {
660            ++$comparison_count;
661            if ($source_component_row->GetValue("ComponentId") ne $target_component_row->GetValue("ComponentId"))
662            {
663                push @differences, [
664                    $componentname,
665                    $source_component_row->GetValue("ComponentId"),
666                    $target_component_row->GetValue("ComponentId"),
667                    $target_component_row->GetValue("Component"),
668                ];
669            }
670        }
671    }
672
673    if (scalar @differences > 0)
674    {
675        $installer::logger::Info->printf(
676            "Error: there are %d components with different 'ComponentId' values after %d comparisons.\n",
677            scalar @differences,
678            $comparison_count);
679        foreach my $item (@differences)
680        {
681            $installer::logger::Info->printf("%s  %s\n", $item->[1], $item->[2]);
682        }
683        return 0;
684    }
685    else
686    {
687        $installer::logger::Info->printf("OK: components in source and target are identical\n");
688        return 1;
689    }
690}
691
692
693
694
695=head2 CheckFileSequence($source_msi, $target_msi)
696
697    In the 'File' table the 'Sequence' numbers for corresponding files has to be identical.
698
699=cut
700sub CheckFileSequence($$)
701{
702    my ($source_msi, $target_msi) = @_;
703
704    # Get the 'File' tables.
705    my $source_file_table = $source_msi->GetTable("File");
706    my $target_file_table = $target_msi->GetTable("File");
707
708    # Create temporary data structures for fast access.
709    my %source_file_map = map {$_->GetValue("File") => $_} @{$source_file_table->GetAllRows()};
710    my %target_file_map = map {$_->GetValue("File") => $_} @{$target_file_table->GetAllRows()};
711
712    # Search files with mismatching sequence numbers.
713    my @mismatching_files = ();
714    while (my ($uniquename,$source_file_row) = each %source_file_map)
715    {
716        my $target_file_row = $target_file_map{$uniquename};
717        if (defined $target_file_row)
718        {
719            if ($source_file_row->GetValue('Sequence') ne $target_file_row->GetValue('Sequence'))
720            {
721                push @mismatching_files, [
722                    $uniquename,
723                    $source_file_row,
724                    $target_file_row
725                ];
726            }
727        }
728    }
729
730    if (scalar @mismatching_files > 0)
731    {
732        $installer::logger::Info->printf("Error: there are %d files with mismatching 'Sequence' numbers\n",
733            scalar @mismatching_files);
734        foreach my $item (@mismatching_files)
735        {
736            $installer::logger::Info->printf("    %s: %d != %d\n",
737                $item->[0],
738                $item->[1]->GetValue("Sequence"),
739                $item->[2]->GetValue("Sequence"));
740        }
741        return 0;
742    }
743    else
744    {
745        $installer::logger::Info->printf("OK: all files have matching 'Sequence' numbers\n");
746        return 1;
747    }
748}
749
750
751
752
753=head2 CheckFileSequenceUnique($source_msi, $target_msi)
754
755    In the 'File' table the 'Sequence' values have to be unique.
756
757=cut
758sub CheckFileSequenceUnique($$)
759{
760    my ($source_msi, $target_msi) = @_;
761
762    # Get the 'File' tables.
763    my $target_file_table = $target_msi->GetTable("File");
764
765    my %sequence_numbers = ();
766    my $collision_count = 0;
767    foreach my $row (@{$target_file_table->GetAllRows()})
768    {
769        my $sequence_number = $row->GetValue("Sequence");
770        if (defined $sequence_numbers{$sequence_number})
771        {
772            ++$collision_count;
773        }
774        else
775        {
776            $sequence_numbers{$sequence_number} = 1;
777        }
778    }
779
780    if ($collision_count > 0)
781    {
782        $installer::logger::Info->printf("Error: there are %d collisions ofn the sequence numbers\n",
783            $collision_count);
784        return 0;
785    }
786    else
787    {
788        $installer::logger::Info->printf("OK: sequence numbers are unique\n");
789        return 1;
790    }
791}
792
793
794
795
796=head2 CheckFileSequenceHoles ($target_msi)
797
798    Check the sequence numbers of the target msi if the n files use numbers 1..n or if there are holes.
799    Holes are reported as warnings.
800
801=cut
802sub CheckFileSequenceHoles ($$)
803{
804    my ($source_msi, $target_msi) = @_;
805
806    my $target_file_table = $target_msi->GetTable("File");
807    my %sequence_numbers = map {$_->GetValue("Sequence") => $_} @{$target_file_table->GetAllRows()};
808    my @sorted_sequence_numbers = sort {$a <=> $b} keys %sequence_numbers;
809    my $expected_next_sequence_number = 1;
810    my @holes = ();
811    foreach my $sequence_number (@sorted_sequence_numbers)
812    {
813        if ($sequence_number != $expected_next_sequence_number)
814        {
815            push @holes, [$expected_next_sequence_number, $sequence_number-1];
816        }
817        $expected_next_sequence_number = $sequence_number+1;
818    }
819    if (scalar @holes > 0)
820    {
821        $installer::logger::Info->printf("Warning: sequence numbers have %d holes\n");
822        foreach my $hole (@holes)
823        {
824            if ($hole->[0] != $hole->[1])
825            {
826                $installer::logger::Info->printf("    %d\n", $hole->[0]);
827            }
828            else
829            {
830                $installer::logger::Info->printf("    %d -> %d\n", $hole->[0], $hole->[1]);
831            }
832        }
833    }
834    else
835    {
836        $installer::logger::Info->printf("OK: there are no holes in the sequence numbers\n");
837    }
838    return 1;
839}
840
841
842
843
844=head2 CheckRegistryItems($source_msi, $target_msi)
845
846    In the 'Registry' table the 'Component_' and 'Key' values must not
847    depend on the version number (beyond the unchanging major
848    version).
849
850    'Value' values must only depend on the major version number to
851    avoid duplicate entries in the start menu.
852
853    Violations are reported as warnings for now.
854
855=cut
856sub CheckRegistryItems($$$)
857{
858    my ($source_msi, $target_msi, $product_name) = @_;
859
860    # Get the registry tables.
861    my $source_registry_table = $source_msi->GetTable("Registry");
862    my $target_registry_table = $target_msi->GetTable("Registry");
863
864    my $registry_index = $target_registry_table->GetColumnIndex("Registry");
865    my $component_index = $target_registry_table->GetColumnIndex("Component_");
866
867    # Create temporary data structures for fast access.
868    my %source_registry_map = map {$_->GetValue($registry_index) => $_} @{$source_registry_table->GetAllRows()};
869    my %target_registry_map = map {$_->GetValue($registry_index) => $_} @{$target_registry_table->GetAllRows()};
870
871    # Prepare version numbers to search.
872    my $source_version_number = $source_msi->{'version'};
873    my $source_version_nodots = installer::patch::Version::ArrayToNoDotName(
874        installer::patch::Version::StringToNumberArray($source_version_number));
875    my $source_component_pattern = lc($product_name).$source_version_nodots;
876    my $target_version_number = $target_msi->{'version'};
877    my $target_version_nodots = installer::patch::Version::ArrayToNoDotName(
878        installer::patch::Version::StringToNumberArray($target_version_number));
879    my $target_component_pattern = lc($product_name).$target_version_nodots;
880
881    foreach my $source_row (values %source_registry_map)
882    {
883        my $target_row = $target_registry_map{$source_row->GetValue($registry_index)};
884        if ( ! defined $target_row)
885        {
886            $installer::logger::Info->printf("Error: sets of registry entries differs\n");
887            return 1;
888        }
889
890        my $source_component_name = $source_row->GetValue($component_index);
891        my $target_component_name = $source_row->GetValue($component_index);
892
893    }
894
895    $installer::logger::Info->printf("OK: registry items are OK\n");
896    return 1;
897}
898
899
900
901
902=head2
903
904    Component->KeyPath must not change. (see component.pm/get_component_keypath)
905
906=cut
907sub CheckComponentKeyPath ($$)
908{
909    my ($source_msi, $target_msi) = @_;
910
911    # Get the registry tables.
912    my $source_component_table = $source_msi->GetTable("Component");
913    my $target_component_table = $target_msi->GetTable("Component");
914
915    # Create temporary data structures for fast access.
916    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
917    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
918
919    my @mismatches = ();
920    while (my ($componentname, $source_component_row) = each %source_component_map)
921    {
922        my $target_component_row = $target_component_map{$componentname};
923        if (defined $target_component_row)
924        {
925            my $source_keypath = $source_component_row->GetValue("KeyPath");
926            my $target_keypath = $target_component_row->GetValue("KeyPath");
927            if ($source_keypath ne $target_keypath)
928            {
929                push @mismatches, [$componentname, $source_keypath, $target_keypath];
930            }
931        }
932    }
933
934    if (scalar @mismatches > 0)
935    {
936        $installer::logger::Info->printf(
937            "Error: there are %d mismatches in the 'KeyPath' column of the 'Component' table\n",
938            scalar @mismatches);
939
940        foreach my $item (@mismatches)
941        {
942            $installer::logger::Info->printf(
943                "    %s: %s != %s\n",
944                $item->[0],
945                $item->[1],
946                $item->[2]);
947        }
948
949        return 0;
950    }
951    else
952    {
953        $installer::logger::Info->printf(
954            "OK: no mismatches in the 'KeyPath' column of the 'Component' table\n");
955        return 1;
956    }
957}
958
959
960
961
962sub Check ($$$$)
963{
964    my ($source_msi, $target_msi, $variables, $product_name) = @_;
965
966    $installer::logger::Info->printf("checking if source and target releases are compatable\n");
967    $installer::logger::Info->increase_indentation();
968
969    my $result = 1;
970
971    $result &&= CheckUpgradeCode($source_msi, $target_msi);
972    $result &&= CheckProductCode($source_msi, $target_msi);
973    $result &&= CheckBuildIdCode($source_msi, $target_msi);
974    $result &&= CheckProductName($source_msi, $target_msi);
975    $result &&= CheckRemovedFiles($source_msi, $target_msi);
976    $result &&= CheckNewFiles($source_msi, $target_msi);
977    $result &&= CheckComponentSets($source_msi, $target_msi);
978    $result &&= CheckComponentValues($source_msi, $target_msi, $variables);
979    $result &&= CheckFileSequence($source_msi, $target_msi);
980    $result &&= CheckFileSequenceUnique($source_msi, $target_msi);
981    $result &&= CheckFileSequenceHoles($source_msi, $target_msi);
982    $result &&= CheckRegistryItems($source_msi, $target_msi, $product_name);
983    $result &&= CheckComponentKeyPath($source_msi, $target_msi);
984
985    $installer::logger::Info->decrease_indentation();
986
987    return $result;
988}
989
990
991
992
993=head2 FindPcpTemplate ()
994
995    The template.pcp file is part of the Windows SDK.
996
997=cut
998sub FindPcpTemplate ()
999{
1000    my $psdk_home = $ENV{'PSDK_HOME'};
1001    if ( ! defined $psdk_home)
1002    {
1003        $installer::logger::Info->printf("Error: the PSDK_HOME environment variable is not set.\n");
1004        $installer::logger::Info->printf("       did you load the AOO build environment?\n");
1005        $installer::logger::Info->printf("       you may want to use the --with-psdk-home configure option\n");
1006        return undef;
1007    }
1008    if ( ! -d $psdk_home)
1009    {
1010        $installer::logger::Info->printf(
1011            "Error: the PSDK_HOME environment variable does not point to a valid directory: %s\n",
1012            $psdk_home);
1013        return undef;
1014    }
1015
1016    my $schema_path = File::Spec->catfile($psdk_home, "Bin", "msitools", "Schemas", "MSI");
1017    if (  ! -d $schema_path)
1018    {
1019        $installer::logger::Info->printf("Error: Can not locate the msi template folder in the Windows SDK\n");
1020        $installer::logger::Info->printf("       %s\n", $schema_path);
1021        $installer::logger::Info->printf("       Is the Windows SDK properly installed?\n");
1022        return undef;
1023    }
1024
1025    my $schema_filename = File::Spec->catfile($schema_path, "template.pcp");
1026    if (  ! -f $schema_filename)
1027    {
1028        $installer::logger::Info->printf("Error: Can not locate the pcp template at\n");
1029        $installer::logger::Info->printf("       %s\n", $schema_filename);
1030        $installer::logger::Info->printf("       Is the Windows SDK properly installed?\n");
1031        return undef;
1032    }
1033
1034    return $schema_filename;
1035}
1036
1037
1038
1039
1040sub SetupPcpPatchMetadataTable ($$$)
1041{
1042    my ($pcp, $source_msi, $target_msi) = @_;
1043
1044    # Determine values for eg product name and source and new version.
1045    my $source_version = $source_msi->{'version'};
1046    my $target_version = $target_msi->{'version'};
1047
1048    my $property_table = $target_msi->GetTable("Property");
1049    my $display_product_name = $property_table->GetValue("Property", "DEFINEDPRODUCT", "Value");
1050
1051    # Set table.
1052    my $table = $pcp->GetTable("PatchMetadata");
1053    $table->SetRow(
1054        "Company", "",
1055        "*Property", "Description",
1056        "Value", sprintf("Update of %s from %s to %s", $display_product_name, $source_version, $target_version)
1057        );
1058    $table->SetRow(
1059        "Company", "",
1060        "*Property", "DisplayName",
1061        "Value", sprintf("Update of %s from %s to %s", $display_product_name, $source_version, $target_version)
1062        );
1063    $table->SetRow(
1064        "Company", "",
1065        "*Property", "ManufacturerName",
1066        "Value", $property_table->GetValue("Property", "Manufacturer", "Value"),
1067        );
1068    $table->SetRow(
1069        "Company", "",
1070        "*Property", "MoreInfoURL",
1071        "Value", $property_table->GetValue("Property", "ARPURLINFOABOUT", "Value")
1072        );
1073    $table->SetRow(
1074        "Company", "",
1075        "*Property", "TargetProductName",
1076        "Value", $property_table->GetValue("Property", "ProductName", "Value")
1077        );
1078    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time);
1079
1080    $table->SetRow(
1081        "Company", "",
1082        "*Property", "CreationTimeUTC",
1083        "Value", sprintf("%d/%d/%d %d:%02d", $mon+1,$mday,$year+1900,$hour,$min)
1084        );
1085}
1086
1087
1088
1089
1090sub SetupPropertiesTable ($$)
1091{
1092    my ($pcp, $msp_filename) = @_;
1093
1094    my $table = $pcp->GetTable("Properties");
1095
1096    $table->SetRow(
1097        "*Name", "PatchOutputPath",
1098        "Value", installer::patch::Tools::ToWindowsPath($msp_filename)
1099        );
1100    # Request at least Windows installer 2.0.
1101    # Version 2.0 allows us to omit some values from ImageFamilies table.
1102    $table->SetRow(
1103        "*Name", "MinimumRequiredMsiVersion",
1104        "Value", 200
1105        );
1106    # Allow diffs for binary files.
1107    $table->SetRow(
1108        "*Name", "IncludeWholeFilesOnly",
1109        "Value", 0
1110        );
1111
1112    my $uuid = installer::windows::msiglobal::create_guid();
1113    my $uuid_string = "{" . $uuid . "}";
1114    $table->SetRow(
1115        "*Name", "PatchGUID",
1116        "Value", $uuid_string
1117        );
1118    $installer::logger::Info->printf("created new PatchGUID %s\n", $uuid_string);
1119
1120    # Prevent sequence table from being generated.
1121    $table->SetRow(
1122        "*Name", "SEQUENCE_DATA_GENERATION_DISABLED",
1123        "Value", 1);
1124}
1125
1126
1127
1128
1129sub SetupImageFamiliesTable ($)
1130{
1131    my ($pcp) = @_;
1132
1133    $pcp->GetTable("ImageFamilies")->SetRow(
1134        "Family", $ImageFamily,
1135        "MediaSrcPropName", "",#"MNPSrcPropName",
1136        "MediaDiskId", "",
1137        "FileSequenceStart", "",
1138        "DiskPrompt", "",
1139        "VolumeLabel", "");
1140}
1141
1142
1143
1144
1145sub SetupUpgradedImagesTable ($$)
1146{
1147    my ($pcp, $target_msi_path) = @_;
1148
1149    my $msi_path = installer::patch::Tools::ToWindowsPath($target_msi_path);
1150    $pcp->GetTable("UpgradedImages")->SetRow(
1151        "Upgraded", $TargetImageName,
1152        "MsiPath", $msi_path,
1153        "PatchMsiPath", "",
1154        "SymbolPaths", "",
1155        "Family", $ImageFamily);
1156}
1157
1158
1159
1160
1161sub SetupTargetImagesTable ($$)
1162{
1163    my ($pcp, $source_msi_path) = @_;
1164
1165    $pcp->GetTable("TargetImages")->SetRow(
1166        "Target", $SourceImageName,
1167        "MsiPath", installer::patch::Tools::ToWindowsPath($source_msi_path),
1168        "SymbolPaths", "",
1169        "Upgraded", $TargetImageName,
1170        "Order", 1,
1171        "ProductValidateFlags", "",
1172        "IgnoreMissingSrcFiles", 0);
1173}
1174
1175
1176
1177
1178sub SetAdditionalValues ($%)
1179{
1180    my ($pcp, %data) = @_;
1181
1182    while (my ($key,$value) = each(%data))
1183    {
1184        $key =~ /^([^\/]+)\/([^:]+):(.+)$/
1185            || die("invalid key format");
1186        my ($table_name, $key_column,$key_value) = ($1,$2,$3);
1187        $value =~ /^([^:]+):(.*)$/
1188            || die("invalid value format");
1189        my ($value_column,$value_value) = ($1,$2);
1190
1191        my $table = $pcp->GetTable($table_name);
1192        $table->SetRow(
1193                "*".$key_column, $key_value,
1194                $value_column, $value_value);
1195    }
1196}
1197
1198
1199
1200
1201sub CreatePcp ($$$$$$%)
1202{
1203    my ($source_msi,
1204        $target_msi,
1205        $language,
1206        $context,
1207        $msp_path,
1208        $pcp_schema_filename,
1209        %additional_values) = @_;
1210
1211    # Create filenames.
1212    my $pcp_filename = File::Spec->catfile($msp_path, "openoffice.pcp");
1213    my $msp_filename = File::Spec->catfile($msp_path, "openoffice.msp");
1214
1215    # Setup msp path and filename.
1216    unlink($pcp_filename) if -f $pcp_filename;
1217    if ( ! File::Copy::copy($pcp_schema_filename, $pcp_filename))
1218    {
1219        $installer::logger::Info->printf("Error: could not create openoffice.pcp as copy of pcp schema\n");
1220        $installer::logger::Info->printf("       %s\n", $pcp_schema_filename);
1221        $installer::logger::Info->printf("       %s\n", $pcp_filename);
1222        return undef;
1223    }
1224    my $pcp = installer::patch::Msi->new(
1225        $pcp_filename,
1226        undef,
1227        undef,
1228        $language,
1229        $context->{'product-name'});
1230
1231    # Store some values in the pcp for easy reference in the msp creation.
1232    $pcp->{'msp_filename'} = $msp_filename;
1233
1234    SetupPcpPatchMetadataTable($pcp, $source_msi, $target_msi);
1235    SetupPropertiesTable($pcp, $msp_filename);
1236    SetupImageFamiliesTable($pcp);
1237    SetupUpgradedImagesTable($pcp, $target_msi->{'filename'});
1238    SetupTargetImagesTable($pcp, $source_msi->{'filename'});
1239
1240    SetAdditionalValues(%additional_values);
1241
1242    $pcp->Commit();
1243
1244    # Remove the PatchSequence table to avoid MsiMsp error message:
1245    # "Since MSI 3.0 will block installation of major upgrade patches with
1246    #  sequencing information, creation of such patches is blocked."
1247    #$pcp->RemoveTable("PatchSequence");
1248    # TODO: alternatively add property SEQUENCE_DATA_GENERATION_DISABLED to pcp Properties table.
1249
1250
1251    $installer::logger::Info->printf("created pcp file at\n");
1252    $installer::logger::Info->printf("    %s\n", $pcp->{'filename'});
1253
1254    return $pcp;
1255}
1256
1257
1258
1259
1260sub ShowLog ($$$$)
1261{
1262    my ($log_path, $log_filename, $log_basename, $new_title) = @_;
1263
1264    if ( -f $log_filename)
1265    {
1266        my $destination_path = File::Spec->catfile($log_path, $log_basename);
1267        File::Path::make_path($destination_path) if ! -d $destination_path;
1268        my $command = join(" ",
1269            "wilogutl.exe",
1270            "/q",
1271            "/l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1272            "/o", "'".installer::patch::Tools::ToWindowsPath($destination_path)."'");
1273        printf("running command $command\n");
1274        my $response = qx($command);
1275        printf("response is '%s'\n", $response);
1276        my @candidates = glob($destination_path . "/Details*");
1277        foreach my $candidate (@candidates)
1278        {
1279            next unless -f $candidate;
1280            my $new_name = $candidate;
1281            $new_name =~ s/Details.*$/$log_basename.html/;
1282
1283            # Rename the top-level html file and replace the title.
1284            open my $in, "<", $candidate;
1285            open my $out, ">", $new_name;
1286            while (<$in>)
1287            {
1288                if (/^(.*\<title\>)([^<]+)(.*)$/)
1289                {
1290                    print $out $1.$new_title.$3;
1291                }
1292                else
1293                {
1294                    print $out $_;
1295                }
1296            }
1297            close $in;
1298            close $out;
1299
1300            my $URL = $new_name;
1301            $URL =~ s/\/c\//c|\//;
1302            $URL =~ s/^(.):/$1|/;
1303            $URL = "file:///". $URL;
1304            $installer::logger::Info->printf("open %s in your browser to see the log messages\n", $URL);
1305        }
1306    }
1307    else
1308    {
1309        $installer::logger::Info->printf("Error: log file not found at %s\n", $log_filename);
1310    }
1311}
1312
1313
1314
1315
1316sub CreateMsp ($)
1317{
1318    my ($pcp) = @_;
1319
1320    # Prepare log files.
1321    my $log_path = File::Spec->catfile($pcp->{'path'}, "log");
1322    my $log_basename = "msp";
1323    my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
1324    my $performance_log_basename = "performance";
1325    my $performance_log_filename = File::Spec->catfile($log_path, $performance_log_basename.".log");
1326    File::Path::make_path($log_path) if ! -d $log_path;
1327    unlink($log_filename) if -f $log_filename;
1328    unlink($performance_log_filename) if -f $performance_log_filename;
1329
1330    # Create the .msp patch file.
1331    my $temporary_msimsp_path = File::Spec->catfile($pcp->{'path'}, "tmp");
1332    if ( ! -d $temporary_msimsp_path)
1333    {
1334        File::Path::make_path($temporary_msimsp_path)
1335            || die ("can not create temporary path ".$temporary_msimsp_path);
1336    }
1337    $installer::logger::Info->printf("running msimsp.exe, that will take a while\n");
1338    my $command = join(" ",
1339        "msimsp.exe",
1340        "-s", "'".installer::patch::Tools::ToWindowsPath($pcp->{'filename'})."'",
1341        "-p", "'".installer::patch::Tools::ToWindowsPath($pcp->{'msp_filename'})."'",
1342        "-l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1343        "-f", "'".installer::patch::Tools::ToWindowsPath($temporary_msimsp_path)."'");
1344#	    "-lp", MsiTools::ToEscapedWindowsPath($performance_log_filename),
1345    $installer::logger::Info->printf("running command %s\n", $command);
1346    my $response = qx($command);
1347    $installer::logger::Info->printf("response of msimsp is %s\n", $response);
1348    if ( ! -d $temporary_msimsp_path)
1349    {
1350        die("msimsp failed and deleted temporary path ".$temporary_msimsp_path);
1351    }
1352
1353    # Show the log file that was created by the msimsp.exe command.
1354    ShowLog($log_path, $log_filename, $log_basename, "msp creation");
1355    ShowLog($log_path, $performance_log_filename, $performance_log_basename, "msp creation perf");
1356}
1357
1358
1359
1360sub CreatePatch ($$)
1361{
1362    my ($context, $variables) = @_;
1363
1364    $installer::logger::Info->printf("patch will update product %s from %s to %s\n",
1365        $context->{'product-name'},
1366        $context->{'source-version'},
1367        $context->{'target-version'});
1368
1369    # Locate the Pcp schema file early on to report any errors before the lengthy operations that follow.
1370    my $pcp_schema_filename = FindPcpTemplate();
1371    if ( ! defined $pcp_schema_filename)
1372    {
1373        exit(1);
1374    }
1375
1376    my $release_data = installer::patch::ReleasesList::Instance()
1377        ->{$context->{'source-version'}}
1378        ->{$context->{'package-format'}};
1379
1380    # Create a patch for each language.
1381    my @requested_languages = GetLanguages();
1382    my @valid_languages = FindValidLanguages($context, $release_data, \@requested_languages);
1383    $installer::logger::Info->printf("of the requested languages '%s' are valid: '%s'\n",
1384        join("', '", @requested_languages),
1385        join("', '", @valid_languages));
1386    foreach my $language (@valid_languages)
1387    {
1388        $installer::logger::Info->printf("processing language '%s'\n", $language);
1389        $installer::logger::Info->increase_indentation();
1390
1391        # Provide .msi and .cab files and unpacke .cab for the source release.
1392        $installer::logger::Info->printf("locating source package (%s)\n", $context->{'source-version'});
1393        $installer::logger::Info->increase_indentation();
1394        if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
1395            $context->{'source-version'},
1396            0,
1397            $language,
1398            "msi",
1399            $context->{'product-name'}))
1400        {
1401            die "could not provide unpacked .cab file";
1402        }
1403        my $source_msi = installer::patch::Msi->FindAndCreate(
1404            $context->{'source-version'},
1405            0,
1406            $language,
1407            $context->{'product-name'});
1408        die unless $source_msi->IsValid();
1409
1410        $installer::logger::Info->decrease_indentation();
1411
1412        # Provide .msi and .cab files and unpacke .cab for the target release.
1413        $installer::logger::Info->printf("locating target package (%s)\n", $context->{'target-version'});
1414        $installer::logger::Info->increase_indentation();
1415        if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
1416            $context->{'target-version'},
1417            1,
1418            $language,
1419            "msi",
1420            $context->{'product-name'}))
1421        {
1422            die;
1423        }
1424        my $target_msi = installer::patch::Msi->FindAndCreate(
1425            $context->{'target-version'},
1426            0,
1427            $language,
1428            $context->{'product-name'});
1429        die unless defined $target_msi;
1430        die unless $target_msi->IsValid();
1431        $installer::logger::Info->decrease_indentation();
1432
1433        # Trigger reading of tables.
1434        foreach my $table_name (("File", "Component", "Registry"))
1435        {
1436            $source_msi->GetTable($table_name);
1437            $target_msi->GetTable($table_name);
1438            $installer::logger::Info->printf("read %s table (source and target\n", $table_name);
1439        }
1440
1441        # Check if the source and target msis fullfil all necessary requirements.
1442        if ( ! Check($source_msi, $target_msi, $variables, $context->{'product-name'}))
1443        {
1444            $installer::logger::Info->printf("Error: Source and target releases are not compatible.\n");
1445            $installer::logger::Info->printf("       => Can not create patch.\n");
1446            $installer::logger::Info->printf("       Did you create the target installation set with 'release=t' ?\n");
1447            exit(1);
1448        }
1449        else
1450        {
1451            $installer::logger::Info->printf("OK: Source and target releases are compatible.\n");
1452        }
1453
1454        # Provide the base path for creating .pcp and .mcp file.
1455        my $msp_path = File::Spec->catfile(
1456            $context->{'output-path'},
1457            $context->{'product-name'},
1458            "msp",
1459            sprintf("%s_%s",
1460              installer::patch::Version::ArrayToDirectoryName(
1461                installer::patch::Version::StringToNumberArray(
1462                    $source_msi->{'version'})),
1463              installer::patch::Version::ArrayToDirectoryName(
1464                installer::patch::Version::StringToNumberArray(
1465                    $target_msi->{'version'}))),
1466            $language
1467            );
1468        File::Path::make_path($msp_path) unless -d $msp_path;
1469
1470        # Create the .pcp file that drives the msimsp.exe command.
1471        my $pcp = CreatePcp(
1472            $source_msi,
1473            $target_msi,
1474            $language,
1475            $context,
1476            $msp_path,
1477            $pcp_schema_filename,
1478            "Properties/Name:DontRemoveTempFolderWhenFinished" => "Value:1",
1479            "Properties/Name:SEQUENCE_DATA_GENERATION_DISABLED" => "Value:1",
1480            "Properties/Name:TrustMsi" => "Value:0",
1481            "Properties/Name:ProductName" => "Value:OOO341");
1482
1483        # Finally create the msp.
1484        CreateMsp($pcp);
1485
1486        $installer::logger::Info->decrease_indentation();
1487    }
1488}
1489
1490
1491
1492
1493sub ApplyPatch ($$)
1494{
1495    my ($context, $variables) = @_;
1496
1497    $installer::logger::Info->printf("will apply patches that update product %s from %s to %s\n",
1498        $context->{'product-name'},
1499        $context->{'source-version'},
1500        $context->{'target-version'});
1501    my @languages = GetLanguages();
1502
1503    my $source_version_dirname = installer::patch::Version::ArrayToDirectoryName(
1504      installer::patch::Version::StringToNumberArray(
1505          $context->{'source-version'}));
1506    my $target_version_dirname = installer::patch::Version::ArrayToDirectoryName(
1507      installer::patch::Version::StringToNumberArray(
1508          $context->{'target-version'}));
1509
1510    foreach my $language (@languages)
1511    {
1512        my $msp_filename = File::Spec->catfile(
1513            $context->{'output-path'},
1514            $context->{'product-name'},
1515            "msp",
1516            $source_version_dirname . "_" . $target_version_dirname,
1517            $language,
1518            "openoffice.msp");
1519        if ( ! -f $msp_filename)
1520        {
1521            $installer::logger::Info->printf("%s does not point to a valid file\n", $msp_filename);
1522            next;
1523        }
1524
1525        my $log_path = File::Spec->catfile(dirname($msp_filename), "log");
1526        my $log_basename = "apply-msp";
1527        my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
1528
1529        my $command = join(" ",
1530            "msiexec.exe",
1531            "/update", "'".installer::patch::Tools::ToWindowsPath($msp_filename)."'",
1532            "/L*xv!", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1533            "REINSTALL=ALL",
1534#            "REINSTALLMODE=vomus",
1535            "REINSTALLMODE=omus",
1536            "MSIENFORCEUPGRADECOMPONENTRULES=1");
1537
1538        printf("executing command %s\n", $command);
1539        my $response = qx($command);
1540        Encode::from_to($response, "UTF16LE", "UTF8");
1541        printf("response was '%s'\n", $response);
1542
1543        ShowLog($log_path, $log_filename, $log_basename, "msp application");
1544    }
1545}
1546
1547
1548
1549
1550sub main ()
1551{
1552    installer::logger::SetupSimpleLogging(undef);
1553    my $context = ProcessCommandline();
1554    my ($variables, undef, undef) = installer::ziplist::read_openoffice_lst_file(
1555        $context->{'lst-file'},
1556        $context->{'product-name'},
1557        undef);
1558    DetermineVersions($context, $variables);
1559
1560    if ($context->{'command'} eq "create")
1561    {
1562        CreatePatch($context, $variables);
1563    }
1564    elsif ($context->{'command'} eq "apply")
1565    {
1566        ApplyPatch($context, $variables);
1567    }
1568}
1569
1570
1571main();
1572