xref: /trunk/main/solenv/bin/patch_tool.pl (revision d575d58f)
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::InstallationSet::Download(
259                $language,
260                $release_data,
261                $filename);
262            return 0 if ! -f $filename;
263        }
264
265        # Unpack the installation set.
266        if ( -d $unpacked_path)
267        {
268            # Take the existence of the destination path as proof that the
269            # installation set was successfully unpacked before.
270        }
271        else
272        {
273            installer::patch::InstallationSet::Unpack($filename, $unpacked_path);
274        }
275    }
276}
277
278
279
280
281# Find the source and target version between which the patch will be
282# created.  Typically the target version is the current version and
283# the source version is the version of the previous release.
284sub DetermineVersions ($$)
285{
286    my ($context, $variables) = @_;
287
288    if (defined $context->{'source-version'} && defined $context->{'target-version'})
289    {
290        # Both source and target version have been specified on the
291        # command line.  There remains nothing to be be done.
292        return;
293    }
294
295    if ( ! defined $context->{'target-version'})
296    {
297        # Use the current version as target version.
298        $context->{'target-version'} = $variables->{PRODUCTVERSION};
299    }
300
301    my @target_version = installer::patch::Version::StringToNumberArray($context->{'target-version'});
302    shift @target_version;
303    my $is_target_version_major = 1;
304    foreach my $number (@target_version)
305    {
306        $is_target_version_major = 0 if ($number ne "0");
307    }
308    if ($is_target_version_major)
309    {
310        installer::logger::PrintError("can not create patch where target version is a new major version (%s)\n",
311            $context->{'target-version'});
312        die;
313    }
314
315    if ( ! defined $context->{'source-version'})
316    {
317        my $releases = installer::patch::ReleasesList::Instance();
318
319        # Search for target release in the list of previous releases.
320        # If it is found, use the previous version as source version.
321        # Otherwise use the last released version.
322        my $last_release = undef;
323        foreach my $release (@{$releases->{'releases'}})
324        {
325            last if ($release eq $context->{'target-version'});
326            $last_release = $release;
327        }
328        $context->{'source-version'} = $last_release;
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 identical\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: ProductCode properties differ\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 {$_->GetValue("File")} @{$target_file_table->GetAllRows()};
505
506    # Search for added files (files in target that where not in source).
507    my $added_file_count = 0;
508    foreach my $uniquename (@target_files)
509    {
510        if ( ! defined $source_file_map{$uniquename})
511        {
512            ++$added_file_count;
513        }
514    }
515
516    if ($added_file_count > 0)
517    {
518        $installer::logger::Info->printf("Warning: %d files have been added\n", $added_file_count);
519
520        $installer::logger::Info->printf("Check for new files being part of new components is not yet implemented\n");
521
522        return 1;
523    }
524    else
525    {
526        $installer::logger::Info->printf("OK: no files have been added\n");
527        return 1;
528    }
529}
530
531
532
533
534=head2 CheckComponentSets($source_msi, $target_msi)
535
536    Components must not be removed but can be added.
537    Features of added components have also to be new.
538
539=cut
540sub CheckComponentSets($$)
541{
542    my ($source_msi, $target_msi) = @_;
543
544    # Get the 'Component' tables.
545    my $source_component_table = $source_msi->GetTable("Component");
546    my $target_component_table = $target_msi->GetTable("Component");
547
548    # Create data structures for fast lookup.
549    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
550    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
551
552    # Check that no component has been removed.
553    my @removed_components = ();
554    foreach my $componentname (keys %source_component_map)
555    {
556        if ( ! defined $target_component_map{$componentname})
557        {
558            push @removed_components, $componentname;
559        }
560    }
561    if (scalar @removed_components > 0)
562    {
563        # There are removed components.
564
565        # Check if any of them is not a registry component.
566        my $is_file_component_removed = 0;
567        foreach my $componentname (@removed_components)
568        {
569            if ($componentname !~ /^registry/)
570            {
571                $is_file_component_removed = 1;
572            }
573        }
574        if ($is_file_component_removed)
575        {
576            $installer::logger::Info->printf(
577                "Error: %d components have been removed, some of them are file components:\n",
578                scalar @removed_components);
579            $installer::logger::Info->printf("       %s\n", join(", ", @removed_components));
580            return 0;
581        }
582        else
583        {
584            $installer::logger::Info->printf(
585                "Error: %d components have been removed, all of them are registry components:\n",
586                scalar @removed_components);
587            return 0;
588        }
589    }
590
591    # Check that added components belong to new features.
592    my @added_components = ();
593    foreach my $componentname (keys %target_component_map)
594    {
595        if ( ! defined $source_component_map{$componentname})
596        {
597            push @added_components, $componentname;
598        }
599    }
600
601    if (scalar @added_components > 0)
602    {
603        # Check if any of them is not a registry component.
604        my $is_file_component_removed = 0;
605        foreach my $componentname (@removed_components)
606        {
607            if ($componentname !~ /^registry/)
608            {
609                $is_file_component_removed = 1;
610            }
611        }
612
613        if ($is_file_component_removed)
614        {
615            $installer::logger::Info->printf(
616                "Warning: %d components have been addded\n",
617                scalar @added_components);
618            $installer::logger::Info->printf(
619                "Test for new components belonging to new features has not yet been implemented\n");
620            return 0;
621        }
622        else
623        {
624            $installer::logger::Info->printf(
625                "Warning: %d components have been addded, all of them registry components\n",
626                scalar @added_components);
627        }
628    }
629
630    $installer::logger::Info->printf("OK: component sets in source and target are compatible\n");
631    return 1;
632}
633
634
635
636
637=head2 CheckComponent($source_msi, $target_msi)
638
639    In the 'Component' table the 'ComponentId' and 'Component' values
640    for corresponding componts in the source and target release have
641    to be identical.
642
643=cut
644sub CheckComponentValues($$$)
645{
646    my ($source_msi, $target_msi, $variables) = @_;
647
648    # Get the 'Component' tables.
649    my $source_component_table = $source_msi->GetTable("Component");
650    my $target_component_table = $target_msi->GetTable("Component");
651
652    # Create data structures for fast lookup.
653    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
654    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
655
656    my @differences = ();
657    my $comparison_count = 0;
658    while (my ($componentname, $source_component_row) = each %source_component_map)
659    {
660        my $target_component_row = $target_component_map{$componentname};
661        if (defined $target_component_row)
662        {
663            ++$comparison_count;
664            if ($source_component_row->GetValue("ComponentId") ne $target_component_row->GetValue("ComponentId"))
665            {
666                push @differences, [
667                    $componentname,
668                    $source_component_row->GetValue("ComponentId"),
669                    $target_component_row->GetValue("ComponentId"),
670                    $target_component_row->GetValue("Component"),
671                ];
672            }
673        }
674    }
675
676    if (scalar @differences > 0)
677    {
678        $installer::logger::Info->printf(
679            "Error: there are %d components with different 'ComponentId' values after %d comparisons.\n",
680            scalar @differences,
681            $comparison_count);
682        foreach my $item (@differences)
683        {
684            $installer::logger::Info->printf("%s  %s\n", $item->[1], $item->[2]);
685        }
686        return 0;
687    }
688    else
689    {
690        $installer::logger::Info->printf("OK: components in source and target are identical\n");
691        return 1;
692    }
693}
694
695
696
697
698=head2 CheckFileSequence($source_msi, $target_msi)
699
700    In the 'File' table the 'Sequence' numbers for corresponding files has to be identical.
701
702=cut
703sub CheckFileSequence($$)
704{
705    my ($source_msi, $target_msi) = @_;
706
707    # Get the 'File' tables.
708    my $source_file_table = $source_msi->GetTable("File");
709    my $target_file_table = $target_msi->GetTable("File");
710
711    # Create temporary data structures for fast access.
712    my %source_file_map = map {$_->GetValue("File") => $_} @{$source_file_table->GetAllRows()};
713    my %target_file_map = map {$_->GetValue("File") => $_} @{$target_file_table->GetAllRows()};
714
715    # Search files with mismatching sequence numbers.
716    my @mismatching_files = ();
717    while (my ($uniquename,$source_file_row) = each %source_file_map)
718    {
719        my $target_file_row = $target_file_map{$uniquename};
720        if (defined $target_file_row)
721        {
722            if ($source_file_row->GetValue('Sequence') ne $target_file_row->GetValue('Sequence'))
723            {
724                push @mismatching_files, [
725                    $uniquename,
726                    $source_file_row,
727                    $target_file_row
728                ];
729            }
730        }
731    }
732
733    if (scalar @mismatching_files > 0)
734    {
735        $installer::logger::Info->printf("Error: there are %d files with mismatching 'Sequence' numbers\n",
736            scalar @mismatching_files);
737        foreach my $item (@mismatching_files)
738        {
739            $installer::logger::Info->printf("    %s: %d != %d\n",
740                $item->[0],
741                $item->[1]->GetValue("Sequence"),
742                $item->[2]->GetValue("Sequence"));
743        }
744        return 0;
745    }
746    else
747    {
748        $installer::logger::Info->printf("OK: all files have matching 'Sequence' numbers\n");
749        return 1;
750    }
751}
752
753
754
755
756=head2 CheckFileSequenceUnique($source_msi, $target_msi)
757
758    In the 'File' table the 'Sequence' values have to be unique.
759
760=cut
761sub CheckFileSequenceUnique($$)
762{
763    my ($source_msi, $target_msi) = @_;
764
765    # Get the 'File' tables.
766    my $target_file_table = $target_msi->GetTable("File");
767
768    my %sequence_numbers = ();
769    my $collision_count = 0;
770    foreach my $row (@{$target_file_table->GetAllRows()})
771    {
772        my $sequence_number = $row->GetValue("Sequence");
773        if (defined $sequence_numbers{$sequence_number})
774        {
775            ++$collision_count;
776        }
777        else
778        {
779            $sequence_numbers{$sequence_number} = 1;
780        }
781    }
782
783    if ($collision_count > 0)
784    {
785        $installer::logger::Info->printf("Error: there are %d collisions ofn the sequence numbers\n",
786            $collision_count);
787        return 0;
788    }
789    else
790    {
791        $installer::logger::Info->printf("OK: sequence numbers are unique\n");
792        return 1;
793    }
794}
795
796
797
798
799=head2 CheckFileSequenceHoles ($target_msi)
800
801    Check the sequence numbers of the target msi if the n files use numbers 1..n or if there are holes.
802    Holes are reported as warnings.
803
804=cut
805sub CheckFileSequenceHoles ($$)
806{
807    my ($source_msi, $target_msi) = @_;
808
809    my $target_file_table = $target_msi->GetTable("File");
810    my %sequence_numbers = map {$_->GetValue("Sequence") => $_} @{$target_file_table->GetAllRows()};
811    my @sorted_sequence_numbers = sort {$a <=> $b} keys %sequence_numbers;
812    my $expected_next_sequence_number = 1;
813    my @holes = ();
814    foreach my $sequence_number (@sorted_sequence_numbers)
815    {
816        if ($sequence_number != $expected_next_sequence_number)
817        {
818            push @holes, [$expected_next_sequence_number, $sequence_number-1];
819        }
820        $expected_next_sequence_number = $sequence_number+1;
821    }
822    if (scalar @holes > 0)
823    {
824        $installer::logger::Info->printf("Warning: sequence numbers have %d holes\n");
825        foreach my $hole (@holes)
826        {
827            if ($hole->[0] != $hole->[1])
828            {
829                $installer::logger::Info->printf("    %d\n", $hole->[0]);
830            }
831            else
832            {
833                $installer::logger::Info->printf("    %d -> %d\n", $hole->[0], $hole->[1]);
834            }
835        }
836    }
837    else
838    {
839        $installer::logger::Info->printf("OK: there are no holes in the sequence numbers\n");
840    }
841    return 1;
842}
843
844
845
846
847=head2 CheckRegistryItems($source_msi, $target_msi)
848
849    In the 'Registry' table the 'Component_' and 'Key' values must not
850    depend on the version number (beyond the unchanging major
851    version).
852
853    'Value' values must only depend on the major version number to
854    avoid duplicate entries in the start menu.
855
856    Violations are reported as warnings for now.
857
858=cut
859sub CheckRegistryItems($$$)
860{
861    my ($source_msi, $target_msi, $product_name) = @_;
862
863    # Get the registry tables.
864    my $source_registry_table = $source_msi->GetTable("Registry");
865    my $target_registry_table = $target_msi->GetTable("Registry");
866
867    my $registry_index = $target_registry_table->GetColumnIndex("Registry");
868    my $component_index = $target_registry_table->GetColumnIndex("Component_");
869
870    # Create temporary data structures for fast access.
871    my %source_registry_map = map {$_->GetValue($registry_index) => $_} @{$source_registry_table->GetAllRows()};
872    my %target_registry_map = map {$_->GetValue($registry_index) => $_} @{$target_registry_table->GetAllRows()};
873
874    # Prepare version numbers to search.
875    my $source_version_number = $source_msi->{'version'};
876    my $source_version_nodots = installer::patch::Version::ArrayToNoDotName(
877        installer::patch::Version::StringToNumberArray($source_version_number));
878    my $source_component_pattern = lc($product_name).$source_version_nodots;
879    my $target_version_number = $target_msi->{'version'};
880    my $target_version_nodots = installer::patch::Version::ArrayToNoDotName(
881        installer::patch::Version::StringToNumberArray($target_version_number));
882    my $target_component_pattern = lc($product_name).$target_version_nodots;
883
884    foreach my $source_row (values %source_registry_map)
885    {
886        my $target_row = $target_registry_map{$source_row->GetValue($registry_index)};
887        if ( ! defined $target_row)
888        {
889            $installer::logger::Info->printf("Error: sets of registry entries differs\n");
890            return 1;
891        }
892
893        my $source_component_name = $source_row->GetValue($component_index);
894        my $target_component_name = $source_row->GetValue($component_index);
895
896    }
897
898    $installer::logger::Info->printf("OK: registry items are OK\n");
899    return 1;
900}
901
902
903
904
905=head2
906
907    Component->KeyPath must not change. (see component.pm/get_component_keypath)
908
909=cut
910sub CheckComponentKeyPath ($$)
911{
912    my ($source_msi, $target_msi) = @_;
913
914    # Get the registry tables.
915    my $source_component_table = $source_msi->GetTable("Component");
916    my $target_component_table = $target_msi->GetTable("Component");
917
918    # Create temporary data structures for fast access.
919    my %source_component_map = map {$_->GetValue("Component") => $_} @{$source_component_table->GetAllRows()};
920    my %target_component_map = map {$_->GetValue("Component") => $_} @{$target_component_table->GetAllRows()};
921
922    my @mismatches = ();
923    while (my ($componentname, $source_component_row) = each %source_component_map)
924    {
925        my $target_component_row = $target_component_map{$componentname};
926        if (defined $target_component_row)
927        {
928            my $source_keypath = $source_component_row->GetValue("KeyPath");
929            my $target_keypath = $target_component_row->GetValue("KeyPath");
930            if ($source_keypath ne $target_keypath)
931            {
932                push @mismatches, [$componentname, $source_keypath, $target_keypath];
933            }
934        }
935    }
936
937    if (scalar @mismatches > 0)
938    {
939        $installer::logger::Info->printf(
940            "Error: there are %d mismatches in the 'KeyPath' column of the 'Component' table\n",
941            scalar @mismatches);
942
943        foreach my $item (@mismatches)
944        {
945            $installer::logger::Info->printf(
946                "    %s: %s != %s\n",
947                $item->[0],
948                $item->[1],
949                $item->[2]);
950        }
951
952        return 0;
953    }
954    else
955    {
956        $installer::logger::Info->printf(
957            "OK: no mismatches in the 'KeyPath' column of the 'Component' table\n");
958        return 1;
959    }
960}
961
962
963
964
965sub Check ($$$$)
966{
967    my ($source_msi, $target_msi, $variables, $product_name) = @_;
968
969    $installer::logger::Info->printf("checking if source and target releases are compatable\n");
970    $installer::logger::Info->increase_indentation();
971
972    my $result = 1;
973
974    $result &&= CheckUpgradeCode($source_msi, $target_msi);
975    $result &&= CheckProductCode($source_msi, $target_msi);
976    $result &&= CheckBuildIdCode($source_msi, $target_msi);
977    $result &&= CheckProductName($source_msi, $target_msi);
978    $result &&= CheckRemovedFiles($source_msi, $target_msi);
979    $result &&= CheckNewFiles($source_msi, $target_msi);
980    $result &&= CheckComponentSets($source_msi, $target_msi);
981    $result &&= CheckComponentValues($source_msi, $target_msi, $variables);
982    $result &&= CheckFileSequence($source_msi, $target_msi);
983    $result &&= CheckFileSequenceUnique($source_msi, $target_msi);
984    $result &&= CheckFileSequenceHoles($source_msi, $target_msi);
985    $result &&= CheckRegistryItems($source_msi, $target_msi, $product_name);
986    $result &&= CheckComponentKeyPath($source_msi, $target_msi);
987
988    $installer::logger::Info->decrease_indentation();
989
990    return $result;
991}
992
993
994
995
996=head2 FindPcpTemplate ()
997
998    The template.pcp file is part of the Windows SDK.
999
1000=cut
1001sub FindPcpTemplate ()
1002{
1003    my $psdk_home = $ENV{'PSDK_HOME'};
1004    if ( ! defined $psdk_home)
1005    {
1006        $installer::logger::Info->printf("Error: the PSDK_HOME environment variable is not set.\n");
1007        $installer::logger::Info->printf("       did you load the AOO build environment?\n");
1008        $installer::logger::Info->printf("       you may want to use the --with-psdk-home configure option\n");
1009        return undef;
1010    }
1011    if ( ! -d $psdk_home)
1012    {
1013        $installer::logger::Info->printf(
1014            "Error: the PSDK_HOME environment variable does not point to a valid directory: %s\n",
1015            $psdk_home);
1016        return undef;
1017    }
1018
1019    my $schema_path = File::Spec->catfile($psdk_home, "Bin", "msitools", "Schemas", "MSI");
1020    if (  ! -d $schema_path)
1021    {
1022        $installer::logger::Info->printf("Error: Can not locate the msi template folder in the Windows SDK\n");
1023        $installer::logger::Info->printf("       %s\n", $schema_path);
1024        $installer::logger::Info->printf("       Is the Windows SDK properly installed?\n");
1025        return undef;
1026    }
1027
1028    my $schema_filename = File::Spec->catfile($schema_path, "template.pcp");
1029    if (  ! -f $schema_filename)
1030    {
1031        $installer::logger::Info->printf("Error: Can not locate the pcp template at\n");
1032        $installer::logger::Info->printf("       %s\n", $schema_filename);
1033        $installer::logger::Info->printf("       Is the Windows SDK properly installed?\n");
1034        return undef;
1035    }
1036
1037    return $schema_filename;
1038}
1039
1040
1041
1042
1043sub SetupPcpPatchMetadataTable ($$$)
1044{
1045    my ($pcp, $source_msi, $target_msi) = @_;
1046
1047    # Determine values for eg product name and source and new version.
1048    my $source_version = $source_msi->{'version'};
1049    my $target_version = $target_msi->{'version'};
1050
1051    my $property_table = $target_msi->GetTable("Property");
1052    my $display_product_name = $property_table->GetValue("Property", "DEFINEDPRODUCT", "Value");
1053
1054    # Set table.
1055    my $table = $pcp->GetTable("PatchMetadata");
1056    $table->SetRow(
1057        "Company", "",
1058        "*Property", "Description",
1059        "Value", sprintf("Update of %s from %s to %s", $display_product_name, $source_version, $target_version)
1060        );
1061    $table->SetRow(
1062        "Company", "",
1063        "*Property", "DisplayName",
1064        "Value", sprintf("Update of %s from %s to %s", $display_product_name, $source_version, $target_version)
1065        );
1066    $table->SetRow(
1067        "Company", "",
1068        "*Property", "ManufacturerName",
1069        "Value", $property_table->GetValue("Property", "Manufacturer", "Value"),
1070        );
1071    $table->SetRow(
1072        "Company", "",
1073        "*Property", "MoreInfoURL",
1074        "Value", $property_table->GetValue("Property", "ARPURLINFOABOUT", "Value")
1075        );
1076    $table->SetRow(
1077        "Company", "",
1078        "*Property", "TargetProductName",
1079        "Value", $property_table->GetValue("Property", "ProductName", "Value")
1080        );
1081    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time);
1082
1083    $table->SetRow(
1084        "Company", "",
1085        "*Property", "CreationTimeUTC",
1086        "Value", sprintf("%d/%d/%d %d:%02d", $mon+1,$mday,$year+1900,$hour,$min)
1087        );
1088}
1089
1090
1091
1092
1093sub SetupPropertiesTable ($$)
1094{
1095    my ($pcp, $msp_filename) = @_;
1096
1097    my $table = $pcp->GetTable("Properties");
1098
1099    $table->SetRow(
1100        "*Name", "PatchOutputPath",
1101        "Value", installer::patch::Tools::ToWindowsPath($msp_filename)
1102        );
1103    # Request at least Windows installer 2.0.
1104    # Version 2.0 allows us to omit some values from ImageFamilies table.
1105    $table->SetRow(
1106        "*Name", "MinimumRequiredMsiVersion",
1107        "Value", 200
1108        );
1109    # Allow diffs for binary files.
1110    $table->SetRow(
1111        "*Name", "IncludeWholeFilesOnly",
1112        "Value", 0
1113        );
1114
1115    my $uuid = installer::windows::msiglobal::create_guid();
1116    my $uuid_string = "{" . $uuid . "}";
1117    $table->SetRow(
1118        "*Name", "PatchGUID",
1119        "Value", $uuid_string
1120        );
1121    $installer::logger::Info->printf("created new PatchGUID %s\n", $uuid_string);
1122
1123    # Prevent sequence table from being generated.
1124    $table->SetRow(
1125        "*Name", "SEQUENCE_DATA_GENERATION_DISABLED",
1126        "Value", 1);
1127
1128    # We don't provide file size and hash values.
1129    # This value is set to make this fact explicit (0 should be the default).
1130    $table->SetRow(
1131        "*Name", "TrustMsi",
1132        "Value", 0);
1133}
1134
1135
1136
1137
1138sub SetupImageFamiliesTable ($)
1139{
1140    my ($pcp) = @_;
1141
1142    $pcp->GetTable("ImageFamilies")->SetRow(
1143        "Family", $ImageFamily,
1144        "MediaSrcPropName", "",#"MNPSrcPropName",
1145        "MediaDiskId", "",
1146        "FileSequenceStart", "",
1147        "DiskPrompt", "",
1148        "VolumeLabel", "");
1149}
1150
1151
1152
1153
1154sub SetupUpgradedImagesTable ($$)
1155{
1156    my ($pcp, $target_msi_path) = @_;
1157
1158    my $msi_path = installer::patch::Tools::ToWindowsPath($target_msi_path);
1159    $pcp->GetTable("UpgradedImages")->SetRow(
1160        "Upgraded", $TargetImageName,
1161        "MsiPath", $msi_path,
1162        "PatchMsiPath", "",
1163        "SymbolPaths", "",
1164        "Family", $ImageFamily);
1165}
1166
1167
1168
1169
1170sub SetupTargetImagesTable ($$)
1171{
1172    my ($pcp, $source_msi_path) = @_;
1173
1174    $pcp->GetTable("TargetImages")->SetRow(
1175        "Target", $SourceImageName,
1176        "MsiPath", installer::patch::Tools::ToWindowsPath($source_msi_path),
1177        "SymbolPaths", "",
1178        "Upgraded", $TargetImageName,
1179        "Order", 1,
1180        "ProductValidateFlags", "",
1181        "IgnoreMissingSrcFiles", 0);
1182}
1183
1184
1185
1186
1187sub SetAdditionalValues ($%)
1188{
1189    my ($pcp, %data) = @_;
1190
1191    while (my ($key,$value) = each(%data))
1192    {
1193        $key =~ /^([^\/]+)\/([^:]+):(.+)$/
1194            || die("invalid key format");
1195        my ($table_name, $key_column,$key_value) = ($1,$2,$3);
1196        $value =~ /^([^:]+):(.*)$/
1197            || die("invalid value format");
1198        my ($value_column,$value_value) = ($1,$2);
1199
1200        my $table = $pcp->GetTable($table_name);
1201        $table->SetRow(
1202                "*".$key_column, $key_value,
1203                $value_column, $value_value);
1204    }
1205}
1206
1207
1208
1209
1210sub CreatePcp ($$$$$$%)
1211{
1212    my ($source_msi,
1213        $target_msi,
1214        $language,
1215        $context,
1216        $msp_path,
1217        $pcp_schema_filename,
1218        %additional_values) = @_;
1219
1220    # Create filenames.
1221    my $pcp_filename = File::Spec->catfile($msp_path, "openoffice.pcp");
1222    my $msp_filename = File::Spec->catfile($msp_path, "openoffice.msp");
1223
1224    # Setup msp path and filename.
1225    unlink($pcp_filename) if -f $pcp_filename;
1226    if ( ! File::Copy::copy($pcp_schema_filename, $pcp_filename))
1227    {
1228        $installer::logger::Info->printf("Error: could not create openoffice.pcp as copy of pcp schema\n");
1229        $installer::logger::Info->printf("       %s\n", $pcp_schema_filename);
1230        $installer::logger::Info->printf("       %s\n", $pcp_filename);
1231        return undef;
1232    }
1233    my $pcp = installer::patch::Msi->new(
1234        $pcp_filename,
1235        undef,
1236        undef,
1237        $language,
1238        $context->{'product-name'});
1239
1240    # Store some values in the pcp for easy reference in the msp creation.
1241    $pcp->{'msp_filename'} = $msp_filename;
1242
1243    SetupPcpPatchMetadataTable($pcp, $source_msi, $target_msi);
1244    SetupPropertiesTable($pcp, $msp_filename);
1245    SetupImageFamiliesTable($pcp);
1246    SetupUpgradedImagesTable($pcp, $target_msi->{'filename'});
1247    SetupTargetImagesTable($pcp, $source_msi->{'filename'});
1248
1249    SetAdditionalValues(%additional_values);
1250
1251    $pcp->Commit();
1252
1253    # Remove the PatchSequence table to avoid MsiMsp error message:
1254    # "Since MSI 3.0 will block installation of major upgrade patches with
1255    #  sequencing information, creation of such patches is blocked."
1256    #$pcp->RemoveTable("PatchSequence");
1257    # TODO: alternatively add property SEQUENCE_DATA_GENERATION_DISABLED to pcp Properties table.
1258
1259
1260    $installer::logger::Info->printf("created pcp file at\n");
1261    $installer::logger::Info->printf("    %s\n", $pcp->{'filename'});
1262
1263    return $pcp;
1264}
1265
1266
1267
1268
1269sub ShowLog ($$$$)
1270{
1271    my ($log_path, $log_filename, $log_basename, $new_title) = @_;
1272
1273    if ( -f $log_filename)
1274    {
1275        my $destination_path = File::Spec->catfile($log_path, $log_basename);
1276        File::Path::make_path($destination_path) if ! -d $destination_path;
1277        my $command = join(" ",
1278            "wilogutl.exe",
1279            "/q",
1280            "/l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1281            "/o", "'".installer::patch::Tools::ToWindowsPath($destination_path)."'");
1282        printf("running command $command\n");
1283        my $response = qx($command);
1284        printf("response is '%s'\n", $response);
1285        my @candidates = glob($destination_path . "/Details*");
1286        foreach my $candidate (@candidates)
1287        {
1288            next unless -f $candidate;
1289            my $new_name = $candidate;
1290            $new_name =~ s/Details.*$/$log_basename.html/;
1291
1292            # Rename the top-level html file and replace the title.
1293            open my $in, "<", $candidate;
1294            open my $out, ">", $new_name;
1295            while (<$in>)
1296            {
1297                if (/^(.*\<title\>)([^<]+)(.*)$/)
1298                {
1299                    print $out $1.$new_title.$3;
1300                }
1301                else
1302                {
1303                    print $out $_;
1304                }
1305            }
1306            close $in;
1307            close $out;
1308
1309            my $URL = $new_name;
1310            $URL =~ s/\/c\//c|\//;
1311            $URL =~ s/^(.):/$1|/;
1312            $URL = "file:///". $URL;
1313            $installer::logger::Info->printf("open %s in your browser to see the log messages\n", $URL);
1314        }
1315    }
1316    else
1317    {
1318        $installer::logger::Info->printf("Error: log file not found at %s\n", $log_filename);
1319    }
1320}
1321
1322
1323
1324
1325sub CreateMsp ($)
1326{
1327    my ($pcp) = @_;
1328
1329    # Prepare log files.
1330    my $log_path = File::Spec->catfile($pcp->{'path'}, "log");
1331    my $log_basename = "msp";
1332    my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
1333    my $performance_log_basename = "performance";
1334    my $performance_log_filename = File::Spec->catfile($log_path, $performance_log_basename.".log");
1335    File::Path::make_path($log_path) if ! -d $log_path;
1336    unlink($log_filename) if -f $log_filename;
1337    unlink($performance_log_filename) if -f $performance_log_filename;
1338
1339    # Create the .msp patch file.
1340    my $temporary_msimsp_path = File::Spec->catfile($pcp->{'path'}, "tmp");
1341    if ( ! -d $temporary_msimsp_path)
1342    {
1343        File::Path::make_path($temporary_msimsp_path)
1344            || die ("can not create temporary path ".$temporary_msimsp_path);
1345    }
1346    $installer::logger::Info->printf("running msimsp.exe, that will take a while\n");
1347    my $command = join(" ",
1348        "msimsp.exe",
1349        "-s", "'".installer::patch::Tools::ToWindowsPath($pcp->{'filename'})."'",
1350        "-p", "'".installer::patch::Tools::ToWindowsPath($pcp->{'msp_filename'})."'",
1351        "-l", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1352        "-f", "'".installer::patch::Tools::ToWindowsPath($temporary_msimsp_path)."'");
1353#	    "-lp", MsiTools::ToEscapedWindowsPath($performance_log_filename),
1354    $installer::logger::Info->printf("running command %s\n", $command);
1355    my $response = qx($command);
1356    $installer::logger::Info->printf("response of msimsp is %s\n", $response);
1357    if ( ! -d $temporary_msimsp_path)
1358    {
1359        die("msimsp failed and deleted temporary path ".$temporary_msimsp_path);
1360    }
1361
1362    # Show the log file that was created by the msimsp.exe command.
1363    ShowLog($log_path, $log_filename, $log_basename, "msp creation");
1364    ShowLog($log_path, $performance_log_filename, $performance_log_basename, "msp creation perf");
1365}
1366
1367
1368
1369
1370=head CreatePatch($context, $variables)
1371
1372    Create MSP patch files for all relevant languages.
1373    The different steps are:
1374    1. Determine the set of languages for which both the source and target installation sets are present.
1375    Per language:
1376        2. Unpack CAB files (for source and target).
1377        3. Check if source and target releases are compatible.
1378        4. Create the PCP driver file.
1379        5. Create the MSP patch file.
1380
1381=cut
1382sub CreatePatch ($$)
1383{
1384    my ($context, $variables) = @_;
1385
1386    $installer::logger::Info->printf("patch will update product %s from %s to %s\n",
1387        $context->{'product-name'},
1388        $context->{'source-version'},
1389        $context->{'target-version'});
1390
1391    # Locate the Pcp schema file early on to report any errors before the lengthy operations that follow.
1392    my $pcp_schema_filename = FindPcpTemplate();
1393    if ( ! defined $pcp_schema_filename)
1394    {
1395        exit(1);
1396    }
1397
1398    my $release_data = installer::patch::ReleasesList::Instance()
1399        ->{$context->{'source-version'}}
1400        ->{$context->{'package-format'}};
1401
1402    # 1. Determine the set of languages for which we can create patches.
1403    my @requested_languages = GetLanguages();
1404    my @valid_languages = FindValidLanguages($context, $release_data, \@requested_languages);
1405    $installer::logger::Info->printf("of the requested languages '%s' are valid: '%s'\n",
1406        join("', '", @requested_languages),
1407        join("', '", @valid_languages));
1408    foreach my $language (@valid_languages)
1409    {
1410        $installer::logger::Info->printf("processing language '%s'\n", $language);
1411        $installer::logger::Info->increase_indentation();
1412
1413        # 2a. Provide .msi and .cab files and unpacke .cab for the source release.
1414        $installer::logger::Info->printf("locating source package (%s)\n", $context->{'source-version'});
1415        $installer::logger::Info->increase_indentation();
1416        if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
1417            $context->{'source-version'},
1418            0,
1419            $language,
1420            "msi",
1421            $context->{'product-name'}))
1422        {
1423            die "could not provide unpacked .cab file";
1424        }
1425        my $source_msi = installer::patch::Msi->FindAndCreate(
1426            $context->{'source-version'},
1427            0,
1428            $language,
1429            $context->{'product-name'});
1430        die unless $source_msi->IsValid();
1431
1432        $installer::logger::Info->decrease_indentation();
1433
1434        # 2b. Provide .msi and .cab files and unpacke .cab for the target release.
1435        $installer::logger::Info->printf("locating target package (%s)\n", $context->{'target-version'});
1436        $installer::logger::Info->increase_indentation();
1437        if ( ! installer::patch::InstallationSet::ProvideUnpackedCab(
1438            $context->{'target-version'},
1439            1,
1440            $language,
1441            "msi",
1442            $context->{'product-name'}))
1443        {
1444            die;
1445        }
1446        my $target_msi = installer::patch::Msi->FindAndCreate(
1447            $context->{'target-version'},
1448            0,
1449            $language,
1450            $context->{'product-name'});
1451        die unless defined $target_msi;
1452        die unless $target_msi->IsValid();
1453        $installer::logger::Info->decrease_indentation();
1454
1455        # Trigger reading of tables.
1456        foreach my $table_name (("File", "Component", "Registry"))
1457        {
1458            $source_msi->GetTable($table_name);
1459            $target_msi->GetTable($table_name);
1460            $installer::logger::Info->printf("read %s table (source and target\n", $table_name);
1461        }
1462
1463        # 3. Check if the source and target msis fullfil all necessary requirements.
1464        if ( ! Check($source_msi, $target_msi, $variables, $context->{'product-name'}))
1465        {
1466            $installer::logger::Info->printf("Error: Source and target releases are not compatible.\n");
1467            $installer::logger::Info->printf("       => Can not create patch.\n");
1468            $installer::logger::Info->printf("       Did you create the target installation set with 'release=t' ?\n");
1469            exit(1);
1470        }
1471        else
1472        {
1473            $installer::logger::Info->printf("OK: Source and target releases are compatible.\n");
1474        }
1475
1476        # Provide the base path for creating .pcp and .mcp file.
1477        my $msp_path = File::Spec->catfile(
1478            $context->{'output-path'},
1479            $context->{'product-name'},
1480            "msp",
1481            sprintf("%s_%s",
1482              installer::patch::Version::ArrayToDirectoryName(
1483                installer::patch::Version::StringToNumberArray(
1484                    $source_msi->{'version'})),
1485              installer::patch::Version::ArrayToDirectoryName(
1486                installer::patch::Version::StringToNumberArray(
1487                    $target_msi->{'version'}))),
1488            $language
1489            );
1490        File::Path::make_path($msp_path) unless -d $msp_path;
1491
1492        # 4. Create the .pcp file that drives the msimsp.exe command.
1493        my $pcp = CreatePcp(
1494            $source_msi,
1495            $target_msi,
1496            $language,
1497            $context,
1498            $msp_path,
1499            $pcp_schema_filename,
1500            "Properties/Name:DontRemoveTempFolderWhenFinished" => "Value:1");
1501
1502        # 5. Finally create the msp.
1503        CreateMsp($pcp);
1504
1505        $installer::logger::Info->decrease_indentation();
1506    }
1507}
1508
1509
1510
1511=cut ApplyPatch ($context, $variables)
1512
1513    This is for testing only.
1514    The patch is applied and (extensive) log information is created and transformed into HTML format.
1515
1516=cut
1517sub ApplyPatch ($$)
1518{
1519    my ($context, $variables) = @_;
1520
1521    $installer::logger::Info->printf("will apply patches that update product %s from %s to %s\n",
1522        $context->{'product-name'},
1523        $context->{'source-version'},
1524        $context->{'target-version'});
1525    my @languages = GetLanguages();
1526
1527    my $source_version_dirname = installer::patch::Version::ArrayToDirectoryName(
1528      installer::patch::Version::StringToNumberArray(
1529          $context->{'source-version'}));
1530    my $target_version_dirname = installer::patch::Version::ArrayToDirectoryName(
1531      installer::patch::Version::StringToNumberArray(
1532          $context->{'target-version'}));
1533
1534    foreach my $language (@languages)
1535    {
1536        my $msp_filename = File::Spec->catfile(
1537            $context->{'output-path'},
1538            $context->{'product-name'},
1539            "msp",
1540            $source_version_dirname . "_" . $target_version_dirname,
1541            $language,
1542            "openoffice.msp");
1543        if ( ! -f $msp_filename)
1544        {
1545            $installer::logger::Info->printf("%s does not point to a valid file\n", $msp_filename);
1546            next;
1547        }
1548
1549        my $log_path = File::Spec->catfile(dirname($msp_filename), "log");
1550        my $log_basename = "apply-msp";
1551        my $log_filename = File::Spec->catfile($log_path, $log_basename.".log");
1552
1553        my $command = join(" ",
1554            "msiexec.exe",
1555            "/update", "'".installer::patch::Tools::ToWindowsPath($msp_filename)."'",
1556            "/L*xv!", "'".installer::patch::Tools::ToWindowsPath($log_filename)."'",
1557            "REINSTALL=ALL",
1558#            "REINSTALLMODE=vomus",
1559            "REINSTALLMODE=omus",
1560            "MSIENFORCEUPGRADECOMPONENTRULES=1");
1561
1562        printf("executing command %s\n", $command);
1563        my $response = qx($command);
1564        Encode::from_to($response, "UTF16LE", "UTF8");
1565        printf("response was '%s'\n", $response);
1566
1567        ShowLog($log_path, $log_filename, $log_basename, "msp application");
1568    }
1569}
1570
1571
1572
1573
1574=head2 DownloadFile ($url)
1575
1576    A simpler version of InstallationSet::Download().  It is simple because it is used to
1577    setup the $release_data structure that is used by InstallationSet::Download().
1578
1579=cut
1580sub DownloadFile ($)
1581{
1582    my ($url) = shift;
1583
1584    my $agent = LWP::UserAgent->new();
1585    $agent->timeout(120);
1586    $agent->show_progress(0);
1587
1588    my $file_content = "";
1589    my $last_was_redirect = 0;
1590    my $bytes_read = 0;
1591    $agent->add_handler('response_redirect'
1592        => sub{
1593            $last_was_redirect = 1;
1594            return;
1595        });
1596    $agent->add_handler('response_data'
1597        => sub{
1598            if ($last_was_redirect)
1599            {
1600                $last_was_redirect = 0;
1601                # Throw away the data we got so far.
1602		$file_content = "";
1603            }
1604            my($response,$agent,$h,$data)=@_;
1605	    $file_content .= $data;
1606        });
1607    $agent->get($url);
1608
1609    return $file_content;
1610}
1611
1612
1613
1614
1615sub CreateReleaseItem ($$$)
1616{
1617    my ($language, $exe_filename, $msi) = @_;
1618
1619    die "can not open installation set at ".$exe_filename unless -f $exe_filename;
1620
1621    open my $in, "<", $exe_filename;
1622    my $sha256_checksum = new Digest("SHA-256")->addfile($in)->hexdigest();
1623    close $in;
1624
1625    my $filesize = -s $exe_filename;
1626
1627    # Get the product code property from the msi and strip the enclosing braces.
1628    my $product_code = $msi->GetTable("Property")->GetValue("Property", "ProductCode", "Value");
1629    $product_code =~ s/(^{|}$)//g;
1630    my $upgrade_code = $msi->GetTable("Property")->GetValue("Property", "UpgradeCode", "Value");
1631    $upgrade_code =~ s/(^{|}$)//g;
1632    my $build_id = $msi->GetTable("Property")->GetValue("Property", "PRODUCTBUILDID", "Value");
1633
1634    return {
1635        'language' => $language,
1636        'checksum-type' => "sha256",
1637        'checksum-value' => $sha256_checksum,
1638        'file-size' => $filesize,
1639        'product-code' => $product_code,
1640        'upgrade-code' => $upgrade_code,
1641        'build-id' => $build_id
1642    };
1643}
1644
1645
1646
1647
1648sub GetReleaseItemForCurrentBuild ($$$)
1649{
1650    my ($context, $language, $exe_basename) = @_;
1651
1652    # Target version is the current version.
1653    # Search instsetoo_native for the installation set.
1654    my $filename = File::Spec->catfile(
1655        $context->{'output-path'},
1656        $context->{'product-name'},
1657        $context->{'package-format'},
1658        "install",
1659        $language."_download",
1660        $exe_basename);
1661
1662    printf("        current : %s\n", $filename);
1663    if ( ! -f $filename)
1664    {
1665        printf("ERROR: can not find %s\n", $filename);
1666        return undef;
1667    }
1668    else
1669    {
1670        my $msi = installer::patch::Msi->FindAndCreate(
1671            $context->{'target-version'},
1672            1,
1673            $language,
1674            $context->{'product-name'});
1675        return CreateReleaseItem($language, $filename, $msi);
1676    }
1677}
1678
1679
1680
1681sub GetReleaseItemForOldBuild ($$$$)
1682{
1683    my ($context, $language, $exe_basename, $url_template) = @_;
1684
1685    # Use ext_sources/ as local cache for archive.apache.org
1686    # and search these for the installation set.
1687
1688    my $version = $context->{'target-version'};
1689    my $package_format =  $context->{'package-format'};
1690    my $releases_list = installer::patch::ReleasesList::Instance();
1691
1692    my $url = $url_template;
1693    $url =~ s/%L/$language/g;
1694    $releases_list->{$version}->{$package_format}->{$language}->{'URL'} = $url;
1695
1696    if ( ! installer::patch::InstallationSet::ProvideUnpackedExe(
1697               $version,
1698               0,
1699               $language,
1700               $package_format,
1701               $context->{'product-name'}))
1702    {
1703        # Can not provide unpacked EXE.
1704        return undef;
1705    }
1706    else
1707    {
1708        my $exe_filename = File::Spec->catfile(
1709            $ENV{'TARFILE_LOCATION'},
1710            $exe_basename);
1711        my $msi = installer::patch::Msi->FindAndCreate(
1712            $version,
1713            0,
1714            $language,
1715            $context->{'product-name'});
1716        return CreateReleaseItem($language, $exe_filename, $msi);
1717    }
1718}
1719
1720
1721
1722
1723sub UpdateReleasesXML($$)
1724{
1725    my ($context, $variables) = @_;
1726
1727    my $releases_list = installer::patch::ReleasesList::Instance();
1728    my $output_filename = File::Spec->catfile(
1729        $context->{'output-path'},
1730        "misc",
1731        "releases.xml");
1732
1733    my $target_version = $context->{'target-version'};
1734    my %version_hash = map {$_=>1} @{$releases_list->{'releases'}};
1735    my $item_hash = undef;
1736    if ( ! defined $version_hash{$context->{'target-version'}})
1737    {
1738        # Target version is not yet present.  Add it and print message that asks caller to check order.
1739        push @{$releases_list->{'releases'}}, $target_version;
1740        printf("adding data for new version %s to list of released versions.\n", $target_version);
1741        printf("please check order of releases in $output_filename\n");
1742        $item_hash = {};
1743    }
1744    else
1745    {
1746        printf("adding data for existing version %s to releases.xml\n", $target_version);
1747        $item_hash = $releases_list->{$target_version}->{$context->{'package-format'}};
1748    }
1749    $releases_list->{$target_version} = {$context->{'package-format'} => $item_hash};
1750
1751    my @languages = GetLanguages();
1752    my %language_items = ();
1753    foreach my $language (@languages)
1754    {
1755        # There are three different sources where to find the downloadable installation sets.
1756        # 1. archive.apache.org for previously released versions.
1757        # 2. A local cache or repository directory that conceptually is a local copy of archive.apache.org
1758        # 3. The downloadable installation sets built in instsetoo_native/.
1759
1760        my $exe_basename = sprintf(
1761            "%s_%s_Win_x86_install_%s.exe",
1762            $context->{'product-name'},
1763            $target_version,
1764            $language);
1765        my $url_template = sprintf(
1766            "http://archive.apache.org/dist/openoffice/%s/binaries/%%L/%s_%s_Win_x86_install_%%L.exe",
1767            $target_version,
1768            $context->{'product-name'},
1769            $target_version);
1770
1771        my $item = undef;
1772        if ($target_version eq $variables->{PRODUCTVERSION})
1773        {
1774            $item = GetReleaseItemForCurrentBuild($context, $language, $exe_basename);
1775        }
1776        else
1777        {
1778            $item = GetReleaseItemForOldBuild($context, $language, $exe_basename, $url_template);
1779        }
1780
1781        next unless defined $item;
1782
1783        $language_items{$language} = $item;
1784        $item_hash->{$language} = $item;
1785        $item_hash->{'upgrade-code'} = $item->{'upgrade-code'};
1786        $item_hash->{'build-id'} = $item->{'build-id'};
1787        $item_hash->{'url-template'} = $url_template;
1788    }
1789
1790    my @valid_languages = sort keys %language_items;
1791    $item_hash->{'languages'} = \@valid_languages;
1792
1793    $releases_list->Write($output_filename);
1794
1795    printf("\n\n");
1796    printf("please copy '%s' to main/instsetoo_native/data\n", $output_filename);
1797    printf("and check in the modified file to the version control system\n");
1798}
1799
1800
1801
1802
1803sub main ()
1804{
1805    installer::logger::SetupSimpleLogging(undef);
1806    my $context = ProcessCommandline();
1807    die "ERROR: list file is not defined, please use --lst-file option"
1808        unless defined $context->{'lst-file'};
1809    die "ERROR: product name is not defined, please use --product-name option"
1810        unless defined $context->{'product-name'};
1811
1812    my ($variables, undef, undef) = installer::ziplist::read_openoffice_lst_file(
1813        $context->{'lst-file'},
1814        $context->{'product-name'},
1815        undef);
1816    DetermineVersions($context, $variables);
1817
1818    if ($context->{'command'} eq "create")
1819    {
1820        CreatePatch($context, $variables);
1821    }
1822    elsif ($context->{'command'} eq "apply")
1823    {
1824        ApplyPatch($context, $variables);
1825    }
1826    elsif ($context->{'command'} eq "update-releases-xml")
1827    {
1828        UpdateReleasesXML($context, $variables);
1829    }
1830}
1831
1832
1833main();
1834