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