Subversion Repositories DevTools

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
5485 dpurdie 1
########################################################################
2
# Copyright (c) VIX TECHNOLOGY (AUST) LTD
3
#
4
# Module name   : create_dpkgFromTar.pl
5
# Module type   : JATS Utility
6
# Compiler(s)   : Perl
7
# Environment(s): jats
8
#
9
# Description   : This JATS utility is used by the build system to merge
10
#                 build artifacts from multiple build machines into one
11
#                 package.
12
#                 
13
#                 It complements the 'tarmode' provided by create_dpkg
14
#                 
15
#                 It is not intended to be run by a user.
16
#                 It is not intended to be run directly by the build system
17
#                 It is intended to be run from the build daemons via a shh session
18
#                       Progress is reported via stdout
19
#                       Exit code indicates success or error
20
#
21
# Usage         : See POD at the end of this file
22
#
23
#......................................................................#
24
 
25
require 5.008_002;
26
 
27
# Include Standard Perl Functions
28
#
29
use strict;
30
use warnings;
31
use Cwd;
32
use Getopt::Long;
33
use File::Basename;
34
use File::Find;
35
use File::Path;
36
use File::Copy;
37
use Pod::Usage;
38
use XML::Simple;
39
use Encode qw(decode encode);
40
use File::Temp qw/ tempfile tempdir /;
41
 
42
use JatsError;
43
use JatsEnv;
44
use FileUtils;
45
use JatsSystem;
5486 dpurdie 46
use ArrayHashUtils;
5485 dpurdie 47
 
48
# define Global variables
49
#
50
my $VERSION = "1.0.0";
51
my $PROGNAME = "create_dpkgFromTar.pl";
52
 
53
# Globals imported from environment
54
#
55
our $GBE_MACHTYPE;
56
our $GBE_HOSTNAME;
57
our $GBE_DPKG;
58
our $USER;
59
our $GBE_ABT;
60
 
61
# Global variables
62
#
5486 dpurdie 63
my $tmpDirInfo;
5485 dpurdie 64
my $workDir;
65
my $startDir;
66
my $maxHostNameLength = 8;
5486 dpurdie 67
my $pkgTargetDir;
68
my $deleteTargetDir;
5485 dpurdie 69
 
70
#
71
#   Option variables
72
#
73
my $opt_help = 0;
74
my $opt_manual = 0;
75
my $opt_verbose = 0;
76
my $opt_pname;
77
my $opt_pversion;
78
my $opt_srcPath;
5486 dpurdie 79
my $opt_MergeErrors = 0;
5485 dpurdie 80
my $opt_outputPath;
5486 dpurdie 81
my $opt_preDelete;
82
my $opt_tmpDir;
5485 dpurdie 83
 
84
#-------------------------------------------------------------------------------
85
# Function        : main entry point 
86
#
87
# Description     : Main Entry point
88
#
89
# Inputs          : 
90
#
91
# Returns         : 
92
#
93
    # Process any command line arguements...
94
    my $result = GetOptions (
95
                'help:+'        => \$opt_help,              # flag, multiple use allowed
96
                'manual:3'      => \$opt_help,              # flag
97
                'verbose:+'     => \$opt_verbose,           # flag, multiple use allowed
98
                'pname=s'       => \$opt_pname,             # string
99
                'pversion=s'    => \$opt_pversion,          # string
100
                'srcpath=s'     => \$opt_srcPath,           # string
101
                'mergeErrors!'  => \$opt_MergeErrors,       # [no]flag
102
                'output=s'      => \$opt_outputPath,        # String
5486 dpurdie 103
                'tmpdir=s'      => \$opt_tmpDir,            # String
104
                'predelete!'    => \$opt_preDelete,         # [no]flag
5485 dpurdie 105
                );              
106
 
107
    #
108
    #   Process help and manual options
109
    #
110
    pod2usage(-verbose => 0, -message => "Version: $VERSION")  if ($opt_help == 1  || ! $result);
111
    pod2usage(-verbose => 1)  if ($opt_help == 2 );
112
    pod2usage(-verbose => 2)  if ($opt_help > 2);
113
 
114
    #
115
    #   Init the error and message subsystem
116
    #
117
    ErrorConfig( 'name'    =>'CREATE_DPKG',
118
                 'verbose' => $opt_verbose );
119
 
120
    if ($opt_verbose)
121
    {
122
       Verbose ("Program: $PROGNAME");
123
       Verbose ("Version: $VERSION");
124
    }
125
 
126
    #
127
    #   Needed EnvVars
128
    #
129
    EnvImport ('GBE_MACHTYPE');
130
    EnvImport ('GBE_HOSTNAME');
131
    EnvImport ('USER' );
132
    EnvImport ('GBE_DPKG' );
133
    EnvImportOptional ('GBE_ABT', '');
134
 
5486 dpurdie 135
    # Defaults
136
    InitFileUtils();
137
    $startDir = Getcwd;
138
    $opt_outputPath = $::GBE_DPKG unless defined $opt_outputPath;
139
    $opt_srcPath = AbsPath($opt_srcPath) if defined $opt_srcPath;
140
    $opt_tmpDir = AbsPath($opt_tmpDir) if defined $opt_tmpDir;
141
 
5485 dpurdie 142
    #
143
    #   Basic sanity testing
144
    #
145
    Error ("Path for package fragments not specified") unless defined $opt_srcPath;
146
    Error ("Package fragment path not found", $opt_srcPath) unless -d $opt_srcPath;
147
    Error ("DPKG_ARCHIVE not found", $GBE_DPKG) unless -d $GBE_DPKG;
148
    Error ("Package name not specified") unless defined $opt_pname;
149
    Error ("Package version not specified") unless defined $opt_pversion;
5486 dpurdie 150
    Error ("Output path not specified" ) unless defined $opt_outputPath;
151
    Error ("Output path does not exist", $opt_outputPath) unless -d $opt_outputPath;
152
    Error ("TmpDir does not exist:", $opt_tmpDir) if (defined($opt_tmpDir) && ! -d ($opt_tmpDir));
5485 dpurdie 153
 
154
    #
155
    #   Create a temp work directory for this
156
    #       This will be removed on program exit 
157
    #       Not by File:Temp as it doesn't handle the case where we have chdir'd to the temp area
158
    #
5486 dpurdie 159
    if ($opt_tmpDir)
160
    {
161
        $workDir = $opt_tmpDir;
162
    }
163
    else
164
    {
165
        $tmpDirInfo = File::Temp->newdir( 'dpkgFromTar_XXXX', CLEANUP => 0, DIR => '/tmp' );
166
        $workDir = $tmpDirInfo->dirname;
167
    }
5485 dpurdie 168
    Verbose("WorkDir", $workDir);
169
    chdir($workDir)|| Error ("Cannot chdir to working directory: $workDir");
170
 
171
    #
172
    #   Locate all package fragements
173
    #   There must be at least one
174
    #   Package fragments are named after the package name and version and have a .tar.gz suffix
175
    #
176
    my $basename = join('_', $opt_pname, $opt_pversion);
177
    my $basenameLen = 1 + length $basename;
178
    $basename .= '_*.tar.gz';
179
    my @packageFragments = glob (catfile($opt_srcPath, $basename ));
180
    Error ("No package fragments found.", "Path: $opt_srcPath", "Glob: $basename" ) unless @packageFragments;
181
    Message("Package Fragmnets found:", @packageFragments);
182
 
183
    #
184
    #   Extract the built.files.<hostname>.xml and descpkg from each of package fragments
5486 dpurdie 185
    #   Note: Use of -m flag to tar is to overcome issues with the bsdtar used under windows
186
    #         to create the tar.gz files. It appears to insert localtime and not GMT into 
187
    #         the file.
5485 dpurdie 188
    #
189
    my %pkgData;   
190
    foreach my $srcfile ( @packageFragments)
191
    {
5486 dpurdie 192
        Message ("Extracting metadata from " . StripDir($srcfile));
5485 dpurdie 193
        my $basename = $srcfile;
194
        $basename =~ s~^.*/~~;
195
        $basename =~ s~\.gz$~~;
196
        $basename =~ s~\.tar$~~;
197
        $basename = substr($basename, $basenameLen);
198
        $pkgData{$srcfile}{basename} = $basename;
199
        mkpath ($basename);
200
        Error ("Temp subdir $basename not created: $!") unless -d $basename;
5486 dpurdie 201
        my $rv = System ('tar', '-xzmf', $srcfile, 
5485 dpurdie 202
                            IsVerbose(1) ? '-v' : undef, 
203
                            '-C', $basename, 
5486 dpurdie 204
                            '--wildcards', './built.files.*.xml' );
5485 dpurdie 205
        Error("Tar extraction error: $srcfile") if ($rv);
206
    }
207
 
208
    #
209
    #   Read in the XML from each of the files
210
    #
211
    my %fileData;
212
    foreach my $srcfile ( keys %pkgData )
213
    {
214
        my @extracted = glob(catfile($pkgData{$srcfile}{basename}, 'built.files.*.xml'));
5486 dpurdie 215
        foreach my $srcfile ( @extracted)
5485 dpurdie 216
        {
5486 dpurdie 217
            my $ref = XML::Simple::XMLin($srcfile, ForceArray => 1, KeyAttr => []);
218
            #DebugDumpData("REF - $srcfile, " .ref($ref), $ref);
219
 
5485 dpurdie 220
            foreach my $entry (@{$ref->{file}})
221
            {
5486 dpurdie 222
                #   directory - no processing required
5485 dpurdie 223
                next if $entry->{type} eq 'dir';
5486 dpurdie 224
 
225
                #   link - no processing reqiuired
226
                next if $entry->{type} eq 'link';
227
 
228
                #   file - ensure there is no clash
5485 dpurdie 229
                if ($entry->{type} eq 'file')
230
                {
231
                    # Calc max host name length for pretty printing
232
                    my $hostnameLen = length $entry->{host};
233
                    $maxHostNameLength = $hostnameLen if ($hostnameLen > $maxHostNameLength);
234
 
235
                    my $hostEntry = {host => $entry->{host}, md5sum => $entry->{md5sum}}; 
236
                    push @{$fileData{$entry->{fullname}}{hosts}}, $hostEntry;
237
                    my $store = $fileData{$entry->{fullname}}; 
238
 
239
                    if (exists $store->{md5sum})
240
                    {
241
                        # Compare existing entry and add new info
242
                        unless ($store->{md5sum} eq $entry->{md5sum})
243
                        {
244
                            $store->{bad} = 1;
245
                        }
246
                    }
247
                    else
248
                    {
249
                        # Create new entry
250
                        $store->{md5sum} = $entry->{md5sum};
251
                    }
252
                next;
253
                }
5486 dpurdie 254
                #   Unknown - just a warning for now
255
                Warning( "Unknown type:" . $entry->{fullname} . ':' . $entry->{type} );
5485 dpurdie 256
            }
257
        }
258
    }
259
 
260
    #
261
    #   Cleanout the non-bad entries
262
    #   Report on merge errors
263
    #
264
    my $headerReported;
265
    foreach my $entry (keys %fileData)
266
    {
267
        #
268
        #   Some entries are allowed to differ
269
        #       descpkg
270
        #       version_*.h 
271
        #           files as these are generated and may contain different dates and line endings
272
        #
5486 dpurdie 273
        if ($entry eq 'descpkg')
5485 dpurdie 274
        {
275
            delete $fileData{$entry};
276
            next;
277
        }
278
 
279
        if ($entry =~ m~/version[^/]*\.h$~)
280
        {
281
            Verbose("Ignore merge error on: $entry");
282
            delete $fileData{$entry};
283
            next;
284
        }
285
 
286
        #
287
        #   Delete entry if its not marked as bad
5486 dpurdie 288
        unless (exists $fileData{$entry}{bad} )
289
        {
5485 dpurdie 290
            delete $fileData{$entry};
291
            next;
292
        }
293
 
294
        #
295
        #   Have a merge error
5486 dpurdie 296
        #       Detail what has happened
297
        #       Generate pretty output showning on which machines that are command.
5485 dpurdie 298
        #
299
        unless ($headerReported)
300
        {
301
            $headerReported = 1;
302
            reportMergeError('Package Merge Error. File provided by different builds are not identical');
303
            reportMergeError('This prevents the build from being reproducible.');
304
        }
305
        reportMergeError('File Name:  ' . $entry);
5486 dpurdie 306
 
307
#       foreach my $e ( @{$fileData{$entry}{hosts}} )
308
#       {
309
#           reportMergeError('    Provided by:  ' . sprintf('%-*s',$maxHostNameLength,$e->{host}) . '  Signature: ' . $e->{md5sum});
310
#       }
311
 
312
        my %md5List;
313
        foreach my $e ( @{$fileData{$entry}{hosts}} ) {
314
            UniquePush (\@{$md5List{$e->{md5sum}}}, $e->{host});
315
        }
316
 
5485 dpurdie 317
        foreach my $e ( @{$fileData{$entry}{hosts}} )
318
        {
5486 dpurdie 319
            my $hostList;
320
            my @sameHosts = @{$md5List{$e->{md5sum}}};
321
            ArrayDelete (\@sameHosts, $e->{host});
322
            if (@sameHosts) {
323
                $hostList = ' Same as: ' . join(', ', @sameHosts);
324
            } else {
325
                $hostList = ' Unique to: '. $e->{host};
326
            }
327
 
328
            reportMergeError('    Provided by:  ' . sprintf('%-*s',$maxHostNameLength,$e->{host}) . $hostList );
5485 dpurdie 329
        }
5486 dpurdie 330
 
331
 
5485 dpurdie 332
    }
333
    ErrorDoExit();
334
 
335
    #
5486 dpurdie 336
    #   Calculate target package location
337
    #   
338
    $pkgTargetDir = catdir($opt_outputPath, $opt_pname, $opt_pversion);
339
    Verbose("Package Target: $pkgTargetDir");
340
    RmDirTree($pkgTargetDir) if $opt_preDelete;
341
    Error ("Target package directory exists") if -d $pkgTargetDir;
342
    mkpath ($pkgTargetDir);
343
    Error ("Package target not created: $!", $pkgTargetDir) unless -d $pkgTargetDir;
344
    $deleteTargetDir = 1;
345
 
346
    #
5485 dpurdie 347
    #   Extract the archive contents and merge them into one directory
348
    #       If there are overlaps - don't replace them
349
    #
350
    foreach my $srcfile ( keys %pkgData )
351
    {
5486 dpurdie 352
        Message ("Extracting all files from " . StripDir($srcfile));
353
        my $rv = System ('tar', '-xzmf', $srcfile, IsVerbose(1) ? '-v' : undef, '-C', $pkgTargetDir );
5485 dpurdie 354
        Error("Tar extraction error: $srcfile") if ($rv);
355
    }
356
 
357
    #
358
    #   Fix file permissions
359
    #   We know we are running under unix so we will use a unix command
360
    #
361
    Message('Setting file permissions');
5486 dpurdie 362
    System('chmod', '-R', 'a+rx', $pkgTargetDir);
5485 dpurdie 363
 
364
    #
365
    #   Fix descpkg file
366
    #   Original create_dpkg uses the CopyDescpkg function. This is a bit wonky
367
    #   All it appears to do is:
368
    #       Force build machine name
369
    #       Force user name
370
    #       Force build time into the descpkg file
371
    #  If a package was built on multiple machines then the build machine names were lost
5486 dpurdie 372
    #  
373
    #   This implementation
374
    #       Use the descpkg file in the first package fragment
375
    #       There is enough other information in the build system to track where the package
376
    #       was built. This was not available when CopyDescpkg was implemented
5485 dpurdie 377
 
378
 
379
    #
380
    #   All Done
5486 dpurdie 381
    #       Flag  - don't cleanup generated dierctory
382
    #       
383
    Information("Package Target: $pkgTargetDir");
384
    $deleteTargetDir = 0;
5485 dpurdie 385
    exit 0;
386
 
387
#-------------------------------------------------------------------------------
388
# Function        : END 
389
#
390
# Description     : Cleanup process 
391
#
392
# Inputs          : 
393
#
394
# Returns         : 
395
#
396
END
397
{
398
    #
399
    #   Delete everything in the temp directory
400
    #   It was a directory created by this instance for the use of this instance
401
    #
5486 dpurdie 402
    if ($tmpDirInfo)
5485 dpurdie 403
    {
404
        Message("Cleanup processing");
405
        chdir($startDir);
5486 dpurdie 406
        RmDirTree($workDir);
407
        if (-d $workDir)
5485 dpurdie 408
        {
5486 dpurdie 409
            Warning("TMPDIR still exists: $workDir");
5485 dpurdie 410
        }
5486 dpurdie 411
    } 
412
    elsif ($workDir)
413
    {
414
        Message ("Retaining workdir: $workDir");
5485 dpurdie 415
    }
5486 dpurdie 416
 
417
    #
418
    #   Delete the package target dir
419
    #   We must have created it - as we error if it exists.
420
    #   
421
    #   Remove the packageName and packageVersion directories fi possible
422
    #   
423
    if ($deleteTargetDir)
424
    {
425
        Message("Remove partially created package");
426
        RmDirTree($pkgTargetDir);
427
 
428
        my $pkgDir = StripFileExt($pkgTargetDir);
429
        rmdir($pkgDir) && Message("Remove package dir: $pkgDir");
430
    }
5485 dpurdie 431
}
432
 
433
#-------------------------------------------------------------------------------
434
# Function        : reportMergeError 
435
#
436
# Description     : Report an error or a warning
437
#
438
# Inputs          : All arguments passed to ReportError or Warning
439
#
440
# Returns         : Nothing 
441
#
442
sub reportMergeError
443
{
5486 dpurdie 444
    $opt_MergeErrors ? Warning(@_) : ReportError(@_);
5485 dpurdie 445
}
446
 
447
#-------------------------------------------------------------------------------
448
#   Documentation
449
#
450
 
451
=pod
452
 
453
=for htmltoc    SYSUTIL::
454
 
455
=head1 NAME
456
 
457
create_dpkgFromTar - Create a dpkg_archive entry from a set of tar files
458
 
459
=head1 SYNOPSIS
460
 
461
 jats create_dpkgFromTar [options]
462
 
463
 Options:
464
    -help              - Brief help message
465
    -help -help        - Detailed help message
466
    -man               - Full documentation
467
    -verbose           - Display additional progress messages
468
    -pname=name        - Ensure package is named correctly
469
    -pversion=version  - Ensure package version is correct
470
    -srcdir=path       - Location of the package fragments
471
 
5486 dpurdie 472
  Debug and Testing:
473
    -[no]mergeErrors   - Allow merge errors
474
    -[no]predelete     - Predelete generated package
475
    -output=path       - Base of test package archive
476
 
5485 dpurdie 477
=head1 OPTIONS
478
 
479
=over 8
480
 
481
=item B<-help>
482
 
483
Print a brief help message and exits.
484
 
485
=item B<-help -help>
486
 
487
Print a detailed help message with an explanation for each option.
488
 
489
=item B<-man>
490
 
491
Prints the manual page and exits.
492
 
493
=item B<-srcdir=path>
494
 
495
This option specifies the path of the packages fragments. The fragments will be
496
located using the package name and package version.
497
 
498
=item B<-pname=name>
499
 
500
The name of the target package
501
 
502
=item B<-pversion=version>
503
 
504
The version of the target package.
505
 
5486 dpurdie 506
=item B<-[no]mergeErrors>
507
 
508
This option allows the merging process to continue if merge errors are located.
509
The default is -noMergeErrors
510
 
511
This option is  intended for testing use only.
512
 
513
=item B<-[no]predelete>
514
 
515
This option will delete the target package instance before the package is assembled.
516
The default is -noPreDelete
517
 
518
This option is  intended for testing use only.
519
 
520
=item B<-output=path>
521
 
522
This option allows the user to specify to root of a test package archive.
523
The dafualt is to use the value provided by GBE_DPKG - the main package archive.
524
 
525
This option is  intended for testing use only.
526
 
5485 dpurdie 527
=back
528
 
529
=head1 DESCRIPTION
530
 
531
This utility program is used by the build system to merge build artifacts from several
532
build machines into one package.
533
 
534
The build artifacts have been delivered to the package store as a collection
535
of zipped tar files (.tar.gz). There will be one tar file from each machine in the build set.
536
 
537
The process has been designed to overcome several problems:
538
 
539
=over 4
540
 
541
=item Speed
542
 
543
If some of the build machines are not co-located with the master package server, then 
544
the process of transferring a package with a large number of files can be very slow.
545
 
546
ie: > 1 second per file to transfer a file from AWS(Sydney) to PCC(Perth). 
547
If a package has several thousand files then this can take an hour.
548
 
549
If the packaged files are compressed into a single file, then the file creation overhead is eliminated.
550
 
551
=item Atomic File Creation
552
 
553
For package fragments to be transferred from multiple machines without error some form of 
554
multi-machine mutex is required. This has not been successfully implemented - after many attempts.
555
 
556
If the merge operation is done by the package server, then there is no need for a mutex.
557
 
558
=back
559
 
560
The process of transferring tarballs and then merging then in one location solves these two problems.
561
 
562
The reconstruction process is performed by a daemon on the package archive server to address the following issues:
563
 
564
=over 4
565
 
566
=item * Windows handling of symlinks
567
 
568
Symbolic links will be handled correctly on the package server as the file system is native.
569
 
570
=item * Network Speed
571
 
572
By running the merge on the package server the contents of the package are not dragged to and 
573
from the build server. If the build server is not co-located with the package archive then there
574
will be a major speed penalty.
575
 
576
=back
577
 
578
The basic process performed by this utility is:
579
 
580
=over 4
581
 
582
=item * 
583
 
584
Locate all parts of the package. There should be one from each build machine that is a part 
585
of the build set, unless the build was generic. For each package fragment:
586
 
587
=over 4
588
 
589
=item * 
590
 
591
Extract a 'built.files.<machname>' file - the file must exist.
592
 
593
=item *
594
 
595
Read all 'built.files.<machname>' files and in the process determine if there are any conflicts.
596
A conflict is deemed to exist if the files have different MD5 digests. This allows the same file
597
to be provided by different builds - as long as the content is the same. Line endings are handled
598
in a machine independent manner. 
599
 
600
=item *
601
 
602
Detect dead symbolic links.
603
 
604
=back
605
 
606
=item *
607
 
608
If there are no file conflicts or other detected errors, then all parts of the package will be 
609
extracted into a single directory.
610
 
611
=item *
612
 
613
File permisions will be adjusted. All directories will be made world readable and all files will be made world executable.
614
 
615
=back
616
 
617
=cut
618