tools/createsrc
author Tom Sutcliffe <thomas.sutcliffe@accenture.com>
Mon, 25 Oct 2010 10:37:01 +0100
changeset 75 6bffbe8be665
parent 0 7f656887cf89
permissions -rw-r--r--
Added bsym file format spec.

#!perl
# createsrc
# 
# Copyright (c) 2009 - 2010 Accenture. All rights reserved.
# This component and the accompanying materials are made available
# under the terms of the "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
# 
# Initial Contributors:
# Accenture - Initial contribution
#

use strict;
use Getopt::Long;
use Cwd;
use FindBin;

# forward decls
sub homeDir();
sub processCommandLine();
sub Usage();
sub make4CharId($);
sub caseRename($$$);
sub renameFileCopy($$);
sub GetMacroValue($);
sub PromptMacro($);
sub CheckMacro($$);
sub CheckUid($);
sub CheckIdentifier($);
sub listTemplates($);
sub getTemplates();
sub query($$);
sub getConfigFileName();
sub getConfig($);
sub setConfig($$);
sub setCopyright($);


#globals
my $template;
my $newName;
my $templateDir;
my $templateName = "SkeletonTemplate";
my $dest;
my $help;
my $verbose = 0;
my %macroValue;
my %macroDescription = ( 'UID' => 'a UID', 'COPYRIGHT' => 'a copyright notice' );
my $homeDir;
my $listTemplates;
my $overwrite = 0;
my $addToPerforce = 0;
my $addToMercurial = 0;
my $interactive = 0;

processCommandLine();

unless (defined $macroValue{'COPYRIGHT'}) {
	$macroValue{'COPYRIGHT'} = getConfig('copyright');
	$macroValue{'COPYRIGHT'} = "Copyright (c) [[YEAR]] Accenture. All rights reserved." unless $macroValue{'COPYRIGHT'};
}


$homeDir = homeDir() unless defined ($homeDir);

my $year = [localtime()]->[5]+1900;
$macroValue{'COPYRIGHT'} =~ s|\[\[YEAR\]\]|$year|g;

if ($interactive) {
  my $templates = getTemplates();
  print "Available Templates:\n";
  my $count = 1;
  my $options="";
  foreach my $template (@$templates) {
    print "$count) $template->[0] - $template->[1]\n";
    $options=$options . $count;
    $count++;
  }
  do {
  	print "Which template do you want to use? ";
  	my $in = <>;
  	chomp $in;
  	my $choice;
  	$choice = eval "int($in)";
  	unless ($@) {
  	  if (($choice>0) && ($choice <= scalar(@$templates))) {
  	    $template = $templates->[$choice-1]->[0];
  	  }
  	}
  	my $max = $count-1;
    print "Please enter a number between 1 and $max\n" unless defined $template;
  }	until (defined $template);
  print "Using template $template\n" if $verbose;
  
  do {
    print "Name for the new code? ";
    $newName = <>;
    chomp $newName;
    
    unless (CheckIdentifier($newName)) {
      print "$newName is not a valid C++ identifier\n";
      undef $newName;
    }
   
  } until ((defined $newName) && (length $newName));
  
   my $cwd = cwd();
   print "Directoy to create new code in? [$cwd] ";
   $dest = <>;
   chomp $dest;
   $dest = $cwd unless (length $dest);
  
} else {

  $dest = cwd() unless $dest;
  
}

$templateDir = $homeDir."/createsrc-templates/$template";
die "ERROR: Template $template not found at $templateDir\n" unless -d $templateDir;

$dest =~ s|\\|/|g;
$dest =~ s|/$||;
print "Creating $newName at $dest from template $template at $templateDir\n" if ($verbose);

# read [[MACRO]] descriptions from .txt file
my $infoFile;
my $infoFileName = "$templateDir.txt";
if ((-f $infoFileName) && (open $infoFile, $infoFileName)) {
	print "Reading $infoFileName\n" if ($verbose);
	<$infoFile>; # skip template description
	while (my $line = <$infoFile>) {
		chomp $line;
		$line =~ s|#.*||; # remove perl style comments
		if ($line =~ m|^\s*(\w[\w\d]*)\s*\:\s*(.*)$|) {
			my ($macro, $desc) = ($1, $2);
			print "Macro '$macro': '$desc'\n" if ($verbose>1);
			$macroDescription{$macro} = $desc;
		} elsif ($line =~ m|^\s*$|) {
			# blank line
		} else {
			die "Error: cannot parse line $. of $infoFileName\n";
		}
	}
	
	close $infoFile;
}

unless (-d $dest) {
  mkdir($dest) or die "ERROR: Couldn't create directory $dest: $!\n";
  print "Created directory $dest\n" if ($verbose>1);
}

my @dirStack;
push @dirStack, "";


while ($#dirStack>=0) {
  my $dir = pop(@dirStack);

  my $dirName = $dir;
  $dir = "/$dir" unless ($dir eq "") || ($dir =~ m|^/|);

  # create destination directory
  unless (-d "$dest$dir") {
    mkdir "$dest$dir" or die "ERROR: cannot create directory $dest$dir: $!\n";
    print "Created directory $dest$dir\n" if ($verbose);
  }

  # read template directory contents
  opendir DIR, "$templateDir$dir";
  my @dirContents = readdir(DIR);
  closedir(DIR);

  foreach my $f (@dirContents) {
    if (-f "$templateDir$dir/$f") {
      my $newFile = caseRename($f, $templateName, $newName);
      print "Renaming from $templateDir$dir/$f to $dest$dir/$newFile\n" if ($verbose);
      renameFileCopy("$templateDir$dir/$f", "$dest$dir/$newFile"); 
    } elsif (-d "$templateDir$dir/$f") {
      unless ($f =~ m|^\.\.?$|) {
        print "Adding $dirName/$f to stack\n" if ($verbose);
        push @dirStack, "$dirName/$f";
      }
    } else {
      print "WARNING: $templateDir$dir/$f is neither a file nor a directory";
    }
  }

}



sub homeDir() {
  my $homeDir = $FindBin::Bin;
  print "homeDir=$homeDir\n" if ($verbose > 1);
  return $homeDir;
}

sub processCommandLine() {
  my $setCopyright = undef;
  Getopt::Long::Configure("bundling");
  my @macroDefs;
  GetOptions('h'   => \$help,
             'v+'  => \$verbose,
             'd=s' => \$dest,
             's=s' => \$homeDir,
             't=s' => \$templateName,
             'l'   => \$listTemplates,
             'o'   => \$overwrite,
             'p'   => \$addToPerforce,
	     'm'   => \$addToMercurial,
             'i'   => \$interactive,
             'c=s' => \$setCopyright,
	     'D=s' => \@macroDefs);

  Usage() if ($help);

  listTemplates(1) if ($listTemplates);
  
  setCopyright($setCopyright) if (defined $setCopyright);

  unless ($interactive) {
    if ($#ARGV < 1) {
      print "Insufficient arguments; entering interactive mode\n";
      $interactive = 1;
    } else {
      $template = shift(@ARGV);
      $newName = shift(@ARGV);
      die "$newName is not a valid C++ identifier\n" unless CheckIdentifier($newName);
    }
  }
  foreach my $d (@macroDefs) {
	if ($d =~ m|^(\w[\w\d]*)=(.*)$|) {
		my ($macro, $value) = ($1, $2);
		print "Got macro '$macro'='$value' from command line\n" if ($verbose>1);
		die "Error: '$value' is not a valid value for $macro\n" unless CheckMacro($macro, $value);
		$macroValue{$macro} = $value;
	} else {
		die "Error: Incorrect usage of -D: $d: expected a string of the format 'id=value'\n";
	}
  }  
  print "Warning: ignoring " . scalar(@ARGV) . " extra arguments\n" if scalar(@ARGV);
}

sub Usage() {
  require Pod::Text;
  my $parser = Pod::Text->new();
  $parser->parse_from_file($0);
  exit;
}

sub make4CharId($) {
	my $longId = shift;
	while (length($longId)<2) {
		$longId = $longId . '_';
	}
	return uc( substr($longId, 0, 2) . substr($longId, length($longId)-2, 2) );
}

sub caseRename($$$) {
  my $string = shift;
  my $oldName = shift;
  my $newName = shift;

  my $oldUpper = $oldName;
  $oldUpper =~ tr/a-z/A-Z/;

  my $oldLower = $oldName;
  $oldLower =~ tr/A-Z/a-z/;

  my $oldLowerFirst = "\l$oldName;\E";

  my $uppercase = $newName;
  $uppercase =~ tr/a-z/A-Z/;

  my $lowercase = $newName;
  $lowercase =~ tr/A-Z/a-z/;

  my $lowerFirst = "\l$newName\E";
  
  my $old4Char = make4CharId($oldName);
  my $new4Char = make4CharId($newName);

  $string =~ s/$oldName/$newName/g;
  $string =~ s/$oldUpper/$uppercase/g;
  $string =~ s/$oldLower/$lowercase/g;
  $string =~ s/$oldLowerFirst/$lowerFirst/g;
  $string =~ s/$old4Char/$new4Char/g;
  return $string;
}

sub renameFileCopy($$) {
  my $src = shift;
  my $dest = shift;
  open SOURCE, "<$src" or die "ERROR: can't open $src for reading: $!\n";
  unless ($overwrite) {
    die "ERROR: $dest already exists. Use -o to overwrite. Aborting.\n" if (-f $dest);
  }
  open DEST, ">$dest" or die "ERROR: can't open $dest for writing: $!\n";
  while (my $line = <SOURCE>) {
    while ($line =~ m|\[\[(\w[\w\d]*)\]\]|) {
	my $id = $1;
	my $value = GetMacroValue($id);
	$line =~ s|\[\[$id\]\]|$value|g;
    }
    print DEST caseRename($line, $templateName, $newName);
    
  }
  close (SOURCE);
  close (DEST);
  if ($addToPerforce) {
    print "p4 add $dest\n";
    `p4 add $dest` ;
  }
  if ($addToMercurial) {
    print "hg add $dest\n";
    `hg add $dest` ;
  }
}

sub GetMacroValue($) {
	my $macro = shift;
	if (!defined($macroValue{$macro})) {
		$macroValue{$macro} = PromptMacro($macro);
	}
	return $macroValue{$macro};
}

sub PromptMacro($) {
	my $macro = shift;
	my $description = $macroDescription{$macro};
	$description = $macro unless defined $description;		
	
	print "Enter $description for $newName: ";
	my $value;
	do  {
		$value = <>;
		chomp $value;
	} while (!CheckMacro($macro, $value));
	return $value;
}

sub CheckMacro($$) {
	my ($macro, $value) = @_;
	if ($macro eq 'UID') {
		return CheckUid($value);
	}
	return 1;
}

sub CheckUid($) {
  my $uid = shift;
  return 1 if ($uid =~ m|^0x[0-9a-f]{1,8}$|i);
  return 1 if ($uid =~ m|^[0-9]{1,10}$|);
  return 0;
}

sub CheckIdentifier($)	{
  my $id = shift;
  return ($id =~ m|[A-Za-z_][A-Za-z_0-9]*|);
}

sub getTemplates() {
  $homeDir = homeDir() unless defined ($homeDir);
  opendir TEMPLATES, "$homeDir/createsrc-templates" or die "Can't read directory $homeDir/createsrc-templates: $!\n";
  my @templateNames = grep((-d "$homeDir/createsrc-templates/$_")&&!(m|\.\.?|), readdir(TEMPLATES));
  close TEMPLATES;
  die "No templates found at $homeDir/createsrc-templates\n" unless scalar(@templateNames);
  my @templates = ();
  foreach my $template (@templateNames) {
  	my $desc;
    my $descFile = "$homeDir/createsrc-templates/$template.txt";
    if ((-f $descFile)&&(open DESC, $descFile)) {
      $desc = <DESC>;
      chomp $desc;
      close DESC;
    } else {
     $desc = "<no description>";
    }
    push @templates, [$template, $desc];
  }
  @templates = sort {$a->[0] cmp $b->[0]} @templates;
  return \@templates;
}

sub listTemplates($) {
  my $exitWhenDone = shift;
  my $templates = getTemplates();
  foreach my $template (@$templates) {
    print $template->[0] . " - " . $template->[1] . "\n";
  }
  exit if $exitWhenDone;
}

sub getConfigFileName() {
  my $fn;
  if (defined $ENV{'USERPROFILE'}) {
    $fn = $ENV{'USERPROFILE'};
  } else {
    $fn = homeDir();
  }
  $fn =~ s|[/\\]$||;
  $fn .= "\\createsrc.cfg";
  return $fn;
}

sub getConfig($) {
  my $findKey = shift;
  my $cfg = getConfigFileName();
  my $foundValue = undef;
  open CFGFILE, "<$cfg" or return undef;
  print "Reading $cfg\n" if ($verbose);
  while (my $line = <CFGFILE>) {
    chomp $line;
    if ($line =~ m|^(.*?)\s*=\s*(.*)$|)	{
      my ($key, $value) = ($1, $2);
      print "Read '$key'='$value'\n" if ($verbose>1);
      $foundValue = $value if ($key eq $findKey);
    } else {
      print "ignoring line '$line'\n" if ($verbose>1);
    }
  }
  return $foundValue;
  close CFGFILE;  
}

sub setConfig($$) {
  my ($key, $value) = @_;
  my $cfg = getConfigFileName();
  my @lines;
  if (open CFGFILE, "<$cfg") {
    print "Reading $cfg\n";
    @lines = <CFGFILE>;
    close CFGFILE;
  }
  open CFGFILE, ">$cfg" or die "Cannot write to $cfg: $!\n";
  print "Writing $cfg\n" if ($verbose);
  my $written = 0;
  foreach my $line (@lines) {
    if ($line =~ m|^(.*?)\s*=\s*(.*)$|)	{
      my ($foundKey, $foundValue) = ($1, $2);
      print "Read '$foundKey'='$foundValue'\n" if ($verbose>1);
      if ($key eq $foundKey) {
        $foundValue = $value;
        $written = 1;
        print "Updated key $foundKey to '$foundValue'\n" if ($verbose>1);
      }
      print CFGFILE "$foundKey=$foundValue\n";
    } else {
      print "ignoring line '$line'\n" if ($verbose>1);
      print CFGFILE $line;
    }
  }
  unless ($written) {
    print "Adding $key=$value\n";
    print CFGFILE "$key=$value\n";
  }
  close CFGFILE;
  
}

sub setCopyright($) {
  my $copyright =  shift;
  print "Setting copyright notice to '$copyright'\n" if ($verbose);
  setConfig('copyright', $copyright);
  exit(1);
}


__END__

=head1 NAME

createsrc - creates new code from templates

=head1 SYNOPSIS

	createsrc [options] [TemplateName NewName]

options:

=over

=item -i

Use L<Interactive mode>. This will be the default if C<TemplateName> and C<NewName> are not given.

=item -d <directory>

Put the new code in the specified directory. Defaults to current directory.

=item -DMACRO=value

Specfies a value for a macro. See "Macros" section below.

=item -c "Copyright Notice"

Sets the default copyright to use when none other is specified. See L<Copyright Notices> below.

=item -o

Overwrites any existing files when creating the new code.

=item -p

Add the new files to perforce as they are created.

=item -m

Add the new files to mercurial as they are created.

=item -s <directory>

Specifies the home directory of the script, where the templates are located. Defaults to the directory where the script is located.

=item -t <TemplateText>

Specified the text to be replaced in the template. Defaults to C<SkeletonTemplate>. It's not usually necessary to change this.

=item -l

List available templates, then exits.

=item -h

Shows this help.

=item -v

Verbose mode (-vv very verbose).

=back

=head1 DESCRIPTION

This script creates new projects or code snippets from templates. For example, assuming you have a template C<UiqBasic> that consists of a basic UIQ application, you could create a HelloWorld application as follows:

	createsrc UiqBasic HelloWorld

When creating the new code, it performs the following steps:

=over

=item 1.

Copies and renames the files as appropriate.

=item 2.

Replaces text within the files as appropriate.

=item 3.

Substitutes macros enclosed in double square brackets, C<[[MACRO]]>.

=back

When renaming files or substituting text withing the files, case is preseved as follows:

=over

=item 1.

C<MixedCaseText> is preserved - i.e. C<TemplateText> becomes C<NewText>.

=item 2.

C<lowercasetext> is preserved - i.e. C<templatetext> becomes C<newtext>.

=item 3.

C<UPPERCASETEXT> is preserved - i.e. C<TEMPLATETEXT> becomes C<NEWTEXT>.

=item 4.

C<lowerCaseFirst> is preserved - i.e. C<templateText> becomes C<newText>.

=back

This should cover all forms of capitalisation used within normal Symbian code.

In addition, the capitalised first and last two letters of C<TemplateText> is replaced with the same letters of C<NewText> - i.e. C<TEXT> becomes C<NEXT>. This is to support the C<NAME> value in an applications resource file, which must always have 4 letters.

If the new text has less than 4 characters, it will be padded with underscores. For example, an application called C<A> will be given a C<NAME> of C<A_A_>; C<Ap> becomes C<APAP>; C<HelloWorld> becomes C<HELD> and so on.

=head2 Interactive mode

If the C<-i> option is given, of if no C<TemplateName> and C<TemplateText> are given, interactive mode will be used:

	D:\work\new>createsrc
	Insufficient arguments; entering interactive mode
	Available Templates:
	1) UiqBasic - Basic UIQ application
	2) UiqImaging - Basic UIQ application with one large control, suitable for displaying an image, a video, etc.
	3) UiqList - Basic UIQ application with a list in the view.
	4) server - Symbian 9 transient server
	Which template do you want to use? 1
	Name for the new code? HelloWorld
	Directoy to create new code in? [D:/work/new]
	Enter a UID for HelloWorld: 0x10002000

When prompted for the directory to create the code in, simply hit enter to use the suggestion in square brackets. Or you can enter a relative or absolute path name to use instead.

=head2 Macros

Macros allow extra custom strings to be inserted into the generate code. Macro values can be specified on the command line using the C<-D> option; if no value is specified then it will be prompted for at runtime.

There are two macros that are used by all (or most) templates:

=over

=item C<COPYRIGHT>

Defines a copyright notice for the code, usually inserted in a comment at the top of each source file. See L<Copyright Notices> below.

=item C<UID>

Specifies a UID for the new code. This will be verified as a valid integer.

=back

Some templates may define other, custom macros. To define a macro value on the command line, use -D syntax in a similar way the a C preprocessor:

	createsrc -DUID=0x01234567


=head2 Copyright Notices

Copyright notices are automatically inserted into the source files, including the current year. When using the C<-DCOPYRIGHT=...> and C<-c> options, the copyright text may include C<[[YEAR]]>, which will be replaced with the current year when the code was created.

For example,

	createsrc -DCOPYRIGHT="Copyright (c) [[YEAR]] Joe Bloggs. All rights reserved"

The default copyright notice is C<Copyright (c) [[YEAR]] Accenture. All rights reserved.>

When the C<-c> option is used, the copyright notice is stored in C<%USERPROFILE%\createsrc.cfg> and so is persisted for future runs of C<createsrc>.

=head2 Templates

The templates are located in F<\epoc32\tools\Templates\TemplateName>. Each template usually contains an entire buildable project, but this may vary depending upon the nature of the template. The text that is replaced in template filenames and files is C<SkeletonTemplate> by default, but this can be modified with the C<-t TemplateText> option (although it is not recommended).

In the template, macros are enclosed in double square brackets, for example C<[[UID]]>. Templates should make use of the standard C<[[UID]]> and C<[[COPYRIGHT]]> macros and can use others if necessary.

If a F<TemplateName.txt> file is found in F<\epoc32\tools\Templates>, the first line of this file will be used as the description of the template when the C<-l> flag is given. The remainder of the file is expected to contain lines of the format:

	MACRO: description

giving descriptions for any custom macros used in the project. The descriptions will be used instead of the macro name when prmopting the user for a value at runtime.

=head1 COPYRIGHT

Copyright (c) 2008-2010 Accenture. All rights reserved.

=cut