Fix existing directory problem with copytree.
[bertos.git] / wizard / BProject.py
1 #!/usr/bin/env python
2 # encoding: utf-8
3 #
4 # This file is part of BeRTOS.
5 #
6 # Bertos is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19 #
20 # As a special exception, you may use this file as part of a free software
21 # library without restriction.  Specifically, if other files instantiate
22 # templates or use macros or inline functions from this file, or you compile
23 # this file and link it with other files to produce an executable, this
24 # file does not by itself cause the resulting executable to be covered by
25 # the GNU General Public License.  This exception does not however
26 # invalidate any other reasons why the executable file might be covered by
27 # the GNU General Public License.
28 #
29 # Copyright 2008 Develer S.r.l. (http://www.develer.com/)
30 #
31 # $Id$
32 #
33 # Author: Lorenzo Berni <duplo@develer.com>
34 #
35
36 import os
37 import fnmatch
38 import copy
39 import pickle
40 import shutil
41 import copytree
42
43 import DefineException
44
45 from LoadException import VersionException, ToolchainException
46
47 import const
48
49 from bertos_utils import (
50                             # Utility functions
51                             isBertosDir, getTagSet, getInfos, updateConfigurationValues,
52                             loadConfigurationInfos, loadDefineLists, loadModuleDefinition,
53                             getCommentList, sub,
54                             
55                             # Project creation functions
56                             projectFileGenerator, versionFileGenerator, makefileGenerator,
57                             userMkGenerator, mkGenerator, loadPlugin, mergeSources,
58
59                             # Custom exceptions
60                             ParseError, SupportedException
61                         )
62
63 class BProject(object):
64     """
65     Simple class for store and retrieve project informations.
66     """
67
68     def __init__(self, project_file="", info_dict={}):
69         self.infos = {}
70         self._cached_queries = {}
71         self.edit = False
72         if project_file:
73             self.edit = True
74             self.loadBertosProject(project_file, info_dict)
75
76     #--- Load methods (methods that loads data into project) ------------------#
77
78     def loadBertosProject(self, project_file, info_dict):
79         project_dir = os.path.dirname(project_file)
80         project_data = pickle.loads(open(project_file, "r").read())
81         # If PROJECT_NAME is not defined it use the directory name as PROJECT_NAME
82         # NOTE: this can throw an Exception if the user has changed the directory containing the project
83         self.infos["PROJECT_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(project_dir))
84         self.infos["PROJECT_PATH"] = os.path.dirname(project_file)
85
86         # Check for the Wizard version
87         wizard_version = project_data.get("WIZARD_VERSION", 0)
88         if wizard_version < 1:
89             # Ignore the SOURCES_PATH inside the project file for older projects
90             project_data["SOURCES_PATH"] = project_dir
91         self._loadBertosSourceStuff(project_data["SOURCES_PATH"], info_dict.get("SOURCES_PATH", None))
92
93         # For those projects that don't have a VERSION file create a dummy one.
94         if not isBertosDir(project_dir):
95             version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
96             open(os.path.join(project_dir, "VERSION"), "w").write(version_file.replace("$version", "").strip())
97
98         self.loadSourceTree()
99         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
100         self._loadToolchainStuff(project_data["TOOLCHAIN"], info_dict.get("TOOLCHAIN", None))
101         self.infos["OUTPUT"] = project_data["OUTPUT"]
102         self.loadModuleData(True)
103         self.setEnabledModules(project_data["ENABLED_MODULES"])
104
105     def _loadBertosSourceStuff(self, sources_path, forced_version=None):
106         if forced_version:
107             sources_path = forced_version
108         if os.path.exists(sources_path):
109             self.infos["SOURCES_PATH"] = sources_path
110         else:
111             raise VersionException(self)
112
113     def _loadCpuStuff(self, cpu_name, cpu_frequency):
114         self.infos["CPU_NAME"] = cpu_name
115         cpu_info = self.getCpuInfos()
116         for cpu in cpu_info:
117             if cpu["CPU_NAME"] == cpu_name:
118                 self.infos["CPU_INFOS"] = cpu
119                 break
120         tag_list = getTagSet(cpu_info)
121         # Create, fill and store the dict with the tags
122         tag_dict = {}
123         for element in tag_list:
124             tag_dict[element] = False
125         infos = self.info("CPU_INFOS")
126         for tag in tag_dict:
127             if tag in infos["CPU_TAGS"] + [infos["CPU_NAME"], infos["TOOLCHAIN"]]:
128                 tag_dict[tag] = True
129             else:
130                 tag_dict[tag] = False
131         self.infos["ALL_CPU_TAGS"] = tag_dict
132         self.infos["SELECTED_FREQ"] = cpu_frequency
133
134     def _loadToolchainStuff(self, toolchain, forced_toolchain=None):
135         toolchain = toolchain
136         if forced_toolchain:
137             toolchain = forced_toolchain
138         if os.path.exists(toolchain["path"]):
139             self.infos["TOOLCHAIN"] = toolchain
140         else:
141             raise ToolchainException(self)
142
143     def loadProjectFromPreset(self, preset):
144         """
145         Load a project from a preset.
146         NOTE: this is a stub.
147         """
148         project_file = os.path.join(preset, "project.bertos")
149         project_data = pickle.loads(open(project_file, "r").read())
150         self.loadSourceTree()
151         self._loadCpuStuff(project_data["CPU_NAME"], project_data["SELECTED_FREQ"])
152         self._loadToolchainStuff(project_data["TOOLCHAIN"])
153         # NOTE: this is a HACK!!!
154         # TODO: find a better way to reuse loadModuleData
155         old_project_name = self.infos["PROJECT_NAME"]
156         old_project_path = self.infos["PROJECT_PATH"]
157         self.infos["PROJECT_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(preset))
158         self.infos["PROJECT_PATH"] = preset
159         self.loadModuleData(True)
160         self.setEnabledModules(project_data["ENABLED_MODULES"])
161         self.infos["PROJECT_NAME"] = old_project_name
162         self.infos["PROJECT_PATH"] = old_project_path        
163         self.infos["PRESET_NAME"] = project_data.get("PROJECT_NAME", os.path.basename(preset))
164         self.infos["PRESET_PATH"] = preset
165
166     def loadProjectPresets(self):
167         """
168         Load the default presets (into the const.PREDEFINED_BOARDS_DIR).
169         """
170         # NOTE: this method does nothing (for now).
171         preset_path = os.path.join(self.infos["SOURCES_PATH"], const.PREDEFINED_BOARDS_DIR)
172         preset_tree = {}
173         if os.path.exists(preset_path):
174             preset_tree = self._loadProjectPresetTree(preset_path)
175         self.infos["PRESET_TREE"] = preset_tree
176
177     def _loadProjectPresetTree(self, path):
178         _tree = {}
179         _tree['info'] = self._loadPresetInfo(os.path.join(path, const.PREDEFINED_BOARD_SPEC_FILE))
180         _tree['info']['filename'] = os.path.basename(path)
181         _tree['info']['path'] = path
182         _tree['children'] = []
183         entries = set(os.listdir(path))
184         for entry in entries:
185             _path = os.path.join(path, entry)
186             if os.path.isdir(_path):
187                 sub_entries = set(os.listdir(_path))
188                 if const.PREDEFINED_BOARD_SPEC_FILE in sub_entries:
189                     _tree['children'].append(self._loadProjectPresetTree(_path))
190         # Add into the info dict the dir type (dir/project)
191         if _tree['children']:
192             _tree['info']['type'] = 'dir'
193         else:
194             _tree['info']['type'] = 'project'
195         return _tree
196
197     def _loadPresetInfo(self, preset_spec_file):
198         D = {}
199         execfile(preset_spec_file, {}, D)
200         return D
201
202     def loadModuleData(self, edit=False):
203         module_info_dict = {}
204         list_info_dict = {}
205         configuration_info_dict = {}
206         file_dict = {}
207         for filename, path in self.findDefinitions("*.h") + self.findDefinitions("*.c") + self.findDefinitions("*.s") + self.findDefinitions("*.S"):
208             comment_list = getCommentList(open(path + "/" + filename, "r").read())
209             if len(comment_list) > 0:
210                 module_info = {}
211                 configuration_info = {}
212                 try:
213                     to_be_parsed, module_dict = loadModuleDefinition(comment_list[0])
214                 except ParseError, err:
215                     raise DefineException.ModuleDefineException(path, err.line_number, err.line)
216                 for module, information in module_dict.items():
217                     if "depends" not in information:
218                         information["depends"] = ()
219                     information["depends"] += (filename.split(".")[0],)
220                     information["category"] = os.path.basename(path)
221                     if "configuration" in information and len(information["configuration"]):
222                         configuration = module_dict[module]["configuration"]
223                         try:
224                             configuration_info[configuration] = loadConfigurationInfos(self.infos["SOURCES_PATH"] + "/" + configuration)
225                         except ParseError, err:
226                             raise DefineException.ConfigurationDefineException(self.infos["SOURCES_PATH"] + "/" + configuration, err.line_number, err.line)
227                         if edit:
228                             try:
229                                 path = self.infos["PROJECT_NAME"]
230                                 directory = self.infos["PROJECT_PATH"]
231                                 user_configuration = loadConfigurationInfos(directory + "/" + configuration.replace("bertos", path))
232                                 configuration_info[configuration] = updateConfigurationValues(configuration_info[configuration], user_configuration)
233                             except ParseError, err:
234                                 raise DefineException.ConfigurationDefineException(directory + "/" + configuration.replace("bertos", path))
235                 module_info_dict.update(module_dict)
236                 configuration_info_dict.update(configuration_info)
237                 if to_be_parsed:
238                     try:
239                         list_dict = loadDefineLists(comment_list[1:])
240                         list_info_dict.update(list_dict)
241                     except ParseError, err:
242                         raise DefineException.EnumDefineException(path, err.line_number, err.line)
243         for tag in self.infos["CPU_INFOS"]["CPU_TAGS"]:
244             for filename, path in self.findDefinitions("*_" + tag + ".h"):
245                 comment_list = getCommentList(open(path + "/" + filename, "r").read())
246                 list_info_dict.update(loadDefineLists(comment_list))
247         self.infos["MODULES"] = module_info_dict
248         self.infos["LISTS"] = list_info_dict
249         self.infos["CONFIGURATIONS"] = configuration_info_dict
250         self.infos["FILES"] = file_dict
251
252     def loadSourceTree(self):
253         """
254         Index BeRTOS source and load it in memory.
255         """
256         # Index only the SOURCES_PATH/bertos content
257         bertos_sources_dir = os.path.join(self.info("SOURCES_PATH"), 'bertos')
258         file_dict = {}
259         if os.path.exists(bertos_sources_dir):
260             for element in os.walk(bertos_sources_dir):
261                 for f in element[2]:
262                     file_dict[f] = file_dict.get(f, []) + [element[0]]
263         self.infos["FILE_DICT"] = file_dict
264
265     def reloadCpuInfo(self):
266         for cpu_info in self.getCpuInfos():
267             if cpu_info["CPU_NAME"] == self.infos["CPU_NAME"]:
268                 self.infos["CPU_INFOS"] = cpu_info
269
270     #-------------------------------------------------------------------------#
271
272     def createBertosProject(self):
273         # NOTE: Temporary hack.
274         if self.edit:
275             self._editBertosProject()
276         else:
277             if not self.from_preset:
278                 self._newCustomBertosProject()
279             else:
280                 self._newBertosProjectFromPreset()
281     
282     def _newBertosProject(self):
283         for directory in (self.maindir, self.srcdir, self.prjdir, self.hwdir, self.cfgdir):
284             self._createDirectory(directory)
285         # Write the project file
286         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
287         # VERSION file
288         self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
289         # Destination makefile
290         self._writeMakefile(os.path.join(self.maindir, "Makefile"))
291         # Copy the sources
292         self._copySources(self.sources_dir, self.srcdir)
293         # Copy all the hw files
294         self._writeHwFiles(self.sources_dir, self.hwdir)
295         # Set properly the autoenabled parameters
296         self._setupAutoenabledParameters()
297         # Copy all the configuration files
298         self._writeCfgFiles(self.sources_dir, self.cfgdir)
299         # Destination wizard mk file
300         self._writeWizardMkFile(os.path.join(self.prjdir, os.path.basename(self.prjdir) + "_wiz.mk"))
301
302     def _newCustomBertosProject(self):
303         # Create/write/copy the common things
304         self._newBertosProject()
305         # Destination user mk file
306         self._writeUserMkFile(os.path.join(self.prjdir, os.path.basename(self.prjdir) + ".mk"))
307         # Destination main.c file
308         self._writeMainFile(self.prjdir + "/main.c")
309         # Create project files for selected plugins
310         self._createProjectFiles()
311
312     def _newBertosProjectFromPreset(self):
313         # Create/write/copy the common things
314         self._newBertosProject()
315
316         # Copy all the files and dirs except cfg/hw/*_wiz.mk
317         self._writeCustomSrcFiles()
318
319         if self.infos["EMPTY_MAIN"]:
320             # Create and empty main.c file only if the user check the box
321             self._writeMainFile(self.prjdir + "/main.c")
322
323         # Create project files for selected plugins
324         self._createProjectFiles()
325
326     def _editBertosProject(self):
327         # Write the project file
328         self._writeProjectFile(os.path.join(self.maindir, "project.bertos"))
329         # VERSION file
330         self._writeVersionFile(os.path.join(self.maindir, "VERSION"))
331         # Destination makefile
332         self._writeMakefile(os.path.join(self.maindir, "Makefile"))
333         # Merge sources
334         self._mergeSources(self.sources_dir, self.srcdir, self.old_srcdir)
335         # Copy all the hw files
336         self._writeHwFiles(self.sources_dir, self.hwdir)
337         # Set properly the autoenabled parameters
338         self._setupAutoenabledParameters()
339         # Copy all the configuration files
340         self._writeCfgFiles(self.sources_dir, self.cfgdir)
341         # Destination wizard mk file
342         self._writeWizardMkFile(os.path.join(self.prjdir, os.path.basename(self.prjdir) + "_wiz.mk"))
343         # Create project files for selected plugins
344         self._createProjectFiles()
345
346     def _createProjectFiles(self):
347         # Files for selected plugins
348         relevants_files = {}
349         for plugin in self.infos["OUTPUT"]:
350             module = loadPlugin(plugin)
351             relevants_files[plugin] = module.createProject(self)
352         self.infos["RELEVANT_FILES"] = relevants_files
353
354     def _writeVersionFile(self, filename):
355         version_file = open(os.path.join(const.DATA_DIR, "vtemplates/VERSION"), "r").read()
356         open(filename, "w").write(versionFileGenerator(self, version_file))
357
358     def _writeProjectFile(self, filename):
359         f = open(filename, "w")
360         f.write(projectFileGenerator(self))
361         f.close()
362
363     def _writeMakefile(self, filename):
364         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/Makefile"), 'r').read()
365         makefile = makefileGenerator(self, makefile)
366         open(filename, "w").write(makefile)
367
368     def _writeUserMkFile(self, filename):
369         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template.mk"), "r").read()
370         # Deadly performances loss was here :(
371         makefile = userMkGenerator(self, makefile)
372         open(filename, "w").write(makefile)
373
374     def _writeWizardMkFile(self, filename):
375         makefile = open(os.path.join(const.DATA_DIR, "mktemplates/template_wiz.mk"), "r").read()
376         makefile = mkGenerator(self, makefile)
377         open(filename, "w").write(makefile)
378
379     def _writeMainFile(self, filename):
380         main = open(os.path.join(const.DATA_DIR, "srctemplates/main.c"), "r").read()
381         open(filename, "w").write(main)
382
383     def _writeHwFiles(self, source_dir, destination_dir):
384         for module, information in self.infos["MODULES"].items():
385             for hwfile in information["hw"]:
386                 string = open(source_dir + "/" + hwfile, "r").read()
387                 hwfile_path = destination_dir + "/" + os.path.basename(hwfile)
388                 if not self.edit or not os.path.exists(hwfile_path):
389                     # If not in editing mode it copies all the hw files. If in
390                     # editing mode it copies only the files that don't exist yet
391                     open(os.path.join(destination_dir,os.path.basename(hwfile)), "w").write(string)
392
393     def _writeCfgFiles(self, source_dir, destination_dir):
394         for configuration, information in self.infos["CONFIGURATIONS"].items():
395             string = open(source_dir + "/" + configuration, "r").read()
396             for start, parameter in information["paramlist"]:
397                 infos = information[parameter]
398                 value = infos["value"]
399                 if "unsigned" in infos["informations"] and infos["informations"]["unsigned"]:
400                     value += "U"
401                 if "long" in infos["informations"] and infos["informations"]["long"]:
402                     value += "L"
403                 string = sub(string, parameter, value)
404             f = open(os.path.join(destination_dir, os.path.basename(configuration)), "w")
405             f.write(string)
406             f.close()
407
408     def _writeCustomSrcFiles(self):
409         preset = self.infos["PRESET_PATH"]
410         preset_name = self.infos["PRESET_NAME"]
411         origin = os.path.join(preset, preset_name)
412         project_related_stuff = ("cfg", "hw", self.infos["PROJECT_NAME"] + "_wiz.mk") + const.IGNORE_LIST
413         for element in os.listdir(origin):
414             if element not in project_related_stuff:
415                 full_path = os.path.join(origin, element)
416                 if os.path.isdir(full_path):
417                     copytree.copytree(full_path, os.path.join(self.prjdir, element), ignore_list=const.IGNORE_LIST)
418                 else:
419                     shutil.copy(full_path, self.prjdir)        
420
421     def _setupAutoenabledParameters(self):
422         for module, information in self.infos["MODULES"].items():
423             if "configuration" in information and information["configuration"] != "":
424                 configurations = self.infos["CONFIGURATIONS"]
425                 configuration = configurations[information["configuration"]]
426                 for start, parameter in configuration["paramlist"]:
427                     if "type" in configuration[parameter]["informations"] and configuration[parameter]["informations"]["type"] == "autoenabled":
428                         configuration[parameter]["value"] = "1" if information["enabled"] else "0"
429                 self.infos["CONFIGURATIONS"] = configurations
430
431     @property
432     def maindir(self):
433         return self.infos.get("PROJECT_PATH", None)
434     
435     @property
436     def srcdir(self):
437         if self.maindir:
438             return os.path.join(self.maindir, "bertos")
439         else:
440             return None
441
442     @property
443     def prjdir(self):
444         if self.maindir:
445             return os.path.join(self.maindir, self.infos["PROJECT_NAME"])
446         else:
447             return None
448
449     @property
450     def hwdir(self):
451         if self.prjdir:
452             return os.path.join(self.prjdir, 'hw')
453         else:
454             return None
455
456     @property
457     def cfgdir(self):
458         if self.prjdir:
459             return os.path.join(self.prjdir, 'cfg')
460         else:
461             return None
462
463     @property
464     def old_srcdir(self):
465         return self.infos.get("OLD_SOURCES_PATH", None)
466
467     @property
468     def sources_dir(self):
469         return self.infos.get("SOURCES_PATH", None)
470
471     @property
472     def from_preset(self):
473         return self.infos.get("PROJECT_FROM_PRESET", False)
474
475     def _createDirectory(self, directory):
476         if not directory:
477             return
478         if os.path.isdir(directory):
479             shutil.rmtree(directory, True)        
480         os.makedirs(directory)
481
482     def _copySources(self, origin, destination):
483         # If not in editing mode it copies all the bertos sources in the /bertos subdirectory of the project
484         shutil.rmtree(destination, True)        
485         copytree.copytree(origin + "/bertos", destination, ignore_list=const.IGNORE_LIST)
486
487     def _mergeSources(self, origin, destination, old_sources_dir):
488         if old_sources_dir:
489             mergeSources(destination, origin, old_sources_dir)
490
491     def setInfo(self, key, value):
492         """
493         Store the given value with the name key.
494         """
495         self.infos[key] = value
496
497     def info(self, key, default=None):
498         """
499         Retrieve the value associated with the name key.
500         """
501         if key in self.infos:
502             return copy.deepcopy(self.infos[key])
503         return default
504
505     def getCpuInfos(self):
506         cpuInfos = []
507         for definition in self.findDefinitions(const.CPU_DEFINITION):
508             cpuInfos.append(getInfos(definition))
509         return cpuInfos
510
511     def searchFiles(self, filename):
512         file_dict = self.infos["FILE_DICT"]
513         return [(filename, dirname) for dirname in file_dict.get(filename, [])]
514
515     def findDefinitions(self, ftype):
516         # Maintain a cache for every scanned SOURCES_PATH
517         definitions_dict = self._cached_queries.get(self.infos["SOURCES_PATH"], {})
518         definitions = definitions_dict.get(ftype, None)
519         if definitions is not None:
520             return definitions
521         file_dict = self.infos["FILE_DICT"]
522         definitions = []
523         for filename in file_dict:
524             if fnmatch.fnmatch(filename, ftype):
525                 definitions += [(filename, dirname) for dirname in file_dict.get(filename, [])]
526
527         # If no cache for the current SOURCES_PATH create an empty one
528         if not definitions_dict:
529             self._cached_queries[self.infos["SOURCES_PATH"]] = {}
530         # Fill the empty cache with the result
531         self._cached_queries[self.infos["SOURCES_PATH"]][ftype] = definitions
532         return definitions
533
534     def setEnabledModules(self, enabled_modules):
535         modules = self.infos["MODULES"]
536         files = {}
537         for module, information in modules.items():
538             information["enabled"] = module in enabled_modules
539             if information["enabled"]:
540                 for dependency in information["depends"]:
541                     if not dependency in modules:
542                         files[dependency] = files.get(dependency, 0) + 1
543         self.infos["MODULES"] = modules
544         self.infos["FILES"] = files
545
546     def __repr__(self):
547         return '<BProject:instance %d>%s' %(id(self), repr(self.infos))