Subversion Repositories camp_sysinfo_client_3

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
101 rodolico 1
#! /usr/bin/env perl
2
 
3
use strict;
4
use warnings;
5
 
132 rodolico 6
# install.pl
7
#
8
# installer for perl script, in this case, sysinfo
9
#
10
# Revision history
11
#
12
# Version 1.1.7 20161010 RWR
13
# Added ability to validate required libraries are installed
14
#
15
# version 1.2 20170327 RWR
16
# did some major modifications to correctly work on BSD systems also
17
#
18
# version 2.0 20190330 RWR
19
# changed it so all configs are YAML
20
#
21
# version 3.0 20191105 RWR
22
# set up so all options are presented on initial screen
23
# user can choose options to edit
24
# rest of install is automatic
138 rodolico 25
#
26
# version 3.1 20191112 RWR
27
# Added logging. Log file will be written to the install directory with
28
# the application name.log as the name (application name taken from installer_config.pl)
144 rodolico 29
#
30
# version 3.1.1 20191117 RWR
31
# Added the ability to verify cpan is installed and configured on systems which need it
32
#
156 rodolico 33
# version 3.1.2 20200215 RWR
34
# Adding version comparisons
101 rodolico 35
 
132 rodolico 36
# find our location and use it for searching for libraries
37
BEGIN {
38
   use FindBin;
39
   use File::Spec;
40
   use lib File::Spec->catdir($FindBin::Bin);
41
   eval( 'use YAML::Tiny' );
42
}
43
 
101 rodolico 44
my $sourceDir = File::Spec->catdir($FindBin::Bin);
45
 
156 rodolico 46
use Digest::MD5 qw(md5_hex);
47
 
48
# define the version number
49
# see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
50
use version;
51
our $VERSION = version->declare("v3.001.002");
52
 
53
 
132 rodolico 54
use Data::Dumper;
55
use File::Basename;
56
use Getopt::Long;
101 rodolico 57
 
132 rodolico 58
our %install;
59
our %operatingSystems;
60
our %libraries;
61
our %binaries;
62
 
63
do "$sourceDir/installer_config.pl";
138 rodolico 64
#
65
# set up log file
66
my $logFile = $install{'application name'};
67
$logFile =~ s/ /_/g;
68
$logFile = "$sourceDir/$logFile.log";
132 rodolico 69
 
70
Getopt::Long::Configure ("bundling"); # allow -vd --os='debian'
71
my $dryRun = 0;
72
my $os;
73
my $help = 0;
74
my $version = 0;
75
 
76
my @messages; # stores any messages we want to show up at the end
77
my @feedback; # store feedback when we execute command line actions
78
 
79
my %configuration;
80
 
81
# simple display if --help is passed
82
sub help {
83
   my $oses = join( ' ', keys %operatingSystems );
156 rodolico 84
   use File::Basename;
85
   print basename($0) . " $VERSION\n";
132 rodolico 86
   print <<END
156 rodolico 87
$0 [options]
88
Options:
89
   --os      - osname is one of [$oses]
90
   --dryrun  - do not actually do anything, just tell you what I'd do
91
   --version - display version and exit
132 rodolico 92
END
101 rodolico 93
}
94
 
138 rodolico 95
#######################################################
144 rodolico 96
#
97
# timeStamp
98
#
99
# return current system date as YYYY-MM-DD HH:MM:SS
100
#
101
#######################################################
102
sub timeStamp {
103
   my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
104
   return sprintf "%4d-%02d-%02d %02d:%02d:%02d",$year+1900,$mon+1,$mday,$hour,$min,$sec;
105
}
106
 
107
sub yesno {
108
   my ( $prompt, $default ) = @_;
109
   $default = 'yes' unless $default;
110
   my $answer = &getAnswer( $prompt, $default eq 'yes' ? ('yes','no' ) : ('no', 'yes') );
111
   return lc( substr( $answer, 0, 1 ) ) eq 'y';
112
}
113
 
114
# prompt the user for a response, then allow them to enter it
115
# the first response is considered the default and is printed
116
# in all caps if more than one exists
117
# first answer is used if they simply press the Enter
118
# key. The response is returned all lower case if more than one
119
# exists.
120
# it is assumed 
121
sub getAnswer {
122
   my ( $prompt, @answers ) = @_;
123
   $answers[0] = '' unless defined( $answers[0] );
124
   my $default = $answers[0];
125
   my $onlyOneAnswer = scalar( @answers ) == 1;
126
   print $prompt . '[ ';
127
   $answers[0] = uc $answers[0] unless $onlyOneAnswer;
128
   print join( ' | ', @answers ) . ' ]: ';
129
   my $thisAnswer = <>;
130
   chomp $thisAnswer;
131
   $thisAnswer = $default unless $thisAnswer;
132
   return $thisAnswer;
133
}
134
 
135
 
136
#######################################################
138 rodolico 137
# function to simply log things
138
# first parameter is the priority, if <= $logDef->{'log level'} will print
139
# all subsequent parameters assumed to be strings to sent to the log
140
# returns 0 on failure
141
#         1 on success
142
#         2 if priority > log level
143
#        -1 if $logDef is unset
144
# currently, only logs to a file
145
#######################################################
146
sub logIt {
140 rodolico 147
   open LOG, ">>$logFile" or die "Could not append to $logFile: $!\n";
138 rodolico 148
   while ( my $t = shift ) {
149
      print LOG &timeStamp() . "\t$t\n";
150
   }
151
   close LOG;
152
   return 1;
153
}
101 rodolico 154
 
138 rodolico 155
 
132 rodolico 156
# attempt to locate the operating system.
157
# if found, will set some defaults for it.
158
sub setUpOperatingSystemSpecific {
159
   my ( $install, $operatingSystems, $os, $installDir ) = @_;
138 rodolico 160
   &logIt( 'Entering setUpOperatingSystemSpecific' );
161
   &logIt( "They passed $os in as the \$os" );
132 rodolico 162
   if ( $os ) {
163
      # We found the OS, set up some defaults
164
      $$install{'os'} = $os;
138 rodolico 165
      &logIt( "Setting keys for operating system" );
132 rodolico 166
      # merge operatingSystems into install
167
      foreach my $key ( keys %{$operatingSystems->{$os}} ) {
168
         if ( $key eq 'files' ) {
169
            $install->{'files'} = { %{$install->{'files'}}, %{$operatingSystems->{$os}->{'files'}} }
170
         } else {
171
            $install->{$key} = $operatingSystems->{ $os }->{$key};
172
         }
173
      } # if it is a known OS
174
   } # if
175
   return $os;
176
} # getOperatingSystem
177
 
178
# validates the libraries needed exist
179
# simply eval's each library. If it doesn't exist, creates a list of
180
# commands to be executed to install it, and displays missing libraries
181
# offering to install them if possible.
182
sub validateLibraries {
183
   my ( $libraries, $os ) = @_;
138 rodolico 184
   &logIt( 'Entering validateLibraries' );
132 rodolico 185
   my %return;
186
   foreach my $lib ( keys %$libraries ) {
138 rodolico 187
      &logIt( "Checking on library $lib" );
132 rodolico 188
      eval( "use $lib;" );
189
      if ( $@ ) {
190
         $return{ $lib } = $libraries->{$lib}->{$os} ? $libraries->{$lib}->{$os} : 'UNK';
191
      }
192
   }
193
   return \%return;
194
} # validateLibraries
195
 
196
 
197
# check for any required binaries
198
sub validateBinaries {
199
   my ( $binaries, $os ) = @_;
138 rodolico 200
   &logIt( 'Entering validateBinaries' );
132 rodolico 201
   my %return;
202
   foreach my $bin ( keys %$binaries ) {
203
      unless ( `which $bin` ) {
204
         $return{$bin} = $binaries->{$bin}->{$os} ? $binaries->{$bin}->{$os} : 'UNK';
205
      }
206
   }
207
   return \%return;
208
} # validateBinaries
209
 
210
# get some input from the user and decide how to install/upgrade/remove/whatever
211
sub getInstallActions {
212
   my $install = shift;
138 rodolico 213
   &logIt( 'Entering getInstallActions' );
132 rodolico 214
   if ( -d $install->{'confdir'} ) {
215
      $install->{'action'} = "upgrade";
216
   } else {
217
      $install->{'action'} = 'install';
218
   }
219
   $install->{'build config'} = 'Y';
220
   $install->{'setup cron'} = 'Y';
101 rodolico 221
}
222
 
132 rodolico 223
# locate all items in $hash which have one of the $placeholder elements in it
224
# and replace, ie <binddir> is replaced with the actual binary directory
225
sub doPlaceholderSubstitution {
226
   my ($hash, $placeholder) = @_;
138 rodolico 227
   &logIt( 'Entering doPlaceholderSubstitution' );
132 rodolico 228
   return if ref $hash ne 'HASH';
229
   foreach my $key ( keys %$hash ) {
230
      if ( ref( $$hash{$key} ) ) {
231
         &doPlaceholderSubstitution( $$hash{$key}, $placeholder );
232
      } else {
233
         foreach my $place ( keys %$placeholder ) {
234
            $$hash{$key} =~ s/$place/$$placeholder{$place}/;
235
         } # foreach
236
      } # if..else
237
   } # foreach
238
   return;
239
}
240
 
241
# This will go through and first, see if anything is a directory, in
242
# which case, we'll create new entries for all files in there.
243
# then, it will do keyword substitution of <bindir> and <confdir>
244
# to populate the target.
245
# When this is done, each file should have a source and target that is
246
# a fully qualified path and filename
247
sub populateSourceDir {
248
   my ( $install, $sourceDir ) = @_;
138 rodolico 249
   &logIt( 'Entering populateSourceDir' );
132 rodolico 250
   my %placeHolders = 
251
      ( 
252
        '<bindir>' => $$install{'bindir'},
253
        '<confdir>' => $$install{'confdir'},
254
        '<default owner>' => $$install{'default owner'},
255
        '<default group>' => $$install{'default group'},
256
        '<default permission>' => $$install{'default permission'},
257
        '<installdir>' => $sourceDir
258
      );
101 rodolico 259
 
132 rodolico 260
   my $allFiles = $$install{'files'};
101 rodolico 261
 
132 rodolico 262
   # find all directory entries and load files in that directory into $$install{'files'}
263
   foreach my $dir ( keys %$allFiles ) {
264
      if ( defined( $$allFiles{$dir}{'type'} ) && $$allFiles{$dir}{'type'} eq 'directory' ) {
138 rodolico 265
         &logIt( "Found directory $dir" );
132 rodolico 266
         if ( opendir( my $dh, "$sourceDir/$dir" ) ) {
267
            my @files = map{ $dir . '/' . $_ } grep { ! /^\./ && -f "$sourceDir/$dir/$_" } readdir( $dh );
138 rodolico 268
            &logIt( "\tFound files " . join( ' ', @files ) );
132 rodolico 269
            foreach my $file ( @files ) {
270
               $$allFiles{ $file }{'type'} = 'file';
271
               if ( $dir eq 'modules' ) {
272
                  $$allFiles{ $file }{'permission'} = ( $file =~ m/$$install{'modules'}/ ) ? '0700' : '0600';
273
               } else {
274
                  $$allFiles{ $file }{'permission'} = $$allFiles{ $dir }{'permission'};
275
               }
276
               $$allFiles{ $file }{'owner'} = $$allFiles{ $dir }{'owner'};
277
               $$allFiles{ $file }{'target'} = $$allFiles{ $dir }{'target'};
278
            } # foreach
279
            closedir $dh;
280
         } # if opendir
281
      } # if it is a directory
282
   } # foreach
283
   # find all files, and set the source directory, and add the filename to
284
   # the target
285
   foreach my $file ( keys %$allFiles ) {
286
      $$allFiles{$file}{'source'} = "$sourceDir/$file";
287
      $$allFiles{$file}{'target'} .= "/$file";
288
   } # foreach
101 rodolico 289
 
132 rodolico 290
   # finally, do place holder substitution. This recursively replaces all keys
291
   # in  %placeHolders with the values.
292
   &doPlaceholderSubstitution( $install, \%placeHolders );
101 rodolico 293
 
144 rodolico 294
   &logIt( "Exiting populateSourceDir with values\n" . Dumper( $install ) ) ;
101 rodolico 295
 
132 rodolico 296
   return 1;
297
} # populateSourceDir
298
 
299
sub GetPermission {
300
   my $install = shift;
138 rodolico 301
   &logIt( 'Entering GetPermission' );
132 rodolico 302
   print "Ready to install, please verify the following\n";
303
   print "A. Operating System:  " . $install->{'os'} . "\n";
304
   print "B. Installation Type: " . $install->{'action'} . "\n";
305
   print "C. Using Seed file: " . $install->{'configuration seed file'} . "\n" if -e $install->{'configuration seed file'};
306
   print "D. Target Binary Directory: " . $install->{'bindir'} . "\n";
307
   print "E. Target Configuration Directory: " . $install->{'confdir'} . "\n";
308
   print "F. Automatic Running: " . ( $install->{'crontab'} ? $install->{'crontab'} : 'No' ) . "\n";
309
   print "G. Install Missing Perl Libraries\n";
310
   foreach my $task ( sort keys %{ $install->{'missing libraries'} } ) {
144 rodolico 311
      print "\t$task -> " . $install->{'missing libraries'}->{$task}->{'command'} . ' ' . $install->{'missing libraries'}->{$task}->{'parameter'} . "\n";
101 rodolico 312
   }
132 rodolico 313
   print "H. Install Missing Binaries\n";
314
   foreach my $task ( sort keys %{ $install->{'missing binaries'} } ) {
144 rodolico 315
      print "\t$task -> " . $install->{'missing binaries'}->{$task}->{'command'} . ' ' . $install->{'missing binaries'}->{$task}->{'parameter'} . "\n";
132 rodolico 316
   }
317
   return &yesno( "Do you want to proceed?" );
101 rodolico 318
}
319
 
132 rodolico 320
# note, this fails badly if you stick non-numerics in the version number
321
# simply create a "number" from the version which may have an arbitrary
322
# number of digits separated by a period, for example
323
# 1.2.5
324
# while there are digits, divide current calculation by 100, then add the last
325
# digit popped of. So, 1.2.5 would become
326
# 1 + 2/100 + 5/10000, or 1.0205
327
# and 1.25.16 would become 1.2516
328
# 
329
# Will give invalid results if any set of digits is more than 99
330
sub dotVersion2Number {
331
   my $dotVersion = shift;
332
 
333
   my @t = split( '\.', $dotVersion );
334
   #print "\n$dotVersion\n" . join( "\n", @t ) . "\n";
335
   my $return = 0;
336
   while ( @t ) {
337
      $return /= 100;
338
      $return += pop @t;
339
   }
340
   #print "$return\n";
341
   return $return;
342
}
343
 
344
# there is a file named VERSIONS. We get the values out of the install
345
# directory and (if it exists) the target so we can decide what needs
346
# to be updated.
347
sub getVersions {
348
   my $install = shift;
138 rodolico 349
   &logIt( 'Entering getVersions' );
132 rodolico 350
   my $currentVersionFile = $install->{'files'}->{'VERSION'}->{'target'};
351
   my $newVersionFile = $install->{'files'}->{'VERSION'}->{'source'};
352
   if ( open FILE,"<$currentVersionFile" ) {
353
      while ( my $line = <FILE> ) {
354
         chomp $line;
355
         my ( $filename, $version, $checksum ) = split( "\t", $line );
356
         $install{'files'}->{$filename}->{'installed version'} = $version ? $version : '';
156 rodolico 357
         $install{'files'}->{$filename}->{'installed checksum'} = $checksum ? $checksum : '';
132 rodolico 358
      }
359
      close FILE;
360
   }
361
   if ( open FILE,"<$newVersionFile" ) {
362
      while ( my $line = <FILE> ) {
363
         chomp $line;
364
         my ( $filename, $version, $checksum ) = split( "\t", $line );
365
         $install->{'files'}->{$filename}->{'new version'} = $version ? $version : '';
156 rodolico 366
         $install->{'files'}->{$filename}->{'new checksum'} = $checksum ? $checksum : '';
132 rodolico 367
      }
368
      close FILE;
369
   }
370
   foreach my $file ( keys %{$$install{'files'}} ) {
156 rodolico 371
      $install{'files'}->{$file}->{'installed version'} = '' unless defined $install->{'files'}->{$file}->{'installed version'};
372
      $install{'files'}->{$file}->{'new version'} = '' unless defined $install->{'files'}->{$file}->{'new version'};
132 rodolico 373
   }
374
   return 1;
375
} # getVersions
376
 
144 rodolico 377
# checks if a directory exists and, if not, creates it
378
my %directories; # holds list of directories already created so no need to do an I/O
132 rodolico 379
 
144 rodolico 380
sub checkDirectoryExists {
381
   my ( $filename,$mod,$owner ) = @_;
382
   $mod = "0700" unless $mod;
383
   $owner = "root:root" unless $owner;
384
   &logIt( "Checking Directory for $filename with $mod and $owner" );
385
   my ($fn, $dirname) = fileparse( $filename );
386
   logIt( "\tParsing out $dirname and $filename" );
387
   return '' if exists $directories{$dirname};
388
   if ( -d $dirname ) {
389
      $directories{$dirname} = 1;
390
      return '';
391
   }
392
   if ( &runCommand( "mkdir -p $dirname", "chmod $mod $dirname", "chown $owner $dirname" ) ) {
393
      $directories{$dirname} = 1;
394
   }
395
   return '';   
396
}
397
 
398
# runs a system command. Also, if in testing mode, simply shows what
399
# would have been done.
400
sub runCommand {
401
   while ( my $command = shift ) {
402
      if ( $dryRun ) {
403
         print "$command\n";
404
      } else {
405
         `$command`;
406
      }
407
   }
408
   return 1;
409
} # runCommand
410
 
132 rodolico 411
# this actually does the installation, except for the configuration
412
sub doInstall {
413
   my $install = shift;
138 rodolico 414
   &logIt( 'Entering doInstall' );
132 rodolico 415
   my $fileList = $install->{'files'};
416
 
417
   &checkDirectoryExists( $install->{'bindir'} . '/', $install->{'default permission'}, $install->{'default owner'} . ':' . $install->{'default group'} );
418
   foreach my $file ( keys %$fileList ) {
419
      next unless ( $fileList->{$file}->{'type'} && $fileList->{$file}->{'type'} eq 'file' );
420
 
156 rodolico 421
#   *********** Removed error checking to get this working; should reenable later
422
#      if ( version->parse( $fileList->{$file}->{'installed version'} ) && version->parse( $fileList->{$file}->{'installed version'} ) < version->parse( $fileList->{$file}->{'new version'} ) ) {
423
#         # we have a new version, so overwrite it
424
#      } elsif ( $fileList->{$file}->{'installed checksum'} eq $fileList->{$file}->{'installed checksum'} ) { # has file been modified
425
#      }
426
#         
427
#
428
#      next if $install->{'action'} eq 'upgrade' && ! defined( $fileList->{$file}->{'installed version'} )
429
#              ||
430
#              ( &dotVersion2Number( $fileList->{$file}->{'new version'} ) <= &dotVersion2Number($fileList->{$file}->{'installed version'} ) );
431
 
132 rodolico 432
      &checkDirectoryExists( $fileList->{$file}->{'target'}, $install->{'default permission'}, $install->{'default owner'} . ':' . $install->{'default group'} );
433
      &runCommand( 
434
            "cp $fileList->{$file}->{'source'} $fileList->{$file}->{'target'}",
435
            "chmod $fileList->{$file}->{'permission'} $fileList->{$file}->{'target'}",
436
            "chown $fileList->{$file}->{'owner'} $fileList->{$file}->{'target'}"
437
            );
438
      # if there is a post action, run it and store any return in @feedback
439
      push @feedback, `$fileList->{$file}->{'post action'}` if defined $fileList->{$file}->{'post action'};
440
      # if there is a message to be passed, store it in @messages
156 rodolico 441
      push @messages, $fileList->{$file}->{'message'} if defined $fileList->{$file}->{'message'};
132 rodolico 442
   } # foreach file
443
   # set up crontab, if necessary
444
   &runCommand( $install->{'crontab'} ) if defined ( $install->{'crontab'} );
445
   return 1;
446
}
447
 
448
 
449
# installs binaries and libraries
450
sub installOSStuff {
451
   my $install = shift;
144 rodolico 452
   my %commands;
132 rodolico 453
   my @actions = values( %{ $install->{'missing libraries'} } );
454
   push @actions, values( %{ $install->{'missing binaries'} } );
455
   foreach my $action ( @actions ) {
144 rodolico 456
      $commands{$action->{'command'}} .= ' ' . $action->{'parameter'};
457
      #&logIt( $action );
458
      #&runCommand( $action );
132 rodolico 459
   }
144 rodolico 460
   foreach my $command ( keys %commands ) {
461
      print "Running command $command $commands{$command}\n";
462
      `$command $commands{$command}`;
463
   }
132 rodolico 464
}
144 rodolico 465
 
466
################################################################
467
# validateCPAN
468
#
469
# some of the systems will need to run cpan to get some perl modules.
470
# this will go through each of them and see if command starts with cpan
471
# and, if so, will check that cpan is installed and configured for root.
472
#
473
# when cpan is installed, it requires one manual run as root from the cli
474
# to determine where to get the files. If that is not done, cpan can not
475
# be controlled by a program. We check to see if cpan is installed, then
476
# verify /root/.cpan has been created by the configuration tool
477
################################################################
478
 
479
 
480
sub validateCPAN {
481
   my $libraries = shift;
482
   my $needCPAN = 0;
483
   foreach my $app ( keys %$libraries ) {
484
      if ( $libraries->{$app}->{'command'} =~ m/^cpan/ ) {
485
         $needCPAN = 1;
486
         last;
487
      }
488
   }
489
   return unless $needCPAN;
490
   if ( `which cpan` ) {
491
      die "****ERROR****\nWe need cpan, and it is installed, but not configured\nrun cpan as root one time to configure it\n" unless -d '/root/.cpan';
492
   } else {
493
      die 'In order to install on this OS, we need cpan, which should have been installed with perl.' .
494
          " Can not continue until cpan is installed and configured\n";
495
   }
496
}   
132 rodolico 497
 
498
 
499
################################################################
500
#               Main Code                                      #
501
################################################################
502
 
503
# handle any command line parameters that may have been passed in
504
 
505
GetOptions (
506
            "os|o=s"      => \$os,      # pass in the operating system
507
            "dryrun|n"    => \$dryRun,  # do NOT actually do anything
508
            'help|h'      => \$help,
138 rodolico 509
            'version|v'   => \$version
132 rodolico 510
            ) or die "Error parsing command line\n";
511
 
512
if ( $help ) { &help() ; exit; }
156 rodolico 513
if ( $version ) { use File::Basename; print basename($0) . " $VERSION\n"; exit; }
132 rodolico 514
 
156 rodolico 515
print "Logging to $logFile\n";
516
 
138 rodolico 517
&logIt( 'Beginning installation' );
518
 
132 rodolico 519
$install{'os'} = &setUpOperatingSystemSpecific( \%install, \%operatingSystems, $os ? $os : `$sourceDir/determineOS`, $sourceDir );
138 rodolico 520
&logIt( "Operating System is $install{'os'}" );
521
 
132 rodolico 522
$install{'missing libraries'} = &validateLibraries( \%libraries, $install{'os'} );
144 rodolico 523
&logIt( "Missing Libraries\n" . Dumper( $install{'missing libraries'} ) );
138 rodolico 524
 
132 rodolico 525
$install{'missing binaries'} = &validateBinaries( \%binaries, $install{'os'} );
144 rodolico 526
&logIt( "Missing binaries\n" . Dumper( $install{'missing binaries'} ) );
138 rodolico 527
 
144 rodolico 528
if ( $install{'missing libraries'} ) {
529
   &validateCPAN( $install{'missing libraries'} );
530
}
531
 
132 rodolico 532
&getInstallActions( \%install );
144 rodolico 533
&logIt( "Completed getInstallActions\n" .  Dumper( \%install ) );
138 rodolico 534
 
144 rodolico 535
&populateSourceDir( \%install, $sourceDir );
132 rodolico 536
 
537
if ( &GetPermission( \%install ) ) {
538
   &installOSStuff( \%install );
539
   &getVersions( \%install ) unless $install{'action'} eq 'new';
540
   &doInstall( \%install );
541
} else {
542
   die "Please fix whatever needs to be done and try again\n";
138 rodolico 543
   &logIt( "User chose to kill process" );
132 rodolico 544
}
545
 
144 rodolico 546
&logIt( "Installation done, running \&postInstall if it exists" );
132 rodolico 547
 
144 rodolico 548
&postInstall( \%install )if defined( &postInstall );
549
 
550
1;