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