]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
Loglevels in our python scripts (lilypond-book, musicxml2ly, convert-ly)
[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--2011',
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     ly.system(cmd,
384               be_verbose=ly.is_verbose (),
385               redirect_output=global_options.redirect_output,
386               log_file=logfile,
387               progress_p=1)
388     os.chdir (current)
389
390
391 def process_snippets (cmd, snippets,
392                       formatter, lily_output_dir):
393     """Run cmd on all of the .ly files from snippets."""
394
395     if not snippets:
396         return
397
398     cmd = formatter.adjust_snippet_command (cmd)
399
400     checksum = snippet_list_checksum (snippets)
401     contents = '\n'.join (['snippet-map-%d.ly' % checksum]
402                           + list (set ([snip.basename() + '.ly' for snip in snippets])))
403     name = os.path.join (lily_output_dir,
404                          'snippet-names-%d.ly' % checksum)
405     logfile = name.replace('.ly', '')
406     file (name, 'wb').write (contents)
407
408     system_in_directory (' '.join ([cmd, ly.mkarg (name)]),
409                          lily_output_dir,
410                          logfile)
411
412 def snippet_list_checksum (snippets):
413     return hash (' '.join([l.basename() for l in snippets]))
414
415 def write_file_map (lys, name):
416     snippet_map = file (os.path.join (
417         global_options.lily_output_dir,
418         'snippet-map-%d.ly' % snippet_list_checksum (lys)), 'w')
419
420     snippet_map.write ("""
421 #(define version-seen #t)
422 #(define output-empty-score-list #f)
423 #(ly:add-file-name-alist '(%s
424     ))\n
425 """ % '\n'.join(['("%s.ly" . "%s")\n' % (ly.basename (), name)
426                  for ly in lys]))
427
428 def split_output_files(directory):
429     """Returns directory entries in DIRECTORY/XX/ , where XX are hex digits.
430
431     Return value is a set of strings.
432     """
433     files = []
434     for subdir in glob.glob (os.path.join (directory, '[a-f0-9][a-f0-9]')):
435         base_subdir = os.path.split (subdir)[1]
436         sub_files = [os.path.join (base_subdir, name)
437                      for name in os.listdir (subdir)]
438         files += sub_files
439     return set (files)
440
441 def do_process_cmd (chunks, input_name, options):
442     snippets = [c for c in chunks if isinstance (c, BookSnippet.LilypondSnippet)]
443
444     output_files = split_output_files (options.lily_output_dir)
445     outdated = [c for c in snippets if c.is_outdated (options.lily_output_dir, output_files)]
446
447     write_file_map (outdated, input_name)
448     progress (_ ("Writing snippets..."))
449     for snippet in outdated:
450         snippet.write_ly()
451     progress ('\n')
452
453     if outdated:
454         progress (_ ("Processing..."))
455         progress ('\n')
456         process_snippets (options.process_cmd, outdated,
457                           options.formatter, options.lily_output_dir)
458
459     else:
460         progress (_ ("All snippets are up to date..."))
461
462     if options.lily_output_dir != options.output_dir:
463         output_files = split_output_files (options.lily_output_dir)
464         for snippet in snippets:
465             snippet.link_all_output_files (options.lily_output_dir,
466                                            output_files,
467                                            options.output_dir)
468
469     progress ('\n')
470
471
472 ###
473 # Format guessing data
474
475 def guess_format (input_filename):
476     format = None
477     e = os.path.splitext (input_filename)[1]
478     for formatter in BookBase.all_formats:
479       if formatter.can_handle_extension (e):
480         return formatter
481     error (_ ("cannot determine format for: %s" % input_filename))
482     exit (1)
483
484 def write_if_updated (file_name, lines):
485     try:
486         f = file (file_name)
487         oldstr = f.read ()
488         new_str = ''.join (lines)
489         if oldstr == new_str:
490             progress (_ ("%s is up to date.") % file_name)
491             progress ('\n')
492
493             # this prevents make from always rerunning lilypond-book:
494             # output file must be touched in order to be up to date
495             os.utime (file_name, None)
496             return
497     except:
498         pass
499
500     output_dir = os.path.dirname (file_name)
501     if not os.path.exists (output_dir):
502         os.makedirs (output_dir)
503
504     progress (_ ("Writing `%s'...") % file_name)
505     file (file_name, 'w').writelines (lines)
506     progress ('\n')
507
508
509 def note_input_file (name, inputs=[]):
510     ## hack: inputs is mutable!
511     inputs.append (name)
512     return inputs
513
514 def samefile (f1, f2):
515     try:
516         return os.path.samefile (f1, f2)
517     except AttributeError:                # Windoze
518         f1 = re.sub ("//*", "/", f1)
519         f2 = re.sub ("//*", "/", f2)
520         return f1 == f2
521
522 def do_file (input_filename, included=False):
523     # Ugh.
524     input_absname = input_filename
525     if not input_filename or input_filename == '-':
526         in_handle = sys.stdin
527         input_fullname = '<stdin>'
528     else:
529         if os.path.exists (input_filename):
530             input_fullname = input_filename
531         else:
532             input_fullname = global_options.formatter.input_fullname (input_filename)
533         # Normalize path to absolute path, since we will change cwd to the output dir!
534         # Otherwise, "lilypond-book -o out test.tex" will complain that it is
535         # overwriting the input file (which it is actually not), since the
536         # input filename is relative to the CWD...
537         input_absname = os.path.abspath (input_fullname)
538
539         note_input_file (input_fullname)
540         in_handle = file (input_fullname)
541
542     if input_filename == '-':
543         input_base = 'stdin'
544     elif included:
545         input_base = os.path.splitext (input_filename)[0]
546     else:
547         input_base = os.path.basename (
548             os.path.splitext (input_filename)[0])
549
550     # don't complain when global_options.output_dir is existing
551     if not global_options.output_dir:
552         global_options.output_dir = os.getcwd()
553     else:
554         global_options.output_dir = os.path.abspath(global_options.output_dir)
555
556         if not os.path.isdir (global_options.output_dir):
557             os.mkdir (global_options.output_dir, 0777)
558         os.chdir (global_options.output_dir)
559
560     output_filename = os.path.join(global_options.output_dir,
561                                    input_base + global_options.formatter.default_extension)
562     if (os.path.exists (input_filename)
563         and os.path.exists (output_filename)
564         and samefile (output_filename, input_absname)):
565      error (
566      _ ("Output would overwrite input file; use --output."))
567      exit (2)
568
569     try:
570         progress (_ ("Reading %s...") % input_fullname)
571         source = in_handle.read ()
572         progress ('\n')
573
574         if not included:
575             global_options.formatter.init_default_snippet_options (source)
576
577
578         progress (_ ("Dissecting..."))
579         chunks = find_toplevel_snippets (source, global_options.formatter)
580
581         # Let the formatter modify the chunks before further processing
582         chunks = global_options.formatter.process_chunks (chunks)
583         progress ('\n')
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             progress ('\n')
592             write_if_updated (output_filename,
593                      [s.replacement_text ()
594                      for s in chunks])
595
596         def process_include (snippet):
597             os.chdir (original_dir)
598             name = snippet.substring ('filename')
599             progress (_ ("Processing include: %s") % name)
600             progress ('\n')
601             return do_file (name, included=True)
602
603         include_chunks = map (process_include,
604                               filter (lambda x: isinstance (x, BookSnippet.IncludeSnippet),
605                                       chunks))
606
607         return chunks + reduce (lambda x, y: x + y, include_chunks, [])
608
609     except BookSnippet.CompileError:
610         os.chdir (original_dir)
611         progress (_ ("Removing `%s'") % output_filename)
612         progress ('\n')
613         raise BookSnippet.CompileError
614
615 def do_options ():
616     global global_options
617
618     opt_parser = get_option_parser()
619     (global_options, args) = opt_parser.parse_args ()
620
621     global_options.information = {'program_version': ly.program_version, 'program_name': ly.program_name }
622
623     global_options.include_path =  map (os.path.abspath, global_options.include_path)
624
625     # Load the python packages (containing e.g. custom formatter classes)
626     # passed on the command line
627     nr = 0
628     for i in global_options.custom_packages:
629         nr += 1
630         print imp.load_source ("book_custom_package%s" % nr, i)
631
632
633     if global_options.warranty:
634         warranty ()
635         exit (0)
636     if not args or len (args) > 1:
637         opt_parser.print_help ()
638         exit (2)
639
640     return args
641
642 def main ():
643     # FIXME: 85 lines of `main' macramee??
644     if (os.environ.has_key ("LILYPOND_BOOK_LOGLEVEL")):
645         ly.set_loglevel (os.environ["LILYPOND_BOOK_LOGLEVEL"])
646     files = do_options ()
647
648     basename = os.path.splitext (files[0])[0]
649     basename = os.path.split (basename)[1]
650
651     if global_options.format:
652       # Retrieve the formatter for the given format
653       for formatter in BookBase.all_formats:
654         if formatter.can_handle_format (global_options.format):
655           global_options.formatter = formatter
656     else:
657         global_options.formatter = guess_format (files[0])
658         global_options.format = global_options.formatter.format
659
660     # make the global options available to the formatters:
661     global_options.formatter.global_options = global_options
662     formats = global_options.formatter.image_formats
663
664     if global_options.process_cmd == '':
665         global_options.process_cmd = (lilypond_binary
666                                       + ' --formats=%s -dbackend=eps ' % formats)
667
668     if global_options.process_cmd:
669         includes = global_options.include_path
670         if global_options.lily_output_dir:
671             # This must be first, so lilypond prefers to read .ly
672             # files in the other lybookdb dir.
673             includes = [os.path.abspath(global_options.lily_output_dir)] + includes
674         global_options.process_cmd += ' '.join ([' -I %s' % ly.mkarg (p)
675                                                  for p in includes])
676
677     global_options.formatter.process_options (global_options)
678
679     if global_options.lily_loglevel:
680         ly.debug_output (_ ("Setting LilyPond's loglevel to %s") % global_options.lily_loglevel, True)
681         global_options.process_cmd += " --loglevel=%s" % global_options.lily_loglevel
682     elif ly.is_verbose ():
683         if os.environ.get ("LILYPOND_LOGLEVEL", None):
684             ly.debug_output (_ ("Setting LilyPond's loglevel to %s (from environment variable LILYPOND_LOGLEVEL)") % os.environ.get ("LILYPOND_LOGLEVEL", None), True)
685             global_options.process_cmd += " --loglevel=%s" % os.environ.get ("LILYPOND_LOGLEVEL", None)
686         else:
687             ly.debug_output (_ ("Setting LilyPond's output to --verbose, implied by lilypond-book's setting"), True)
688             global_options.process_cmd += " --verbose"
689
690     if global_options.padding_mm:
691         global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
692
693     global_options.process_cmd += " -dread-file-list -dno-strip-output-dir"
694
695     if global_options.lily_output_dir:
696         global_options.lily_output_dir = os.path.abspath(global_options.lily_output_dir)
697         if not os.path.isdir (global_options.lily_output_dir):
698             os.makedirs (global_options.lily_output_dir)
699     else:
700         global_options.lily_output_dir = os.path.abspath(global_options.output_dir)
701
702     relative_output_dir = global_options.output_dir
703
704     identify ()
705     try:
706         chunks = do_file (files[0])
707     except BookSnippet.CompileError:
708         exit (1)
709
710     inputs = note_input_file ('')
711     inputs.pop ()
712
713     base_file_name = os.path.splitext (os.path.basename (files[0]))[0]
714     dep_file = os.path.join (global_options.output_dir, base_file_name + '.dep')
715     final_output_file = os.path.join (relative_output_dir,
716                      base_file_name + global_options.formatter.default_extension)
717
718     os.chdir (original_dir)
719     file (dep_file, 'w').write ('%s: %s'
720                                 % (final_output_file, ' '.join (inputs)))
721
722 if __name__ == '__main__':
723     main ()