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