Extend SConstruct et al. to build .zip files, and to build the scons-src package...
[scons.git] / Construct
1 #
2 # Construct file to build scons during development.
3 # (Kind of ironic that we're using the classic Perl Cons
4 # to build its Python child...)
5 #
6
7 #
8 # Copyright (c) 2001, 2002 Steven Knight
9 #
10 # Permission is hereby granted, free of charge, to any person obtaining
11 # a copy of this software and associated documentation files (the
12 # "Software"), to deal in the Software without restriction, including
13 # without limitation the rights to use, copy, modify, merge, publish,
14 # distribute, sublicense, and/or sell copies of the Software, and to
15 # permit persons to whom the Software is furnished to do so, subject to
16 # the following conditions:
17 #
18 # The above copyright notice and this permission notice shall be included
19 # in all copies or substantial portions of the Software.
20 #
21 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
22 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
23 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 #
29
30 print <<_EOF_;
31
32         Using Cons to build the SCons packages is no longer supported,
33         and this Construct file is deprecated.
34
35         Use SCons to build its own packages instead.
36
37         See the README file for details.
38
39 _EOF_
40 exit 0;
41
42 $project = 'scons';
43
44 Default qw( . );
45
46 #
47 # An internal "whereis" routine to figure out if we have a
48 # given program available.  Put it in the "cons::" package
49 # so subsidiary Conscript files can get at it easily, too.
50 #
51 use Config;
52 sub cons::whereis {
53     my $file = shift;
54     foreach my $dir (split(/$Config{path_sep}/, $ENV{PATH})) {
55         $f = File::Spec->catfile($dir, $file);
56         return $f if -x $f && ! -d $f;
57     }
58     return undef
59 }
60
61 sub cons::read_file {
62     my $file = shift;
63     open(F, "<$file") || die "cannot open $file: $!";
64     my @lines = <F>;
65     close(F);
66     return wantarray ? @lines : join('', @lines);
67 }
68
69 #
70 # We let the presence or absence of various utilities determine
71 # whether or not we bother to build certain pieces of things.
72 # This will allow people to still do SCons work even if they
73 # don't have Aegis or RPM installed, for example.
74 #
75 $aegis = cons::whereis('aegis');
76 $aesub = cons::whereis('aesub');
77 $rpm = cons::whereis('rpm');
78 $dh_builddeb = cons::whereis('dh_builddeb');
79 $fakeroot = cons::whereis('fakeroot');
80
81 # My installation on Red Hat doesn't like any debhelper version
82 # beyond 2, so let's use 2 as the default on any non-Debian build.
83 $DH_COMPAT = (-f "/etc/debian_version") ? 3 : 2;
84
85 #
86 # Now grab the information that we "build" into the files (using sed).
87 #
88 chomp($date = $ARG{date});
89 if (! $date) {
90     ($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
91     $year += 1900;
92     $date = sprintf("%4d/%02d/%02d %02d:%02d:%02d",
93                     $year, $mon, $mday, $hour, $min, $sec);
94 }
95
96 $developer = $ARG{developer} || $ENV{USERNAME} || $ENV{LOGNAME} || $ENV{USER};
97
98 $revision = $ARG{version};
99 chomp($revision = `$aesub '\$version' 2>/dev/null`) if $aesub && ! $revision;
100 $revision = '0.05' if ! $revision;
101
102 @arr = split(/\./, $revision);
103 @arr = ($arr[0], map {length($_) == 1 ? "0$_" : $_} @arr[1 .. $#arr]);
104 $revision = join('.', @arr);
105
106 # Here's how we'd turn the calculated $revision into our package $version.
107 # This makes it difficult to coordinate with other files (debian/changelog
108 # and rpm/scons.spec) that hard-code the version number, so just go with
109 # the flow for now.
110 #pop @arr if $#arr >= 2;
111 #map {s/^[CD]//, s/^0*(\d\d)$/$1/} @arr;
112 #$version = join('.', @arr);
113 $version = '0.05';
114
115 $change = $ARG{change};
116 chomp($change = `$aesub '\$change' 2>/dev/null`) if $aesub && ! $change;
117
118 chomp($python_ver = `python -c "import sys; print sys.version[0:3]"`);
119
120 chomp($platform = `python -c "from distutils.util import get_platform; print get_platform()"`);
121
122 if ($platform eq "win32") {
123     $archsuffix = "zip"
124 } else {
125     $archsuffix = "tar.gz"
126 }
127
128 use Cwd;
129 $test1_dir = File::Spec->catfile(cwd, "build", "test1");
130 $test2_dir = File::Spec->catfile(cwd, "build", "test2");
131
132 $lib_project = File::Spec->catfile("lib", "$project");
133
134 # Originally, we were going to package the build engine in a
135 # private SCons library that contained the version number, so
136 # we could easily have multiple side-by-side versions of SCons
137 # installed.  Keep this around in case we ever want to go back
138 # to that scheme.  Note that this also requires changes to
139 # runtest.py and src/setup.py.
140 #$lib_project = File::Spec->catfile("lib", "$project-$version");
141
142 $test1_lib_dir = File::Spec->catfile($test1_dir, $lib_project);
143
144 $test2_lib_dir = File::Spec->catfile($test2_dir,
145                                      "lib",
146                                      "python${python_ver}",
147                                      "site-packages");
148
149 $unpack_dir = File::Spec->catfile(cwd, "build", "unpack");
150
151 $env = new cons( ENV => {
152                           AEGIS_PROJECT => $ENV{AEGIS_PROJECT},
153                           PATH => $ENV{PATH},
154                         },
155
156                  TEST1_LIB_DIR  => $test1_lib_dir,
157                  TEST2_LIB_DIR  => $test2_lib_dir,
158
159                  DATE           => $date,
160                  DEVELOPER      => $developer,
161                  REVISION       => $revision,
162                  VERSION        => $version,
163
164                  SED            => 'sed',
165                                    # Use %(-%) around the date so date
166                                    # changes don't cause rebuilds.
167                  SEDFLAGS       => " %( -e 's+__DATE__+%DATE+' %)" .
168                                    " -e 's+__DEVELOPER__+%DEVELOPER+'" .
169                                    " -e 's+__FILE__+%<+'" .
170                                    " -e 's+__REVISION__+%REVISION+'" .
171                                    " -e 's+__VERSION__+%VERSION+'",
172                  SEDCOM         => "%SED %SEDFLAGS %< > %>",
173                 );
174
175 #
176 # Define SCons packages.
177 #
178 # In the original, more complicated packaging scheme, we were going
179 # to have separate packages for:
180 #
181 #       python-scons    only the build engine
182 #       scons-script    only the script
183 #       scons           the script plus the build engine
184 #
185 # We're now only delivering a single "scons" package, but this is still
186 # "built" as two sub-packages (the build engine and the script), so
187 # the definitions remain here, even though we're not using them for
188 # separate packages.
189 #
190
191 $python_scons = {
192         'pkg'           => "python-$project",
193         'src_subdir'    => 'engine',
194         'inst_subdir'   => File::Spec->catfile("lib",
195                                                "python1.5",
196                                                "site-packages"),
197         'prefix'        => $test2_dir,
198
199         'debian_deps'   => [ qw(debian/rules debian/control
200                                 debian/changelog debian/copyright
201                                 debian/python-scons.postinst
202                                 debian/python-scons.prerm) ],
203
204         'files'         => [ qw(LICENSE.txt README.txt setup.cfg setup.py) ],
205         'filemap'       => {
206                                 'LICENSE.txt'   => '../LICENSE.txt',
207         }
208 };
209
210 #
211 # The original packaging scheme would have have required us to push
212 # the Python version number into the package name (python1.5-scons,
213 # python2.0-scons, etc.), which would have required a definition
214 # like the following.  Leave this here in case we ever decide to do
215 # this in the future, but note that this would require some modification
216 # to src/engine/setup.py before it would really work.
217 #
218 #$python2_scons = {
219 #        'pkg'          => "python2-$project",
220 #        'src_subdir'   => 'engine',
221 #        'inst_subdir'  => File::Spec->catfile("lib",
222 #                                               "python2.1",
223 #                                               "site-packages"),
224 #        'prefix'       => $test2_dir,
225 #
226 #        'debian_deps'  => [ qw(debian/rules debian/control
227 #                                debian/changelog debian/copyright
228 #                                debian/python2-scons.postinst
229 #                                debian/python2-scons.prerm) ],
230 #
231 #        'files'                => [ qw(LICENSE.txt README.txt setup.cfg setup.py) ],
232 #        'filemap'      => {
233 #                                'LICENSE.txt'  => '../LICENSE.txt',
234 #        }
235 #};
236
237 $scons_script = {
238         'pkg'           => "$project-script",
239         'src_subdir'    => 'script',
240         'inst_subdir'   => 'bin',
241         'prefix'        => $test2_dir,
242
243         'debian_deps'   => [ qw(debian/rules debian/control
244                                 debian/changelog debian/copyright
245                                 debian/python-scons.postinst
246                                 debian/python-scons.prerm) ],
247
248         'files'         => [ qw(LICENSE.txt README.txt setup.cfg setup.py) ],
249         'filemap'               => {
250                                 'LICENSE.txt'   => '../LICENSE.txt',
251                                 'scons'         => 'scons.py',
252                         }
253 };
254
255 $scons = {
256         'pkg'           => $project,
257         'inst_subdir'   => undef,
258         'prefix'        => $test1_dir,
259
260         'debian_deps'   => [ qw(debian/rules debian/control
261                                 debian/changelog debian/copyright
262                                 debian/scons.postinst
263                                 debian/scons.prerm) ],
264
265         'files'         => [ qw(CHANGES.txt LICENSE.txt
266                                 README.txt RELEASE.txt
267                                 os_spawnv_fix.diff scons.1
268                                 script/scons.bat setup.cfg setup.py) ],
269         'filemap'               => {
270                                 'scons.1'       => '../doc/man/scons.1',
271                         },
272
273         'subpkgs'       => [ $python_scons, $scons_script ],
274         'subinst_dirs'  => { "python-$project" => $lib_project,
275                              "$project-script" => 'bin',
276                            },
277 };
278
279 my @src_deps;
280
281 for $p ($scons) {
282     #
283     # Initialize variables with the right directories for this package.
284     #
285     my $pkg = $p->{'pkg'};
286
287     my $src = 'src';
288     $src = File::Spec->catfile($src, $p->{'src_subdir'}) if $p->{'src_subdir'};
289
290     my $build = File::Spec->catfile('build', $pkg);
291
292     my $prefix = $p->{'prefix'};
293     my $install = $prefix;
294     if ($p->{'inst_subdir'}) {
295         $install = File::Spec->catfile($install, $p->{'inst_subdir'});
296     }
297
298     #
299     # Read up the list of source files from our MANIFEST.in.
300     # This list should *not* include LICENSE.txt, MANIFEST,
301     # README.txt, or setup.py.  Make a copy of the list for the
302     # destination files.
303     #
304     my @src_files = cons::read_file("$src/MANIFEST.in");
305     chomp(@src_files);
306     my @dst_files = map(File::Spec->catfile($install, $_), @src_files);
307
308     if ($p->{'subpkgs'}) {
309         #
310         # This package includes some sub-packages.  Read up their
311         # MANIFEST.in files, and add them to our source and destination
312         # file lists, modifying them as appropriate to add the
313         # specified subdirs.
314         #
315         foreach $sp (@{$p->{'subpkgs'}}) {
316             my $ssubdir = $sp->{'src_subdir'};
317             my $isubdir = $p->{'subinst_dirs'}->{$sp->{'pkg'}};
318             my $manifest = File::Spec->catfile($src, $ssubdir, 'MANIFEST.in');
319             my @f = cons::read_file($manifest);
320             chomp(@f);
321             push(@src_files, map(File::Spec->catfile($sp->{'src_subdir'}, $_),
322                                  @f));
323             push(@dst_files, map(File::Spec->catfile($install, $isubdir, $_),
324                                  @f));
325             my $k;
326             foreach $k (keys %{$sp->{'filemap'}}) {
327                 my $f = $sp->{'filemap'}->{$k};
328                 next if ! defined $f;
329                 $k = File::Spec->catfile($sp->{'src_subdir'}, $k); 
330                 $p->{'filemap'}->{$k} = File::Spec->catfile($sp->{'src_subdir'},
331                                                             $f); 
332             }
333         }
334     }
335
336     #
337     # Now that we have the "normal" source files, add those files
338     # that are standard for each distribution.  Note that we don't
339     # add these to dst_files, because they don't get installed.
340     # And we still have the MANIFEST to add.
341     #
342     push(@src_files, @{$p->{'files'}});
343
344     #
345     # Now run everything in src_file through the sed command we
346     # concocted to expand __FILE__, __VERSION__, etc.
347     #
348     foreach $b (@src_files) {
349         my $s = $p->{'filemap'}->{$b} || $b;
350         $env->Command("$build/$b", "$src/$s", "%SEDCOM");
351     }
352
353     #
354     # NOW, finally, we can create the MANIFEST, which we do
355     # by having Perl spit out the contents of the @src_files
356     # array we've carefully created.  After we've added
357     # MANIFEST itself to the array, of course.
358     #
359     push(@src_files, "MANIFEST");
360     $env->Command("$build/MANIFEST", "$src/MANIFEST.in",
361         qq([perl] open(F, ">%>"); print F join("\\n", sort qw(@src_files)), "\\n"; close(F)));
362
363     #
364     # Use the Python distutils to generate the packages.
365     #
366     my $archive = "$build/dist/$pkg-$version.$archsuffix";
367
368     push(@src_deps, $archive);
369
370     my @build_targets = (
371         "$build/dist/$pkg-$version.$platform.$archsuffix",
372         $archive,
373         "$build/dist/$pkg-$version.win32.exe",
374     );
375     my @install_targets = @build_targets;
376
377     # We can get away with calling setup.py using a directory path
378     # like this because we put a preamble in it that will chdir()
379     # to the directory in which setup.py exists.
380     my @bdist_dirs = ("$build/build/lib", "$build/build/scripts");
381     my $commands = qq(rm -rf @bdist_dirs && python $build/setup.py bdist
382                       python $build/setup.py sdist
383                       python $build/setup.py bdist_wininst);
384
385     if ($rpm) {
386         chomp($cwd = `pwd`);
387         $topdir = "$cwd/$build/build/bdist.$platform/rpm";
388
389         $BUILDdir = "$topdir/BUILD/$pkg-$version";
390         $RPMSdir = "$topdir/RPMS/noarch";
391         $SOURCESdir = "$topdir/SOURCES";
392         $SPECSdir = "$topdir/SPECS";
393         $SRPMSdir = "$topdir/SRPMS";
394
395         $specfile = "$SPECSdir/$pkg-$version-1.spec";
396         $sourcefile = "$SOURCESdir/$pkg-$version.$archsuffix";
397         $rpm = "$RPMSdir/$pkg-$version-1.noarch.rpm";
398         $src_rpm = "$SRPMSdir/$pkg-$version-1.src.rpm";
399
400         # We'd like to use the following here:
401         #
402         #       $env->InstallAs($specfile, "rpm/$pkg.spec");
403         #       $env->InstallAs($sourcefile, $archive);
404         #
405         # but it looks like InstallAs doesn't propogate the
406         # signatures correctly, which means that the RPM file
407         # wouldn't always get rebuilt when it should.  Work
408         # around it.
409         use File::Copy;
410         $env->Command($specfile, "rpm/$pkg.spec",
411                       "[perl] File::Copy::copy('%<', '%>')");
412         $env->Command($sourcefile, $archive,
413                       "[perl] File::Copy::copy('%<', '%>')");
414
415         if (! -d $BUILDdir) {
416             $cmd = "mkdir -p $BUILDdir; ";
417         }
418         my @targets = ( $rpm, $src_rpm );
419         $env->Command(\@targets, $specfile,
420                         "${cmd}rpm --define '_topdir $topdir' -ba %<");
421         $env->Depends(\@targets, $sourcefile);
422
423         push(@install_targets, @targets);
424     };
425
426     @build_src_files = map("$build/$_", @src_files);
427
428     if ($dh_builddeb && $fakeroot) {
429         # Debian builds directly into build/dist, so we don't
430         # need to add the .debs to the install_targets.
431         my $deb = "build/dist/${pkg}_$version-1_all.deb";
432         $env->Command($deb, @build_src_files, qq(
433                 fakeroot make -f debian/rules VERSION=%VERSION DH_COMPAT=$DH_COMPAT ENVOKED_BY_CONSTRUCT=1 binary-$pkg
434                 env DH_COMPAT=$DH_COMPAT dh_clean));
435         $env->Depends($deb, @{$p->{'debian_deps'}});
436     }
437
438     #
439     # Now set up creation and installation of the packages.
440     #
441     $env->Command([@build_targets], @build_src_files, $commands);
442
443     $env->Install("build/dist", @install_targets);
444
445     #
446     # Unpack the archive created by the distutils into build/unpack.
447     #
448     my @unpack_files = map("$unpack_dir/$pkg-$version/$_", @src_files);
449
450     # We'd like to replace the last three lines with the following:
451     #
452     #   tar zxf %< -C $unpack_dir
453     #
454     # but that gives heartburn to Cygwin's tar, so work around it
455     # with separate zcat-tar-rm commands.
456     Command $env [@unpack_files], $archive, qq(
457         rm -rf $unpack_dir/$pkg-$version
458         zcat %< > .temp
459         tar xf .temp -C $unpack_dir
460         rm -f .temp
461     );
462
463     #
464     # Run setup.py in the unpacked subdirectory to "install" everything
465     # into our build/test subdirectory.  Auxiliary modules that we need
466     # (TestCmd.py, TestSCons.py, unittest.py) will be copied in by
467     # etc/Conscript.  The runtest.py script will set PYTHONPATH so that
468     # the tests only look under build/test.  This makes sure that our
469     # tests pass with what we really packaged, not because of something
470     # hanging around in the development directory.
471     #
472     # We can get away with calling setup.py using a directory path
473     # like this because we put a preamble in it that will chdir()
474     # to the directory in which setup.py exists.
475     Command $env [@dst_files], @unpack_files, qq(
476         rm -rf $install
477         python $unpack_dir/$pkg-$version/setup.py install --prefix=$prefix
478     );
479 }
480
481 #
482 # Arrange for supporting packages to be installed in the test directories.
483 #
484 Export qw( env );
485
486 Build "etc/Conscript";
487
488 #
489 # Documentation.
490 #
491 Link 'build/doc' => 'doc';
492
493 Build 'build/doc/Conscript';
494
495
496 #
497 # If we're running in the actual Aegis project, pack up a complete
498 # source archive from the project files and files in the change,
499 # so we can share it with helpful developers who don't use Aegis.
500 #
501 # First, lie and say that we've seen any files removed by this
502 # change, so they don't get added to the source files list
503 # that goes into the archive.
504 #
505
506 if ($change) {
507     foreach (`aegis -list -unf -c $change cf 2>/dev/null`) {
508         $seen{"$1\n"}++ if /^(?:source|test) remove(?:\s.*)+\s(\S+)$/;
509     }
510
511     eval '@src_files = grep(! $seen{$_}++,
512                     `aegis -list -terse pf 2>/dev/null`,
513                     `aegis -list -terse cf 2>/dev/null`)';
514
515     @src_files = grep($_ !~ /(\.aeignore|\.consign)$/, @src_files);
516
517     if (@src_files) {
518         chomp(@src_files);
519
520         foreach $file (@src_files) {
521             $env->Command("build/$project-src/$file", 
522                           $file, 
523                           qq(%SEDCOM
524                              chmod --reference=%< %>)
525             );
526         }
527
528         $env->Command("build/dist/$project-src-$version.tar.gz",
529                       @src_deps,
530                       map("build/$project-src/$_", @src_files),
531                       qq(
532                 rm -rf build/$project-src-$version
533                 cp -rp build/$project-src build/$project-src-$version
534                 find build/$project-src-$version -name .consign -exec rm {} \\;
535                 tar zcf %> -C build $project-src-$version
536         ));
537     }
538 }