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