source: ProjectBuilder/devel/pb/lib/ProjectBuilder/CMS.pm@ 475

Last change on this file since 475 was 452, checked in by Bruno Cornec, 16 years ago
  • Make newproj action work again
  • Add pb_cms_add function
  • Change interface of pb_cms_checkin (third param)
File size: 18.5 KB
Line 
1#!/usr/bin/perl -w
2#
3# Project Builder CMS module
4# CMS subroutines brought by the the Project-Builder project
5# which can be easily used by pbinit scripts
6#
7# $Id$
8#
9# Copyright B. Cornec 2007
10# Provided under the GPL v2
11
12package ProjectBuilder::CMS;
13
14use strict 'vars';
15use Data::Dumper;
16use English;
17use File::Basename;
18use POSIX qw(strftime);
19use lib qw (lib);
20use ProjectBuilder::Base;
21use ProjectBuilder::Conf;
22
23# Inherit from the "Exporter" module which handles exporting functions.
24
25use Exporter;
26
27# Export, by default, all the functions into the namespace of
28# any code which uses this module.
29
30our @ISA = qw(Exporter);
31our @EXPORT = qw(pb_cms_init pb_cms_export pb_cms_get_uri pb_cms_copy pb_cms_checkout pb_cms_up pb_cms_checkin pb_cms_isdiff pb_cms_get_pkg pb_cms_compliant pb_cms_log pb_cms_add);
32
33=pod
34
35=head1 NAME
36
37ProjectBuilder::CMS, part of the project-builder.org
38
39=head1 DESCRIPTION
40
41This modules provides configuration management system functions suitable for pbinit calls.
42
43=head1 USAGE
44
45=over 4
46
47=item B<pb_cms_init>
48
49This function setup the environment for the CMS system related to the URL given by the pburl configuration parameter.
50The potential parameter indicates whether we should inititate the context or not.
51It sets up environement variables (PBPROJDIR, PBDIR, PBREVISION, PBCMSLOGFILE)
52
53=cut
54
55sub pb_cms_init {
56
57my $pbinit = shift || undef;
58
59my ($pburl) = pb_conf_get("pburl");
60pb_log(2,"DEBUG: Project URL of $ENV{'PBPROJ'}: $pburl->{$ENV{'PBPROJ'}}\n");
61my ($scheme, $account, $host, $port, $path) = pb_get_uri($pburl->{$ENV{'PBPROJ'}});
62
63my ($pbprojdir) = pb_conf_get_if("pbprojdir");
64
65if ((defined $pbprojdir) && (defined $pbprojdir->{$ENV{'PBPROJ'}})) {
66 $ENV{'PBPROJDIR'} = $pbprojdir->{$ENV{'PBPROJ'}};
67} else {
68 $ENV{'PBPROJDIR'} = "$ENV{'PBDEFDIR'}/$ENV{'PBPROJ'}";
69}
70
71# Computing the default dir for PBDIR.
72# what we have is PBPROJDIR so work from that.
73# Tree identical between PBCONFDIR and PBROOTDIR on one side and
74# PBPROJDIR and PBDIR on the other side.
75
76my $tmp = $ENV{'PBROOTDIR'};
77$tmp =~ s|^$ENV{'PBCONFDIR'}||;
78
79#
80# Check project cms compliance
81#
82pb_cms_compliant(undef,'PBDIR',"$ENV{'PBPROJDIR'}/$tmp",$pburl->{$ENV{'PBPROJ'}},$pbinit);
83
84if ($scheme =~ /^svn/) {
85 # svnversion more precise than svn info
86 $tmp = `(cd "$ENV{'PBDIR'}" ; svnversion .)`;
87 chomp($tmp);
88 $ENV{'PBREVISION'}=$tmp;
89 $ENV{'PBCMSLOGFILE'}="svn.log";
90} elsif (($scheme eq "file") || ($scheme eq "ftp") || ($scheme eq "http")) {
91 $ENV{'PBREVISION'}="flat";
92 $ENV{'PBCMSLOGFILE'}="flat.log";
93} elsif ($scheme =~ /^cvs/) {
94 # Way too slow
95 #$ENV{'PBREVISION'}=`(cd "$ENV{'PBROOTDIR'}" ; cvs rannotate -f . 2>&1 | awk '{print \$1}' | grep -E '^[0-9]' | cut -d. -f2 |sort -nu | tail -1)`;
96 #chomp($ENV{'PBREVISION'});
97 $ENV{'PBREVISION'}="cvs";
98 $ENV{'PBCMSLOGFILE'}="cvs.log";
99 $ENV{'CVS_RSH'} = "ssh" if ($scheme =~ /ssh/);
100} else {
101 die "cms $scheme unknown";
102}
103
104return($scheme,$pburl->{$ENV{'PBPROJ'}});
105}
106
107=item B<pb_cms_export>
108
109This function exports a CMS content to a directory.
110The first parameter is the URL of the CMS content.
111The second parameter is the directory in which it is locally exposed (result of a checkout).
112The third parameter is the directory where we want to deliver it (result of export).
113
114=cut
115
116sub pb_cms_export {
117
118my $uri = shift;
119my $source = shift;
120my $destdir = shift;
121my $tmp;
122my $tmp1;
123
124my @date = pb_get_date();
125# If it's not flat, then we have a real uri as source
126my ($scheme, $account, $host, $port, $path) = pb_get_uri($uri);
127
128if ($scheme =~ /^svn/) {
129 if (-d $source) {
130 $tmp = $destdir;
131 } else {
132 $tmp = "$destdir/".basename($source);
133 }
134 pb_system("svn export $source $tmp","Exporting $source from SVN to $tmp");
135} elsif ($scheme eq "dir") {
136 pb_system("cp -a $path $destdir","Copying $uri from DIR to $destdir");
137} elsif (($scheme eq "http") || ($scheme eq "ftp")) {
138 my $f = basename($path);
139 unlink "$ENV{'PBTMP'}/$f";
140 if (-x "/usr/bin/wget") {
141 pb_system("/usr/bin/wget -nv -O $ENV{'PBTMP'}/$f $uri"," ");
142 } elsif (-x "/usr/bin/curl") {
143 pb_system("/usr/bin/curl $uri -o $ENV{'PBTMP'}/$f","Downloading $uri with curl to $ENV{'PBTMP'}/$f\n");
144 } else {
145 die "Unable to download $uri.\nNo wget/curl available, please install one of those";
146 }
147 pb_cms_export("file://$ENV{'PBTMP'}/$f",$source,$destdir);
148} elsif ($scheme eq "file") {
149 use File::MimeInfo;
150 my $mm = mimetype($path);
151 pb_log(2,"mimetype: $mm\n");
152 pb_mkdir_p($destdir);
153
154 # Check whether the file is well formed
155 # (containing already a directory with the project-version name)
156 my ($pbwf) = pb_conf_get_if("pbwf");
157 if ((defined $pbwf) && (defined $pbwf->{$ENV{'PBPROJ'}})) {
158 $destdir = dirname($destdir);
159 }
160
161 if ($mm =~ /\/x-bzip-compressed-tar$/) {
162 # tar+bzip2
163 pb_system("cd $destdir ; tar xfj $path","Extracting $path in $destdir");
164 } elsif ($mm =~ /\/x-lzma-compressed-tar$/) {
165 # tar+lzma
166 pb_system("cd $destdir ; tar xfY $path","Extracting $path in $destdir");
167 } elsif ($mm =~ /\/x-compressed-tar$/) {
168 # tar+gzip
169 pb_system("cd $destdir ; tar xfz $path","Extracting $path in $destdir");
170 } elsif ($mm =~ /\/x-tar$/) {
171 # tar
172 pb_system("cd $destdir ; tar xf $path","Extracting $path in $destdir");
173 } elsif ($mm =~ /\/zip$/) {
174 # zip
175 pb_system("cd $destdir ; unzip $path","Extracting $path in $destdir");
176 }
177} elsif ($scheme =~ /^cvs/) {
178 # CVS needs a relative path !
179 my $dir=dirname($destdir);
180 my $base=basename($destdir);
181 # CVS also needs a modules name not a dir
182 #if (-d $source) {
183 $tmp1 = basename($source);
184 #} else {
185 #$tmp1 = dirname($source);
186 #$tmp1 = basename($tmp1);
187 #}
188 my $optcvs = "";
189
190 # If we're working on the CVS itself
191 my $cvstag = basename($ENV{'PBROOTDIR'});
192 my $cvsopt = "";
193 if ($cvstag eq "cvs") {
194 my $pbdate = strftime("%Y-%m-%d %H:%M:%S", @date);
195 $cvsopt = "-D \"$pbdate\"";
196 } else {
197 # we're working on a tag which should be the last part of PBROOTDIR
198 $cvsopt = "-r $cvstag";
199 }
200 pb_system("cd $dir ; cvs -d $account\@$host:$path export $cvsopt -d $base $tmp1","Exporting $tmp1 from $source under CVS to $destdir");
201} else {
202 die "cms $scheme unknown";
203}
204}
205
206=item B<pb_cms_get_uri>
207
208This function is only called with a real CMS system and gives the URL stored in the checked out directory.
209The first parameter is the schema of the CMS systems (svn, cvs, svn+ssh, ...)
210The second parameter is the directory in which it is locally exposed (result of a checkout).
211
212=cut
213
214sub pb_cms_get_uri {
215
216my $scheme = shift;
217my $dir = shift;
218
219my $res = "";
220my $void = "";
221
222if ($scheme =~ /^svn/) {
223 open(PIPE,"LANGUAGE=C svn info $dir |") || return("");
224 while (<PIPE>) {
225 ($void,$res) = split(/^URL:/) if (/^URL:/);
226 }
227 $res =~ s/^\s*//;
228 close(PIPE);
229 chomp($res);
230} elsif ($scheme =~ /^cvs/) {
231 # This path is always the root path of CVS, but we may be below
232 open(FILE,"$dir/CVS/Root") || die "$dir isn't CVS controlled";
233 $res = <FILE>;
234 chomp($res);
235 close(FILE);
236 # Find where we are in the tree
237 my $rdir = $dir;
238 while ((! -d "$rdir/CVSROOT") && ($rdir ne "/")) {
239 $rdir = dirname($rdir);
240 }
241 die "Unable to find a CVSROOT dir in the parents of $dir" if (! -d "$rdir/CVSROOT");
242 #compute our place under that root dir - should be a relative path
243 $dir =~ s|^$rdir||;
244 my $suffix = "";
245 $suffix = "$dir" if ($dir ne "");
246
247 my $prefix = "";
248 if ($scheme =~ /ssh/) {
249 $prefix = "cvs+ssh://";
250 } else {
251 $prefix = "cvs://";
252 }
253 $res = $prefix.$res.$suffix;
254} else {
255 die "cms $scheme unknown";
256}
257pb_log(2,"Found CMS info: $res\n");
258return($res);
259}
260
261=item B<pb_cms_copy>
262
263This function copies a CMS content to another.
264The first parameter is the schema of the CMS systems (svn, cvs, svn+ssh, ...)
265The second parameter is the URL of the original CMS content.
266The third parameter is the URL of the destination CMS content.
267
268Only coded for SVN now.
269
270=cut
271
272sub pb_cms_copy {
273my $scheme = shift;
274my $oldurl = shift;
275my $newurl = shift;
276
277if ($scheme =~ /^svn/) {
278 pb_system("svn copy -m \"Creation of $newurl from $oldurl\" $oldurl $newurl","Copying $oldurl to $newurl ");
279} elsif ($scheme eq "flat") {
280} elsif ($scheme =~ /^cvs/) {
281} else {
282 die "cms $scheme unknown";
283}
284}
285
286=item B<pb_cms_checkout>
287
288This function checks a CMS content out to a directory.
289The first parameter is the schema of the CMS systems (svn, cvs, svn+ssh, ...)
290The second parameter is the URL of the CMS content.
291The third parameter is the directory where we want to deliver it (result of export).
292
293=cut
294
295sub pb_cms_checkout {
296my $scheme = shift;
297my $url = shift;
298my $destination = shift;
299
300if ($scheme =~ /^svn/) {
301 pb_system("svn co $url $destination","Checking out $url to $destination ");
302} elsif (($scheme eq "ftp") || ($scheme eq "http")) {
303 return;
304} elsif ($scheme =~ /^cvs/) {
305 pb_system("cvs co $url $destination","Checking out $url to $destination ");
306} else {
307 die "cms $scheme unknown";
308}
309}
310
311=item B<pb_cms_up>
312
313This function updates a local directory with the CMS content.
314The first parameter is the schema of the CMS systems (svn, cvs, svn+ssh, ...)
315The second parameter is the directory to update.
316
317=cut
318
319sub pb_cms_up {
320my $scheme = shift;
321my $dir = shift;
322
323if ($scheme =~ /^svn/) {
324 pb_system("svn up $dir","Updating $dir");
325} elsif ($scheme eq "flat") {
326} elsif ($scheme =~ /^cvs/) {
327 pb_system("cvs up $dir","Updating $dir");
328} else {
329 die "cms $scheme unknown";
330}
331}
332
333=item B<pb_cms_checkin>
334
335This function updates a CMS content from a local directory.
336The first parameter is the schema of the CMS systems (svn, cvs, svn+ssh, ...)
337The second parameter is the directory to update from.
338The third parameter indicates if we are in a new version creation (undef) or in a new project creation (1)
339
340=cut
341
342sub pb_cms_checkin {
343my $scheme = shift;
344my $dir = shift;
345my $pbinit = shift || undef;
346
347my $ver = basename($dir);
348my $msg = "updated to $ver";
349$msg = "Project $ENV{PBPROJ} creation" if (defined $pbinit);
350
351if ($scheme =~ /^svn/) {
352 pb_system("svn ci -m \"$msg\" $dir","Checking in $dir");
353} elsif ($scheme eq "flat") {
354} elsif ($scheme =~ /^cvs/) {
355 pb_system("cvs ci -m \"$msg\" $dir","Checking in $dir");
356} else {
357 die "cms $scheme unknown";
358}
359pb_cms_up($scheme,$dir);
360}
361
362=item B<pb_cms_add>
363
364This function adds to a CMS content from a local directory.
365The first parameter is the schema of the CMS systems (svn, cvs, svn+ssh, ...)
366The second parameter is the directory/file to add.
367
368=cut
369
370sub pb_cms_add {
371my $scheme = shift;
372my $f = shift;
373
374if ($scheme =~ /^svn/) {
375 pb_system("svn add $f","Adding $f to SVN");
376} elsif ($scheme eq "flat") {
377} elsif ($scheme =~ /^cvs/) {
378 pb_system("cvs add $f","Adding $f to CVS");
379} else {
380 die "cms $scheme unknown";
381}
382pb_cms_up($scheme,$f);
383}
384
385=item B<pb_cms_isdiff>
386
387This function returns a integer indicating the number f differences between the CMS content and the local directory where it's checked out.
388The first parameter is the schema of the CMS systems (svn, cvs, svn+ssh, ...)
389The second parameter is the directory to consider.
390
391=cut
392
393sub pb_cms_isdiff {
394my $scheme = shift;
395my $dir =shift;
396
397if ($scheme =~ /^svn/) {
398 open(PIPE,"svn diff $dir |") || die "Unable to get svn diff from $dir";
399 my $l = 0;
400 while (<PIPE>) {
401 $l++;
402 }
403 return($l);
404} elsif ($scheme eq "flat") {
405} elsif ($scheme =~ /^cvs/) {
406 open(PIPE,"cvs diff $dir |") || die "Unable to get svn diff from $dir";
407 my $l = 0;
408 while (<PIPE>) {
409 # Skipping normal messages
410 next if (/^cvs diff:/);
411 $l++;
412 }
413 return($l);
414} else {
415 die "cms $scheme unknown";
416}
417}
418
419=item B<pb_cms_isdiff>
420
421This function returns the list of packages we are working on in a CMS action.
422The first parameter is the default list of packages from the configuration file.
423The second parameter is the optional list of packages from the configuration file.
424
425=cut
426
427sub pb_cms_get_pkg {
428
429my @pkgs = ();
430my $defpkgdir = shift || undef;
431my $extpkgdir = shift || undef;
432
433# Get packages list
434if (not defined $ARGV[0]) {
435 @pkgs = keys %$defpkgdir if (defined $defpkgdir);
436} elsif ($ARGV[0] =~ /^all$/) {
437 @pkgs = keys %$defpkgdir if (defined $defpkgdir);
438 push(@pkgs, keys %$extpkgdir) if (defined $extpkgdir);
439} else {
440 @pkgs = @ARGV;
441}
442pb_log(0,"Packages: ".join(',',@pkgs)."\n");
443return(\@pkgs);
444}
445
446=item B<pb_cms_compliant>
447
448This function checks the compliance of the project and the pbconf directory.
449The first parameter is the key name of the value that needs to be read in the configuration file.
450The second parameter is the environment variable this key will populate.
451The third parameter is the location of the pbconf dir.
452The fourth parameter is the URI of the CMS content related to the pbconf dir.
453The fifth parameter indicates whether we should inititate the context or not.
454
455=cut
456
457sub pb_cms_compliant {
458
459my $param = shift;
460my $envar = shift;
461my $defdir = shift;
462my $uri = shift;
463my $pbinit = shift;
464my %pdir;
465
466my ($pdir) = pb_conf_get_if($param) if (defined $param);
467if (defined $pdir) {
468 %pdir = %$pdir;
469}
470
471
472if ((defined $pdir) && (%pdir) && (defined $pdir{$ENV{'PBPROJ'}})) {
473 # That's always the environment variable that will be used
474 $ENV{$envar} = $pdir{$ENV{'PBPROJ'}};
475} else {
476 if (defined $param) {
477 pb_log(1,"WARNING: no $param defined, using $defdir\n");
478 pb_log(1," Please create a $param reference for project $ENV{'PBPROJ'} in $ENV{'PBETC'}\n");
479 pb_log(1," if you want to use another directory\n");
480 }
481 $ENV{$envar} = "$defdir";
482}
483
484# Expand potential env variable in it
485eval { $ENV{$envar} =~ s/(\$ENV.+\})/$1/eeg };
486pb_log(2,"$envar: $ENV{$envar}\n");
487
488my ($scheme, $account, $host, $port, $path) = pb_get_uri($uri);
489
490if ((! -d "$ENV{$envar}") || (defined $pbinit)) {
491 if (defined $pbinit) {
492 pb_mkdir_p("$ENV{$envar}");
493 } else {
494 pb_log(1,"Checking out $uri\n");
495 pb_cms_checkout($scheme,$uri,$ENV{$envar});
496 }
497} elsif (($scheme !~ /^cvs/) || ($scheme !~ /^svn/)) {
498 # Do not compare if it's not a real cms
499 return;
500} else {
501 pb_log(1,"$uri found locally, checking content\n");
502 my $cmsurl = pb_cms_get_uri($scheme,$ENV{$envar});
503 my ($scheme2, $account2, $host2, $port2, $path2) = pb_get_uri($cmsurl);
504 if ($cmsurl ne $uri) {
505 # The local content doesn't correpond to the repository
506 pb_log(0,"ERROR: Inconsistency detected:\n");
507 pb_log(0," * $ENV{$envar} refers to $cmsurl but\n");
508 pb_log(0," * $ENV{'PBETC'} refers to $uri\n");
509 die "Project $ENV{'PBPROJ'} is not Project-Builder compliant.";
510 } else {
511 pb_log(1,"Content correct - doing nothing - you may want to update your repository however\n");
512 # they match - do nothing - there may be local changes
513 }
514}
515}
516
517=item B<pb_cms_create_authors>
518
519This function creates a AUTHORS files for the project. It call it AUTHORS.pb if an AUTHORS file already exists.
520The first parameter is the source file for authors information.
521The second parameter is the directory where to create the final AUTHORS file.
522The third parameter is the schema of the CMS systems (svn, cvs, svn+ssh, ...)
523
524=cut
525
526sub pb_cms_create_authors {
527
528my $authors=shift;
529my $dest=shift;
530my $scheme=shift;
531
532return if ($authors eq "/dev/null");
533open(SAUTH,$authors) || die "Unable to open $authors";
534# Save a potentially existing AUTHORS file and write instead to AUTHORS.pb
535my $ext = "";
536if (-f "$dest/AUTHORS") {
537 $ext = ".pb";
538}
539open(DAUTH,"> $dest/AUTHORS$ext") || die "Unable to create $dest/AUTHORS$ext";
540print DAUTH "Authors of the project are:\n";
541print DAUTH "===========================\n";
542while (<SAUTH>) {
543 my ($nick,$gcos) = split(/:/);
544 chomp($gcos);
545 print DAUTH "$gcos";
546 if (defined $scheme) {
547 # Do not give a scheme for flat types
548 my $endstr="";
549 if ("$ENV{'PBREVISION'}" ne "flat") {
550 $endstr = " under $scheme";
551 }
552 print DAUTH " ($nick$endstr)\n";
553 } else {
554 print DAUTH "\n";
555 }
556}
557close(DAUTH);
558close(SAUTH);
559}
560
561=item B<pb_cms_log>
562
563This function creates a ChangeLog file for the project.
564The first parameter is the schema of the CMS systems (svn, cvs, svn+ssh, ...)
565The second parameter is the directory where the CMS content was checked out.
566The third parameter is the directory where to create the final ChangeLog file.
567The fourth parameter is unused.
568The fifth parameter is the source file for authors information.
569
570It may use a tool like svn2cl or cvs2cl to generate it if present, or the log file from the CMS if not.
571
572=cut
573
574
575sub pb_cms_log {
576
577my $scheme = shift;
578my $pkgdir = shift;
579my $dest = shift;
580my $chglog = shift;
581my $authors = shift;
582my $testver = shift || undef;
583
584pb_cms_create_authors($authors,$dest,$scheme);
585
586if ((defined $testver) && (defined $testver->{$ENV{'PBPROJ'}}) && ($testver->{$ENV{'PBPROJ'}} =~ /true/i)) {
587 if (! -f "$dest/ChangeLog") {
588 open(CL,"> $dest/ChangeLog") || die "Unable to create $dest/ChangeLog";
589 # We need a minimal version for debian type of build
590 print CL "\n";
591 print CL "\n";
592 print CL "\n";
593 print CL "\n";
594 print CL "1990-01-01 none\n";
595 print CL "\n";
596 print CL " * test version\n";
597 print CL "\n";
598 close(CL);
599 pb_log(0,"Generating fake ChangeLog for test version\n");
600 open(CL,"> $dest/$ENV{'PBCMSLOGFILE'}") || die "Unable to create $dest/$ENV{'PBCMSLOGFILE'}";
601 close(CL);
602 }
603}
604
605if ($scheme =~ /^svn/) {
606 if (! -f "$dest/ChangeLog") {
607 # In case we have no network, just create an empty one before to allow correct build
608 open(CL,"> $dest/ChangeLog") || die "Unable to create $dest/ChangeLog";
609 close(CL);
610 if (-x "/usr/bin/svn2cl") {
611 pb_system("/usr/bin/svn2cl --group-by-day --authors=$authors -i -o $dest/ChangeLog $pkgdir","Generating ChangeLog from SVN with svn2cl");
612 } else {
613 # To be written from pbcl
614 pb_system("svn log -v $pkgdir > $dest/$ENV{'PBCMSLOGFILE'}","Extracting log info from SVN");
615 }
616 }
617} elsif (($scheme eq "file") || ($scheme eq "dir") || ($scheme eq "http") || ($scheme eq "ftp")) {
618 if (! -f "$dest/ChangeLog") {
619 pb_system("echo ChangeLog for $pkgdir > $dest/ChangeLog","Empty ChangeLog file created");
620 }
621} elsif ($scheme =~ /^cvs/) {
622 my $tmp=basename($pkgdir);
623 # CVS needs a relative path !
624 if (! -f "$dest/ChangeLog") {
625 # In case we have no network, just create an empty one before to allow correct build
626 open(CL,"> $dest/ChangeLog") || die "Unable to create $dest/ChangeLog";
627 close(CL);
628 if (-x "/usr/bin/cvs2cl") {
629 pb_system("/usr/bin/cvs2cl --group-by-day -U $authors -f $dest/ChangeLog $pkgdir","Generating ChangeLog from CVS with cvs2cl");
630 } else {
631 # To be written from pbcl
632 pb_system("cvs log $tmp > $dest/$ENV{'PBCMSLOGFILE'}","Extracting log info from CVS");
633 }
634 }
635} else {
636 die "cms $scheme unknown";
637}
638}
639
640=back
641
642=head1 WEB SITES
643
644The main Web site of the project is available at L<http://www.project-builder.org/>. Bug reports should be filled using the trac instance of the project at L<http://trac.project-builder.org/>.
645
646=head1 USER MAILING LIST
647
648None exists for the moment.
649
650=head1 AUTHORS
651
652The Project-Builder.org team L<http://trac.project-builder.org/> lead by Bruno Cornec L<mailto:bruno@project-builder.org>.
653
654=head1 COPYRIGHT
655
656Project-Builder.org is distributed under the GPL v2.0 license
657described in the file C<COPYING> included with the distribution.
658
659=cut
660
6611;
Note: See TracBrowser for help on using the repository browser.