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