Fix #1192.
[lilypond/mpolesky.git] / scripts / lilypond-book.py
blob79ec2ababbf3a4223478a238c19571d1dd013356
1 #!@TARGET_PYTHON@
2 # -*- coding: utf-8 -*-
4 # This file is part of LilyPond, the GNU music typesetter.
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.
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.
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/>.
19 '''
20 Example usage:
22 test:
23 lilypond-book --filter="tr '[a-z]' '[A-Z]'" BOOK
25 convert-ly on book:
26 lilypond-book --filter="convert-ly --no-version --from=1.6.11 -" BOOK
28 classic lilypond-book:
29 lilypond-book --process="lilypond" BOOK.tely
31 TODO:
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?
39 * Converting from lilypond-book source, substitute:
40 @mbinclude foo.itely -> @include foo.itely
41 \mbinput -> \input
43 '''
46 # TODO: Better solve the global_options copying to the snippets...
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
58 """
59 @relocate-preamble@
60 """
62 import lilylib as ly
63 import fontextract
64 import langdefs
65 global _;_=ly._
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
74 ly.require_python_version ()
76 original_dir = os.getcwd ()
77 backend = 'ps'
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")})
89 authors = ('Jan Nieuwenhuizen <janneke@gnu.org>',
90 'Han-Wen Nienhuys <hanwen@xs4all.nl>')
92 ################################################################
93 def exit (i):
94 if global_options.verbose:
95 raise Exception (_ ('Exiting (%d)...') % i)
96 else:
97 sys.exit (i)
99 def identify ():
100 ly.encoded_write (sys.stdout, '%s (GNU LilyPond) %s\n' % (ly.program_name, ly.program_version))
102 progress = ly.progress
103 warning = ly.warning
104 error = ly.error
107 def warranty ():
108 identify ()
109 ly.encoded_write (sys.stdout, '''
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.")))
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)
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)
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')
138 p.add_option("-h", "--help",
139 action="help",
140 help=_ ("show this help and exit"))
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 ())])
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='')
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)
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)
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=[])
172 p.add_option ("-o", '--output', help=_ ("write output to DIR"),
173 metavar=_ ("DIR"),
174 action='store', dest='output_dir',
175 default='')
177 p.add_option ('-P', '--process', metavar=_ ("COMMAND"),
178 help = _ ("process ly_files using COMMAND FILE..."),
179 action='store',
180 dest='process_cmd', default='')
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)
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)
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)
199 p.add_option ('-V', '--verbose', help=_ ("be verbose"),
200 action="store_true",
201 default=False,
202 dest="verbose")
204 p.version = "@TOPLEVEL_VERSION@"
205 p.add_option("--version",
206 action="version",
207 help=_ ("show version number and exit"))
209 p.add_option ('-w', '--warranty',
210 help=_ ("show warranty and copyright"),
211 action='store_true')
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)
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')
234 for formatter in BookBase.all_formats:
235 formatter.add_options (p)
237 return p
239 lilypond_binary = os.path.join ('@bindir@', 'lilypond')
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')
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'
251 global_options = None
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
265 i = i + 1
266 nls.append (i)
267 start = i
269 nls.append (len (s))
270 return nls
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))
278 snippets = []
279 index = 0
280 found = dict ([(t, None) for t in types])
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
297 m = res[type].search (input_string[index:endex])
298 if not m:
299 continue
301 klass = global_options.formatter.snippet_class (type)
303 start = index + m.start ('match')
304 line_number = line_start_idx
305 while (line_starts[line_number] < start):
306 line_number += 1
308 line_number += 1
309 snip = klass (type, m, formatter, line_number, global_options)
311 found[type] = (start, snip)
313 if (found[type]
314 and (not first
315 or found[type][0] < found[first][0])):
316 first = type
318 # FIXME.
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.
328 endex = found[first][0]
330 if not first:
331 snippets.append (BookSnippet.Substring (input_string, index, len (input_string), line_start_idx))
332 break
334 while (start > line_starts[line_start_idx+1]):
335 line_start_idx += 1
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'))
343 return snippets
345 def system_in_directory (cmd, directory):
346 """Execute a command in a different directory.
348 Because of win32 compatibility, we can't simply use subprocess.
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)
358 def process_snippets (cmd, snippets,
359 formatter, lily_output_dir):
360 """Run cmd on all of the .ly files from snippets."""
362 if not snippets:
363 return
365 cmd = formatter.adjust_snippet_command (cmd)
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)
374 system_in_directory (' '.join ([cmd, ly.mkarg (name)]),
375 lily_output_dir)
378 def snippet_list_checksum (snippets):
379 return hash (' '.join([l.basename() for l in snippets]))
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')
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]))
394 def split_output_files(directory):
395 """Returns directory entries in DIRECTORY/XX/ , where XX are hex digits.
397 Return value is a set of strings.
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)
407 def do_process_cmd (chunks, input_name, options):
408 snippets = [c for c in chunks if isinstance (c, BookSnippet.LilypondSnippet)]
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)]
413 write_file_map (outdated, input_name)
414 progress (_ ("Writing snippets..."))
415 for snippet in outdated:
416 snippet.write_ly()
417 progress ('\n')
419 if outdated:
420 progress (_ ("Processing..."))
421 progress ('\n')
422 process_snippets (options.process_cmd, outdated,
423 options.formatter, options.lily_output_dir)
425 else:
426 progress (_ ("All snippets are up to date..."))
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)
435 progress ('\n')
439 # Format guessing data
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)
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')
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
466 output_dir = os.path.dirname (file_name)
467 if not os.path.exists (output_dir):
468 os.makedirs (output_dir)
470 progress (_ ("Writing `%s'...") % file_name)
471 file (file_name, 'w').writelines (lines)
472 progress ('\n')
475 def note_input_file (name, inputs=[]):
476 ## hack: inputs is mutable!
477 inputs.append (name)
478 return inputs
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
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 input_fullname = os.path.abspath (input_fullname)
501 note_input_file (input_fullname)
502 in_handle = file (input_fullname)
504 if input_filename == '-':
505 input_base = 'stdin'
506 elif included:
507 input_base = os.path.splitext (input_filename)[0]
508 else:
509 input_base = os.path.basename (
510 os.path.splitext (input_filename)[0])
512 # don't complain when global_options.output_dir is existing
513 if not global_options.output_dir:
514 global_options.output_dir = os.getcwd()
515 else:
516 global_options.output_dir = os.path.abspath(global_options.output_dir)
518 if not os.path.isdir (global_options.output_dir):
519 os.mkdir (global_options.output_dir, 0777)
520 os.chdir (global_options.output_dir)
522 output_filename = os.path.join(global_options.output_dir,
523 input_base + global_options.formatter.default_extension)
524 if (os.path.exists (input_filename)
525 and os.path.exists (output_filename)
526 and samefile (output_filename, input_fullname)):
527 error (
528 _ ("Output would overwrite input file; use --output."))
529 exit (2)
531 try:
532 progress (_ ("Reading %s...") % input_fullname)
533 source = in_handle.read ()
534 progress ('\n')
536 if not included:
537 global_options.formatter.init_default_snippet_options (source)
540 progress (_ ("Dissecting..."))
541 chunks = find_toplevel_snippets (source, global_options.formatter)
543 # Let the formatter modify the chunks before further processing
544 chunks = global_options.formatter.process_chunks (chunks)
545 progress ('\n')
547 if global_options.filter_cmd:
548 write_if_updated (output_filename,
549 [c.filter_text () for c in chunks])
550 elif global_options.process_cmd:
551 do_process_cmd (chunks, input_fullname, global_options)
552 progress (_ ("Compiling %s...") % output_filename)
553 progress ('\n')
554 write_if_updated (output_filename,
555 [s.replacement_text ()
556 for s in chunks])
558 def process_include (snippet):
559 os.chdir (original_dir)
560 name = snippet.substring ('filename')
561 progress (_ ("Processing include: %s") % name)
562 progress ('\n')
563 return do_file (name, included=True)
565 include_chunks = map (process_include,
566 filter (lambda x: isinstance (x, BookSnippet.IncludeSnippet),
567 chunks))
569 return chunks + reduce (lambda x, y: x + y, include_chunks, [])
571 except BookSnippet.CompileError:
572 os.chdir (original_dir)
573 progress (_ ("Removing `%s'") % output_filename)
574 progress ('\n')
575 raise BookSnippet.CompileError
577 def do_options ():
578 global global_options
580 opt_parser = get_option_parser()
581 (global_options, args) = opt_parser.parse_args ()
583 global_options.information = {'program_version': ly.program_version, 'program_name': ly.program_name }
585 global_options.include_path = map (os.path.abspath, global_options.include_path)
587 # Load the python packages (containing e.g. custom formatter classes)
588 # passed on the command line
589 nr = 0
590 for i in global_options.custom_packages:
591 nr += 1
592 print imp.load_source ("book_custom_package%s" % nr, i)
595 if global_options.warranty:
596 warranty ()
597 exit (0)
598 if not args or len (args) > 1:
599 opt_parser.print_help ()
600 exit (2)
602 return args
604 def main ():
605 # FIXME: 85 lines of `main' macramee??
606 files = do_options ()
608 basename = os.path.splitext (files[0])[0]
609 basename = os.path.split (basename)[1]
611 if global_options.format:
612 # Retrieve the formatter for the given format
613 for formatter in BookBase.all_formats:
614 if formatter.can_handle_format (global_options.format):
615 global_options.formatter = formatter
616 else:
617 global_options.formatter = guess_format (files[0])
618 global_options.format = global_options.formatter.format
620 # make the global options available to the formatters:
621 global_options.formatter.global_options = global_options
622 formats = global_options.formatter.image_formats
624 if global_options.process_cmd == '':
625 global_options.process_cmd = (lilypond_binary
626 + ' --formats=%s -dbackend=eps ' % formats)
628 if global_options.process_cmd:
629 includes = global_options.include_path
630 if global_options.lily_output_dir:
631 # This must be first, so lilypond prefers to read .ly
632 # files in the other lybookdb dir.
633 includes = [os.path.abspath(global_options.lily_output_dir)] + includes
634 global_options.process_cmd += ' '.join ([' -I %s' % ly.mkarg (p)
635 for p in includes])
637 global_options.formatter.process_options (global_options)
639 if global_options.verbose:
640 global_options.process_cmd += " --verbose "
642 if global_options.padding_mm:
643 global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
645 global_options.process_cmd += " -dread-file-list -dno-strip-output-dir"
647 if global_options.lily_output_dir:
648 global_options.lily_output_dir = os.path.abspath(global_options.lily_output_dir)
649 if not os.path.isdir (global_options.lily_output_dir):
650 os.makedirs (global_options.lily_output_dir)
651 else:
652 global_options.lily_output_dir = os.path.abspath(global_options.output_dir)
655 identify ()
656 try:
657 chunks = do_file (files[0])
658 except BookSnippet.CompileError:
659 exit (1)
661 inputs = note_input_file ('')
662 inputs.pop ()
664 base_file_name = os.path.splitext (os.path.basename (files[0]))[0]
665 dep_file = os.path.join (global_options.output_dir, base_file_name + '.dep')
666 final_output_file = os.path.join (global_options.output_dir,
667 base_file_name
668 + '.%s' % global_options.format)
670 os.chdir (original_dir)
671 file (dep_file, 'w').write ('%s: %s'
672 % (final_output_file, ' '.join (inputs)))
674 if __name__ == '__main__':
675 main ()