Remove not needed edit checks.
[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         # Destination user mk file (only on project creation)
296         self._writeUserMkFile(os.path.join(prjdir, os.path.basename(prjdir) + ".mk"))
297         # Destination wizard mk file
298         self._writeWizardMkFile(prjdir + "/" + os.path.basename(prjdir) + "_wiz.mk")
299         # Destination main.c file
300         self._writeMainFile(prjdir + "/main.c")
301         # Files for selected plugins
302         relevants_files = {}
303         for plugin in self.infos["OUTPUT"]:
304             module = loadPlugin(plugin)
305             relevants_files[plugin] = module.createProject(self)
306         self.infos["RELEVANT_FILES"] = relevants_files
307
308     def _editBertosProject(self):
309         maindir, srcdir, prjdir, hwdir, cfgdir, sources_dir, old_srcdir = self._projectDirectories()
310         # Write the project file
311         self._writeProjectFile(maindir + "/project.bertos")
312         # VERSION file
313         self._writeVersionFile(maindir + "/VERSION")
314         # Destination makefile
315         self._writeMakefile(maindir + "/Makefile")
316         # Merge sources
317         self._mergeSources(sources_dir, srcdir, old_srcdir)
318         # Copy all the hw files
319         self._writeHwFiles(sources_dir, hwdir)
320         # Set properly the autoenabled parameters
321         self._setupAutoenabledParameters()
322         # Copy all the configuration files
323         self._writeCfgFiles(sources_dir, cfgdir)
324         # Destination wizard mk file
325         self._writeWizardMkFile(prjdir + "/" + os.path.basename(prjdir) + "_wiz.mk")
326         # Files for selected plugins
327         relevants_files = {}
328         for plugin in self.infos["OUTPUT"]:
329             module = loadPlugin(plugin)
330             relevants_files[plugin] = module.createProject(self)
331         self.infos["RELEVANT_FILES"] = relevants_files
332
333     def _writeVersionFile(self, filename):
334         version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
335         open(filename, "w").write(versionFileGenerator(self, version_file))
336
337     def _writeProjectFile(self, filename):
338         f = open(filename, "w")
339         f.write(projectFileGenerator(self))
340         f.close()
341
342     def _writeMakefile(self, filename):
343         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/Makefile"), 'r').read()
344         makefile = makefileGenerator(self, makefile)
345         open(filename, "w").write(makefile)
346
347     def _writeUserMkFile(self, filename):
348         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template.mk"), "r").read()
349         # Deadly performances loss was here :(
350         makefile = userMkGenerator(self, makefile)
351         open(filename, "w").write(makefile)
352
353     def _writeWizardMkFile(self, filename):
354         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template_wiz.mk"), "r").read()
355         makefile = mkGenerator(self, makefile)
356         open(filename, "w").write(makefile)
357
358     def _writeMainFile(self, filename):
359         main = open(os.path.join(const.DATA_DIR, "srctemplates/main.c"), "r").read()
360         open(filename, "w").write(main)
361
362     def _writeHwFiles(self, source_dir, destination_dir):
363         for module, information in self.infos["MODULES"].items():
364             for hwfile in information["hw"]:
365                 string = open(source_dir + "/" + hwfile, "r").read()
366                 hwfile_path = destination_dir + "/" + os.path.basename(hwfile)
367                 if not self.edit or not os.path.exists(hwfile_path):
368                     # If not in editing mode it copies all the hw files. If in
369                     # editing mode it copies only the files that don't exist yet
370                     open(destination_dir + "/" + os.path.basename(hwfile), "w").write(string)
371
372     def _writeCfgFiles(self, source_dir, destination_dir):
373         for configuration, information in self.infos["CONFIGURATIONS"].items():
374             string = open(source_dir + "/" + configuration, "r").read()
375             for start, parameter in information["paramlist"]:
376                 infos = information[parameter]
377                 value = infos["value"]
378                 if "unsigned" in infos["informations"] and infos["informations"]["unsigned"]:
379                     value += "U"
380                 if "long" in infos["informations"] and infos["informations"]["long"]:
381                     value += "L"
382                 string = sub(string, parameter, value)
383             f = open(destination_dir + "/" + os.path.basename(configuration), "w")
384             f.write(string)
385             f.close()
386
387     def _setupAutoenabledParameters(self):
388         for module, information in self.infos["MODULES"].items():
389             if "configuration" in information and information["configuration"] != "":
390                 configurations = self.infos["CONFIGURATIONS"]
391                 configuration = configurations[information["configuration"]]
392                 for start, parameter in configuration["paramlist"]:
393                     if "type" in configuration[parameter]["informations"] and configuration[parameter]["informations"]["type"] == "autoenabled":
394                         configuration[parameter]["value"] = "1" if information["enabled"] else "0"
395                 self.infos["CONFIGURATIONS"] = configurations
396
397     def _projectDirectories(self):
398         maindir = self.infos["PROJECT_PATH"]
399         srcdir =  maindir + "/bertos"
400         prjdir = os.path.join(maindir, self.infos["PROJECT_NAME"])
401         hwdir = prjdir + "/hw"
402         cfgdir = prjdir + "/cfg"
403         old_srcdir = self.infos.get("OLD_SOURCES_PATH", None)
404         sources_dir = self.infos["SOURCES_PATH"]
405         return maindir, srcdir, prjdir, hwdir, cfgdir, sources_dir, old_srcdir
406
407     def _createDirectory(self, directory):
408         if not directory:
409             return
410         if os.path.isdir(directory):
411             shutil.rmtree(directory, True)        
412         os.makedirs(directory)
413
414     def _copySources(self, origin, destination):
415         # If not in editing mode it copies all the bertos sources in the /bertos subdirectory of the project
416         shutil.rmtree(destination, True)        
417         copytree.copytree(origin + "/bertos", destination, ignore_list=const.IGNORE_LIST)
418
419     def _mergeSources(self, origin, destination, old_sources_dir):
420         if old_sources_dir:
421             mergeSources(destination, origin, old_sources_dir)
422
423     def setInfo(self, key, value):
424         """
425         Store the given value with the name key.
426         """
427         self.infos[key] = value
428
429     def info(self, key, default=None):
430         """
431         Retrieve the value associated with the name key.
432         """
433         if key in self.infos:
434             return copy.deepcopy(self.infos[key])
435         return default
436
437     def getCpuInfos(self):
438         cpuInfos = []
439         for definition in self.findDefinitions(const.CPU_DEFINITION):
440             cpuInfos.append(getInfos(definition))
441         return cpuInfos
442
443     def searchFiles(self, filename):
444         file_dict = self.infos["FILE_DICT"]
445         return [(filename, dirname) for dirname in file_dict.get(filename, [])]
446
447     def findDefinitions(self, ftype):
448         # Maintain a cache for every scanned SOURCES_PATH
449         definitions_dict = self._cached_queries.get(self.infos["SOURCES_PATH"], {})
450         definitions = definitions_dict.get(ftype, None)
451         if definitions is not None:
452             return definitions
453         file_dict = self.infos["FILE_DICT"]
454         definitions = []
455         for filename in file_dict:
456             if fnmatch.fnmatch(filename, ftype):
457                 definitions += [(filename, dirname) for dirname in file_dict.get(filename, [])]
458
459         # If no cache for the current SOURCES_PATH create an empty one
460         if not definitions_dict:
461             self._cached_queries[self.infos["SOURCES_PATH"]] = {}
462         # Fill the empty cache with the result
463         self._cached_queries[self.infos["SOURCES_PATH"]][ftype] = definitions
464         return definitions
465
466     def setEnabledModules(self, enabled_modules):
467         modules = self.infos["MODULES"]
468         files = {}
469         for module, information in modules.items():
470             information["enabled"] = module in enabled_modules
471             if information["enabled"]:
472                 for dependency in information["depends"]:
473                     if not dependency in modules:
474                         files[dependency] = files.get(dependency, 0) + 1
475         self.infos["MODULES"] = modules
476         self.infos["FILES"] = files
477
478     def __repr__(self):
479         return repr(self.infos)