]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
Merge remote branch 'origin' into release/unstable
[lilypond.git] / scripts / lilypond-book.py
1 #!@TARGET_PYTHON@
2 # -*- coding: utf-8 -*-
3
4 # This file is part of LilyPond, the GNU music typesetter.
5 #
6 # LilyPond is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # LilyPond is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with LilyPond.  If not, see <http://www.gnu.org/licenses/>.
18
19 '''
20 Example usage:
21
22 test:
23   lilypond-book --filter="tr '[a-z]' '[A-Z]'" BOOK
24
25 convert-ly on book:
26   lilypond-book --filter="convert-ly --no-version --from=1.6.11 -" BOOK
27
28 classic lilypond-book:
29   lilypond-book --process="lilypond" BOOK.tely
30
31 TODO:
32
33   *  ly-options: intertext?
34   *  --line-width?
35   *  eps in latex / eps by lilypond -b ps?
36   *  check latex parameters, twocolumn, multicolumn?
37   *  use --png --ps --pdf for making images?
38
39   *  Converting from lilypond-book source, substitute:
40    @mbinclude foo.itely -> @include foo.itely
41    \mbinput -> \input
42
43 '''
44
45
46 # TODO: Better solve the global_options copying to the snippets...
47
48 import glob
49 import os
50 import re
51 import stat
52 import sys
53 import tempfile
54 import imp
55 from optparse import OptionGroup
56
57
58 """
59 @relocate-preamble@
60 """
61
62 import lilylib as ly
63 import fontextract
64 import langdefs
65 global _;_=ly._
66
67 import book_base as BookBase
68 import book_snippets as BookSnippet
69 import book_html
70 import book_docbook
71 import book_texinfo
72 import book_latex
73
74 ly.require_python_version ()
75
76 original_dir = os.getcwd ()
77 backend = 'ps'
78
79 help_summary = (
80 _ ("Process LilyPond snippets in hybrid HTML, LaTeX, texinfo or DocBook document.")
81 + '\n\n'
82 + _ ("Examples:")
83 + '''
84  $ lilypond-book --filter="tr '[a-z]' '[A-Z]'" %(BOOK)s
85  $ lilypond-book -F "convert-ly --no-version --from=2.0.0 -" %(BOOK)s
86  $ lilypond-book --process='lilypond -I include' %(BOOK)s
87 ''' % {'BOOK': _ ("BOOK")})
88
89 authors = ('Jan Nieuwenhuizen <janneke@gnu.org>',
90            'Han-Wen Nienhuys <hanwen@xs4all.nl>')
91
92 ################################################################
93 def exit (i):
94     if global_options.verbose:
95         raise Exception (_ ('Exiting (%d)...') % i)
96     else:
97         sys.exit (i)
98
99 def identify ():
100     ly.encoded_write (sys.stdout, '%s (GNU LilyPond) %s\n' % (ly.program_name, ly.program_version))
101
102 progress = ly.progress
103 warning = ly.warning
104 error = ly.error
105
106
107 def warranty ():
108     identify ()
109     ly.encoded_write (sys.stdout, '''
110 %s
111
112   %s
113
114 %s
115 %s
116 ''' % ( _ ('Copyright (c) %s by') % '2001--2011',
117         '\n  '.join (authors),
118         _ ("Distributed under terms of the GNU General Public License."),
119         _ ("It comes with NO WARRANTY.")))
120
121 def get_option_parser ():
122     p = ly.get_option_parser (usage=_ ("%s [OPTION]... FILE") % 'lilypond-book',
123                               description=help_summary,
124                               conflict_handler="resolve",
125                               add_help_option=False)
126
127     p.add_option ('-F', '--filter', metavar=_ ("FILTER"),
128                   action="store",
129                   dest="filter_cmd",
130                   help=_ ("pipe snippets through FILTER [default: `convert-ly -n -']"),
131                   default=None)
132
133     p.add_option ('-f', '--format',
134                   help=_ ("use output format FORMAT (texi [default], texi-html, latex, html, docbook)"),
135                   metavar=_ ("FORMAT"),
136                   action='store')
137
138     p.add_option("-h", "--help",
139                  action="help",
140                  help=_ ("show this help and exit"))
141
142     p.add_option ("-I", '--include', help=_ ("add DIR to include path"),
143                   metavar=_ ("DIR"),
144                   action='append', dest='include_path',
145                   default=[os.path.abspath (os.getcwd ())])
146
147     p.add_option ('--info-images-dir',
148                   help=_ ("format Texinfo output so that Info will "
149                           "look for images of music in DIR"),
150                   metavar=_ ("DIR"),
151                   action='store', dest='info_images_dir',
152                   default='')
153
154     p.add_option ('--left-padding',
155                   metavar=_ ("PAD"),
156                   dest="padding_mm",
157                   help=_ ("pad left side of music to align music inspite of uneven bar numbers (in mm)"),
158                   type="float",
159                   default=3.0)
160
161     p.add_option ('--lily-output-dir',
162                   help=_ ("write lily-XXX files to DIR, link into --output dir"),
163                   metavar=_ ("DIR"),
164                   action='store', dest='lily_output_dir',
165                   default=None)
166
167     p.add_option ('--load-custom-package', help=_ ("Load the additional python PACKAGE (containing e.g. a custom output format)"),
168                   metavar=_ ("PACKAGE"),
169                   action='append', dest='custom_packages',
170                   default=[])
171
172     p.add_option ("-o", '--output', help=_ ("write output to DIR"),
173                   metavar=_ ("DIR"),
174                   action='store', dest='output_dir',
175                   default='')
176
177     p.add_option ('-P', '--process', metavar=_ ("COMMAND"),
178                   help = _ ("process ly_files using COMMAND FILE..."),
179                   action='store',
180                   dest='process_cmd', default='')
181
182     p.add_option ('--redirect-lilypond-output',
183                   help = _ ("Redirect the lilypond output"),
184                   action='store_true',
185                   dest='redirect_output', default=False)
186
187     p.add_option ('-s', '--safe', help=_ ("Compile snippets in safe mode"),
188                   action="store_true",
189                   default=False,
190                   dest="safe_mode")
191
192     p.add_option ('--skip-lily-check',
193                   help=_ ("do not fail if no lilypond output is found"),
194                   metavar=_ ("DIR"),
195                   action='store_true', dest='skip_lilypond_run',
196                   default=False)
197
198     p.add_option ('--skip-png-check',
199                   help=_ ("do not fail if no PNG images are found for EPS files"),
200                   metavar=_ ("DIR"),
201                   action='store_true', dest='skip_png_check',
202                   default=False)
203
204     p.add_option ('--use-source-file-names',
205                   help=_ ("write snippet output files with the same base name as their source file"),
206                   action='store_true', dest='use_source_file_names',
207                   default=False)
208
209     p.add_option ('-V', '--verbose', help=_ ("be verbose"),
210                   action="store_true",
211                   default=False,
212                   dest="verbose")
213
214     p.version = "@TOPLEVEL_VERSION@"
215     p.add_option("--version",
216                  action="version",
217                  help=_ ("show version number and exit"))
218
219     p.add_option ('-w', '--warranty',
220                   help=_ ("show warranty and copyright"),
221                   action='store_true')
222
223     group = OptionGroup (p, "Options only for the latex and texinfo backends")
224     group.add_option ('--latex-program',
225               help=_ ("run executable PROG instead of latex, or in\n\
226 case --pdf option is set instead of pdflatex"),
227               metavar=_ ("PROG"),
228               action='store', dest='latex_program',
229               default='latex')
230     group.add_option ('--pdf',
231               action="store_true",
232               dest="create_pdf",
233               help=_ ("create PDF files for use with PDFTeX"),
234               default=False)
235     p.add_option_group (group)
236
237     p.add_option_group ('',
238                         description=(
239         _ ("Report bugs via %s")
240         % ' http://post.gmane.org/post.php'
241         '?group=gmane.comp.gnu.lilypond.bugs') + '\n')
242
243
244     for formatter in BookBase.all_formats:
245       formatter.add_options (p)
246
247     return p
248
249 lilypond_binary = os.path.join ('@bindir@', 'lilypond')
250
251 # If we are called with full path, try to use lilypond binary
252 # installed in the same path; this is needed in GUB binaries, where
253 # @bindir is always different from the installed binary path.
254 if 'bindir' in globals () and bindir:
255     lilypond_binary = os.path.join (bindir, 'lilypond')
256
257 # Only use installed binary when we are installed too.
258 if '@bindir@' == ('@' + 'bindir@') or not os.path.exists (lilypond_binary):
259     lilypond_binary = 'lilypond'
260
261 global_options = None
262
263
264
265
266 def find_linestarts (s):
267     nls = [0]
268     start = 0
269     end = len (s)
270     while 1:
271         i = s.find ('\n', start)
272         if i < 0:
273             break
274
275         i = i + 1
276         nls.append (i)
277         start = i
278
279     nls.append (len (s))
280     return nls
281
282 def find_toplevel_snippets (input_string, formatter):
283     res = {}
284     types = formatter.supported_snippet_types ()
285     for t in types:
286         res[t] = re.compile (formatter.snippet_regexp (t))
287
288     snippets = []
289     index = 0
290     found = dict ([(t, None) for t in types])
291
292     line_starts = find_linestarts (input_string)
293     line_start_idx = 0
294     # We want to search for multiple regexes, without searching
295     # the string multiple times for one regex.
296     # Hence, we use earlier results to limit the string portion
297     # where we search.
298     # Since every part of the string is traversed at most once for
299     # every type of snippet, this is linear.
300     while 1:
301         first = None
302         endex = 1 << 30
303         for type in types:
304             if not found[type] or found[type][0] < index:
305                 found[type] = None
306
307                 m = res[type].search (input_string[index:endex])
308                 if not m:
309                     continue
310
311                 klass = global_options.formatter.snippet_class (type)
312
313                 start = index + m.start ('match')
314                 line_number = line_start_idx
315                 while (line_starts[line_number] < start):
316                     line_number += 1
317
318                 line_number += 1
319                 snip = klass (type, m, formatter, line_number, global_options)
320
321                 found[type] = (start, snip)
322
323             if (found[type]
324                 and (not first
325                      or found[type][0] < found[first][0])):
326                 first = type
327
328                 # FIXME.
329
330                 # Limiting the search space is a cute
331                 # idea, but this *requires* to search
332                 # for possible containing blocks
333                 # first, at least as long as we do not
334                 # search for the start of blocks, but
335                 # always/directly for the entire
336                 # @block ... @end block.
337
338                 endex = found[first][0]
339
340         if not first:
341             snippets.append (BookSnippet.Substring (input_string, index, len (input_string), line_start_idx))
342             break
343
344         while (start > line_starts[line_start_idx+1]):
345             line_start_idx += 1
346
347         (start, snip) = found[first]
348         snippets.append (BookSnippet.Substring (input_string, index, start, line_start_idx + 1))
349         snippets.append (snip)
350         found[first] = None
351         index = start + len (snip.match.group ('match'))
352
353     return snippets
354
355 def system_in_directory (cmd, directory, logfile):
356     """Execute a command in a different directory.
357
358     Because of win32 compatibility, we can't simply use subprocess.
359     """
360
361     current = os.getcwd()
362     os.chdir (directory)
363     ly.system(cmd,
364               be_verbose=global_options.verbose,
365               redirect_output=global_options.redirect_output,
366               log_file=logfile,
367               progress_p=1)
368     os.chdir (current)
369
370
371 def process_snippets (cmd, snippets,
372                       formatter, lily_output_dir):
373     """Run cmd on all of the .ly files from snippets."""
374
375     if not snippets:
376         return
377
378     cmd = formatter.adjust_snippet_command (cmd)
379
380     checksum = snippet_list_checksum (snippets)
381     contents = '\n'.join (['snippet-map-%d.ly' % checksum]
382                           + list (set ([snip.basename() + '.ly' for snip in snippets])))
383     name = os.path.join (lily_output_dir,
384                          'snippet-names-%d.ly' % checksum)
385     logfile = name.replace('.ly', '')
386     file (name, 'wb').write (contents)
387
388     system_in_directory (' '.join ([cmd, ly.mkarg (name)]),
389                          lily_output_dir,
390                          logfile)
391
392
393 def snippet_list_checksum (snippets):
394     return hash (' '.join([l.basename() for l in snippets]))
395
396 def write_file_map (lys, name):
397     snippet_map = file (os.path.join (
398         global_options.lily_output_dir,
399         'snippet-map-%d.ly' % snippet_list_checksum (lys)), 'w')
400
401     snippet_map.write ("""
402 #(define version-seen #t)
403 #(define output-empty-score-list #f)
404 #(ly:add-file-name-alist '(%s
405     ))\n
406 """ % '\n'.join(['("%s.ly" . "%s")\n' % (ly.basename (), name)
407                  for ly in lys]))
408
409 def split_output_files(directory):
410     """Returns directory entries in DIRECTORY/XX/ , where XX are hex digits.
411
412     Return value is a set of strings.
413     """
414     files = []
415     for subdir in glob.glob (os.path.join (directory, '[a-f0-9][a-f0-9]')):
416         base_subdir = os.path.split (subdir)[1]
417         sub_files = [os.path.join (base_subdir, name)
418                      for name in os.listdir (subdir)]
419         files += sub_files
420     return set (files)
421
422 def do_process_cmd (chunks, input_name, options):
423     snippets = [c for c in chunks if isinstance (c, BookSnippet.LilypondSnippet)]
424
425     output_files = split_output_files (options.lily_output_dir)
426     outdated = [c for c in snippets if c.is_outdated (options.lily_output_dir, output_files)]
427
428     write_file_map (outdated, input_name)
429     progress (_ ("Writing snippets..."))
430     for snippet in outdated:
431         snippet.write_ly()
432     progress ('\n')
433
434     if outdated:
435         progress (_ ("Processing..."))
436         progress ('\n')
437         process_snippets (options.process_cmd, outdated,
438                           options.formatter, options.lily_output_dir)
439
440     else:
441         progress (_ ("All snippets are up to date..."))
442
443     if options.lily_output_dir != options.output_dir:
444         output_files = split_output_files (options.lily_output_dir)
445         for snippet in snippets:
446             snippet.link_all_output_files (options.lily_output_dir,
447                                            output_files,
448                                            options.output_dir)
449
450     progress ('\n')
451
452
453 ###
454 # Format guessing data
455
456 def guess_format (input_filename):
457     format = None
458     e = os.path.splitext (input_filename)[1]
459     for formatter in BookBase.all_formats:
460       if formatter.can_handle_extension (e):
461         return formatter
462     error (_ ("cannot determine format for: %s" % input_filename))
463     exit (1)
464
465 def write_if_updated (file_name, lines):
466     try:
467         f = file (file_name)
468         oldstr = f.read ()
469         new_str = ''.join (lines)
470         if oldstr == new_str:
471             progress (_ ("%s is up to date.") % file_name)
472             progress ('\n')
473
474             # this prevents make from always rerunning lilypond-book:
475             # output file must be touched in order to be up to date
476             os.utime (file_name, None)
477             return
478     except:
479         pass
480
481     output_dir = os.path.dirname (file_name)
482     if not os.path.exists (output_dir):
483         os.makedirs (output_dir)
484
485     progress (_ ("Writing `%s'...") % file_name)
486     file (file_name, 'w').writelines (lines)
487     progress ('\n')
488
489
490 def note_input_file (name, inputs=[]):
491     ## hack: inputs is mutable!
492     inputs.append (name)
493     return inputs
494
495 def samefile (f1, f2):
496     try:
497         return os.path.samefile (f1, f2)
498     except AttributeError:                # Windoze
499         f1 = re.sub ("//*", "/", f1)
500         f2 = re.sub ("//*", "/", f2)
501         return f1 == f2
502
503 def do_file (input_filename, included=False):
504     # Ugh.
505     input_absname = input_filename
506     if not input_filename or input_filename == '-':
507         in_handle = sys.stdin
508         input_fullname = '<stdin>'
509     else:
510         if os.path.exists (input_filename):
511             input_fullname = input_filename
512         else:
513             input_fullname = global_options.formatter.input_fullname (input_filename)
514         # Normalize path to absolute path, since we will change cwd to the output dir!
515         # Otherwise, "lilypond-book -o out test.tex" will complain that it is
516         # overwriting the input file (which it is actually not), since the
517         # input filename is relative to the CWD...
518         input_absname = os.path.abspath (input_fullname)
519
520         note_input_file (input_fullname)
521         in_handle = file (input_fullname)
522
523     if input_filename == '-':
524         input_base = 'stdin'
525     elif included:
526         input_base = os.path.splitext (input_filename)[0]
527     else:
528         input_base = os.path.basename (
529             os.path.splitext (input_filename)[0])
530
531     # don't complain when global_options.output_dir is existing
532     if not global_options.output_dir:
533         global_options.output_dir = os.getcwd()
534     else:
535         global_options.output_dir = os.path.abspath(global_options.output_dir)
536
537         if not os.path.isdir (global_options.output_dir):
538             os.mkdir (global_options.output_dir, 0777)
539         os.chdir (global_options.output_dir)
540
541     output_filename = os.path.join(global_options.output_dir,
542                                    input_base + global_options.formatter.default_extension)
543     if (os.path.exists (input_filename)
544         and os.path.exists (output_filename)
545         and samefile (output_filename, input_absname)):
546      error (
547      _ ("Output would overwrite input file; use --output."))
548      exit (2)
549
550     try:
551         progress (_ ("Reading %s...") % input_fullname)
552         source = in_handle.read ()
553         progress ('\n')
554
555         if not included:
556             global_options.formatter.init_default_snippet_options (source)
557
558
559         progress (_ ("Dissecting..."))
560         chunks = find_toplevel_snippets (source, global_options.formatter)
561
562         # Let the formatter modify the chunks before further processing
563         chunks = global_options.formatter.process_chunks (chunks)
564         progress ('\n')
565
566         if global_options.filter_cmd:
567             write_if_updated (output_filename,
568                      [c.filter_text () for c in chunks])
569         elif global_options.process_cmd:
570             do_process_cmd (chunks, input_fullname, global_options)
571             progress (_ ("Compiling %s...") % output_filename)
572             progress ('\n')
573             write_if_updated (output_filename,
574                      [s.replacement_text ()
575                      for s in chunks])
576
577         def process_include (snippet):
578             os.chdir (original_dir)
579             name = snippet.substring ('filename')
580             progress (_ ("Processing include: %s") % name)
581             progress ('\n')
582             return do_file (name, included=True)
583
584         include_chunks = map (process_include,
585                               filter (lambda x: isinstance (x, BookSnippet.IncludeSnippet),
586                                       chunks))
587
588         return chunks + reduce (lambda x, y: x + y, include_chunks, [])
589
590     except BookSnippet.CompileError:
591         os.chdir (original_dir)
592         progress (_ ("Removing `%s'") % output_filename)
593         progress ('\n')
594         raise BookSnippet.CompileError
595
596 def do_options ():
597     global global_options
598
599     opt_parser = get_option_parser()
600     (global_options, args) = opt_parser.parse_args ()
601
602     global_options.information = {'program_version': ly.program_version, 'program_name': ly.program_name }
603
604     global_options.include_path =  map (os.path.abspath, global_options.include_path)
605
606     # Load the python packages (containing e.g. custom formatter classes)
607     # passed on the command line
608     nr = 0
609     for i in global_options.custom_packages:
610         nr += 1
611         print imp.load_source ("book_custom_package%s" % nr, i)
612
613
614     if global_options.warranty:
615         warranty ()
616         exit (0)
617     if not args or len (args) > 1:
618         opt_parser.print_help ()
619         exit (2)
620
621     return args
622
623 def main ():
624     # FIXME: 85 lines of `main' macramee??
625     files = do_options ()
626
627     basename = os.path.splitext (files[0])[0]
628     basename = os.path.split (basename)[1]
629
630     if global_options.format:
631       # Retrieve the formatter for the given format
632       for formatter in BookBase.all_formats:
633         if formatter.can_handle_format (global_options.format):
634           global_options.formatter = formatter
635     else:
636         global_options.formatter = guess_format (files[0])
637         global_options.format = global_options.formatter.format
638
639     # make the global options available to the formatters:
640     global_options.formatter.global_options = global_options
641     formats = global_options.formatter.image_formats
642
643     if global_options.process_cmd == '':
644         global_options.process_cmd = (lilypond_binary
645                                       + ' --formats=%s -dbackend=eps ' % formats)
646
647     if global_options.process_cmd:
648         includes = global_options.include_path
649         if global_options.lily_output_dir:
650             # This must be first, so lilypond prefers to read .ly
651             # files in the other lybookdb dir.
652             includes = [os.path.abspath(global_options.lily_output_dir)] + includes
653         global_options.process_cmd += ' '.join ([' -I %s' % ly.mkarg (p)
654                                                  for p in includes])
655
656     global_options.formatter.process_options (global_options)
657
658     if global_options.verbose:
659         global_options.process_cmd += " --verbose "
660
661     if global_options.padding_mm:
662         global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
663
664     global_options.process_cmd += " -dread-file-list -dno-strip-output-dir"
665
666     if global_options.lily_output_dir:
667         global_options.lily_output_dir = os.path.abspath(global_options.lily_output_dir)
668         if not os.path.isdir (global_options.lily_output_dir):
669             os.makedirs (global_options.lily_output_dir)
670     else:
671         global_options.lily_output_dir = os.path.abspath(global_options.output_dir)
672
673
674     identify ()
675     try:
676         chunks = do_file (files[0])
677     except BookSnippet.CompileError:
678         exit (1)
679
680     inputs = note_input_file ('')
681     inputs.pop ()
682
683     base_file_name = os.path.splitext (os.path.basename (files[0]))[0]
684     dep_file = os.path.join (global_options.output_dir, base_file_name + '.dep')
685     final_output_file = os.path.join (global_options.output_dir,
686                      base_file_name
687                      + '.%s' % global_options.format)
688
689     os.chdir (original_dir)
690     file (dep_file, 'w').write ('%s: %s'
691                                 % (final_output_file, ' '.join (inputs)))
692
693 if __name__ == '__main__':
694     main ()