Reuse most of the available code.
[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         # Check for the Wizard version
87         wizard_version = project_data.get("WIZARD_VERSION", 0)
88         if wizard_version < 1:
89             # Ignore the SOURCES_PATH inside the project file for older projects
90             project_data["SOURCES_PATH"] = project_dir
91         self._loadBertosSourceStuff(project_data["SOURCES_PATH"], info_dict.get("SOURCES_PATH", None))
92
93         # For those projects that don't have a VERSION file create a dummy one.
94         if not isBertosDir(project_dir):
95             version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
96             open(os.path.join(project_dir, "VERSION"), "w").write(version_file.replace("$version", "").strip())
97
98         self.loadSourceTree()
99         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
100         self._loadToolchainStuff(project_data["TOOLCHAIN"], info_dict.get("TOOLCHAIN", None))
101         self.infos["OUTPUT"] = project_data["OUTPUT"]
102         self.loadModuleData(True)
103         self.setEnabledModules(project_data["ENABLED_MODULES"])
104
105     def _loadBertosSourceStuff(self, sources_path, forced_version=None):
106         if forced_version:
107             sources_path = forced_version
108         if os.path.exists(sources_path):
109             self.infos["SOURCES_PATH"] = sources_path
110         else:
111             raise VersionException(self)
112
113     def _loadCpuStuff(self, cpu_name, cpu_frequency):
114         self.infos["CPU_NAME"] = cpu_name
115         cpu_info = self.getCpuInfos()
116         for cpu in cpu_info:
117             if cpu["CPU_NAME"] == cpu_name:
118                 self.infos["CPU_INFOS"] = cpu
119                 break
120         tag_list = getTagSet(cpu_info)
121         # Create, fill and store the dict with the tags
122         tag_dict = {}
123         for element in tag_list:
124             tag_dict[element] = False
125         infos = self.info("CPU_INFOS")
126         for tag in tag_dict:
127             if tag in infos["CPU_TAGS"] + [infos["CPU_NAME"], infos["TOOLCHAIN"]]:
128                 tag_dict[tag] = True
129             else:
130                 tag_dict[tag] = False
131         self.infos["ALL_CPU_TAGS"] = tag_dict
132         self.infos["SELECTED_FREQ"] = cpu_frequency
133
134     def _loadToolchainStuff(self, toolchain, forced_toolchain=None):
135         toolchain = toolchain
136         if forced_toolchain:
137             toolchain = forced_toolchain
138         if os.path.exists(toolchain["path"]):
139             self.infos["TOOLCHAIN"] = toolchain
140         else:
141             raise ToolchainException(self)
142
143     def loadProjectFromPreset(self, preset):
144         """
145         Load a project from a preset.
146         NOTE: this is a stub.
147         """
148         project_file = os.path.join(preset, "project.bertos")
149         project_data = pickle.loads(open(project_file, "r").read())
150         self.loadSourceTree()
151         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
152         self._loadToolchainStuff(project_data["TOOLCHAIN"])
153         # NOTE: this is a HACK!!!
154         # TODO: find a better way to reuse loadModuleData
155         old_project_name = self.infos["PROJECT_NAME"]
156         old_project_path = self.infos["PROJECT_PATH"]
157         self.infos["PROJECT_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(preset))
158         self.infos["PROJECT_PATH"] = preset
159         self.loadModuleData(True)
160         self.setEnabledModules(project_data["ENABLED_MODULES"])
161         self.infos["PROJECT_NAME"] = old_project_name
162         self.infos["PROJECT_PATH"] = old_project_path        
163
164     def loadProjectPresets(self):
165         """
166         Load the default presets (into the const.PREDEFINED_BOARDS_DIR).
167         """
168         # NOTE: this method does nothing (for now).
169         preset_path = os.path.join(self.infos["SOURCES_PATH"], const.PREDEFINED_BOARDS_DIR)
170         preset_tree = {}
171         if os.path.exists(preset_path):
172             preset_tree = self._loadProjectPresetTree(preset_path)
173         self.infos["PRESET_TREE"] = preset_tree
174
175     def _loadProjectPresetTree(self, path):
176         _tree = {}
177         _tree['info'] = self._loadPresetInfo(os.path.join(path, const.PREDEFINED_BOARD_SPEC_FILE))
178         _tree['info']['filename'] = os.path.basename(path)
179         _tree['info']['path'] = path
180         _tree['children'] = []
181         entries = set(os.listdir(path))
182         for entry in entries:
183             _path = os.path.join(path, entry)
184             if os.path.isdir(_path):
185                 sub_entries = set(os.listdir(_path))
186                 if const.PREDEFINED_BOARD_SPEC_FILE in sub_entries:
187                     _tree['children'].append(self._loadProjectPresetTree(_path))
188         # Add into the info dict the dir type (dir/project)
189         if _tree['children']:
190             _tree['info']['type'] = 'dir'
191         else:
192             _tree['info']['type'] = 'project'
193         return _tree
194
195     def _loadPresetInfo(self, preset_spec_file):
196         D = {}
197         execfile(preset_spec_file, {}, D)
198         return D
199
200     def loadModuleData(self, edit=False):
201         module_info_dict = {}
202         list_info_dict = {}
203         configuration_info_dict = {}
204         file_dict = {}
205         for filename, path in self.findDefinitions("*.h") + self.findDefinitions("*.c") + self.findDefinitions("*.s") + self.findDefinitions("*.S"):
206             comment_list = getCommentList(open(path + "/" + filename, "r").read())
207             if len(comment_list) > 0:
208                 module_info = {}
209                 configuration_info = {}
210                 try:
211                     to_be_parsed, module_dict = loadModuleDefinition(comment_list[0])
212                 except ParseError, err:
213                     raise DefineException.ModuleDefineException(path, err.line_number, err.line)
214                 for module, information in module_dict.items():
215                     if "depends" not in information:
216                         information["depends"] = ()
217                     information["depends"] += (filename.split(".")[0],)
218                     information["category"] = os.path.basename(path)
219                     if "configuration" in information and len(information["configuration"]):
220                         configuration = module_dict[module]["configuration"]
221                         try:
222                             configuration_info[configuration] = loadConfigurationInfos(self.infos["SOURCES_PATH"] + "/" + configuration)
223                         except ParseError, err:
224                             raise DefineException.ConfigurationDefineException(self.infos["SOURCES_PATH"] + "/" + configuration, err.line_number, err.line)
225                         if edit:
226                             try:
227                                 path = self.infos["PROJECT_NAME"]
228                                 directory = self.infos["PROJECT_PATH"]
229                                 user_configuration = loadConfigurationInfos(directory + "/" + configuration.replace("bertos", path))
230                                 configuration_info[configuration] = updateConfigurationValues(configuration_info[configuration], user_configuration)
231                             except ParseError, err:
232                                 raise DefineException.ConfigurationDefineException(directory + "/" + configuration.replace("bertos", path))
233                 module_info_dict.update(module_dict)
234                 configuration_info_dict.update(configuration_info)
235                 if to_be_parsed:
236                     try:
237                         list_dict = loadDefineLists(comment_list[1:])
238                         list_info_dict.update(list_dict)
239                     except ParseError, err:
240                         raise DefineException.EnumDefineException(path, err.line_number, err.line)
241         for tag in self.infos["CPU_INFOS"]["CPU_TAGS"]:
242             for filename, path in self.findDefinitions("*_" + tag + ".h"):
243                 comment_list = getCommentList(open(path + "/" + filename, "r").read())
244                 list_info_dict.update(loadDefineLists(comment_list))
245         self.infos["MODULES"] = module_info_dict
246         self.infos["LISTS"] = list_info_dict
247         self.infos["CONFIGURATIONS"] = configuration_info_dict
248         self.infos["FILES"] = file_dict
249
250     def loadSourceTree(self):
251         """
252         Index BeRTOS source and load it in memory.
253         """
254         # Index only the SOURCES_PATH/bertos content
255         bertos_sources_dir = os.path.join(self.info("SOURCES_PATH"), 'bertos')
256         file_dict = {}
257         if os.path.exists(bertos_sources_dir):
258             for element in os.walk(bertos_sources_dir):
259                 for f in element[2]:
260                     file_dict[f] = file_dict.get(f, []) + [element[0]]
261         self.infos["FILE_DICT"] = file_dict
262
263     def reloadCpuInfo(self):
264         for cpu_info in self.getCpuInfos():
265             if cpu_info["CPU_NAME"] == self.infos["CPU_NAME"]:
266                 self.infos["CPU_INFOS"] = cpu_info
267
268     #-------------------------------------------------------------------------#
269
270     def createBertosProject(self):
271         # NOTE: Temporary hack.
272         if self.edit:
273             self._editBertosProject()
274         else:
275             self._newBertosProject()
276     
277     def _newBertosProject(self):
278         maindir, srcdir, prjdir, hwdir, cfgdir, sources_dir, old_srcdir = self._projectDirectories()
279         for directory in (maindir, srcdir, prjdir, hwdir, cfgdir):
280             self._createDirectory(directory)
281         # Write the project file
282         self._writeProjectFile(maindir + "/project.bertos")
283         # VERSION file
284         self._writeVersionFile(maindir + "/VERSION")
285         # Destination makefile
286         self._writeMakefile(maindir + "/Makefile")
287         # Copy the sources
288         self._copySources(sources_dir, srcdir)
289         # Copy all the hw files
290         self._writeHwFiles(sources_dir, hwdir)
291         # Set properly the autoenabled parameters
292         self._setupAutoenabledParameters()
293         # Copy all the configuration files
294         self._writeCfgFiles(sources_dir, cfgdir)
295         if not self.edit:
296             # Destination user mk file (only on project creation)
297             self._writeUserMkFile(os.path.join(prjdir, os.path.basename(prjdir) + ".mk"))
298         # Destination wizard mk file
299         self._writeWizardMkFile(prjdir + "/" + os.path.basename(prjdir) + "_wiz.mk")
300         # Destination main.c file
301         if not self.edit:
302             self._writeMainFile(prjdir + "/main.c")
303         # Files for selected plugins
304         relevants_files = {}
305         for plugin in self.infos["OUTPUT"]:
306             module = loadPlugin(plugin)
307             relevants_files[plugin] = module.createProject(self)
308         self.infos["RELEVANT_FILES"] = relevants_files
309
310     def _editBertosProject(self):
311         maindir, srcdir, prjdir, hwdir, cfgdir, sources_dir, old_srcdir = self._projectDirectories()
312         # Write the project file
313         self._writeProjectFile(maindir + "/project.bertos")
314         # VERSION file
315         self._writeVersionFile(maindir + "/VERSION")
316         # Destination makefile
317         self._writeMakefile(maindir + "/Makefile")
318         # Merge sources
319         self._mergeSources(sources_dir, srcdir, old_srcdir)
320         # Copy all the hw files
321         self._writeHwFiles(sources_dir, hwdir)
322         # Set properly the autoenabled parameters
323         self._setupAutoenabledParameters()
324         # Copy all the configuration files
325         self._writeCfgFiles(sources_dir, cfgdir)
326         # Destination wizard mk file
327         self._writeWizardMkFile(prjdir + "/" + os.path.basename(prjdir) + "_wiz.mk")
328         # Files for selected plugins
329         relevants_files = {}
330         for plugin in self.infos["OUTPUT"]:
331             module = loadPlugin(plugin)
332             relevants_files[plugin] = module.createProject(self)
333         self.infos["RELEVANT_FILES"] = relevants_files
334
335     def _writeVersionFile(self, filename):
336         version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
337         open(filename, "w").write(versionFileGenerator(self, version_file))
338
339     def _writeProjectFile(self, filename):
340         f = open(filename, "w")
341         f.write(projectFileGenerator(self))
342         f.close()
343
344     def _writeMakefile(self, filename):
345         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/Makefile"), 'r').read()
346         makefile = makefileGenerator(self, makefile)
347         open(filename, "w").write(makefile)
348
349     def _writeUserMkFile(self, filename):
350         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template.mk"), "r").read()
351         # Deadly performances loss was here :(
352         makefile = userMkGenerator(self, makefile)
353         open(filename, "w").write(makefile)
354
355     def _writeWizardMkFile(self, filename):
356         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template_wiz.mk"), "r").read()
357         makefile = mkGenerator(self, makefile)
358         open(filename, "w").write(makefile)
359
360     def _writeMainFile(self, filename):
361         main = open(os.path.join(const.DATA_DIR, "srctemplates/main.c"), "r").read()
362         open(filename, "w").write(main)
363
364     def _writeHwFiles(self, source_dir, destination_dir):
365         for module, information in self.infos["MODULES"].items():
366             for hwfile in information["hw"]:
367                 string = open(source_dir + "/" + hwfile, "r").read()
368                 hwfile_path = destination_dir + "/" + os.path.basename(hwfile)
369                 if not self.edit or not os.path.exists(hwfile_path):
370                     # If not in editing mode it copies all the hw files. If in
371                     # editing mode it copies only the files that don't exist yet
372                     open(destination_dir + "/" + os.path.basename(hwfile), "w").write(string)
373
374     def _writeCfgFiles(self, source_dir, destination_dir):
375         for configuration, information in self.infos["CONFIGURATIONS"].items():
376             string = open(source_dir + "/" + configuration, "r").read()
377             for start, parameter in information["paramlist"]:
378                 infos = information[parameter]
379                 value = infos["value"]
380                 if "unsigned" in infos["informations"] and infos["informations"]["unsigned"]:
381                     value += "U"
382                 if "long" in infos["informations"] and infos["informations"]["long"]:
383                     value += "L"
384                 string = sub(string, parameter, value)
385             f = open(destination_dir + "/" + os.path.basename(configuration), "w")
386             f.write(string)
387             f.close()
388
389     def _setupAutoenabledParameters(self):
390         for module, information in self.infos["MODULES"].items():
391             if "configuration" in information and information["configuration"] != "":
392                 configurations = self.infos["CONFIGURATIONS"]
393                 configuration = configurations[information["configuration"]]
394                 for start, parameter in configuration["paramlist"]:
395                     if "type" in configuration[parameter]["informations"] and configuration[parameter]["informations"]["type"] == "autoenabled":
396                         configuration[parameter]["value"] = "1" if information["enabled"] else "0"
397                 self.infos["CONFIGURATIONS"] = configurations
398
399     def _projectDirectories(self):
400         maindir = self.infos["PROJECT_PATH"]
401         srcdir =  maindir + "/bertos"
402         prjdir = os.path.join(maindir, self.infos["PROJECT_NAME"])
403         hwdir = prjdir + "/hw"
404         cfgdir = prjdir + "/cfg"
405         old_srcdir = self.infos.get("OLD_SOURCES_PATH", None)
406         sources_dir = self.infos["SOURCES_PATH"]
407         return maindir, srcdir, prjdir, hwdir, cfgdir, sources_dir, old_srcdir
408
409     def _createDirectory(self, directory):
410         if not directory:
411             return
412         if os.path.isdir(directory):
413             shutil.rmtree(directory, True)        
414         os.makedirs(directory)
415
416     def _copySources(self, origin, destination):
417         # If not in editing mode it copies all the bertos sources in the /bertos subdirectory of the project
418         shutil.rmtree(destination, True)        
419         copytree.copytree(origin + "/bertos", destination, ignore_list=const.IGNORE_LIST)
420
421     def _mergeSources(self, origin, destination, old_sources_dir):
422         if old_sources_dir:
423             mergeSources(destination, origin, old_sources_dir)
424
425     def setInfo(self, key, value):
426         """
427         Store the given value with the name key.
428         """
429         self.infos[key] = value
430
431     def info(self, key, default=None):
432         """
433         Retrieve the value associated with the name key.
434         """
435         if key in self.infos:
436             return copy.deepcopy(self.infos[key])
437         return default
438
439     def getCpuInfos(self):
440         cpuInfos = []
441         for definition in self.findDefinitions(const.CPU_DEFINITION):
442             cpuInfos.append(getInfos(definition))
443         return cpuInfos
444
445     def searchFiles(self, filename):
446         file_dict = self.infos["FILE_DICT"]
447         return [(filename, dirname) for dirname in file_dict.get(filename, [])]
448
449     def findDefinitions(self, ftype):
450         # Maintain a cache for every scanned SOURCES_PATH
451         definitions_dict = self._cached_queries.get(self.infos["SOURCES_PATH"], {})
452         definitions = definitions_dict.get(ftype, None)
453         if definitions is not None:
454             return definitions
455         file_dict = self.infos["FILE_DICT"]
456         definitions = []
457         for filename in file_dict:
458             if fnmatch.fnmatch(filename, ftype):
459                 definitions += [(filename, dirname) for dirname in file_dict.get(filename, [])]
460
461         # If no cache for the current SOURCES_PATH create an empty one
462         if not definitions_dict:
463             self._cached_queries[self.infos["SOURCES_PATH"]] = {}
464         # Fill the empty cache with the result
465         self._cached_queries[self.infos["SOURCES_PATH"]][ftype] = definitions
466         return definitions
467
468     def setEnabledModules(self, enabled_modules):
469         modules = self.infos["MODULES"]
470         files = {}
471         for module, information in modules.items():
472             information["enabled"] = module in enabled_modules
473             if information["enabled"]:
474                 for dependency in information["depends"]:
475                     if not dependency in modules:
476                         files[dependency] = files.get(dependency, 0) + 1
477         self.infos["MODULES"] = modules
478         self.infos["FILES"] = files
479
480     def __repr__(self):
481         return repr(self.infos)