#!/usr/bin/perl -w
#
# $Id$
#
# Copyright (c) 2001, Jeremy Mates.  This script is free software;
# you can redistribute it and/or modify it under the same terms as
# Perl itself.
#
# Run perldoc(1) on this file for additional documentation.
#
######################################################################
#
# REQUIREMENTS

require 5;

use strict;

######################################################################
#
# MODULES

use Carp;           # better error reporting
use Getopt::Std;    # command line option processing

######################################################################
#
# VARIABLES

# default method to use when parsing dmesg and config files
my $method;
chomp($method = `uname`);

# re-conf and re-dmesg match the device in $1 and parent device
# in $2.  prefix is what to tag disabled lines with in output.
# extra is a built-in hack for devices I like enabled by default.
my $how = {
    'simple' => {
        're-conf'  => '^([^\s:]+):\s*(.*)',
        're-dmesg' => '^([^\s:]+):\s*(.*)',
        'prefix'   => '#',
    },
    'OpenBSD' => {
        'extra'    => 'root mainbus isa pci inphy',
        're-conf'  => '^([a-z]+).{0,3}?\s+at\s+([a-z]+)',
        're-dmesg' => '^([a-z]+)\d*\s+at\s+([a-z]+)',
        'prefix'   => '#PRUNED#',

    },
    'FreeBSD' => {
        'extra' =>
        'npx sio ata atadisk atapicd ppbus miibus scbus da sa cd pass',
        're-conf'  => '^device\s+([a-z]+)(?:\d*.*?at\s+([a-z]+))?',
        're-dmesg' => '^([a-z]+)\d*:(?:.*?on\s+([a-z]+)\d*)?',
        'prefix'   => '#PRUNED#',
    }
};

my $VERSION;
($VERSION = '$Revision: 2.9 $ ') =~ s/[^0-9.]//g;

my (%opts, $dmesg_lookup, $debug);

######################################################################
#
# MAIN

# parse command-line options
getopts('h?k:K:m:ld', \%opts);

help() if exists $opts{'h'} or exists $opts{'?'};

if (exists $opts{'l'}) {
    print "Available -m methods: ", join (" ", sort keys %$how), "\n";
    exit;
}

# load user-supplied spared devices list(s)
$how->{$method}->{'extra'} .= $opts{'k'} if exists $opts{'k'};
$how->{$method}->{'evict'} .= $opts{'K'} if exists $opts{'K'};

$method = $opts{'m'} if exists $opts{'m'};

unless (exists $how->{$method}) {
    warn "No such method: $method\n";
    die "Available -m methods: ", join (" ", sort keys %$how), "\n";
}

$debug = 1 if exists $opts{'d'};

# read required files...
my $kconf_file = shift or help();
my $dmesg_file = shift;

# read in dmesg output, turn into lookup table
unless (defined $dmesg_file) {
    $dmesg_lookup = parse_dmesg(*STDIN, $how->{$method}->{'re-dmesg'});
} else {
    open DMESG, $dmesg_file or die "Problem opening $dmesg_file: $!\n";
    $dmesg_lookup = parse_dmesg(*DMESG, $how->{$method}->{'re-dmesg'});
    close DMESG;
}

#use Data::Dumper;
#warn Dumper $dmesg_lookup;

# walk arch-specific conf file, disabling devices not in lookup table
open CONF, $kconf_file or die "Problem opening $kconf_file: $!\n";
while (<CONF>) {
    chomp;
    my $zorch;

    my ($dev, $req) = /$how->{$method}->{'re-conf'}/;

    if ($dev) {
        unless (exists $dmesg_lookup->{$dev}) {
            warn "Disabling $dev (", $req || '-', ") as not in lookup table\n"
              if $debug;
            $zorch = 1;
        }

        if (defined $dmesg_lookup->{$dev} and $req) {
            unless ($dmesg_lookup->{$dev} =~ /\b$req\b/) {
                warn "Disabling $dev ($req) as no supporting requirement\n"
                  if $debug;
                $zorch = 1;
            }
        }

        if ($how->{$method}->{'extra'} =~ /\b$dev\b/) {
            warn "Extra match on $dev, not disabling\n" if $debug;
            $zorch = 0;
        }

        if ($how->{$method}->{'evict'}
            and $how->{$method}->{'evict'} =~ /\b$dev\b/) {
            warn "Forced to disable $dev!\n" if $debug;
            $zorch = 1;
        }

        $_ = $how->{$method}->{prefix} . $_ if $zorch;
    }

    print $_, "\n";
}
close CONF;

exit;

######################################################################
#
# SUBROUTINES

# extracts dmesg(8) device list into hash, returns reference of such
# expects filehandle containing dmesg output, and a regex to match
# entries with
sub parse_dmesg {
    my $fh    = shift;
    my $regex = shift;

    my $dmesg_lookup;

    while (<$fh>) {
        chomp;
        s/^\s+//;
        next if /^#/;
        s/\s+$//;
        next if /^$/;

        my ($dev, $req) = /$regex/;
        if ($dev) {
            $req = '' unless $req;
            $dmesg_lookup->{$dev} .= ' ' . $req;
        }
    }

    return $dmesg_lookup;
}

# takes a filename to open, and returns a stringified version of
# the file (as additonal device keys to match against).
# sub load_extras {
#     my $file = shift;
#     my $tmp;
# 
#     open FILE, $file or die "Couldn't load $file: $!\n";
#     while (<FILE>) {
#         chomp;
#         s/^\s+//;
#         next if /^#/;
#         s/\s+$//;
#         next if /^$/;
# 
#         $tmp .= ' ' . $_;
#     }
#     close FILE;
# 
#     return $tmp;
# }

# a generic help blarb
sub help {
    print <<"HELP";
Usage: $0 arch-conf-file [dmesg-file]

Kernel configuration unused device pruner.

Options for version $VERSION:
  -h/-?  Display this message

  -k en  List of extra devices not to disable, space or CSV.
  -K di  Devices to disable (done after enable check).
  
  -m xx  Use method xx for parsing files.  Defaults to output
         of uname(1) command.
  
  -l     List available methods and quit.

Run perldoc(1) on this script for additional documentation.

HELP
    exit;
}

######################################################################
#
# DOCUMENTATION

=head1 NAME

kernel-helper.pl - unused device pruner for *BSD kernel config

=head1 SYNOPSIS

Run the following to create the beginnings of a new kernel
configuration on OpenBSD.

  # cd /usr/src/sys/arch/`uname -m`/conf
  # dmesg | kernel-helper.pl GENERIC > MYCONFIG

=head1 DESCRIPTION

=head2 Overview

This script uses information extracted from dmesg(8) to comment out
lines in *BSD configuration files (FreeBSD and OpenBSD, at present). 
The lines disabled are those that do not reference any of the devices
detected at startup.  This allows one to easily build a kernel that
runs faster and takes up less memory (at the cost of flexibility).

One should always review the changes done to the configuration file by
this script, as something vital may have been disabled.  Lines changed
by this script are marked with C<#PRUNED#> by default.

For debugging purposes, you should also build a generic kernel from
the GENERIC configuration, in case of problems with the machine
running the customized kernel.

=head2 Normal Usage

  $ kernel-helper.pl [options] arch-conf-file [dmesg-file]

See L<"OPTIONS"> for details on the command line switches supported.

The I<arch-conf-file> is the kernel configuration file containing all
the device definitions for your system, whose location various by the
system and arch in question.

FreeBSD: C</usr/src/sys/`uname -m`/conf>

OpenBSD: C</usr/src/sys/arch/`uname -m`/conf>

I<dmesg-file> is optional, and if omitted the script will attempt to
read the dmesg information from STDIN.  This allows live dmesg output
to be fed into the script.

The resulting configuration file is printed to STDOUT.

=head1 OPTIONS

This script currently supports the following command line switches:

=over 4

=item B<-h>, B<-?>

Prints a brief usage note about the script.

=item B<-m> I<method>

Use I<method> instead of the default pull from uname(1).

=item B<-l>

List available methods and quit.

=item B<-k> I<devices>

List of I<devices> not to disable.  Should be the device name(s)
separated by whitespace or comma.

=item B<-K> I<baddevices>

List of I<baddevices> to be sure to disable.  This step is done after
the enable checkes.

=item B<-d>

Invoke debug mode.  Such output is done to STDERR.

=back

=head1 EXAMPLES

The B<-k> and B<-K> options can be used to fine-tune which devices you
do and do not want; the following enables USB, and the Hub and
Keyboard subdrivers on OpenBSD.

  # kernel-helper.pl -k "usb uhub ukbd" GENERIC /tmp/dmesg.out > MYCONFIG

Due to the custom prefix appended to changed lines in the resulting
configuration file (C<#PRUNED#> by default), undoing the changes is
easy, in the event you are applying an already customized config to
new or updated hardware.

  # perl -i.orig -ple 's/#PRUNED#//' MYCONFIG

See perlrun(1) for more information on the above command line
arguments to perl.

=head1 ENVIRONMENT

Expects to be run on FreeBSD or OpenBSD systems, which are detected
automatically via uname(1).  Otherwise, one can specify a method to
use manually.

=head1 BUGS

=head2 Reporting Bugs

Newer versions of this script may be available from:

http://sial.org/code/perl/

If the bug is in the latest version, send a report to the author.
Patches that fix problems or add new features are welcome.

=head2 Known Issues

The script tends to trash various devices that one may need; one
should always review whether the resulting config is actually viable. 
I use the script as a "close enough" starting point for kernel
configuration.

USB is notable for getting trampled on, unless you have all the
devices you will ever use plugged and detected at boot.

=head1 SEE ALSO

config(8), OpenBSD's options(4), perl(1)

Another set of perl scripts by Camiel Dobbelaar to do similar things
(found after writing this script) on OpenBSD:

http://www.sentia.org/downloads/tweakboot

http://www.sentia.org/downloads/tweakcompile

=head1 AUTHOR

Jeremy Mates, http://sial.org/contact/

=head1 COPYRIGHT

Copyright (c) 2001, Jeremy Mates.  This script is free software;
you can redistribute it and/or modify it under the same terms as Perl
itself.

=head1 VERSION

  $Id$

=cut

