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