Channels ▼
RSS

Design

A Build System for Complex Projects: Part 4


The make_solution() uses several mini-templates to construct different parts of the .sln file. Here are the templates. The names pretty much explain the purpose of each template. The templates use the same principle as the project templates and are just segments of text with placeholder for substitution values that the make_solution() function populates with the proper values and weave together:


# A project template has a header and a list of project sections
# such as ProjectDependencies. The ProjectDependencies
# duplicate the dependency information in .vcproj files in VS2005.
# For generating a solution that contains only C++ projects, no other
# project section is needed.
project_template_with_dependencies = """\
Project("${TypeGUID}") = "${Name}", "${Filename}", "${GUID}"
	ProjectSection(ProjectDependencies) = postProject
${ProjectDependencies}
	EndProjectSection
EndProject
"""
project_template_without_dependencies = """\
Project("${TypeGUID}") = "${Name}", "${Filename}", "${GUID}"
EndProject
"""
project_configuration_platform_template = """\
\t\t${GUID}.Debug|Win32.ActiveCfg = Debug|Win32
\t\t${GUID}.Debug|Win32.Build.0 = Debug|Win32
\t\t${GUID}.Release|Win32.ActiveCfg = Release|Win32
\t\t${GUID}.Release|Win32.Build.0 = Release|Win32
"""
# This is the solution template for VS 2008
# The template arguments are:
#
# Projects
# ProjectConfigurationPlatforms
# NestedProjects
#
solution_template = """
Microsoft Visual Studio Solution File, Format Version 10.00
# Visual Studio 2008
${Projects}
Global
\tGlobalSection(SolutionConfigurationPlatforms) = preSolution
\t\tDebug|Win32 = Debug|Win32
\t\tRelease|Win32 = Release|Win32
\tEndGlobalSection
\tGlobalSection(ProjectConfigurationPlatforms) = postSolution
${Configurations}
\tEndGlobalSection
\tGlobalSection(SolutionProperties) = preSolution
\t\tHideSolutionNode = FALSE
\tEndGlobalSection
\tGlobalSection(NestedProjects) = preSolution
${NestedProjects}
\tEndGlobalSection
EndGlobal
"""

Whoever designed the Visual Studio build system was big on GUIDs. Almost every object is identified by a GUID including the types of various solution items like folders and projects:


# Guids for regular project and solution folder
project_type = '{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}'
folder_type = '{2150E333-8FDC-42A3-9474-1A3956D46DE8}'

The SolutionItem class itself is really just a named tuple, but since ibs is not limited to Python 2.6 and up (when named tuples were introduced into the language) I use a dedicated class:


class SolutionItem(object):
  """Represents a solution folder or project
  
  The set of solution projects contain all the information
  necessary to generate a solution file.
  
  name - the name of the project/folder
  type - folder_type or project_type
  path - the relative path from the root dir to the .vcproj file for projects,
         same as name for folders
  guid - the GUID of the project/folder
  dependencies - A list of project guids the project depends on.
    It is empty for folders and projects with no dependencies.
    
  projects - list of projects hosted by the folder. It is empty for projects.
  """
  def __init__(self, item_type, name, path, guid, dependencies, projects):
    title()
    self.name = name
    self.type = item_type
    self.path = path
    self.guid = guid
    self.dependencies = dependencies
    self.projects = projects

The make_solution() takes the source directory and the folders list to generate the solution file using a bunch of nested functions.


def make_solution(source_dir, folders):
  """Return a string representing the .sln file

  It uses a lot of nested functions to make the different parts
  of a solution file:
   - make_project_dependencies
   - make_projects
   - make_configurations
   - make nested_projects

  @param folders - a dictionary whose keys are VS folders and the values
    are the projects each folder contains. Each project must be an object that
    has a directory path (relative to the root dir), a guid and a
    list of dependencies (each dependency is another projects). This directory
    should contain a .vcproj file whose name matches the directory name.
  @param projects - a list of projects that don't have a folder and are contained
    directly by the solution node.
  """

The get_existing_folders() nested function takes and existing .sln file and extracts the GUIDs of every project in it. It returns a dictionary of project names and GUIDs that can be used to regenerate a .sln file with identical GUIDs to the existing ones.


  def get_existing_folders(sln_filename):
    title()
    lines = open(sln_filename).readlines()
    results = {}
    for line in lines:
      if line.startswith('Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") ='):
        tokens = line.split('"')
        print tokens
        name = tokens[-4]
        guid = tokens[-2]
        results[name] = guid
        
    return results

The make_project_dependencies() nested function takes a list of dependency GUIDs of a project and returns the text fragment that is the ProjectDependencies sub-section of this project in the .sln file.


  def make_project_dependencies(dependency_guids):
    title()
    if dependency_guids == []:
      return ''
    
    result = []
    for g in dependency_guids:
      result.append('\t\t%s = %s' % (g, g))
    
    result = '\n'.join(result)
    return result

The make_projects() nested function takes the source directory and the list of projects and generates a text fragment that represents all the projects in the .sln file. It uses the micro templates defined earlier and the make_project_dependencies() function.


  def make_projects(source_dir, projects):
    title()
    result = ''
    t1 = string.Template(project_template_with_dependencies)
    t2 = string.Template(project_template_without_dependencies)
    for p in projects:
      if p.type == project_type:
        filename = p.path[len(source_dir) + 1:].replace('/', '\\')
      else:
        filename = p.name
      dependency_guids = [get_guid(p.path) for d in p.dependencies]
      guid = get_guid(filename) if p.guid is None else p.guid
      d = dict(TypeGUID=p.type,
               Name=p.name,
               Filename=filename,
               GUID=guid,
               ProjectDependencies=make_project_dependencies(p.dependencies))
      t = t1 if p.dependencies != [] else t2
      s = t.substitute(d)
      result += s

    return result[:-1]

The make_configurations() function returns a text fragment that represents all the project configuration platforms. It works by iterating over the projects list and populating the project_configuration_platform template with each project's GUID.


  def make_configurations(projects):
    title()
    result = ''
    t = string.Template(project_configuration_platform_template)
    for p in projects:
      d = dict(GUID=p.guid)
      s = t.substitute(d)
      result += s

    return result[:-1]

The make_nested_projects() function returns a text fragment that represents all the nested projects in the .sln file. It works by iterating over the folders and populating the nested_project template with the guids of each nested project and its containing folder. Each folder is an object that has guid attribute and a projects attribute (which is a list of its contained projects):


  def make_nested_projects(folders):
    title()
    for f in folders.values():
      assert hasattr(f, 'guid') and type(f.guid) == str
      assert hasattr(f, 'projects') and type(f.projects) in (list, tuple)
    
    result = ''
    nested_project = '\t\t${GUID} = ${FolderGUID}\n'
    t = string.Template(nested_project)
    for folder in folders.values():
      for p in folder.projects:
        d = dict(GUID=p.guid, FolderGUID=folder.guid)
        s = t.substitute(d)
        result += s

    return result[:-1]

These were all the nested functions and here is how the containing make_solution() function puts them to good use.


  try:
    sln_filename = glob.glob(os.path.join(source_dir, '*.sln'))[0]
    existing_folders = get_existing_folders(sln_filename)
  except:
    existing_folders = []
    
  # Use folders GUIDs from existing .sln file (if there is any) 
  for name, f in folders.items():
    if name in existing_folders:
      f.guid = existing_folders[name]
    else:
      f.guid = make_guid()

  # Prepare a flat list of all projects
  all_projects =[]
  for f in folders.values():
    all_projects.append(f)
    all_projects += f.projects

  # Prepare the substitution dict for the solution template
  projects = [p for p in all_projects if p.type == project_type]
  all_projects = make_projects(source_dir, all_projects)
  
  configurations = make_configurations(projects)
  nested_projects = make_nested_projects(folders)
  d = dict(Projects=all_projects,
           Configurations=configurations,
           NestedProjects=nested_projects)

  # Create the final solution text by substituting the dict into the template
  t = string.Template(solution_template)
  solution = t.substitute(d)

  return 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