File Coverage

blib/lib/LCFG/Build/Skeleton.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package LCFG::Build::Skeleton; # -*-perl-*-
2 1     1   566 use strict;
  1         2  
  1         32  
3 1     1   4 use warnings;
  1         1  
  1         43  
4              
5             # $Id: Skeleton.pm.in 27451 2015-02-24 11:16:13Z squinney@INF.ED.AC.UK $
6             # $Source: /var/cvs/dice/LCFG-Build-Skeleton/lib/LCFG/Build/Skeleton.pm.in,v $
7             # $Revision: 27451 $
8             # $HeadURL: https://svn.lcfg.org/svn/source/tags/LCFG-Build-Skeleton/LCFG_Build_Skeleton_0_5_0/lib/LCFG/Build/Skeleton.pm.in $
9             # $Date: 2015-02-24 11:16:13 +0000 (Tue, 24 Feb 2015) $
10              
11             our $VERSION = '0.5.0';
12              
13 1     1   523 use Email::Address ();
  1         20722  
  1         60  
14 1     1   588 use Email::Valid ();
  1         77450  
  1         25  
15 1     1   7 use File::Basename ();
  1         1  
  1         9  
16 1     1   4 use File::Path ();
  1         1  
  1         8  
17 1     1   3 use File::Spec ();
  1         1  
  1         9  
18 1     1   635 use File::Temp ();
  1         7556  
  1         22  
19 1     1   197 use LCFG::Build::PkgSpec ();
  0            
  0            
20             use List::MoreUtils qw(none);
21             use Sys::Hostname ();
22             use Template ();
23             use UNIVERSAL::require;
24             use YAML::Syck ();
25              
26             my $TMPLDIR
27             = exists $ENV{LCFG_BUILD_TMPLDIR}
28             ? $ENV{LCFG_BUILD_TMPLDIR}
29             : '/usr/share/lcfgbuild/templates';
30              
31             use Moose;
32             use Moose::Util::TypeConstraints;
33              
34             with 'MooseX::Getopt';
35              
36             # This is a little bit of hackery so that the class implements the
37             # role but avoids loading the standard role of this name
38              
39             my $extra_role = Moose::Meta::Role->initialize('MooseX::ConfigFromFile');
40             __PACKAGE__->meta->add_role($extra_role);
41              
42             subtype 'LCFG::Types::ComponentName'
43             => as 'Str'
44             => where { m/^[A-Za-z][A-Za-z0-9_]+$/ };
45              
46             subtype 'LCFG::Types::EmailAddress'
47             => as 'Str'
48             => where { Email::Valid->address( -address => $_ ) }
49             => message { "Address ($_) for report must be a valid email address" };
50              
51             subtype 'LCFG::Types::Response'
52             => as 'Str'
53             => where { $_ eq 'yes' || $_ eq 'no' };
54              
55             coerce 'LCFG::Types::Response' => from 'Str' =>
56             via { $_ && ( $_ eq '1' || m/^ye(s|p|ah)!?$/i ) ? 'yes' : 'no' };
57              
58             MooseX::Getopt::OptionTypeMap->add_option_type_to_map(
59             'LCFG::Types::Response' => q{!}, );
60              
61             has 'configfile' => (
62             is => 'ro',
63             isa => 'Str',
64             default => sub {
65             my $class = shift;
66             $class->_get_default_configfile if $class->can('_get_default_configfile');
67             },
68             predicate => 'has_configfile',
69             documentation => 'Where defaults should be stored',
70             );
71              
72             sub _get_default_configfile {
73             my ($class) = @_;
74              
75             my $default = File::Spec->catfile( $ENV{HOME}, '.lcfg',
76             'skeleton', 'defaults.yml' );
77              
78             return $default;
79             }
80              
81             has 'tmpldir' => (
82             is => 'ro',
83             isa => 'Str',
84             default => sub { File::Spec->catdir( $ENV{HOME}, '.lcfg',
85             'skeleton', 'templates' ) },
86             documentation => 'Local templates directory',
87             );
88              
89             has 'name' => (
90             is => 'rw',
91             isa => 'LCFG::Types::ComponentName',
92             documentation => 'Name of the project',
93             );
94              
95             has 'abstract' => (
96             is => 'rw',
97             isa => 'Str',
98             lazy => 1,
99             documentation => 'Short description of the project',
100             default => sub {
101             $_[0]->lcfg_component eq 'yes'
102             ? 'The LCFG ' . $_[0]->name . ' component'
103             : q{};
104             },
105             );
106              
107             has 'author_name' => (
108             is => 'rw',
109             isa => 'Str',
110             default => sub { ( getpwuid $< )[6] },
111             documentation => 'Name of the author',
112             );
113              
114             has 'author_email' => (
115             is => 'rw',
116             isa => 'LCFG::Types::EmailAddress',
117             builder => '_default_email',
118             documentation => 'Email address for the author',
119             );
120              
121             sub _default_email {
122              
123             my $email;
124             if ( $ENV{EMAIL} ) {
125             $email = $ENV{EMAIL};
126             } else {
127             my $username = ( getpwuid $< )[0];
128              
129             my ( $hostname, @domain ) = split /\./, Sys::Hostname::hostname;
130              
131             my $domain = join q{.}, @domain;
132             $email = join q{@}, $username, $domain;
133             }
134              
135             return $email;
136             }
137              
138             has 'lang' => (
139             is => 'rw',
140             isa => enum( [qw/perl shell/] ),
141             documentation => 'Language for component (perl/shell)',
142             default => 'perl',
143             );
144              
145             has 'vcs' => (
146             is => 'rw',
147             isa => enum( [qw/SVN CVS None/] ),
148             documentation => 'Version Control System (SVN/CVS/None)',
149             default => 'SVN',
150             );
151              
152             has 'platforms' => (
153             is => 'rw',
154             isa => 'Maybe[Str]',
155             documentation => 'Supported platforms',
156             );
157              
158             has 'license' => (
159             is => 'rw',
160             isa => 'Str',
161             documentation => 'Distribution license',
162             default => 'GPLv2',
163             );
164              
165             has 'restart' => (
166             is => 'rw',
167             isa => 'LCFG::Types::Response',
168             coerce => 1,
169             documentation => 'Restart component on RPM update (yes/no)',
170             default => 'yes',
171             );
172              
173             has 'gencmake' => (
174             is => 'rw',
175             isa => 'LCFG::Types::Response',
176             coerce => 1,
177             documentation => 'Use the CMake build system (yes/no)',
178             default => 'yes',
179             );
180              
181             has 'genchangelog' => (
182             is => 'rw',
183             isa => 'LCFG::Types::Response',
184             coerce => 1,
185             documentation => 'Generate the ChangeLog from the Revision-Control log? (yes/no)',
186             default => 'no',
187             );
188              
189             has 'checkcommitted' => (
190             is => 'rw',
191             isa => 'LCFG::Types::Response',
192             coerce => 1,
193             documentation => 'Check all changes are committed before a release? (yes/no)',
194             default => 'yes',
195             );
196              
197             has 'lcfg_component' => (
198             is => 'rw',
199             isa => 'LCFG::Types::Response',
200             coerce => 1,
201             documentation => 'Is this an LCFG component? (yes/no)',
202             default => 'yes',
203             );
204              
205             has 'interactive' => (
206             is => 'ro',
207             isa => 'Bool',
208             documentation => 'Interactively query the user',
209             default => 1,
210             );
211              
212             has 'force' => (
213             is => 'ro',
214             isa => 'Bool',
215             documentation => 'Forceably remove an old project directory',
216             default => 0,
217             );
218              
219             sub get_config_from_file {
220             my ( $self, $file ) = @_;
221              
222             my $cfg = {};
223             if ( -f $file ) {
224             $cfg = YAML::Syck::LoadFile($file);
225             }
226              
227             return $cfg;
228             }
229              
230             sub new_with_config {
231             my ( $class, @args ) = @_;
232              
233             my %opts;
234             if ( scalar @args == 1 && ref $args[0] eq 'HASH' ) {
235             %opts = %{$args[0]};
236             } elsif ( @args % 2 == 0 ) {
237             %opts = @args;
238             }
239              
240             my $configfile = delete $opts{configfile};
241              
242             if ( !defined $configfile && $class->can('_get_default_configfile') ) {
243             $configfile = $class->_get_default_configfile();
244             }
245              
246             if ( defined $configfile ) {
247             %opts = ( %{$class->get_config_from_file($configfile)}, %opts );
248             }
249              
250             $class->new(%opts);
251             }
252              
253             my @questions = qw(
254             name
255             lcfg_component
256             abstract
257             author_name
258             author_email
259             lang
260             vcs
261             platforms
262             license
263             restart
264             gencmake
265             checkcommitted
266             genchangelog
267             );
268              
269             sub store_answers {
270             my ($self) = @_;
271              
272             my @ignore = qw(name lcfg_component abstract);
273             my @extra = qw(tmpldir);
274              
275             my %store;
276             for my $question ( @questions, @extra ) {
277             if ( none { $question eq $_ } @ignore ) {
278             $store{$question} = $self->$question;
279             }
280             }
281              
282             my $cfg = $self->configfile;
283              
284             my ( $name, $path ) = File::Basename::fileparse($cfg);
285             if ( !-d $path ) {
286             eval { File::Path::mkpath($path) };
287             if ($@) {
288             die "Failed to create directory, $cfg: $!\n";
289             }
290             }
291              
292             YAML::Syck::DumpFile( $cfg, \%store );
293              
294             return;
295             }
296              
297             sub query_user {
298             my ($self) = @_;
299              
300             if ( $self->interactive ) {
301             for my $question (@questions) {
302              
303             my $doc = $self->meta->get_attribute($question)->documentation;
304              
305             my $default = $self->$question;
306              
307             my $defstring = q{};
308             if ( defined $default ) {
309             $defstring = ' [' . $default . ']';
310             }
311              
312             while (1) {
313             print $doc . $defstring . q{: };
314             chomp( my $answer = <STDIN> );
315              
316             # trim any whitespace from the response
317             $answer =~ s/^\s+//;
318             $answer =~ s/\s+$//;
319              
320             if ( length $answer > 0 ) {
321             eval { $self->$question($answer) };
322             }
323              
324             if ($@) {
325             print "Error: Bad choice, please try again.\n";
326             } else {
327             last;
328             }
329              
330             }
331             }
332             }
333              
334             # always store the answers as they may have come from the command line
335             $self->store_answers;
336              
337             return;
338             }
339              
340             sub create_package {
341             my ($self) = @_;
342              
343             # Make an attempt to sanitise whatever the user gave us as an
344             # email address.
345              
346             my ($addr) = Email::Address->parse($self->author_email);
347              
348             my $new_addr = Email::Address->new( $self->author_name,
349             $addr->address );
350             my $author = $new_addr->format;
351              
352             my @platforms;
353             if ( $self->platforms ) {
354             @platforms = split /\s*,\s*/, $self->platforms;
355             }
356              
357             # Sometimes people mistakenly put a 'lcfg-' prefix on the name.
358             my $name = $self->name;
359             if ( $self->lcfg_component eq 'yes' ) {
360             $name =~ s/^lcfg-//;
361             }
362              
363             my $pkgspec = LCFG::Build::PkgSpec->new(
364             name => $name,
365             version => '0.0.1',
366             release => '1',
367             author => [$author],
368             abstract => $self->abstract,
369             license => $self->license,
370             translate => ['*.cin'],
371             platforms => [@platforms],
372             );
373              
374             if ( $self->lcfg_component eq 'yes' ) {
375             $pkgspec->schema(1);
376             $pkgspec->base('lcfg');
377             $pkgspec->group('LCFG');
378             }
379              
380             my $localdir = $pkgspec->fullname;
381             if ( -e $localdir ) {
382             die "There is already a local directory or file named \"$localdir\".\n Please move it aside and try again\n";
383             }
384              
385             if ( $self->gencmake eq 'yes' ) {
386             $pkgspec->set_buildinfo( gencmake => 1 );
387             }
388             else { # not essential but good to be explicit here
389             $pkgspec->set_buildinfo( gencmake => 0 );
390             }
391              
392             # version control information
393              
394             $pkgspec->set_vcsinfo( logname => 'ChangeLog' );
395              
396             if ( $self->checkcommitted eq 'yes' ) {
397             $pkgspec->set_vcsinfo( checkcommitted => 1 );
398             }
399             if ( $self->genchangelog eq 'yes' ) {
400             $pkgspec->set_vcsinfo( genchangelog => 1 );
401             }
402              
403             my $tempdir = File::Temp::tempdir( CLEANUP => 1 );
404              
405             my $new_metafile = File::Spec->catfile( $tempdir, 'lcfg.yml' );
406             $pkgspec->metafile($new_metafile);
407             $pkgspec->save_metafile();
408              
409             print "Stored LCFG build metadata\n";
410              
411             my @include;
412             if ( -d $self->tmpldir ) {
413             push @include, $self->tmpldir;
414             }
415             push @include, $TMPLDIR;
416              
417             my $tt = Template->new(
418             { INCLUDE_PATH => \@include,
419             FILTERS => { to_bool => sub { $_ eq 'yes' ? 1 : 0 }, },
420             PRE_CHOMP => 1,
421             }
422             ) or die $Template::ERROR;
423              
424             # Key is the template filename
425             # Value is the target file path stored as a ref to a list of parts
426              
427             my %files = (
428             'specfile.tt' => ['specfile'],
429             'ChangeLog.tt' => ['ChangeLog'],
430             'README.BUILD.tt' => ['README.BUILD'],
431             'README.tt' => ['README'],
432             );
433              
434             # Create some essential directories if the project is perl based
435              
436             if ( $self->lang eq 'perl' ) {
437             my $libdir = File::Spec->catdir( $tempdir, 'lib' );
438             mkdir $libdir
439             or die "Could not Perl library directory, $libdir: $!\n";
440              
441             if ( $self->lcfg_component eq 'yes' ) {
442             my $compdir =
443             File::Spec->catdir( $tempdir, 'lib', 'LCFG', 'Component' );
444             eval { File::Path::make_path($compdir) };
445             if ($@) {
446             die "Failed to create LCFG component perl directory: $@\n";
447             }
448             } else {
449             $files{'README.perl.tt'} = [ 'lib', 'README' ];
450             }
451              
452             my $testdir = File::Spec->catdir( $tempdir, 't' );
453             mkdir $testdir
454             or die "Could not create tests directory, $testdir: $!\n";
455             }
456              
457             my @exefiles;
458              
459             if ( $self->lcfg_component eq 'yes' ) {
460             my $comp = $pkgspec->name;
461              
462             if ( $self->lang eq 'perl' ) {
463             $files{'COMPONENT.pl.tt'} = ["$comp.cin"];
464             $files{'COMPONENT.pm.tt'} = ['lib','LCFG','Component',"\u$comp.pm.cin"];
465             $files{'perlcomp_cmake.tt'} = ['CMakeLists.txt'];
466             }
467             else {
468             $files{'COMPONENT.sh.tt'} = ["$comp.cin"];
469             }
470             push @exefiles, "$comp.cin";
471              
472             $files{'COMPONENT.def.tt'} = ["$comp.def.cin"];
473             $files{'COMPONENT.pod.tt'} = ["$comp.pod.cin"];
474              
475             my $nagios_dir = File::Spec->catdir( $tempdir, 'nagios' );
476             mkdir $nagios_dir
477             or die "Could not create nagios directory, $nagios_dir: $!\n";
478              
479             $files{'README.nagios.tt'} = [ 'nagios', 'README' ];
480              
481             my $templates_dir = File::Spec->catdir( $tempdir, 'templates' );
482             mkdir $templates_dir
483             or die "Could not create templates directory, $templates_dir: $!\n";
484              
485             $files{'README.templates.tt'} = [ 'templates', 'README' ];
486             }
487              
488             for my $template ( keys %files ) {
489              
490             my @file = @{$files{$template}};
491             my $output = File::Spec->catfile( $tempdir, @file );
492              
493             print "Generating $output\n";
494             $tt->process(
495             $template,
496             { skel => $self,
497             pkgspec => $pkgspec,
498             },
499             $output
500             ) or warn $tt->error();
501             }
502              
503             for my $exe (@exefiles) {
504             my $path = File::Spec->catfile( $tempdir, $exe );
505             chmod 0755, $path;
506             }
507              
508             eval {
509             my $vcsmodule = 'LCFG::Build::VCS::' . $self->vcs;
510              
511             $vcsmodule->require or die $@;
512              
513             my $vcs = $vcsmodule->new(
514             quiet => 0,
515             dryrun => 0,
516             module => $pkgspec->fullname,
517             );
518              
519             $vcs->import_project( $tempdir, $pkgspec->version,
520             'Created with lcfg-skeleton' );
521              
522             $vcs->checkout_project();
523              
524             };
525              
526             if ($@) {
527             die "Failed to import project to your chosen version-control system:\n $@\n";
528             }
529              
530             print "Successfully imported your project into your version-control system.\n";
531              
532             return $pkgspec;
533             }
534              
535             1;
536             __END__
537              
538             =head1 NAME
539              
540             LCFG::Build::Skeleton - LCFG software package generator
541              
542             =head1 VERSION
543              
544             This documentation refers to LCFG::Build::Skeleton version 0.5.0
545              
546             =head1 SYNOPSIS
547              
548             my $skel = LCFG::Build::Skeleton->new_with_options();
549              
550             $skel->query_user();
551              
552             $skel->create_package();
553              
554             =head1 DESCRIPTION
555              
556             This module handles the creation of the skeleton of an LCFG software
557             project. Typically, it prompts the user to answer a set of standard
558             questions and then generates the necessary files from a set of
559             templates. These generated files include the necessary metadata, build
560             files and, for LCFG components, example code. It can also add the new
561             project into the revision-control system of choice.
562              
563             =head1 ATTRIBUTES
564              
565             If using the new_with_options() method then any of the attributes can
566             be set from the commandline (or, more precisely, via the @ARGV
567             list). An attribute named C<foo> is accessible as the commandline
568             option C<--foo>. If it is a boolean value then the module will also
569             support the C<--no-foo> form to turn off a feature
570             (e.g. --no-gencmake).
571              
572             =over 4
573              
574             =item name
575              
576             The name of the project. Note that in the case of an LCFG component
577             this should be C<foo> B<NOT> C<lcfg-foo>.
578              
579             =item abstract
580              
581             A short description of the project. If this is an LCFG component the
582             default value suggested to the user is "The LCFG $name component".
583              
584             =item author_name
585              
586             The name of the author (i.e you!). The default is the string stored in
587             the gecos field of the passwd entry.
588              
589             =item author_email
590              
591             The email address for the author. The default is built from the
592             current username and domain name.
593              
594             =item lcfg_component
595              
596             This controls whether the generated project is an LCFG component. This
597             is a yes/no answer and it defaults to "yes" (it is handled in the same
598             way as a boolean value on the command line).
599              
600             =item lang
601              
602             The language which will be used, this is either "perl" or
603             "shell". This only really has an affect if you are creating an LCFG
604             component.
605              
606             =item vcs
607              
608             Which revision-control system you intend to use for the
609             project. Currently only "CVS" and "None" are supported. You will need
610             the relevant LCFG::Build::VCS helper module installed for this to
611             work.
612              
613             =item platforms
614              
615             The comma-separate list of platforms which are supported by this code
616             (e.g. ScientificLinux5).
617              
618             =item license
619              
620             The license under which the source code can be distributed. This
621             defaults to "GPLv2".
622              
623             =item restart
624              
625             This controls whether, if this is an LCFG component, should it be
626             restarted after package upgrade if the component is already
627             running. This is a yes/no answer and it defaults to "yes" (it is
628             handled in the same way as a boolean value on the command line).
629              
630             =item gencmake
631              
632             This controls whether the LCFG CMake infrastructure will be used to
633             build the project. This is a yes/no answer and it defaults to "yes"
634             (it is handled in the same way as a boolean value on the command
635             line).
636              
637             =item genchangelog
638              
639             This controls whether or not to generate the project changelog from
640             the revision-control commit logs. This is a yes/no answer and it
641             defaults to "no" (it is handled in the same way as a boolean value on
642             the command line).
643              
644             =item checkcommitted
645              
646             This controls whether the revision-control tools should check that all
647             files are committed before making a new release. This is a yes/no
648             answer and it defaults to "yes" (it is handled in the same way as a
649             boolean value on the command line).
650              
651             =item interactive
652              
653             This controls whether the L<query_user()> method will actually interact with the user or just store the values taken from the defaults file and any commandline options. This is a boolean value which defaults to false (zero).
654              
655             =item force
656              
657             This controls whether an existing project directory will be removed if the name matches that required for the new skeleton project. This is a boolean value which defaults to false (zero).
658              
659             =item configfile
660              
661             This is the configuration file which is used to store the defaults
662             between calls to the lcfg-skeleton tool. Normally you should not need
663             to modify this and it defaults to C<~/.lcfg/skeleton/defaults.yml>.
664              
665             =item tmpldir
666              
667             This is the directory into which local versions of templates should be
668             placed. Normally you should not need to modify this and it defaults to
669             C<~/.lcfg/skeleton/templates/>. For reference, the standard templates
670             are normally stored in C</usr/share/lcfgbuild/templates>.
671              
672             =back
673              
674              
675             =head1 SUBROUTINES/METHODS
676              
677             =over 4
678              
679             =item new(%hash_of_options)
680              
681             Creates a new object of the LCFG::Build::Skeleton class. Values for
682             any attribute can be specified in the hash of options.
683              
684             =item new_with_options(%hash_of_options)
685              
686             Creates a new object of the LCFG::Build::Skeleton class and if any
687             attribute values were specified on the command line those will be set
688             in the returned instance. Values for any attribute can be specified in
689             the hash of options.
690              
691             =item query_user()
692              
693             This prompts the user to answer a set of standard questions (except
694             when the C<interactive> option is set to false) and stores the answer
695             in the object attributes. If an invalid value is given the user will
696             be prompted again. For convenience, the answers are also stored in a
697             file and used as the defaults in the next run of the command. The
698             default value is shown in the prompt between square-brackets and just
699             pressing return is enough to accept the default.
700              
701             =item create_package()
702              
703             This uses the skeleton object attribute values to generate the
704             skeleton tree of files for the new project. If a project directory of
705             the desired name already exists you will need to move it aside, choose
706             a different project name or set the C<force> attribute to true.
707              
708             =item store_answers()
709              
710             This is primarily intended for internal usage. It will store the
711             values of the answers given be the user (except the project name and
712             abstract) so that they can be used as defaults in future calls to the
713             lcfg-skeleton command. The default values are stored in the file name
714             specified in the C<configfile> attribute, that defaults to
715             C<~/lcfg/skeleton/defaults.yml>
716              
717             =item get_config_from_file($filename)
718              
719             This is primarily intended for internal usage. This retrieves the
720             configuration data from the specified file, which must be in YAML
721             format, and returns it as a reference to a hash.
722              
723             =back
724              
725             =head1 CONFIGURATION AND ENVIRONMENT
726              
727             The default values for the answers are stored in the file referred to
728             in the C<configfile> attribute. This is normally
729             C<~/.lcfg/skeleton/defaults.yml> but that can be overridden by the
730             user. If the file does not exist it will be created when this tool is
731             first run.
732              
733             It is possible to override any of the standard templates used to
734             generate the skeleton project by placing your own version into the
735             directory referred to in the C<tmpldir> attribute. This is normally
736             C<~/.lcfg/skeleton/templates/> but that can be overridden by the
737             user. For reference, the standard templates are normally stored in
738             C</usr/share/lcfgbuild/templates>.
739              
740             =head1 DEPENDENCIES
741              
742             This module is L<Moose> powered and uses L<MooseX::Getopt> to provide
743             a new_with_options() method for creating new instances from the
744             options specified in @ARGV (typically via the commandline). The
745             L<YAML::Syck> module is used to parse the file which holds the default
746             values for the answers. You will also need the L<List::MoreUtils> and
747             L<UNIVERSAL::require> modules.
748              
749             The Perl Template Toolkit is required to generate the files for the
750             skeleton project.
751              
752             The following LCFG Build Tools modules are also required:
753             L<LCFG::Build::PkgSpec>(3), L<LCFG::Build::VCS>(3) and VCS helper modules.
754              
755             =head1 SEE ALSO
756              
757             L<LCFG::Build::Tools>, lcfg-skeleton(1), lcfg-reltool(1)
758              
759             =head1 PLATFORMS
760              
761             This is the list of platforms on which we have tested this
762             software. We expect this software to work on any Unix-like platform
763             which is supported by Perl.
764              
765             ScientificLinux6, EnterpriseLinux7, MacOSX
766              
767             =head1 BUGS AND LIMITATIONS
768              
769             There are no known bugs in this application. Please report any
770             problems to bugs@lcfg.org, feedback and patches are also always very
771             welcome.
772              
773             =head1 AUTHOR
774              
775             Stephen Quinney <squinney@inf.ed.ac.uk>
776              
777             =head1 LICENSE AND COPYRIGHT
778              
779             Copyright (C) 2008 University of Edinburgh
780              
781             This library is free software; you can redistribute it and/or modify
782             it under the terms of the GPL, version 2 or later.
783              
784             =cut