Channels ▼
RSS

Open Source

A Build System for Complex Projects: Part 4


The Visual Studio Helper

The VC++ 2008 Helper class is responsible for the VC++ specific code used to generate the .vcproj file for every project. It is equivalent to the NetBeans 6 Helper class. The generic build_system_generator.py script is using this helper to generate the .vcproj file and the solution (.sln) file. Let's take a closer look at this class. The first thing it does is import some useful system modules and then import the BaseHelper and Template classes from the build_system_generator module (as well as the 'title' function for debugging purposes):


#!/usr/bin/env python
import os
import sys
import glob
import string
import uuid
from pprint import pprint as pp

sys.path.insert(0, os.path.join(os.path.abspath(os.path.dirname(__file__)), '../'))
from build_system_generator import (BaseHelper,
                                    Template,
                                    title)

Then, there are a couple of utility functions for handling GUIDs. The make_guid() function simply creates a new GUID. Conveniently enough, Python has a module called uuid that can generate GUIDs (and much more). Handy modules like uuid are exactly why Python earned the "Batteries Included" reputation. In any other language, you would have to go and hunt for a 3rd party library (or even worse... implement GUID generation yourself), download it, test it, integrate it into your code and your deployment/packaging script and hope it's not too buggy.


def make_guid():
  title()
  return '{' + str(uuid.uuid4()) + '}'

The get_guid() function extracts the the GUID of a project from an existing .vcproj file or creates a new one if the file doesn't exist.


def get_guid(filename):
  title(additional=filename)
  if os.path.isfile(filename):
    lines = open(filename).readlines()
    guid_line = lines[5]
    assert 'ProjectGUID=' in guid_line
    g = guid_line.split('=')[1][1:-2]
  else:
    g = make_guid()
    
  return g.upper()

The Helper class itself subclasses BaseHelper to benefit from all its common functionality. The __init__() method initializes the templates dir and sets the path separator to a back slash. This is not strictly necessary. Windows can actually work with back and forward slashes and even mix them in the same path. This is a valid path on Windows: "c:/a/b\c\d". However, for esthetic and readability purposes it is best to have a uniform convention and on Windows the back slash is more prevalent. The separator is used when constructing paths.


class Helper(BaseHelper):
  """VC++ 2008 helper
  """
  def __init__(self, templates_dir):
    BaseHelper.__init__(self, templates_dir)
    self.sep = '\\'

The get_templates() method is pretty simple and returns a list containing a single Template object with the template type (program, static library or dynamic library), the path and the relative path of the .vcproj file.


  def get_templates(self, template_type):
    """Get all the template files associated with a particular template type

    Often there will be just one template file, but some build systems
    require multiple build files per template type
    
    @param template_type - 'program', 'dynamic_lib' or 'static_lib'
    @return a Template object
    """
    result = []

    vcproj_file = os.path.join(self.templates_dir,
                               template_type,
                               '%s.vcproj' % template_type)

    assert os.path.isfile(vcproj_file)
    relative_path = '%s.vcproj'
    template = Template(vcproj_file, relative_path, template_type)
    return [template]

The prepare_substitution_dict() is the essential method that prepares the values that the generic ibs uses to populate the template for the .vcproj file. It is much simpler than the corresponding NetBeans method because it needs to generate just one file and not six and also the dynamic information that needs to be substituted in is concentrated in a few places in a uniform way. There are only three placeholders: GUID, SourceFiles and HeaderFiles. All the other information is encoded in the templates. Here is signature:


  def prepare_substitution_dict(self,
                                project_name,
                                project_type,
                                project_file_template,
                                project_dir,
                                libs_dir,
                                dependencies,
                                source_files,
                                header_files,
                                platform):

The prepare_substitution_dict() method uses a nested function called make_files_section() to prepare the SourceFiles and HeaderFiles lists. This function sorts the file lists too (using a case insensitive custom compare function). Note the recursive nature of this operation to create the files section a mini-template is populated with the file's relative path for each file. The result is an XML fragment that can later be embedded directly in the .vcproj file:


    def make_files_section(file_list):
      def icase_cmp(s1, s2):
        return cmp(s1.lower(), s2.lower())
      file_template = """\
\t\t\t<File
\t\t\t\tRelativePath=".\%s"
\t\t\t\t>
\t\t\t7<</File>"""                      
      
      if file_list == []:
        return ''
      file_list = sorted(file_list, icase_cmp) 
      files = [file_template % os.path.basename(f) for f in file_list]
      return '\n'.join(files) + '\n'

The code of prepare_substitution_dict() itself is trivial. It prepares the filename and then gets the GUID from the get_guid() function and the SourceFiles and HeaderFiles from the nested make_files_section()function and just populates the result dict:


    filename = os.path.join(project_dir, project_name + '.vcproj')
    return dict(GUID=get_guid(filename),
                SourceFiles=make_files_section(source_files),
                HeaderFiles=make_files_section(header_files))
  

The generate_workspace_files() is much more complicated in Visual Studio. It generates the solution file for the entire system. I'll walk you through it because there is a lot going on and it could be hard to figure it out just by staring at the code. It takes as input the solution name, the root path and a list of Project objects and starts iterating over all the sub-directories under the root path using Python's excellent os.walk() function that returns a 3-tuple for each directory under the root path that includes the current directory, its sub-directories and its files. That allows complete iteration of every file and directory. The Visual Studio Helper class supports the notion of folders. As always ibs uses convention over configuration. The convention is that a project must be a direct sub-directory of a folder. So, to figure out the folders automatically all the sub-directories are iterated and whenever a directory that contains a .vcproj file is found its parent must be a folder. Here is the code to iterate over all the sub-directories.


  def generate_workspace_files(self, solution_name, root_path, projects):
    """Generate a VC++ 2008 solution file

    """
    title()
    folders = {}
    for d, subdirs, files in os.walk(root_path):
      if os.path.dirname(d) != root_path:
        continue
      folder_projects = []
      for s in subdirs:
        ...

The project list is provided so non-project directories are skipped. The path to the .vcproj file is constructed and the project GUID is extracted. The correct paths to the dependencies of the current project are computed. Finally a SolutionItem object is constructed that contains all the relevant information of the project and the appended to the list of folder_projects.


        project_dir = os.path.join(d, s)
        if not project_dir in projects:
          continue
        vcproj_filename = os.path.join(project_dir,
                                       os.path.basename(s) + '.vcproj')
        assert os.path.isfile(vcproj_filename)

        guid = get_guid(vcproj_filename)
        
        # Get the directories of of all the dependency projects
        proj_dependencies = projects[project_dir].dependencies
        
        # Get the GUIDs of all the dependency projects
        dependencies = []
        for dep in proj_dependencies:
          basename = os.path.basename(dep)
          dep_path = os.path.join(dep, basename + '.vcproj')
          dependencies.append(get_guid(dep_path))
             
        si = SolutionItem(item_type=project_type,
                          name=s,
   						  path=vcproj_filename,
						  guid=guid,
						  dependencies=dependencies,
						  projects=[])
        folder_projects.append(si)

If the folder_projects list is not empty then a folder SolutionItem is created that contains all the folder's project. The guid for a folder is just a dummy '?'. After all the folder objects are constructed the make_solution() function is called, which actually generates the .sln file from all the information collected so far and the .sln file is saved to disk.


      guid = '?'
      if folder_projects != []:
        name = os.path.basename(d)
        folder = SolutionItem(name=name,
                              item_type=folder_type,
                              path=None,
                              guid=guid,
                              dependencies=[],
                              projects=folder_projects)
        folders[os.path.basename(d)] = folder
    
    gen_solution = make_solution(root_path, folders)
    solution_filename = os.path.join(root_path, solution_name + '.sln')
    open(solution_filename, 'w').write(gen_solution)


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video