#!/usr/bin/perl -w ##------------------------------------------------------------------------## ## File: ## $Id: release,v 1.24 2005/07/16 21:58:48 ehood Exp $ ## Author: ## Earl Hood earl@earlhood.com ## Description: ## Program to perform project releases from CVS. ## POD at __END__. ##------------------------------------------------------------------------## ## Copyright (C) 2001-2002 Earl Hood, earl@earlhood.com ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA ## 02111-1307, USA ##------------------------------------------------------------------------## package Devtools::release; use FindBin; use lib "$FindBin::Bin/../lib"; # Add relative lib to search path use Cwd; use Getopt::Long; use CommandUtil; # Possible locations of configuration file my @CONF_FILES = ( $ENV{'HOME'}.'/.release.conf', '/usr/local/etc/devtools/release.conf', '/etc/devtools/release.conf', ); # Configuration fallbacks use constant CVS_ROOT => '/home/cvs/root'; use constant REL_DIR => '/home/projects/release'; use constant RSH_PRG => 'rsh'; use constant MAIL_PRG => '/usr/lib/sendmail -t'; use constant MAKE_PRG => 'gmake'; use constant MK_TARGET_RELEASE => 'release'; my $ECHO_ONLY = 0; my $TEST_ONLY = 0; my %ENV_ORG = (%ENV); my $CONFIG = { }; ############################################################################ MAIN: { ## Command-line parsing my $cwd = getcwd; my %opt = (); my $status = GetOptions(\%opt, 'arch', # Create architectural-specific release 'conf=s', # Pathname to configuration file to use 'create', # Tag project in CVS before releasing 'cvsroot=s', # CVS root (def: $CVSROOT envariable) 'force-tag', # Force tagging, regardless 'keeptest', # Do not delete test files 'test-only', # Just do a test build and exit 'mailprg=s', # Mail program (def: /usr/lib/sendmail -t) 'mailto=s@', # Address(es) to mail when release is done 'makefile=s', # Main project makefile 'makeprg=s', # Make program (def: gmake) 'n', # Just echo what would be done. 'news=s', # News file to include mail sent out 'notest', # Skip the test build 'notlatest', # Not latest release, so do not update latest link 'noversion-check', # Bypass version check 'project=s', # Project name 'umask=i', # Umask for creating files 'rhost=s@', # Remote hosts to co-build on 'rshprg=s', # Remote shell program (def: rsh) 'snapmode', # Snapshot mode 'url=s', # URL to list in mail sent 'help|?', # Brief help message 'man' ); if (!$status) { die qq/Use -help for usage information\n/; } usage(1, 0) if ($opt{'help'}); usage(2, 0) if ($opt{'man'}); $ECHO_ONLY = $opt{'n'}; $TEST_ONLY = $opt{'test-only'}; $keeptest = $opt{'keeptest'}; ## Read site configuration file if ($opt{'conf'}) { if (! -e $opt{'conf'}) { die qq/Error: "/, $opt{'conf'}, qq/" does not exist.\n/; } $CONFIG = load_config($opt{'conf'}); } else { foreach my $file (@CONF_FILES) { $CONFIG = load_config($file) if -e $file; } } ## Initialize variables my $cvsroot = $opt{'cvsroot'} || $ENV_ORG{'CVSROOT'} || $CONFIG->{'CVS_ROOT'}; die qq/Error: CVS root not defined!\n/ unless $cvsroot; my $umask = $CONFIG->{'UMASK'} || oct($opt{'umask'}) || 002; my $releases_dir = $CONFIG->{'REL_DIR'} || REL_DIR; my $mk_target_release = $CONFIG->{'MK_TARGET_RELEASE'} || MK_TARGET_RELEASE; my $project = $opt{'project'} || die qq/Error: -project not specified!\n/; my $reldir = $releases_dir . "/$project"; die qq/Error: "$reldir" does not exist!\n/ unless -e $reldir; die qq/Error: "$reldir" not a directory!\n/ unless -d $reldir; die qq/Error: "$reldir" not writable!\n/ unless -w $reldir; my $makeprg = $opt{'makeprg'} || $ENV_ORG{'MAKE'} || $CONFIG->{'MAKE_PRG'} || MAKE_PRG; my $rshprg = $opt{'rshprg'} || $ENV_ORG{'RSHELL'} || $CONFIG->{'RSH_PRG'} || RSH_PRG; my $mailprg = $opt{'mailprg'} || $CONFIG->{'MAIL_PRG'} || MAIL_PRG; my $tar_dirname = $CONFIG->{'DIST_DIRNAME'} || 'tar'; my $tar_dir = join('/', $reldir, $tar_dirname); my $arch = $opt{'arch'}; my $create = $opt{'create'}; my $force_tag = $opt{'force-tag'}; my $makefile = $opt{'makefile'}; my $mailto = $opt{'mailto'}; my $newsfile = $opt{'news'}; my $url = $opt{'url'}; my $notlatest = $opt{'notlatest'}; my @rhosts = (); @rhosts = @{$opt{'rhost'}} if $opt{'rhost'}; my $snapmode = $opt{'snapmode'}; my($version, $override, $version_dir); if ($snapmode) { $create = 0; $force_tag = 0; $notlatest = 1; $version = get_snap_date() . '-snap'; $version_dir = 'snapshot'; $override = 1; } else { ($version, $override) = determine_version($reldir, shift(@ARGV), $opt{'noversion-check'}); $version_dir = $version; # Force create flag to true if doing an override release $create = 1 if $override; } my @cvs = ('cvs', '-d', $cvsroot); my $platform = "generic"; my $buildroot = $reldir . "/$version_dir"; my $buildir = $buildroot; my $buildir_parent = $reldir; my $buildir_basename = $version_dir; my $testname = ",test-$version"; my $testroot = $reldir . "/$testname"; my $testdir = $testroot; my $testdir_parent = $reldir; my $testdir_basename = $testname; if ($arch) { $platform = lc `uname -s -p`; $platform =~ s/\s+$//g; $platform =~ s/\s/-/g; $buildir_parent = $buildir; $buildir_basename = $platform; $buildir = $buildir . "/$platform"; $testdir_parent = $testdir; $testdir_basename = $platform; $testdir = $testdir . "/$platform"; } my $cvstag = $version; $cvstag = "v$cvstag" unless $cvstag =~ /\A[A-Za-z]/; # CVS tag names must start with an alpha $cvstag =~ s/\./-/g; # CVS does not allow '.'s in tag names my @makeprg = ($makeprg); if (defined($makefile)) { push(@makeprg, '-f', $makefile); } my(@a); ## Make clean environment to avoid environment influence during builds %ENV = ( PATH => join(':', '/usr/local/bin', '/opt/sfw/bin', # Solaris '/usr/ccs/bin', # Solaris '/usr/bin', '/bin', '/usr/X11R6/bin', '/usr/openwin/bin'), # Solaris SHELL => '/bin/sh', CVSROOT => $cvsroot, HOME => $ENV_ORG{'HOME'}, # Special envariable that makefiles can key off of _RELEASE_MODE => '1', _RELEASE_VERSION => $version, ); if ($snapmode) { $ENV{'_SNAP_MODE'} = '1'; } foreach (@{$CONFIG->{'ENV_KEEP'}}) { $ENV{$_} = $ENV_ORG{$_} if defined $ENV_ORG{$_}; } if ($CONFIG->{'PATH'}) { $ENV{'PATH'} = $CONFIG->{'PATH'}; } $ENV{'TZ'} = $ENV_ORG{'TZ'} if defined $ENV_ORG{'TZ'}; $ENV{'CVS_RSH'} = $ENV_ORG{'CVS_RSH'} if defined $ENV_ORG{'CVS_RSH'}; $ENV{'SSH_AGENT_PID'} = $ENV_ORG{'SSH_AGENT_PID'} if defined $ENV_ORG{'SSH_AGENT_PID'}; $ENV{'SSH_AUTH_SOCK'} = $ENV_ORG{'SSH_AUTH_SOCK'} if defined $ENV_ORG{'SSH_AUTH_SOCK'}; $ENV{'SSH_TTY'} = $ENV_ORG{'SSH_TTY'} if defined $ENV_ORG{'SSH_TTY'}; ## Run a test build TEST: { last TEST if $opt{'notest'}; if ( -e $testdir ) { die qq/Error: Test build directory already exists, "$testdir".\n/, qq/ Is someone else doing a release also?\n/; } run_prg('/bin/mkdir', '-p', $testdir); local $SIG{__DIE__} = sub { system('/bin/rm', '-rf', $testdir) unless $keeptest; die $_[0]; }; ch_dir($testdir_parent) || die qq/Error: Unable to chdir to "$testdir_parent": $!\n/; if (!$create && !$snapmode) { run_prg(@cvs, 'export', '-r', $cvstag, '-d', $testdir_basename, $project); } else { run_prg(@cvs, 'export', '-D', scalar(localtime), '-d', $testdir_basename, $project); } if (@rhosts) { foreach (@rhosts) { run_prg($rshprg, $_, qq|@makeprg -C "$testdir" |.$mk_target_release); } } else { run_prg(@makeprg, '-C', $testdir, $mk_target_release); } ch_dir($reldir) || die qq/Error: Unable to chdir to "$reldir": $!\n/; if (!$keeptest) { run_prg('/bin/rm', '-rf', $testroot) if ($TEST_ONLY || !$snapmode); } } ## If only testing, we are done. if ($TEST_ONLY) { last MAIN; } ## Check if we need to tag the project if ($create) { my @args = ('rtag'); push(@args, '-F') if ($override || $force_tag); push(@args, $cvstag, $project); run_prg(@cvs, @args); } ## Export project and make release if ($snapmode) { run_prg('/bin/rm', '-rf', $buildroot); change_name($testroot, $buildroot) || die qq/Error: Unable to rename "$testroot" to "$buildroot": $!\n/; } else { run_prg('/bin/mkdir', '-p', $buildir); ch_dir($buildir_parent) || die qq/Error: Unable to chdir to "$buildir_parent": $!\n/; run_prg(@cvs, 'export', '-r', $cvstag, '-d', $buildir_basename, $project); if (@rhosts) { foreach (@rhosts) { run_prg($rshprg, $_, qq|@makeprg -C "$buildir" |.$mk_target_release); } } else { run_prg(@makeprg, '-C', $buildir, $mk_target_release); } } if (defined($newsfile)) { $newsfile = "$buildir/$newsfile"; } elsif (-e "$buildir/NEWS") { $newsfile = "$buildir/NEWS"; } unless ($notlatest) { if ((-e 'latest') && !(-l 'latest')) { die qq|Error: "$reldir/latest" is not a symlink!\n|; } run_prg('/bin/rm', '-f', 'latest'); run_prg('/bin/ln', '-s', $version_dir, 'latest'); } ## Make tar bundle mk_dir($tar_dir) unless -e $tar_dir; if ($snapmode) { remove_snap_bundles($tar_dir); } my $bundle = "$tar_dir/$project-$version-$platform.tar"; my $tarroot = "$project/$version_dir"; $tarroot = $tarroot . "/$platform" if $arch; my $distdir = join('/', $releases_dir, $tarroot, 'dist'); if ( -e $distdir ) { run_prg('/bin/chmod', 'ug+w', $distdir); copy_dist_bundles($distdir, $tar_dir); cmd('/bin/rm', '-rf', $distdir); } else { if (cmd("(cd '".$releases_dir."'; tar cvf '$bundle' '$tarroot')")) { warn qq/Warning: Problem creating "$bundle": $?\n/, qq/\tRelease still made; creating tar bundle failed.\n/; } else { if (cmd('gzip', "$bundle")) { # error in gzip, just have tar file warn qq/Warning: Problem gzipping "$bundle": $?\n/, qq/\tRelease still made; gzipping tar bundle failed.\n/; cmd('/bin/chmod', '0444', $bundle); } else { # no error, include .gz extension to chmod cmd('/bin/chmod', '0444', "$bundle.gz"); } } } run_prg('/bin/chmod', '-R', 'a-w', $buildir) unless $snapmode; ## Send mail notification (if requested) if ($create && $mailto && @$mailto) { local(*MAIL); my $mailcmd = $mailprg; if ($ECHO_ONLY) { print $mailcmd, "\n"; } else { if (open(MAIL, "|$mailcmd")) { my $release_word = ($override ? 'Re-release' : 'Release'); print MAIL "To: ", join(", ", @$mailto), "\n", "Subject: $project $release_word: $version\n", "\n", "$release_word $version made in $reldir.\n"; if ($url) { print MAIL "<$url>\n"; } if ($newsfile) { local(*NEWS); local($_); if (open(NEWS, $newsfile)) { while () { last if /^\s*={40}/; } if (!eof(NEWS)) { print MAIL $_ unless eof(NEWS); while () { print MAIL $_; last if /^\s*={40}/; } } else { # Not using "==..." to separate release comments, so send # the whole file. seek(NEWS, 0, 0); while () { print MAIL $_; } } close NEWS; } else { warn qq/Warning: Unable to open "$newsfile": $!\n/, qq/\tRelease still made.\n/; } } } } } } # END: MAIN ############################################################################ ## Routines ############################################################################ #== Run a shell command and return wait(2) status. #=# sub cmd { if ($ECHO_ONLY) { print "@_\n"; return 0; # bogus ok exit status } system @_; } #== Run program and die if non-zero exit status. #=# sub run_prg { print "@_\n"; if (!$ECHO_ONLY) { my $wait = system @_; die qq/system @_ failed: $?\n/ if $wait; } } #== Create a directory. #=# sub mk_dir { my $dir = shift; my $mask = shift; if ($ECHO_ONLY) { print "mkdir $dir\n"; } else { if (defined($mask)) { mkdir $dir, $mask; } else { mkdir $dir, 0777; } } } #== Change the current working directory. #=# sub ch_dir { my $dir = shift; if ($ECHO_ONLY) { print "chdir $dir\n"; } else { chdir $dir; } } #== Rename a file #=# sub change_name { my $curname = shift; my $newname = shift; if ($ECHO_ONLY) { print qq/rename "$curname" "$newname"\n/; } else { rename($curname, $newname); } } #== Determine project release version. #=# sub determine_version { my $dir = shift; my $version = shift; my $nocheck = shift; my $override = 0; if (!$version) { ## If version not specified, we determine what the next version number ## for the project and confirm it with the user. local(*DIR); opendir(DIR, $dir) || die qq/Error: Unable to open "$dir": $!\n/; my @versions = sort by_version grep { /^\d+\.\d+(?:\.\d+)?$/ } readdir(DIR); my($next); if (@versions) { my @p = split(/\./, $versions[0]); ++$p[$#p]; $next = join('.', @p); } else { $next = "0.1.0"; } while (1) { $version = get_user_input("Release version ($next)? ") || $next; last if $nocheck || valid_version($version); warn qq/Invalid version format!\n/; } } ## We have a version, check that is valid format. die qq/Error: Invalid version "$version"\n/ unless $nocheck || valid_version($version); if ( -e "$dir/$version" ) { ## If version already exists, ask to override. exit(0) unless ask_yn("Overwrite existing $version", 0); run_prg('/bin/mv', "$dir/$version", "$dir/,$version"); $override = 1; } ($version, $override); } #== Check for valid version format. #=# sub valid_version { $_[0] =~ /^\d+\.\d+(?:\.\d+)?$/ } #== Copy distribution bundles from one directory to another #=# sub copy_dist_bundles { my $srcdir = shift; # Source directory my $dstdir = shift; # Destination directory # Get list of files that look like distribution bundles local(*DIR); if (!opendir(DIR, $srcdir)) { # Should hopefully not get here since existence of directory # is pre-checked earlier and we should be the owner of the directory. warn qq/Warning: Unable to open "$srcdir" for reading: $!\n/, qq/\tCopying of distribution bundles failed!\n/; return; } my @files = grep { /\.tar$/ || /\.tar\.(?:gz|bz2|Z)$/ || /\.zip$/ || /\.dep$/ || # Debian package /\.rpm$/ # RPM package } readdir(DIR); closedir(DIR); # Copy each file to destination and make it read-only foreach my $file (@files) { cmd('/bin/cp', '-f', "$srcdir/$file", $dstdir); cmd('/bin/chmod', 'a-w', "$dstdir/$file"); } } #== Remove snapshot bundles from given directory #=# sub remove_snap_bundles { my $dir = shift; local(*DIR); opendir(DIR, $dir) || die qq/Error: Unable to open "$dir" for reading: $!\n/; my @names = grep { /\b-snap\b/i } readdir(DIR); closedir(DIR); if (@names) { my @files = map { join('/', $dir, $_) } @names; cmd('/bin/rm', '-f', @files); } } #== Load configuration file. #=# sub load_config { my $filename = shift; my $conf = undef; eval { $conf = require $filename; }; if ($@) { die qq/Error: Unable to require "$filename": $@\n/; } $conf; } #== Get a line of input from the user. #=# sub get_user_input { print @_; my $line = scalar(); chomp $line; return "" if $line !~ /\S/; $line; } #== Ask user a yes/no question. #=# sub ask_yn { my $prompt = shift; my $def = shift; my $defstr = $def ? "y" : "n"; my $in = get_user_input($prompt, " ($defstr)? "); return 1 if $in =~ /^y|yes$/; return 0 if $in =~ /^n|no$/; return $def; } sub by_version { my(@a) = split(/\./, $a); my(@b) = split(/\./, $b); my($p); my $an = shift @a; $an .= '.'; foreach $p (@a) { $an .= sprintf("%03d", $p); } my $bn = shift @b; $bn .= '.'; foreach $p (@b) { $bn .= sprintf("%03d", $p); } $bn <=> $an; } sub get_snap_date { my($yr, $mon, $day) = (localtime(time))[5,4,3]; $yr += 1900; ++$mon; sprintf("%4d-%02d-%02d", $yr, $mon, $day); } ############################################################################ __END__ =head1 NAME release - Perform a project release for projects. =head1 SYNOPSIS release [options] [version] =head1 DESCRIPTION B is a Perl program to automate project releases from CVS. This program is typically called from a project-specific release script with the proper options. Example: #/bin/sh exec perl /home/projects/release/devtools/latest/bin/release \ -cvsroot /home/cvs/root \ -project someapp \ -mailto earl@earlhood.com \ ${1+"$@"} The C<${1+"$@"}> is Bourne shell construct that safely passes any options specified to the script to the program being called. If the script is called, "release-someapp", then sample invocation would be: shell> release-someapp -create 1.0.0 Every project must define a top-level makefile that defines the C target. The target should at a minimum compile/build the project. The following basic tasks are performed during a project release: =over =item * A test build of the project in a "clean" environment. I.e. Only basic environment variables are set when executing the build. This is to insure there are no indirect influences in caller's environment that can affect the release process. The test build can be skipped by using the C<-notest> option. =item * Automatic tagging of the project in CVS -- if C<-create> option specified -- to allow source code recovering for a specified release version. If C<-create> is not specified, the project must already be tagged for the specified version. =item * Export of project into project release directory. This provides a browsable version of files for a release. =item * Tar-gzipping of the built project, unless the project itself creates its own distribution bundles (see L). =item * Update of C symlink to point to release just make (unless the C<-notlatest> option is specified). =back The version number of the release can be specified on the command-line. If it is not, this program will prompt you for a release number giving a default value as the next version number increment. If you specify a version that already exists, you will be prompted for confirmation that you want override an existing release. Version numbers must conform to the following Perl regular expression: C. Examples: C<1.0>, C<0.10.4>, C<23.0.1>. Arbitrary version number formats can be done by use the C<-noversion-check> option. =head1 OPTIONS =over =item C<-arch> Perform an architectural-specific release. This option should be used for projects that will create machine-executable code (however, see the B> option for an alternative). The released project will be placed in an architecture identifiable directory and the tar-gz bundle will include the architecture in the name. The name to identify the architecture is the return value of the following command: C. =item C<-conf> I Pathname of configuration file to use. See L for more information. =item C<-create> Tag the project in CVS. If not specified, it is assumed that the project has been tagged for the specified version. If this option is specified, a C will be performed on the project. The tag name used is the release version specified modified as follows: dots are converted to hyphens and the letter "C" is prepended. For example, if the version number is C<0.5.1>, the tag wil be C. The translation is done since CVS does not support dots in tag names, and tag names must start with an alpha character. In most usage cases, the C<-create> option is always specified. Examples of when it is not specified is if a manual CVS tagging was performed; or multiple architectural releases of a project are being done, and after the first architectural release, the other releases do not need C<-create> since the first release already did the tagging (see B> option for an alternative for multiple architecture releases). C<-create> is also not required if doing an override release: the specified version already exists and the new release will override the existing one. =item C<-cvsroot> I The CVS repository root for the project. If not specified, the value of the C environment variable is used or the root specified in the site-wide configuration file. If no cvs root is specified, then the program terminates with an error. =item C<-keeptest> Do not delete test files. Useful for debugging problems and when C<-test-only> is specified. =item C<-force-tag> Force tagging of repository, regardless. Forcing is automatic when an overriding release is detected. =item C<-help> Display help message. =item C<-mailprg> I Mail program to use to send mail notification if C<-mailto> options are specified. The program must be able to take a valid RFC 822 formated message with the destination receipients specifed in the C message header field. The default value is C. =item C<-mailto> I
Address(es) to mail when release is done. This option can be specified multiple times inorder to provide multiple recipients. =item C<-makefile> I The name of the project makefile. This option allows one to specify a makefile to invoke when performing a release if the makefile is not one of the default filenames searched for by C. =item C<-makeprg> I C program. If not specified, the value of the C environment variable is used. If this is not specified, C is used. =item C<-man> Display manpage. =item C<-n> Echo to stdout what would be done, but do B do it. This option is useful to see what actions will be performed without them being executed; useful for debugging. =item C<-news> I Filename of file (relative to project root) that provides a summary of changes for this release. This option is used if C<-mailto> is specified. Contents of I will be included in the mail message sent. To minimize the size of the message, an attempt is made to only include the comments only for the release. To do this, a check is made for separators lines composed of the 'C<=>' character. Example: ====================================================================== The first occurrence is ignored. All text up to the next occurrence will be included in the mail message sent. If no occurrences of a separator is detected, then the entire contents of the file will be included in the message. If this option is not specified, the file called C in the released project's root directory will be used. =item C<-noversion-check> Do not validate the version number. This is useful if the project does not following the normal versioning format. =item C<-project> I Name of project to release. This is a required option. =item C<-notest> Skip performing a test build. =item C<-notlatest> Not latest release, so do not update latest link. Normally, the C symlink will be updated to point to the release just made. This option supresses the update. This is useful for cases when older releases are redone. =item C<-rhost> I Build project on specified I. This option can be specified multiple times to specify multiple hosts to build against. The build process is invoked for each remote host in the order specified. This option is useful for providing a build for multiple OS/hardware platforms. B It is assumed that the hosts specified NFS mount the release directory so that C exists on all hosts specified, including localhost. B Using this option assumes that the project makefiles are configured to support compilation under multiple platforms. If not, use the C<-arch> option instead for creating separate releases for each platform. =item C<-rshprg> I Specify the remote shell program to use when invoking C for hostnames specified with C<-rhost>. The program specified must follow the calling sematics of C. Note, depending on system configuration, authentication may need to be prompted for each remote host, like for C. The default value is C. =item C<-snapmode> Run in snapshot mode. Instead of doing a versioned release, a release is done on the latest source within CVS. =item C<-test-only> Only perform the test build. After the test build, the program exits. The test build directory is still removed. =item C<-url> I URL to include in mail message if C<-mailto> is specified. =back =head1 SITE CONFIGURATION B supports site-wide configuration to allow for sites to customize to the behavior of B to suit local preferences. The following files are checked for when B starts: =over =item 1 The pathname specified by the C<-conf> option. =item 2 C<$HOME/.release.conf> =item 3 C =item 4 C =back The first file found is used, and any subsequent files will be ignored. The following example shows what configuration settings are supported, the default values for those settings, and the syntax of the configuration file: # Site configuration file for devtools release program # # The syntax of this file is Perl and it should return a reference # to a hash when required. +{ # Pathname to CVS respository: Overridden by -cvsroot option # or CVSROOT envariable. The value can be any legal cvs root # specification, including external repositories like ":ext:..." CVS_ROOT => '/home/cvs/root', # Name of directory under a project's directory containing releases of # the project. DIST_DIRNAME => 'tar', # Mail program: Overridden by -mailprg option. MAIL_PRG => '/usr/lib/sendmail -t', # Make program: Overridden by -makeprg option or MAKE envariable. MAKE_PRG => 'gmake', # Make target name to invoke to create a release. MK_TARGET_RELEASE => 'release', # Program search path to use when performing release. This # should contain the absolute minimum required to build a project. PATH => join(':', '/usr/local/bin', '/opt/sfw/bin', # Solaris '/usr/ccs/bin', # Solaris '/usr/bin', '/bin', '/usr/X11R6/bin', '/usr/openwin/bin'), # Solaris # Pathname to project releases. REL_DIR => '/home/projects/release', # Remote shell program (for multi-architecture releases): Overridden # by RSHELL envariable. Use ssh if security is a concern. RSH_PRG => 'rsh', # File creation umask. Notice how there is NO quotes used. UMASK => 002, # List of environment variables to preserve from calling process ENV_KEEP => [ ], }; =head1 ENVIRONMENT =over =item C Pathname to CVS repository. Overridden by C<-cvsroot> option. =item C Name of C program. Overridden by C<-makeprg> option. =item C Name of remote shell program. Overridden by C<-rshprg> option. =item C<_RELEASE_MODE> Envariable set by B. This variable can be referenced by a project's makefile if it needs to know if it is being invoked by this program. The value of C<_RELEASE_MODE> is C<1>. =item C<_RELEASE_VERSION> Envariable set by B containing the release version number of the project. This variable can be referenced by a project's makefile if it needs to know the version number when executing the makefile's C target. If C<-snapmode> is specified, the release version will be in the format C-snap>. =item C<_SNAP_MODE> Envariable set by B. This variable can be referenced by a project's makefile if it needs to know if it is being invoked by this program with C<-snapmode>. The value of C<_SNAP_MODE> is C<1>. B The C<_RELEASE_VERSION> envariable will still be set to C<1> when C<-snapmode> is specified. =back =head1 FILES =over =item C Root directory where all releases are placed. =item CprojectE>> Root directory containing releases for IprojectE>. B This directory must be manually created before any releases can be done for a given IprojectE>. The reasons for this are as follows: =over =item * Allows one to explicitly control when releases can be performed for a project. When a project is first started, it may be desirable to prevent any releases to be performed until some initial milestone is reached. =item * Allows explicit review of who is allowed to perform releases. I.e. The directory ownership and permissions can be properly set to reflect who is allowed to invoke the release process. =back =item CprojectE>/IversionE>> Directory where a given I of a I is placed by this program. =item CprojectE>/IversionE>/IplatformE>> Directory where a given I of a I is placed by this program when C<-arch> has been specified to do platform-specific release. =item CprojectE>/tar> Directory where tar-gz bundles are placed for I. =back =head1 NOTES =over =item * Projects can define their own distribution bundles to be placed into CprojectE>/tar>, overriding the bundle creation of this program. To do this, the project can create the bundle (or bundles if multiple, alternative bundles are relevant for the project) within a directory called C under the project root. Any filename that ends in the following extensions, C<.tar>, C<.tar.gz>, C<.tar.bz2>, C<.tar.Z>, C<.zip>, C<.dep>, C<.rpm>, will be copied into CprojectE>/tar>. The C directory will then be removed once the files are copied. B It is important that any bundles created by the project itself have filenames that reflect the version of the project being released to avoid overwritting past version bundles in CprojectE>/tar>. To assist in this, this program defines the environment variable C<_RELEASE_VERSION> to contain the release version number for use within the project's makefile. =item * If an override release is being performed, the existing release directory is moved to C<,I> where I is the version of the release as a backup. If the backup is no longer required, or another override release is to be done for the specified version, the backup directory must be manually removed. =item * If ssh is the prefered, or required, mode for communicating with a remote repository, performing automated snapshots can be a problem since a password is required, unless you decide to use passwordless key authentication. An option is to utilize ssh-agent(1). B preserves ssh-agent related environment variables when creating the clean environment. =back =head1 AUTHOR Earl Hood, earl@earlhood.com This program comes with ABSOLUTELY NO WARRANTY and may be copied only under the terms of the GNU General Public License. =cut