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