adf78febaca716f03cba94fd5b84bb4893a485a8
[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, loadPlugin, 
57                             mergeSources,
58
59                             # Custom exceptions
60                             ParseError, SupportedException
61                         )
62 import bertos_utils
63
64 from compatibility import updateProject
65
66 class BProject(object):
67     """
68     Simple class for store and retrieve project informations.
69     """
70
71     def __init__(self, project_file="", info_dict={}):
72         self.infos = {}
73         self._cached_queries = {}
74         self.edit = False
75         if project_file:
76             self.edit = True
77             self.loadBertosProject(project_file, info_dict)
78
79     #--- Load methods (methods that loads data into project) ------------------#
80
81     def loadBertosProject(self, project_file, info_dict):
82         project_dir = os.path.dirname(project_file)
83         project_data = pickle.loads(open(project_file, "r").read())
84         updateProject(project_data)
85         # If PROJECT_NAME is not defined it use the directory name as PROJECT_NAME
86         # NOTE: this can throw an Exception if the user has changed the directory containing the project
87         self.infos["PROJECT_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(project_dir))
88         self.infos["PROJECT_PATH"] = os.path.dirname(project_file)
89         project_src_path = os.path.join(project_dir, project_data.get("PROJECT_SRC_PATH", project_data["PROJECT_NAME"]))
90         if project_src_path:
91             self.infos["PROJECT_SRC_PATH"] = project_src_path
92             
93         else:
94             # In projects created with older versions of the Wizard this metadata doesn't exist
95             self.infos["PROJECT_SRC_PATH"] = os.path.join(self.infos["PROJECT_PATH"], self.infos["PROJECT_NAME"])
96         self.infos["PROJECT_HW_PATH"] = os.path.join(self.infos["PROJECT_PATH"], project_data.get("PROJECT_HW_PATH", self.infos["PROJECT_PATH"]))
97
98         linked_sources_path = project_data["BERTOS_PATH"]
99         sources_abspath = os.path.abspath(os.path.join(project_dir, linked_sources_path))
100         project_data["BERTOS_PATH"] = sources_abspath
101         
102         self._loadBertosSourceStuff(project_data["BERTOS_PATH"], info_dict.get("BERTOS_PATH", None))
103         
104         self.infos["PRESET"] = project_data.get("PRESET", False)
105
106         # For those projects that don't have a VERSION file create a dummy one.
107         if not isBertosDir(project_dir) and not self.is_preset:
108             version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
109             open(os.path.join(project_dir, "VERSION"), "w").write(version_file.replace("$version", "").strip())
110
111         self.loadSourceTree()
112         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
113         self._loadToolchainStuff(project_data["TOOLCHAIN"], info_dict.get("TOOLCHAIN", None))
114         self.infos["OUTPUT"] = project_data["OUTPUT"]
115         self.loadModuleData(True)
116         self.setEnabledModules(project_data["ENABLED_MODULES"])
117
118     def _loadBertosSourceStuff(self, sources_path, forced_version=None):
119         if forced_version:
120             sources_path = forced_version
121         if os.path.exists(sources_path):
122             self.infos["BERTOS_PATH"] = sources_path
123         else:
124             raise VersionException(self)
125
126     def _loadCpuStuff(self, cpu_name, cpu_frequency):
127         self.infos["CPU_NAME"] = cpu_name
128         cpu_info = self.getCpuInfos()
129         for cpu in cpu_info:
130             if cpu["CPU_NAME"] == cpu_name:
131                 self.infos["CPU_INFOS"] = cpu
132                 break
133         tag_list = getTagSet(cpu_info)
134         # Create, fill and store the dict with the tags
135         tag_dict = {}
136         for element in tag_list:
137             tag_dict[element] = False
138         infos = self.info("CPU_INFOS")
139         for tag in tag_dict:
140             if tag in infos["CPU_TAGS"] + [infos["CPU_NAME"], infos["TOOLCHAIN"]]:
141                 tag_dict[tag] = True
142             else:
143                 tag_dict[tag] = False
144         self.infos["ALL_CPU_TAGS"] = tag_dict
145         self.infos["SELECTED_FREQ"] = cpu_frequency
146
147     def _loadToolchainStuff(self, toolchain, forced_toolchain=None):
148         toolchain = toolchain
149         if forced_toolchain:
150             toolchain = forced_toolchain
151         if os.path.exists(toolchain["path"]):
152             self.infos["TOOLCHAIN"] = toolchain
153         else:
154             raise ToolchainException(self)
155
156     def loadProjectFromPreset(self, preset):
157         """
158         Load a project from a preset.
159         NOTE: this is a stub.
160         """
161         project_file = os.path.join(preset, "project.bertos")
162         project_data = pickle.loads(open(project_file, "r").read())
163         self.loadSourceTree()
164         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
165         self._loadToolchainStuff(project_data["TOOLCHAIN"])
166
167         # NOTE: this is a HACK!!!
168         # TODO: find a better way to reuse loadModuleData
169         preset_project_name = project_data.get("PROJECT_NAME", os.path.basename(preset))
170         preset_prj_src_path = os.path.join(preset, project_data.get("PROJECT_SRC_PATH", os.path.join(preset, preset_project_name)))
171         preset_prj_hw_path = os.path.join(preset, project_data.get("PROJECT_HW_PATH", preset))
172
173         old_project_name = self.infos["PROJECT_NAME"]
174         old_project_path = self.infos["PROJECT_PATH"]
175         old_project_src_path = self.infos["PROJECT_SRC_PATH"]
176         old_project_hw_path = self.infos["PROJECT_HW_PATH"]
177
178         self.infos["PROJECT_NAME"] = preset_project_name
179         self.infos["PROJECT_PATH"] = preset
180         self.infos["PROJECT_SRC_PATH"] = preset_prj_src_path
181         self.infos["PROJECT_HW_PATH"] = preset_prj_hw_path
182
183         self.loadModuleData(True)
184         self.setEnabledModules(project_data["ENABLED_MODULES"])
185
186         self.infos["PROJECT_NAME"] = old_project_name
187         self.infos["PROJECT_PATH"] = old_project_path
188         self.infos["PROJECT_SRC_PATH"] = old_project_src_path
189         self.infos["PROJECT_HW_PATH"] = old_project_hw_path
190         # End of the ugly HACK!
191
192         self.infos["PRESET_NAME"] = preset_project_name
193         self.infos["PRESET_PATH"] = preset
194         self.infos["PRESET_SRC_PATH"] = preset_prj_src_path
195         self.infos["PRESET_HW_PATH"] = preset_prj_hw_path
196
197     def loadProjectPresets(self):
198         """
199         Load the default presets (into the const.PREDEFINED_BOARDS_DIR).
200         """
201         # NOTE: this method does nothing (for now).
202         preset_path = os.path.join(self.infos["BERTOS_PATH"], const.PREDEFINED_BOARDS_DIR)
203         preset_tree = {}
204         if os.path.exists(preset_path):
205             preset_tree = self._loadProjectPresetTree(preset_path)
206         self.infos["PRESET_TREE"] = preset_tree
207
208     def _loadProjectPresetTree(self, path):
209         _tree = {}
210         _tree["info"] = self._loadPresetInfo(os.path.join(path, const.PREDEFINED_BOARD_SPEC_FILE))
211         _tree["info"]["filename"] = os.path.basename(path)
212         _tree["info"]["path"] = path
213         _tree["children"] = []
214         entries = set(os.listdir(path))
215         for entry in entries:
216             _path = os.path.join(path, entry)
217             if os.path.isdir(_path):
218                 sub_entries = set(os.listdir(_path))
219                 if const.PREDEFINED_BOARD_SPEC_FILE in sub_entries:
220                     _tree["children"].append(self._loadProjectPresetTree(_path))
221         # Add into the info dict the dir type (dir/project)
222         if _tree["children"]:
223             _tree["info"]["type"] = "dir"
224         else:
225             _tree["info"]["type"] = "project"
226         return _tree
227
228     def _loadPresetInfo(self, preset_spec_file):
229         D = {}
230         execfile(preset_spec_file, {}, D)
231         return D
232
233     def loadModuleData(self, edit=False):
234         module_info_dict = {}
235         list_info_dict = {}
236         configuration_info_dict = {}
237         file_dict = {}
238         for filename, path in self.findDefinitions("*.h") + self.findDefinitions("*.c") + self.findDefinitions("*.s") + self.findDefinitions("*.S"):
239             comment_list = getCommentList(open(path + "/" + filename, "r").read())
240             if len(comment_list) > 0:
241                 module_info = {}
242                 configuration_info = {}
243                 try:
244                     to_be_parsed, module_dict = loadModuleDefinition(comment_list[0])
245                 except ParseError, err:
246                     raise DefineException.ModuleDefineException(path, err.line_number, err.line)
247                 for module, information in module_dict.items():
248                     if "depends" not in information:
249                         information["depends"] = ()
250                     information["depends"] += (filename.split(".")[0],)
251                     information["category"] = os.path.basename(path)
252                     if "configuration" in information and len(information["configuration"]):
253                         configuration = module_dict[module]["configuration"]
254                         try:
255                             configuration_info[configuration] = loadConfigurationInfos(self.infos["BERTOS_PATH"] + "/" + configuration)
256                         except ParseError, err:
257                             raise DefineException.ConfigurationDefineException(self.infos["BERTOS_PATH"] + "/" + configuration, err.line_number, err.line)
258                         if edit:
259                             try:
260                                 path = self.infos["PROJECT_SRC_PATH"]
261                                 user_configuration = loadConfigurationInfos(configuration.replace("bertos", path))
262                                 configuration_info[configuration] = updateConfigurationValues(configuration_info[configuration], user_configuration)
263                             except ParseError, err:
264                                 raise DefineException.ConfigurationDefineException(configuration.replace("bertos", path))
265                 module_info_dict.update(module_dict)
266                 configuration_info_dict.update(configuration_info)
267                 if to_be_parsed:
268                     try:
269                         list_dict = loadDefineLists(comment_list[1:])
270                         list_info_dict.update(list_dict)
271                     except ParseError, err:
272                         raise DefineException.EnumDefineException(path, err.line_number, err.line)
273         for tag in self.infos["CPU_INFOS"]["CPU_TAGS"]:
274             for filename, path in self.findDefinitions("*_" + tag + ".h"):
275                 comment_list = getCommentList(open(path + "/" + filename, "r").read())
276                 list_info_dict.update(loadDefineLists(comment_list))
277         self.infos["MODULES"] = module_info_dict
278         self.infos["LISTS"] = list_info_dict
279         self.infos["CONFIGURATIONS"] = configuration_info_dict
280         self.infos["FILES"] = file_dict
281
282     def loadSourceTree(self):
283         """
284         Index BeRTOS source and load it in memory.
285         """
286         # Index only the BERTOS_PATH/bertos content
287         bertos_sources_dir = os.path.join(self.info("BERTOS_PATH"), "bertos")
288         file_dict = {}
289         if os.path.exists(bertos_sources_dir):
290             for element in os.walk(bertos_sources_dir):
291                 for f in element[2]:
292                     file_dict[f] = file_dict.get(f, []) + [element[0]]
293         self.infos["FILE_DICT"] = file_dict
294
295     def reloadCpuInfo(self):
296         for cpu_info in self.getCpuInfos():
297             if cpu_info["CPU_NAME"] == self.infos["CPU_NAME"]:
298                 self.infos["CPU_INFOS"] = cpu_info
299
300     #-------------------------------------------------------------------------#
301
302     def createBertosProject(self):
303         # NOTE: Temporary hack.
304         if self.edit:
305             self._editBertosProject()
306         else:
307             if not self.from_preset:
308                 self._newCustomBertosProject()
309             else:
310                 self._newBertosProjectFromPreset()
311
312     def _newBertosProject(self):
313         for directory in (self.maindir, self.srcdir, self.prjdir, self.cfgdir, self.hwdir):
314             self._createDirectory(directory)
315         # Write the project file
316         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
317         # VERSION file
318         self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
319         # Destination makefile
320         self._writeMakefile()
321         # Copy the sources
322         self._copySources(self.sources_dir, self.srcdir)
323         # Set properly the autoenabled parameters
324         self._setupAutoenabledParameters()
325         # Copy all the configuration files
326         self._writeCfgFiles(self.sources_dir, self.cfgdir)
327         # Destination wizard mk file
328         self._writeWizardMkFile()
329
330     def _newCustomBertosProject(self):
331         # Create/write/copy the common things
332         self._newBertosProject()
333         # Copy the clean hw files
334         self._createDirectory(self.hwdir)
335         # Copy all the hw files
336         self._writeHwFiles(self.sources_dir, self.hwdir)
337         # Destination user mk file
338         self._writeUserMkFile()
339         # Destination main.c file
340         self._writeMainFile(self.prjdir + "/main.c")
341         # Create project files for selected plugins
342         self._createProjectFiles()
343
344     def _newBertosProjectFromPreset(self):
345         # Create/write/copy the common things
346         self._newBertosProject()
347
348         # Copy all the files and dirs except cfg/hw/*.mk
349         self._writeCustomSrcFiles()
350         
351         # Copy the hw files
352         self._writeHwFiles(self.src_hwdir, self.hwdir)
353
354         # Copyt the new *_user.mk file
355         self._writeUserMkFileFromPreset()
356
357         if self.infos["EMPTY_MAIN"]:
358             # Create and empty main.c file only if the user check the box
359             self._writeMainFile(self.prjdir + "/main.c")
360
361         # Create project files for selected plugins
362         self._createProjectFiles()
363
364     def _editBertosProject(self):
365         # Write the project file
366         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
367         if not self.is_preset:
368             # Generate this files only if the project isn't a preset
369             # VERSION file
370             self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
371             # Destination makefile
372             self._writeMakefile()
373             # Merge sources
374             self._mergeSources(self.sources_dir, self.srcdir, self.old_srcdir)
375             # Copy all the hw files
376             self._writeHwFiles(self.sources_dir, self.hwdir)
377             # Destination wizard mk file
378             self._writeWizardMkFile()
379         # Set properly the autoenabled parameters
380         self._setupAutoenabledParameters()
381         # Copy all the configuration files
382         self._writeCfgFiles(self.sources_dir, self.cfgdir)
383         if not self.is_preset:
384             # Create project files for selected plugins only if the project isn't a preset
385             self._createProjectFiles()
386
387     def _createProjectFiles(self):
388         # Files for selected plugins
389         relevants_files = {}
390         for plugin in self.infos["OUTPUT"]:
391             module = loadPlugin(plugin)
392             relevants_files[plugin] = module.createProject(self)
393         self.infos["RELEVANT_FILES"] = relevants_files
394
395     def _writeVersionFile(self, filename):
396         version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
397         open(filename, "w").write(versionFileGenerator(self, version_file))
398
399     def _writeProjectFile(self, filename):
400         f = open(filename, "w")
401         f.write(projectFileGenerator(self))
402         f.close()
403
404     def _writeMakefile(self):
405         bertos_utils.makefileGenerator(self)
406
407     def _writeUserMkFile(self):
408         bertos_utils.userMkGenerator(self)
409
410     def _writeUserMkFileFromPreset(self):
411         bertos_utils.userMkGeneratorFromPreset(self)
412
413     def _writeWizardMkFile(self):
414         bertos_utils.mkGenerator(self)
415
416     def _writeMainFile(self, filename):
417         main = open(os.path.join(const.DATA_DIR, "srctemplates/main.c"), "r").read()
418         open(filename, "w").write(main)
419
420     def _writeHwFiles(self, source_dir, destination_dir):
421         for module, information in self.infos["MODULES"].items():
422             for hwfile in information["hw"]:
423                 string = open(source_dir + "/" + hwfile, "r").read()
424                 hwfile_path = destination_dir + "/" + os.path.basename(hwfile)
425                 if not self.edit or not os.path.exists(hwfile_path):
426                     # If not in editing mode it copies all the hw files. If in
427                     # editing mode it copies only the files that don't exist yet
428                     open(os.path.join(destination_dir,os.path.basename(hwfile)), "w").write(string)
429
430     def _writeCfgFiles(self, source_dir, destination_dir):
431         for configuration, information in self.infos["CONFIGURATIONS"].items():
432             string = open(source_dir + "/" + configuration, "r").read()
433             for start, parameter in information["paramlist"]:
434                 infos = information[parameter]
435                 value = infos["value"]
436                 if "unsigned" in infos["informations"] and infos["informations"]["unsigned"]:
437                     value += "U"
438                 if "long" in infos["informations"] and infos["informations"]["long"]:
439                     value += "L"
440                 string = sub(string, parameter, value)
441             f = open(os.path.join(destination_dir, os.path.basename(configuration)), "w")
442             f.write(string)
443             f.close()
444
445     def _writeCustomSrcFiles(self):
446         origin = self.infos["PRESET_SRC_PATH"]
447         # Files to be ignored (all project files, cfg dir, wizard mk file, all global ignored dirs)
448         project_related_stuff = (
449             "cfg",
450             "hw",
451             self.infos["PRESET_NAME"] + ".mk",
452             self.infos["PRESET_NAME"] + "_user.mk",
453             "project.bertos",
454             self.infos["PRESET_NAME"] + ".project",
455             self.infos["PRESET_NAME"] + ".workspace",
456         ) + const.IGNORE_LIST
457         for element in os.listdir(origin):
458             if element not in project_related_stuff:
459                 full_path = os.path.join(origin, element)
460                 if os.path.isdir(full_path):
461                     copytree.copytree(full_path, os.path.join(self.prjdir, element), ignore_list=const.IGNORE_LIST)
462                 else:
463                     shutil.copy(full_path, self.prjdir)
464
465     def _setupAutoenabledParameters(self):
466         for module, information in self.infos["MODULES"].items():
467             if "configuration" in information and information["configuration"] != "":
468                 configurations = self.infos["CONFIGURATIONS"]
469                 configuration = configurations[information["configuration"]]
470                 for start, parameter in configuration["paramlist"]:
471                     if "type" in configuration[parameter]["informations"] and configuration[parameter]["informations"]["type"] == "autoenabled":
472                         configuration[parameter]["value"] = "1" if information["enabled"] else "0"
473                 self.infos["CONFIGURATIONS"] = configurations
474
475     @property
476     def maindir(self):
477         return self.infos.get("PROJECT_PATH", None)
478
479     @property
480     def srcdir(self):
481         if self.maindir:
482             return os.path.join(self.maindir, "bertos")
483         else:
484             return None
485
486     @property
487     def prjdir(self):
488         return self.infos.get("PROJECT_SRC_PATH", None)
489
490     @property
491     def hwdir(self):
492         if self.prjdir:
493             return os.path.join(self.prjdir, "hw")
494         else:
495             return None
496
497     @property
498     def cfgdir(self):
499         if self.prjdir:
500             return os.path.join(self.prjdir, "cfg")
501         else:
502             return None
503
504     @property
505     def old_srcdir(self):
506         return self.infos.get("OLD_BERTOS_PATH", None)
507
508     @property
509     def sources_dir(self):
510         return self.infos.get("BERTOS_PATH", None)
511
512     @property
513     def src_hwdir(self):
514         if self.from_preset:
515             return os.path.join(self.infos["PRESET_PATH"], self.infos["PRESET_HW_PATH"])
516         else:
517             return self.sources_dir
518
519     @property
520     def from_preset(self):
521         return self.infos.get("PROJECT_FROM_PRESET", False)
522
523     @property
524     def is_preset(self):
525         return self.infos.get("PRESET", False)
526
527     def _createDirectory(self, directory):
528         if not directory:
529             return
530         if os.path.isdir(directory):
531             shutil.rmtree(directory, True)
532         os.makedirs(directory)
533
534     def _copySources(self, origin, destination):
535         # If not in editing mode it copies all the bertos sources in the /bertos subdirectory of the project
536         shutil.rmtree(destination, True)
537         copytree.copytree(origin + "/bertos", destination, ignore_list=const.IGNORE_LIST)
538
539     def _mergeSources(self, origin, destination, old_sources_dir):
540         if old_sources_dir:
541             mergeSources(destination, origin, old_sources_dir)
542
543     def setInfo(self, key, value):
544         """
545         Store the given value with the name key.
546         """
547         self.infos[key] = value
548
549     def info(self, key, default=None):
550         """
551         Retrieve the value associated with the name key.
552         """
553         if key in self.infos:
554             return copy.deepcopy(self.infos[key])
555         return default
556
557     def getCpuInfos(self):
558         cpuInfos = []
559         for definition in self.findDefinitions(const.CPU_DEFINITION):
560             cpuInfos.append(getInfos(definition))
561         return cpuInfos
562
563     def searchFiles(self, filename):
564         file_dict = self.infos["FILE_DICT"]
565         return [(filename, dirname) for dirname in file_dict.get(filename, [])]
566
567     def findDefinitions(self, ftype):
568         # Maintain a cache for every scanned BERTOS_PATH
569         definitions_dict = self._cached_queries.get(self.infos["BERTOS_PATH"], {})
570         definitions = definitions_dict.get(ftype, None)
571         if definitions is not None:
572             return definitions
573         file_dict = self.infos["FILE_DICT"]
574         definitions = []
575         for filename in file_dict:
576             if fnmatch.fnmatch(filename, ftype):
577                 definitions += [(filename, dirname) for dirname in file_dict.get(filename, [])]
578
579         # If no cache for the current BERTOS_PATH create an empty one
580         if not definitions_dict:
581             self._cached_queries[self.infos["BERTOS_PATH"]] = {}
582         # Fill the empty cache with the result
583         self._cached_queries[self.infos["BERTOS_PATH"]][ftype] = definitions
584         return definitions
585
586     def setEnabledModules(self, enabled_modules):
587         modules = self.infos["MODULES"]
588         files = {}
589         for module, information in modules.items():
590             information["enabled"] = module in enabled_modules
591             if information["enabled"]:
592                 for dependency in information["depends"]:
593                     if not dependency in modules:
594                         files[dependency] = files.get(dependency, 0) + 1
595         self.infos["MODULES"] = modules
596         self.infos["FILES"] = files
597
598     def __repr__(self):
599         return "<BProject:instance %d>%s" %(id(self), repr(self.infos))