Add --subgraphs support (aka. --group is back ;).
[deps.git] / graph-includes
blob7c021dd1c71e59651049cd58901db20936797cba
1 #!/usr/bin/env perl
3 # graph-includes - create a graphviz graph of source-files
4 # dependencies, with an emphasis on getting usable graphs even for
5 # large projects
7 # (c) 2005,2006 Yann Dirson <ydirson@altern.org>
8 # Distributed under version 2 of the GNU GPL.
10 use warnings;
11 use strict;
13 use File::Basename qw(dirname);
14 use File::Spec::Functions qw(catdir canonpath);
15 use lib catdir(dirname($0), 'lib');
17 #BEGIN { print STDERR '@INC=', join (':', @INC)}
19 use Getopt::Long qw(GetOptions);
20 use List::Util qw(sum);
21 use File::Find qw(find);
22 use graphincludes::params;
24 our $showalldeps=0;
25 our $showsubgraphs=0;
26 our $class='default';
27 our $language='C';
28 our (@colors, @nodestylers, @edgestylers);
29 our ($outfile, $prefixstrip, $paper);
30 our $rendererclass = 'graphincludes::renderer::dot';
32 our $usage = <<EOF;
33 Usage: $0 [options] src/*.[ch]
34 Options:
35 -class {default|uniqueincludes|<your-own-class>}
36 Select "class" of source code
37 -language <lang> Select language syntax for dependency extraction (default: C)
38 -fileregexp <perl-regexp>
39 Use this regexp to identify interesting files inside directories
40 (overrides per-language default regexp)
41 -Include <directory> Adds a directory to the path where to look for project's include files
42 -sysInclude <directory> Adds a directory to the path where to look for system include files
43 -prefixstrip <prefix> Strip <prefix> (eg. "src/") from filenames in the graph
44 -consolidate <min>-<max>
45 Consolidate file groups of levels <min> through <max> (default: 1-1)
46 -color <n>:<label>=<color>[,<label>=<color>...]
47 Use specified colors to show members of level-<n> group labelled <label>
48 -alldeps Do not apply transitive reduction to the graph
50 -showdropped Show in special color edges dropped during transitive reduction
51 -focus <node-label> Like -showdropped but only for edges starting from given node
53 -renderer <engine> Select the rendering program to produce a graph for (default: dot)
54 -output <outfile>.<fmt>
55 Format to output file, using <fmt> as target format
56 -paper a4|a3|letter Select paper size of multi-paged postscript output
58 -verbose Show progress
59 -debug Loads of debuging output
61 -version Display this program's version
62 -help This help text
63 EOF
65 our @colspecs;
67 # memorize command-line for the report
68 our @commandline = @ARGV;
70 GetOptions ('alldeps' => \$showalldeps,
71 'showdropped' => \$graphincludes::params::showdropped,
73 'focus=s' => \@graphincludes::params::focus,
74 'class=s' => \$class,
75 'language=s' => \$language,
76 'fileregexp=s' => \$graphincludes::params::filename_regexp,
78 'renderer=s' => sub {
79 my (undef, $renderer) = @_;
80 $rendererclass = 'graphincludes::renderer::' . $renderer;
83 'Include=s' => \@graphincludes::params::inclpath,
84 'sysInclude=s' => \@graphincludes::params::sysinclpath,
86 'consolidate=s' => sub {
87 my (undef, $range) = @_;
88 ($graphincludes::params::minshow, $graphincludes::params::maxshow) = split /-/, $range;
90 'subgraphs' => \$showsubgraphs,
91 'color=s@' => sub {
92 my (undef, $colspec) = @_;
93 my @temp = split /:/, $colspec;
94 push @colspecs, [$temp[0], $temp[1]];
96 'output=s' => \$outfile,
97 'paper=s' => \$paper,
99 'prefixstrip=s' => \$prefixstrip,
101 'verbose+' => \$graphincludes::params::verbose,
102 'debug' => \$graphincludes::params::debug,
103 'help' => sub { print $usage; exit 0; },
104 'version' => sub { print "$0 version $graphincludes::params::VERSION\n"; exit 0; },
106 ) or print STDERR $usage and exit 1;
108 if (@ARGV == 0) {
109 print STDERR $usage;
110 exit 1;
113 eval "require $rendererclass" or die "cannot load '$rendererclass': $@";
114 my $renderer = new $rendererclass;
116 # deal with non-default output formats
118 $renderer->set_multipage($paper) if defined $paper;
119 $renderer->set_outputfile($outfile) if defined $outfile;
121 # create a project with specified files
122 our $classmodule = "graphincludes::project::" . $class;
123 eval "require $classmodule" or die "cannot load '$classmodule': $@";
124 $classmodule->set_language ($language) or die "cannot set language to '$language'";
125 our @files;
126 foreach my $arg (@ARGV) {
127 if (-d $arg) {
128 find ( { no_chdir => 0,
129 wanted => sub {
130 if ($classmodule->accepts_file ($_)) {
131 push @files, canonpath($File::Find::name);
132 print STDERR "Adding $File::Find::name\n" if $graphincludes::params::debug;
134 } }, $arg);
135 } elsif (-r $arg) {
136 push @files, $arg;
137 } else {
138 die "file does not exist: $arg";
142 # generate "level 0" graph
143 our $project = ($classmodule)->new(prefixstrip => $prefixstrip,
144 files => \@files);
145 push @graphincludes::params::sysinclpath, $project->get_default_sysincludes();
146 $project->init();
148 # Generate group graphs according to filelabel()
149 # Since we may use coloring according to groups, regardless of which groups the
150 # nodes we draw are from, we must compute graphs for all group levels
151 my @previous = ('files');
152 for (my $i = 1; $i <= $project->nlevels; $i++) {
153 $project->apply_transform('DEPS::Transform::CompatGroup',
154 {level => $i,
155 labeller => $project,
156 previous => \@previous},
157 "level$i-groups",
158 'files' );
159 DEPS::Transform::CompatGroup::fixup_dep($project->{TRANSGRAPH},
160 $previous[$#previous], "level$i-groups", 'files');
161 push @previous, "level$i-groups";
164 my $graphtodraw;
166 # consolidate graphs as requested by --group flag
167 if ($graphincludes::params::maxshow != $graphincludes::params::minshow) {
168 my @graphnames = map { ($_==0) ? 'files':"level$_-groups" } ($graphincludes::params::minshow .. $graphincludes::params::maxshow);
169 $graphtodraw = "consolidation $graphincludes::params::minshow-$graphincludes::params::maxshow";
170 $project->apply_transform('DEPS::Transform::Consolidate',
171 {suppress_children => !$showsubgraphs},
172 $graphtodraw,
173 @graphnames );
174 } elsif ($graphincludes::params::maxshow == 0) {
175 $graphtodraw = 'files';
176 } else {
177 $graphtodraw = 'level'.$graphincludes::params::maxshow.'-groups';
180 # maybe get rid of shortcut deps (transitive reduction)
181 unless ($showalldeps) {
182 $project->apply_transform('DEPS::Transform::TransitiveReduction',
184 'reduction',
185 $graphtodraw );
186 $graphtodraw = 'reduction';
189 @colors = $project->defaultcolors();
190 foreach my $colspec (@colspecs) {
191 foreach my $coldef (split /,/, $colspec->[1]) {
192 my @coldef = split /=/, $coldef;
193 $colors[$colspec->[0]]->{$coldef[0]} = $coldef[1];
197 # assign a role to each color: background, outline
199 use DEPS::Style::Node::PerGroup;
200 my @roles = qw(bgcolor bordercolor); my $role=0;
201 for (my $i=$#colors; $i >= $graphincludes::params::minshow; $i--) {
202 if (defined($colors[$i])) {
203 die "not enough supported color roles to color level $i"
204 if $role >= 2;
205 push @nodestylers, new DEPS::Style::Node::PerGroup(attribute => $roles[$role],
206 valuemap => $colors[$i],
207 transgraph => $project->{TRANSGRAPH},
208 graph => 'files',
209 refgraph => "level$i-groups");
210 $role++;
215 # number of ingredients and intra edges in nodes
216 use DEPS::Style::Node::GroupStats;
217 push @nodestylers, new DEPS::Style::Node::GroupStats();
218 # number of ingredients in edges
219 use DEPS::Style::Edge::WeightLabel;
220 push @edgestylers, new DEPS::Style::Edge::WeightLabel();
222 our $stat_nfiles = scalar $project->{ROOTGRAPH}->get_nodes;
223 # NOTE: $stat_nedges below is a cut'n'paste of $stat_ndeps
224 our $stat_ndeps = sum (map { scalar ($project->{ROOTGRAPH}->get_edges_from($_)) }
225 ($project->{ROOTGRAPH}->get_edge_origins) );
227 if (!defined $stat_ndeps or $stat_ndeps == 0) {
228 print STDERR "$0: found no dependency\n";
229 exit 0;
232 # the graph to be drawn
233 my $thegraph = $project->{TRANSGRAPH}->get_node_from_name($graphtodraw)->{DATA};
235 #FIXME: ...
236 our $stat_nnodes = scalar $thegraph->get_nodes;
237 our $stat_nroots = $stat_nnodes - scalar ($thegraph->get_edge_origins);
238 # NOTE: $stat_ndeps above is a cut'n'paste of $stat_nedges
239 our $stat_nedges = sum (map { scalar ($thegraph->get_edges_from($_)) }
240 ($thegraph->get_edge_origins) );
242 # print graph
243 $renderer->printgraph($project->{TRANSGRAPH}->get_node_from_name($graphtodraw),
244 \@nodestylers, \@edgestylers);
246 # print report
248 our $report = 'graph-includes.report';
249 $report = $outfile . '.' . $report if defined $outfile;
250 open REPORT, ">$report" or die "cannot open $report for writing: $!";
251 print REPORT "\n Graph-includes report";
252 print REPORT "\n =====================\n";
254 print REPORT "\nGeneral statistics:";
255 print REPORT "\n-------------------\n\n";
256 print REPORT "$stat_nfiles files, $stat_nnodes nodes (",
257 int(100*($stat_nfiles-$stat_nnodes)/$stat_nfiles), "% dropped)\n";
258 print REPORT "$stat_ndeps dependencies, $stat_nedges edges (",
259 int(100*($stat_ndeps-$stat_nedges)/$stat_ndeps), "% dropped)\n";
260 print REPORT "$stat_nroots root node(s)\n";
262 print REPORT "\n";
263 print REPORT scalar keys %{$project->{REPORT}->{HDR}}, " dependencies not found\n";
264 print REPORT scalar keys %{$project->{REPORT}->{SYS}}, " dependencies identified as system headers\n";
266 print REPORT "\nDeclared dependencies not found:";
267 print REPORT "\n--------------------------------\n\n";
268 for my $dep (sort keys %{$project->{REPORT}->{HDR}}) {
269 print REPORT " $dep\n";
270 for my $src (@{$project->{REPORT}->{HDR}->{$dep}}) {
271 print REPORT " from $src\n";
275 print REPORT "\nUsed system headers:";
276 print REPORT "\n--------------------\n\n";
277 for my $dep (sort keys %{$project->{REPORT}->{SYS}}) {
278 print REPORT " $dep\n";
281 print REPORT "\nCommand-line used:";
282 print REPORT "\n------------------\n\n";
283 # display arguments separated by space, quoting any argument with embedded whitespace
284 print REPORT "$0 ", join ' ', map { m/\s/ ? "\"$_\"" : $_ } @commandline;
286 print REPORT "\n\nThis was $0 version $graphincludes::params::VERSION\n";
287 print REPORT "\n=== End of report ===\n";
288 close REPORT;
290 # wait for renderer to finish if needed
291 $renderer->wait();