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