#! /usr/bin/perl # Copyright (c) 2021, PostgreSQL Global Development Group # # This script looks for symbols that are referenced in #ifdef or defined() # tests without having #include'd the file that defines them. Since this # situation won't necessarily lead to any compiler message, it seems worth # having an automated check for it. In particular, use this to audit the # results of pgrminclude! # # Usage: configure and build a PG source tree (non-VPATH), then start this # script at the top level. It's best to enable as many configure options # as you can, especially --enable-cassert which is known to affect include # requirements. NB: you MUST use gcc, unless you have another compiler that # can be persuaded to spit out the names of referenced include files. # # The results are necessarily platform-dependent, so use care in interpreting # them. We try to process all .c files, even those not intended for the # current platform, so there will be some phony failures. # # src/tools/pginclude/pgcheckdefines # use strict; use warnings; use Cwd; use File::Basename; my $topdir = cwd(); # Programs to use my $FIND = "find"; my $MAKE = "make"; # # Build arrays of all the .c and .h files in the tree # # We ignore .h files under src/include/port/, since only the one exposed as # src/include/port.h is interesting. (XXX Windows ports have additional # files there?) Ditto for .h files in src/backend/port/ subdirectories. # Including these .h files would clutter the list of define'd symbols and # cause a lot of false-positive results. # my (@cfiles, @hfiles); open my $pipe, '-|', "$FIND * -type f -name '*.c'" or die "can't fork: $!"; while (<$pipe>) { chomp; push @cfiles, $_; } close $pipe or die "$FIND failed: $!"; open $pipe, '-|', "$FIND * -type f -name '*.h'" or die "can't fork: $!"; while (<$pipe>) { chomp; push @hfiles, $_ unless m|^src/include/port/| || m|^src/backend/port/\w+/|; } close $pipe or die "$FIND failed: $!"; # # For each .h file, extract all the symbols it #define's, and add them to # a hash table. To cover the possibility of multiple .h files defining # the same symbol, we make each hash entry a hash of filenames. # my %defines; foreach my $hfile (@hfiles) { open my $fh, '<', $hfile or die "can't open $hfile: $!"; while (<$fh>) { if (m/^\s*#\s*define\s+(\w+)/) { $defines{$1}{$hfile} = 1; } } close $fh; } # # For each file (both .h and .c), run the compiler to get a list of what # files it #include's. Then extract all the symbols it tests for defined-ness, # and check each one against the previously built hashtable. # foreach my $file (@hfiles, @cfiles) { my ($fname, $fpath) = fileparse($file); chdir $fpath or die "can't chdir to $fpath: $!"; # # Ask 'make' to parse the makefile so we can get the correct flags to # use. CPPFLAGS in particular varies for each subdirectory. If we are # processing a .h file, we might be in a subdirectory that has no # Makefile, in which case we have to fake it. Note that there seems # no easy way to prevent make from recursing into subdirectories and # hence printing multiple definitions --- we keep the last one, which # should come from the current Makefile. # my $MAKECMD; if (-f "Makefile" || -f "GNUmakefile") { $MAKECMD = "$MAKE -qp"; } else { my $subdir = $fpath; chop $subdir; my $top_builddir = ".."; my $tmp = $fpath; while (($tmp = dirname($tmp)) ne '.') { $top_builddir = $top_builddir . "/.."; } $MAKECMD = "$MAKE -qp 'subdir=$subdir' 'top_builddir=$top_builddir' -f '$top_builddir/src/Makefile.global'"; } my ($CPPFLAGS, $CFLAGS, $CFLAGS_SL, $PTHREAD_CFLAGS, $CC); open $pipe, '-|', "$MAKECMD" or die "can't fork: $!"; while (<$pipe>) { if (m/^CPPFLAGS :?= (.*)/) { $CPPFLAGS = $1; } elsif (m/^CFLAGS :?= (.*)/) { $CFLAGS = $1; } elsif (m/^CFLAGS_SL :?= (.*)/) { $CFLAGS_SL = $1; } elsif (m/^PTHREAD_CFLAGS :?= (.*)/) { $PTHREAD_CFLAGS = $1; } elsif (m/^CC :?= (.*)/) { $CC = $1; } } # If make exits with status 1, it's not an error, it just means make # thinks some files may not be up-to-date. Only complain on status 2. close PIPE; die "$MAKE failed in $fpath\n" if $? != 0 && $? != 256; # Expand out stuff that might be referenced in CFLAGS $CFLAGS =~ s/\$\(CFLAGS_SL\)/$CFLAGS_SL/; $CFLAGS =~ s/\$\(PTHREAD_CFLAGS\)/$PTHREAD_CFLAGS/; # # Run the compiler (which had better be gcc) to get the inclusions. # "gcc -H" reports inclusions on stderr as "... filename" where the # number of dots varies according to nesting depth. # my @includes = (); my $COMPILE = "$CC $CPPFLAGS $CFLAGS -H -E $fname"; open $pipe, '-|', "$COMPILE 2>&1 >/dev/null" or die "can't fork: $!"; while (<$pipe>) { if (m/^\.+ (.*)/) { my $include = $1; # Ignore system headers (absolute paths); but complain if a # .c file includes a system header before any PG header. if ($include =~ m|^/|) { warn "$file includes $include before any Postgres inclusion\n" if $#includes == -1 && $file =~ m/\.c$/; next; } # Strip any "./" (assume this appears only at front) $include =~ s|^\./||; # Make path relative to top of tree my $ipath = $fpath; while ($include =~ s|^\.\./||) { $ipath = dirname($ipath) . "/"; } $ipath =~ s|^\./||; push @includes, $ipath . $include; } else { warn "$CC: $_"; } } # The compiler might fail, particularly if we are checking a file that's # not supposed to be compiled at all on the current platform, so don't # quit on nonzero status. close PIPE or warn "$COMPILE failed in $fpath\n"; # # Scan the file to find #ifdef, #ifndef, and #if defined() constructs # We assume #ifdef isn't continued across lines, and that defined(foo) # isn't split across lines either # open my $fh, '<', $fname or die "can't open $file: $!"; my $inif = 0; while (<$fh>) { my $line = $_; if ($line =~ m/^\s*#\s*ifdef\s+(\w+)/) { checkit($file, $1, @includes); } if ($line =~ m/^\s*#\s*ifndef\s+(\w+)/) { checkit($file, $1, @includes); } if ($line =~ m/^\s*#\s*if\s+/) { $inif = 1; } if ($inif) { while ($line =~ s/\bdefined(\s+|\s*\(\s*)(\w+)//) { checkit($file, $2, @includes); } if (!($line =~ m/\\$/)) { $inif = 0; } } } close $fh; chdir $topdir or die "can't chdir to $topdir: $!"; } exit 0; # Check an is-defined reference sub checkit { my ($file, $symbol, @includes) = @_; # Ignore if symbol isn't defined in any PG include files if (!defined $defines{$symbol}) { return; } # # Try to match source(s) of symbol to the inclusions of the current file # (including itself). We consider it OK if any one matches. # # Note: these tests aren't bulletproof; in theory the inclusion might # occur after the use of the symbol. Given our normal file layout, # however, the risk is minimal. # foreach my $deffile (keys %{ $defines{$symbol} }) { return if $deffile eq $file; foreach my $reffile (@includes) { return if $deffile eq $reffile; } } # # If current file is a .h file, it's OK for it to assume that one of the # base headers (postgres.h or postgres_fe.h) has been included. # if ($file =~ m/\.h$/) { foreach my $deffile (keys %{ $defines{$symbol} }) { return if $deffile eq 'src/include/c.h'; return if $deffile eq 'src/include/postgres.h'; return if $deffile eq 'src/include/postgres_fe.h'; return if $deffile eq 'src/include/pg_config.h'; return if $deffile eq 'src/include/pg_config_manual.h'; } } # my @places = keys %{ $defines{$symbol} }; print "$file references $symbol, defined in @places\n"; # print "includes: @includes\n"; return; }