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