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