#!/usr/bin/perl -w # -*- cperl -*- # setup-ini-graph - Generate a graph of cygwin dependencies culled # from setup.hint # Copyright (C) 2013 Charles Wilson, David Stacey # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. use strict; use version; use Getopt::Long qw(:config gnu_getopt auto_version); use Pod::Usage; use Storable; $main::VERSION = version->new("0.9"); my $man = 0; my $help = 0; my $base_mode = \&strip_base_and_rqmnts; my $opt_verbose = 0; # default value = false my $base_color = 'plum'; my $base_reqs_color = 'pink'; my $key_color = 'lightblue'; my $key_reqs_color = 'palegreen'; my $infile = '-'; my $IFH = \*STDIN; my $outfile = '-'; my $OFH = \*STDOUT; my %valid_mode = ( 'strip-base-and-requirements' => \&strip_base_and_rqmnts, 'strip-base-only' => \&strip_base_only, 'collapse-base-and-requirements' => \&collapse_base_and_rqmnts, 'collapse-base-only' => \&collapse_base_only, 'show-only-base-and-requirements' => \&show_only_base_and_rqmnts, 'show-base-requirements-only' => \&show_base_rqmnts_only, 'show-all' => \&show_all, ); GetOptions ('help|?' => \$help, 'man' => \$man, 'mode=s' => \&mode_handler, 'color' => \$key_color, 'color-req' => \$key_reqs_color, 'color-base' => \$base_color, 'color-base-req' => \$base_reqs_color, 'verbose+' => \$opt_verbose, 'input|i=s' => \&input_handler, 'output|o=s' => \&output_handler) or pod2usage(2); pod2usage(1) if $help; pod2usage(-exitval => 0, -verbose => 2) if $man; sub mode_handler { my ($opt_name, $opt_value) = @_; die "invalid value for $opt_name option: '$opt_value'" unless (exists($valid_mode{$opt_value})); $base_mode = $valid_mode{$opt_value}; } sub input_handler { my ($opt_name, $opt_value) = @_; # if we've already opened a file for input (other than STDIN), close it if ($IFH and not $IFH == \*STDIN) { close $IFH; } if ($opt_value eq '-') { $IFH = \*STDIN; $infile=$opt_value; } elsif ($opt_value) { open $IFH, '<', $opt_value or die $!; $infile=$opt_value; } } sub output_handler { my ($opt_name, $opt_value) = @_; # if we've already opened a file for output (other than STDOUT), close it if ($OFH and not $OFH == \*STDOUT) { close $OFH; } if ($opt_value eq '-') { $OFH = \*STDOUT; $outfile=$opt_value; } elsif ($opt_value) { open $OFH, '>', $opt_value or die $!; $outfile=$opt_value; } } sub collect_key_pkgs { my ($KeyPkgsRef, $ArgvRef, $LinesRef) = @_; my $package_name = ''; my $numArgs = scalar @{$ArgvRef}; if ($numArgs == 0) { # parse all @ lines from setup.ini foreach my $line (@{$LinesRef}) { $line =~ s/\R//g; if ($line =~ /^@ /) { # and add them all to KeyPkgs ($package_name = $line) =~ s/^@ //; $KeyPkgsRef->{$package_name} = (); } } } else { my %pkgArgs; @pkgArgs{@{$ArgvRef}} = (); # parse all @ lines from setup.ini foreach my $line (@{$LinesRef}) { $line =~ s/\R//g; if ($line =~ /^@ /) { ($package_name = $line) =~ s/^@ //; # check if it is in %pkgArgs, and if so add it if (exists($pkgArgs{$package_name})) { $KeyPkgsRef->{$package_name} = (); # and remove from pkgArgs delete $pkgArgs{$package_name}; } } } # Check that %pkgArgs is empty; if not, then the user # specified packages that do not exist if (not scalar keys %pkgArgs == 0) { my $errMsg = ','.join(keys(%pkgArgs)); die 'Error: some packages specified do not exist in the setup.ini:\n\t$errMsg' } } } sub get_base { my ($BaseRef, $LinesRef) = @_; my $package_name = ''; foreach my $line (@{$LinesRef}) { $line =~ s/\R//g; if ($line =~ /^@ /) { ($package_name = $line) =~ s/^@ //; } elsif ($line =~ /^category: /) { (my $categories = $line) =~ s/^category: //; my %cats; my @keys = split(/ /, $categories); @cats{@keys} = (); if (exists($cats{'Base'})) { $BaseRef->{$package_name} = (); } } } } sub get_requires { my ($PlusReqsRef, $ReqsRef, $PkgSetRef, $LinesRef) = @_; %{$PlusReqsRef} = %{ Storable::dclone ($PkgSetRef) }; my $package_name = ''; my $oldsetsize = 0; my $newsetsize = scalar keys %{$PlusReqsRef}; while ($oldsetsize != $newsetsize) { $oldsetsize = $newsetsize; foreach my $line (@{$LinesRef}) { $line =~ s/\R//g; if ($line =~ /^@ /) { ($package_name = $line) =~ s/^@ //; } if (exists($PlusReqsRef->{$package_name})) { if ($line =~ /^requires: /) { (my $requires = $line) =~ s/^requires: //; my @rqts = split(/ /, $requires); foreach my $rqt (@rqts) { if (not exists($PlusReqsRef->{$rqt})) { $PlusReqsRef->{$rqt} = (); } } } } } $newsetsize = scalar keys %{$PlusReqsRef}; } %{$ReqsRef} = %{ Storable::dclone ($PlusReqsRef) }; foreach my $key (keys %{$PkgSetRef}) { delete $ReqsRef->{$key}; } } sub strip_base_and_rqmnts { my ($NodeOrEdgeMode, $BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, $HasBaseRqmntRef, $PackageName, $line) = @_; if ($NodeOrEdgeMode == 0) { # color keypkg nodes light goldenrod yellow color_nodes ($KeyPkgsRef, $key_color); # color nodes required by keypkgs # color_nodes ($KeyPkgsReqsRef, $key_reqs_color); } else { # if it is not in our "universe", skip it return if (not exists($KeyPkgsPlusReqsRef->{$PackageName}) and not exists($BasePlusReqsRef->{$PackageName})); # don't do anything for packages in Base or packages required # by Base. return if (exists($BasePlusReqsRef->{$PackageName})); (my $requires = $line) =~ s/^requires: //; my @rqts = split(/ /, $requires); foreach my $rqt (@rqts) { if (not exists($BasePlusReqsRef->{$rqt})) { print $OFH "\t\"$PackageName\" -> \"$rqt\";\n"; } } } } sub strip_base_only { my ($NodeOrEdgeMode, $BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, $HasBaseRqmntRef, $PackageName, $line) = @_; if ($NodeOrEdgeMode == 0) { # color nodes required by base powderblue color_nodes ($BaseReqsRef, $base_reqs_color); # color keypkg nodes light goldenrod yellow color_nodes ($KeyPkgsRef, $key_color); # color nodes required by keypkgs # color_nodes ($KeyPkgsReqsRef, $key_reqs_color); } else { # if it is not in our "universe", skip it return if (not exists($KeyPkgsPlusReqsRef->{$PackageName}) and not exists($BasePlusReqsRef->{$PackageName})); # don't do anything for packages in Base return if (exists($BaseRef->{$PackageName})); (my $requires = $line) =~ s/^requires: //; my @rqts = split(/ /, $requires); foreach my $rqt (@rqts) { if (not exists($BaseRef->{$rqt})) { print $OFH "\t\"$PackageName\" -> \"$rqt\";\n"; } } } } sub collapse_base_and_rqmnts { my ($NodeOrEdgeMode, $BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, $HasBaseRqmntRef, $PackageName, $line) = @_; if ($NodeOrEdgeMode == 0) { my %CollapsedBase; $CollapsedBase{'Base'} = (); # color 'Base' node color_nodes (\%CollapsedBase, $base_color); # color keypkg nodes color_nodes ($KeyPkgsRef, $key_color); # color nodes required by keypkgs # color_nodes ($KeyPkgsReqsRef, $key_reqs_color); } else { # if it is not in our "universe", skip it return if (not exists($KeyPkgsPlusReqsRef->{$PackageName}) and not exists($BasePlusReqsRef->{$PackageName})); # don't do anything for packages in Base or packages required # by Base. return if (exists($BasePlusReqsRef->{$PackageName})); (my $requires = $line) =~ s/^requires: //; my @rqts = split(/ /, $requires); foreach my $rqt (@rqts) { if (exists($BasePlusReqsRef->{$rqt})) { # This requirement is one of the packages in Base, or is # one of the requirements OF one of the packages in Base. # Replace it with a requirement labeled "Base" but ensure # we only have one such for each. if (not $$HasBaseRqmntRef) { print $OFH "\t\"$PackageName\" -> \"Base\";\n"; $$HasBaseRqmntRef = 1; } } else { print $OFH "\t\"$PackageName\" -> \"$rqt\";\n"; } } } } sub collapse_base_only { my ($NodeOrEdgeMode, $BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, $HasBaseRqmntRef, $PackageName, $line) = @_; if ($NodeOrEdgeMode == 0) { my %CollapsedBase; $CollapsedBase{'Base'} = (); # color 'Base' node color_nodes (\%CollapsedBase, $base_color); # color nodes required by base color_nodes ($BaseReqsRef, $base_reqs_color); # color keypkg nodes color_nodes ($KeyPkgsRef, $key_color); # color nodes required by keypkgs # color_nodes ($KeyPkgsReqsRef, $key_reqs_color); } else { # if it is not in our "universe", skip it return if (not exists($KeyPkgsPlusReqsRef->{$PackageName}) and not exists($BasePlusReqsRef->{$PackageName})); # don't do anything for packages in Base return if (exists($BaseRef->{$PackageName})); (my $requires = $line) =~ s/^requires: //; my @rqts = split(/ /, $requires); foreach my $rqt (@rqts) { if (exists($BaseRef->{$rqt})) { # This requirement is one of the packages in Base # Replace it with a requirement labeled "Base" but ensure # we only have one such for each. if (not $$HasBaseRqmntRef) { print $OFH "\t\"$PackageName\" -> \"Base\";\n"; $$HasBaseRqmntRef = 1; } } else { print $OFH "\t\"$PackageName\" -> \"$rqt\";\n"; } } } } sub show_only_base_and_rqmnts { my ($NodeOrEdgeMode, $BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, $HasBaseRqmntRef, $PackageName, $line) = @_; if ($NodeOrEdgeMode == 0) { # color base nodes color_nodes ($BaseRef, $base_color); # color nodes required by base color_nodes ($BaseReqsRef, $base_reqs_color); } else { # if it is not in our "universe", skip it return if (not exists($KeyPkgsPlusReqsRef->{$PackageName}) and not exists($BasePlusReqsRef->{$PackageName})); # don't do anything for packages that are not either (a) in # Base or (b) required by a package in Base. return if (not exists($BasePlusReqsRef->{$PackageName})); (my $requires = $line) =~ s/^requires: //; my @rqts = split(/ /, $requires); foreach my $rqt (@rqts) { print $OFH "\t\"$PackageName\" -> \"$rqt\";\n"; } } } sub show_base_rqmnts_only { my ($NodeOrEdgeMode, $BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, $HasBaseRqmntRef, $PackageName, $line) = @_; if ($NodeOrEdgeMode == 0) { my %CollapsedBase; $CollapsedBase{'Base'} = (); # color 'Base' node color_nodes (\%CollapsedBase, $base_color); # color nodes required by base color_nodes ($BaseReqsRef, $base_reqs_color); } else { # if it is not in our "universe", skip it return if (not exists($KeyPkgsPlusReqsRef->{$PackageName}) and not exists($BasePlusReqsRef->{$PackageName})); # don't do anything for packages that are not a requirement # of a package in Base. return if (not exists($BaseReqsRef->{$PackageName})); (my $requires = $line) =~ s/^requires: //; my @rqts = split(/ /, $requires); foreach my $rqt (@rqts) { if (exists($BaseRef->{$rqt})) { # This requirement is one of the packages in Base # Replace it with a requirement labeled "Base" but ensure # we only have one such for each. if (not $$HasBaseRqmntRef) { print $OFH "\t\"$PackageName\" -> \"Base\";\n"; $$HasBaseRqmntRef = 1; } } else { print $OFH "\t\"$PackageName\" -> \"$rqt\";\n"; } } } } sub show_all { my ($NodeOrEdgeMode, $BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, $HasBaseRqmntRef, $PackageName, $line) = @_; if ($NodeOrEdgeMode == 0) { # color base nodes skyblue color_nodes ($BaseRef, $base_color); # color nodes required by base color_nodes ($BaseReqsRef, $base_reqs_color); # color keypkg nodes color_nodes ($KeyPkgsRef, $key_color); # color nodes required by keypkgs color_nodes ($KeyPkgsReqsRef, $key_reqs_color); } else { # if it is not in our "universe", skip it return if (not exists($KeyPkgsPlusReqsRef->{$PackageName}) and not exists($BasePlusReqsRef->{$PackageName})); (my $requires = $line) =~ s/^requires: //; my @rqts = split(/ /, $requires); foreach my $rqt (@rqts) { print $OFH "\t\"$PackageName\" -> \"$rqt\";\n"; } } } sub color_nodes { my ($PkgsRef, $ColorName) = @_; print $OFH "\tnode [fillcolor=$ColorName];\n"; foreach my $pkg (keys %{$PkgsRef}) { print $OFH "\t\"$pkg\";\n"; } print $OFH "\tnode [fillcolor=$key_reqs_color];\n"; } sub generate_output { my ($BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, $LinesRef) = @_; my $package_name = ''; my $hasBaseRqmnt = 0; print $OFH "digraph cygwin_setup_ini {\n"; print $OFH "\tnode [shape = ellipse, style=filled, fillcolor=$key_reqs_color];\n"; $base_mode->(0, $BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, \$hasBaseRqmnt, '', ''); foreach my $line (@{$LinesRef}) { $hasBaseRqmnt = 0; $line =~ s/\R//g; if ($line =~ /^@ /) { ($package_name = $line) =~ s/^@ //; $hasBaseRqmnt = 0; } elsif ($line =~ /^requires: /) { $base_mode->(1, # draw edges mode $BaseRef, $BasePlusReqsRef, $BaseReqsRef, $KeyPkgsRef, $KeyPkgsPlusReqsRef, $KeyPkgsReqsRef, \$hasBaseRqmnt, $package_name, $line); } } print $OFH "}\n"; } my @lines = <$IFH>; close $IFH if (not $IFH == \*STDIN); # first, determine the packages we're requesting in the # graph. If not specified in @ARGV, use all packages found # in the setup.ini. my %KeyPkgs = (); my %KeyPkgsPlusReqs = (); my %KeyPkgsReqs = (); collect_key_pkgs (\%KeyPkgs, \@ARGV, \@lines); # Now, collect (recursively) all the "requires" of these items get_requires (\%KeyPkgsPlusReqs, \%KeyPkgsReqs, \%KeyPkgs, \@lines); if ($opt_verbose) { my $setsize = scalar keys %KeyPkgs; my @lbase = sort(keys(%KeyPkgs)); print STDERR "KeyPkgs($setsize) = ",join(" ",@lbase),"\n"; $setsize = scalar keys %KeyPkgsPlusReqs; @lbase = sort(keys(%KeyPkgsPlusReqs)); print STDERR "KeyPkgsPlusReqs($setsize) = ",join(" ",@lbase),"\n"; $setsize = scalar keys %KeyPkgsReqs; @lbase = sort(keys(%KeyPkgsReqs)); print STDERR "KeyPkgsReqs($setsize) = ",join(" ",@lbase),"\n"; } # then build database of Base elements my %Base = (); my %BasePlusReqs = (); my %BaseReqs = (); get_base (\%Base, \@lines); # And collect (recursively) all the "requires" of Base. get_requires (\%BasePlusReqs, \%BaseReqs, \%Base, \@lines); if ($opt_verbose) { my $setsize = scalar keys %Base; my @lbase = sort(keys(%Base)); print STDERR "Base($setsize) = ",join(" ",@lbase),"\n"; $setsize = scalar keys %BasePlusReqs; @lbase = sort(keys(%BasePlusReqs)); print STDERR "BasePlusReqs($setsize) = ",join(" ",@lbase),"\n"; $setsize = scalar keys %BaseReqs; @lbase = sort(keys(%BaseReqs)); print STDERR "BaseReqs($setsize) = ",join(" ",@lbase),"\n"; } generate_output (\%Base, \%BasePlusReqs, \%BaseReqs, \%KeyPkgs, \%KeyPkgsPlusReqs, \%KeyPkgsReqs, \@lines); close $OFH if (not $OFH == \*STDOUT); __END__ =head1 NAME setup_ini_graph - Chart cygwin package dependencies =head1 SYNOPSIS setup_ini_graph [options] [packagename [packagename ...]] Options: --help|-? brief help message --man full documentation --version show version information --mode STRING specify the output mode. Possible values: strip-base-and-requirements strip-base-only collapse-base-and-requirements collapse-base-only show-only-base-and-requirements show-base-requirements-only show-all --color STRING set color of specified packages --color-req STRING set color of package requirements --color-base STRING set color of Base packages --color-base-req STRING set color of Base requirements --input|-i FILE specify input setup.ini file --output|-o FILE specify output .dot file --verbose turn on debugging output If packagenames are specified, then only the specified packages and their dependencies are analyzed. If no packages are specified, then all packages in the setup.ini are analyzed. =head1 OPTIONS =over 8 =item B<--help> Print a brief help message and exits. =item B<--man> Prints the manual page and exits. =item B<--version> Prints version information and exits. =item B<--mode STRING> Specify the output mode. The STRING may be one of the following options: =over =item B All packages in the Base category, and packages on which they depend, are completely removed from the output. This is the default mode. =item B All packages in the Base category are completely removed from the output. =item B All packages in the Base category, and packages on which they depend, are replaced by a single node 'Base'. =item B All packages in the Base category are replaced by a single node 'Base'. =item B Ignore all packages that are not in the Base category, or are not required by packages in the Base category. =item B As above, but the packages which are members of the Base category are replaced by a single node 'Base'. =item B Chart the entire dependency tree. =back =item B<--color STRING> Sets the color to be used for nodes that represent the packages listed on the command line (or all packages extracted from setup.ini that do not fall into one of the other categories below). May be an RGB color name such as palegoldenrod, or an RGB triple of the form #DA70D6. Note that in the latter case, the # must be escaped as \#DA70D6 or '#DA70D6'. The default value is lightblue (#ADD8E6). =item B<--color-req STRING> Sets the color to be used for nodes that represent packages that are required by the ones listed on the command line, but which do not fall into one of the other categories below. The default value is palegreen (#98FB98). See L<--color> for more information. =item B<--color-base STRING> Sets the color to be used for nodes that represent packages that are in the Base category. It is also used to color the 'Base' node in the collapse L<--mode> options. The default value is plum (#DDA0DD). See L<--color> for more information. =item B<--color-base-req STRING> Sets the color to be used for nodes that represent packages which are required by packages in the Base category, but are not in the Base category themselves. The default value is pink (#FFC0CB). =item B<--input FILENAME> Use the specified input file. Defaults to . =item B<--output FILENAME> Use the specified output file. Defaults to . =item B<--verbose> Turn on verbose output (to STDERR). =back =head1 DESCRIPTION B will process a cygwin setup.ini file and generate a dependency chart in dot format. Use C cygwin-deps.svg> to process the result. See the C manpage for more information; note that C is not available in stock cygwin, and must be installed from L. =cut # vim: set ff=unix ts=2 sw=2 et: