]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
Merge branch 'lilypond/translation' into staging
[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 ly.is_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--2012',
117         '\n  '.join (authors),
118         _ ("Distributed under terms of the GNU General Public License."),
119         _ ("It comes with NO WARRANTY.")))
120
121
122 def get_option_parser ():
123     p = ly.get_option_parser (usage=_ ("%s [OPTION]... FILE") % 'lilypond-book',
124                               description=help_summary,
125                               conflict_handler="resolve",
126                               add_help_option=False)
127
128     p.add_option ('-F', '--filter', metavar=_ ("FILTER"),
129                   action="store",
130                   dest="filter_cmd",
131                   help=_ ("pipe snippets through FILTER [default: `convert-ly -n -']"),
132                   default=None)
133
134     p.add_option ('-f', '--format',
135                   help=_ ("use output format FORMAT (texi [default], texi-html, latex, html, docbook)"),
136                   metavar=_ ("FORMAT"),
137                   action='store')
138
139     p.add_option("-h", "--help",
140                  action="help",
141                  help=_ ("show this help and exit"))
142
143     p.add_option ("-I", '--include', help=_ ("add DIR to include path"),
144                   metavar=_ ("DIR"),
145                   action='append', dest='include_path',
146                   default=[os.path.abspath (os.getcwd ())])
147
148     p.add_option ('--info-images-dir',
149                   help=_ ("format Texinfo output so that Info will "
150                           "look for images of music in DIR"),
151                   metavar=_ ("DIR"),
152                   action='store', dest='info_images_dir',
153                   default='')
154
155     p.add_option ('--left-padding',
156                   metavar=_ ("PAD"),
157                   dest="padding_mm",
158                   help=_ ("pad left side of music to align music inspite of uneven bar numbers (in mm)"),
159                   type="float",
160                   default=3.0)
161
162     p.add_option ('--lily-loglevel',
163                   help=_ ("Print lilypond log messages according to LOGLEVEL"),
164                   metavar=_ ("LOGLEVEL"),
165                   action='store', dest='lily_loglevel',
166                   default=os.environ.get ("LILYPOND_LOGLEVEL", None))
167
168     p.add_option ('--lily-output-dir',
169                   help=_ ("write lily-XXX files to DIR, link into --output dir"),
170                   metavar=_ ("DIR"),
171                   action='store', dest='lily_output_dir',
172                   default=None)
173
174     p.add_option ('--load-custom-package', help=_ ("Load the additional python PACKAGE (containing e.g. a custom output format)"),
175                   metavar=_ ("PACKAGE"),
176                   action='append', dest='custom_packages',
177                   default=[])
178
179     p.add_option ("-l", "--loglevel",
180                   help=_ ("Print log messages according to LOGLEVEL "
181                           "(NONE, ERROR, WARNING, PROGRESS (default), DEBUG)"),
182                   metavar=_ ("LOGLEVEL"),
183                   action='callback',
184                   callback=ly.handle_loglevel_option,
185                   type='string')
186
187     p.add_option ("-o", '--output', help=_ ("write output to DIR"),
188                   metavar=_ ("DIR"),
189                   action='store', dest='output_dir',
190                   default='')
191
192     p.add_option ('-P', '--process', metavar=_ ("COMMAND"),
193                   help = _ ("process ly_files using COMMAND FILE..."),
194                   action='store',
195                   dest='process_cmd', default='')
196
197     p.add_option ('--redirect-lilypond-output',
198                   help = _ ("Redirect the lilypond output"),
199                   action='store_true',
200                   dest='redirect_output', default=False)
201
202     p.add_option ('-s', '--safe', help=_ ("Compile snippets in safe mode"),
203                   action="store_true",
204                   default=False,
205                   dest="safe_mode")
206
207     p.add_option ('--skip-lily-check',
208                   help=_ ("do not fail if no lilypond output is found"),
209                   metavar=_ ("DIR"),
210                   action='store_true', dest='skip_lilypond_run',
211                   default=False)
212
213     p.add_option ('--skip-png-check',
214                   help=_ ("do not fail if no PNG images are found for EPS files"),
215                   metavar=_ ("DIR"),
216                   action='store_true', dest='skip_png_check',
217                   default=False)
218
219     p.add_option ('--use-source-file-names',
220                   help=_ ("write snippet output files with the same base name as their source file"),
221                   action='store_true', dest='use_source_file_names',
222                   default=False)
223
224     p.add_option ('-V', '--verbose', help=_ ("be verbose"),
225                   action="callback",
226                   callback=ly.handle_loglevel_option,
227                   callback_args=("DEBUG",))
228
229     p.version = "@TOPLEVEL_VERSION@"
230     p.add_option("--version",
231                  action="version",
232                  help=_ ("show version number and exit"))
233
234     p.add_option ('-w', '--warranty',
235                   help=_ ("show warranty and copyright"),
236                   action='store_true')
237
238     group = OptionGroup (p, "Options only for the latex and texinfo backends")
239     group.add_option ('--latex-program',
240               help=_ ("run executable PROG instead of latex, or in\n\
241 case --pdf option is set instead of pdflatex"),
242               metavar=_ ("PROG"),
243               action='store', dest='latex_program',
244               default='latex')
245     group.add_option ('--texinfo-program',
246               help=_ ("run executable PROG instead of texi2pdf"),
247               metavar=_ ("PROG"),
248               action='store', dest='texinfo_program',
249               default='texi2pdf')
250     group.add_option ('--pdf',
251               action="store_true",
252               dest="create_pdf",
253               help=_ ("create PDF files for use with PDFTeX"),
254               default=False)
255     p.add_option_group (group)
256
257     p.add_option_group ('',
258                         description=(
259         _ ("Report bugs via %s")
260         % ' http://post.gmane.org/post.php'
261         '?group=gmane.comp.gnu.lilypond.bugs') + '\n')
262
263
264     for formatter in BookBase.all_formats:
265       formatter.add_options (p)
266
267     return p
268
269 lilypond_binary = os.path.join ('@bindir@', 'lilypond')
270
271 # If we are called with full path, try to use lilypond binary
272 # installed in the same path; this is needed in GUB binaries, where
273 # @bindir is always different from the installed binary path.
274 if 'bindir' in globals () and bindir:
275     lilypond_binary = os.path.join (bindir, 'lilypond')
276
277 # Only use installed binary when we are installed too.
278 if '@bindir@' == ('@' + 'bindir@') or not os.path.exists (lilypond_binary):
279     lilypond_binary = 'lilypond'
280
281 global_options = None
282
283
284
285
286 def find_linestarts (s):
287     nls = [0]
288     start = 0
289     end = len (s)
290     while 1:
291         i = s.find ('\n', start)
292         if i < 0:
293             break
294
295         i = i + 1
296         nls.append (i)
297         start = i
298
299     nls.append (len (s))
300     return nls
301
302 def find_toplevel_snippets (input_string, formatter):
303     res = {}
304     types = formatter.supported_snippet_types ()
305     for t in types:
306         res[t] = re.compile (formatter.snippet_regexp (t))
307
308     snippets = []
309     index = 0
310     found = dict ([(t, None) for t in types])
311
312     line_starts = find_linestarts (input_string)
313     line_start_idx = 0
314     # We want to search for multiple regexes, without searching
315     # the string multiple times for one regex.
316     # Hence, we use earlier results to limit the string portion
317     # where we search.
318     # Since every part of the string is traversed at most once for
319     # every type of snippet, this is linear.
320     while 1:
321         first = None
322         endex = 1 << 30
323         for type in types:
324             if not found[type] or found[type][0] < index:
325                 found[type] = None
326
327                 m = res[type].search (input_string[index:endex])
328                 if not m:
329                     continue
330
331                 klass = global_options.formatter.snippet_class (type)
332
333                 start = index + m.start ('match')
334                 line_number = line_start_idx
335                 while (line_starts[line_number] < start):
336                     line_number += 1
337
338                 line_number += 1
339                 snip = klass (type, m, formatter, line_number, global_options)
340
341                 found[type] = (start, snip)
342
343             if (found[type]
344                 and (not first
345                      or found[type][0] < found[first][0])):
346                 first = type
347
348                 # FIXME.
349
350                 # Limiting the search space is a cute
351                 # idea, but this *requires* to search
352                 # for possible containing blocks
353                 # first, at least as long as we do not
354                 # search for the start of blocks, but
355                 # always/directly for the entire
356                 # @block ... @end block.
357
358                 endex = found[first][0]
359
360         if not first:
361             snippets.append (BookSnippet.Substring (input_string, index, len (input_string), line_start_idx))
362             break
363
364         while (start > line_starts[line_start_idx+1]):
365             line_start_idx += 1
366
367         (start, snip) = found[first]
368         snippets.append (BookSnippet.Substring (input_string, index, start, line_start_idx + 1))
369         snippets.append (snip)
370         found[first] = None
371         index = start + len (snip.match.group ('match'))
372
373     return snippets
374
375 def system_in_directory (cmd, directory, logfile):
376     """Execute a command in a different directory.
377
378     Because of win32 compatibility, we can't simply use subprocess.
379     """
380
381     current = os.getcwd()
382     os.chdir (directory)
383     """NB - ignore_error is deliberately set to the same value
384     as redirect_output - this is not a typo."""
385     retval = ly.system(cmd,
386               be_verbose=ly.is_verbose (),
387               redirect_output=global_options.redirect_output,
388               log_file=logfile,
389               progress_p=1,
390               ignore_error=global_options.redirect_output)
391     if retval != 0:
392         print ("Error trapped by lilypond-book")
393         print ("\nPlease see " + logfile + ".log\n")
394         sys.exit(1)
395
396     os.chdir (current)
397
398
399 def process_snippets (cmd, snippets,
400                       formatter, lily_output_dir):
401     """Run cmd on all of the .ly files from snippets."""
402
403     if not snippets:
404         return
405
406     cmd = formatter.adjust_snippet_command (cmd)
407
408     checksum = snippet_list_checksum (snippets)
409     contents = '\n'.join (['snippet-map-%d.ly' % checksum]
410                           + list (set ([snip.basename() + '.ly' for snip in snippets])))
411     name = os.path.join (lily_output_dir,
412                          'snippet-names-%d.ly' % checksum)
413     logfile = name.replace('.ly', '')
414     file (name, 'wb').write (contents)
415
416     system_in_directory (' '.join ([cmd, ly.mkarg (name)]),
417                          lily_output_dir,
418                          logfile)
419
420 def snippet_list_checksum (snippets):
421     return hash (' '.join([l.basename() for l in snippets]))
422
423 def write_file_map (lys, name):
424     snippet_map = file (os.path.join (
425         global_options.lily_output_dir,
426         'snippet-map-%d.ly' % snippet_list_checksum (lys)), 'w')
427
428     snippet_map.write ("""
429 #(define version-seen #t)
430 #(define output-empty-score-list #f)
431 #(ly:add-file-name-alist '(%s
432     ))\n
433 """ % '\n'.join(['("%s.ly" . "%s")\n' % (ly.basename ().replace('\\','/'), name)
434                  for ly in lys]))
435
436 def split_output_files(directory):
437     """Returns directory entries in DIRECTORY/XX/ , where XX are hex digits.
438
439     Return value is a set of strings.
440     """
441     files = []
442     for subdir in glob.glob (os.path.join (directory, '[a-f0-9][a-f0-9]')):
443         base_subdir = os.path.split (subdir)[1]
444         sub_files = [os.path.join (base_subdir, name)
445                      for name in os.listdir (subdir)]
446         files += sub_files
447     return set (files)
448
449 def do_process_cmd (chunks, input_name, options):
450     snippets = [c for c in chunks if isinstance (c, BookSnippet.LilypondSnippet)]
451
452     output_files = split_output_files (options.lily_output_dir)
453     outdated = [c for c in snippets if c.is_outdated (options.lily_output_dir, output_files)]
454
455     write_file_map (outdated, input_name)
456     progress (_ ("Writing snippets..."))
457     for snippet in outdated:
458         snippet.write_ly()
459
460     if outdated:
461         progress (_ ("Processing..."))
462         process_snippets (options.process_cmd, outdated,
463                           options.formatter, options.lily_output_dir)
464
465     else:
466         progress (_ ("All snippets are up to date..."))
467
468     if options.lily_output_dir != options.output_dir:
469         output_files = split_output_files (options.lily_output_dir)
470         for snippet in snippets:
471             snippet.link_all_output_files (options.lily_output_dir,
472                                            output_files,
473                                            options.output_dir)
474
475
476 ###
477 # Format guessing data
478
479 def guess_format (input_filename):
480     format = None
481     e = os.path.splitext (input_filename)[1]
482     for formatter in BookBase.all_formats:
483       if formatter.can_handle_extension (e):
484         return formatter
485     error (_ ("cannot determine format for: %s" % input_filename))
486     exit (1)
487
488 def write_if_updated (file_name, lines):
489     try:
490         f = file (file_name)
491         oldstr = f.read ()
492         new_str = ''.join (lines)
493         if oldstr == new_str:
494             progress (_ ("%s is up to date.") % file_name)
495
496             # this prevents make from always rerunning lilypond-book:
497             # output file must be touched in order to be up to date
498             os.utime (file_name, None)
499             return
500     except:
501         pass
502
503     output_dir = os.path.dirname (file_name)
504     if not os.path.exists (output_dir):
505         os.makedirs (output_dir)
506
507     progress (_ ("Writing `%s'...") % file_name)
508     file (file_name, 'w').writelines (lines)
509
510
511 def note_input_file (name, inputs=[]):
512     ## hack: inputs is mutable!
513     inputs.append (name)
514     return inputs
515
516 def samefile (f1, f2):
517     try:
518         return os.path.samefile (f1, f2)
519     except AttributeError:                # Windoze
520         f1 = re.sub ("//*", "/", f1)
521         f2 = re.sub ("//*", "/", f2)
522         return f1 == f2
523
524 def do_file (input_filename, included=False):
525     # Ugh.
526     input_absname = input_filename
527     if not input_filename or input_filename == '-':
528         in_handle = sys.stdin
529         input_fullname = '<stdin>'
530     else:
531         if os.path.exists (input_filename):
532             input_fullname = input_filename
533         else:
534             input_fullname = global_options.formatter.input_fullname (input_filename)
535         # Normalize path to absolute path, since we will change cwd to the output dir!
536         # Otherwise, "lilypond-book -o out test.tex" will complain that it is
537         # overwriting the input file (which it is actually not), since the
538         # input filename is relative to the CWD...
539         input_absname = os.path.abspath (input_fullname)
540
541         note_input_file (input_fullname)
542         in_handle = file (input_fullname)
543
544     if input_filename == '-':
545         input_base = 'stdin'
546     elif included:
547         input_base = os.path.splitext (input_filename)[0]
548     else:
549         input_base = os.path.basename (
550             os.path.splitext (input_filename)[0])
551
552     # don't complain when global_options.output_dir is existing
553     if not global_options.output_dir:
554         global_options.output_dir = os.getcwd()
555     else:
556         global_options.output_dir = os.path.abspath(global_options.output_dir)
557
558         if not os.path.isdir (global_options.output_dir):
559             os.mkdir (global_options.output_dir, 0777)
560         os.chdir (global_options.output_dir)
561
562     output_filename = os.path.join(global_options.output_dir,
563                                    input_base + global_options.formatter.default_extension)
564     if (os.path.exists (input_filename)
565         and os.path.exists (output_filename)
566         and samefile (output_filename, input_absname)):
567      error (
568      _ ("Output would overwrite input file; use --output."))
569      exit (2)
570
571     try:
572         progress (_ ("Reading %s...") % input_fullname)
573         source = in_handle.read ()
574
575         if not included:
576             global_options.formatter.init_default_snippet_options (source)
577
578
579         progress (_ ("Dissecting..."))
580         chunks = find_toplevel_snippets (source, global_options.formatter)
581
582         # Let the formatter modify the chunks before further processing
583         chunks = global_options.formatter.process_chunks (chunks)
584
585         if global_options.filter_cmd:
586             write_if_updated (output_filename,
587                      [c.filter_text () for c in chunks])
588         elif global_options.process_cmd:
589             do_process_cmd (chunks, input_fullname, global_options)
590             progress (_ ("Compiling %s...") % output_filename)
591             write_if_updated (output_filename,
592                      [s.replacement_text ()
593                      for s in chunks])
594
595         def process_include (snippet):
596             os.chdir (original_dir)
597             name = snippet.substring ('filename')
598             progress (_ ("Processing include: %s") % name)
599             return do_file (name, included=True)
600
601         include_chunks = map (process_include,
602                               filter (lambda x: isinstance (x, BookSnippet.IncludeSnippet),
603                                       chunks))
604
605         return chunks + reduce (lambda x, y: x + y, include_chunks, [])
606
607     except BookSnippet.CompileError:
608         os.chdir (original_dir)
609         progress (_ ("Removing `%s'") % output_filename)
610         raise BookSnippet.CompileError
611
612 def do_options ():
613     global global_options
614
615     opt_parser = get_option_parser()
616     (global_options, args) = opt_parser.parse_args ()
617
618     global_options.information = {'program_version': ly.program_version, 'program_name': ly.program_name }
619
620     global_options.include_path =  map (os.path.abspath, global_options.include_path)
621
622     # Load the python packages (containing e.g. custom formatter classes)
623     # passed on the command line
624     nr = 0
625     for i in global_options.custom_packages:
626         nr += 1
627         print imp.load_source ("book_custom_package%s" % nr, i)
628
629
630     if global_options.warranty:
631         warranty ()
632         exit (0)
633     if not args or len (args) > 1:
634         opt_parser.print_help ()
635         exit (2)
636
637     return args
638
639 def main ():
640     # FIXME: 85 lines of `main' macramee??
641     if (os.environ.has_key ("LILYPOND_BOOK_LOGLEVEL")):
642         ly.set_loglevel (os.environ["LILYPOND_BOOK_LOGLEVEL"])
643     files = do_options ()
644
645     basename = os.path.splitext (files[0])[0]
646     basename = os.path.split (basename)[1]
647
648     if global_options.format:
649       # Retrieve the formatter for the given format
650       for formatter in BookBase.all_formats:
651         if formatter.can_handle_format (global_options.format):
652           global_options.formatter = formatter
653     else:
654         global_options.formatter = guess_format (files[0])
655         global_options.format = global_options.formatter.format
656
657     # make the global options available to the formatters:
658     global_options.formatter.global_options = global_options
659     formats = global_options.formatter.image_formats
660
661     if global_options.process_cmd == '':
662         global_options.process_cmd = (lilypond_binary
663                                       + ' --formats=%s -dbackend=eps ' % formats)
664
665     if global_options.process_cmd:
666         includes = global_options.include_path
667         if global_options.lily_output_dir:
668             # This must be first, so lilypond prefers to read .ly
669             # files in the other lybookdb dir.
670             includes = [os.path.abspath(global_options.lily_output_dir)] + includes
671         global_options.process_cmd += ' '.join ([' -I %s' % ly.mkarg (p)
672                                                  for p in includes])
673
674     global_options.formatter.process_options (global_options)
675
676     if global_options.lily_loglevel:
677         ly.debug_output (_ ("Setting LilyPond's loglevel to %s") % global_options.lily_loglevel, True)
678         global_options.process_cmd += " --loglevel=%s" % global_options.lily_loglevel
679     elif ly.is_verbose ():
680         if os.environ.get ("LILYPOND_LOGLEVEL", None):
681             ly.debug_output (_ ("Setting LilyPond's loglevel to %s (from environment variable LILYPOND_LOGLEVEL)") % os.environ.get ("LILYPOND_LOGLEVEL", None), True)
682             global_options.process_cmd += " --loglevel=%s" % os.environ.get ("LILYPOND_LOGLEVEL", None)
683         else:
684             ly.debug_output (_ ("Setting LilyPond's output to --verbose, implied by lilypond-book's setting"), True)
685             global_options.process_cmd += " --verbose"
686
687     if global_options.padding_mm:
688         global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
689
690     global_options.process_cmd += " -dread-file-list -dno-strip-output-dir"
691
692     if global_options.lily_output_dir:
693         global_options.lily_output_dir = os.path.abspath(global_options.lily_output_dir)
694         if not os.path.isdir (global_options.lily_output_dir):
695             os.makedirs (global_options.lily_output_dir)
696     else:
697         global_options.lily_output_dir = os.path.abspath(global_options.output_dir)
698
699     relative_output_dir = global_options.output_dir
700
701     identify ()
702     try:
703         chunks = do_file (files[0])
704     except BookSnippet.CompileError:
705         exit (1)
706
707     inputs = note_input_file ('')
708     inputs.pop ()
709
710     base_file_name = os.path.splitext (os.path.basename (files[0]))[0]
711     dep_file = os.path.join (global_options.output_dir, base_file_name + '.dep')
712     final_output_file = os.path.join (relative_output_dir,
713                      base_file_name + global_options.formatter.default_extension)
714
715     os.chdir (original_dir)
716     file (dep_file, 'w').write ('%s: %s'
717                                 % (final_output_file, ' '.join (inputs)))
718
719 if __name__ == '__main__':
720     main ()