Templates: Create default dir (to be used by all templates as fallback)
[orchestrallily.git] / generate_oly_score.py
blob047fb410398d1d221386ff9ee221dbd62eb789c4
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)
45 def import_file (filename):
46 res={}
47 try:
48 in_f = codecs.open (filename, "r", "utf-8")
49 s = in_f.read()
50 in_f.close()
51 res = eval(s);
52 except IOError:
53 print ("Unable to load file '%s'. Exiting..." % filename)
54 exit (-1);
55 except SyntaxError as ex:
56 print ex;
57 print ("Unable to interpret settings file '%s', it's syntax is invalid. Exiting..." % filename);
58 exit (-1);
59 return res
62 # Load the score type/number definitions from the oly directory
63 score_types = import_file (os.path.join(sys.path[0], "oly_defs.py"));
67 ######################################################################
68 # Settings
69 ######################################################################
71 class Settings:
72 options = {};
73 arguments = [];
74 globals={};
75 raw_data={};
76 out_dir = "";
77 template_env = None;
79 def __init__ (self):
80 settings_file = self.load_options ();
81 self.load (settings_file);
82 self.raw_data["settings_file_path"] = settings_file;
83 self.raw_data["settings_file"] = os.path.basename(settings_file);
84 self.init_template_env ();
85 self.init_arrays ();
87 def output_dir (self):
88 return self.out_dir
89 def defaults (self):
90 return self.raw_data.get ("defaults", {});
91 def score_names (self):
92 return self.raw_data.get ("scores", ["Score"]);
94 def get_score_settings (self, id):
95 settings = self.globals.copy();
96 settings.update (self.defaults ());
97 settings.update ({"name": id})
98 settings.update (self.raw_data.get (id, {}));
99 self.normalize_part_definitions (settings);
100 return settings;
102 def normalize_part_definitions (self, score_settings):
103 scorename = score_settings.get ("name", "Score");
104 parts = score_settings.get ("parts", [scorename]);
105 result = [];
106 for this_part in parts:
107 if isinstance (this_part, basestring):
108 this_part = {"id": this_part, "filename": this_part }
109 elif not isinstance (this_part, dict):
110 warning ("Invalid part list for score %a: %a" % (scorename, p));
111 return [];
112 if "id" not in this_part:
113 this_part["id"] = scorename;
114 if "filename" not in this_part:
115 this_part["filename"] = this_part["id"];
116 if "piece" not in this_part:
117 this_part["piece"] = this_part["id"];
118 this_part["score"] = scorename;
119 this_part.update (score_settings);
120 result.append (this_part);
121 score_settings["parts"] = result;
123 def has_tex (self):
124 return "latex" in self.raw_data;
125 def get_tex_settings (self):
126 settings = self.globals.copy();
127 settings.update (self.defaults ());
128 settings.update (self.raw_data.get ("latex", {}));
129 return settings;
131 def get_score_parts (self, score_settings):
132 scorename = score_settings.get ("name", "Score");
133 parts = score_settings.get ("parts", [scorename]);
134 result = [];
135 for p in parts:
136 this_part = p;
137 if isinstance (this_part, basestring):
138 this_part = {"id": this_part, "filename": this_part }
139 elif not isinstance (this_part, dict):
140 warning ("Invalid part list for score %a: %a" % (scorename, p));
141 return [];
142 if "id" not in this_part:
143 this_part["id"] = scorename;
144 if "filename" not in this_part:
145 this_part["filename"] = this_part["id"];
146 if "piece" not in this_part:
147 this_part["piece"] = this_part["id"];
148 this_part["score"] = scorename;
149 this_part.update (score_settings);
150 result.append (this_part);
151 return result;
153 def load_options (self):
154 (self.options, self.arguments) = getopt.getopt (sys.argv[1:], 'ho:', ['help', 'output='])
155 for opt in self.options:
156 o = opt[0]
157 a = opt[1]
158 if o == '-h' or o == '--help':
159 help (help_text % globals ())
160 elif o == '-o' or o == '--output':
161 self.out_dir = a
162 else:
163 raise Exception ('unknown option: ' + o)
164 if self.arguments:
165 return self.arguments[0]
166 else:
167 return "oly_structure.def";
169 def load (self, filename):
170 self.raw_data = import_file (filename)
171 if not self.out_dir:
172 self.out_dir = self.raw_data.get ("output_dir", self.out_dir) + "/";
174 def get_template_name (self):
175 return self.raw_data.get ("template", "Full");
176 def get_template_names (self, pattern):
177 allfiles = os.listdir (self.templatepath);
178 files = [];
179 for f in allfiles:
180 if fnmatch.fnmatch(f, pattern):
181 files.append (f);
182 return files;
185 def init_template_env (self):
186 global program_name;
187 global script_path;
188 templatename = self.get_template_name ();
189 self.defaulttemplatepath = script_path + '/Templates/default';
190 self.templatepath = script_path + '/Templates/' + templatename;
191 self.template_env = Environment (
192 loader = FileSystemLoader([self.templatepath,self.defaulttemplatepath]),
193 block_start_string = '<$', block_end_string = '$>',
194 variable_start_string = '<<', variable_end_string = '>>',
195 comment_start_string = '<#', comment_end_string = '#>',
196 #line_statement_prefix = '#'
198 def get_template (self, template):
199 return self.template_env.get_template (template);
202 def init_arrays (self):
203 # Globals
204 self.globals = self.raw_data.copy ();
205 del self.globals["defaults"];
206 if "latex" in self.globals:
207 del self.globals["latex"];
208 for i in self.globals.get ("scores", []):
209 if i in self.globals:
210 del self.globals[i];
212 def assemble_filename (self, base, id, name, ext):
213 parts = [base, id, name];
214 if "" in parts:
215 parts.remove ("")
216 return "_".join (parts) + "." + ext;
217 def assemble_movement_filename (self, basename, mvmnt):
218 return self.assemble_filename( basename, "Music", mvmnt, "ily");
219 def assemble_instrument_filename (self, basename, instr):
220 return self.assemble_filename( basename, "Instrument", instr, "ly");
221 def assemble_score_filename (self, basename, instr):
222 return self.assemble_filename( basename, "Score", instr, "ly");
223 def assemble_settings_filename (self, basename, s):
224 return self.assemble_filename( basename, "Settings", s, "ily");
225 def assemble_itex_filename (self, basename, filename):
226 return self.assemble_filename( "TeX", basename, filename, "itex");
227 def assemble_texscore_filename (self, basename, score):
228 return self.assemble_filename( "TeX", basename, "Score_" + score, "tex");
230 ######################################################################
231 # File Writing
232 ######################################################################
234 def write_file (path, fname, contents):
235 file_name = path + fname;
236 fn = file_name
237 if os.path.exists (file_name):
238 fn += ".new"
240 dir = os.path.dirname (fn);
241 if not os.path.exists (dir):
242 os.mkdir (dir)
244 try:
245 out_f = codecs.open (fn, "w", "utf-8")
246 s = out_f.write(contents)
247 out_f.write ("\n")
248 out_f.close()
249 except IOError:
250 print ("Unable to write to output file '%s'. Exiting..." % fn)
251 exit (-1);
253 # If the file already existed, check if the new file is identical.
254 if (fn != file_name):
255 patchfile = os.path.join (dir, "patches", os.path.basename(file_name) + ".patch")
256 if os.path.exists (patchfile):
257 try:
258 retcode = subprocess.call("patch -Ns \""+ fn+ "\" \"" + patchfile + "\"", shell=True)
259 if retcode < 0:
260 print >>sys.stderr, "Unable to apply patch to file \"", fn, "\"."
261 except OSError, e:
262 print >>sys.stderr, "Execution failed:", e
264 if filecmp.cmp (fn, file_name):
265 os.unlink (fn);
266 else:
267 print ("A file %s already existed, created new file %s." % (os.path.basename(file_name), os.path.basename(fn)))
271 ######################################################################
272 # Creating movement files
273 ######################################################################
276 def generate_movement_files (score_name, score_settings, settings):
277 parts_files = [];
278 for part_settings in settings.get_score_parts (score_settings):
279 template = settings.get_template ("Lily_Music_Movement.ily");
280 basename = part_settings.get ("basename", score_name);
281 filename = part_settings.get ("filename", score_name + "Part");
282 filename = settings.assemble_movement_filename (basename, filename);
283 write_file (settings.out_dir, filename, template.render (part_settings));
284 parts_files.append (filename);
286 return parts_files;
290 ######################################################################
291 # Creating instrumental score files
292 ######################################################################
294 def generate_instrument_files (score_name, score_settings, settings):
295 instrument_files = [];
296 settings_files = set ();
297 instrument_settings = score_settings.copy ();
298 instrument_settings["parts"] = settings.get_score_parts (score_settings);
300 template = settings.get_template ("Lily_Instrument.ly");
301 basename = score_settings.get ("basename", score_name );
303 noscore_instruments = score_settings.get ("noscore_instruments", [])
304 for i in score_settings.get ("instruments", []):
305 if i in noscore_instruments:
306 continue;
307 instrument_settings["instrument"] = i;
308 if i in score_settings.get ("vocalvoices"):
309 instrument_settings["settings"] = "VocalVoice";
310 else:
311 instrument_settings["settings"] = "Instrument";
312 settings_files.add (instrument_settings["settings"]);
313 filename = settings.assemble_instrument_filename (basename, i);
314 write_file (settings.out_dir, filename, template.render (instrument_settings));
315 instrument_files.append (filename);
317 return (instrument_files, settings_files);
321 ######################################################################
322 # Creating score files
323 ######################################################################
326 score_name_map = {
327 "Particell": "Particell",
329 settings_map = {
330 "OrganScore": "VocalScore",
331 "ChoralScore": "ChoralScore",
332 "LongScore": "FullScore",
333 "OriginalScore": "FullScore",
334 "Particell": "FullScore"
336 scores_cues = ["ChoralScore", "VocalScore"];
338 def generate_score_files (score_name, score_settings, settings):
339 score_files = [];
340 settings_files = set ();
341 s_settings = score_settings.copy ();
342 s_settings["parts"] = settings.get_score_parts (score_settings);
343 s_settings["fullscore"] = True;
345 template = settings.get_template ("Lily_Score.ly");
346 basename = score_settings.get ("basename", score_name );
348 for s in score_settings.get ("scores", []):
349 fullsn = score_name_map.get (s, (s + "Score"));
350 s_settings["score"] = fullsn;
351 s_settings["nocues"] = fullsn not in scores_cues;
352 s_settings["settings"] = settings_map.get (fullsn,fullsn)
353 settings_files.add (s_settings["settings"]);
354 filename = settings.assemble_score_filename (basename, s);
355 write_file (settings.out_dir, filename, template.render (s_settings));
356 score_files.append (filename);
358 return (score_files, settings_files);
362 ######################################################################
363 # Creating settings files
364 ######################################################################
366 def write_settings_file_if_exists (settings, score_settings, template, filename):
367 try:
368 template = settings.get_template (template);
369 basename = score_settings.get ("basename", "");
370 filename = settings.assemble_settings_filename (basename, filename);
371 write_file (settings.out_dir, filename, template.render (score_settings));
372 return filename;
373 except jinja2.exceptions.TemplateNotFound as e:
374 return None;
376 def generate_settings_files (score_name, score_settings, settings, settings_files):
377 out_files = [];
378 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Global.ily", "Global" ));
379 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings.ily", "" ));
381 for s in settings_files:
382 score_settings["settings"] = s.lower ();
383 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Generic.ily", s));
385 return out_files;
389 ######################################################################
390 # Generation of Lilypond Files
391 ######################################################################
393 def generate_scores (settings):
394 files = {}
395 for s in settings.score_names ():
396 score_settings = settings.get_score_settings (s);
397 parts_files = generate_movement_files (s, score_settings, settings);
398 (instrument_files, isettings_files) = generate_instrument_files (s, score_settings, settings);
399 (score_files, ssettings_files) = generate_score_files (s, score_settings, settings);
400 included_settings_files = ssettings_files | isettings_files;
401 score_settings["parts_files"] = parts_files;
402 settings_files = generate_settings_files (s, score_settings, settings, included_settings_files );
403 files[s] = {"settings": settings_files,
404 "scores": score_files,
405 "instruments": instrument_files,
406 "parts": parts_files };
407 return files
411 ######################################################################
412 # Creating LaTeX files
413 ######################################################################
415 def write_itex_file (settings, tex_settings, template, filename):
416 template = settings.get_template (template);
417 basename = tex_settings.get ("basename", "");
418 filename = settings.assemble_itex_filename (basename, filename);
419 write_file (settings.out_dir, filename, template.render (tex_settings));
420 return filename;
422 def write_texscore_file (settings, tex_settings, template, score):
423 template = settings.get_template (template);
424 basename = tex_settings.get ("basename", "");
425 filename = settings.assemble_texscore_filename (basename, score);
426 write_file (settings.out_dir, filename, template.render (tex_settings));
427 return filename;
429 no_criticalcomments_scores = ["Vocal", "Choral", "Organ"];
430 tex_options_map = {
431 "Organ": "vocalscore",
432 "Choral": "choralscore",
433 "Vocal": "vocalscore",
434 "Long": "fullscore",
435 "Full": "fullscore",
436 "Original": "fullscore",
437 "Particell": "vocalscore",
438 "InstrumentalParts": "instrumentalparts",
439 # TODO: chambermusic
442 def generate_tex_files (settings, lily_files):
443 tex_files = [];
444 tex_includes = [];
445 tex_settings = settings.get_tex_settings ();
446 tex_settings["lily_files"] = lily_files;
448 score_map = {};
449 instruments_scores = [];
450 for s in settings.score_names ():
451 score_settings = settings.get_score_settings (s);
452 instruments_scores.append (score_settings);
453 for p in score_settings.get ("scores", []):
454 score_map[p] = score_map.get (p, []) + [score_settings];
455 for i in tex_settings.get ("instruments", []):
456 if i in tex_settings.get ("instrumentalscores", []):
457 score_map[i] = score_map.get (i, []) + [score_settings];
458 tex_settings["works"] = instruments_scores;
460 tex_include_templates = settings.get_template_names("TeX_*.itex");
461 for t in tex_include_templates:
462 base = re.sub( r'TeX_(.*)\.itex', r'\1', t);
463 tex_includes.append (write_itex_file (settings, tex_settings, t, base));
465 for (score, parts) in score_map.items ():
466 this_settings = copy.deepcopy (tex_settings);
467 this_settings["scoretype"] = score;
468 this_settings["scores"] = parts;
469 #this_settings["scorebasetype"] = "Score";
470 #this_settings["scorenamebase"] = "ScoreType";
471 tmpopts = this_settings.get ("tex_options", [])
472 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
473 this_settings["tex_options"] = tmpopts
474 if "createCriticalComments" not in tex_settings:
475 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
476 this_settings["is_instrument"] = score in tex_settings.get ("instruments", []);
477 #this_settings["scorebasetype"] = "Instrument";
478 #this_settings["scorenamebase"] = "Name";
479 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Score.tex", score))
481 this_settings = copy.deepcopy (tex_settings);
482 this_settings["scoretype"] = "InstrumentalParts";
483 tmpopts = this_settings.get ("tex_options", [])
484 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
485 this_settings["tex_options"] = tmpopts
486 if "createCriticalComments" not in tex_settings:
487 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
488 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Instruments.tex", "Instruments"))
490 return [tex_files, tex_includes];
494 ######################################################################
495 # Creating Makefile
496 ######################################################################
498 def generate_make_files (settings, lily_files, tex_files):
499 make_settings = settings.raw_data.copy ();
501 if settings.has_tex ():
502 tex_settings = settings.get_tex_settings ();
503 tex_settings["includes"] = tex_files[1];
504 tex_settings["files"] = tex_files[0];
505 make_settings["latex"] = tex_settings;
507 score_map = {};
508 instruments_scores = [];
509 nr = 0;
510 for s in settings.score_names ():
511 nr += 1;
512 score_settings = settings.get_score_settings (s);
513 if nr > 1:
514 score_settings["nr"] = nr;
515 else:
516 score_settings["nr"] = "";
517 score_settings["srcfiles"] = lily_files[s];
518 instruments_scores.append (score_settings);
519 for p in score_settings.get ("scores", []):
520 score_map[p] = score_map.get (p, []) + [score_settings];
521 make_settings["works"] = instruments_scores;
523 template = settings.get_template ("Makefile");
524 #basename = tex_settings.get ("basename", "");
525 file = write_file (settings.out_dir, "Makefile", template.render (make_settings));
526 return file;
531 ######################################################################
532 # Creating webshop_descriptions.def
533 ######################################################################
535 def generate_webshop_files (settings, lily_files, tex_files):
536 webshop_settings = settings.raw_data.copy ();
537 template = settings.get_template ("webshop_descriptions.def");
538 #basename = tex_settings.get ("basename", "");
539 #print pp.pprint(settings.raw_data);
540 #print pp.pprint(settings.score_names ())
542 scores=[];
543 noscore_instruments = webshop_settings.get ("noscore_instruments", [])
545 for s in settings.score_names ():
546 score_settings = settings.get_score_settings (s);
547 for i in score_settings.get ("scores", []) + ["Instruments"] + score_settings.get ("instruments", []):
548 if i in noscore_instruments:
549 continue;
550 score_info = score_types.get (i, {});
551 score_type = score_info.get ("Name", "");
552 score_id = score_info.get ("Number", "XXX");
553 scores.append({"sku": score_settings.get ("scorenumber")+"-"+score_id, "type": score_type });
555 webshop_settings["webshop_editions"] = scores;
556 file = write_file (settings.out_dir, "webshop_descriptions.def", template.render (webshop_settings));
560 ######################################################################
561 # Link the orchestrallily package
562 ######################################################################
564 def generate_oly_link (settings):
565 global script_path;
566 try:
567 os.symlink ("../"+script_path, settings.out_dir+"orchestrallily")
568 except OSError:
569 pass
571 ######################################################################
572 # Main function
573 ######################################################################
575 def main ():
576 settings = Settings ();
577 print ("Creating OrchestralLily template in \"%s\", using template \"%s\"." %
578 (settings.out_dir, settings.get_template_name () ));
580 print ("Creating Lilypond files")
581 lily_files = generate_scores (settings);
583 tex_files = []
584 if settings.has_tex ():
585 print ("Creating LaTeX files")
586 tex_files = generate_tex_files (settings, lily_files);
587 print ("Creating OrchestralLily package link")
588 generate_oly_link (settings);
589 print ("Creating webshop_descriptions.def")
590 generate_webshop_files (settings, lily_files, tex_files);
591 print ("Creating Makefile")
592 generate_make_files (settings, lily_files, tex_files);
594 main ();