#!/usr/bin/perl # # Samba Printing Packages - Construction Utility # # Copyright (C) 2000 VA Linux Systems, Inc. All rights reserved. # # This utility takes a vendor's printer driver and turns it into a nice form # for installation into a Samba server. use strict; use FindBin; use lib "$FindBin::RealBin/../lib/perl5"; use Carp; use Getopt::Long; use File::Basename; use File::Copy; use File::Path; use POSIX qw(tmpnam); use INF qw(read_inf inf_get_key inf_get_keys inf_has_key); use inf_nt qw(parse_inf_nt get_models); use MSExpand; sub usage { print "usage: $0 SPEC ...\n"; print " --name=NAME Name of the package\n"; print " --version=VERSION Version of the package\n"; print " --display-name=DN Pretty name for GUI tools, etc.\n"; print " SPEC Either a work directory or archive.\n"; print " --arch=ARCH Override architecture: win95, win98, nt, W32X86,\n"; print " nt-mips, mips, W32mips, nt-alpha, alpha, W32alpha,\n"; print " nt-ppc, ppc, W32ppc\n"; print " --inf=INF_FILENAME Override name of INF file\n"; print " -m MANUFACTURER Override manufacturer name in INF\n"; print " --model=MODEL Override model name in devices section\n"; print " --debug Display debugging messages.\n"; print "\n"; print "hypothetical example:\n"; print "$0 --name=\"hp4050\" --version=0.01\n"; print " --display-name=\"HP LaserJet 4050\" /tmp/pkg-work-dir\n"; exit(1); } # Convert an architecture name to a subdirectory name. sub arch2subdir { croak("arch2subdir(ARCH)") if @_ != 1; my($arch) = @_; $arch =~ tr/A-Z/a-z/; if ($arch eq 'nt' or $arch eq 'w32x86') { return "W32X86"; } elsif ($arch eq 'win95' or $arch eq 'win98'or $arch eq 'win40') { return "WIN40"; } elsif ($arch eq 'nt-mips' or $arch eq 'mips' or $arch eq 'w32mips') { return "W32mips"; } elsif ($arch eq 'nt-alpha' or $arch eq 'alpha' or $arch eq 'w32alpha') { return "W32alpha"; } elsif ($arch eq 'nt-ppc' or $arch eq 'ppc' or $arch eq 'w32ppc') { return "W32ppc"; } else { return undef; } } # Convert a short architecture name to a long name. sub arch2longname { croak("arch2longname(ARCH)") if @_ != 1; my($arch) = @_; $arch =~ tr/A-Z/a-z/; if ($arch eq 'w32x86') { return "Windows NT X86"; } elsif ($arch eq 'win40') { return "Windows 95/98"; } elsif ($arch eq 'w32alpha') { return "Windows NT Alpha"; } elsif ($arch eq 'w32mips') { return "Windows NT MIPS"; } elsif ($arch eq 'w32ppc') { return "Windows NT PowerPC"; } else { return undef; } } # Return the contents of a directory as an array. sub get_files_in_dir { croak("get_files_in_dir(DIR)") if @_ != 1; my($dir) = @_; my @files = (); local(*DIR); if (opendir(DIR, $dir)) { my $fname; while (defined($fname = readdir(DIR))) { next if $fname eq '.' or $fname eq '..'; push(@files, $fname); } closedir(DIR); } return @files; } # Scan an array of filenames for a filename matching the passed filename # while ignoring case. (Solves problem with case-sensitivity on UNIX.) sub get_real_filename { croak("get_real_filename(FILENAME,FILE1,FILE2,...)") if @_ <= 1; my $lookup_fname = shift @_; # Make the filename we are searching for be in lower-case. $lookup_fname =~ tr/A-Z/a-z/; foreach my $fname (@_) { my $lc_fname = $fname; $lc_fname =~ tr/A-Z/a-z/; return $fname if $lookup_fname eq $lc_fname; } return undef; } # Create a temporary directory and extract all files in the provided ZIP # file into the directory. The subdirectory structure of the ZIP file is # not replicated. Return the temporary directory. sub unpack_zip { croak("unpack_zip(ZIP_FILENAME)") if @_ != 1; my($zip_fname) = @_; # Create the temporary directory. my $tempdir = tmpnam(); if (! mkdir($tempdir, 0700)) { print STDERR "$0: unable to make temporary subdirectory: $!\n"; return undef; } # Use the unzip utility to do all the hard work. Hope it's in the PATH ... system("unzip -qq -o -j $zip_fname -d $tempdir"); return $tempdir; } # Return the passed filename with any extension replaced with a underscore. sub add_trailing_underscore { croak("add_trailing_underscore(FILENAME)") if @_ != 1; my($fname) = @_; my($name, $path, $suffix) = fileparse($fname, '\..*'); substr($suffix, length($suffix) - 1, 1) = "_"; return $name . $suffix; } my($pkg_name, $pkg_version, $display_name, $default_arch, $default_inf_fname, $default_manufacturer, $default_model, @SPECS, $debug); # All command line arguments that did not fit into # getopt (that is normally the driver.exe, driver.zip or # the driver directory are the args of this function sub process { my($arg) = @_; my $hash = { 'location' => $arg, }; push(@SPECS, $hash); } Getopt::Long::Configure("bundling"); Getopt::Long::Configure("permute"); GetOptions("name|n=s" => \$pkg_name, "version|v=s" => \$pkg_version, "display-name|N=s" => \$display_name, "inf-fname|inf|i=s" => \$default_inf_fname, "architecture|arch|a=s" => \$default_arch, "manufacturer|m=s" => \$default_manufacturer, "model|d=s" => \$default_model, "debug" => \$debug, "<>" => \&process); usage() unless $pkg_name ne '' and $pkg_version ne '' and $display_name ne ''; # Some debugging print "With the command line options you have configured this:\n"; print "pkg_name = \"$pkg_name\"\n"; print "pkg_version = \"$pkg_version\"\n"; print "display_name = \"$display_name\"\n"; print "default_inf_fname = \"$default_inf_fname\"\n"; print "default_arch = \"$default_arch\"\n"; print "default_manufacturer = \"$default_manufacturer\"\n"; print "default_model = \"$default_model\"\n"; print "debug = \"$debug\"\n\n"; # Complain if no driver spec arguments were given. if (scalar(@SPECS) == 0) { print STDERR "$0: no driver specifications found\n"; usage(); } # insert the default parameter here foreach my $hash (@SPECS) { if ($default_inf_fname ne '') { $hash->{'inf_fname'} = $default_inf_fname; $default_inf_fname = ""; } if ($default_arch ne '') { $hash->{'arch'} = $default_arch; $default_arch = ""; } if ($default_manufacturer ne '') { $hash->{'manufacturer'} = $default_manufacturer; $default_manufacturer = ""; } if ($default_model ne '') { $hash->{'model'} = $default_model; $default_model = ""; } } # Some Debugging foreach my $schluessel (@SPECS) { print "$schluessel = \%SPECS{$schluessel}\n"; } # Create and move into a subdirectory to use to build the package. my $pkgdir = "/tmp/printpkg-$$-${pkg_name}-$pkg_version"; if (! mkdir($pkgdir, 0755)) { print STDERR "$0: unable to make temporary subdirectory: $!\n"; exit(1); } # Install an exit handler to remove the package directory when done. my @byebye_dirs = ($pkgdir); END { foreach my $path (@byebye_dirs) { rmtree($path, 0, 0); } } # Build the first part of control file as just text in a variable for now. my $control_text = "[common]\n"; $control_text .= "name=${pkg_name}\n"; $control_text .= "version=${pkg_version}\n"; $control_text .= "display_name=\"${display_name}\"\n"; # Build the list of files for the package. my @pkg_files = ("control"); # Announce the package that is being built. print "Package: ${pkg_name}-${pkg_version}.tar.gz\n"; print "Display Name: ${display_name}\n"; # Loop for each driver spec provided by the user. foreach my $spec (@SPECS) { my($workdir, $section, $inf_fname, $real_inf_fname, $manufacturer, $model); # Print a blank line to seperate from header and other archs. print "\n"; # The first argument for a driver should either be a work directory # with all of the files unpacked or an archive file that we should # unpack. if (-d $spec->{location}) { # User has given us a directory. Use the value as is. $workdir = $spec->{location}; print "Location: $workdir\n"; } elsif (-f $spec->{location}) { # User has given us a file. Check to see what type of archive it is. if ($spec->{location} =~ /\.zip$/i or $spec->{location} =~ /\.exe$/i) { $workdir = unpack_zip($spec->{location}); if (not defined $workdir) { print "ERROR: Unable to unpack archive, skipping ...\n"; next; } # Add this directory to list of directories to be removed. push(@byebye_dirs, $workdir); print "Using archive $spec->{location} for files.\n"; } else { print "ERROR: Unsupported extension for archive, skipping...\n"; next; } } else { print "ERROR: You must specify a work directory or archive.\n"; exit(1); } # Scan the work directory for a list of all the files. my @workdir_files = get_files_in_dir($workdir); # Now attempt to infer the name of the .INF file. if ($spec->{inf_fname} ne '') { $inf_fname = $spec->{inf_fname}; $real_inf_fname = get_real_filename($inf_fname, @workdir_files); } else { my @inf_files = grep { /\.inf/i } @workdir_files; if (scalar(@inf_files) == 1) { $inf_fname = $inf_files[0]; $real_inf_fname = $inf_files[0]; print "Using file $real_inf_fname for INF information.\n"; } elsif (scalar(@inf_files) == 0) { print "ERROR: No INF file found in the provided files.\n"; exit(1); } else { print "ERROR: Unable to infer which INF file to use.\n"; print "ERROR: Use the -i option with one of the following files:\n"; foreach my $x (@inf_files) { print " $x\n"; } exit(1); } } # Parse the .INF file provided. my $inf = read_inf("$workdir/$real_inf_fname"); if (not defined $inf) { print "ERROR: Unable to read INF file `$real_inf_fname', skipping ...\n"; next; } # Infer the target architecture from the INF signature or use the # one provided on the command-line. if ($spec->{arch} ne '') { $section = arch2subdir($spec->{arch}); if (not defined $section) { print "ERROR: Unknown architecture name `$spec->{arch}'.\n\n"; usage(); } } elsif (inf_get_keys($inf, "Version", "Signature") =~ /^\$Windows NT\$$/i) { $section = 'W32X86'; } elsif (inf_get_keys($inf, "Version", "Signature") =~ /^\$CHICAGO\$$/i) { $section = 'WIN40'; } else { print "ERROR: Unable to infer target architecture, use --arch option to specify one.\n"; exit(1); } # Announce the architecture being used. print "Architecture: ", arch2longname($section); print " (detected from INF)" if $spec->{arch} eq ''; print "\n"; # Check to see if the INF file's signature matches the architecture. if ($section eq 'W32X86' or $section eq 'W32alpha' or $section eq 'W32mips' or $section eq 'W32ppc') { if (inf_get_keys($inf, "Version", "Signature") !~ /^\$Windows NT\$$/i) { print "WARNING: INF signature is not a Windows NT signature.\n"; } } elsif ($section eq 'WIN40') { if (inf_get_keys($inf, "Version", "Signature") !~ /^\$CHICAGO\$$/i) { print "WARNING: INF signature is not a Windows 95/98 signature.\n"; } } # Determine the manufacturer string to use. if ($spec->{manufacturer} ne '') { $manufacturer = $spec->{manufacturer}; } else { if (inf_has_key($inf, "Manufacturer")) { my @keys = keys %{inf_get_key($inf, "Manufacturer")}; if (scalar(@keys) == 1) { $manufacturer = $keys[0]; } elsif (scalar(@keys) == 0) { print "ERROR: INF file had no manufacturers listed.\n"; exit(1); } else { print "ERROR: Unable to infer which manufacturer to use.\n"; print "ERROR: Use the -m option to specify one of the following:\n"; foreach my $x (@keys) { print " \"$x\"\n"; } exit(1); } } else { print "ERROR: INF file does not have a [Manufacturer] section.\n"; exit(1); } } # Announce the manufacturer being used. print "Manufacturer: $manufacturer"; print " (detected from INF)" if $spec->{manufacturer} eq ''; print "\n"; # Determine the model string to use. if ($spec->{model} ne '') { $model = $spec->{model}; } else { my @models = get_models($inf, $manufacturer); if (scalar(@models) == 1) { $model = $models[0]; } elsif (scalar(@models) == 0) { print "ERROR: INF file had no models listed.\n"; exit(1); } else { print "ERROR: Unable to infer which model to use.\n"; print "ERROR: Use the -d option to specify one of the following:.\n"; foreach my $x (@models) { print " \"$x\"\n"; } exit(1); } } # Announce the model being used. print "Model: $model"; print " (detected from INF)" if $spec->{model} eq ''; print "\n"; # Parse the driver .INF file into magical Perl data structure. my %info = parse_inf_nt($inf, $manufacturer, $model, $section); exit(1) unless defined %info; # Create a subdirectory in the package toplevel for this architecture. if (! mkdir("$pkgdir/$section", 0755)) { print "ERROR: Unable to create subdirectory in package ($!).\n"; exit(1); } # Copy the .INF file to the architecture subdirectory. if (! copy("$workdir/$real_inf_fname", "$pkgdir/$section/")) { print "ERROR: Unable to copy $workdir/$real_inf_fname to package ($!)\n"; exit(1); } # Copy each of the destination files to the architecture subdirectory. foreach my $filehash (@{$info{CopyFiles}}) { my $dst_fname = $filehash->{DstFilename}; my $src_fname = $filehash->{SrcFilename}; my $real_dst_fname = get_real_filename($dst_fname, @workdir_files); my $real_src_fname = get_real_filename($src_fname, @workdir_files); my $file_action = 0; # 0 - copy, 1 - expand my $action_src = ""; # source file to expand or copy from my $action_dst = ""; # destination directory for copy or expanded file # Output a status message for the user. print ">>> file - `$src_fname' -> `$dst_fname'\n" if $debug; # Test for the existence of the destination filename in # the work directory. if (-f "$workdir/$real_dst_fname") { # Copy the destination file to the architecture subdirectory. $file_action = 0; $action_src = "$workdir/$real_dst_fname"; $action_dst = "$pkgdir/$section/"; } elsif (-f "$workdir/$real_src_fname") { # Check to see if the source file is compressed. if ($real_src_fname =~ /_$/) { $file_action = 1; $action_src = "$workdir/$real_src_fname"; $action_dst = "$pkgdir/$section/$dst_fname"; } else { $file_action = 0; $action_src = "$workdir/$real_src_fname"; $action_dst = "$pkgdir/$section/$dst_fname"; print "WARNING: Copying source file `$src_fname' to `$dst_fname'.\n"; } } else { # Check to see if there is a source filename with a trailing _ # which signifes compression. Some INF files have the source # filename specified without the _ in it. Icky spoo-tang. my $u_src_name = get_real_filename(add_trailing_underscore($src_fname), @workdir_files); my $u_dst_name = get_real_filename(add_trailing_underscore($dst_fname), @workdir_files); if (-f "$workdir/$u_dst_name") { $file_action = 1; $action_src = "$workdir/$u_dst_name"; $action_dst = "$pkgdir/$section/$dst_fname"; } elsif (-f "$workdir/$u_src_name") { $file_action = 1; $action_src = "$workdir/$u_src_name"; $action_dst = "$pkgdir/$section/$dst_fname"; } else { print "ERROR: Unable to determine what to do with this file:\n"; print "ERROR: Source filename is `$src_fname'.\n"; print "ERROR: Destination filename is `$dst_fname'.\n"; exit(1); } } # Implement whatever action identified above. if ($file_action == 0) { # Copy file to package. print ">>> Copying `$action_src' to `$action_dst'.\n" if $debug; if (! copy($action_src, $action_dst)) { print "ERROR: Unable to copy `$action_src' to package.\n"; exit(1); } } elsif ($file_action == 1) { # Expand the source file into the package. print ">>> Expanding `$action_src' to `$action_dst'.\n" if $debug; if (! ms_expand($action_src, $action_dst)) { print "ERROR: Unable to expand `$action_src' to package.\n"; exit(1); } } else { print "INTERNAL ERROR: Unknown file action given.\n"; exit(1); } } # Add a section to the control file for this architecture. $control_text .= "\n"; $control_text .= "[$section]\n"; $control_text .= "inf_fname=${inf_fname}\n"; $control_text .= "manufacturer=\"$manufacturer\"\n"; $control_text .= "model=\"$model\"\n"; # Add list of files for package. push(@pkg_files, "$section/*"); } # Write the package's control file. if (! open(CONTROL, ">$pkgdir/control")) { print "ERROR: Unable to create control file in package ($!).\n"; exit(1); } print CONTROL $control_text; close(CONTROL); # Create the the package file. system("( cd $pkgdir/ ; tar cf - " . join(' ', @pkg_files) . " ) | gzip -9 > ${pkg_name}-${pkg_version}.tar.gz"); print "\nPackage created successfully as ${pkg_name}-${pkg_version}.tar.gz\n";