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