#!/usr/bin/perl
# afsmodname - return the name of the AFS module to load
# usage: afsmodname [path]
# THIS SCRIPT IS UNDER SOURCE CONTROL!
# The master copy is /afs/cs/misc/openafs/src/client-support/afsmodname

use strict;
use vars qw($modbase $VERSION @TrySyms @AddSyms);
use vars qw($DEBUG $Mode $OutFile %SymCache);

$VERSION = '$Revision: 1.6 $';
$VERSION =~ s/^\$[a-z]*:?\s*(\S*)\s*\$$/$1/i;
$VERSION = 'testing' unless $VERSION;
$modbase = '/usr/vice/etc/modload';
@TrySyms = qw(__iget iget __iget4 iget4);
@AddSyms = qw(sock_create);


sub parse_symbol ($) {
  my($symbol) = @_;

  if ($symbol =~ /^(.*)_R((?:smp)?(?:2gig)?_?[0-9a-f]{8})$/) {
    ($1, $2);
  } else {
    ($symbol, '--none--');
  }
}


sub get_ksym ($) {
  my($req_sym) = @_;
  my($addr, $symbol, $module, $version, @answer);


  if (exists($SymCache{$req_sym})) {
    print STDERR "get_ksym($req_sym) [cached]\n" if $DEBUG > 1;
    return $SymCache{$req_sym};
  }

  print STDERR "get_ksym($req_sym)" if $DEBUG;
  $SymCache{$req_sym} = undef;
  open(KSYMS, '/proc/ksyms') or die "open /proc/ksyms: $!\n";
  while (<KSYMS>) {
    if (/^(\w+)\s+(\w+)\s+\[(.*)\]/) {
      ($addr, $symbol, $module) = ($1, $2, $3)
    } elsif (/^(\w+)\s+(\w+)/) {
      ($addr, $symbol, $module) = ($1, $2, 'KERNEL')
    } else { next }

    ($symbol, $version) = parse_symbol($symbol);

    if ($symbol eq $req_sym) {
      $SymCache{$req_sym} = [$addr, $version, $module];
      print STDERR " => [addr=$addr, vers=$version, mod=$module]\n" if $DEBUG;
      last;
    }
  }
  close(KSYMS);

  print STDERR " => not found\n" if $DEBUG && !defined($SymCache{$req_sym});
  $SymCache{$req_sym};
}


sub get_modsyms ($) {
  my($modpath) = @_;
  my($symbol, $version, $V);

  $V = {};
  open(NM, "nm $modpath|") or die "nm $modpath: $!\n";
  while (<NM>) {
    chomp;
    next unless /^\s+U\s+/;
    ($symbol, $version) = parse_symbol($');
    $$V{$symbol} = $version unless $version eq '--none--';
  }
  close(NM);
  $V;
}


sub get_hdrsyms ($) {
  my($srcpath) = @_;
  my($moddir, @hdrs, $h);
  my($symbol, $version, $V);


  $moddir = "$srcpath/include/linux/modules";
  opendir(HDRS, $moddir) or die "$moddir: $!\n";
  @hdrs = readdir(HDRS);
  closedir(HDRS);

  $V = {};
  foreach $h (@hdrs) {
    next unless $h =~ /\.ver$/;
    open(HDR, "$moddir/$h") or die "$moddir/$h: $!\n";
    while (<HDR>) {
      chomp;
      next unless /#define __ver_(\S+)\s+(\S+)/;
      $$V{$1} = $2;
    }
    close(HDR);
  }
  $V;
}


sub get_cputype () {
  my($cputype, $family, $vendor, $model);

  open(CPUINFO, '/proc/cpuinfo') or die "open /proc/cpuinfo: $!\n";
  while (<CPUINFO>) {
    if    (/^cpu\s*\:\s*(\S+)/)       { $cputype = $1 }
    elsif (/^cpu family\s*:\s*(\S+)/) { $family  = $1 }
    elsif (/^vendor_id\s*:\s*(\S+)/)  { $vendor  = $1 }
    elsif (/^model\s*:\s*(\S+)/)      { $model   = $1 }
  }
  close(CPUINFO);
  if    ($vendor eq 'GenuineIntel') { $vendor = 'intel' }
  elsif ($vendor eq 'AuthenticAMD') { $vendor = 'amd'   }
  $cputype = "${family}86" if !defined($cputype);
  [$cputype, $vendor, $model];
}


sub table_lookup ($@) {
  my($cpu, @paths) = @_;
  my($path, $symbol, $version, $mincpu, @mincpu, $module, $info, @supp);
  my($prev_module);     # last module line we saw
  my($match_module);    # last matching module
  my($prev_match);      # true if last module matches so far

  foreach $path (@paths) {
    next unless -f $path;
    $prev_match = 0;
    open(TABLE, $path) or die "open $path: $!\n";
    while (<TABLE>) {
      # Skip comments
      next if (/^\#/ || /^\s*$/);

      # Check supplemental requirements
      if (/^\s*\>/) {
        @supp = split;
        foreach (@supp) {
          if (/([^=]*)=([^=]*)/) {
            ($symbol, $version) = ($1, $2);
            $info = get_ksym($symbol);
            $prev_match = 0 if !$info || $version ne $$info[1];
          }
        }
        next;
      }

      # This is a new module, so all supplemental requirements for the
      # previous module have been processed.  If they all passed, then
      # the previous module is a matching module.
      $match_module = $prev_module if $prev_match;

      # Parse the line and remember the module name
      ($symbol, $version, $mincpu, $module) = split;
      $prev_module = $module;
      $prev_match  = 0;
      if ($DEBUG) {
        print STDERR "Try $module ($symbol=$version)",
                     ($mincpu ne '-') ?  " mincpu = $mincpu" : "",
                     "\n";
      }

      # Check mincpu requirement
      if ($mincpu ne '-') {
        @mincpu = split(/\./, $mincpu);
        if ($mincpu[0] ne '' && $mincpu[0] >  $$cpu[0]) {  # min family
          print STDERR " mincpu failed: $mincpu[0] > $$cpu[0]\n" if $DEBUG;
          next;
        }
        if ($mincpu[1] ne '' && $mincpu[1] ne $$cpu[1]) {  # exact vendor
          print STDERR " mincpu failed: $mincpu[1] != $$cpu[1]\n" if $DEBUG;
          next;
        }
        if ($mincpu[2] ne '' && $mincpu[2] >  $$cpu[2]) {  # min model
          print STDERR " mincpu failed: $mincpu[2] > $$cpu[2]\n" if $DEBUG;
          next;
        }
      }

      # Check primary symbol requirement
      $info = get_ksym($symbol);
      next unless $info;
      next unless $version eq $$info[1];

      # OK; it's a match so far.  There may still be some supplemental
      # requirements that we need to check.
      $prev_match = 1;
    }
    close(TABLE);
    $match_module = $prev_module if $prev_match;
  }
  $match_module;
}


sub dump_versions ($) {
  my($cpu) = @_;
  my($version);

  print STDERR "CPU Type:       ", join('.', @$cpu), "\n";

  chomp($version = `uname -rv`);
  print STDERR "Linux version:  $version\n";

  if (open(RHR, "/etc/redhat-release")) {
    chomp($version = <RHR>);
     print STDERR "RedHat release: $version\n";
  }
}


sub dump_syms (@) {
  my(@syms) = @_;
  my($sym, $info);

  print STDERR "Symbol versions:\n";
  foreach $sym (@syms) {
    $info = get_ksym($sym);
    printf STDERR "  %-10s %s\n", $sym, $$info[1] if $info;
  }
}


sub gen_table (@) {
  my(@modules) = @_;
  my($module, $modname, $V, $sym, $count, @add);

  print <<'EOF';
# This file describes the available AFS kernel modules and what kernel
# versions they work with.  Each line matches against some kernel symbol
# version, and specifies a module which may be used with kernels containing
# that version of the specified symbol.  Only lines which match the
# currently-running kernel are considered.
#
# In addition, each line may specify a minimum CPU model on which the module
# will work.  If this value is present, the actual CPU model must be greater
# than or equal to the version specified; otherwise, the module is assumed
# to work on any CPU.
#
# The last match found will be used.
#
# Symbol  Version       MinCPU  Module
#=======  ============  ======  ====================
EOF
  foreach $module (@modules) {
    ($modname = $module) =~ s/.*\///;
    $modname =~ s/^libafs[-.](.*)\.o$/$1/;
    $V = get_modsyms($module);
    $count = 0;
    foreach $sym (@TrySyms) {
      next unless exists $$V{$sym};
      $count++;
      printf "%-8s  %-12s  %-6s  %s\n", $sym, $$V{$sym}, '-', $modname;
      last;
    }
    if (!$count) {
      print STDERR "Unable to find a suitable symbol reference in $modname!\n";
      next;
    }
    @add = ();
    foreach $sym (@AddSyms) {
      next unless exists $$V{$sym};
      push(@add, "$sym=$$V{$sym}");
    }
    print "> ", join(' ', @add), "\n" if @add;
  }
}


sub scan_kernels (@) {
  my(@kernels) = @_;
  my($kernel, $kpath, $kname, $V);

eval <<"EOF";
format =
@<<<<<<<<<<<<<<<<<<<<<<<< @{[' @<<<<<<<<<<<' x scalar(@TrySyms)]}
\$kname, @{[join(',', map(q/$$V{'/ . $_ . q/'}/, @TrySyms))]}
.
EOF

  $kname = 'Kernel';
  $V = { map(($_ => $_), @TrySyms) };
  write;

  $kname = '=========================';
  $V = { map(($_ => '============'), @TrySyms) };
  write;

  foreach $kernel (@kernels) {
    if    (-d "$kernel/src/include/linux/modules") { $kpath = "$kernel/src" }
    elsif (-d "$kernel/include/linux/modules")     { $kpath = $kernel       }
    else { next }
    ($kname = $kpath) =~ s#/src$##;
    $kname =~ s/.*\///;

    $V = get_hdrsyms($kpath);
    write;
  }
}


sub symcompare ($$) {
  my($module, $kernel) = @_;
  my($ksyms, $msyms, $sym, $kvers, $mvers, $info);

eval <<'EOF';
format =
@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<  @<<<<<<<<<<<  @<<<<<<<<<<<
$sym, $kvers, $mvers
.
EOF

  if (defined($kernel)) { $ksyms = get_hdrsyms($kernel) }
  $msyms = get_modsyms($module);

  print "Symbol                          Kernel        Module\n";
  print "==============================  ============  ============\n";
  foreach (keys %$msyms) {
    $sym = $_;
    $mvers = $$msyms{$sym};
    if (defined($kernel)) {
      $kvers = $$ksyms{$sym};
    } else {
      $info = get_ksym($sym);
      $kvers = $$info[1];
    }
    next if $kvers eq $mvers;
    write;
  }
}


## MAIN PROGRAM

my($cpu, $module);

while ($ARGV[0] =~ /^-/) {
  if      ($ARGV[0] eq '-d') {
    shift @ARGV;
    $DEBUG++;
  } elsif ($ARGV[0] eq '-c') {
    shift @ARGV;
    $Mode = 'c';
  } elsif ($ARGV[0] eq '-k') {
    shift @ARGV;
    $Mode = 'k';
  } elsif ($ARGV[0] eq '-x') {
    shift @ARGV;
    $Mode = 'x';
  } elsif ($ARGV[0] eq '-f') {
    shift @ARGV;
    $OutFile = shift @ARGV;
  } elsif ($ARGV[0] eq '-h') {
    print <<'EOF';
usage: afsmodname [-d] modbase                          (find module)
       afsmodname [-d] [-f outfile] -x modules ...      (make table)
       afsmodname [-d] [-f outfile] -k kernels ...      (scan kernels)
       afsmodname [-d] [-f outfile] -c module [kernel]  (check module)
       afsmodname -h                                    (print help)
       afsmodname -v                                    (print version)
EOF
    exit(0);
  } elsif ($ARGV[0] eq '-v') {
    print "afsmodname $VERSION\n";
    exit(0);
  } else {
    die "Unknown option: $ARGV[0]\n";
  }
}

if ($Mode) {
  if ($OutFile) {
    open(STDOUT, ">$OutFile") or die "$OutFile: $!\n";
  }
  if ($Mode eq 'x') { gen_table(@ARGV)               }
  if ($Mode eq 'k') { scan_kernels(@ARGV)            }
  if ($Mode eq 'c') { symcompare($ARGV[0], $ARGV[1]) }
  exit(0);
}

$modbase = $ARGV[0] if @ARGV;

$cpu = get_cputype();

$module = table_lookup($cpu, "$modbase/SymTable", "$modbase/SymTable.local");

if ($module) {
  print "libafs-$module.o";
  exit(0);
}

print STDERR <<'EOF';
Hmm...  I can't seem to find an AFS kernel module suitable for your Linux
kernel.  That means you will need to build or obtain a suitable module.
The following information may be of some use in obtaining assistance:
EOF

dump_versions($cpu);
dump_syms(sort (@TrySyms, keys %SymCache));

exit(1);
