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