cheind / ruby-snippets

Handy ruby snippets to get the job done quickly!

This URL has Read+Write access

ruby-snippets / dependencies / boost.rb
100644 143 lines (130 sloc) 5.763 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#
# 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