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