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