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