webshob target, quiet pdflatex, etc. in the template
[orchestrallily.git] / generate_oly_score.py
blob716cd3ff076ea8fc9da1102da94e08f0fb3b3975
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 import sys
5 import os
6 import os.path
7 import getopt
8 import re
9 import filecmp
10 import string
11 import subprocess
12 import copy
13 import codecs
14 import fnmatch
15 from jinja2 import Environment, Template, FileSystemLoader
16 import jinja2
18 import pprint
19 pp = pprint.PrettyPrinter(indent=4)
21 program_name = 'generate_oly_score';
22 settings_file = 'oly_structure.def';
23 script_path = os.path.dirname(__file__);
25 ######################################################################
26 # Options handling
27 ######################################################################
29 help_text = r"""Usage: %(program_name)s [OPTIONS]... [DEF-FILE]
30 Create a complete file structure for a score using OrchestralLily.
31 If no definitions file it given, the file oly_structure.def is used
33 Options:
34 -h, --help print this help
35 -o, --output=DIR write output files to DIR (default: read from settings file)
36 """
38 def help (text):
39 sys.stdout.write (text)
40 sys.exit (0)
44 ######################################################################
45 # Settings
46 ######################################################################
48 class Settings:
49 options = {};
50 arguments = [];
51 globals={};
52 raw_data={};
53 out_dir = "";
54 template_env = None;
56 def __init__ (self):
57 settings_file = self.load_options ();
58 self.load (settings_file);
59 self.raw_data["settings_file_path"] = settings_file;
60 self.raw_data["settings_file"] = os.path.basename(settings_file);
61 self.init_template_env ();
62 self.init_arrays ();
64 def output_dir (self):
65 return self.out_dir
66 def defaults (self):
67 return self.raw_data.get ("defaults", {});
68 def score_names (self):
69 return self.raw_data.get ("scores", ["Score"]);
71 def get_score_settings (self, id):
72 settings = self.globals.copy();
73 settings.update (self.defaults ());
74 settings.update ({"name": id})
75 settings.update (self.raw_data.get (id, {}));
76 self.normalize_part_definitions (settings);
77 return settings;
79 def normalize_part_definitions (self, score_settings):
80 scorename = score_settings.get ("name", "Score");
81 parts = score_settings.get ("parts", [scorename]);
82 result = [];
83 for this_part in parts:
84 if isinstance (this_part, basestring):
85 this_part = {"id": this_part, "filename": this_part }
86 elif not isinstance (this_part, dict):
87 warning ("Invalid part list for score %a: %a" % (scorename, p));
88 return [];
89 if "id" not in this_part:
90 this_part["id"] = scorename;
91 if "filename" not in this_part:
92 this_part["filename"] = this_part["id"];
93 if "piece" not in this_part:
94 this_part["piece"] = this_part["id"];
95 this_part["score"] = scorename;
96 this_part.update (score_settings);
97 result.append (this_part);
98 score_settings["parts"] = result;
100 def has_tex (self):
101 return "latex" in self.raw_data;
102 def get_tex_settings (self):
103 settings = self.globals.copy();
104 settings.update (self.defaults ());
105 settings.update (self.raw_data.get ("latex", {}));
106 return settings;
108 def get_score_parts (self, score_settings):
109 scorename = score_settings.get ("name", "Score");
110 parts = score_settings.get ("parts", [scorename]);
111 result = [];
112 for p in parts:
113 this_part = p;
114 if isinstance (this_part, basestring):
115 this_part = {"id": this_part, "filename": this_part }
116 elif not isinstance (this_part, dict):
117 warning ("Invalid part list for score %a: %a" % (scorename, p));
118 return [];
119 if "id" not in this_part:
120 this_part["id"] = scorename;
121 if "filename" not in this_part:
122 this_part["filename"] = this_part["id"];
123 if "piece" not in this_part:
124 this_part["piece"] = this_part["id"];
125 this_part["score"] = scorename;
126 this_part.update (score_settings);
127 result.append (this_part);
128 return result;
130 def load_options (self):
131 (self.options, self.arguments) = getopt.getopt (sys.argv[1:], 'ho:', ['help', 'output='])
132 for opt in self.options:
133 o = opt[0]
134 a = opt[1]
135 if o == '-h' or o == '--help':
136 help (help_text % globals ())
137 elif o == '-o' or o == '--output':
138 self.out_dir = a
139 else:
140 raise Exception ('unknown option: ' + o)
141 if self.arguments:
142 return self.arguments[0]
143 else:
144 return "oly_structure.def";
146 def load (self, filename):
147 try:
148 in_f = codecs.open (filename, "r", "utf-8")
149 s = in_f.read()
150 in_f.close()
151 self.raw_data = eval(s);
152 except IOError:
153 print ("Unable to load settings file '%s'. Exiting..." % file_name)
154 exit (-1);
155 except SyntaxError as ex:
156 print ex;
157 print ("Unable to interpret settings file '%s', it's syntax is invalid. Exiting..." % filename);
158 exit (-1);
159 if not self.out_dir:
160 self.out_dir = self.raw_data.get ("output_dir", self.out_dir) + "/";
162 def get_template_name (self):
163 return self.raw_data.get ("template", "Full");
164 def get_template_names (self, pattern):
165 allfiles = os.listdir (self.templatepath);
166 files = [];
167 for f in allfiles:
168 if fnmatch.fnmatch(f, pattern):
169 files.append (f);
170 return files;
173 def init_template_env (self):
174 global program_name;
175 global script_path;
176 templatename = self.get_template_name ();
177 self.templatepath = script_path + '/Templates/' + templatename;
178 self.template_env = Environment (
179 loader = FileSystemLoader(self.templatepath),
180 block_start_string = '<$', block_end_string = '$>',
181 variable_start_string = '<<', variable_end_string = '>>',
182 comment_start_string = '<#', comment_end_string = '#>',
183 #line_statement_prefix = '#'
185 def get_template (self, template):
186 return self.template_env.get_template (template);
189 def init_arrays (self):
190 # Globals
191 self.globals = self.raw_data.copy ();
192 del self.globals["defaults"];
193 if "latex" in self.globals:
194 del self.globals["latex"];
195 for i in self.globals.get ("scores", []):
196 if i in self.globals:
197 del self.globals[i];
199 def assemble_filename (self, base, id, name, ext):
200 parts = [base, id, name];
201 if "" in parts:
202 parts.remove ("")
203 return "_".join (parts) + "." + ext;
204 def assemble_movement_filename (self, basename, mvmnt):
205 return self.assemble_filename( basename, "Music", mvmnt, "ily");
206 def assemble_instrument_filename (self, basename, instr):
207 return self.assemble_filename( basename, "Instrument", instr, "ly");
208 def assemble_score_filename (self, basename, instr):
209 return self.assemble_filename( basename, "Score", instr, "ly");
210 def assemble_settings_filename (self, basename, s):
211 return self.assemble_filename( basename, "Settings", s, "ily");
212 def assemble_itex_filename (self, basename, filename):
213 return self.assemble_filename( "TeX", basename, filename, "itex");
214 def assemble_texscore_filename (self, basename, score):
215 return self.assemble_filename( "TeX", basename, "Score_" + score, "tex");
217 ######################################################################
218 # File Writing
219 ######################################################################
221 def write_file (path, fname, contents):
222 file_name = path + fname;
223 fn = file_name
224 if os.path.exists (file_name):
225 fn += ".new"
227 dir = os.path.dirname (fn);
228 if not os.path.exists (dir):
229 os.mkdir (dir)
231 try:
232 out_f = codecs.open (fn, "w", "utf-8")
233 s = out_f.write(contents)
234 out_f.write ("\n")
235 out_f.close()
236 except IOError:
237 print ("Unable to write to output file '%s'. Exiting..." % fn)
238 exit (-1);
240 # If the file already existed, check if the new file is identical.
241 if (fn != file_name):
242 patchfile = os.path.join (dir, "patches", os.path.basename(file_name) + ".patch")
243 if os.path.exists (patchfile):
244 try:
245 retcode = subprocess.call("patch -Ns \""+ fn+ "\" \"" + patchfile + "\"", shell=True)
246 if retcode < 0:
247 print >>sys.stderr, "Unable to apply patch to file \"", fn, "\"."
248 except OSError, e:
249 print >>sys.stderr, "Execution failed:", e
251 if filecmp.cmp (fn, file_name):
252 os.unlink (fn);
253 else:
254 print ("A file %s already existed, created new file %s." % (os.path.basename(file_name), os.path.basename(fn)))
258 ######################################################################
259 # Creating movement files
260 ######################################################################
263 def generate_movement_files (score_name, score_settings, settings):
264 parts_files = [];
265 for part_settings in settings.get_score_parts (score_settings):
266 template = settings.get_template ("Lily_Music_Movement.ily");
267 basename = part_settings.get ("basename", score_name);
268 filename = part_settings.get ("filename", score_name + "Part");
269 filename = settings.assemble_movement_filename (basename, filename);
270 write_file (settings.out_dir, filename, template.render (part_settings));
271 parts_files.append (filename);
273 return parts_files;
277 ######################################################################
278 # Creating instrumental score files
279 ######################################################################
281 def generate_instrument_files (score_name, score_settings, settings):
282 instrument_files = [];
283 settings_files = set ();
284 instrument_settings = score_settings.copy ();
285 instrument_settings["parts"] = settings.get_score_parts (score_settings);
287 template = settings.get_template ("Lily_Instrument.ly");
288 basename = score_settings.get ("basename", score_name );
290 noscore_instruments = score_settings.get ("noscore_instruments", [])
291 for i in score_settings.get ("instruments", []):
292 if i in noscore_instruments:
293 continue;
294 instrument_settings["instrument"] = i;
295 if i in score_settings.get ("vocalvoices"):
296 instrument_settings["settings"] = "VocalVoice";
297 else:
298 instrument_settings["settings"] = "Instrument";
299 settings_files.add (instrument_settings["settings"]);
300 filename = settings.assemble_instrument_filename (basename, i);
301 write_file (settings.out_dir, filename, template.render (instrument_settings));
302 instrument_files.append (filename);
304 return (instrument_files, settings_files);
308 ######################################################################
309 # Creating score files
310 ######################################################################
313 score_name_map = {
314 "Particell": "Particell",
316 settings_map = {
317 "OrganScore": "VocalScore",
318 "ChoralScore": "ChoralScore",
319 "LongScore": "FullScore",
320 "OriginalScore": "FullScore",
321 "Particell": "FullScore"
323 scores_cues = ["ChoralScore", "VocalScore"];
325 def generate_score_files (score_name, score_settings, settings):
326 score_files = [];
327 settings_files = set ();
328 s_settings = score_settings.copy ();
329 s_settings["parts"] = settings.get_score_parts (score_settings);
330 s_settings["fullscore"] = True;
332 template = settings.get_template ("Lily_Score.ly");
333 basename = score_settings.get ("basename", score_name );
335 for s in score_settings.get ("scores", []):
336 fullsn = score_name_map.get (s, (s + "Score"));
337 s_settings["score"] = fullsn;
338 s_settings["nocues"] = fullsn not in scores_cues;
339 s_settings["settings"] = settings_map.get (fullsn,fullsn)
340 settings_files.add (s_settings["settings"]);
341 filename = settings.assemble_score_filename (basename, s);
342 write_file (settings.out_dir, filename, template.render (s_settings));
343 score_files.append (filename);
345 return (score_files, settings_files);
349 ######################################################################
350 # Creating settings files
351 ######################################################################
353 def write_settings_file_if_exists (settings, score_settings, template, filename):
354 try:
355 template = settings.get_template (template);
356 basename = score_settings.get ("basename", "");
357 filename = settings.assemble_settings_filename (basename, filename);
358 write_file (settings.out_dir, filename, template.render (score_settings));
359 return filename;
360 except jinja2.exceptions.TemplateNotFound:
361 return None;
363 def generate_settings_files (score_name, score_settings, settings, settings_files):
364 out_files = [];
365 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Global.ily", "Global" ));
366 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings.ily", "" ));
368 for s in settings_files:
369 score_settings["settings"] = s.lower ();
370 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Generic.ily", s));
372 return out_files;
376 ######################################################################
377 # Generation of Lilypond Files
378 ######################################################################
380 def generate_scores (settings):
381 files = {}
382 for s in settings.score_names ():
383 score_settings = settings.get_score_settings (s);
384 parts_files = generate_movement_files (s, score_settings, settings);
385 (instrument_files, isettings_files) = generate_instrument_files (s, score_settings, settings);
386 (score_files, ssettings_files) = generate_score_files (s, score_settings, settings);
387 included_settings_files = ssettings_files | isettings_files;
388 score_settings["parts_files"] = parts_files;
389 settings_files = generate_settings_files (s, score_settings, settings, included_settings_files );
390 files[s] = {"settings": settings_files,
391 "scores": score_files,
392 "instruments": instrument_files,
393 "parts": parts_files };
394 return files
398 ######################################################################
399 # Creating LaTeX files
400 ######################################################################
402 def write_itex_file (settings, tex_settings, template, filename):
403 template = settings.get_template (template);
404 basename = tex_settings.get ("basename", "");
405 filename = settings.assemble_itex_filename (basename, filename);
406 write_file (settings.out_dir, filename, template.render (tex_settings));
407 return filename;
409 def write_texscore_file (settings, tex_settings, template, score):
410 template = settings.get_template (template);
411 basename = tex_settings.get ("basename", "");
412 filename = settings.assemble_texscore_filename (basename, score);
413 write_file (settings.out_dir, filename, template.render (tex_settings));
414 return filename;
416 no_criticalcomments_scores = ["Vocal", "Choral", "Organ"];
417 tex_options_map = {
418 "Organ": "vocalscore",
419 "Choral": "choralscore",
420 "Vocal": "vocalscore",
421 "Long": "fullscore",
422 "Full": "fullscore",
423 "Original": "fullscore",
424 "Particell": "vocalscore",
425 "InstrumentalParts": "instrumentalparts",
426 # TODO: chambermusic
429 def generate_tex_files (settings, lily_files):
430 tex_files = [];
431 tex_includes = [];
432 tex_settings = settings.get_tex_settings ();
433 tex_settings["lily_files"] = lily_files;
435 score_map = {};
436 instruments_scores = [];
437 for s in settings.score_names ():
438 score_settings = settings.get_score_settings (s);
439 instruments_scores.append (score_settings);
440 for p in score_settings.get ("scores", []):
441 score_map[p] = score_map.get (p, []) + [score_settings];
442 tex_settings["works"] = instruments_scores;
444 tex_include_templates = settings.get_template_names("TeX_*.itex");
445 for t in tex_include_templates:
446 base = re.sub( r'TeX_(.*)\.itex', r'\1', t);
447 tex_includes.append (write_itex_file (settings, tex_settings, t, base));
449 for (score, parts) in score_map.items ():
450 this_settings = copy.deepcopy (tex_settings);
451 this_settings["scoretype"] = score;
452 this_settings["scores"] = parts;
453 tmpopts = this_settings.get ("tex_options", [])
454 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
455 this_settings["tex_options"] = tmpopts
456 if "createCriticalComments" not in tex_settings:
457 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
458 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Score.tex", score))
460 this_settings = copy.deepcopy (tex_settings);
461 this_settings["scoretype"] = "InstrumentalParts";
462 tmpopts = this_settings.get ("tex_options", [])
463 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
464 this_settings["tex_options"] = tmpopts
465 if "createCriticalComments" not in tex_settings:
466 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
467 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Instruments.tex", "Instruments"))
469 return [tex_files, tex_includes];
473 ######################################################################
474 # Creating Makefile
475 ######################################################################
477 def generate_make_files (settings, lily_files, tex_files):
478 make_settings = settings.raw_data.copy ();
480 if settings.has_tex ():
481 tex_settings = settings.get_tex_settings ();
482 tex_settings["includes"] = tex_files[1];
483 tex_settings["files"] = tex_files[0];
484 make_settings["latex"] = tex_settings;
486 score_map = {};
487 instruments_scores = [];
488 nr = 0;
489 for s in settings.score_names ():
490 nr += 1;
491 score_settings = settings.get_score_settings (s);
492 if nr > 1:
493 score_settings["nr"] = nr;
494 else:
495 score_settings["nr"] = "";
496 score_settings["srcfiles"] = lily_files[s];
497 instruments_scores.append (score_settings);
498 for p in score_settings.get ("scores", []):
499 score_map[p] = score_map.get (p, []) + [score_settings];
500 make_settings["works"] = instruments_scores;
502 template = settings.get_template ("Makefile");
503 #basename = tex_settings.get ("basename", "");
504 file = write_file (settings.out_dir, "Makefile", template.render (make_settings));
505 return file;
509 # movements = settings.get ("parts", {});
511 # replacements["instruments"] = string.join (settings.get ("instruments", []));
512 # replacements["scores"] = string.join (settings.get ("scores", []))## + " Instruments";
513 # replacements["srcfiles"] = string.join (src_files);
515 # write_file (output_dir + "Makefile", templates["Makefile"] % replacements);
517 # del replacements["instruments"]
518 # del replacements["scores"]
519 # del replacements["srcfiles"]
520 # return;
523 ######################################################################
524 # Link the orchestrallily package
525 ######################################################################
527 def generate_oly_link (settings):
528 global script_path;
529 try:
530 os.symlink ("../"+script_path, settings.out_dir+"orchestrallily")
531 except OSError:
532 pass
534 ######################################################################
535 # Main function
536 ######################################################################
538 def main ():
539 settings = Settings ();
540 print ("Creating OrchestralLily template in \"%s\", using template \"%s\"." %
541 (settings.out_dir, settings.get_template_name () ));
543 print ("Creating Lilypond files")
544 lily_files = generate_scores (settings);
546 tex_files = []
547 if settings.has_tex ():
548 print ("Creating LaTeX files")
549 tex_files = generate_tex_files (settings, lily_files);
550 print ("Creating OrchestralLily package link")
551 generate_oly_link (settings);
552 print ("Creating Makefile")
553 generate_make_files (settings, lily_files, tex_files);
555 main ();