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