skip to main content

LCFG Build Tools : Adding New Commands

You may find yourself in the situation where it has become necessary to extend the LCFG release tool, lcfg-reltool, to add new commands. For example, this might be to support building binary packages for different platforms or maybe a test framework. Whatever the need, it is very easy to extend by adding a fairly simple Perl module.

Creating the Command Module

The first stage is to choose the name of your command as this affects the name of the Perl module you will have to create. Following the standard didactic example of "Hello World" the example command here will be called "hello". The module must be created in the LCFG::Build::Tool namespace and must extend the LCFG::Build::Tool Perl module. The module which will be created should then be called LCFG::Build::Tool::Hello. It does not matter about any capitalisation in the final part of the module name, the command will be the lower-cased version of that string.

The LCFG build tools use a Perl object-oriented programming framework called Moose. It is not necessary to understand how the whole system works to just add a new command, just follow this example. You should also consider looking at the Tool modules shipped as part of the LCFG-Build-Tools package, those are more complex and you might find them quite instructive.

Here is a basic module:

package LCFG::Build::Tool::Hello;
use strict;
use warnings;

use Moose;

extends 'LCFG::Build::Tool';

sub run {
    my ( $self, $opt, $args ) = @_;

    print "hello world\n";

    return;
}

no Moose;
1;

The run() method is essential, it is called when the command is actually called via lcfg-reltool. Add this module into somewhere in your Perl module include path. After that you can run the release tool without any arguments and in the command list you will see a new entry which looks something like:

         hello: (unknown)

Running the command does what we wanted:

$ lcfg-reltool hello
hello world

You may have noticed the "(unknown)" string in the help output above. This is because we have not added any documentation for the command module. This is achieved by simply adding an abstract subroutine which returns the documentation string:

sub abstract {
    return 'Says hello to the world';
}

The help output now contains the following line for the "hello" command:

         hello: Says hello to the world

Accessing Package Information

Inside the run method you can access all the information stored in the LCFG build metadata file. To do this you will either have to specify the --dir command line option or run the release tool within the project directory.

Access to this information is gained through two attributes. The first is named "spec" which holds a reference to the relevant LCFG::Build::PkgSpec object. The second is named "vcs" which holds a reference to the associated LCFG::Build::CVS module. You should read the specific perl documentation for the full capabilities of each of those modules. Here's a simple example which exports the tagged release for a project:

sub run {
    my ( $self, $opt, $args ) = @_;

    my ( $spec, $vcs ) = ( $self->spec, $self->vcs );

    my $version = $spec->version;

    my $srcdir = $vcs->export( $version, $resultsdir );

    return;
}

You do not normally need to consider the second and third parameters to the run method. Everything is also encapsulated within the object-reference which is passed in as the first parameter.

Option Handling

If you look at the help page for the "hello" command you might be surprised to see there are already some options defined. These have been inherited from the LCFG::Build::Tool parent class.

$ lcfg-reltool help hello
lcfg-reltool 

lcfg-reltool hello [long options...]
        --quiet           Less output to the screen
        --resultsdir      Base directory for storing results
        --dryrun          Simulate operation
        --dir             Project working directory

These options become object attributes so are very easy to access from the run() method. Here is a simple extension of the previous example to demonstrate the point:

sub run {
    my ( $self, $opt, $args ) = @_;

    if ( $self->quiet ) {
        print "hello world\n";
    }
    else {
        print "HELLO WORLD!\n";
    }

    return;
}

Running the command now gives:

$ lcfg-reltool hello --quiet
hello world
$ lcfg-reltool hello
HELLO WORLD!

This now leads into adding extra options which are specific to this command. Again this is fairly simple and uses special syntax which is part of the Moose framework. Consider that we want to add an option to take the name of a user.

has 'name' => (
    is            => 'rw',
    isa           => 'Str',
    default       => sub { (getpwuid $< )[6] },
    documentation => 'Your name',
);

You do not particularly need to worry about the "is" clause, it can be either "ro" or "rw", it only affects whether you can modify the attribute within your code.

The "isa" clause is important, it states what type of option you want for your module. Typically you will either want Str for passing in a string or Bool for a boolean option. The interesting thing with the boolean options is that you can then use them from the command line like --foo and --no-foo.

You do not need to provide a default value but it is often a good idea. It can be either a single scalar value or, as in this case, a reference to an anonymous subroutine which does some work to discover the default. In this case it is pulling the name from the GECOS field.

The final part, which you should always supply, is the "documentation" clause. This short string appears in the help page for the command alongside the option and briefly explains its purpose.

There are quite a few more complex ways to control the options. You will need to read the manual pages for the Moose, Moose::Getopt and Moose::App::Cmd Perl modules for complete coverage.

Hiding Options

Occasionally you might want to hide an option which is inherited from the parent class. You may wish to not provide support for that option at all or you might wish to hardwire the setting so it is always the same. This is quite easy to achieve with some like this:

has '+resultsdir' => (
    traits  => [ 'NoGetopt' ],
);

Notice the + symbol in front of the name of the attribute you wish to hide. This is the way to use Moose to override specific bits of an attribute. In this case a "NoGetopt" trait has been set and the option will be ignored. Note though that the attribute still exists for your objects, it just cannot be set from the command line and is not listed in the help page. Running help gives:

$ lcfg-reltool help hello
lcfg-reltool 

lcfg-reltool hello [long options...]
        --quiet      Less output to the screen
        --name       Your name
        --dryrun     Simulate operation
        --dir        Project working directory

Overriding a Default Value

You might need to override a default value for an attribute which is inherited from the parent class. This is done in a similar way to the previous example:

has '+quiet' => (
    default => 1,
);

With that change the module will default to being quiet rather than noisy. To get the noisy output you would do:

$ lcfg-reltool hello --no-quiet --name 'Fred Bloggs'
HELLO Fred Bloggs!

The Completed Example

The code for the completed example is available. To try it out you will need a local perl directory which is in the Perl include path. If you do not have this configured then the easiest approach is this:

mkdir -p ~/perllibs/LCFG/Build/Tool
export PERL5LIB=~/perllibs
cp Hello.pm.txt ~/perllibs/LCFG/Build/Tool/Hello.pm

Inheriting from Other Commands

Occassionally you will want to inherit from other command modules to reuse code and attribute specifications. One particular case is when creating commands for building packages (e.g. "devrpm" or "rpm"). In this situation you should inherit from the "devpack" or "pack" modules. For example:

package LCFG::Build::Tool::DevRPM;
use Moose;

extends 'LCFG::Build::Tool::DevPack';

override 'abstract' => sub {
    return q{Build binary RPMS from the development source tree};
};

override 'run' => sub {
    my ($self) = @_;

    super;

    # do everything else necessary to build an RPM...
};

no Moose;
1;

There is quite a lot going on in this example. Firstly, to inherit from the "devpack" command it is necessary to use the Moose extends function. Secondly, it is necessary to override the abstract and run methods, again this is done with a Moose function. One possible gotcha here is that the closing brace of the override subroutine block MUST be followed with a semi-colon. The clever bit is the usage of the Moose super function to invoke the run method in the "devpack" command module. The module can rely on this to do everything necessary to package the development source tree. It is then only necessary to do the extra RPM building and packaging work. The same concepts would be use for the "rpm" command or a "macosx" command.

Inheriting from another module like this provides access to all the attributes declared in that module and consequently also the associated command-line options. Adding further attributes works as expected, see the full code for the LCFG::Build::Tool::DevRPM and LCFG::Build::Tool::RPM Perl modules for a complete example.