Fix problems with older BeRTOS versions.
[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, loadPlugin, 
57                             mergeSources,
58
59                             # Custom exceptions
60                             ParseError, SupportedException
61                         )
62 import bertos_utils
63
64 from compatibility import updateProject
65
66 class BProject(object):
67     """
68     Simple class for store and retrieve project informations.
69     """
70
71     def __init__(self, project_file="", info_dict={}):
72         self.infos = {}
73         self._cached_queries = {}
74         self.edit = False
75         if project_file:
76             self.edit = True
77             self.loadBertosProject(project_file, info_dict)
78
79     #--- Load methods (methods that loads data into project) ------------------#
80
81     def loadBertosProject(self, project_file, info_dict):
82         project_dir = os.path.dirname(project_file)
83         project_data = pickle.loads(open(project_file, "r").read())
84         updateProject(project_data)
85         # If PROJECT_NAME is not defined it use the directory name as PROJECT_NAME
86         # NOTE: this can throw an Exception if the user has changed the directory containing the project
87         self.infos["PROJECT_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(project_dir))
88         self.infos["PROJECT_PATH"] = os.path.dirname(project_file)
89         project_src_path = os.path.join(project_dir, project_data.get("PROJECT_SRC_PATH", project_data["PROJECT_NAME"]))
90         if project_src_path:
91             self.infos["PROJECT_SRC_PATH"] = project_src_path
92             
93         else:
94             # In projects created with older versions of the Wizard this metadata doesn't exist
95             self.infos["PROJECT_SRC_PATH"] = os.path.join(self.infos["PROJECT_PATH"], self.infos["PROJECT_NAME"])
96
97         linked_sources_path = project_data["BERTOS_PATH"]
98         sources_abspath = os.path.abspath(os.path.join(project_dir, linked_sources_path))
99         project_data["BERTOS_PATH"] = sources_abspath
100         
101         self._loadBertosSourceStuff(project_data["BERTOS_PATH"], info_dict.get("BERTOS_PATH", None))
102         
103         self.infos["PRESET"] = project_data.get("PRESET", False)
104
105         # For those projects that don't have a VERSION file create a dummy one.
106         if not isBertosDir(project_dir) and not self.is_preset:
107             version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
108             open(os.path.join(project_dir, "VERSION"), "w").write(version_file.replace("$version", "").strip())
109
110         self.loadSourceTree()
111         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
112         self._loadToolchainStuff(project_data["TOOLCHAIN"], info_dict.get("TOOLCHAIN", None))
113         self.infos["OUTPUT"] = project_data["OUTPUT"]
114         self.loadModuleData(True)
115         self.setEnabledModules(project_data["ENABLED_MODULES"])
116
117     def _loadBertosSourceStuff(self, sources_path, forced_version=None):
118         if forced_version:
119             sources_path = forced_version
120         if os.path.exists(sources_path):
121             self.infos["BERTOS_PATH"] = sources_path
122         else:
123             raise VersionException(self)
124
125     def _loadCpuStuff(self, cpu_name, cpu_frequency):
126         self.infos["CPU_NAME"] = cpu_name
127         cpu_info = self.getCpuInfos()
128         for cpu in cpu_info:
129             if cpu["CPU_NAME"] == cpu_name:
130                 self.infos["CPU_INFOS"] = cpu
131                 break
132         tag_list = getTagSet(cpu_info)
133         # Create, fill and store the dict with the tags
134         tag_dict = {}
135         for element in tag_list:
136             tag_dict[element] = False
137         infos = self.info("CPU_INFOS")
138         for tag in tag_dict:
139             if tag in infos["CPU_TAGS"] + [infos["CPU_NAME"], infos["TOOLCHAIN"]]:
140                 tag_dict[tag] = True
141             else:
142                 tag_dict[tag] = False
143         self.infos["ALL_CPU_TAGS"] = tag_dict
144         self.infos["SELECTED_FREQ"] = cpu_frequency
145
146     def _loadToolchainStuff(self, toolchain, forced_toolchain=None):
147         toolchain = toolchain
148         if forced_toolchain:
149             toolchain = forced_toolchain
150         if os.path.exists(toolchain["path"]):
151             self.infos["TOOLCHAIN"] = toolchain
152         else:
153             raise ToolchainException(self)
154
155     def loadProjectFromPreset(self, preset):
156         """
157         Load a project from a preset.
158         NOTE: this is a stub.
159         """
160         project_file = os.path.join(preset, "project.bertos")
161         project_data = pickle.loads(open(project_file, "r").read())
162         self.loadSourceTree()
163         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
164         self._loadToolchainStuff(project_data["TOOLCHAIN"])
165
166         # NOTE: this is a HACK!!!
167         # TODO: find a better way to reuse loadModuleData
168         preset_project_name = project_data.get("PROJECT_NAME", os.path.basename(preset))
169         preset_prj_src_path = os.path.join(preset, project_data.get("PROJECT_SRC_PATH", os.path.join(preset, preset_project_name)))
170
171         old_project_name = self.infos["PROJECT_NAME"]
172         old_project_path = self.infos["PROJECT_PATH"]
173         old_project_src_path = self.infos["PROJECT_SRC_PATH"]
174
175         self.infos["PROJECT_NAME"] = preset_project_name
176         self.infos["PROJECT_PATH"] = preset
177         self.infos["PROJECT_SRC_PATH"] = preset_prj_src_path
178
179         self.loadModuleData(True)
180         self.setEnabledModules(project_data["ENABLED_MODULES"])
181
182         self.infos["PROJECT_NAME"] = old_project_name
183         self.infos["PROJECT_PATH"] = old_project_path
184         self.infos["PROJECT_SRC_PATH"] = old_project_src_path
185         # End of the ugly HACK!
186
187         self.infos["PRESET_NAME"] = preset_project_name
188         self.infos["PRESET_PATH"] = preset
189         self.infos["PRESET_SRC_PATH"] = preset_prj_src_path
190
191     def loadProjectPresets(self):
192         """
193         Load the default presets (into the const.PREDEFINED_BOARDS_DIR).
194         """
195         # NOTE: this method does nothing (for now).
196         preset_path = os.path.join(self.infos["BERTOS_PATH"], const.PREDEFINED_BOARDS_DIR)
197         preset_tree = {}
198         if os.path.exists(preset_path):
199             preset_tree = self._loadProjectPresetTree(preset_path)
200         self.infos["PRESET_TREE"] = preset_tree
201
202     def _loadProjectPresetTree(self, path):
203         _tree = {}
204         _tree["info"] = self._loadPresetInfo(os.path.join(path, const.PREDEFINED_BOARD_SPEC_FILE))
205         _tree["info"]["filename"] = os.path.basename(path)
206         _tree["info"]["path"] = path
207         _tree["children"] = []
208         entries = set(os.listdir(path))
209         for entry in entries:
210             _path = os.path.join(path, entry)
211             if os.path.isdir(_path):
212                 sub_entries = set(os.listdir(_path))
213                 if const.PREDEFINED_BOARD_SPEC_FILE in sub_entries:
214                     _tree["children"].append(self._loadProjectPresetTree(_path))
215         # Add into the info dict the dir type (dir/project)
216         if _tree["children"]:
217             _tree["info"]["type"] = "dir"
218         else:
219             _tree["info"]["type"] = "project"
220         return _tree
221
222     def _loadPresetInfo(self, preset_spec_file):
223         D = {}
224         execfile(preset_spec_file, {}, D)
225         return D
226
227     def loadModuleData(self, edit=False):
228         module_info_dict = {}
229         list_info_dict = {}
230         configuration_info_dict = {}
231         file_dict = {}
232         for filename, path in self.findDefinitions("*.h") + self.findDefinitions("*.c") + self.findDefinitions("*.s") + self.findDefinitions("*.S"):
233             comment_list = getCommentList(open(path + "/" + filename, "r").read())
234             if len(comment_list) > 0:
235                 module_info = {}
236                 configuration_info = {}
237                 try:
238                     to_be_parsed, module_dict = loadModuleDefinition(comment_list[0])
239                 except ParseError, err:
240                     raise DefineException.ModuleDefineException(path, err.line_number, err.line)
241                 for module, information in module_dict.items():
242                     if "depends" not in information:
243                         information["depends"] = ()
244                     information["depends"] += (filename.split(".")[0],)
245                     information["category"] = os.path.basename(path)
246                     if "configuration" in information and len(information["configuration"]):
247                         configuration = module_dict[module]["configuration"]
248                         try:
249                             configuration_info[configuration] = loadConfigurationInfos(self.infos["BERTOS_PATH"] + "/" + configuration)
250                         except ParseError, err:
251                             raise DefineException.ConfigurationDefineException(self.infos["BERTOS_PATH"] + "/" + configuration, err.line_number, err.line)
252                         if edit:
253                             try:
254                                 path = self.infos["PROJECT_SRC_PATH"]
255                                 user_configuration = loadConfigurationInfos(configuration.replace("bertos", path))
256                                 configuration_info[configuration] = updateConfigurationValues(configuration_info[configuration], user_configuration)
257                             except ParseError, err:
258                                 raise DefineException.ConfigurationDefineException(configuration.replace("bertos", path))
259                 module_info_dict.update(module_dict)
260                 configuration_info_dict.update(configuration_info)
261                 if to_be_parsed:
262                     try:
263                         list_dict = loadDefineLists(comment_list[1:])
264                         list_info_dict.update(list_dict)
265                     except ParseError, err:
266                         raise DefineException.EnumDefineException(path, err.line_number, err.line)
267         for tag in self.infos["CPU_INFOS"]["CPU_TAGS"]:
268             for filename, path in self.findDefinitions("*_" + tag + ".h"):
269                 comment_list = getCommentList(open(path + "/" + filename, "r").read())
270                 list_info_dict.update(loadDefineLists(comment_list))
271         self.infos["MODULES"] = module_info_dict
272         self.infos["LISTS"] = list_info_dict
273         self.infos["CONFIGURATIONS"] = configuration_info_dict
274         self.infos["FILES"] = file_dict
275
276     def loadSourceTree(self):
277         """
278         Index BeRTOS source and load it in memory.
279         """
280         # Index only the BERTOS_PATH/bertos content
281         bertos_sources_dir = os.path.join(self.info("BERTOS_PATH"), "bertos")
282         file_dict = {}
283         if os.path.exists(bertos_sources_dir):
284             for element in os.walk(bertos_sources_dir):
285                 for f in element[2]:
286                     file_dict[f] = file_dict.get(f, []) + [element[0]]
287         self.infos["FILE_DICT"] = file_dict
288
289     def reloadCpuInfo(self):
290         for cpu_info in self.getCpuInfos():
291             if cpu_info["CPU_NAME"] == self.infos["CPU_NAME"]:
292                 self.infos["CPU_INFOS"] = cpu_info
293
294     #-------------------------------------------------------------------------#
295
296     def createBertosProject(self):
297         # NOTE: Temporary hack.
298         if self.edit:
299             self._editBertosProject()
300         else:
301             if not self.from_preset:
302                 self._newCustomBertosProject()
303             else:
304                 self._newBertosProjectFromPreset()
305
306     def _newBertosProject(self):
307         for directory in (self.maindir, self.srcdir, self.prjdir, self.cfgdir):
308             self._createDirectory(directory)
309         # Write the project file
310         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
311         # VERSION file
312         self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
313         # Destination makefile
314         self._writeMakefile()
315         # Copy the sources
316         self._copySources(self.sources_dir, self.srcdir)
317         # Set properly the autoenabled parameters
318         self._setupAutoenabledParameters()
319         # Copy all the configuration files
320         self._writeCfgFiles(self.sources_dir, self.cfgdir)
321         # Destination wizard mk file
322         self._writeWizardMkFile()
323
324     def _newCustomBertosProject(self):
325         # Create/write/copy the common things
326         self._newBertosProject()
327         # Copy the clean hw files
328         self._createDirectory(self.hwdir)
329         # Copy all the hw files
330         self._writeHwFiles(self.sources_dir, self.hwdir)
331         # Destination user mk file
332         self._writeUserMkFile()
333         # Destination main.c file
334         self._writeMainFile(self.prjdir + "/main.c")
335         # Create project files for selected plugins
336         self._createProjectFiles()
337
338     def _newBertosProjectFromPreset(self):
339         # Create/write/copy the common things
340         self._newBertosProject()
341
342         # Copy all the files and dirs except cfg/hw/*_wiz.mk
343         self._writeCustomSrcFiles()
344
345         if self.infos["EMPTY_MAIN"]:
346             # Create and empty main.c file only if the user check the box
347             self._writeMainFile(self.prjdir + "/main.c")
348
349         # Create project files for selected plugins
350         self._createProjectFiles()
351
352     def _editBertosProject(self):
353         # Write the project file
354         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
355         if not self.is_preset:
356             # Generate this files only if the project isn't a preset
357             # VERSION file
358             self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
359             # Destination makefile
360             self._writeMakefile()
361             # Merge sources
362             self._mergeSources(self.sources_dir, self.srcdir, self.old_srcdir)
363             # Copy all the hw files
364             self._writeHwFiles(self.sources_dir, self.hwdir)
365             # Destination wizard mk file
366             self._writeWizardMkFile()
367         # Set properly the autoenabled parameters
368         self._setupAutoenabledParameters()
369         # Copy all the configuration files
370         self._writeCfgFiles(self.sources_dir, self.cfgdir)
371         if not self.is_preset:
372             # Create project files for selected plugins only if the project isn't a preset
373             self._createProjectFiles()
374
375     def _createProjectFiles(self):
376         # Files for selected plugins
377         relevants_files = {}
378         for plugin in self.infos["OUTPUT"]:
379             module = loadPlugin(plugin)
380             relevants_files[plugin] = module.createProject(self)
381         self.infos["RELEVANT_FILES"] = relevants_files
382
383     def _writeVersionFile(self, filename):
384         version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
385         open(filename, "w").write(versionFileGenerator(self, version_file))
386
387     def _writeProjectFile(self, filename):
388         f = open(filename, "w")
389         f.write(projectFileGenerator(self))
390         f.close()
391
392     def _writeMakefile(self):
393         bertos_utils.makefileGenerator(self)
394
395     def _writeUserMkFile(self):
396         bertos_utils.userMkGenerator(self)
397
398     def _writeWizardMkFile(self):
399         bertos_utils.mkGenerator(self)
400
401     def _writeMainFile(self, filename):
402         main = open(os.path.join(const.DATA_DIR, "srctemplates/main.c"), "r").read()
403         open(filename, "w").write(main)
404
405     def _writeHwFiles(self, source_dir, destination_dir):
406         for module, information in self.infos["MODULES"].items():
407             for hwfile in information["hw"]:
408                 string = open(source_dir + "/" + hwfile, "r").read()
409                 hwfile_path = destination_dir + "/" + os.path.basename(hwfile)
410                 if not self.edit or not os.path.exists(hwfile_path):
411                     # If not in editing mode it copies all the hw files. If in
412                     # editing mode it copies only the files that don't exist yet
413                     open(os.path.join(destination_dir,os.path.basename(hwfile)), "w").write(string)
414
415     def _writeCfgFiles(self, source_dir, destination_dir):
416         for configuration, information in self.infos["CONFIGURATIONS"].items():
417             string = open(source_dir + "/" + configuration, "r").read()
418             for start, parameter in information["paramlist"]:
419                 infos = information[parameter]
420                 value = infos["value"]
421                 if "unsigned" in infos["informations"] and infos["informations"]["unsigned"]:
422                     value += "U"
423                 if "long" in infos["informations"] and infos["informations"]["long"]:
424                     value += "L"
425                 string = sub(string, parameter, value)
426             f = open(os.path.join(destination_dir, os.path.basename(configuration)), "w")
427             f.write(string)
428             f.close()
429
430     def _writeCustomSrcFiles(self):
431         origin = self.infos["PRESET_SRC_PATH"]
432         # Files to be ignored (all project files, cfg dir, wizard mk file, all global ignored dirs)
433         project_related_stuff = (
434             "cfg",
435             self.infos["PRESET_NAME"] + "_wiz.mk",
436             "project.bertos",
437             self.infos["PRESET_NAME"] + ".project",
438             self.infos["PRESET_NAME"] + ".workspace",
439         ) + const.IGNORE_LIST
440         for element in os.listdir(origin):
441             if element not in project_related_stuff:
442                 full_path = os.path.join(origin, element)
443                 if os.path.isdir(full_path):
444                     copytree.copytree(full_path, os.path.join(self.prjdir, element), ignore_list=const.IGNORE_LIST)
445                 else:
446                     shutil.copy(full_path, self.prjdir)
447
448     def _setupAutoenabledParameters(self):
449         for module, information in self.infos["MODULES"].items():
450             if "configuration" in information and information["configuration"] != "":
451                 configurations = self.infos["CONFIGURATIONS"]
452                 configuration = configurations[information["configuration"]]
453                 for start, parameter in configuration["paramlist"]:
454                     if "type" in configuration[parameter]["informations"] and configuration[parameter]["informations"]["type"] == "autoenabled":
455                         configuration[parameter]["value"] = "1" if information["enabled"] else "0"
456                 self.infos["CONFIGURATIONS"] = configurations
457
458     @property
459     def maindir(self):
460         return self.infos.get("PROJECT_PATH", None)
461
462     @property
463     def srcdir(self):
464         if self.maindir:
465             return os.path.join(self.maindir, "bertos")
466         else:
467             return None
468
469     @property
470     def prjdir(self):
471         return self.infos.get("PROJECT_SRC_PATH", None)
472
473     @property
474     def hwdir(self):
475         if self.prjdir:
476             return os.path.join(self.prjdir, "hw")
477         else:
478             return None
479
480     @property
481     def cfgdir(self):
482         if self.prjdir:
483             return os.path.join(self.prjdir, "cfg")
484         else:
485             return None
486
487     @property
488     def old_srcdir(self):
489         return self.infos.get("OLD_BERTOS_PATH", None)
490
491     @property
492     def sources_dir(self):
493         return self.infos.get("BERTOS_PATH", None)
494
495     @property
496     def from_preset(self):
497         return self.infos.get("PROJECT_FROM_PRESET", False)
498
499     @property
500     def is_preset(self):
501         return self.infos.get("PRESET", False)
502
503     def _createDirectory(self, directory):
504         if not directory:
505             return
506         if os.path.isdir(directory):
507             shutil.rmtree(directory, True)
508         os.makedirs(directory)
509
510     def _copySources(self, origin, destination):
511         # If not in editing mode it copies all the bertos sources in the /bertos subdirectory of the project
512         shutil.rmtree(destination, True)
513         copytree.copytree(origin + "/bertos", destination, ignore_list=const.IGNORE_LIST)
514
515     def _mergeSources(self, origin, destination, old_sources_dir):
516         if old_sources_dir:
517             mergeSources(destination, origin, old_sources_dir)
518
519     def setInfo(self, key, value):
520         """
521         Store the given value with the name key.
522         """
523         self.infos[key] = value
524
525     def info(self, key, default=None):
526         """
527         Retrieve the value associated with the name key.
528         """
529         if key in self.infos:
530             return copy.deepcopy(self.infos[key])
531         return default
532
533     def getCpuInfos(self):
534         cpuInfos = []
535         for definition in self.findDefinitions(const.CPU_DEFINITION):
536             cpuInfos.append(getInfos(definition))
537         return cpuInfos
538
539     def searchFiles(self, filename):
540         file_dict = self.infos["FILE_DICT"]
541         return [(filename, dirname) for dirname in file_dict.get(filename, [])]
542
543     def findDefinitions(self, ftype):
544         # Maintain a cache for every scanned BERTOS_PATH
545         definitions_dict = self._cached_queries.get(self.infos["BERTOS_PATH"], {})
546         definitions = definitions_dict.get(ftype, None)
547         if definitions is not None:
548             return definitions
549         file_dict = self.infos["FILE_DICT"]
550         definitions = []
551         for filename in file_dict:
552             if fnmatch.fnmatch(filename, ftype):
553                 definitions += [(filename, dirname) for dirname in file_dict.get(filename, [])]
554
555         # If no cache for the current BERTOS_PATH create an empty one
556         if not definitions_dict:
557             self._cached_queries[self.infos["BERTOS_PATH"]] = {}
558         # Fill the empty cache with the result
559         self._cached_queries[self.infos["BERTOS_PATH"]][ftype] = definitions
560         return definitions
561
562     def setEnabledModules(self, enabled_modules):
563         modules = self.infos["MODULES"]
564         files = {}
565         for module, information in modules.items():
566             information["enabled"] = module in enabled_modules
567             if information["enabled"]:
568                 for dependency in information["depends"]:
569                     if not dependency in modules:
570                         files[dependency] = files.get(dependency, 0) + 1
571         self.infos["MODULES"] = modules
572         self.infos["FILES"] = files
573
574     def __repr__(self):
575         return "<BProject:instance %d>%s" %(id(self), repr(self.infos))