#
# Project:: Ruby-Snippets
#
# Author:: Christoph Heindl (mailto:christoph.heindl@gmail.com)
# Homepage:: http://cheind.blogspot.com
#
# == Overview
#
# Ruby script that builds inter-project dependencies of boost (http://www.boost.org)
# via include file parsing using methods and classes defined in <tt>Dependencies</tt>.
#
# == Prerequisites
#
# To run the application you need to install ruby and RGL (Ruby Graph Library).
# After ruby has been installed, install RGL via command line
#
# > gem install rgl
#
# See http://rgl.rubyforge.org/rgl/index.html for RGL documentation and install
# instructions.
#
# Graphviz http://www.graphviz.org/ is required to transform graphs to images.
#
# == What is it?
#
# The script records all header files within the boost include directory. Each
# file path is assigned a vertex name: if file path contains a nested directory
# inside boost include directory, then the first nested directory name is used
# as a vertex name in the graph. If the file path points directly to the boost
# include directory the vertex name as follows:
# if a directory with the same name exists (except for the file extension '.hpp')
# then the directory name is used. Else, the basename of the file including the
# extension is used (considered as mini-library).
#
# Next, each recorded file is parsed for matching '#include' preprocessor statements.
# When such a statement is encountered, the script tries to lookup the file
# from previous recordings. If found, the vertex name of the file recorded previously
# is used. A dependency is then generated between the vertex name of the file
# parsed and the vertex name from the resolved include statement. If the dependency
# causes a cycle in the dependency graph, the dependency is not added but error'd
# to the logger.
#
# After all files are parsed, the dependency graph is reduced by removing
# edges u -> w, where u -> ... -> w exists and written to a '.dot' file
# that can be converted into various formats using http://www.graphviz.org/.
#
# == What is it not?
#
# This script is not
#
# - <b>a complete preprocessor parser</b>: it does not care about conditional
# include statements or commented ones. Parsing is based on simple pattern matching
# to keep to code small nice. You might however add a complete parser if you like to.
#
# - <b>handling cyclic dependencies</b>: some include statements cause cyclic dependencies
# that simply result from the choice of mapping to vertex names. I.e. when
# file boost/a/detail/detail.hpp includes boost/b/win32/abc.hpp which in turn
# includes boost/a/other/other.hpp and the mapping resolves vertex names from
# the first nested directory inside boost, we generate a dependency from a -> b and
# finally another one from b -> a which will not be added (unless explicitly allowed
# see <tt>Dependencies::Walker#on_cycle</tt>).
#
#
require 'logger'
require 'dependencies/walker'
require 'dependencies/dot'
require 'dependencies/all_dependencies'
def usage(notboost=false)
puts "ruby #{$0} path_to_boost" unless notboost
puts "path #{ARGV[0]} does not seem to be the boost include directory." if notboost
exit(1)
end
# Sanity check for command line arguments
usage(false) unless ARGV.length == 1
exit(1) unless File.directory?(ARGV[0])
usage(true) unless File.exists?(File.join(ARGV[0], 'boost/config.hpp'))
boost_dir = ARGV[0]
# Instance a walker that records files and dependencies between files
logger = Logger.new(STDOUT)
w = Dependencies::Walker.new(logger)
# Record all file paths of files ending with '.hpp' residing in any directory
# nested one-level below boost root include directory
w.index(boost_dir, 'boost/*/**/*.hpp') do |path|
# When such a file is discoverd, the nested directory name is used as vertex
# name in the graph
path.split('/')[1]
end
# Index all files residing directly in the boost root include directory.
w.index(boost_dir, 'boost/*.hpp') do |path|
# The vertex named is determined from the following rule:
# When a nested directory with the same name as the file (except for the extension)
# exists, then the directory name is used as vertex name.
# Else, the filename is used (considered as mini library)
dirname_exists = File.directory?(File.join(boost_dir, 'boost/', File.basename(path, '.hpp')))
if dirname_exists
File.basename(path, '.hpp')
else
File.basename(path)
end
end
# Read the content of all header files inside the boost directory.
w.parse(boost_dir,'boost/**/*.hpp') do |path, file|
# Record dependencies in file by matching include statements
dependencies = []
while (line = file.gets)
if line =~ /\#include\s+[\"<]([^\">]+)?/
# Try looking up the file inside the boost directory.
# On success use the same name as the recorded file.
vertex_name = w.try_resolve($1, boost_dir)
dependencies << vertex_name if vertex_name
end
end
dependencies
end
logger.info('Performing transitive reduction on graph')
# Reduce the graph by removing all edges between vertex v -> w, when
# and a path v -> ... -> w exists.
boost_graph = w.graph.transitive_reduction
Dir.mkdir('images') unless File.directory?('images')
# Write to png file using dot and the default template 'dependencies\graph.template'
logger.info("Saving dependency graph for boost as 'images/boost_graph.png'")
Dependencies.to_png(boost_graph, 'images/boost_graph.png', 'images/boost_graph.dot')
# Finally plot each project seperately
boost_graph.each_vertex do |v|
dot_path = "images/#{v.gsub(/\./, '_')}.dot"
img_path = "images/#{v.gsub(/\./, '_')}.png"
logger.info("Saving dependency graph for #{v} as '#{img_path}'")
g = boost_graph.all_dependencies(v)
Dependencies.to_png(g, img_path, dot_path)
end