blob: f216d2b18a3f5f1b3666a0710cb6ebfe097dcd65 [file] [log] [blame]
#**************************************************************
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
#**************************************************************
package installer::patch::InstallationSet;
use installer::patch::Tools;
use installer::patch::Version;
use installer::logger;
use strict;
# Call Get7Zip() to get access to the filename of the 7z executable.
my $SevenZip = undef;
=head1 NAME
package installer::patch::InstallationSet - Functions for handling installation sets
=head1 DESCRIPTION
This package contains functions for unpacking the .exe files that
are created by the NSIS installer creator and the .cab files in
the installation sets.
=cut
=head2 Detect7ZipOnWindows ()
7Zip seems to be the only program able to unpack an NSIS installer.
Search for it.
=cut
sub Detect7ZipOnWindows ()
{
# Use 'reg query' to read registry entry from Windows registry.
my $registry_key = "HKEY_CURRENT_USER\\\\Software\\\\7-Zip";
my $registry_value_name = "Path";
my $command = sprintf("reg query %s /v %s", $registry_key, $registry_value_name);
my $response = qx($command);
# Process the response.
my $path_to_7zip = undef;
if ($response =~ /\s+REG_SZ\s+([^\r\n]*)/m)
{
$path_to_7zip = $1;
}
# If that failed, then make an educated guess.
if ( ! defined $path_to_7zip)
{
$path_to_7zip = "c:\\Program Files\\7-Zip\\";
}
# Check if the executable exists and is, well, executable.
return undef unless -d $path_to_7zip;
my $fullname = File::Spec->catfile($path_to_7zip, "7z.exe");
return undef unless -f $fullname;
return undef unless -x $fullname;
return $fullname;
}
sub Get7Zip ()
{
if ( ! defined $SevenZip)
{
if ($ENV{'OS'} eq "WNT")
{
$SevenZip = Detect7ZipOnWindows();
}
if ( ! defined $SevenZip)
{
# Use an empty string to avoid repeated (and failing) detections of a missing 7z.
$SevenZip = "";
}
}
return $SevenZip eq "" ? undef : $SevenZip;
}
sub UnpackExe ($$)
{
my ($filename, $destination_path) = @_;
$installer::logger::Info->printf("unpacking installation set to '%s'\n", $destination_path);
# Unpack to a temporary path and change its name to the destination path
# only when the unpacking has completed successfully.
File::Path::make_path($destination_path);
my $windows_filename = installer::patch::Tools::ToEscapedWindowsPath($filename);
my $windows_destination_path = installer::patch::Tools::ToEscapedWindowsPath($destination_path);
my $command = join(" ",
"\"".Get7Zip()."\"",
"x",
"-y",
"-o".$windows_destination_path,
$windows_filename);
my $result = qx($command);
if ( ! $result)
{
installer::exiter::exit_program(
"ERROR: can not unpack downloadable installation set: ".$!,
"installer::patch::InstallationSet::UnpackExe");
}
# Check the existence of the .cab files.
my $cab_filename = File::Spec->catfile($destination_path, "openoffice1.cab");
if ( ! -f $cab_filename)
{
installer::logger::PrintError("cab file '%s' was not extracted from installation set\n", $cab_filename);
return 0;
}
return 1;
}
=head2 UnpackCab($cab_filename, $destination_path)
Unpacking the cabinet file inside an .exe installation set is a
three step process because there is no directory information stored
inside the cab file. This has to be taken from the 'File' and
'Directory' tables in the .msi file.
1. Setup the directory structure of all files in the cab from the 'File' and 'Directory' tables in the msi.
2. Unpack the cab file.
3. Move the files to their destination directories.
=cut
sub UnpackCab ($$$)
{
my ($cab_filename, $msi, $destination_path) = @_;
# Step 1
# Extract the directory structure from the 'File' and 'Directory' tables in the given msi.
$installer::logger::Info->printf("setting up directory tree\n");
my $file_table = $msi->GetTable("File");
my $file_map = $msi->GetFileMap();
# Step 2
# Unpack the .cab file to a temporary path.
my $temporary_destination_path = $destination_path . ".tmp";
if ( -d $temporary_destination_path)
{
# Temporary directory already exists => cab file has already been unpacked (flat), nothing to do.
printf("%s exists\n", $temporary_destination_path);
$installer::logger::Info->printf("cab file has already been unpacked to flat structure\n");
}
else
{
UnpackCabFlat($cab_filename, $temporary_destination_path, $file_table);
}
# Step 3
# Move the files to their destinations.
File::Path::make_path($destination_path);
$installer::logger::Info->printf("moving files to their directories\n");
my $directory_map = $msi->GetDirectoryMap();
my $office_menu_folder_name = $directory_map->{'INSTALLLOCATION'}->{'target_long_name'};
my $count = 0;
foreach my $file_row (@{$file_table->GetAllRows()})
{
my $unique_name = $file_row->GetValue('File');
my $file_item = $file_map->{$unique_name};
my $directory_item = $file_item->{'directory'};
my $long_file_name = $file_item->{'long_name'};
my $full_name = $directory_item->{'full_source_long_name'};
# Strip away the leading OfficeMenuFolder part.
$full_name =~ s/^$office_menu_folder_name\///;
my $flat_filename = File::Spec->catfile($temporary_destination_path, $unique_name);
my $dir_path = File::Spec->catfile($destination_path, $full_name);
my $dir_filename = File::Spec->catfile($dir_path, $long_file_name);
if ( ! -d $dir_path)
{
File::Path::make_path($dir_path);
}
$installer::logger::Lang->printf("moving %s to %s\n", $flat_filename, $dir_filename);
File::Copy::move($flat_filename, $dir_filename)
|| die("can not move file ".$flat_filename.":".$!);
++$count;
}
# Cleanup. Remove the temporary directory. It should be empty by now.
rmdir($temporary_destination_path);
}
=head2 UnpackCabFlat ($cab_filename, $destination_path, $file_table)
Unpack the flat file structure of the $cab_filename to $destination_path.
In order to detect and handle an incomplete (arborted) previous
extraction, the cab file is unpacked to a temprorary directory
that after successful extraction is renamed to $destination_path.
=cut
sub UnpackCabFlat ($$$)
{
my ($cab_filename, $destination_path, $file_table) = @_;
# Unpack the .cab file to a temporary path (note that
# $destination_path may alreay bee a temporary path). Using a
# second one prevents the lengthy flat unpacking to be repeated
# when another step fails.
$installer::logger::Info->printf("unpacking cab file\n");
File::Path::make_path($destination_path);
my $windows_cab_filename = installer::patch::Tools::ToEscapedWindowsPath($cab_filename);
my $windows_destination_path = installer::patch::Tools::ToEscapedWindowsPath($destination_path);
my $command = join(" ",
"\"".Get7Zip()."\"",
"x", "-o".$windows_destination_path,
$windows_cab_filename,
"-y");
open my $cmd, $command."|";
my $extraction_count = 0;
my $file_count = $file_table->GetRowCount();
while (<$cmd>)
{
my $message = $_;
chomp($message);
++$extraction_count;
printf("%4d/%4d %3.2f%% \r",
$extraction_count,
$file_count,
$extraction_count*100/$file_count);
}
close $cmd;
}
=head GetUnpackedExePath ($version, $is_current_version, $language, $package_format, $product)
Convenience function that returns where a downloadable installation set is extracted to.
=cut
sub GetUnpackedExePath ($$$$$)
{
my ($version, $is_current_version, $language, $package_format, $product) = @_;
my $path = GetUnpackedPath($version, $is_current_version, $language, $package_format, $product);
return File::Spec->catfile($path, "unpacked");
}
=head GetUnpackedCabPath ($version, $is_current_version, $language, $package_format, $product)
Convenience function that returns where a cab file is extracted
(with injected directory structure from the msi file) to.
=cut
sub GetUnpackedCabPath ($$$$$)
{
my ($version, $is_current_version, $language, $package_format, $product) = @_;
my $path = GetUnpackedPath($version, $is_current_version, $language, $package_format, $product);
return File::Spec->catfile($path, "unpacked");
}
=head2 GetUnpackedPath($version, $is_current_version, $language, $package_format, $product)
Internal function for creating paths to where archives are unpacked.
=cut
sub GetUnpackedPath ($$$$$)
{
my ($version, $is_current_version, $language, $package_format, $product) = @_;
return File::Spec->catfile(
$ENV{'SRC_ROOT'},
"instsetoo_native",
$ENV{'INPATH'},
$product,
$package_format,
installer::patch::Version::ArrayToDirectoryName(
installer::patch::Version::StringToNumberArray($version)),
installer::languages::get_normalized_language($language));
}
sub GetMsiFilename ($$)
{
my ($path, $version) = @_;
my $no_dot_version = installer::patch::Version::ArrayToNoDotName(
installer::patch::Version::StringToNumberArray(
$version));
return File::Spec->catfile(
$path,
"openoffice" . $no_dot_version . ".msi");
}
sub GetCabFilename ($$)
{
my ($path, $version) = @_;
return File::Spec->catfile(
$path,
"openoffice1.cab");
}
=head2 Download($language, $release_data, $filename)
Download an installation set to $filename. The URL for the
download is taken from $release_data, a snippet from the
instsetoo_native/data/releases.xml file.
=cut
sub Download ($$$)
{
my ($language, $release_data, $filename) = @_;
my $url = $release_data->{'URL'};
$release_data->{'URL'} =~ /^(.*)\/([^\/]+)$/;
my ($location, $basename) = ($1,$2);
$installer::logger::Info->printf("downloading %s\n", $basename);
$installer::logger::Info->printf(" from '%s'\n", $location);
my $filesize = $release_data->{'file-size'};
if (defined $filesize)
{
$installer::logger::Info->printf(" expected size is %d\n", $filesize);
}
else
{
$installer::logger::Info->printf(" file size is not yet known\n");
}
my $temporary_filename = $filename . ".part";
my $resume_size = 0;
# Prepare checksum.
my $checksum = undef;
my $checksum_type = $release_data->{'checksum-type'};
my $checksum_value = $release_data->{'checksum-value'};
my $digest = undef;
if ( ! defined $checksum_value)
{
# No checksum available. Skip test.
}
elsif ($checksum_type eq "sha256")
{
$digest = Digest->new("SHA-256");
}
elsif ($checksum_type eq "md5")
{
$digest = Digest->new("md5");
}
else
{
installer::logger::PrintError(
"checksum type %s is not supported. Supported checksum types are: sha256,md5\n",
$checksum_type);
return 0;
}
# Download the extension.
open my $out, ">$temporary_filename";
binmode($out);
my $mode = $|;
my $handle = select STDOUT;
$| = 1;
select $handle;
my $agent = LWP::UserAgent->new();
$agent->timeout(120);
$agent->show_progress(0);
my $last_was_redirect = 0;
my $bytes_read = 0;
$agent->add_handler('response_redirect'
=> sub{
$last_was_redirect = 1;
return;
});
$agent->add_handler('response_data'
=> sub{
if ($last_was_redirect)
{
$last_was_redirect = 0;
# Throw away the data we got so far.
$digest->reset() if defined $digest;
close $out;
open $out, ">$temporary_filename";
binmode($out);
}
my($response,$agent,$h,$data)=@_;
print $out $data;
$digest->add($data) if defined $digest;
$bytes_read += length($data);
if (defined $filesize)
{
printf("read %*d / %d %d%% \r",
length($filesize),
$bytes_read,
$filesize,
$bytes_read*100/$filesize);
}
else
{
printf("read %6.2f MB\r", $bytes_read/(1024.0*1024.0));
}
});
my $response;
if ($resume_size > 0)
{
$response = $agent->get($url, 'Range' => "bytes=$resume_size-");
}
else
{
$response = $agent->get($url);
}
close $out;
$handle = select STDOUT;
$| = $mode;
select $handle;
$installer::logger::Info->print(" \r");
if ($response->is_success())
{
if ( ! defined $digest
|| $digest->hexdigest() eq $checksum_value)
{
$installer::logger::Info->print("download was successfull\n");
if ( ! rename($temporary_filename, $filename))
{
installer::logger::PrintError("can not rename '%s' to '%s'\n", $temporary_filename, $filename);
return 0;
}
else
{
return 1;
}
}
else
{
installer::logger::PrintError("%s checksum is wrong\n", $checksum_type);
return 0;
}
}
else
{
installer::logger::PrintError("there was a download error\n");
return 0;
}
}
=head2 ProvideDownloadSet ($version, $language, $package_format)
Download an installation set when it is not yet present to
$ENV{'TARFILE_LOCATION'}. Verify the downloaded file with the
checksum that is extracted from the
instsetoo_native/data/releases.xml file.
=cut
sub ProvideDownloadSet ($$$)
{
my ($version, $language, $package_format) = @_;
my $release_item = installer::patch::ReleasesList::Instance()->{$version}->{$package_format}->{$language};
return undef unless defined $release_item;
# Get basename of installation set from URL.
$release_item->{'URL'} =~ /^(.*)\/([^\/]+)$/;
my ($location, $basename) = ($1,$2);
# Is the installation set already present in ext_sources/ ?
my $need_download = 0;
my $ext_sources_filename = File::Spec->catfile(
$ENV{'TARFILE_LOCATION'},
$basename);
if ( ! -f $ext_sources_filename)
{
$installer::logger::Info->printf("download set is not in ext_sources/ (%s)\n", $ext_sources_filename);
$need_download = 1;
}
else
{
$installer::logger::Info->printf("download set exists at '%s'\n", $ext_sources_filename);
if (defined $release_item->{'checksum-value'}
&& $release_item->{'checksum-type'} eq 'sha256')
{
$installer::logger::Info->printf("checking SHA256 checksum\n");
my $digest = Digest->new("SHA-256");
open my $in, "<", $ext_sources_filename;
$digest->addfile($in);
close $in;
if ($digest->hexdigest() ne $release_item->{'checksum-value'})
{
$installer::logger::Info->printf(" mismatch\n", $ext_sources_filename);
$need_download = 1;
}
else
{
$installer::logger::Info->printf(" match\n");
}
}
}
if ($need_download)
{
if ( ! installer::patch::InstallationSet::Download(
$language,
$release_item,
$ext_sources_filename))
{
return 0;
}
if ( ! -f $ext_sources_filename)
{
$installer::logger::Info->printf("download set could not be downloaded\n");
return 0;
}
}
return $ext_sources_filename;
}
sub ProvideUnpackedExe ($$$$$)
{
my ($version, $is_current_version, $language, $package_format, $product_name) = @_;
# Check if the exe has already been unpacked.
my $unpacked_exe_path = installer::patch::InstallationSet::GetUnpackedExePath(
$version,
$is_current_version,
$language,
$package_format,
$product_name);
my $unpacked_exe_flag_filename = File::Spec->catfile($unpacked_exe_path, "__exe_is_unpacked");
my $exe_is_unpacked = -f $unpacked_exe_flag_filename;
if ($exe_is_unpacked)
{
# Yes, exe has already been unpacked. There is nothing more to do.
$installer::logger::Info->printf("downloadable installation set has already been unpacked to\n");
$installer::logger::Info->printf(" %s\n", $unpacked_exe_path);
return 1;
}
elsif ($is_current_version)
{
# For the current version the exe is created from the unpacked
# content and both are expected to be already present.
# In order to have the .cab and its unpacked content in one
# directory and don't interfere with the creation of regular
# installation sets, we copy the unpacked .exe into a separate
# directory.
my $original_path = File::Spec->catfile(
$ENV{'SRC_ROOT'},
"instsetoo_native",
$ENV{'INPATH'},
$product_name,
$package_format,
"install",
$language);
$installer::logger::Info->printf("creating a copy\n");
$installer::logger::Info->printf(" of %s\n", $original_path);
$installer::logger::Info->printf(" at %s\n", $unpacked_exe_path);
File::Path::make_path($unpacked_exe_path) unless -d $unpacked_exe_path;
my ($file_count,$directory_count) = CopyRecursive($original_path, $unpacked_exe_path);
return 0 if ( ! defined $file_count);
$installer::logger::Info->printf(" copied %d files in %d directories\n",
$file_count,
$directory_count);
installer::patch::Tools::touch($unpacked_exe_flag_filename);
return 1;
}
else
{
# No, we have to unpack the exe.
# Provide the exe.
my $filename = installer::patch::InstallationSet::ProvideDownloadSet(
$version,
$language,
$package_format);
# Unpack it.
if (defined $filename)
{
if (installer::patch::InstallationSet::UnpackExe($filename, $unpacked_exe_path))
{
$installer::logger::Info->printf("downloadable installation set has been unpacked to\n");
$installer::logger::Info->printf(" %s\n", $unpacked_exe_path);
installer::patch::Tools::touch($unpacked_exe_flag_filename);
return 1;
}
}
else
{
installer::logger::PrintError("could not provide .exe installation set at '%s'\n", $filename);
}
}
return 0;
}
sub CopyRecursive ($$)
{
my ($source_path, $destination_path) = @_;
return (undef,undef) unless -d $source_path;
my @todo = ([$source_path, $destination_path]);
my $file_count = 0;
my $directory_count = 0;
while (scalar @todo > 0)
{
my ($source,$destination) = @{shift @todo};
next if ! -d $source;
File::Path::make_path($destination);
++$directory_count;
# Read list of files in the current source directory.
opendir( my $dir, $source);
my @files = readdir $dir;
closedir $dir;
# Copy all files and push all directories to @todo.
foreach my $file (@files)
{
next if $file =~ /^\.+$/;
my $source_file = File::Spec->catfile($source, $file);
my $destination_file = File::Spec->catfile($destination, $file);
if ( -f $source_file)
{
File::Copy::copy($source_file, $destination_file);
++$file_count;
}
elsif ( -d $source_file)
{
push @todo, [$source_file, $destination_file];
}
}
}
return ($file_count, $directory_count);
}
sub CheckLocalCopy ($$$$)
{
my ($version, $language, $package_format, $product_name) = @_;
# Compare creation times of the original .msi and its copy.
my $original_path = File::Spec->catfile(
$ENV{'SRC_ROOT'},
"instsetoo_native",
$ENV{'INPATH'},
$product_name,
$package_format,
"install",
$language);
my $copy_path = installer::patch::InstallationSet::GetUnpackedExePath(
$version,
1,
$language,
$package_format,
$product_name);
my $msi_basename = "openoffice"
. installer::patch::Version::ArrayToNoDotName(
installer::patch::Version::StringToNumberArray($version))
. ".msi";
my $original_msi_filename = File::Spec->catfile($original_path, $msi_basename);
my $copied_msi_filename = File::Spec->catfile($copy_path, $msi_basename);
my @original_msi_stats = stat($original_msi_filename);
my @copied_msi_stats = stat($copied_msi_filename);
my $original_msi_mtime = $original_msi_stats[9];
my $copied_msi_mtime = $copied_msi_stats[9];
if (defined $original_msi_mtime
&& defined $copied_msi_mtime
&& $original_msi_mtime > $copied_msi_mtime)
{
# The installation set is newer than its copy.
# Remove the copy.
$installer::logger::Info->printf(
"removing copy of installation set (version %s) because it is out of date\n",
$version);
File::Path::remove_tree($copy_path);
}
}
=head2 ProvideUnpackedCab
1a. Make sure that a downloadable installation set is present.
1b. or that a freshly built installation set (packed and unpacked is present)
2. Unpack the downloadable installation set
3. Unpack the .cab file.
The 'Provide' in the function name means that any step that has
already been made is not made again.
=cut
sub ProvideUnpackedCab ($$$$$)
{
my ($version, $is_current_version, $language, $package_format, $product_name) = @_;
if ($is_current_version)
{
# For creating patches we maintain a copy of the unpacked .exe. Make sure that that is updated when
# a new installation set has been built.
CheckLocalCopy($version, $language, $package_format, $product_name);
}
# Check if the cab file has already been unpacked.
my $unpacked_cab_path = installer::patch::InstallationSet::GetUnpackedCabPath(
$version,
$is_current_version,
$language,
$package_format,
$product_name);
my $unpacked_cab_flag_filename = File::Spec->catfile($unpacked_cab_path, "__cab_is_unpacked");
my $cab_is_unpacked = -f $unpacked_cab_flag_filename;
if ($cab_is_unpacked)
{
# Yes. Cab was already unpacked. There is nothing more to do.
$installer::logger::Info->printf("cab has already been unpacked to\n");
$installer::logger::Info->printf(" %s\n", $unpacked_cab_path);
return 1;
}
else
{
# Make sure that the exe is unpacked and the cab file exists.
ProvideUnpackedExe($version, $is_current_version, $language, $package_format, $product_name);
# Unpack the cab file.
my $unpacked_exe_path = installer::patch::InstallationSet::GetUnpackedExePath(
$version,
$is_current_version,
$language,
$package_format,
$product_name);
my $msi = new installer::patch::Msi(
installer::patch::InstallationSet::GetMsiFilename($unpacked_exe_path, $version),
$version,
$is_current_version,
$language,
$product_name);
my $cab_filename = installer::patch::InstallationSet::GetCabFilename(
$unpacked_exe_path,
$version);
if ( ! -f $cab_filename)
{
# Cab file does not exist.
installer::logger::PrintError(
"could not find .cab file at '%s'. Extraction of .exe seems to have failed.\n",
$cab_filename);
return 0;
}
if (installer::patch::InstallationSet::UnpackCab(
$cab_filename,
$msi,
$unpacked_cab_path))
{
$installer::logger::Info->printf("unpacked cab file '%s'\n", $cab_filename);
$installer::logger::Info->printf(" to '%s'\n", $unpacked_cab_path);
installer::patch::Tools::touch($unpacked_cab_flag_filename);
return 1;
}
else
{
return 0;
}
}
}
1;