]> git.donarmstrong.com Git - lilypond.git/blob - scripts/ly2dvi.py
77ebe9808c3134e9472defca0475834b35e97ac9
[lilypond.git] / scripts / ly2dvi.py
1 #!@PYTHON@
2 # Run lilypond, latex, dvips.
3 #
4 # This is the third incarnation of ly2dvi.
5 #
6 # Earlier incarnations of ly2dvi were written by
7 # Jeffrey B. Reed<daboys@austin.rr.com> (Python version)
8 # Jan Arne Fagertun <Jan.A.Fagertun@@energy.sintef.no> (Bourne shell script)
9 #
10
11
12
13 # TODO: should allow to set a central pk cache directory from the command line.
14 # TODO: should allow to switch off pk cache.
15 #
16
17
18 # Note: gettext work best if we use ' for docstrings and "
19 #       for gettextable strings.
20 #       --> DO NOT USE """ for docstrings.
21
22 '''
23 TODO:
24
25   * figure out which set of command line options should make ly2dvi:
26
27       na: create tex only?  
28       na: create latex only? 
29       na: create tex and latex
30       default: create dvi only
31       na: create tex, latex and dvi
32       -P: create dvi and ps
33       na: * create ps only
34
35      etc.
36
37      for foo.ly, rename ly2dvi.dir to out-ly2dvi, foo.ly2dvi, foo.dir ?
38      
39   * move versatile taglines, 
40   
41      \header {
42         beginfooter=\mutopiaPD
43         endfooter=\tagline  -> 'lily was here <version>'
44      }
45
46      lilytagline (->lily was here), usertagline, copyright etc.
47
48   * head/header tagline/endfooter
49
50   * dvi from lilypond .tex output?  This is hairy, because we create dvi
51     from lilypond .tex *and* header output.
52
53   * multiple \score blocks?
54   
55 '''
56
57
58 import os
59 import stat
60 import string
61 import re
62 import getopt
63 import sys
64 import shutil
65 import __main__
66 import operator
67 import tempfile
68 import traceback
69
70 datadir = '@datadir@'
71 sys.path.append ('@datadir@/python')
72 sys.path.append ('@datadir@/buildscripts/out')
73
74 try:
75         import gettext
76         gettext.bindtextdomain ('lilypond', '@localedir@')
77         gettext.textdomain ('lilypond')
78         _ = gettext.gettext
79 except:
80         def _ (s):
81                 return s
82
83 # Attempt to fix problems with limited stack size set by Python!
84 # Sets unlimited stack size. Note that the resource module only
85 # is available on UNIX.
86 try:
87        import resource
88        resource.setrlimit (resource.RLIMIT_STACK, (-1, -1))
89 except:
90        pass
91
92 program_name = 'ly2dvi'
93 package_name = 'lilypond'
94 help_summary = _ ("Generate .dvi with LaTeX for LilyPond")
95
96 option_definitions = [
97         ('', 'd', 'dependencies', _ ("write Makefile dependencies for every input file")),
98         ('', 'h', 'help', _ ("this help")),
99         (_ ("DIR"), 'I', 'include', _ ("add DIR to LilyPond's search path")),
100         ('', 'k', 'keep', _ ("keep all output, and name the directory %s.dir") % program_name),
101         ('', '', 'no-lily', _ ("don't run LilyPond")),
102         ('', 'm', 'no-paper', _ ("produce MIDI output only")),
103         (_ ("FILE"), 'o', 'output', _ ("write ouput to FILE")),
104         (_ ("FILE"), 'f', 'find-pfa', _ ("find pfa fonts used in FILE")),
105         # why capital P?
106         ('', 'P', 'postscript', _ ("generate PostScript output")),
107         (_ ("KEY=VAL"), 's', 'set', _ ("change global setting KEY to VAL")),
108         ('', 'V', 'verbose', _ ("verbose")),
109         ('', 'v', 'version', _ ("print version number")),
110         ('', 'w', 'warranty', _ ("show warranty and copyright")),
111         ]
112
113 from lilylib import *
114
115
116 layout_fields = ['dedication', 'title', 'subtitle', 'subsubtitle',
117           'footer', 'head', 'composer', 'arranger', 'instrument',
118           'opus', 'piece', 'metre', 'meter', 'poet', 'texttranslator']
119
120
121 # init to empty; values here take precedence over values in the file
122
123 ## TODO: change name.
124 extra_init = {
125         'language' : [],
126         'latexheaders' : [],
127         'latexpackages' :  ['geometry'],
128         'latexoptions' : [],
129         'papersize' : [],
130         'pagenumber' : [1],
131         'textheight' : [], 
132         'linewidth' : [],
133         'orientation' : []
134 }
135
136 extra_fields = extra_init.keys ()
137 fields = layout_fields + extra_fields
138
139 include_path = ['.']
140 lily_p = 1
141 paper_p = 1
142 cache_pks_p = 1
143
144 PK_PATTERN='feta.*\.[0-9]+pk'
145
146 output_name = ''
147 targets = {
148         'DVI' : 0,
149         'LATEX' : 0,
150         'MIDI' : 0,
151         'TEX' : 0,
152         }
153
154 track_dependencies_p = 0
155 dependency_files = []
156
157
158 #
159 # Try to cater for bad installations of LilyPond, that have
160 # broken TeX setup.  Just hope this doesn't hurt good TeX
161 # setups.  Maybe we should check if kpsewhich can find
162 # feta16.{afm,mf,tex,tfm}, and only set env upon failure.
163 #
164 environment = {
165         'MFINPUTS' : datadir + '/mf' + ':',
166         'TEXINPUTS': datadir + '/tex:' + datadir + '/ps:' + '.:'
167                 + os.getcwd() + ':',
168         'TFMFONTS' : datadir + '/tfm' + ':',
169         'GS_FONTPATH' : datadir + '/afm:' + datadir + '/pfa',
170         'GS_LIB' : datadir + '/ps',
171 }
172
173 def setup_environment ():
174         for key in environment.keys ():
175                 val = environment[key]
176                 if os.environ.has_key (key):
177                         val = os.environ[key] + os.pathsep + val 
178                 os.environ[key] = val
179
180 def run_lilypond (files, outbase, dep_prefix):
181         opts = ''
182 #       opts = opts + '--output=%s.tex' % outbase
183         opts = opts + ' ' + string.join (map (lambda x : '-I ' + x,
184                                               include_path))
185         if paper_p:
186                 opts = opts + ' ' + string.join (map (lambda x : '-H ' + x,
187                                                       fields))
188         else:
189                 opts = opts + ' --no-paper'
190                 
191         if track_dependencies_p:
192                 opts = opts + " --dependencies"
193                 if dep_prefix:
194                         opts = opts + ' --dep-prefix=%s' % dep_prefix
195
196         fs = string.join (files)
197
198         if not verbose_p:
199                 progress ( _("Running %s...") % 'LilyPond')
200                 # cmd = cmd + ' 1> /dev/null 2> /dev/null'
201         else:
202                 opts = opts + ' --verbose'
203         
204         system ('lilypond %s %s ' % (opts, fs))
205
206 def analyse_lilypond_output (filename, extra):
207         
208         # urg
209         '''Grep FILENAME for interesting stuff, and
210         put relevant info into EXTRA.'''
211         filename = filename+'.tex'
212         progress (_ ("Analyzing %s...") % filename)
213         s = open (filename).read ()
214
215         # search only the first 10k
216         s = s[:10240]
217         for x in ('textheight', 'linewidth', 'papersize', 'orientation'):
218                 m = re.search (r'\\def\\lilypondpaper%s{([^}]*)}'%x, s)
219                 if m:
220                         set_setting (extra, x, m.group (1))
221
222 def find_tex_files_for_base (base, extra):
223
224         """
225         Find the \header fields dumped from BASE.
226         """
227         
228         headerfiles = {}
229         for f in layout_fields:
230                 if os.path.exists (base + '.' + f):
231                         headerfiles[f] = base+'.'+f
232
233         if os.path.exists (base  +'.dep'):
234                 dependency_files.append (base + '.dep')
235
236         for f in extra_fields:
237                 if os.path.exists (base + '.' + f):
238                         extra[f].append (open (base + '.' + f).read ())
239         
240         return (base  +'.tex',headerfiles)
241          
242
243 def find_tex_files (files, extra):
244         """
245         Find all .tex files whose prefixes start with some name in FILES. 
246
247         """
248         
249         tfiles = []
250         
251         for f in files:
252                 x = 0
253                 while 1:
254                         fname = os.path.basename (f)
255                         fname = strip_extension (fname, '.ly')
256                         if x:
257                                 fname = fname + '-%d' % x
258
259                         if os.path.exists (fname + '.tex'):
260                                 tfiles.append (find_tex_files_for_base (fname, extra))
261                                 analyse_lilypond_output (fname, extra)
262                         else:
263                                 break
264
265                         x = x + 1
266         if not x:
267                 fstr = string.join (files, ', ')
268                 warning (_ ("no lilypond output found for %s") % fstr)
269         return tfiles
270
271 def one_latex_definition (defn, first):
272         s = '\n'
273         for (k,v) in defn[1].items ():
274                 val = open (v).read ()
275                 if (string.strip (val)):
276                         s = s + r'''\def\lilypond%s{%s}''' % (k, val)
277                 else:
278                         s = s + r'''\let\lilypond%s\relax''' % k
279                 s = s + '\n'
280
281         if first:
282                 s = s + '\\def\\mustmakelilypondtitle{}\n'
283         else:
284                 s = s + '\\def\\mustmakelilypondpiecetitle{}\n'
285                 
286         s = s + '\\input %s\n' % defn[0] # The final \n seems important here. It ensures that the footers and taglines end up on the right page.
287         return s
288
289
290 ly_paper_to_latexpaper =  {
291         'a4' : 'a4paper',
292         'letter' : 'letterpaper', 
293 }
294
295 def global_latex_definition (tfiles, extra):
296
297         '''construct preamble from EXTRA, dump Latex stuff for each
298 lily output file in TFILES after that, and return the Latex file constructed.  '''
299
300
301         s = ""
302         s = s + '% generation tag\n'
303
304         options = ''
305
306         if extra['papersize']:
307                 try:
308                         options = '%s' % ly_paper_to_latexpaper[extra['papersize'][0]]
309                 except KeyError:
310                         warning (_ ("invalid value: %s") % `extra['papersize'][0]`)
311                         pass
312
313         if extra['latexoptions']:
314                 options = options + ',' + extra['latexoptions'][-1]
315
316         s = s + '\\documentclass[%s]{article}\n' % options
317
318         if extra['language']:
319                 s = s + r'\usepackage[%s]{babel}\n' % extra['language'][-1]
320
321
322         s = s + '\\usepackage{%s}\n' \
323                 % string.join (extra['latexpackages'], ',')
324
325         if extra['latexheaders']:
326                 s = s + '\\include{%s}\n' \
327                         % string.join (extra['latexheaders'], '}\n\\include{')
328
329         textheight = ''
330         if extra['textheight']:
331                 textheight = ',textheight=%fpt' % extra['textheight'][0]
332
333         orientation = 'portrait'
334         if extra['orientation']:
335                 orientation = extra['orientation'][0]
336
337         # set sane geometry width (a4-width) for linewidth = -1.
338         maxlw = max (extra['linewidth'] + [-1])
339         if maxlw < 0:
340                 # who the hell is 597 ?
341                 linewidth = '597'
342         else:
343                 linewidth = maxlw
344         s = s + '\geometry{width=%spt%s,headheight=2mm,footskip=2mm,%s}\n' % (linewidth, textheight, orientation)
345
346         if extra['latexoptions']:
347                 s = s + '\geometry{twosideshift=4mm}\n'
348
349         s = s + r'''
350 \usepackage[latin1]{inputenc}
351 \input{titledefs}
352 '''
353         
354         if extra['pagenumber'] and extra['pagenumber'][-1] and extra['pagenumber'][-1] != 'no':
355                 s = s + '\setcounter{page}{%s}\n' % (extra['pagenumber'][-1])
356                 s = s + '\\pagestyle{plain}\n'
357         else:
358                 s = s + '\\pagestyle{empty}\n'
359
360         s = s + '\\begin{document}\n'
361         s = s + '\\thispagestyle{firstpage}\n'
362
363         first = 1
364         for t in tfiles:
365                 s = s + one_latex_definition (t, first)
366                 first = 0
367
368
369         s = s + '\\thispagestyle{lastpage}\n'
370         s = s + '\\end{document}'
371
372         return s
373
374 def run_latex (files, outbase, extra):
375
376         """Construct latex file, for FILES and EXTRA, dump it into
377 OUTBASE.latex. Run LaTeX on it.
378
379 RETURN VALUE
380
381 None
382         """
383         latex_fn = outbase + '.latex'
384         
385         wfs = find_tex_files (files, extra)
386         s = global_latex_definition (wfs, extra)
387
388         f = open (latex_fn, 'w')
389         f.write (s)
390         f.close ()
391
392         cmd = 'latex \\\\nonstopmode \\\\input %s' % latex_fn
393
394         if not verbose_p:
395                 progress ( _("Running %s...") % 'LaTeX')
396                 cmd = cmd + ' 1> /dev/null 2> /dev/null'
397
398         system (cmd)
399
400 def run_dvips (outbase, extra):
401
402
403         """Run dvips using the correct options taken from EXTRA,
404 leaving a PS file in OUTBASE.ps
405
406 RETURN VALUE
407
408 None.
409 """
410         opts = ''
411         if extra['papersize']:
412                 opts = opts + ' -t%s' % extra['papersize'][0]
413
414         if extra['orientation'] and extra['orientation'][0] == 'landscape':
415                 opts = opts + ' -tlandscape'
416
417         cmd = 'dvips %s -o%s %s' % (opts, outbase + '.ps', outbase + '.dvi')
418         
419         if not verbose_p:
420                 progress ( _("Running %s...") % 'dvips')
421                 cmd = cmd + ' 1> /dev/null 2> /dev/null'
422                 
423         system (cmd)
424
425 def generate_dependency_file (depfile, outname):
426         df = open (depfile, 'w')
427         df.write (outname + ':' )
428         
429         for d in dependency_files:
430                 s = open (d).read ()
431                 s = re.sub ('#[^\n]*\n', '', s)
432                 s = re.sub (r'\\\n', ' ', s)
433                 m = re.search ('.*:(.*)\n', s)
434
435                 # ugh. Different targets?
436                 if m:
437                         df.write ( m.group (1)  + ' ' )
438
439         df.write ('\n')
440         df.close ();
441
442 def find_file_in_path (path, name):
443         for d in string.split (path, os.pathsep):
444                 if name in os.listdir (d):
445                         return os.path.join (d, name)
446
447 # Added as functionality to ly2dvi, because ly2dvi may well need to do this
448 # in future too.
449 PS = '%!PS-Adobe'
450 def find_pfa_fonts (name):
451         s = open (name).read ()
452         if s[:len (PS)] != PS:
453                 # no ps header?
454                 errorport.write (_( "error: ") + _ ("not a PostScript file: `%s\'" % name))
455                 errorport.write ('\n')
456                 sys.exit (1)
457         here = 0
458         m = re.match ('.*?/(feta[-a-z0-9]+) +findfont', s[here:], re.DOTALL)
459         pfa = []
460         while m:
461                 here = m.end (1)
462                 pfa.append (m.group (1))
463                 m = re.match ('.*?/(feta[-a-z0-9]+) +findfont', s[here:], re.DOTALL)
464         return pfa
465
466         
467 (sh, long) = getopt_args (__main__.option_definitions)
468 try:
469         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
470 except getopt.error, s:
471         errorport.write ('\n')
472         errorport.write (_ ("error: ") + _ ("getopt says: `%s\'" % s))
473         errorport.write ('\n')
474         errorport.write ('\n')
475         help ()
476         sys.exit (2)
477         
478 for opt in options:     
479         o = opt[0]
480         a = opt[1]
481
482         if 0:
483                 pass
484         elif o == '--help' or o == '-h':
485                 help ()
486                 sys.exit (0)
487         elif o == '--find-pfa' or o == '-f':
488                 fonts = map (lambda x: x + '.pfa', find_pfa_fonts (a))
489                 files = map (lambda x:
490                              find_file_in_path (os.environ['GS_FONTPATH'], x),
491                              fonts)
492                 print string.join (files, ' ')
493                 sys.exit (0)
494         elif o == '--include' or o == '-I':
495                 include_path.append (a)
496         elif o == '--postscript' or o == '-P':
497                 targets['PS'] = 0
498         elif o == '--keep' or o == '-k':
499                 keep_temp_dir_p = 1
500         elif o == '--no-lily':
501                 lily_p = 0
502         elif o == '--no-paper' or o == '-m':
503                 targets = {}
504                 targets['MIDI'] = 0
505                 paper_p = 0
506         elif o == '--output' or o == '-o':
507                 output_name = a
508         elif o == '--set' or o == '-s':
509                 ss = string.split (a, '=')
510                 set_setting (extra_init, ss[0], ss[1])
511         elif o == '--dependencies' or o == '-d':
512                 track_dependencies_p = 1
513         elif o == '--verbose' or o == '-V':
514                 verbose_p = 1
515         elif o == '--version' or o == '-v':
516                 identify ()
517                 sys.exit (0)
518         elif o == '--warranty' or o == '-w':
519                 status = system ('lilypond -w', ignore_error = 1)
520                 if status:
521                         warranty ()
522
523                 sys.exit (0)
524
525
526 def cp_to_dir (pattern, dir):
527         "Copy files matching re PATTERN from cwd to DIR"
528         # Duh.  Python style portable: cp *.EXT OUTDIR
529         # system ('cp *.%s %s' % (ext, outdir), 1)
530         files = filter (lambda x, p=pattern: re.match (p, x), os.listdir ('.'))
531         map (lambda x, d=dir: shutil.copy2 (x, os.path.join (d, x)), files)
532
533 # Python < 1.5.2 compatibility
534 #
535 # On most platforms, this is equivalent to
536 #`normpath(join(os.getcwd()), PATH)'.  *Added in Python version 1.5.2*
537 if os.path.__dict__.has_key ('abspath'):
538         abspath = os.path.abspath
539 else:
540         def abspath (path):
541                 return os.path.normpath (os.path.join (os.getcwd (), path))
542
543 if os.__dict__.has_key ('makedirs'):
544         makedirs = os.makedirs
545 else:
546         def makedirs (dir, mode=0777):
547                 system ('mkdir -p %s' % dir)
548
549 def mkdir_p (dir, mode=0777):
550         if not os.path.isdir (dir):
551                 makedirs (dir, mode)
552
553 include_path = map (abspath, include_path)
554
555 original_output = output_name
556
557
558 if files and files[0] != '-':
559
560         # Ugh, maybe make a setup () function
561         files = map (lambda x: strip_extension (x, '.ly'), files)
562
563         (outdir, outbase) = ('','')
564         if not output_name:
565                 outbase = os.path.basename (files[0])
566                 outdir = abspath('.')
567         elif output_name[-1] == os.sep:
568                 outdir = abspath (output_name)
569                 outbase = os.path.basename (files[0])
570         else:
571                 (outdir, outbase) = os.path.split (abspath (output_name))
572
573         for i in ('.dvi', '.latex', '.ly', '.ps', '.tex'):
574                 output_name = strip_extension (output_name, i)
575
576         files = map (abspath, files) 
577
578         if os.path.dirname (output_name) != '.':
579                 dep_prefix = os.path.dirname (output_name)
580         else:
581                 dep_prefix = 0
582
583         reldir = os.path.dirname (output_name)
584         if outdir != '.' and (track_dependencies_p or targets.keys ()):
585                 mkdir_p (outdir, 0777)
586
587         setup_environment ()
588         tmpdir = setup_temp ()
589         if cache_pks_p :
590                 os.chdir (outdir)
591                 cp_to_dir (PK_PATTERN, tmpdir)
592
593         # to be sure, add tmpdir *in front* of inclusion path.
594         #os.environ['TEXINPUTS'] =  tmpdir + ':' + os.environ['TEXINPUTS']
595         os.chdir (tmpdir)
596         
597         if lily_p:
598                 try:
599                         run_lilypond (files, outbase, dep_prefix)
600                 except:
601                         # TODO: friendly message about LilyPond setup/failing?
602                         #
603                         # TODO: lilypond should fail with different
604                         # error codes for:
605                         #   - guile setup/startup failure
606                         #   - font setup failure
607                         #   - init.ly setup failure
608                         #   - parse error in .ly
609                         #   - unexpected: assert/core dump
610                         targets = {}
611                         traceback.print_exc ()
612
613         if targets.has_key ('DVI') or targets.has_key ('PS'):
614                 try:
615                         run_latex (files, outbase, extra_init)
616                         # unless: add --tex, or --latex?
617                         del targets['TEX']
618                         del targets['LATEX']
619                 except:
620                         # TODO: friendly message about TeX/LaTeX setup,
621                         # trying to run tex/latex by hand
622                         if targets.has_key ('DVI'):
623                                 del targets['DVI']
624                         if targets.has_key ('PS'):
625                                 del targets['PS']
626                         traceback.print_exc ()
627
628         if targets.has_key ('PS'):
629                 try:
630                         run_dvips (outbase, extra_init)
631                 except: 
632                         if targets.has_key ('PS'):
633                                 del targets['PS']
634                         traceback.print_exc ()
635
636         # add DEP to targets?
637         if track_dependencies_p:
638                 depfile = os.path.join (outdir, outbase + '.dep')
639                 generate_dependency_file (depfile, depfile)
640                 if os.path.isfile (depfile):
641                         progress (_ ("dependencies output to `%s'...") % depfile)
642
643         # Hmm, if this were a function, we could call it the except: clauses
644         for i in targets.keys ():
645                 ext = string.lower (i)
646                 cp_to_dir ('.*\.%s$' % ext, outdir)
647                 outname = outbase + '.' + string.lower (i)
648                 abs = os.path.join (outdir, outname)
649                 if reldir != '.':
650                         outname = os.path.join (reldir, outname)
651                 if os.path.isfile (abs):
652                         progress (_ ("%s output to `%s'...") % (i, outname))
653                 elif verbose_p:
654                         warning (_ ("can't find file: `%s'") % outname)
655                         
656                 if cache_pks_p:
657                         cp_to_dir (PK_PATTERN, outdir)
658                 
659         os.chdir (original_dir)
660         cleanup_temp ()
661         
662 else:
663         # FIXME: read from stdin when files[0] = '-'
664         help ()
665         errorport.write (program_name + ":" + _ ("error: ") + _ ("no files specified on command line.") + '\n')
666         sys.exit (2)
667
668
669