# # Construct file to build scons during development. # (Kind of ironic that we're using the classic Perl Cons # to build its Python child...) # # # Copyright (c) 2001 Steven Knight # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # $project = 'scons'; Default qw( . ); # # An internal "whereis" routine to figure out if we have a # given program available. Put it in the "cons::" package # so subsidiary Conscript files can get at it easily, too. # use Config; sub cons::whereis { my $file = shift; foreach my $dir (split(/$Config{path_sep}/, $ENV{PATH})) { $f = File::Spec->catfile($dir, $file); return $f if -x $f && ! -d $f; } return undef } sub cons::read_file { my $file = shift; open(F, "<$file") || die "cannot open $file: $!"; my @lines = ; close(F); return wantarray ? @lines : join('', @lines); } # # We let the presence or absence of various utilities determine # whether or not we bother to build certain pieces of things. # This will allow people to still do SCons work even if they # don't have Aegis or RPM installed, for example. # $aegis = cons::whereis('aegis'); $aesub = cons::whereis('aesub'); $rpm = cons::whereis('rpm'); $dh_builddeb = cons::whereis('dh_builddeb'); $fakeroot = cons::whereis('fakeroot'); # My installation on Red Hat doesn't like any debhelper version # beyond 2, so let's use 2 as the default on any non-Debian build. $DH_COMPAT = (-f "/etc/debian_version") ? 3 : 2; # # Now grab the information that we "build" into the files (using sed). # chomp($date = $ARG{date}); if (! $date) { ($sec,$min,$hour,$mday,$mon,$year) = localtime(time); $year += 1900; $date = sprintf("%4d/%02d/%02d %02d:%02d:%02d", $year, $mon, $mday, $hour, $min, $sec); } $developer = $ARG{developer} || $ENV{USERNAME} || $ENV{LOGNAME} || $ENV{USER}; $revision = $ARG{version}; chomp($revision = `$aesub '\$version' 2>/dev/null`) if $aesub && ! $revision; $revision = '0.04' if ! $revision; @arr = split(/\./, $revision); @arr = ($arr[0], map {length($_) == 1 ? "0$_" : $_} @arr[1 .. $#arr]); $revision = join('.', @arr); # Here's how we'd turn the calculated $revision into our package $version. # This makes it difficult to coordinate with other files (debian/changelog # and rpm/scons.spec) that hard-code the version number, so just go with # the flow for now. #pop @arr if $#arr >= 2; #map {s/^[CD]//, s/^0*(\d\d)$/$1/} @arr; #$version = join('.', @arr); $version = '0.04'; $change = $ARG{change}; chomp($change = `$aesub '\$change' 2>/dev/null`) if $aesub && ! $change; chomp($python_ver = `python -c "import sys; print sys.version[0:3]"`); chomp($platform = `python -c "from distutils.util import get_platform; print get_platform()"`); if ($platform eq "win32") { $archsuffix = "zip" } else { $archsuffix = "tar.gz" } use Cwd; $test1_dir = File::Spec->catfile(cwd, "build", "test1"); $test2_dir = File::Spec->catfile(cwd, "build", "test2"); $lib_project = File::Spec->catfile("lib", "$project"); # Originally, we were going to package the build engine in a # private SCons library that contained the version number, so # we could easily have multiple side-by-side versions of SCons # installed. Keep this around in case we ever want to go back # to that scheme. Note that this also requires changes to # runtest.py and src/setup.py. #$lib_project = File::Spec->catfile("lib", "$project-$version"); $test1_lib_dir = File::Spec->catfile($test1_dir, $lib_project); $test2_lib_dir = File::Spec->catfile($test2_dir, "lib", "python${python_ver}", "site-packages"); $unpack_dir = File::Spec->catfile(cwd, "build", "unpack"); $env = new cons( ENV => { AEGIS_PROJECT => $ENV{AEGIS_PROJECT}, PATH => $ENV{PATH}, }, TEST1_LIB_DIR => $test1_lib_dir, TEST2_LIB_DIR => $test2_lib_dir, DATE => $date, DEVELOPER => $developer, REVISION => $revision, VERSION => $version, SED => 'sed', # Use %(-%) around the date so date # changes don't cause rebuilds. SEDFLAGS => " %( -e 's+__DATE__+%DATE+' %)" . " -e 's+__DEVELOPER__+%DEVELOPER+'" . " -e 's+__FILE__+%<+'" . " -e 's+__REVISION__+%REVISION+'" . " -e 's+__VERSION__+%VERSION+'", SEDCOM => "%SED %SEDFLAGS %< > %>", ); # # Define SCons packages. # # In the original, more complicated packaging scheme, we were going # to have separate packages for: # # python-scons only the build engine # scons-script only the script # scons the script plus the build engine # # We're now only delivering a single "scons" package, but this is still # "built" as two sub-packages (the build engine and the script), so # the definitions remain here, even though we're not using them for # separate packages. # $python_scons = { 'pkg' => "python-$project", 'src_subdir' => 'engine', 'inst_subdir' => File::Spec->catfile("lib", "python1.5", "site-packages"), 'prefix' => $test2_dir, 'debian_deps' => [ qw(debian/rules debian/control debian/changelog debian/copyright debian/python-scons.postinst debian/python-scons.prerm) ], 'files' => [ qw(LICENSE.txt README.txt setup.cfg setup.py) ], 'filemap' => { 'LICENSE.txt' => '../LICENSE.txt', } }; # # The original packaging scheme would have have required us to push # the Python version number into the package name (python1.5-scons, # python2.0-scons, etc.), which would have required a definition # like the following. Leave this here in case we ever decide to do # this in the future, but note that this would require some modification # to src/engine/setup.py before it would really work. # #$python2_scons = { # 'pkg' => "python2-$project", # 'src_subdir' => 'engine', # 'inst_subdir' => File::Spec->catfile("lib", # "python2.1", # "site-packages"), # 'prefix' => $test2_dir, # # 'debian_deps' => [ qw(debian/rules debian/control # debian/changelog debian/copyright # debian/python2-scons.postinst # debian/python2-scons.prerm) ], # # 'files' => [ qw(LICENSE.txt README.txt setup.cfg setup.py) ], # 'filemap' => { # 'LICENSE.txt' => '../LICENSE.txt', # } #}; $scons_script = { 'pkg' => "$project-script", 'src_subdir' => 'script', 'inst_subdir' => 'bin', 'prefix' => $test2_dir, 'debian_deps' => [ qw(debian/rules debian/control debian/changelog debian/copyright debian/python-scons.postinst debian/python-scons.prerm) ], 'files' => [ qw(LICENSE.txt README.txt setup.cfg setup.py) ], 'filemap' => { 'LICENSE.txt' => '../LICENSE.txt', 'scons' => 'scons.py', } }; $scons = { 'pkg' => $project, 'inst_subdir' => undef, 'prefix' => $test1_dir, 'debian_deps' => [ qw(debian/rules debian/control debian/changelog debian/copyright debian/scons.postinst debian/scons.prerm) ], 'files' => [ qw(CHANGES.txt LICENSE.txt README.txt RELEASE.txt os_spawnv_fix.diff scons.1 script/scons.bat setup.cfg setup.py) ], 'filemap' => { 'scons.1' => '../doc/man/scons.1', }, 'subpkgs' => [ $python_scons, $scons_script ], 'subinst_dirs' => { "python-$project" => $lib_project, "$project-script" => 'bin', }, }; my @src_deps; for $p ($scons) { # # Initialize variables with the right directories for this package. # my $pkg = $p->{'pkg'}; my $src = 'src'; $src = File::Spec->catfile($src, $p->{'src_subdir'}) if $p->{'src_subdir'}; my $build = File::Spec->catfile('build', $pkg); my $prefix = $p->{'prefix'}; my $install = $prefix; if ($p->{'inst_subdir'}) { $install = File::Spec->catfile($install, $p->{'inst_subdir'}); } # # Read up the list of source files from our MANIFEST.in. # This list should *not* include LICENSE.txt, MANIFEST, # README.txt, or setup.py. Make a copy of the list for the # destination files. # my @src_files = cons::read_file("$src/MANIFEST.in"); chomp(@src_files); my @dst_files = map(File::Spec->catfile($install, $_), @src_files); if ($p->{'subpkgs'}) { # # This package includes some sub-packages. Read up their # MANIFEST.in files, and add them to our source and destination # file lists, modifying them as appropriate to add the # specified subdirs. # foreach $sp (@{$p->{'subpkgs'}}) { my $ssubdir = $sp->{'src_subdir'}; my $isubdir = $p->{'subinst_dirs'}->{$sp->{'pkg'}}; my $manifest = File::Spec->catfile($src, $ssubdir, 'MANIFEST.in'); my @f = cons::read_file($manifest); chomp(@f); push(@src_files, map(File::Spec->catfile($sp->{'src_subdir'}, $_), @f)); push(@dst_files, map(File::Spec->catfile($install, $isubdir, $_), @f)); my $k; foreach $k (keys %{$sp->{'filemap'}}) { my $f = $sp->{'filemap'}->{$k}; next if ! defined $f; $k = File::Spec->catfile($sp->{'src_subdir'}, $k); $p->{'filemap'}->{$k} = File::Spec->catfile($sp->{'src_subdir'}, $f); } } } # # Now that we have the "normal" source files, add those files # that are standard for each distribution. Note that we don't # add these to dst_files, because they don't get installed. # And we still have the MANIFEST to add. # push(@src_files, @{$p->{'files'}}); # # Now run everything in src_file through the sed command we # concocted to expand __FILE__, __VERSION__, etc. # foreach $b (@src_files) { my $s = $p->{'filemap'}->{$b} || $b; $env->Command("$build/$b", "$src/$s", "%SEDCOM"); } # # NOW, finally, we can create the MANIFEST, which we do # by having Perl spit out the contents of the @src_files # array we've carefully created. After we've added # MANIFEST itself to the array, of course. # push(@src_files, "MANIFEST"); $env->Command("$build/MANIFEST", "$src/MANIFEST.in", qq([perl] open(F, ">%>"); print F join("\\n", sort qw(@src_files)), "\\n"; close(F))); # # Use the Python distutils to generate the packages. # my $archive = "$build/dist/$pkg-$version.$archsuffix"; push(@src_deps, $archive); my @build_targets = ( "$build/dist/$pkg-$version.$platform.$archsuffix", $archive, "$build/dist/$pkg-$version.win32.exe", ); my @install_targets = @build_targets; # We can get away with calling setup.py using a directory path # like this because we put a preamble in it that will chdir() # to the directory in which setup.py exists. my @bdist_dirs = ("$build/build/lib", "$build/build/scripts"); my $commands = qq(rm -rf @bdist_dirs && python $build/setup.py bdist python $build/setup.py sdist python $build/setup.py bdist_wininst); if ($rpm) { chomp($cwd = `pwd`); $topdir = "$cwd/$build/build/bdist.$platform/rpm"; $BUILDdir = "$topdir/BUILD/$pkg-$version"; $RPMSdir = "$topdir/RPMS/noarch"; $SOURCESdir = "$topdir/SOURCES"; $SPECSdir = "$topdir/SPECS"; $SRPMSdir = "$topdir/SRPMS"; $specfile = "$SPECSdir/$pkg-$version-1.spec"; $sourcefile = "$SOURCESdir/$pkg-$version.$archsuffix"; $rpm = "$RPMSdir/$pkg-$version-1.noarch.rpm"; $src_rpm = "$SRPMSdir/$pkg-$version-1.src.rpm"; # We'd like to use the following here: # # $env->InstallAs($specfile, "rpm/$pkg.spec"); # $env->InstallAs($sourcefile, $archive); # # but it looks like InstallAs doesn't propogate the # signatures correctly, which means that the RPM file # wouldn't always get rebuilt when it should. Work # around it. use File::Copy; $env->Command($specfile, "rpm/$pkg.spec", "[perl] File::Copy::copy('%<', '%>')"); $env->Command($sourcefile, $archive, "[perl] File::Copy::copy('%<', '%>')"); if (! -d $BUILDdir) { $cmd = "mkdir -p $BUILDdir; "; } my @targets = ( $rpm, $src_rpm ); $env->Command(\@targets, $specfile, "${cmd}rpm --define '_topdir $topdir' -ba %<"); $env->Depends(\@targets, $sourcefile); push(@install_targets, @targets); }; @build_src_files = map("$build/$_", @src_files); if ($dh_builddeb && $fakeroot) { # Debian builds directly into build/dist, so we don't # need to add the .debs to the install_targets. my $deb = "build/dist/${pkg}_$version-1_all.deb"; $env->Command($deb, @build_src_files, qq( fakeroot make -f debian/rules VERSION=%VERSION DH_COMPAT=$DH_COMPAT ENVOKED_BY_CONSTRUCT=1 binary-$pkg env DH_COMPAT=$DH_COMPAT dh_clean)); $env->Depends($deb, @{$p->{'debian_deps'}}); } # # Now set up creation and installation of the packages. # $env->Command([@build_targets], @build_src_files, $commands); $env->Install("build/dist", @install_targets); # # Unpack the archive created by the distutils into build/unpack. # my @unpack_files = map("$unpack_dir/$pkg-$version/$_", @src_files); # We'd like to replace the last three lines with the following: # # tar zxf %< -C $unpack_dir # # but that gives heartburn to Cygwin's tar, so work around it # with separate zcat-tar-rm commands. Command $env [@unpack_files], $archive, qq( rm -rf $unpack_dir/$pkg-$version zcat %< > .temp tar xf .temp -C $unpack_dir rm -f .temp ); # # Run setup.py in the unpacked subdirectory to "install" everything # into our build/test subdirectory. Auxiliary modules that we need # (TestCmd.py, TestSCons.py, unittest.py) will be copied in by # etc/Conscript. The runtest.py script will set PYTHONPATH so that # the tests only look under build/test. This makes sure that our # tests pass with what we really packaged, not because of something # hanging around in the development directory. # # We can get away with calling setup.py using a directory path # like this because we put a preamble in it that will chdir() # to the directory in which setup.py exists. Command $env [@dst_files], @unpack_files, qq( rm -rf $install python $unpack_dir/$pkg-$version/setup.py install --prefix=$prefix ); } # # Arrange for supporting packages to be installed in the test directories. # Export qw( env ); Build "etc/Conscript"; # # Documentation. # Link 'build/doc' => 'doc'; Build 'build/doc/Conscript'; # # If we're running in the actual Aegis project, pack up a complete # source archive from the project files and files in the change, # so we can share it with helpful developers who don't use Aegis. # # First, lie and say that we've seen any files removed by this # change, so they don't get added to the source files list # that goes into the archive. # if ($change) { foreach (`aegis -list -unf -c $change cf 2>/dev/null`) { $seen{"$1\n"}++ if /^(?:source|test) remove(?:\s.*)+\s(\S+)$/; } eval '@src_files = grep(! $seen{$_}++, `aegis -list -terse pf 2>/dev/null`, `aegis -list -terse cf 2>/dev/null`)'; @src_files = grep($_ !~ /(\.aeignore|\.consign)$/, @src_files); if (@src_files) { chomp(@src_files); foreach $file (@src_files) { $env->Command("build/$project-src/$file", $file, qq(%SEDCOM chmod --reference=%< %>) ); } $env->Command("build/dist/$project-src-$version.tar.gz", @src_deps, map("build/$project-src/$_", @src_files), qq( rm -rf build/$project-src-$version cp -rp build/$project-src build/$project-src-$version find build/$project-src-$version -name .consign -exec rm {} \\; tar zcf %> -C build $project-src-$version )); } }