2 # Tcl magic -*- tcl -*- \
4 ################################################################################
6 # KernelDriver - FreeBSD driver source installer
8 ################################################################################
11 # Michael Smith. All rights reserved.
13 # Redistribution and use in source and binary forms, with or without
14 # modification, are permitted provided that the following conditions
16 # 1. Redistributions of source code must retain the above copyright
17 # notice, this list of conditions and the following disclaimer.
18 # 2. Redistributions in binary form must reproduce the above copyright
19 # notice, this list of conditions and the following disclaimer in the
20 # documentation and/or other materials provided with the distribution.
21 # 3. Neither the name of the author nor the names of any co-contributors
22 # may be used to endorse or promote products derived from this software
23 # without specific prior written permission.
25 # THIS SOFTWARE IS PROVIDED BY Michael Smith AND CONTRIBUTORS ``AS IS'' AND
26 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28 # ARE DISCLAIMED. IN NO EVENT SHALL Michael Smith OR CONTRIBUTORS BE LIABLE
29 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
31 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
32 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
34 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
37 ################################################################################
39 # KernelDriver provides a means for installing source-form drivers into FreeBSD
40 # kernel source trees in an automated fashion. It can also remove drivers it
43 # Driver information is read from a control file, with the following syntax :
45 # description {<text>} Driver description; used in comments inserted into
47 # driver <name> The name of the driver. (Note that this can't end in .drvinfo :)
48 # filei386 <path> <name> The file <name> in the driver package is installed into
49 # <path> in the kernel source tree. Files whose names
50 # end in '.c' have an entry added to i386/conf/files.i386.
51 # fileconf <path> <name> The file <name> in the driver package is installed into
52 # <path> in the kernel source tree. Files whose names
53 # end in '.c' have an entry added to conf/files.
54 # optioni386 <name> <hdr> Adds an entry to i386/conf/options.i386, such that
55 # the option <name> will be placed in the header <hdr>.
56 # optionconf <name> <hdr> Adds an entry to conf/options, such that
57 # the option <name> will be placed in the header <hdr>.
58 # linttext Lines between this and a subsequent 'end' line are added
59 # to the LINT file to provide configuration examples,
61 # end Ends a text region.
63 # Possible additions :
65 # patch <name> Applies the patch contained in <name>; patch is invoked
66 # at the top level of the kernel source tree, and the
67 # patch must apply cleanly (this is checked).
69 # option <name> <file> Adds an entry to i386/conf/options.i386
71 # Lines beginning with '#' or blanks are considered comments, except in
74 ################################################################################
76 # $FreeBSD: src/tools/tools/kdrv/KernelDriver,v 1.4.2.1 2001/03/05 12:17:23 kris Exp $
77 # $DragonFly: src/tools/tools/kdrv/KernelDriver,v 1.2 2003/06/17 04:29:11 dillon Exp $
79 ################################################################################
81 ################################################################################
84 # Given (hint), use it to locate a driver information file.
85 # (Possible extension; support drivers in gzipped tarballs...)
87 proc findDrvFile_try {hint} {
89 # points to something already
90 if {[file exists $hint]} {
91 # unwind symbolic links
92 while {[file type $hint] == "link"} {
93 set hint [file readlink $hint];
95 switch [file type $hint] {
97 # run with it as it is
101 # look for a drvinfo file in the directory
102 set candidate [glob -nocomplain "$hint/*.drvinfo"];
103 switch [llength $candidate] {
111 error "multiple driver info files in directory : $hint";
116 error "driver info file may be a typewriter : $hint";
120 # maybe we need an extension
121 if {[file exists $hint.drvinfo]} {
122 return $hint.drvinfo;
124 error "can't find a driver info file using '$hint'";
127 proc findDrvFile {hint} {
129 set result [findDrvFile_try $hint];
133 set result [findDrvFile_try ${hint}.drvinfo];
137 error "can't find driver information file using : $hint";
140 ################################################################################
143 # Reads the contents of (fname), which are expected to be in the format
144 # described above, and fill in the global Drv array.
146 proc readDrvFile {fname} {
150 if {$Options(verbose)} {puts "+ read options from '$fname'";}
151 set fh [open $fname r];
154 set Drv(description) "";
156 set Drv(filesi386) "";
157 set Drv(filesconf) "";
158 set Drv(optionsi386) "";
159 set Drv(optionsconf) "";
161 set Drv(linttext) "";
163 while {[gets $fh line] >= 0} {
165 # blank lines/comments
166 if {([llength $line] == 0) ||
167 ([string index $line 0] == "\#")} {
171 # get keyword, process
172 switch -- [lindex $line 0] {
174 set Drv(description) [lindex $line 1];
177 set Drv(driver) [lindex $line 1];
180 set path [lindex $line 1];
181 set plast [expr [string length $path] -1];
182 if {[string index $path $plast] != "/"} {
185 set name [lindex $line 2];
186 set Drv(filei386:$name) $path;
187 lappend Drv(filesi386) $name;
190 set path [lindex $line 1];
191 set plast [expr [string length $path] -1];
192 if {[string index $path $plast] != "/"} {
195 set name [lindex $line 2];
196 set Drv(fileconf:$name) $path;
197 lappend Drv(filesconf) $name;
200 set opt [lindex $line 1];
201 set hdr [lindex $line 2];
202 lappend Drv(optionsi386) $opt;
203 set Drv(optioni386:$opt) $hdr;
206 set opt [lindex $line 1];
207 set hdr [lindex $line 2];
208 lappend Drv(optionsconf) $opt;
209 set Drv(optionconf:$opt) $hdr;
212 lappend Drv(patches) [lindex $line 1];
215 while {[gets $fh line] >= 0} {
216 if {$line == "end"} {
219 lappend Drv(linttext) $line;
225 if {$Options(verbose)} {
230 ################################################################################
233 # With the global Drv filled in, check that the files required are all in
234 # (dir), and that the kernel config at (kpath) can be written.
236 proc validateDrvPackage {dir kpath} {
240 if {$Options(verbose)} {puts "+ checking driver package...";}
244 # check files, patches
245 foreach f $Drv(filesi386) {
246 if {![file readable $dir$f]} {
250 foreach f $Drv(filesconf) {
251 if {![file readable $dir$f]} {
255 foreach f $Drv(patches) {
256 if {![file readable $dir$f]} {
260 if {$missing != ""} {
261 error "missing files : $missing";
265 if {$Options(verbose)} {puts "+ checking kernel source writability...";}
266 foreach f $Drv(filesi386) {
267 set p $Drv(filei386:$f);
268 if {![file isdirectory $kpath$p]} {
271 if {![file writable $kpath$p]} {
272 if {[lsearch -exact $unwritable $p] == -1} {
273 lappend unwritable $p;
278 foreach f $Drv(filesconf) {
279 set p $Drv(fileconf:$f);
280 if {![file isdirectory $kpath$p]} {
283 if {![file writable $kpath$p]} {
284 if {[lsearch -exact $unwritable $p] == -1} {
285 lappend unwritable $p;
292 "i386/conf/files.i386" \
293 "i386/conf/options.i386" \
295 if {![file writable $kpath$f]} {
296 lappend unwritable $f;
299 if {$missing != ""} {
300 error "missing directories : $missing";
302 if {$unwritable != ""} {
303 error "can't write to : $unwritable";
307 ################################################################################
310 # Install the files listed in the global Drv into (kpath) from (dir)
312 proc installDrvFiles {dir kpath} {
316 # clear 'installed' record
317 set Drv(installedi386) "";
318 set Drv(installedconf) "";
321 if {$Options(verbose)} {puts "+ installing driver files...";}
322 foreach f $Drv(filesi386) {
323 if {$Options(verbose)} {puts "$f -> $kpath$Drv(filei386:$f)";}
324 if {$Options(real)} {
325 if {[catch {exec cp $dir$f $kpath$Drv(filei386:$f)} msg]} {
328 lappend Drv(installedi386) $f;
332 foreach f $Drv(filesconf) {
333 if {$Options(verbose)} {puts "$f -> $kpath$Drv(fileconf:$f)";}
334 if {$Options(real)} {
335 if {[catch {exec cp $dir$f $kpath$Drv(fileconf:$f)} msg]} {
338 lappend Drv(installedconf) $f;
343 error "failed to install files : $failed";
347 ################################################################################
350 # Remove files from a failed installation in (kpath)
352 proc backoutDrvChanges {kpath} {
356 if {$Options(verbose)} {puts "+ backing out installed files...";}
357 # delete installed files
358 foreach f $Drv(installedi386) {
359 exec rm -f $kpath$Drv(filei386:$f)$f;
361 foreach f $Drv(installedconf) {
362 exec rm -f $kpath$Drv(fileconf:$f)$f;
366 ################################################################################
369 # Adds an entry to i386/conf/files.i386 and conf/files for the .c files in the driver.
370 # (kpath) points to the kernel.
372 # A comment is added to the file preceding the new entries :
374 # ## driver: <drivername>
376 # # filei386: <path><file>
377 # <file spec (.c files only)>
380 # We only append to the end of the file.
382 # Add linttext to the LINT file.
383 # Add options to i386/conf/options.i386 if any are specified
385 proc registerDrvFiles {kpath} {
389 if {$Options(verbose)} {puts "+ registering installed files...";}
392 if {$Drv(linttext) != ""} {
394 if {$Options(verbose)} {puts "+ updating LINT...";}
395 if {$Options(real)} {
396 set fname [format "%si386/conf/LINT" $kpath];
397 set fh [open $fname a];
400 puts $fh "\#\# driver: $Drv(driver)";
401 puts $fh "\# $Drv(description)";
402 foreach l $Drv(linttext) {
405 puts $fh "\#\# enddriver";
411 if {$Options(real)} {
412 set fname [format "%si386/conf/files.i386" $kpath];
413 set fh [open $fname a];
416 puts $fh "\#\# driver: $Drv(driver)";
417 puts $fh "\# $Drv(description)";
419 foreach f $Drv(filesi386) {
420 puts $fh "\# file: $Drv(filei386:$f)$f";
421 # is it a compilable object?
422 if {[string match "*.c" $f]} {
423 puts $fh "$Drv(filei386:$f)$f\t\toptional\t$Drv(driver)\tdevice-driver";
426 puts $fh "\#\# enddriver";
429 if {$Drv(optionsi386) != ""} {
430 if {$Options(verbose)} {puts "+ adding options...";}
431 if {$Options(real)} {
432 set fname [format "%si386/conf/options.i386" $kpath];
433 set fh [open $fname a];
436 puts $fh "\#\# driver: $Drv(driver)";
437 puts $fh "\# $Drv(description)";
439 foreach opt $Drv(optionsi386) {
440 puts $fh "$opt\t$Drv(optioni386:$opt)";
442 puts $fh "\#\# enddriver";
448 if {$Options(real)} {
449 set fname [format "%sconf/files" $kpath];
450 set fh [open $fname a];
453 puts $fh "\#\# driver: $Drv(driver)";
454 puts $fh "\# $Drv(description)";
456 foreach f $Drv(filesconf) {
457 puts $fh "\# file: $Drv(fileconf:$f)$f";
458 # is it a compilable object?
459 if {[string match "*.c" $f]} {
460 puts $fh "$Drv(fileconf:$f)$f\t\toptional\t$Drv(driver)\tdevice-driver";
463 puts $fh "\#\# enddriver";
466 if {$Drv(optionsconf) != ""} {
467 if {$Options(verbose)} {puts "+ adding options...";}
468 if {$Options(real)} {
469 set fname [format "%sconf/options" $kpath];
470 set fh [open $fname a];
473 puts $fh "\#\# driver: $Drv(driver)";
474 puts $fh "\# $Drv(description)";
476 foreach opt $Drv(optionsconf) {
477 puts $fh "$opt\t$Drv(optionconf:$opt)";
479 puts $fh "\#\# enddriver";
486 ################################################################################
489 # List all drivers recorded as installed, in the kernel at (kpath)
491 # XXX : fix me so I understand conf/{options,files} stuff!
492 proc listInstalledDrv {kpath} {
496 # pick up all the i386 options information first
497 set fname [format "%si386/conf/options.i386" $kpath];
498 if {![file readable $fname]} {
499 error "not a kernel directory";
501 set fh [open $fname r];
503 while {[gets $fh line] >= 0} {
506 if {[scan $line "\#\# driver: %s" driver] == 1} {
507 # read driver details, ignore
509 # loop reading option details
510 while {[gets $fh line] >= 0} {
512 if {$line == "\#\# enddriver"} {
515 # parse option/header tuple
516 if {[scan $line "%s %s" opt hdr] == 2} {
517 # remember that this driver uses this option
518 lappend drivers($driver:optionsi386) $opt;
519 # remember that this option goes in this header
520 set optionsi386($opt) $hdr;
527 # pick up all the conf options information first
528 set fname [format "%sconf/options" $kpath];
529 if {![file readable $fname]} {
530 error "not a kernel directory";
532 set fh [open $fname r];
534 while {[gets $fh line] >= 0} {
537 if {[scan $line "\#\# driver: %s" driver] == 1} {
538 # read driver details, ignore
540 # loop reading option details
541 while {[gets $fh line] >= 0} {
543 if {$line == "\#\# enddriver"} {
546 # parse option/header tuple
547 if {[scan $line "%s %s" opt hdr] == 2} {
548 # remember that this driver uses this option
549 lappend drivers($driver:optionsconf) $opt;
550 # remember that this option goes in this header
551 set optionsconf($opt) $hdr;
558 set fname [format "%si386/conf/files.i386" $kpath];
559 set fh [open $fname r];
561 while {[gets $fh line] >= 0} {
564 if {[scan $line "\#\# driver: %s" driver] == 1} {
565 # clear global and reset
567 set Drv(driver) $driver;
568 # read driver details
570 set Drv(description) [string range $line 2 end];
571 set Drv(filesi386) "";
573 if {[info exists drivers($Drv(driver):optionsi386)]} {
574 set Drv(optionsi386) $drivers($Drv(driver):optionsi386);
576 foreach opt $Drv(optionsi386) {
577 set Drv(optioni386:$opt) $optionsi386($opt);
580 # loop reading file details
581 while {[gets $fh line] >= 0} {
582 if {$line == "\#\# enddriver"} {
583 # print this driver and loop
587 if {[scan $line "\# filei386: %s" fpath] == 1} {
588 set f [file tail $fpath];
589 set Drv(filei386:$f) "[file dirname $fpath]/";
590 lappend Drv(filesi386) $f;
597 set fname [format "%sconf/files" $kpath];
598 set fh [open $fname r];
600 while {[gets $fh line] >= 0} {
603 if {[scan $line "\#\# driver: %s" driver] == 1} {
604 # clear global and reset
606 set Drv(driver) $driver;
607 # read driver details
609 set Drv(description) [string range $line 2 end];
610 set Drv(filesconf) "";
612 if {[info exists drivers($Drv(driver):optionsconf)]} {
613 set Drv(optionsconf) $drivers($Drv(driver):optionsconf);
615 foreach opt $Drv(optionsconf) {
616 set Drv(optionconf:$opt) $optionsconf($opt);
619 # loop reading file details
620 while {[gets $fh line] >= 0} {
621 if {$line == "\#\# enddriver"} {
622 # print this driver and loop
626 if {[scan $line "\# fileconf: %s" fpath] == 1} {
627 set f [file tail $fpath];
628 set Drv(fileconf:$f) "[file dirname $fpath]/";
629 lappend Drv(filesconf) $f;
637 ################################################################################
640 # Print the contents of the global Drv.
646 puts "$Drv(driver) : $Drv(description)";
647 if {$Options(verbose)} {
648 foreach f $Drv(filesi386) {
649 puts " $Drv(filei386:$f)$f"
651 foreach f $Drv(filesconf) {
652 puts " $Drv(fileconf:$f)$f"
654 if {[info exists Drv(optionsi386)]} {
655 foreach opt $Drv(optionsi386) {
656 puts " $opt in $Drv(optioni386:$opt)";
659 if {[info exists Drv(optionsconf)]} {
660 foreach opt $Drv(optionsconf) {
661 puts " $opt in $Drv(optionconf:$opt)";
667 ################################################################################
670 # Given a kernel tree at (kpath), get driver details about an installed
674 proc findInstalledDrvi386 {drvname kpath} {
678 set fname [format "%si386/conf/files.i386" $kpath];
679 set fh [open $fname r];
681 puts "checking i386/conf/files.i386";
683 while {[gets $fh line] >= 0} {
684 if {[scan $line "\#\# driver: %s" name] == 1} {
685 if {$name != $drvname} {
689 set Drv(driver) $drvname;
691 set Drv(description) [string range $line 2 end];
692 set Drv(filesi386) "";
693 # loop reading file details
694 while {[gets $fh line] >= 0} {
695 if {$line == "\#\# enddriver"} {
699 if {[scan $line "\# file: %s" fpath] == 1} {
700 set f [file tail $fpath];
701 set Drv(filei386:$f) "[file dirname $fpath]/";
702 lappend Drv(filesi386) $f;
706 error "unexpected EOF reading '$fname'";
714 proc findInstalledDrvconf {drvname kpath} {
718 set fname [format "%sconf/files" $kpath];
719 set fh [open $fname r];
721 puts "checking conf/files";
723 while {[gets $fh line] >= 0} {
724 if {[scan $line "\#\# driver: %s" name] == 1} {
725 if {$name != $drvname} {
729 set Drv(driver) $drvname;
731 set Drv(description) [string range $line 2 end];
732 set Drv(filesconf) "";
733 # loop reading file details
734 while {[gets $fh line] >= 0} {
735 if {$line == "\#\# enddriver"} {
739 if {[scan $line "\# file: %s" fpath] == 1} {
740 set f [file tail $fpath];
741 set Drv(fileconf:$f) "[file dirname $fpath]/";
742 lappend Drv(filesconf) $f;
746 error "unexpected EOF reading '$fname'";
754 proc findInstalledDrv {drvname kpath} {
758 if {$Options(verbose)} {puts "+ look for driver '$drvname' in '$kpath'";}
760 # Whoops... won't work in a single if statement due to expression shortcircuiting
761 set a [findInstalledDrvi386 $drvname $kpath];
762 set b [findInstalledDrvconf $drvname $kpath];
767 error "driver '$drvname' not recorded as installed";
770 ################################################################################
773 # Verify that we can remove the driver described in the global Drv installed
776 proc validateDrvRemoval {kpath} {
783 if {$Options(verbose)} {puts "+ checking for removabilty...";}
787 "i386/conf/files.i386" \
788 "i386/conf/options.i386" \
792 if {![file exists $kpath$f]} {
793 lappend missing $kpath$f;
795 if {![file writable $kpath$f]} {
796 lappend unwritable $f;
801 foreach f $Drv(filesi386) {
802 set p $Drv(filei386:$f);
803 if {![file isdirectory $kpath$p]} {
806 if {![file writable $kpath$p]} {
807 if {[lsearch -exact $unwritable $p] == -1} {
808 lappend unwritable $p;
813 foreach f $Drv(filesconf) {
814 set p $Drv(fileconf:$f);
815 if {![file isdirectory $kpath$p]} {
818 if {![file writable $kpath$p]} {
819 if {[lsearch -exact $unwritable $p] == -1} {
820 lappend unwritable $p;
825 if {$missing != ""} {
826 error "files/directories missing : $missing";
828 if {$unwritable != ""} {
829 error "can't write to : $unwritable";
833 ################################################################################
836 # Delete the files belonging to the driver devfined in the global Drv in
837 # the kernel tree at (kpath)
839 proc deleteDrvFiles {kpath} {
843 if {$Options(verbose)} {puts "+ delete driver files...";}
845 # loop deleting files
846 foreach f $Drv(filesi386) {
847 if {$Options(verbose)} {puts "- $Drv(filei386:$f)$f";}
848 if {$Options(real)} {
849 exec rm $kpath$Drv(filei386:$f)$f;
852 foreach f $Drv(filesconf) {
853 if {$Options(verbose)} {puts "- $Drv(fileconf:$f)$f";}
854 if {$Options(real)} {
855 exec rm $kpath$Drv(fileconf:$f)$f;
860 ################################################################################
863 # Remove any mention of the current driver from the files.i386 and LINT
866 proc unregisterDrvFiles {ksrc} {
870 if {$Options(verbose)} {puts "+ deregister driver files...";}
872 # don't really do it?
873 if {!$Options(real)} { return ; }
876 "i386/conf/files.i386" \
877 "i386/conf/options.i386" \
881 set ifh [open $ksrc$f r];
882 set ofh [open $ksrc$f.new w];
885 while {[gets $ifh line] >= 0} {
887 if {[scan $line "\#\# driver: %s" name] == 1} {
888 if {$name == $Drv(driver)} {
889 set copying 0; # don't copy this one
893 puts $ofh $line; # copy through
895 if {$line == "\#\# enddriver"} { # end of driver detail
901 exec mv $ksrc$f.new $ksrc$f; # move new over old
905 ################################################################################
908 # Remind the user what goes where
914 set progname [file tail $argv0];
916 puts stderr "Usage is :";
917 puts stderr " $progname \[-v -n\] add <drvinfo> \[<kpath>\]";
918 puts stderr " $progname \[-v -n\] delete <drvname> \[<kpath>\]";
919 puts stderr " $progname \[-v\] list \[<kpath>\]";
920 puts stderr " <drvinfo> is a driver info file";
921 puts stderr " <drvname> is a driver name";
922 puts stderr " <kpath> is the path to the kernel source (default /sys/)";
923 puts stderr " -v be verbose";
924 puts stderr " -n don't actually do anything";
928 ################################################################################
931 # Parse commandline options, return anything that doesn't look like an option
938 set Options(verbose) 0;
941 for {set index 0} {$index < [llength $argv]} {incr index} {
943 switch -- [lindex $argv $index] {
946 set Options(real) 0; # 'do-nothing' mode
949 set Options(verbose) 1; # brag
952 lappend ret [lindex $argv $index];
959 ################################################################################
962 # Given (hint), return the kernel path. If (hint) is empty, return /sys.
963 # If the kernel path is not a directory, complain and dump the usage.
965 proc getKpath {hint} {
969 # check the kernel path
975 if {![file isdirectory $kpath]} {
976 puts "not a directory : $kpath";
979 set plast [expr [string length $kpath] -1];
980 if {[string index $kpath $plast] != "/"} {
986 ################################################################################
989 # Start somewhere here.
995 # Work out what we're trying to do
996 set cmdline [getOptions];
997 set mode [lindex $cmdline 0];
1002 set hint [lindex $cmdline 1];
1003 set kpath [getKpath [lindex $cmdline 2]];
1005 # check driver file argument
1006 if {[catch {set drv [findDrvFile $hint]} msg]} {
1010 if {([file type $drv] != "file") ||
1011 ![file readable $drv]} {
1012 puts "can't read driver file : $drv";
1015 set drvdir "[file dirname $drv]/";
1018 if {[catch {readDrvFile $drv} msg]} {
1023 if {[catch {validateDrvPackage $drvdir $kpath} msg]} {
1028 if {[catch {installDrvFiles $drvdir $kpath} msg]} {
1029 backoutDrvChanges $kpath; # oops, unwind
1033 # register files in config
1034 if {[catch {registerDrvFiles $kpath} msg]} {
1035 backoutDrvChanges $kpath; # oops, unwind
1041 set drv [lindex $cmdline 1];
1042 set kpath [getKpath [lindex $cmdline 2]];
1044 if {[string last ".drvinfo" $drv] != -1} {
1045 set drv [string range $drv 0 [expr [string length $drv] - 9]];
1046 puts "Driver name ends in .drvinfo, removing, is now $drv";
1049 if {[catch {findInstalledDrv $drv $kpath} msg]} {
1053 if {[catch {validateDrvRemoval $kpath} msg]} {
1057 if {[catch {unregisterDrvFiles $kpath} msg]} {
1061 if {[catch {deleteDrvFiles $kpath} msg]} {
1067 set kpath [getKpath [lindex $cmdline 1]];
1068 if {[catch {listInstalledDrv $kpath} msg]} {
1069 puts stderr "can't list drivers in '$kpath' : $msg";
1073 puts stderr "unknown command '$mode'";
1081 ################################################################################