]> git.donarmstrong.com Git - lilypond.git/blob - SConstruct
7b627dac8b5e20fc1fa8595945c717145b7858d6
[lilypond.git] / SConstruct
1 # -*-python-*-
2
3 '''
4 Experimental scons (www.scons.org) building:
5
6 scons TARGET builds from source directory ./TARGET (not recursive)
7
8
9 Usage:
10     scons
11     scons lily            # build lily
12
13     LILYPONDPREFIX=out-scons/usr/share/lilypond lily/out-scons/lilypond-bin
14     scons doc             # build web doc
15
16 ?    scons fonts           # build all font stuff (split this? )
17
18     scons config          # reconfigure
19
20     scons /               # builds all possible targets
21
22     scons install
23     scons -c              # clean
24     scons -h              # help
25
26     scons build=DIR       # scrdir build, write to new tree =build
27     scons out=DIR         # write output to deeper dir DIR
28
29 Optionally, make a custom.py.  I have
30
31 import os
32 out='out-scons'
33 optimising=0
34 debugging=1
35 gui=1
36 os.path.join (os.getcwd (), '=install')
37 prefix=os.path.join (os.environ['HOME'], 'usr', 'pkg', 'lilypond')
38
39 '''
40
41
42 # TODO:
43 #   * add missing dirs
44 #   * cleanup
45
46 #   * separate environments?
47 #     - compile environment checks headers and libraries
48 #     - doc environment checks doc stuff
49
50 #   * commandline targets:
51 #      - clean => -c ?
52 #   * more fine-grained config.h -- move lilypondprefix to version.hh?
53 #     - config.h:   changes after system upgrades, affects all files
54 #     - version.hh:  prefix, version etc?  affects few
55
56 import re
57 import glob
58 import os
59 import string
60 import sys
61 import stat
62 import shutil
63
64 # faster but scary: when changing #includes, do scons --implicit-deps-changed
65 # SetOption ('implicit_cache', 1)
66
67 # SConscripts are only needed in directories where something needs
68 # to be done, building or installing
69 # TODO: Documentation/*, input/*/*, vim, po
70 # rename Documentation/* to ./doc?
71
72 # somethin's broken wrt config.h checksums?
73 FOOSUMS = 1
74
75 subdirs = ['flower', 'lily', 'mf', 'scm', 'ly', 'Documentation',
76            'Documentation/user', 'input', 'scripts', 'elisp',
77            'buildscripts', 'cygwin', 'debian']
78
79 usage = r'''Usage:
80 scons [KEY=VALUE].. [TARGET]..
81
82 where TARGET is config|lily|all|fonts|doc|tar|dist|release
83 '''
84       
85
86 config_cache = 'config.cache'
87
88 config_vars = (
89         'BASH',
90         'CFLAGS',
91         'CPPPATH',
92         'CXXFLAGS',
93         'DEFINES',
94         'LIBS',
95         'METAFONT',
96         'PERL',
97         'PYTHON',
98         )
99
100 # Put your favourite stuff in custom.py
101 opts = Options ([config_cache, 'custom.py'], ARGUMENTS)
102 opts.Add ('prefix', 'Install prefix', '/usr/')
103 opts.Add ('out', 'Output directory', 'out-scons')
104 opts.Add ('build', 'Build directory', '.')
105 opts.AddOptions (
106         BoolOption ('warnings', 'compile with -Wall and similiar',
107                    1),
108         BoolOption ('debugging', 'compile with debugging symbols',
109                     0),
110         BoolOption ('optimising', 'compile with optimising',
111                     1),
112         BoolOption ('shared', 'build shared libraries',
113                     0),
114         BoolOption ('static', 'build static libraries',
115                     1),
116         BoolOption ('gui', 'build with GNOME backend (EXPERIMENTAL)',
117                     1),
118         BoolOption ('verbose', 'run commands with verbose flag',
119                     0),
120         BoolOption ('checksums', 'use checksums instead of timestamps',
121                     1),
122         )
123
124 srcdir = Dir ('.').srcnode ().abspath
125
126 #ugh
127 sys.path.append (os.path.join (srcdir, 'stepmake', 'bin'))
128 import packagepython
129 package = packagepython.Package (srcdir)
130 version = packagepython.version_tuple_to_str (package.version)
131
132 ENV = { 'PATH' : os.environ['PATH'] }
133 for key in ['LD_LIBRARY_PATH', 'GUILE_LOAD_PATH', 'PKG_CONFIG_PATH']:
134         if os.environ.has_key(key):
135                 ENV[key] = os.environ[key]
136
137 env = Environment (
138         ENV = ENV,
139
140         BASH = '/bin/bash',
141         MAKEINFO = 'LANG= makeinfo',
142         PERL = '/usr/bin/perl',
143         PYTHON = '/usr/bin/python',
144         SH = '/bin/sh',
145         
146         ABC2LY_PY = srcdir + '/scripts/abc2ly.py',
147         LILYPOND_BOOK = srcdir + '/scripts/lilypond-book.py',
148         LILYPOND_BOOK_FLAGS = '',
149         LILYPOND_BOOK_FORMAT = 'texi-html',
150         LILYPOND_PY = srcdir + '/scripts/lilypond.py',
151         MF_TO_TABLE_PY = srcdir + '/buildscripts/mf-to-table.py',
152         
153         PKG_CONFIG_PATH = [os.path.join (os.environ['HOME'],
154                                          'usr/pkg/gnome/lib'),
155                            os.path.join (os.environ['HOME'],
156                                          'usr/pkg/pango/lib')],
157         MFMODE = 'ljfour',
158         TEXINFO_PAPERSIZE_OPTION = '-t @afourpaper',
159         TOPLEVEL_VERSION = version,
160         )
161
162 Help (usage + opts.GenerateHelpText (env))
163
164 map (lambda x: opts.AddOptions ((x,)), config_vars)
165 opts.Update (env)
166
167 if env['checksums']:
168         SetOption ('max_drift', 0)
169
170 prefix = env['prefix']
171 bindir = os.path.join (prefix, 'bin')
172 sharedir = os.path.join (prefix, 'share')
173 libdir = os.path.join (prefix, 'lib')
174 localedir = os.path.join (sharedir, 'locale')
175 sharedir_package = os.path.join (sharedir, package.name)
176 sharedir_package_version = os.path.join (sharedir_package, version)
177 lilypondprefix = sharedir_package_version
178
179 # post-option environment-update
180 env.Append (
181         srcdir = srcdir,
182         
183         bindir = bindir,
184         sharedir = sharedir,
185         lilypond_datadir = sharedir_package,
186         local_lilypond_datadir = sharedir_package_version,
187         lilypondprefix = lilypondprefix,
188         sharedir_package = sharedir_package,
189         sharedir_package_version = sharedir_package_version,
190         )
191
192 env.CacheDir (os.path.join (env['build'], '=build-cache'))
193
194 if env['debugging']:
195         env.Append (CFLAGS = '-g')
196         env.Append (CXXFLAGS = '-g')
197 if env['optimising']:
198         env.Append (CFLAGS = '-O2')
199         env.Append (CXXFLAGS = '-O2')
200         env.Append (CXXFLAGS = '-DSTRING_UTILS_INLINED')
201 if env['warnings']:
202         env.Append (CFLAGS = '-W ')
203         env.Append (CFLAGS = '-Wall')
204         # what about = ['-W', '-Wall', ...]?
205         env.Append (CXXFLAGS = '-W')
206         env.Append (CXXFLAGS = '-Wall')
207         env.Append (CXXFLAGS = '-Wconversion')
208 if env['verbose']:
209         env['__verbose'] = '--verbose'
210
211 # Hmm
212 #env.Append (ENV = {'PKG_CONFIG_PATH' : string.join (env['PKG_CONFIG_PATH'],
213 #                                                   os.pathsep),
214 #                  'LD_LIBRARY_PATH' : string.join (env['LD_LIBRARY_PATH'],
215 #                                                   os.pathsep),
216 #                  'GUILE_LOAD_PATH' : string.join (env['GUILE_LOAD_PATH'],
217 #                                                   os.pathsep), })
218
219 outdir = os.path.join (Dir (env['build']).abspath, env['out'])
220 config_h = os.path.join (outdir, 'config.h')
221 version_h = os.path.join (outdir, 'version.hh')
222 config_h = os.path.join (outdir, 'config.h')
223 env.Alias ('config', config_h)
224
225
226 ## Explicit dependencies
227
228 # Without target arguments, build lily only
229 if not COMMAND_LINE_TARGETS:
230         env.Default ('lily')
231 env.Alias ('all', '.')
232 env.Alias ('doc',
233            ['Documentation',
234             'Documentation/user',
235             'Documentation/topdocs'])
236
237 env.Depends ('doc', ['lily', 'mf'])
238 env.Depends ('input', ['lily', 'mf'])
239 env.Depends ('doc', ['lily', 'mf'])
240
241
242 def list_sort (lst):
243         sorted = lst
244         sorted.sort ()
245         return sorted
246
247 def configure (target, source, env):
248         vre = re.compile ('^.*[^-.0-9]([0-9][0-9]*\.[0-9][.0-9]*).*$', re.DOTALL)
249         def get_version (program):
250                 command = '(%(program)s --version || %(program)s -V) 2>&1' % vars ()
251                 pipe = os.popen (command)
252                 output = pipe.read ()
253                 if pipe.close ():
254                         return None
255                 v = re.sub (vre, '\\1', output)
256                 return string.split (v, '.')
257
258         def test_program (lst, program, minimal, description, package):
259                 sys.stdout.write ('Checking %s version... ' % program)
260                 actual = get_version (program)
261                 if not actual:
262                         print 'not found'
263                         lst.append ((description, package, minimal, program,
264                                      'not installed'))
265                         return
266                 sys.stdout.write (string.join (actual, '.'))
267                 sys.stdout.write ('\n')
268                 if actual < string.split (minimal, '.'):
269                         lst.append ((description, package, minimal, program,
270                                      string.join (actual, '.')))
271
272         required = []
273         test_program (required, 'gcc', '2.8', 'GNU C compiler', 'gcc')
274         test_program (required, 'g++', '3.0.5', 'GNU C++ compiler', 'g++')
275         test_program (required, 'python', '2.1', 'Python (www.python.org)', 'python')
276         test_program (required, 'guile-config', '1.6', 'GUILE development',
277                         'libguile-dev or guile-devel')
278         # Do not use bison 1.50 and 1.75.
279         test_program (required, 'bison', '1.25', 'Bison -- parser generator',
280                         'bison')
281         test_program (required, 'flex', '0.0', 'Flex -- lexer generator', 'flex')
282
283
284         optional = []
285         test_program (optional, 'makeinfo', '4.7', 'Makeinfo tool', 'texinfo')
286         test_program (optional, 'guile', '1.6', 'GUILE scheme',
287                         'libguile-dev or guile-devel')
288         test_program (optional, 'mftrace', '1.0.27', 'Metafont tracing Type1',
289                         'mftrace')
290         test_program (optional, 'perl', '4.0',
291                         'Perl practical efficient readonly language', 'perl')
292         #test_program (optional, 'foo', '2.0', 'Foomatic tester', 'bar')
293
294         def CheckYYCurrentBuffer (context):
295                 context.Message ('Checking for yy_current_buffer... ')
296                 ret = conf.TryCompile ("""using namespace std;
297                 #include <FlexLexer.h>
298                 class yy_flex_lexer: public yyFlexLexer
299                 {
300                 public:
301                 yy_flex_lexer ()
302                 {
303                 yy_current_buffer = 0;
304                 }
305                 };""", '.cc')
306                 context.Result (ret)
307
308         conf = Configure (env, custom_tests = { 'CheckYYCurrentBuffer'
309                                                 : CheckYYCurrentBuffer })
310
311         defines = {
312            'DIRSEP' : "'/'",
313            'PATHSEP' : "':'",
314            'TOPLEVEL_VERSION' : '"' + version + '"',
315            'PACKAGE': '"' + package.name + '"',
316            'DATADIR' : '"' + sharedir + '"',
317            'LILYPOND_DATADIR' : '"' + sharedir_package + '"',
318            'LOCAL_LILYPOND_DATADIR' : '"' + sharedir_package_version + '"',
319            'LOCALEDIR' : '"' + localedir + '"',
320         }
321         conf.env.Append (DEFINES = defines)
322
323         command = r"""python -c 'import sys; sys.stdout.write ("%s/include/python%s" % (sys.prefix, sys.version[:3]))'""" #"
324         PYTHON_INCLUDE = os.popen (command).read ()
325         env.Append (CPPPATH = PYTHON_INCLUDE)
326
327         headers = ('sys/stat.h', 'assert.h', 'kpathsea/kpathsea.h', 'Python.h')
328         for i in headers:
329                 if conf.CheckCHeader (i):
330                         key = re.sub ('[./]', '_', 'HAVE_' + string.upper (i))
331                         conf.env['DEFINES'][key] = 1
332
333         ccheaders = ('sstream',)
334         for i in ccheaders:
335                 if conf.CheckCXXHeader (i):
336                         key = re.sub ('[./]', '_', 'HAVE_' + string.upper (i))
337                         conf.env['DEFINES'][key] = 1
338
339         functions = ('gettext', 'isinf', 'memmem', 'snprintf', 'vsnprintf')
340         for i in functions:
341                 if 0 or conf.CheckFunc (i):
342                         key = re.sub ('[./]', '_', 'HAVE_' + string.upper (i))
343                         conf.env['DEFINES'][key] = 1
344
345         if conf.CheckYYCurrentBuffer ():
346                 conf.env['DEFINES']['HAVE_FLEXLEXER_YY_CURRENT_BUFFER'] = 1
347
348         if conf.CheckLib ('dl'):
349                 pass
350
351         if conf.CheckLib ('kpathsea'):
352                 conf.env['DEFINES']['KPATHSEA'] = 1
353
354         # huh? 
355         if conf.CheckLib ('kpathsea', 'kpse_find_file'):
356                 conf.env['DEFINES']['HAVE_KPSE_FIND_FILE'] = '1'
357         if conf.CheckLib ('kpathsea', 'kpse_find_tfm'):
358                 conf.env['DEFINES']['HAVE_KPSE_FIND_TFM'] = '1'
359
360         #this could happen after flower...
361         env.ParseConfig ('guile-config compile')
362
363         #this could happen only for compiling pango-*
364         if env['gui']:
365                 env.ParseConfig ('pkg-config --cflags --libs gtk+-2.0')
366                 env.ParseConfig ('pkg-config --cflags --libs pango')
367                 if conf.CheckCHeader ('pango/pangofc-fontmap.h'):
368                         conf.env['DEFINES']['HAVE_PANGO_PANGOFC_FONTMAP_H'] = '1'
369
370                 if conf.CheckLib ('pango-1.0',
371                                   'pango_fc_font_map_add_decoder_find_func'):
372                         conf.env['DEFINES']['HAVE_PANGO_CVS'] = '1'
373                         conf.env['DEFINES']['HAVE_PANGO_FC_FONT_MAP_ADD_DECODER_FIND_FUNC'] = '1'
374
375         if required:
376                 print
377                 print '********************************'
378                 print 'Please install required packages'
379                 for i in required:
380                         print '%s:      %s-%s or newer (found: %s %s)' % i
381                 sys.exit (1)
382
383         if optional:
384                 print
385                 print '*************************************'
386                 print 'Consider installing optional packages'
387                 for i in optional:
388                         print '%s:      %s-%s or newer (found: %s %s)' % i
389
390         return conf.Finish ()
391
392 def config_header (target, source, env):
393         config = open (str (target[0]), 'w')
394         for i in list_sort (env['DEFINES'].keys ()):
395                 config.write ('#define %s %s\n' % (i, env['DEFINES'][i]))
396         config.close ()
397
398 if os.path.exists (config_cache) and 'config' in COMMAND_LINE_TARGETS:
399         os.unlink (config_cache)
400 # WTF?
401 # scons: *** Calling Configure from Builders is not supported.
402 # env.Command (config_cache, None, configure)
403 if not os.path.exists (config_cache) \
404    or (os.stat ('SConstruct')[stat.ST_MTIME]
405        > os.stat (config_cache)[stat.ST_MTIME]):
406         env = configure (None, None, env)
407
408         # We 'should' save opts each time, but that makes config.h
409         # always out of date, and that triggers recompiles, even when
410         # using checksums?
411         if FOOSUMS: #not env['checksums']:
412
413                 ## FIXME: Is this smart, using option cache for saving
414                 ## config.cache?  I cannot seem to find the official method.
415                 map (lambda x: opts.AddOptions ((x,)), config_vars)
416                 opts.Save (config_cache, env)
417
418                 env.Command (config_h, config_cache, config_header)
419
420 # hmm?
421 def xuniquify (lst):
422         n = []
423         for i in lst:
424                 if not i in n:
425                         n.append (i)
426         lst = n
427         return lst
428
429 def uniquify (lst):
430         d = {}
431         n = len (lst)
432         i = 0
433         while i < n:
434                 if not d.has_key (lst[i]):
435                         d[lst[i]] = 1
436                         i += 1
437                 else:
438                         del lst[i]
439                         n -= 1
440         return lst
441
442 for i in config_vars:
443         if env.has_key (i) and type (env[i]) == type ([]):
444                 env[i] = uniquify (env[i])
445
446 if not FOOSUMS: #env['checksums']:
447         ## FIXME: Is this smart, using option cache for saving
448         ## config.cache?  I cannot seem to find the official method.
449         map (lambda x: opts.AddOptions ((x,)), config_vars)
450         
451         opts.Save (config_cache, env)
452         env.Command (config_h, config_cache, config_header)
453
454 env.Command (version_h, '#/VERSION',
455              '$PYTHON ./stepmake/bin/make-version.py VERSION > $TARGET')
456
457 absbuild = Dir (env['build']).abspath
458 run_prefix = os.path.join (absbuild, os.path.join (env['out'], 'usr'))
459
460 # post-config environment update
461 env.Append (
462         absbuild = absbuild,
463         run_prefix = run_prefix,
464         LILYPONDPREFIX = os.path.join (run_prefix, 'share/lilypond'),
465
466         LIBPATH = [os.path.join (absbuild, 'flower', env['out']),],
467         CPPPATH = [outdir, '#',],
468         LILYPOND_BIN = os.path.join (absbuild, 'lily', env['out'],
469                                      'lilypond-bin'),
470         LILYPOND_BOOK_PATH = ['.', '#/input', '#/input/regression',
471                               '#/input/test', '#/input/tutorial',
472                               os.path.join (absbuild, 'mf', env['out']),
473                               '#/Documentation/user',
474                               os.path.join (absbuild, 'Documentation',
475                                             env['out']),
476                               os.path.join (absbuild, 'Documentation/user',
477                                             env['out']),
478                               ],
479         MAKEINFO_PATH = ['.', '#/Documentation/user',
480                          os.path.join (absbuild, 'Documentation/user',
481                                        env['out'])],
482         )
483
484 Export ('env')
485 SConscript ('buildscripts/builder.py')
486
487
488 def symlink_tree (target, source, env):
489         def mkdirs (dir):
490                 def mkdir (dir):
491                         if not dir:
492                                 os.chdir (os.sep)
493                                 return
494                         if not os.path.isdir (dir):
495                                 if os.path.exists (dir):
496                                         os.unlink (dir)
497                                 os.mkdir (dir)
498                         os.chdir (dir)
499                 map (mkdir, string.split (dir, os.sep))
500         def symlink (src, dst):
501                 os.chdir (absbuild)
502                 dir = os.path.dirname (dst)
503                 mkdirs (dir)
504                 if src[0] == '#':
505                         frm = os.path.join (srcdir, src[1:])
506                 else:
507                         depth = len (string.split (dir, '/'))
508                         frm = os.path.join ('../' * depth, src, env['out'])
509                 os.symlink (frm, os.path.basename (dst))
510         shutil.rmtree (run_prefix)
511         prefix = os.path.join (env['out'], 'usr')
512         map (lambda x: symlink (x[0], os.path.join (prefix, x[1])),
513              (('python', 'lib/lilypond/python'),
514               # UGHR, lilypond.py uses lilypond-bin from PATH
515               ('lily',   'bin'),
516               ('#mf',    'share/lilypond/fonts/mf'),
517               ('mf',     'share/lilypond/fonts/afm'),
518               ('mf',     'share/lilypond/fonts/tfm'),
519               ('mf',     'share/lilypond/fonts/type1'),
520               ('#tex',   'share/lilypond/tex/source'),
521               ('mf',     'share/lilypond/tex/generate'),
522               ('#ly',    'share/lilypond/ly'),
523               ('#scm',   'share/lilypond/scm'),
524               ('#ps',    'share/lilypond/ps'),
525               ('elisp',  'share/lilypond/elisp')))
526         os.chdir (srcdir)
527
528 if env['debugging']:
529         stamp = os.path.join (run_prefix, 'stamp')
530         env.Command (stamp, 'SConstruct', [symlink_tree, 'touch $TARGET'])
531         env.Depends ('lily', stamp)
532
533 #### dist, tar
534 def plus (a, b):
535         a + b
536
537 def cvs_entry_is_dir (line):
538         return line[0] == 'D' and line[-2] == '/'
539
540 def cvs_entry_is_file (line):
541         return line[0] == '/' and line[-2] == '/'
542
543 def cvs_dirs (dir):
544         ENTRIES = os.path.join (dir, 'CVS/Entries')
545         if not os.path.exists (ENTRIES):
546                 return []
547         entries = open (ENTRIES).readlines ()
548         dir_entries = filter (cvs_entry_is_dir, entries)
549         dirs = map (lambda x: os.path.join (dir, x[2:x[2:].index ('/')+3]),
550                     dir_entries)
551         return dirs + map (cvs_dirs, dirs)
552
553 def cvs_files (dir):
554         ENTRIES = os.path.join (dir, 'CVS/Entries')
555         entries = open (ENTRIES).readlines ()
556         file_entries = filter (cvs_entry_is_file, entries)
557         files = map (lambda x: x[1:x[1:].index ('/')+1], file_entries)
558         return map (lambda x: os.path.join (dir, x), files)
559
560 #subdirs = reduce (lambda x, y: x + y, cvs_dirs ('.'))
561 #print `subdirs`
562 readme_files = ['AUTHORS', 'README', 'INSTALL', 'NEWS']
563 foo = map (lambda x: env.TXT (x + '.txt',
564                               os.path.join ('Documentation/topdocs', x)),
565            readme_files)
566 txt_files = map (lambda x: x + '.txt', readme_files)
567 src_files = reduce (lambda x, y: x + y, map (cvs_files, subdirs))
568 tar_base = package.name + '-' + version
569 tar_name = tar_base + '.tar.gz'
570 ball_prefix = os.path.join (outdir, tar_base)
571 tar_ball = os.path.join (outdir, tar_name)
572
573 dist_files = src_files + txt_files
574 ball_files = map (lambda x: os.path.join (ball_prefix, x), dist_files)
575 map (lambda x: env.Depends (tar_ball, x), ball_files)
576 map (lambda x: env.Command (os.path.join (ball_prefix, x), x,
577                             'ln $SOURCE $TARGET'), dist_files)
578 tar = env.Command (tar_ball, src_files,
579                    'tar czf $TARGET -C $TARGET.dir %s' % tar_base)
580 env.Alias ('tar', tar)
581
582 dist_ball = os.path.join (package.release_dir, tar_name)
583 env.Command (dist_ball, tar_ball,
584              'if [ -e $SOURCE -a -e $TARGET ]; then rm $TARGET; fi;' \
585              + 'ln $SOURCE $TARGET')
586 env.Depends ('dist', dist_ball)
587 patch_name = os.path.join (outdir, tar_base + '.diff.gz')
588 patch = env.PATCH (patch_name, tar_ball)
589 env.Depends (patch_name, dist_ball)
590 env.Alias ('release', patch)
591
592
593 for d in subdirs:
594         if os.path.exists (os.path.join (d, 'SConscript')):
595                 b = os.path.join (env['build'], d, env['out'])
596                 # Support clean sourcetree build (--srcdir build)
597                 # and ./out build.
598                 if os.path.abspath (b) != os.path.abspath (d):
599                         env.BuildDir (b, d, duplicate = 0)
600                 SConscript (os.path.join (b, 'SConscript'))