Add information for advanced preset editing.
[bertos.git] / wizard / BProject.py
1 #!/usr/bin/env python
2 # encoding: utf-8
3 #
4 # This file is part of BeRTOS.
5 #
6 # Bertos is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19 #
20 # As a special exception, you may use this file as part of a free software
21 # library without restriction.  Specifically, if other files instantiate
22 # templates or use macros or inline functions from this file, or you compile
23 # this file and link it with other files to produce an executable, this
24 # file does not by itself cause the resulting executable to be covered by
25 # the GNU General Public License.  This exception does not however
26 # invalidate any other reasons why the executable file might be covered by
27 # the GNU General Public License.
28 #
29 # Copyright 2008 Develer S.r.l. (http://www.develer.com/)
30 #
31 # $Id$
32 #
33 # Author: Lorenzo Berni <duplo@develer.com>
34 #
35
36 import os
37 import fnmatch
38 import copy
39 import pickle
40 import shutil
41 import copytree
42
43 import DefineException
44
45 from LoadException import VersionException, ToolchainException
46
47 import const
48
49 from bertos_utils import (
50                             # Utility functions
51                             isBertosDir, getTagSet, getInfos, updateConfigurationValues,
52                             loadConfigurationInfos, loadDefineLists, loadModuleDefinition,
53                             getCommentList, sub,
54
55                             # Project creation functions
56                             projectFileGenerator, versionFileGenerator, makefileGenerator,
57                             userMkGenerator, mkGenerator, loadPlugin, mergeSources,
58
59                             # Custom exceptions
60                             ParseError, SupportedException
61                         )
62
63 class BProject(object):
64     """
65     Simple class for store and retrieve project informations.
66     """
67
68     def __init__(self, project_file="", info_dict={}):
69         self.infos = {}
70         self._cached_queries = {}
71         self.edit = False
72         if project_file:
73             self.edit = True
74             self.loadBertosProject(project_file, info_dict)
75
76     #--- Load methods (methods that loads data into project) ------------------#
77
78     def loadBertosProject(self, project_file, info_dict):
79         project_dir = os.path.dirname(project_file)
80         project_data = pickle.loads(open(project_file, "r").read())
81         # If PROJECT_NAME is not defined it use the directory name as PROJECT_NAME
82         # NOTE: this can throw an Exception if the user has changed the directory containing the project
83         self.infos["PROJECT_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(project_dir))
84         self.infos["PROJECT_PATH"] = os.path.dirname(project_file)
85
86         preset = project_data.get("PRESET", False)
87         if not preset:
88             # Ignore the SOURCES_PATH inside the project file if it's not setted as 'preset'
89             project_data["SOURCES_PATH"] = project_dir
90         else:
91             linked_sources_path = project_data["SOURCES_PATH"]
92             sources_abspath = os.path.abspath(os.path.join(project_dir, linked_sources_path))
93             project_data["SOURCES_PATH"] = sources_abspath
94         
95         self._loadBertosSourceStuff(project_data["SOURCES_PATH"], info_dict.get("SOURCES_PATH", None))
96         
97         self.infos["PRESET"] = preset
98
99         # For those projects that don't have a VERSION file create a dummy one.
100         if not isBertosDir(project_dir):
101             version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
102             open(os.path.join(project_dir, "VERSION"), "w").write(version_file.replace("$version", "").strip())
103
104         self.loadSourceTree()
105         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
106         self._loadToolchainStuff(project_data["TOOLCHAIN"], info_dict.get("TOOLCHAIN", None))
107         self.infos["OUTPUT"] = project_data["OUTPUT"]
108         self.loadModuleData(True)
109         self.setEnabledModules(project_data["ENABLED_MODULES"])
110
111     def _loadBertosSourceStuff(self, sources_path, forced_version=None):
112         if forced_version:
113             sources_path = forced_version
114         if os.path.exists(sources_path):
115             self.infos["SOURCES_PATH"] = sources_path
116         else:
117             raise VersionException(self)
118
119     def _loadCpuStuff(self, cpu_name, cpu_frequency):
120         self.infos["CPU_NAME"] = cpu_name
121         cpu_info = self.getCpuInfos()
122         for cpu in cpu_info:
123             if cpu["CPU_NAME"] == cpu_name:
124                 self.infos["CPU_INFOS"] = cpu
125                 break
126         tag_list = getTagSet(cpu_info)
127         # Create, fill and store the dict with the tags
128         tag_dict = {}
129         for element in tag_list:
130             tag_dict[element] = False
131         infos = self.info("CPU_INFOS")
132         for tag in tag_dict:
133             if tag in infos["CPU_TAGS"] + [infos["CPU_NAME"], infos["TOOLCHAIN"]]:
134                 tag_dict[tag] = True
135             else:
136                 tag_dict[tag] = False
137         self.infos["ALL_CPU_TAGS"] = tag_dict
138         self.infos["SELECTED_FREQ"] = cpu_frequency
139
140     def _loadToolchainStuff(self, toolchain, forced_toolchain=None):
141         toolchain = toolchain
142         if forced_toolchain:
143             toolchain = forced_toolchain
144         if os.path.exists(toolchain["path"]):
145             self.infos["TOOLCHAIN"] = toolchain
146         else:
147             raise ToolchainException(self)
148
149     def loadProjectFromPreset(self, preset):
150         """
151         Load a project from a preset.
152         NOTE: this is a stub.
153         """
154         project_file = os.path.join(preset, "project.bertos")
155         project_data = pickle.loads(open(project_file, "r").read())
156         self.loadSourceTree()
157         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
158         self._loadToolchainStuff(project_data["TOOLCHAIN"])
159         # NOTE: this is a HACK!!!
160         # TODO: find a better way to reuse loadModuleData
161         old_project_name = self.infos["PROJECT_NAME"]
162         old_project_path = self.infos["PROJECT_PATH"]
163         self.infos["PROJECT_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(preset))
164         self.infos["PROJECT_PATH"] = preset
165         self.loadModuleData(True)
166         self.setEnabledModules(project_data["ENABLED_MODULES"])
167         self.infos["PROJECT_NAME"] = old_project_name
168         self.infos["PROJECT_PATH"] = old_project_path
169         self.infos["PRESET_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(preset))
170         self.infos["PRESET_PATH"] = preset
171
172     def loadProjectPresets(self):
173         """
174         Load the default presets (into the const.PREDEFINED_BOARDS_DIR).
175         """
176         # NOTE: this method does nothing (for now).
177         preset_path = os.path.join(self.infos["SOURCES_PATH"], const.PREDEFINED_BOARDS_DIR)
178         preset_tree = {}
179         if os.path.exists(preset_path):
180             preset_tree = self._loadProjectPresetTree(preset_path)
181         self.infos["PRESET_TREE"] = preset_tree
182
183     def _loadProjectPresetTree(self, path):
184         _tree = {}
185         _tree["info"] = self._loadPresetInfo(os.path.join(path, const.PREDEFINED_BOARD_SPEC_FILE))
186         _tree["info"]["filename"] = os.path.basename(path)
187         _tree["info"]["path"] = path
188         _tree["children"] = []
189         entries = set(os.listdir(path))
190         for entry in entries:
191             _path = os.path.join(path, entry)
192             if os.path.isdir(_path):
193                 sub_entries = set(os.listdir(_path))
194                 if const.PREDEFINED_BOARD_SPEC_FILE in sub_entries:
195                     _tree["children"].append(self._loadProjectPresetTree(_path))
196         # Add into the info dict the dir type (dir/project)
197         if _tree["children"]:
198             _tree["info"]["type"] = "dir"
199         else:
200             _tree["info"]["type"] = "project"
201         return _tree
202
203     def _loadPresetInfo(self, preset_spec_file):
204         D = {}
205         execfile(preset_spec_file, {}, D)
206         return D
207
208     def loadModuleData(self, edit=False):
209         module_info_dict = {}
210         list_info_dict = {}
211         configuration_info_dict = {}
212         file_dict = {}
213         for filename, path in self.findDefinitions("*.h") + self.findDefinitions("*.c") + self.findDefinitions("*.s") + self.findDefinitions("*.S"):
214             comment_list = getCommentList(open(path + "/" + filename, "r").read())
215             if len(comment_list) > 0:
216                 module_info = {}
217                 configuration_info = {}
218                 try:
219                     to_be_parsed, module_dict = loadModuleDefinition(comment_list[0])
220                 except ParseError, err:
221                     raise DefineException.ModuleDefineException(path, err.line_number, err.line)
222                 for module, information in module_dict.items():
223                     if "depends" not in information:
224                         information["depends"] = ()
225                     information["depends"] += (filename.split(".")[0],)
226                     information["category"] = os.path.basename(path)
227                     if "configuration" in information and len(information["configuration"]):
228                         configuration = module_dict[module]["configuration"]
229                         try:
230                             configuration_info[configuration] = loadConfigurationInfos(self.infos["SOURCES_PATH"] + "/" + configuration)
231                         except ParseError, err:
232                             raise DefineException.ConfigurationDefineException(self.infos["SOURCES_PATH"] + "/" + configuration, err.line_number, err.line)
233                         if edit:
234                             try:
235                                 path = self.infos["PROJECT_NAME"]
236                                 directory = self.infos["PROJECT_PATH"]
237                                 user_configuration = loadConfigurationInfos(directory + "/" + configuration.replace("bertos", path))
238                                 configuration_info[configuration] = updateConfigurationValues(configuration_info[configuration], user_configuration)
239                             except ParseError, err:
240                                 raise DefineException.ConfigurationDefineException(directory + "/" + configuration.replace("bertos", path))
241                 module_info_dict.update(module_dict)
242                 configuration_info_dict.update(configuration_info)
243                 if to_be_parsed:
244                     try:
245                         list_dict = loadDefineLists(comment_list[1:])
246                         list_info_dict.update(list_dict)
247                     except ParseError, err:
248                         raise DefineException.EnumDefineException(path, err.line_number, err.line)
249         for tag in self.infos["CPU_INFOS"]["CPU_TAGS"]:
250             for filename, path in self.findDefinitions("*_" + tag + ".h"):
251                 comment_list = getCommentList(open(path + "/" + filename, "r").read())
252                 list_info_dict.update(loadDefineLists(comment_list))
253         self.infos["MODULES"] = module_info_dict
254         self.infos["LISTS"] = list_info_dict
255         self.infos["CONFIGURATIONS"] = configuration_info_dict
256         self.infos["FILES"] = file_dict
257
258     def loadSourceTree(self):
259         """
260         Index BeRTOS source and load it in memory.
261         """
262         # Index only the SOURCES_PATH/bertos content
263         bertos_sources_dir = os.path.join(self.info("SOURCES_PATH"), "bertos")
264         file_dict = {}
265         if os.path.exists(bertos_sources_dir):
266             for element in os.walk(bertos_sources_dir):
267                 for f in element[2]:
268                     file_dict[f] = file_dict.get(f, []) + [element[0]]
269         self.infos["FILE_DICT"] = file_dict
270
271     def reloadCpuInfo(self):
272         for cpu_info in self.getCpuInfos():
273             if cpu_info["CPU_NAME"] == self.infos["CPU_NAME"]:
274                 self.infos["CPU_INFOS"] = cpu_info
275
276     #-------------------------------------------------------------------------#
277
278     def createBertosProject(self):
279         # NOTE: Temporary hack.
280         if self.edit:
281             self._editBertosProject()
282         else:
283             if not self.from_preset:
284                 self._newCustomBertosProject()
285             else:
286                 self._newBertosProjectFromPreset()
287
288     def _newBertosProject(self):
289         for directory in (self.maindir, self.srcdir, self.prjdir, self.hwdir, self.cfgdir):
290             self._createDirectory(directory)
291         # Write the project file
292         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
293         # VERSION file
294         self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
295         # Destination makefile
296         self._writeMakefile(os.path.join(self.maindir, "Makefile"))
297         # Copy the sources
298         self._copySources(self.sources_dir, self.srcdir)
299         # Copy all the hw files
300         self._writeHwFiles(self.sources_dir, self.hwdir)
301         # Set properly the autoenabled parameters
302         self._setupAutoenabledParameters()
303         # Copy all the configuration files
304         self._writeCfgFiles(self.sources_dir, self.cfgdir)
305         # Destination wizard mk file
306         self._writeWizardMkFile(os.path.join(self.prjdir, os.path.basename(self.prjdir) + "_wiz.mk"))
307
308     def _newCustomBertosProject(self):
309         # Create/write/copy the common things
310         self._newBertosProject()
311         # Destination user mk file
312         self._writeUserMkFile(os.path.join(self.prjdir, os.path.basename(self.prjdir) + ".mk"))
313         # Destination main.c file
314         self._writeMainFile(self.prjdir + "/main.c")
315         # Create project files for selected plugins
316         self._createProjectFiles()
317
318     def _newBertosProjectFromPreset(self):
319         # Create/write/copy the common things
320         self._newBertosProject()
321
322         # Copy all the files and dirs except cfg/hw/*_wiz.mk
323         self._writeCustomSrcFiles()
324
325         if self.infos["EMPTY_MAIN"]:
326             # Create and empty main.c file only if the user check the box
327             self._writeMainFile(self.prjdir + "/main.c")
328
329         # Create project files for selected plugins
330         self._createProjectFiles()
331
332     def _editBertosProject(self):
333         # Write the project file
334         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
335         # VERSION file
336         self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
337         # Destination makefile
338         self._writeMakefile(os.path.join(self.maindir, "Makefile"))
339         # Merge sources
340         self._mergeSources(self.sources_dir, self.srcdir, self.old_srcdir)
341         # Copy all the hw files
342         self._writeHwFiles(self.sources_dir, self.hwdir)
343         # Set properly the autoenabled parameters
344         self._setupAutoenabledParameters()
345         # Copy all the configuration files
346         self._writeCfgFiles(self.sources_dir, self.cfgdir)
347         # Destination wizard mk file
348         self._writeWizardMkFile(os.path.join(self.prjdir, os.path.basename(self.prjdir) + "_wiz.mk"))
349         # Create project files for selected plugins
350         self._createProjectFiles()
351
352     def _createProjectFiles(self):
353         # Files for selected plugins
354         relevants_files = {}
355         for plugin in self.infos["OUTPUT"]:
356             module = loadPlugin(plugin)
357             relevants_files[plugin] = module.createProject(self)
358         self.infos["RELEVANT_FILES"] = relevants_files
359
360     def _writeVersionFile(self, filename):
361         version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
362         open(filename, "w").write(versionFileGenerator(self, version_file))
363
364     def _writeProjectFile(self, filename):
365         f = open(filename, "w")
366         f.write(projectFileGenerator(self))
367         f.close()
368
369     def _writeMakefile(self, filename):
370         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/Makefile"), "r").read()
371         makefile = makefileGenerator(self, makefile)
372         open(filename, "w").write(makefile)
373
374     def _writeUserMkFile(self, filename):
375         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template.mk"), "r").read()
376         # Deadly performances loss was here :(
377         makefile = userMkGenerator(self, makefile)
378         open(filename, "w").write(makefile)
379
380     def _writeWizardMkFile(self, filename):
381         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template_wiz.mk"), "r").read()
382         makefile = mkGenerator(self, makefile)
383         open(filename, "w").write(makefile)
384
385     def _writeMainFile(self, filename):
386         main = open(os.path.join(const.DATA_DIR, "srctemplates/main.c"), "r").read()
387         open(filename, "w").write(main)
388
389     def _writeHwFiles(self, source_dir, destination_dir):
390         for module, information in self.infos["MODULES"].items():
391             for hwfile in information["hw"]:
392                 string = open(source_dir + "/" + hwfile, "r").read()
393                 hwfile_path = destination_dir + "/" + os.path.basename(hwfile)
394                 if not self.edit or not os.path.exists(hwfile_path):
395                     # If not in editing mode it copies all the hw files. If in
396                     # editing mode it copies only the files that don't exist yet
397                     open(os.path.join(destination_dir,os.path.basename(hwfile)), "w").write(string)
398
399     def _writeCfgFiles(self, source_dir, destination_dir):
400         for configuration, information in self.infos["CONFIGURATIONS"].items():
401             string = open(source_dir + "/" + configuration, "r").read()
402             for start, parameter in information["paramlist"]:
403                 infos = information[parameter]
404                 value = infos["value"]
405                 if "unsigned" in infos["informations"] and infos["informations"]["unsigned"]:
406                     value += "U"
407                 if "long" in infos["informations"] and infos["informations"]["long"]:
408                     value += "L"
409                 string = sub(string, parameter, value)
410             f = open(os.path.join(destination_dir, os.path.basename(configuration)), "w")
411             f.write(string)
412             f.close()
413
414     def _writeCustomSrcFiles(self):
415         preset = self.infos["PRESET_PATH"]
416         preset_name = self.infos["PRESET_NAME"]
417         origin = os.path.join(preset, preset_name)
418         project_related_stuff = ("cfg", "hw", self.infos["PROJECT_NAME"] + "_wiz.mk") + const.IGNORE_LIST
419         for element in os.listdir(origin):
420             if element not in project_related_stuff:
421                 full_path = os.path.join(origin, element)
422                 if os.path.isdir(full_path):
423                     copytree.copytree(full_path, os.path.join(self.prjdir, element), ignore_list=const.IGNORE_LIST)
424                 else:
425                     shutil.copy(full_path, self.prjdir)
426
427     def _setupAutoenabledParameters(self):
428         for module, information in self.infos["MODULES"].items():
429             if "configuration" in information and information["configuration"] != "":
430                 configurations = self.infos["CONFIGURATIONS"]
431                 configuration = configurations[information["configuration"]]
432                 for start, parameter in configuration["paramlist"]:
433                     if "type" in configuration[parameter]["informations"] and configuration[parameter]["informations"]["type"] == "autoenabled":
434                         configuration[parameter]["value"] = "1" if information["enabled"] else "0"
435                 self.infos["CONFIGURATIONS"] = configurations
436
437     @property
438     def maindir(self):
439         return self.infos.get("PROJECT_PATH", None)
440
441     @property
442     def srcdir(self):
443         if self.maindir:
444             return os.path.join(self.maindir, "bertos")
445         else:
446             return None
447
448     @property
449     def prjdir(self):
450         if self.maindir:
451             return os.path.join(self.maindir, self.infos["PROJECT_NAME"])
452         else:
453             return None
454
455     @property
456     def hwdir(self):
457         if self.prjdir:
458             return os.path.join(self.prjdir, "hw")
459         else:
460             return None
461
462     @property
463     def cfgdir(self):
464         if self.prjdir:
465             return os.path.join(self.prjdir, "cfg")
466         else:
467             return None
468
469     @property
470     def old_srcdir(self):
471         return self.infos.get("OLD_SOURCES_PATH", None)
472
473     @property
474     def sources_dir(self):
475         return self.infos.get("SOURCES_PATH", None)
476
477     @property
478     def from_preset(self):
479         return self.infos.get("PROJECT_FROM_PRESET", False)
480
481     def _createDirectory(self, directory):
482         if not directory:
483             return
484         if os.path.isdir(directory):
485             shutil.rmtree(directory, True)
486         os.makedirs(directory)
487
488     def _copySources(self, origin, destination):
489         # If not in editing mode it copies all the bertos sources in the /bertos subdirectory of the project
490         shutil.rmtree(destination, True)
491         copytree.copytree(origin + "/bertos", destination, ignore_list=const.IGNORE_LIST)
492
493     def _mergeSources(self, origin, destination, old_sources_dir):
494         if old_sources_dir:
495             mergeSources(destination, origin, old_sources_dir)
496
497     def setInfo(self, key, value):
498         """
499         Store the given value with the name key.
500         """
501         self.infos[key] = value
502
503     def info(self, key, default=None):
504         """
505         Retrieve the value associated with the name key.
506         """
507         if key in self.infos:
508             return copy.deepcopy(self.infos[key])
509         return default
510
511     def getCpuInfos(self):
512         cpuInfos = []
513         for definition in self.findDefinitions(const.CPU_DEFINITION):
514             cpuInfos.append(getInfos(definition))
515         return cpuInfos
516
517     def searchFiles(self, filename):
518         file_dict = self.infos["FILE_DICT"]
519         return [(filename, dirname) for dirname in file_dict.get(filename, [])]
520
521     def findDefinitions(self, ftype):
522         # Maintain a cache for every scanned SOURCES_PATH
523         definitions_dict = self._cached_queries.get(self.infos["SOURCES_PATH"], {})
524         definitions = definitions_dict.get(ftype, None)
525         if definitions is not None:
526             return definitions
527         file_dict = self.infos["FILE_DICT"]
528         definitions = []
529         for filename in file_dict:
530             if fnmatch.fnmatch(filename, ftype):
531                 definitions += [(filename, dirname) for dirname in file_dict.get(filename, [])]
532
533         # If no cache for the current SOURCES_PATH create an empty one
534         if not definitions_dict:
535             self._cached_queries[self.infos["SOURCES_PATH"]] = {}
536         # Fill the empty cache with the result
537         self._cached_queries[self.infos["SOURCES_PATH"]][ftype] = definitions
538         return definitions
539
540     def setEnabledModules(self, enabled_modules):
541         modules = self.infos["MODULES"]
542         files = {}
543         for module, information in modules.items():
544             information["enabled"] = module in enabled_modules
545             if information["enabled"]:
546                 for dependency in information["depends"]:
547                     if not dependency in modules:
548                         files[dependency] = files.get(dependency, 0) + 1
549         self.infos["MODULES"] = modules
550         self.infos["FILES"] = files
551
552     def __repr__(self):
553         return "<BProject:instance %d>%s" %(id(self), repr(self.infos))