Fix SCon{struct,script} files to build SCons with itself.
[scons.git] / SConstruct
1 #
2 # SConstruct file to build scons during development.
3 #
4 # THIS IS NOT READY YET.  DO NOT TRY TO BUILD SCons WITH ITSELF YET.
5 #
6
7 #
8 # Copyright (c) 2001 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 import distutils.util
31 import os
32 import os.path
33 import stat
34 import string
35 import sys
36 import time
37
38 project = 'scons'
39
40 Default('.')
41
42 #
43 # An internal "whereis" routine to figure out if we have a
44 # given program available.  Put it in the "cons::" package
45 # so subsidiary Conscript files can get at it easily, too.
46 #
47
48 def whereis(file):
49     for dir in string.split(os.environ['PATH'], os.pathsep):
50         f = os.path.join(dir, file)
51         try:
52             st = os.stat(f)
53         except:
54             continue
55         if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
56             return f
57     return None
58
59 #
60 # We let the presence or absence of various utilities determine
61 # whether or not we bother to build certain pieces of things.
62 # This will allow people to still do SCons work even if they
63 # don't have Aegis or RPM installed, for example.
64 #
65 aegis = whereis('aegis')
66 aesub = whereis('aesub')
67 rpm = whereis('rpm')
68 dh_builddeb = whereis('dh_builddeb')
69 fakeroot = whereis('fakeroot')
70
71 # My installation on Red Hat doesn't like any debhelper version
72 # beyond 2, so let's use 2 as the default on any non-Debian build.
73 if os.path.isfile('/etc/debian_version'):
74     dh_compat = 3
75 else:
76     dh_compat = 2
77
78 #
79 # Now grab the information that we "build" into the files (using sed).
80 #
81 try:
82     date = ARGUMENTS['date']
83 except:
84     date = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(time.time()))
85     
86 if ARGUMENTS.has_key('developer'):
87     developer = ARGUMENTS['developer']
88 elif os.environ.has_key('USERNAME'):
89     developer = os.environ['USERNAME']
90 elif os.environ.has_key('LOGNAME'):
91     developer = os.environ['LOGNAME']
92 elif os.environ.has_key('USER'):
93     developer = os.environ['USER']
94
95 try:
96     revision = ARGUMENTS['version']
97 except:
98     if aesub:
99         revision = os.popen(aesub + " \\$version", "r").read()[:-1]
100     else:
101         revision = '0.04'
102
103 a = string.split(revision, '.')
104 arr = [a[0]]
105 for s in a[1:]:
106     if len(s) == 1:
107         s = '0' + s
108     arr.append(s)
109 revision = string.join(arr, '.')
110
111 # Here's how we'd turn the calculated $revision into our package $version.
112 # This makes it difficult to coordinate with other files (debian/changelog
113 # and rpm/scons.spec) that hard-code the version number, so just go with
114 # the flow for now and hard code it here, too.
115 #if len(arr) >= 2:
116 #    arr = arr[:-1]
117 #def xxx(str):
118 #    if str[0] == 'C' or str[0] == 'D':
119 #        str = str[1:]
120 #    while len(str) > 2 and str[0] == '0':
121 #        str = str[1:]
122 #    return str
123 #arr = map(lambda x, xxx=xxx: xxx(x), arr)
124 #version = string.join(arr, '.')
125 version = '0.04'
126
127 try:
128     change = ARGUMENTS['change']
129 except:
130     if aesub:
131         change = os.popen(aesub + " \\$change", "r").read()[:-1]
132     else:
133         change = '0.04'
134
135 python_ver = sys.version[0:3]
136
137 platform = distutils.util.get_platform()
138
139 if platform == "win32":
140     archsuffix = "zip"
141 else:
142     archsuffix = "tar.gz"
143
144 ENV = { 'PATH' : os.environ['PATH'] }
145 if os.environ.has_key('AEGIS_PROJECT'):
146     ENV['AEGIS_PROJECT'] = os.environ['AEGIS_PROJECT']
147
148 test1_dir = os.path.join(os.getcwd(), "build", "test1")
149 test2_dir = os.path.join(os.getcwd(), "build", "test2")
150
151 lib_project = os.path.join("lib", project)
152
153 # Originally, we were going to package the build engine in a
154 # private SCons library that contained the version number, so
155 # we could easily have multiple side-by-side versions of SCons
156 # installed.  Keep this around in case we ever want to go back
157 # to that scheme.  Note that this also requires changes to
158 # runtest.py and src/setup.py.
159 #lib_project = os.path.join("lib", project + '-' + version)
160
161 test1_lib_dir = os.path.join(test1_dir, lib_project)
162
163 test2_lib_dir = os.path.join(test2_dir,
164                              "lib",
165                              "python" + python_ver,
166                              "site-packages")
167
168 unpack_dir = os.path.join(os.getcwd(), "build", "unpack")
169
170 env = Environment(
171                    ENV           = ENV,
172
173                    TEST1_LIB_DIR = test1_lib_dir,
174                    TEST2_LIB_DIR = test2_lib_dir,
175  
176                    DATE          = date,
177                    DEVELOPER     = developer,
178                    REVISION      = revision,
179                    VERSION       = version,
180                    DH_COMPAT     = dh_compat,
181  
182                    SED           = 'sed',
183                    SEDFLAGS      = "$( -e 's+__DATE__+$DATE+' $)" + \
184                                    " -e 's+__DEVELOPER__+$DEVELOPER+'" + \
185                                    " -e 's+__FILE__+$SOURCES'+" + \
186                                    " -e 's+__REVISION__+$REVISION'+" + \
187                                    " -e 's+__VERSION__+$VERSION'+",
188                    SEDCOM        = '$SED $SEDFLAGS $SOURCES > $TARGET',
189                  )
190
191 #
192 # Define SCons packages.
193 #
194 # In the original, more complicated packaging scheme, we were going
195 # to have separate packages for:
196 #
197 #       python-scons    only the build engine
198 #       scons-script    only the script
199 #       scons           the script plus the build engine
200 #
201 # We're now only delivering a single "scons" package, but this is still
202 # "built" as two sub-packages (the build engine and the script), so
203 # the definitions remain here, even though we're not using them for
204 # separate packages.
205 #
206
207 python_scons = {
208         'pkg'           : 'python-' + project,
209         'src_subdir'    : 'engine',
210         'inst_subdir'   : os.path.join('lib', 'python1.5', 'site-packages'),
211         'prefix'        : test2_dir,
212
213         'debian_deps'   : [
214                             'debian/rules',
215                             'debian/control',
216                             'debian/changelog',
217                             'debian/copyright',
218                             'debian/python-scons.postinst',
219                             'debian/python-scons.prerm',
220                           ],
221
222         'files'         : [ 'LICENSE.txt',
223                             'README.txt',
224                             'setup.cfg',
225                             'setup.py',
226                           ],
227
228         'filemap'       : {
229                             'LICENSE.txt' : '../LICENSE.txt'
230                           },
231 }
232
233 #
234 # The original packaging scheme would have have required us to push
235 # the Python version number into the package name (python1.5-scons,
236 # python2.0-scons, etc.), which would have required a definition
237 # like the following.  Leave this here in case we ever decide to do
238 # this in the future, but note that this would require some modification
239 # to src/engine/setup.py before it would really work.
240 #
241 #python2_scons = {
242 #        'pkg'          : 'python2-' + project,
243 #        'src_subdir'   : 'engine',
244 #        'inst_subdir'  : os.path.join('lib', 'python2.1', 'site-packages'),
245 #        'prefix'       : test2_dir,
246 #
247 #        'debian_deps'  : [
248 #                           'debian/rules',
249 #                           'debian/control',
250 #                           'debian/changelog',
251 #                           'debian/copyright',
252 #                           'debian/python2-scons.postinst',
253 #                           'debian/python2-scons.prerm',
254 #                          ],
255 #
256 #        'files'        : [
257 #                            'LICENSE.txt',
258 #                            'README.txt',
259 #                            'setup.cfg',
260 #                            'setup.py',
261 #                          ],
262 #        'filemap'      : {
263 #                            'LICENSE.txt' : '../LICENSE.txt',
264 #                          },
265 #}
266 #
267
268 scons_script = {
269         'pkg'           : project + '-script',
270         'src_subdir'    : 'script',
271         'inst_subdir'   : 'bin',
272         'prefix'        : test2_dir,
273
274         'debian_deps'   : [
275                             'debian/rules',
276                             'debian/control',
277                             'debian/changelog',
278                             'debian/copyright',
279                             'debian/python-scons.postinst',
280                             'debian/python-scons.prerm',
281                           ],
282
283         'files'         : [
284                             'LICENSE.txt',
285                             'README.txt',
286                             'setup.cfg',
287                             'setup.py',
288                           ],
289
290         'filemap'       : {
291                             'LICENSE.txt' : '../LICENSE.txt',
292                             'scons'       : 'scons.py',
293                            }
294 }
295
296 scons = {
297         'pkg'           : project,
298         #'inst_subdir'   : None,
299         'prefix'        : test1_dir,
300
301         'debian_deps'   : [ 
302                             'debian/rules',
303                             'debian/control',
304                             'debian/changelog',
305                             'debian/copyright',
306                             'debian/scons.postinst',
307                             'debian/scons.prerm',
308                           ],
309
310         'files'         : [ 
311                             'CHANGES.txt',
312                             'LICENSE.txt',
313                             'README.txt',
314                             'RELEASE.txt',
315                             'os_spawnv_fix.diff',
316                             'scons.1',
317                             'script/scons.bat',
318                             'setup.cfg',
319                             'setup.py',
320                           ],
321
322         'filemap'       : {
323                             'scons.1' : '../doc/man/scons.1',
324                           },
325
326         'subpkgs'       : [ python_scons, scons_script ],
327
328         'subinst_dirs'  : {
329                              'python-' + project : lib_project,
330                              project + '-script' : 'bin',
331                            },
332 }
333
334 src_deps = []
335 src_files = []
336
337 for p in [ scons ]:
338     #
339     # Initialize variables with the right directories for this package.
340     #
341     pkg = p['pkg']
342
343     src = 'src'
344     if p.has_key('src_subdir'):
345         src = os.path.join(src, p['src_subdir'])
346
347     build = os.path.join('build', pkg)
348
349     prefix = p['prefix']
350     install = prefix
351     if p.has_key('inst_subdir'):
352         install = os.path.join(install, p['inst_subdir'])
353
354     #
355     # Read up the list of source files from our MANIFEST.in.
356     # This list should *not* include LICENSE.txt, MANIFEST,
357     # README.txt, or setup.py.  Make a copy of the list for the
358     # destination files.
359     #
360     global src_files
361     src_files = map(lambda x: x[:-1],
362                     open(os.path.join(src, 'MANIFEST.in')).readlines())
363     dst_files = map(lambda x: os.path.join(install, x), src_files)
364
365     if p.has_key('subpkgs'):
366         #
367         # This package includes some sub-packages.  Read up their
368         # MANIFEST.in files, and add them to our source and destination
369         # file lists, modifying them as appropriate to add the
370         # specified subdirs.
371         #
372         for sp in p['subpkgs']:
373             ssubdir = sp['src_subdir']
374             isubdir = p['subinst_dirs'][sp['pkg']]
375             f = map(lambda x: x[:-1],
376                     open(os.path.join(src, ssubdir, 'MANIFEST.in')).readlines())
377             src_files.extend(map(lambda x, s=sp['src_subdir']:
378                                         os.path.join(s, x),
379                                  f))
380             dst_files.extend(map(lambda x, i=install, s=isubdir:
381                                         os.path.join(i, s, x),
382                                  f))
383             for k in sp['filemap'].keys():
384                 f = sp['filemap'][k]
385                 if f:
386                     k = os.path.join(sp['src_subdir'], k)
387                     p['filemap'][k] = os.path.join(sp['src_subdir'], f)
388
389     #
390     # Now that we have the "normal" source files, add those files
391     # that are standard for each distribution.  Note that we don't
392     # add these to dst_files, because they don't get installed.
393     # And we still have the MANIFEST to add.
394     #
395     src_files.extend(p['files'])
396
397     #
398     # Now run everything in src_file through the sed command we
399     # concocted to expand __FILE__, __VERSION__, etc.
400     #
401     for b in src_files:
402         s = p['filemap'].get(b, b)
403         env.Command(os.path.join(build, b),
404                     os.path.join(src, s),
405                     "$SEDCOM")
406
407     #
408     # NOW, finally, we can create the MANIFEST, which we do
409     # by having Python spit out the contents of the src_files
410     # array we've carefully created.  After we've added
411     # MANIFEST itself to the array, of course.
412     #
413     src_files.append("MANIFEST")
414     def copy(target, source, **kw):
415         global src_files
416         src_files.sort()
417         f = open(target, 'wb')
418         for file in src_files:
419             f.write(file + "\n")
420         f.close()
421         return 0
422     env.Command(os.path.join(build, 'MANIFEST'),
423                 os.path.join(src, 'MANIFEST.in'),
424                 copy)
425
426     #
427     # Use the Python distutils to generate the packages.
428     #
429     archive = os.path.join(build,
430                            'dist',
431                            "%s-%s.%s" % (pkg, version, archsuffix))
432
433     src_deps.append(archive)
434
435     build_targets = [
436         os.path.join(build, 'dist', "%s-%s.%s.%s" % (pkg, version, platform, archsuffix)),
437         archive,
438         os.path.join(build, 'dist', "%s-%s.win32.exe" % (pkg, version)),
439     ]
440     install_targets = build_targets[:]
441
442     # We can get away with calling setup.py using a directory path
443     # like this because we put a preamble in it that will chdir()
444     # to the directory in which setup.py exists.
445     bdist_dirs = [
446         os.path.join(build, 'build', 'lib'),
447         os.path.join(build, 'build', 'scripts'),
448     ]
449     setup_py = os.path.join(build, 'setup.py')
450     commands = [
451         "rm -rf %s && python %s bdist" %
452             (string.join(map(lambda x: str(x), bdist_dirs)), setup_py),
453         "python %s sdist" % setup_py,
454         "python %s bdist_wininst" % setup_py,
455     ]
456
457     if rpm:
458         topdir = os.path.join(os.getcwd(), build, 'build',
459                               'bdist.' + platform, 'rpm')
460
461         BUILDdir = os.path.join(topdir, 'BUILD', pkg + '-' + version)
462         RPMSdir = os.path.join(topdir, 'RPMS', 'noarch')
463         SOURCESdir = os.path.join(topdir, 'SOURCES')
464         SPECSdir = os.path.join(topdir, 'SPECS')
465         SRPMSdir = os.path.join(topdir, 'SRPMS')
466
467         specfile = os.path.join(SPECSdir, "%s-%s-1.spec" % (pkg, version))
468         sourcefile = os.path.join(SOURCESdir, "%s-%s.%s" % (pkg, version, archsuffix));
469         rpm = os.path.join(RPMSdir, "%s-%s-1.noarch.rpm" % (pkg, version))
470         src_rpm = os.path.join(SRPMSdir, "%s-%s-1.src.rpm" % (pkg, version))
471
472         env.InstallAs(specfile, os.path.join('rpm', "%s.spec" % pkg))
473         env.InstallAs(sourcefile, archive)
474
475         targets = [ rpm, src_rpm ]
476         cmd = "rpm --define '_topdir %s' -ba $SOURCES" % topdir
477         if not os.path.isdir(BUILDdir):
478             cmd = "mkdir -p " + BUILDdir + "; " + cmd
479         env.Command(targets, specfile, cmd)
480         env.Depends(targets, sourcefile)
481
482         install_targets.extend(targets)
483
484     build_src_files = map(lambda x, b=build: os.path.join(b, x), src_files)
485
486     if dh_builddeb and fakeroot:
487         # Debian builds directly into build/dist, so we don't
488         # need to add the .debs to the install_targets.
489         deb = os.path.join('build', 'dist', "%s_%s-1_all.deb" % (pkg, version))
490         env.Command(deb, build_src_files, [
491             "fakeroot make -f debian/rules VERSION=$VERSION DH_COMPAT=$DH_COMPAT ENVOKED_BY_CONSTRUCT=1 binary-%s" % pkg,
492             "env DH_COMPAT=$DH_COMPAT dh_clean"
493                     ])
494         env.Depends(deb, p['debian_deps'])
495
496
497     #
498     # Now set up creation and installation of the packages.
499     #
500     env.Command(build_targets, build_src_files, commands)
501     env.Install(os.path.join('build', 'dist'), install_targets)
502
503     #
504     # Unpack the archive created by the distutils into build/unpack.
505     #
506     d = os.path.join(unpack_dir, "%s-%s" % (pkg, version))
507     unpack_files = map(lambda x, d=d: os.path.join(d, x), src_files)
508
509     # We'd like to replace the last three lines with the following:
510     #
511     #   tar zxf %< -C $unpack_dir
512     #
513     # but that gives heartburn to Cygwin's tar, so work around it
514     # with separate zcat-tar-rm commands.
515     env.Command(unpack_files, archive, [
516         "rm -rf " + os.path.join(unpack_dir, '%s-%s' % (pkg, version)),
517         "zcat $SOURCES > .temp",
518         "tar xf .temp -C %s" % unpack_dir,
519         "rm -f .temp",
520     ])
521
522     #
523     # Run setup.py in the unpacked subdirectory to "install" everything
524     # into our build/test subdirectory.  Auxiliary modules that we need
525     # (TestCmd.py, TestSCons.py, unittest.py) will be copied in by
526     # etc/Conscript.  The runtest.py script will set PYTHONPATH so that
527     # the tests only look under build/test.  This makes sure that our
528     # tests pass with what we really packaged, not because of something
529     # hanging around in the development directory.
530     #
531     # We can get away with calling setup.py using a directory path
532     # like this because we put a preamble in it that will chdir()
533     # to the directory in which setup.py exists.
534     env.Command(dst_files, unpack_files, [
535         "rm -rf %s" % install,
536         "python %s install --prefix=%s" % (os.path.join(unpack_dir,
537                                                         '%s-%s' % (pkg, version),
538                                                         'setup.py'),
539                                            prefix
540                                           ),
541     ])
542
543 #
544 # Arrange for supporting packages to be installed in the test directories.
545 #
546 Export('env', 'whereis')
547
548 SConscript('etc/SConscript')
549
550 #
551 # Documentation.
552 #
553 BuildDir('build/doc', 'doc')
554
555 SConscript('build/doc/SConscript');
556
557
558 #
559 # If we're running in the actual Aegis project, pack up a complete
560 # source archive from the project files and files in the change,
561 # so we can share it with helpful developers who don't use Aegis.
562 #
563 # First, lie and say that we've seen any files removed by this
564 # change, so they don't get added to the source files list
565 # that goes into the archive.
566 #
567
568 if change:
569     df = []
570     cmd = "aegis -list -unf -c %s cf 2>/dev/null" % change
571     for line in map(lambda x: x[:-1], os.popen(cmd, "r").readlines()):
572         a = string.split(line)
573         if a[1] == "remove":
574             df.append(a[3])
575
576     cmd = "aegis -list -terse pf 2>/dev/null"
577     pf = map(lambda x: x[:-1], os.popen(cmd, "r").readlines())
578     cmd = "aegis -list -terse cf 2>/dev/null"
579     cf = map(lambda x: x[:-1], os.popen(cmd, "r").readlines())
580     u = {}
581     for f in pf + cf:
582         u[f] = 1
583     for f in df:
584         del u[f]
585     sfiles = filter(lambda x: x[-9:] != '.aeignore' and x[-7:] != '.consign',
586                        u.keys())
587
588     if sfiles:
589         ps = "%s-src" % project
590         psv = "%s-src-%s" % (project, version)
591         b_ps = os.path.join('build', ps)
592         b_psv = os.path.join('build', psv)
593
594         for file in sfiles:
595             env.Command(os.path.join(b_ps, file), file,
596                         [ "$SEDCOM", "chmod --reference=$SOURCES $TARGET" ])
597
598         b_ps_files = map(lambda x, d=b_ps: os.path.join(d, x), sfiles)
599         cmds = [
600             "rm -rf %s" % b_psv,
601             "cp -rp %s %s" % (b_ps, b_psv),
602             "find %s -name .consign -exec rm {} \\;" % b_psv,
603             "tar zcf $TARGET -C build %s" % psv,
604         ]
605         env.Command(os.path.join('build',
606                                  'dist',
607                                  '%s-src-%s.tar.gz' % (project, version)),
608                     src_deps + b_ps_files, cmds)