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