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