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