9c1fd837ed1d97d3e0d2cd9787be62b504c9e3eb
[scons.git] / SConstruct
1 #
2 # SConstruct file to build scons packages during development.
3 #
4
5 #
6 # Copyright (c) 2001, 2002 Steven Knight
7 #
8 # Permission is hereby granted, free of charge, to any person obtaining
9 # a copy of this software and associated documentation files (the
10 # "Software"), to deal in the Software without restriction, including
11 # without limitation the rights to use, copy, modify, merge, publish,
12 # distribute, sublicense, and/or sell copies of the Software, and to
13 # permit persons to whom the Software is furnished to do so, subject to
14 # the following conditions:
15 #
16 # The above copyright notice and this permission notice shall be included
17 # in all copies or substantial portions of the Software.
18 #
19 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
20 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
21 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 #
27
28 import distutils.util
29 import os
30 import os.path
31 import stat
32 import string
33 import sys
34 import time
35
36 project = 'scons'
37 default_version = '0.06'
38
39 Default('.')
40
41 #
42 # An internal "whereis" routine to figure out if we have a
43 # given program available.  Put it in the "cons::" package
44 # so subsidiary Conscript files can get at it easily, too.
45 #
46
47 def whereis(file):
48     for dir in string.split(os.environ['PATH'], os.pathsep):
49         f = os.path.join(dir, file)
50         if os.path.isfile(f):
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 = default_version
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 = default_version
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 = default_version
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 lib_project = os.path.join("lib", project)
149
150 unpack_dir = os.path.join(os.getcwd(), "build", "unpack")
151
152 test_arch_dir = os.path.join(os.getcwd(),
153                              "build",
154                              "test-%s" % string.replace(archsuffix, '.', '-'))
155
156 test_src_arch_dir = os.path.join(os.getcwd(),
157                                  "build",
158                                  "test-src-%s" % string.replace(archsuffix,
159                                                                 '.',
160                                                                 '-'))
161
162 test_rpm_dir = os.path.join(os.getcwd(), "build", "test-rpm")
163
164 test_deb_dir = os.path.join(os.getcwd(), "build", "test-deb")
165
166 def SCons_revision(target, source, env):
167     """Interpolate specific values from the environment into a file.
168     
169     This is used to copy files into a tree that gets packaged up
170     into the source file package.
171     """
172     # Note:  We don't use $VERSION from the environment so that
173     # this routine will change when the version number changes
174     # and things will get rebuilt properly.
175     global version
176     print "SCons_revision() < %s > %s" % (source[0], target)
177     inf = open(source[0], 'rb')
178     outf = open(target, 'wb')
179     for line in inf.readlines():
180         # Note:  We construct the __*__ substitution strings here
181         # so that they don't get replaced when this file gets
182         # copied into the tree for packaging.
183         line = string.replace(line, '_' + '_DATE__', env['DATE'])
184         line = string.replace(line, '_' + '_DEVELOPER__', env['DEVELOPER'])
185         line = string.replace(line, '_' + '_FILE__', source[0])
186         line = string.replace(line, '_' + '_REVISION__', env['REVISION'])
187         line = string.replace(line, '_' + '_VERSION__', version)
188         outf.write(line)
189     inf.close()
190     outf.close()
191     os.chmod(target, os.stat(source[0])[0])
192
193 revbuilder = Builder(name = 'SCons_revision', action = SCons_revision)
194
195 env = Environment(
196                    ENV           = ENV,
197  
198                    DATE          = date,
199                    DEVELOPER     = developer,
200                    REVISION      = revision,
201                    VERSION       = version,
202                    DH_COMPAT     = dh_compat,
203
204                    BUILDERS      = [ revbuilder ],
205                  )
206
207 #
208 # Define SCons packages.
209 #
210 # In the original, more complicated packaging scheme, we were going
211 # to have separate packages for:
212 #
213 #       python-scons    only the build engine
214 #       scons-script    only the script
215 #       scons           the script plus the build engine
216 #
217 # We're now only delivering a single "scons" package, but this is still
218 # "built" as two sub-packages (the build engine and the script), so
219 # the definitions remain here, even though we're not using them for
220 # separate packages.
221 #
222
223 python_scons = {
224         'pkg'           : 'python-' + project,
225         'src_subdir'    : 'engine',
226         'inst_subdir'   : os.path.join('lib', 'python1.5', 'site-packages'),
227
228         'debian_deps'   : [
229                             'debian/rules',
230                             'debian/control',
231                             'debian/changelog',
232                             'debian/copyright',
233                             'debian/python-scons.postinst',
234                             'debian/python-scons.prerm',
235                           ],
236
237         'files'         : [ 'LICENSE.txt',
238                             'README.txt',
239                             'setup.cfg',
240                             'setup.py',
241                           ],
242
243         'filemap'       : {
244                             'LICENSE.txt' : '../LICENSE.txt'
245                           },
246 }
247
248 #
249 # The original packaging scheme would have have required us to push
250 # the Python version number into the package name (python1.5-scons,
251 # python2.0-scons, etc.), which would have required a definition
252 # like the following.  Leave this here in case we ever decide to do
253 # this in the future, but note that this would require some modification
254 # to src/engine/setup.py before it would really work.
255 #
256 #python2_scons = {
257 #        'pkg'          : 'python2-' + project,
258 #        'src_subdir'   : 'engine',
259 #        'inst_subdir'  : os.path.join('lib', 'python2.1', 'site-packages'),
260 #
261 #        'debian_deps'  : [
262 #                           'debian/rules',
263 #                           'debian/control',
264 #                           'debian/changelog',
265 #                           'debian/copyright',
266 #                           'debian/python2-scons.postinst',
267 #                           'debian/python2-scons.prerm',
268 #                          ],
269 #
270 #        'files'        : [
271 #                            'LICENSE.txt',
272 #                            'README.txt',
273 #                            'setup.cfg',
274 #                            'setup.py',
275 #                          ],
276 #        'filemap'      : {
277 #                            'LICENSE.txt' : '../LICENSE.txt',
278 #                          },
279 #}
280 #
281
282 scons_script = {
283         'pkg'           : project + '-script',
284         'src_subdir'    : 'script',
285         'inst_subdir'   : 'bin',
286
287         'debian_deps'   : [
288                             'debian/rules',
289                             'debian/control',
290                             'debian/changelog',
291                             'debian/copyright',
292                             'debian/python-scons.postinst',
293                             'debian/python-scons.prerm',
294                           ],
295
296         'files'         : [
297                             'LICENSE.txt',
298                             'README.txt',
299                             'setup.cfg',
300                             'setup.py',
301                           ],
302
303         'filemap'       : {
304                             'LICENSE.txt' : '../LICENSE.txt',
305                             'scons'       : 'scons.py',
306                            }
307 }
308
309 scons = {
310         'pkg'           : project,
311
312         'debian_deps'   : [ 
313                             'debian/rules',
314                             'debian/control',
315                             'debian/changelog',
316                             'debian/copyright',
317                             'debian/scons.postinst',
318                             'debian/scons.prerm',
319                           ],
320
321         'files'         : [ 
322                             'CHANGES.txt',
323                             'LICENSE.txt',
324                             'README.txt',
325                             'RELEASE.txt',
326                             'os_spawnv_fix.diff',
327                             'scons.1',
328                             'script/scons.bat',
329                             'setup.cfg',
330                             'setup.py',
331                           ],
332
333         'filemap'       : {
334                             'scons.1' : '../doc/man/scons.1',
335                           },
336
337         'subpkgs'       : [ python_scons, scons_script ],
338
339         'subinst_dirs'  : {
340                              'python-' + project : lib_project,
341                              project + '-script' : 'bin',
342                            },
343 }
344
345 src_deps = []
346 src_files = []
347
348 for p in [ scons ]:
349     #
350     # Initialize variables with the right directories for this package.
351     #
352     pkg = p['pkg']
353     pkg_version = "%s-%s" % (pkg, version)
354
355     src = 'src'
356     if p.has_key('src_subdir'):
357         src = os.path.join(src, p['src_subdir'])
358
359     build = os.path.join('build', pkg)
360
361     #
362     # Read up the list of source files from our MANIFEST.in.
363     # This list should *not* include LICENSE.txt, MANIFEST,
364     # README.txt, or setup.py.  Make a copy of the list for the
365     # destination files.
366     #
367     src_files = map(lambda x: x[:-1],
368                     open(os.path.join(src, 'MANIFEST.in')).readlines())
369     dst_files = src_files[:]
370
371     if p.has_key('subpkgs'):
372         #
373         # This package includes some sub-packages.  Read up their
374         # MANIFEST.in files, and add them to our source and destination
375         # file lists, modifying them as appropriate to add the
376         # specified subdirs.
377         #
378         for sp in p['subpkgs']:
379             ssubdir = sp['src_subdir']
380             isubdir = p['subinst_dirs'][sp['pkg']]
381             f = map(lambda x: x[:-1],
382                     open(os.path.join(src, ssubdir, 'MANIFEST.in')).readlines())
383             src_files.extend(map(lambda x, s=ssubdir: os.path.join(s, x), f))
384             dst_files.extend(map(lambda x, i=isubdir: os.path.join(i, x), f))
385             for k in sp['filemap'].keys():
386                 f = sp['filemap'][k]
387                 if f:
388                     k = os.path.join(sp['src_subdir'], k)
389                     p['filemap'][k] = os.path.join(sp['src_subdir'], f)
390
391     #
392     # Now that we have the "normal" source files, add those files
393     # that are standard for each distribution.  Note that we don't
394     # add these to dst_files, because they don't get installed.
395     # And we still have the MANIFEST to add.
396     #
397     src_files.extend(p['files'])
398
399     #
400     # Now run everything in src_file through the sed command we
401     # concocted to expand __FILE__, __VERSION__, etc.
402     #
403     for b in src_files:
404         s = p['filemap'].get(b, b)
405         env.SCons_revision(os.path.join(build, b), os.path.join(src, s))
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, 'dist', "%s.%s" % (pkg_version, archsuffix))
430     platform_archive = os.path.join(build,
431                                     'dist',
432                                     "%s.%s.%s" % (pkg_version,
433                                                   platform,
434                                                   archsuffix))
435     win32_exe = os.path.join(build, 'dist', "%s.win32.exe" % pkg_version)
436
437     src_deps.append(archive)
438
439     build_targets = [ platform_archive, archive, win32_exe ]
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" % (string.join(bdist_dirs), setup_py),
452         "python %s sdist" % setup_py,
453         "python %s bdist_wininst" % setup_py,
454     ]
455
456     if rpm:
457         topdir = os.path.join(os.getcwd(), build, 'build',
458                               'bdist.' + platform, 'rpm')
459
460         BUILDdir = os.path.join(topdir, 'BUILD', pkg + '-' + version)
461         RPMSdir = os.path.join(topdir, 'RPMS', 'noarch')
462         SOURCESdir = os.path.join(topdir, 'SOURCES')
463         SPECSdir = os.path.join(topdir, 'SPECS')
464         SRPMSdir = os.path.join(topdir, 'SRPMS')
465
466         specfile = os.path.join(SPECSdir, "%s-1.spec" % pkg_version)
467         sourcefile = os.path.join(SOURCESdir,
468                                   "%s.%s" % (pkg_version, archsuffix));
469         rpm = os.path.join(RPMSdir, "%s-1.noarch.rpm" % pkg_version)
470         src_rpm = os.path.join(SRPMSdir, "%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 %s; $)" % BUILDdir) + cmd
479         env.Command(targets, specfile, cmd)
480         env.Depends(targets, sourcefile)
481
482         install_targets.extend(targets)
483
484         dfiles = map(lambda x, d=test_rpm_dir: os.path.join(d, 'usr', x),
485                      dst_files)
486         env.Command(dfiles,
487                     rpm,
488                     "rpm2cpio $SOURCES | (cd %s && cpio -id)" % test_rpm_dir)
489
490     build_src_files = map(lambda x, b=build: os.path.join(b, x), src_files)
491
492     if dh_builddeb and fakeroot:
493         # Debian builds directly into build/dist, so we don't
494         # need to add the .debs to the install_targets.
495         deb = os.path.join('build', 'dist', "%s_%s-1_all.deb" % (pkg, version))
496         env.Command(deb, build_src_files, [
497             "fakeroot make -f debian/rules VERSION=$VERSION DH_COMPAT=$DH_COMPAT ENVOKED_BY_CONSTRUCT=1 binary-%s" % pkg,
498             "env DH_COMPAT=$DH_COMPAT dh_clean"
499                     ])
500         env.Depends(deb, p['debian_deps'])
501
502         dfiles = map(lambda x, d=test_deb_dir: os.path.join(d, 'usr', x),
503                      dst_files)
504         env.Command(dfiles,
505                     deb,
506                     "dpkg --fsys-tarfile $SOURCES | (cd %s && tar -xf -)" % test_deb_dir)
507
508     #
509     # Now set up creation and installation of the packages.
510     #
511     env.Command(build_targets, build_src_files, commands)
512     env.Install(os.path.join('build', 'dist'), install_targets)
513
514     #
515     # Unpack the archive created by the distutils into
516     # build/unpack/scons-{version}.
517     #
518     unpack_files = map(lambda x, u=unpack_dir, pv=pkg_version:
519                               os.path.join(u, pv, x),
520                        src_files)
521
522     #
523     # We'd like to replace the last three lines with the following:
524     #
525     #   tar zxf %< -C $unpack_dir
526     #
527     # but that gives heartburn to Cygwin's tar, so work around it
528     # with separate zcat-tar-rm commands.
529     env.Command(unpack_files, archive, [
530         "rm -rf %s" % os.path.join(unpack_dir, pkg_version),
531         "zcat $SOURCES > .temp",
532         "tar xf .temp -C %s" % unpack_dir,
533         "rm -f .temp",
534     ])
535
536     #
537     # Run setup.py in the unpacked subdirectory to "install" everything
538     # into our build/test subdirectory.  The runtest.py script will set
539     # PYTHONPATH so that the tests only look under build/test-{package},
540     # and under etc (for the testing modules TestCmd.py, TestSCons.py,
541     # and unittest.py).  This makes sure that our tests pass with what
542     # we really packaged, not because of something hanging around in
543     # the development directory.
544     #
545     # We can get away with calling setup.py using a directory path
546     # like this because we put a preamble in it that will chdir()
547     # to the directory in which setup.py exists.
548     dfiles = map(lambda x, d=test_arch_dir: os.path.join(d, x), dst_files)
549     env.Command(dfiles, unpack_files, [
550         "rm -rf %s" % os.path.join(unpack_dir, pkg_version, 'build'),
551         "rm -rf %s" % test_arch_dir,
552         "python %s install --prefix=%s" % (os.path.join(unpack_dir,
553                                                         pkg_version,
554                                                         'setup.py'),
555                                            test_arch_dir
556                                           ),
557     ])
558
559 #
560 #
561 #
562 Export('env')
563
564 SConscript('etc/SConscript')
565
566 #
567 # Documentation.
568 #
569 BuildDir('build/doc', 'doc')
570
571 Export('env', 'whereis')
572
573 SConscript('build/doc/SConscript')
574
575 #
576 # If we're running in the actual Aegis project, pack up a complete
577 # source archive from the project files and files in the change,
578 # so we can share it with helpful developers who don't use Aegis.
579 #
580
581 if change:
582     df = []
583     cmd = "aegis -list -unf -c %s cf 2>/dev/null" % change
584     for line in map(lambda x: x[:-1], os.popen(cmd, "r").readlines()):
585         a = string.split(line)
586         if a[1] == "remove":
587             if a[3][0] == '(':
588                 df.append(a[4])
589             else:
590                 df.append(a[3])
591
592     cmd = "aegis -list -terse pf 2>/dev/null"
593     pf = map(lambda x: x[:-1], os.popen(cmd, "r").readlines())
594     cmd = "aegis -list -terse cf 2>/dev/null"
595     cf = map(lambda x: x[:-1], os.popen(cmd, "r").readlines())
596     u = {}
597     for f in pf + cf:
598         u[f] = 1
599     for f in df:
600         del u[f]
601     sfiles = filter(lambda x: x[-9:] != '.aeignore' 
602                               and x[-8:] != '.consign'
603                               and x[-9:] != '.sconsign',
604                     u.keys())
605
606     if sfiles:
607         ps = "%s-src" % project
608         psv = "%s-%s" % (ps, version)
609         b_ps = os.path.join('build', ps)
610         b_psv = os.path.join('build', psv)
611
612         src_archive = os.path.join('build', 'dist', '%s.tar.gz' % psv)
613
614         for file in sfiles:
615             env.SCons_revision(os.path.join(b_ps, file), file)
616
617         b_ps_files = map(lambda x, d=b_ps: os.path.join(d, x), sfiles)
618         cmds = [
619             "rm -rf %s" % b_psv,
620             "cp -rp %s %s" % (b_ps, b_psv),
621             "find %s -name .consign -exec rm {} \\;" % b_psv,
622             "find %s -name .sconsign -exec rm {} \\;" % b_psv,
623             "tar czh -f $TARGET -C build %s" % psv,
624         ]
625
626         env.Command(src_archive, src_deps + b_ps_files, cmds)
627
628         #
629         # Unpack the archive created by the distutils into
630         # build/unpack/scons-{version}.
631         #
632         unpack_files = map(lambda x, u=unpack_dir, psv=psv:
633                                   os.path.join(u, psv, x),
634                            sfiles)
635
636         #
637         # We'd like to replace the last three lines with the following:
638         #
639         #       tar zxf %< -C $unpack_dir
640         #
641         # but that gives heartburn to Cygwin's tar, so work around it
642         # with separate zcat-tar-rm commands.
643         env.Command(unpack_files, src_archive, [
644             "rm -rf %s" % os.path.join(unpack_dir, psv),
645             "zcat $SOURCES > .temp",
646             "tar xf .temp -C %s" % unpack_dir,
647             "rm -f .temp",
648         ])
649
650         #
651         # Run setup.py in the unpacked subdirectory to "install" everything
652         # into our build/test subdirectory.  The runtest.py script will set
653         # PYTHONPATH so that the tests only look under build/test-{package},
654         # and under etc (for the testing modules TestCmd.py, TestSCons.py,
655         # and unittest.py).  This makes sure that our tests pass with what
656         # we really packaged, not because of something hanging around in
657         # the development directory.
658         #
659         # We can get away with calling setup.py using a directory path
660         # like this because we put a preamble in it that will chdir()
661         # to the directory in which setup.py exists.
662         dfiles = map(lambda x, d=test_src_arch_dir: os.path.join(d, x),
663                         dst_files)
664         ENV = env.Dictionary('ENV')
665         ENV['SCONS_LIB_DIR'] = os.path.join(unpack_dir, psv, 'src', 'engine')
666         ENV['USERNAME'] = developer
667         env.Copy(ENV = ENV).Command(dfiles, unpack_files, [
668             "rm -rf %s" % os.path.join(unpack_dir,
669                                        psv,
670                                        'build',
671                                        'scons',
672                                        'build'),
673             "rm -rf %s" % test_src_arch_dir,
674             "cd %s && python %s %s" % \
675                 (os.path.join(unpack_dir, psv),
676                  os.path.join('src', 'script', 'scons.py'),
677                  os.path.join('build', 'scons')),
678             "python %s install --prefix=%s" % (os.path.join(unpack_dir,
679                                                             psv,
680                                                             'build',
681                                                             'scons',
682                                                             'setup.py'),
683                                                test_src_arch_dir
684                                               ),
685         ])