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