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