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