b5617ee2132c553c08121357b747f6d03369c969
[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         self.infos["PRESET_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(preset))
164         self.infos["PRESET_PATH"] = preset
165
166     def loadProjectPresets(self):
167         """
168         Load the default presets (into the const.PREDEFINED_BOARDS_DIR).
169         """
170         # NOTE: this method does nothing (for now).
171         preset_path = os.path.join(self.infos["SOURCES_PATH"], const.PREDEFINED_BOARDS_DIR)
172         preset_tree = {}
173         if os.path.exists(preset_path):
174             preset_tree = self._loadProjectPresetTree(preset_path)
175         self.infos["PRESET_TREE"] = preset_tree
176
177     def _loadProjectPresetTree(self, path):
178         _tree = {}
179         _tree['info'] = self._loadPresetInfo(os.path.join(path, const.PREDEFINED_BOARD_SPEC_FILE))
180         _tree['info']['filename'] = os.path.basename(path)
181         _tree['info']['path'] = path
182         _tree['children'] = []
183         entries = set(os.listdir(path))
184         for entry in entries:
185             _path = os.path.join(path, entry)
186             if os.path.isdir(_path):
187                 sub_entries = set(os.listdir(_path))
188                 if const.PREDEFINED_BOARD_SPEC_FILE in sub_entries:
189                     _tree['children'].append(self._loadProjectPresetTree(_path))
190         # Add into the info dict the dir type (dir/project)
191         if _tree['children']:
192             _tree['info']['type'] = 'dir'
193         else:
194             _tree['info']['type'] = 'project'
195         return _tree
196
197     def _loadPresetInfo(self, preset_spec_file):
198         D = {}
199         execfile(preset_spec_file, {}, D)
200         return D
201
202     def loadModuleData(self, edit=False):
203         module_info_dict = {}
204         list_info_dict = {}
205         configuration_info_dict = {}
206         file_dict = {}
207         for filename, path in self.findDefinitions("*.h") + self.findDefinitions("*.c") + self.findDefinitions("*.s") + self.findDefinitions("*.S"):
208             comment_list = getCommentList(open(path + "/" + filename, "r").read())
209             if len(comment_list) > 0:
210                 module_info = {}
211                 configuration_info = {}
212                 try:
213                     to_be_parsed, module_dict = loadModuleDefinition(comment_list[0])
214                 except ParseError, err:
215                     raise DefineException.ModuleDefineException(path, err.line_number, err.line)
216                 for module, information in module_dict.items():
217                     if "depends" not in information:
218                         information["depends"] = ()
219                     information["depends"] += (filename.split(".")[0],)
220                     information["category"] = os.path.basename(path)
221                     if "configuration" in information and len(information["configuration"]):
222                         configuration = module_dict[module]["configuration"]
223                         try:
224                             configuration_info[configuration] = loadConfigurationInfos(self.infos["SOURCES_PATH"] + "/" + configuration)
225                         except ParseError, err:
226                             raise DefineException.ConfigurationDefineException(self.infos["SOURCES_PATH"] + "/" + configuration, err.line_number, err.line)
227                         if edit:
228                             try:
229                                 path = self.infos["PROJECT_NAME"]
230                                 directory = self.infos["PROJECT_PATH"]
231                                 user_configuration = loadConfigurationInfos(directory + "/" + configuration.replace("bertos", path))
232                                 configuration_info[configuration] = updateConfigurationValues(configuration_info[configuration], user_configuration)
233                             except ParseError, err:
234                                 raise DefineException.ConfigurationDefineException(directory + "/" + configuration.replace("bertos", path))
235                 module_info_dict.update(module_dict)
236                 configuration_info_dict.update(configuration_info)
237                 if to_be_parsed:
238                     try:
239                         list_dict = loadDefineLists(comment_list[1:])
240                         list_info_dict.update(list_dict)
241                     except ParseError, err:
242                         raise DefineException.EnumDefineException(path, err.line_number, err.line)
243         for tag in self.infos["CPU_INFOS"]["CPU_TAGS"]:
244             for filename, path in self.findDefinitions("*_" + tag + ".h"):
245                 comment_list = getCommentList(open(path + "/" + filename, "r").read())
246                 list_info_dict.update(loadDefineLists(comment_list))
247         self.infos["MODULES"] = module_info_dict
248         self.infos["LISTS"] = list_info_dict
249         self.infos["CONFIGURATIONS"] = configuration_info_dict
250         self.infos["FILES"] = file_dict
251
252     def loadSourceTree(self):
253         """
254         Index BeRTOS source and load it in memory.
255         """
256         # Index only the SOURCES_PATH/bertos content
257         bertos_sources_dir = os.path.join(self.info("SOURCES_PATH"), 'bertos')
258         file_dict = {}
259         if os.path.exists(bertos_sources_dir):
260             for element in os.walk(bertos_sources_dir):
261                 for f in element[2]:
262                     file_dict[f] = file_dict.get(f, []) + [element[0]]
263         self.infos["FILE_DICT"] = file_dict
264
265     def reloadCpuInfo(self):
266         for cpu_info in self.getCpuInfos():
267             if cpu_info["CPU_NAME"] == self.infos["CPU_NAME"]:
268                 self.infos["CPU_INFOS"] = cpu_info
269
270     #-------------------------------------------------------------------------#
271
272     def createBertosProject(self):
273         # NOTE: Temporary hack.
274         if self.edit:
275             self._editBertosProject()
276         else:
277             if not self.from_preset:
278                 self._newCustomBertosProject()
279             else:
280                 self._newBertosProjectFromPreset()
281     
282     def _newBertosProject(self):
283         for directory in (self.maindir, self.srcdir, self.prjdir, self.hwdir, self.cfgdir):
284             self._createDirectory(directory)
285         # Write the project file
286         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
287         # VERSION file
288         self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
289         # Destination makefile
290         self._writeMakefile(os.path.join(self.maindir, "Makefile"))
291         # Copy the sources
292         self._copySources(self.sources_dir, self.srcdir)
293         # Copy all the hw files
294         self._writeHwFiles(self.sources_dir, self.hwdir)
295         # Set properly the autoenabled parameters
296         self._setupAutoenabledParameters()
297         # Copy all the configuration files
298         self._writeCfgFiles(self.sources_dir, self.cfgdir)
299         # Destination wizard mk file
300         self._writeWizardMkFile(os.path.join(self.prjdir, os.path.basename(self.prjdir) + "_wiz.mk"))
301
302     def _newCustomBertosProject(self):
303         # Create/write/copy the common things
304         self._newBertosProject()
305         # Destination user mk file
306         self._writeUserMkFile(os.path.join(self.prjdir, os.path.basename(self.prjdir) + ".mk"))
307         # Destination main.c file
308         self._writeMainFile(self.prjdir + "/main.c")
309         # Create project files for selected plugins
310         self._createProjectFiles()
311
312     def _newBertosProjectFromPreset(self):
313         # Create/write/copy the common things
314         self._newBertosProject()
315
316         # Copy all the files and dirs except cfg/hw/*_wiz.mk
317         self._writeCustomSrcFiles()
318         # Override the main.c with the empty one if requested by the user
319         # TODO: implement it!
320         
321         # Create project files for selected plugins
322         self._createProjectFiles()
323
324     def _editBertosProject(self):
325         # Write the project file
326         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
327         # VERSION file
328         self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
329         # Destination makefile
330         self._writeMakefile(os.path.join(self.maindir, "Makefile"))
331         # Merge sources
332         self._mergeSources(self.sources_dir, self.srcdir, self.old_srcdir)
333         # Copy all the hw files
334         self._writeHwFiles(self.sources_dir, self.hwdir)
335         # Set properly the autoenabled parameters
336         self._setupAutoenabledParameters()
337         # Copy all the configuration files
338         self._writeCfgFiles(self.sources_dir, self.cfgdir)
339         # Destination wizard mk file
340         self._writeWizardMkFile(os.path.join(self.prjdir, os.path.basename(self.prjdir) + "_wiz.mk"))
341         # Create project files for selected plugins
342         self._createProjectFiles()
343
344     def _createProjectFiles(self):
345         # Files for selected plugins
346         relevants_files = {}
347         for plugin in self.infos["OUTPUT"]:
348             module = loadPlugin(plugin)
349             relevants_files[plugin] = module.createProject(self)
350         self.infos["RELEVANT_FILES"] = relevants_files
351
352     def _writeVersionFile(self, filename):
353         version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
354         open(filename, "w").write(versionFileGenerator(self, version_file))
355
356     def _writeProjectFile(self, filename):
357         f = open(filename, "w")
358         f.write(projectFileGenerator(self))
359         f.close()
360
361     def _writeMakefile(self, filename):
362         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/Makefile"), 'r').read()
363         makefile = makefileGenerator(self, makefile)
364         open(filename, "w").write(makefile)
365
366     def _writeUserMkFile(self, filename):
367         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template.mk"), "r").read()
368         # Deadly performances loss was here :(
369         makefile = userMkGenerator(self, makefile)
370         open(filename, "w").write(makefile)
371
372     def _writeWizardMkFile(self, filename):
373         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template_wiz.mk"), "r").read()
374         makefile = mkGenerator(self, makefile)
375         open(filename, "w").write(makefile)
376
377     def _writeMainFile(self, filename):
378         main = open(os.path.join(const.DATA_DIR, "srctemplates/main.c"), "r").read()
379         open(filename, "w").write(main)
380
381     def _writeHwFiles(self, source_dir, destination_dir):
382         for module, information in self.infos["MODULES"].items():
383             for hwfile in information["hw"]:
384                 string = open(source_dir + "/" + hwfile, "r").read()
385                 hwfile_path = destination_dir + "/" + os.path.basename(hwfile)
386                 if not self.edit or not os.path.exists(hwfile_path):
387                     # If not in editing mode it copies all the hw files. If in
388                     # editing mode it copies only the files that don't exist yet
389                     open(os.path.join(destination_dir,os.path.basename(hwfile)), "w").write(string)
390
391     def _writeCfgFiles(self, source_dir, destination_dir):
392         for configuration, information in self.infos["CONFIGURATIONS"].items():
393             string = open(source_dir + "/" + configuration, "r").read()
394             for start, parameter in information["paramlist"]:
395                 infos = information[parameter]
396                 value = infos["value"]
397                 if "unsigned" in infos["informations"] and infos["informations"]["unsigned"]:
398                     value += "U"
399                 if "long" in infos["informations"] and infos["informations"]["long"]:
400                     value += "L"
401                 string = sub(string, parameter, value)
402             f = open(os.path.join(destination_dir, os.path.basename(configuration)), "w")
403             f.write(string)
404             f.close()
405
406     def _writeCustomSrcFiles(self):
407         preset = self.infos["PRESET_PATH"]
408         preset_name = self.infos["PRESET_NAME"]
409         origin = os.path.join(preset, preset_name)
410         project_related_stuff = ("cfg", "hw", self.infos["PROJECT_NAME"] + "_wiz.mk") + const.IGNORE_LIST
411         for element in os.listdir(origin):
412             if element not in project_related_stuff:
413                 element = os.path.join(origin, element)
414                 if os.path.isdir(element):
415                     copytree.copytree(element, self.prjdir, ignore_list=const.IGNORE_LIST)
416                 else:
417                     shutil.copy(element, self.prjdir)        
418
419     def _setupAutoenabledParameters(self):
420         for module, information in self.infos["MODULES"].items():
421             if "configuration" in information and information["configuration"] != "":
422                 configurations = self.infos["CONFIGURATIONS"]
423                 configuration = configurations[information["configuration"]]
424                 for start, parameter in configuration["paramlist"]:
425                     if "type" in configuration[parameter]["informations"] and configuration[parameter]["informations"]["type"] == "autoenabled":
426                         configuration[parameter]["value"] = "1" if information["enabled"] else "0"
427                 self.infos["CONFIGURATIONS"] = configurations
428
429     @property
430     def maindir(self):
431         return self.infos.get("PROJECT_PATH", None)
432     
433     @property
434     def srcdir(self):
435         if self.maindir:
436             return os.path.join(self.maindir, "bertos")
437         else:
438             return None
439
440     @property
441     def prjdir(self):
442         if self.maindir:
443             return os.path.join(self.maindir, self.infos["PROJECT_NAME"])
444         else:
445             return None
446
447     @property
448     def hwdir(self):
449         if self.prjdir:
450             return os.path.join(self.prjdir, 'hw')
451         else:
452             return None
453
454     @property
455     def cfgdir(self):
456         if self.prjdir:
457             return os.path.join(self.prjdir, 'cfg')
458         else:
459             return None
460
461     @property
462     def old_srcdir(self):
463         return self.infos.get("OLD_SOURCES_PATH", None)
464
465     @property
466     def sources_dir(self):
467         return self.infos.get("SOURCES_PATH", None)
468
469     @property
470     def from_preset(self):
471         return self.infos.get("PROJECT_FROM_PRESET", False)
472
473     def _createDirectory(self, directory):
474         if not directory:
475             return
476         if os.path.isdir(directory):
477             shutil.rmtree(directory, True)        
478         os.makedirs(directory)
479
480     def _copySources(self, origin, destination):
481         # If not in editing mode it copies all the bertos sources in the /bertos subdirectory of the project
482         shutil.rmtree(destination, True)        
483         copytree.copytree(origin + "/bertos", destination, ignore_list=const.IGNORE_LIST)
484
485     def _mergeSources(self, origin, destination, old_sources_dir):
486         if old_sources_dir:
487             mergeSources(destination, origin, old_sources_dir)
488
489     def setInfo(self, key, value):
490         """
491         Store the given value with the name key.
492         """
493         self.infos[key] = value
494
495     def info(self, key, default=None):
496         """
497         Retrieve the value associated with the name key.
498         """
499         if key in self.infos:
500             return copy.deepcopy(self.infos[key])
501         return default
502
503     def getCpuInfos(self):
504         cpuInfos = []
505         for definition in self.findDefinitions(const.CPU_DEFINITION):
506             cpuInfos.append(getInfos(definition))
507         return cpuInfos
508
509     def searchFiles(self, filename):
510         file_dict = self.infos["FILE_DICT"]
511         return [(filename, dirname) for dirname in file_dict.get(filename, [])]
512
513     def findDefinitions(self, ftype):
514         # Maintain a cache for every scanned SOURCES_PATH
515         definitions_dict = self._cached_queries.get(self.infos["SOURCES_PATH"], {})
516         definitions = definitions_dict.get(ftype, None)
517         if definitions is not None:
518             return definitions
519         file_dict = self.infos["FILE_DICT"]
520         definitions = []
521         for filename in file_dict:
522             if fnmatch.fnmatch(filename, ftype):
523                 definitions += [(filename, dirname) for dirname in file_dict.get(filename, [])]
524
525         # If no cache for the current SOURCES_PATH create an empty one
526         if not definitions_dict:
527             self._cached_queries[self.infos["SOURCES_PATH"]] = {}
528         # Fill the empty cache with the result
529         self._cached_queries[self.infos["SOURCES_PATH"]][ftype] = definitions
530         return definitions
531
532     def setEnabledModules(self, enabled_modules):
533         modules = self.infos["MODULES"]
534         files = {}
535         for module, information in modules.items():
536             information["enabled"] = module in enabled_modules
537             if information["enabled"]:
538                 for dependency in information["depends"]:
539                     if not dependency in modules:
540                         files[dependency] = files.get(dependency, 0) + 1
541         self.infos["MODULES"] = modules
542         self.infos["FILES"] = files
543
544     def __repr__(self):
545         return '<BProject:instance %d>%s' %(id(self), repr(self.infos))