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