Msi.pm (c9b362f6) Msi.pm (9f91b7e3)
1#**************************************************************
2#
3# Licensed to the Apache Software Foundation (ASF) under one
4# or more contributor license agreements. See the NOTICE file
5# distributed with this work for additional information
6# regarding copyright ownership. The ASF licenses this file
7# to you under the Apache License, Version 2.0 (the
8# "License"); you may not use this file except in compliance

--- 9 unchanged lines hidden (view full) ---

18# under the License.
19#
20#**************************************************************
21
22package installer::patch::Msi;
23
24use installer::patch::MsiTable;
25use installer::patch::Tools;
1#**************************************************************
2#
3# Licensed to the Apache Software Foundation (ASF) under one
4# or more contributor license agreements. See the NOTICE file
5# distributed with this work for additional information
6# regarding copyright ownership. The ASF licenses this file
7# to you under the Apache License, Version 2.0 (the
8# "License"); you may not use this file except in compliance

--- 9 unchanged lines hidden (view full) ---

18# under the License.
19#
20#**************************************************************
21
22package installer::patch::Msi;
23
24use installer::patch::MsiTable;
25use installer::patch::Tools;
26use installer::patch::InstallationSet;
27
28use File::Basename;
29use File::Copy;
30
26use strict;
27
28
29=head1 NAME
30
31 package installer::patch::Msi - Class represents a single MSI file and gives access to its tables.
32
33=cut
34
31use strict;
32
33
34=head1 NAME
35
36 package installer::patch::Msi - Class represents a single MSI file and gives access to its tables.
37
38=cut
39
40sub FindAndCreate($$$$$)
41{
42 my ($class, $version, $is_current_version, $language, $product_name) = @_;
35
43
44 my $condensed_version = $version;
45 $condensed_version =~ s/\.//g;
36
46
37=head2 new($class, $version, $language, $product_name)
47 # When $version is the current version we have to search the msi at a different place.
48 my $path;
49 my $filename;
50 my $is_current = 0;
51 $path = installer::patch::InstallationSet::GetUnpackedExePath(
52 $version,
53 $is_current_version,
54 $language,
55 "msi",
56 $product_name);
38
57
58 # Find the msi in the path.ls .
59 $filename = File::Spec->catfile($path, "openoffice".$condensed_version.".msi");
60 $is_current = $is_current_version;
61
62 return $class->new($filename, $version, $is_current, $language, $product_name);
63}
64
65
66
67
68
69
70=head2 new($class, $filename, $version, $is_current_version, $language, $product_name)
71
39 Create a new object of the Msi class. The values of $version, $language, and $product_name define
40 where to look for the msi file.
41
42 If construction fails then IsValid() will return false.
43
44=cut
72 Create a new object of the Msi class. The values of $version, $language, and $product_name define
73 where to look for the msi file.
74
75 If construction fails then IsValid() will return false.
76
77=cut
45sub new ($$$$)
78sub new ($$$$$$)
46{
79{
47 my ($class, $version, $language, $product_name) = @_;
80 my ($class, $filename, $version, $is_current_version, $language, $product_name) = @_;
48
81
49 my $path = installer::patch::InstallationSet::GetUnpackedMsiPath(
50 $version,
51 $language,
52 "msi",
53 $product_name);
54
55 # Find the msi in the path.
56 my $filename = undef;
57 if ( -d $path)
58 {
59 my @msi_files = glob(File::Spec->catfile($path, "*.msi"));
60 if (scalar @msi_files != 1)
61 {
62 printf STDERR ("there are %d msi files in %s, should be 1", scalar @msi_files, $filename);
63 $filename = "";
64 }
65 else
66 {
67 $filename = $msi_files[0];
68 }
69 }
70 else
71 {
72 installer::logger::PrintError("can not access path '%s' to find msi\n", $path);
73 return undef;
74 }
75
76 if ( ! -f $filename)
77 {
82 if ( ! -f $filename)
83 {
78 installer::logger::PrintError("can not access MSI file at '%s'\n", $filename);
84 installer::logger::PrintError("can not find the .msi file for version %s and language %s at '%s'\n",
85 $version,
86 $language,
87 $filename);
79 return undef;
80 }
88 return undef;
89 }
81
90
82 my $self = {
83 'filename' => $filename,
91 my $self = {
92 'filename' => $filename,
84 'path' => $path,
93 'path' => dirname($filename),
85 'version' => $version,
94 'version' => $version,
95 'is_current_version' => $is_current_version,
86 'language' => $language,
87 'package_format' => "msi",
88 'product_name' => $product_name,
89 'tmpdir' => File::Temp->newdir(CLEANUP => 1),
90 'is_valid' => -f $filename
91 };
92 bless($self, $class);
93

--- 8 unchanged lines hidden (view full) ---

102 my ($self) = @_;
103
104 return $self->{'is_valid'};
105}
106
107
108
109
96 'language' => $language,
97 'package_format' => "msi",
98 'product_name' => $product_name,
99 'tmpdir' => File::Temp->newdir(CLEANUP => 1),
100 'is_valid' => -f $filename
101 };
102 bless($self, $class);
103

--- 8 unchanged lines hidden (view full) ---

112 my ($self) = @_;
113
114 return $self->{'is_valid'};
115}
116
117
118
119
120=head2 Commit($self)
121
122 Write all modified tables back into the databse.
123
124=cut
125sub Commit ($)
126{
127 my $self = shift;
128
129 my @tables_to_update = ();
130 foreach my $table (values %{$self->{'tables'}})
131 {
132 push @tables_to_update,$table if ($table->IsModified());
133 }
134
135 if (scalar @tables_to_update > 0)
136 {
137 $installer::logger::Info->printf("writing modified tables to database:\n");
138 foreach my $table (@tables_to_update)
139 {
140 $installer::logger::Info->printf(" %s\n", $table->GetName());
141 $self->PutTable($table);
142 }
143
144 foreach my $table (@tables_to_update)
145 {
146 $table->UpdateTimestamp();
147 $table->MarkAsUnmodified();
148 }
149 }
150}
151
152
153
154
110=head2 GetTable($seld, $table_name)
111
112 Return an MsiTable object for $table_name. Table objects are kept
113 alive for the life time of the Msi object. Therefore the second
114 call for the same table is very cheap.
115
116=cut
117sub GetTable ($$)

--- 6 unchanged lines hidden (view full) ---

124 my $table_filename = File::Spec->catfile($self->{'tmpdir'}, $table_name .".idt");
125 if ( ! -f $table_filename
126 || ! EnsureAYoungerThanB($table_filename, $self->{'fullname'}))
127 {
128 # Extract table from database to text file on disk.
129 my $truncated_table_name = length($table_name)>8 ? substr($table_name,0,8) : $table_name;
130 my $command = join(" ",
131 "msidb.exe",
155=head2 GetTable($seld, $table_name)
156
157 Return an MsiTable object for $table_name. Table objects are kept
158 alive for the life time of the Msi object. Therefore the second
159 call for the same table is very cheap.
160
161=cut
162sub GetTable ($$)

--- 6 unchanged lines hidden (view full) ---

169 my $table_filename = File::Spec->catfile($self->{'tmpdir'}, $table_name .".idt");
170 if ( ! -f $table_filename
171 || ! EnsureAYoungerThanB($table_filename, $self->{'fullname'}))
172 {
173 # Extract table from database to text file on disk.
174 my $truncated_table_name = length($table_name)>8 ? substr($table_name,0,8) : $table_name;
175 my $command = join(" ",
176 "msidb.exe",
132 "-d", installer::patch::Tools::CygpathToWindows($self->{'filename'}),
133 "-f", installer::patch::Tools::CygpathToWindows($self->{'tmpdir'}),
177 "-d", installer::patch::Tools::ToEscapedWindowsPath($self->{'filename'}),
178 "-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}),
134 "-e", $table_name);
135 my $result = qx($command);
136 print $result;
137 }
138
139 # Read table into memory.
140 $table = new installer::patch::MsiTable($table_filename, $table_name);
141 $self->{'tables'}->{$table_name} = $table;
142 }
143
144 return $table;
145}
146
147
148
149
179 "-e", $table_name);
180 my $result = qx($command);
181 print $result;
182 }
183
184 # Read table into memory.
185 $table = new installer::patch::MsiTable($table_filename, $table_name);
186 $self->{'tables'}->{$table_name} = $table;
187 }
188
189 return $table;
190}
191
192
193
194
195=head2 PutTable($self, $table)
196
197 Write the given table back to the databse.
198
199=cut
200sub PutTable ($$)
201{
202 my ($self, $table) = @_;
203
204 # Create text file from the current table content.
205 $table->WriteFile();
206
207 my $table_name = $table->GetName();
208
209 # Store table from text file into database.
210 my $table_filename = $table->{'filename'};
211
212 if (length($table_name) > 8)
213 {
214 # The file name of the table data must not be longer than 8 characters (not counting the extension).
215 # The name passed as argument to the -i option may be longer.
216 my $truncated_table_name = substr($table_name,0,8);
217 my $table_truncated_filename = File::Spec->catfile(
218 dirname($table_filename),
219 $truncated_table_name.".idt");
220 File::Copy::copy($table_filename, $table_truncated_filename) || die("can not create table file with short name");
221 }
222
223 my $command = join(" ",
224 "msidb.exe",
225 "-d", installer::patch::Tools::ToEscapedWindowsPath($self->{'filename'}),
226 "-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}),
227 "-i", $table_name);
228 my $result = system($command);
229
230 if ($result != 0)
231 {
232 installer::logger::PrintError("writing table '%s' back to database failed", $table_name);
233 # For error messages see http://msdn.microsoft.com/en-us/library/windows/desktop/aa372835%28v=vs.85%29.aspx
234 }
235}
236
237
238
239
150=head2 EnsureAYoungerThanB ($filename_a, $filename_b)
151
152 Internal function (not a method) that compares to files according
153 to their last modification times (mtime).
154
155=cut
156sub EnsureAYoungerThanB ($$)
157{

--- 63 unchanged lines hidden (view full) ---

221 else
222 {
223 my ($long,$short) = installer::patch::Msi::SplitLongShortName($name);
224 return ($long,$short,$long,$short);
225 }
226}
227
228
240=head2 EnsureAYoungerThanB ($filename_a, $filename_b)
241
242 Internal function (not a method) that compares to files according
243 to their last modification times (mtime).
244
245=cut
246sub EnsureAYoungerThanB ($$)
247{

--- 63 unchanged lines hidden (view full) ---

311 else
312 {
313 my ($long,$short) = installer::patch::Msi::SplitLongShortName($name);
314 return ($long,$short,$long,$short);
315 }
316}
317
318
319=head2 GetDirectoryMap($self)
229
320
321 Return a map that maps directory unique names (column 'Directory' in table 'Directory')
322 to hashes that contains short and long source and target names.
230
323
231=head2 GetFileToDirectoryMap ($)
232
233 Return a map (hash) that maps the unique name (column 'File' in
234 the 'File' table) to its directory names. Each value is a
235 reference to an array of two elements: the source path and the
236 target path.
237
238 The map is kept alive for the lifetime of the Msi object. All
239 calls but the first are cheap.
240
241=cut
324=cut
242sub GetFileToDirectoryMap ($)
325sub GetDirectoryMap ($)
243{
244 my ($self) = @_;
245
326{
327 my ($self) = @_;
328
246 if (defined $self->{'FileToDirectoryMap'})
329 if (defined $self->{'DirectoryMap'})
247 {
330 {
248 return $self->{'FileToDirectoryMap'};
331 return $self->{'DirectoryMap'};
249 }
250
332 }
333
251 my $file_table = $self->GetTable("File");
252 my $directory_table = $self->GetTable("Directory");
334 my $directory_table = $self->GetTable("Directory");
253 my $component_table = $self->GetTable("Component");
254 $installer::logger::Info->printf("got access to tables File, Directory, Component\n");
255
256 my %dir_map = ();
257 foreach my $row (@{$directory_table->GetAllRows()})
258 {
335 my %dir_map = ();
336 foreach my $row (@{$directory_table->GetAllRows()})
337 {
259 my ($target_name, undef, $source_name, undef)
338 my ($target_long_name, $target_short_name, $source_long_name, $source_short_name)
260 = installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir"));
339 = installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir"));
261 $dir_map{$row->GetValue("Directory")} = {
340 my $unique_name = $row->GetValue("Directory");
341 $dir_map{$unique_name} =
342 {
343 'unique_name' => $unique_name,
262 'parent' => $row->GetValue("Directory_Parent"),
344 'parent' => $row->GetValue("Directory_Parent"),
263 'source_name' => $source_name,
264 'target_name' => $target_name};
345 'default_dir' => $row->GetValue("DefaultDir"),
346 'source_long_name' => $source_long_name,
347 'source_short_name' => $source_short_name,
348 'target_long_name' => $target_long_name,
349 'target_short_name' => $target_short_name
350 };
265 }
266
267 # Set up full names for all directories.
268 my @todo = map {$_} (keys %dir_map);
351 }
352
353 # Set up full names for all directories.
354 my @todo = map {$_} (keys %dir_map);
269 my $process_count = 0;
270 my $push_count = 0;
271 while (scalar @todo > 0)
272 {
355 while (scalar @todo > 0)
356 {
273 ++$process_count;
274
275 my $key = shift @todo;
276 my $item = $dir_map{$key};
277 next if defined $item->{'full_source_name'};
278
279 if ($item->{'parent'} eq "")
280 {
281 # Directory has no parent => full names are the same as the name.
357 my $key = shift @todo;
358 my $item = $dir_map{$key};
359 next if defined $item->{'full_source_name'};
360
361 if ($item->{'parent'} eq "")
362 {
363 # Directory has no parent => full names are the same as the name.
282 $item->{'full_source_name'} = $item->{'source_name'};
283 $item->{'full_target_name'} = $item->{'target_name'};
364 $item->{'full_source_long_name'} = $item->{'source_long_name'};
365 $item->{'full_source_short_name'} = $item->{'source_short_name'};
366 $item->{'full_target_long_name'} = $item->{'target_long_name'};
367 $item->{'full_target_short_name'} = $item->{'target_short_name'};
284 }
285 else
286 {
287 my $parent = $dir_map{$item->{'parent'}};
368 }
369 else
370 {
371 my $parent = $dir_map{$item->{'parent'}};
288 if ( defined $parent->{'full_source_name'})
372 if ( defined $parent->{'full_source_long_name'})
289 {
290 # Parent aleady has full names => we can create the full name of the current item.
373 {
374 # Parent aleady has full names => we can create the full name of the current item.
291 $item->{'full_source_name'} = $parent->{'full_source_name'} . "/" . $item->{'source_name'};
292 $item->{'full_target_name'} = $parent->{'full_target_name'} . "/" . $item->{'target_name'};
375 $item->{'full_source_long_name'}
376 = $parent->{'full_source_long_name'} . "/" . $item->{'source_long_name'};
377 $item->{'full_source_short_name'}
378 = $parent->{'full_source_short_name'} . "/" . $item->{'source_short_name'};
379 $item->{'full_target_long_name'}
380 = $parent->{'full_target_long_name'} . "/" . $item->{'target_long_name'};
381 $item->{'full_target_short_name'}
382 = $parent->{'full_target_short_name'} . "/" . $item->{'target_short_name'};
293 }
294 else
295 {
296 # Parent has to be processed before the current item can be processed.
297 # Push both to the head of the list.
298 unshift @todo, $key;
299 unshift @todo, $item->{'parent'};
383 }
384 else
385 {
386 # Parent has to be processed before the current item can be processed.
387 # Push both to the head of the list.
388 unshift @todo, $key;
389 unshift @todo, $item->{'parent'};
300
301 ++$push_count;
302 }
303 }
304 }
305
390 }
391 }
392 }
393
306 foreach my $key (keys %dir_map)
394 # Postprocess the path names for cleanup.
395 foreach my $item (values %dir_map)
307 {
396 {
308 $dir_map{$key}->{'full_source_name'} =~ s/\/(\.\/)+/\//g;
309 $dir_map{$key}->{'full_source_name'} =~ s/^SourceDir\///;
310 $dir_map{$key}->{'full_target_name'} =~ s/\/(\.\/)+/\//g;
311 $dir_map{$key}->{'full_target_name'} =~ s/^SourceDir\///;
397 foreach my $id (
398 'full_source_long_name',
399 'full_source_short_name',
400 'full_target_long_name',
401 'full_target_short_name')
402 {
403 $item->{$id} =~ s/\/(\.\/)+/\//g;
404 $item->{$id} =~ s/^SourceDir\///;
405 $item->{$id} =~ s/^\.$//;
406 }
312 }
407 }
313 $installer::logger::Info->printf("for %d directories there where %d processing steps and %d pushes\n",
314 $directory_table->GetRowCount(),
315 $process_count,
316 $push_count);
317
408
409 $self->{'DirectoryMap'} = \%dir_map;
410 return $self->{'DirectoryMap'};
411}
412
413
414
415
416=head2 GetFileMap ($)
417
418 Return a map (hash) that maps the unique name (column 'File' in
419 the 'File' table) to data that is associated with that file, like
420 the directory or component.
421
422 The map is kept alive for the lifetime of the Msi object. All
423 calls but the first are cheap.
424
425=cut
426sub GetFileMap ($)
427{
428 my ($self) = @_;
429
430 if (defined $self->{'FileMap'})
431 {
432 return $self->{'FileMap'};
433 }
434
435 my $file_table = $self->GetTable("File");
436 my $component_table = $self->GetTable("Component");
437 my $dir_map = $self->GetDirectoryMap();
438
318 # Setup a map from component names to directory items.
439 # Setup a map from component names to directory items.
319 my %component_to_directory_map = map {$_->GetValue('Component') => $_->GetValue('Directory_')} @{$component_table->GetAllRows()};
440 my %component_to_directory_map =
441 map
442 {$_->GetValue('Component') => $_->GetValue('Directory_')}
443 @{$component_table->GetAllRows()};
320
321 # Finally, create the map from files to directories.
444
445 # Finally, create the map from files to directories.
322 my $map = {};
446 my $file_map = {};
323 my $file_component_index = $file_table->GetColumnIndex("Component_");
324 my $file_file_index = $file_table->GetColumnIndex("File");
325 foreach my $file_row (@{$file_table->GetAllRows()})
326 {
327 my $component_name = $file_row->GetValue($file_component_index);
328 my $directory_name = $component_to_directory_map{$component_name};
447 my $file_component_index = $file_table->GetColumnIndex("Component_");
448 my $file_file_index = $file_table->GetColumnIndex("File");
449 foreach my $file_row (@{$file_table->GetAllRows()})
450 {
451 my $component_name = $file_row->GetValue($file_component_index);
452 my $directory_name = $component_to_directory_map{$component_name};
329 my $dir_item = $dir_map{$directory_name};
330 my $unique_name = $file_row->GetValue($file_file_index);
453 my $unique_name = $file_row->GetValue($file_file_index);
331 $map->{$unique_name} = [$dir_item->{'full_source_name'},$dir_item->{'full_target_name'}];
454 $file_map->{$unique_name} = {
455 'directory' => $dir_map->{$directory_name},
456 'component_name' => $component_name
457 };
332 }
333
458 }
459
334 $installer::logger::Info->printf("got full paths for %d files\n",
335 $file_table->GetRowCount());
336
337 $self->{'FileToDirectoryMap'} = $map;
338 return $map;
460 $self->{'FileMap'} = $file_map;
461 return $file_map;
339}
340
341
3421;
462}
463
464
4651;