Brother HL-L2375 printer and NixOS
I finally got a new printer. After my lexmark CX510 printer had issue after issue with printing (paper jam, no paper alarm etc.) I decided it is time for a new printer.
So which one should I buy? I was quite happy with the linux support for my lexmark printer, but not with the printer itself. I had an epson and brother printer before and they worked fine but in the end I was sorting my candidates by price and availability. The brother HL-L2375 won. On paper these are the features:
- print speeds of up to 34 pages per minute
- Automatic 2-sided print
- 250 sheet paper Input
- Built in wired and wireless connectivity
- Up to 1,200 page* inbox toner
- 64MB memory
So, an OK printer (IMHO). There are linux drivers on the homepage and someone on amazon actually said it worked out of the box without further printer driver install. Yeah..no
Anyway, I was hoping so and bought it. It obviously did not work out of the box. I needed the driver.
I already looked at the nixpkgs search for brother printer and knew, the driver wasn’t ported over, yet. So lets do it:
I first went to the brother homepage and downloaded the driver .deb
file. The directory structure is
brother-printer/
├── control
│ ├── control
│ ├── md5sums
│ ├── postinst
│ ├── postrm
│ └── prerm
├── control.tar.gz
├── data
│ ├── etc
│ │ └── opt
│ │ └── brother
│ │ └── Printers
│ │ └── HLL2375DW
│ │ └── inf
│ ├── opt
│ │ └── brother
│ │ └── Printers
│ │ └── HLL2375DW
│ │ ├── cupswrapper
│ │ │ ├── brother-HLL2375DW-cups-en.ppd
│ │ │ ├── Copying
│ │ │ ├── lpdwrapper
│ │ │ ├── lpdwrapper_own
│ │ │ └── paperconfigml2
│ │ ├── inf
│ │ │ ├── brHLL2375DWfunc
│ │ │ ├── brHLL2375DWrc
│ │ │ └── setupPrintcap
│ │ ├── LICENSE_ENG.txt
│ │ ├── LICENSE_JPN.txt
│ │ └── lpd
│ │ ├── armv7l
│ │ │ ├── brprintconflsr3
│ │ │ └── rawtobr3
│ │ ├── i686
│ │ │ ├── brprintconflsr3
│ │ │ └── rawtobr3
│ │ ├── lpdfilter
│ │ └── x86_64
│ │ ├── brprintconflsr3
│ │ └── rawtobr3
│ ├── usr
│ │ └── share
│ │ └── doc
│ └── var
│ └── spool
│ └── lpd
│ └── HLL2375DW
├── data.tar.gz
└── debian-binary
Lots of empty directories. When we concentrate on the opt
directory, we can see three directories. cupswrapper
, inf
and lpd
. The last one branches itself in three architectures armv7l
, i686
and x86_64
.
We obviously need the brother-HLL2375DW-cups-en.ppd
file. In it we find the following code:
*cupsFilter: "application/vnd.cups-postscript 0 brother_lpdwrapper_HLL2375DW"
*cupsFilter: "application/vnd.cups-pdf 0 brother_lpdwrapper_HLL2375DW"
So, we need a cupsFilter
. Where does this come from? No file is named this way. So lets have a look at the debian control files.
This is the content of postinst
which gets executed when installing the package on a debian system:
postinst
#!/bin/sh
if [ "$(echo $(uname -m) | grep -i 'arm')" != '' ]; then
ln -s /opt/brother/Printers/HLL2375DW/lpd/armv7l/rawtobr3 /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
ln -s /opt/brother/Printers/HLL2375DW/lpd/armv7l/brprintconflsr3 /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
elif [ "$(echo $(uname -m) | grep -i -e 'x86_64' -e 'amd64')" != '' ]; then
ln -s /opt/brother/Printers/HLL2375DW/lpd/x86_64/rawtobr3 /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
ln -s /opt/brother/Printers/HLL2375DW/lpd/x86_64/brprintconflsr3 /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
else
ln -s /opt/brother/Printers/HLL2375DW/lpd/i686/rawtobr3 /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
ln -s /opt/brother/Printers/HLL2375DW/lpd/i686/brprintconflsr3 /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
fi
if [ -e /etc/init.d/lprng ]; then
/opt/brother/Printers/HLL2375DW/inf/setupPrintcap HLL2375DW -i USB
/etc/init.d/lprng restart
elif [ -e /etc/init.d/lpd ]; then
/opt/brother/Printers/HLL2375DW/inf/setupPrintcap HLL2375DW -i USB
/etc/init.d/lpd restart
fi
if [ ! -e /usr/sbin/pstops ];then
PSTOPS=`which pstops 2>/dev/null`
if [ "`echo $PSTOPS | grep -i cups`" != "" ];then
PSTOPS=""
fi
fi
ln -s /opt/brother/Printers/HLL2375DW/inf/brHLL2375DWrc /etc/opt/brother/Printers/HLL2375DW/inf/brHLL2375DWrc
if [ ! -e /usr/bin/brprintconflsr3_HLL2375DW ];then
echo "#! /bin/sh" > /usr/bin/brprintconflsr3_HLL2375DW
echo "/opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3 -P HLL2375DW" '$''*' >>/usr/bin/brprintconflsr3_HLL2375DW
chmod 755 /usr/bin/brprintconflsr3_HLL2375DW
fi
if [ ! -e /usr/bin/perl ] && [ "`which perl`" != '' ];then
if [ -e "`which perl`" ];then
echo ln -s "`which perl`" /usr/bin/perl
ln -s "`which perl`" /usr/bin/perl
fi
fi
if [ ! -e /usr/bin/perl ]; then
echo ' ****** WARNING: /usr/bin/perl is required. ******'
fi
if [ ! -e /usr/bin/perl ] && [ "`which perl`" != '' ];then
if [ -e "`which perl`" ];then
echo ln -s "`which perl`" /usr/bin/perl
ln -s "`which perl`" /usr/bin/perl
fi
fi
if [ ! -e /usr/bin/perl ]; then
echo ' ****** WARNING: /usr/bin/perl is required. ******'
fi
if [ -e /usr/lib/cups/filter ] && [ ! -e /usr/lib/cups/filter/lpdwrapper ];then
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper /usr/lib/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/lib32/cups/filter ] && [ ! -e /usr/lib32/cups/filter/lpdwrapper ];then
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper /usr/lib32/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/lib64/cups/filter ] && [ ! -e /usr/lib64/cups/filter/lpdwrapper ];then
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper /usr/lib64/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/libexec/cups/filter ] && [ ! -e /usr/libexec/cups/filter/lpdwrapper ];then
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper /usr/libexec/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/share/cups/model ];then
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/brother-HLL2375DW-cups-en.ppd /usr/share/cups/model
PPDDIR=/usr/share/cups/model/
fi
if [ -e /usr/share/ppd ];then
if [ ! -e /usr/share/ppd/brother ];then
mkdir /usr/share/ppd/brother
fi
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/brother-HLL2375DW-cups-en.ppd /usr/share/ppd/brother
PPDDIR=/usr/share/ppd/brother/
fi
if [ "$(which lpinfo 2> /dev/null)" != '' ];then
uris=$(lpinfo -v)
for uri in $uris
do
URI=$(echo $uri | grep "HL-L2375DW" | grep usb)
if [ "$URI" != '' ];then
break;
fi
done
if [ "$URI" = '' ];then
for uri in $uris
do
URI=$(echo $uri | grep "HL-L2375DW")
if [ "$URI" != '' ];then
break;
fi
done
fi
if [ "$URI" = '' ];then
for uri in $uris
do
URI=$(echo $uri | grep -i "Brother" | grep usb)
if [ "$URI" != '' ];then
break;
fi
done
fi
if [ "$URI" = '' ];then
for uri in $uris
do
URI=$(echo $uri | grep usb)
if [ "$URI" != '' ];then
break;
fi
done
fi
if [ "$URI" = '' ];then
URI="usb://dev/usb/lp0"
fi
if [ "$(which lpadmin 2> /dev/null)" != '' ];then
echo lpadmin -p HLL2375DW -E -v $URI -P ${PPDDIR}brother-HLL2375DW-cups-en.ppd
lpadmin -p HLL2375DW -E -v $URI -P ${PPDDIR}brother-HLL2375DW-cups-en.ppd
fi
fi
if [ "$(which semanage 2> /dev/null)" != '' ];then
semanage fcontext -a -t cupsd_rw_etc_t '/etc/opt/brother/Printers/HLL2375DW/inf(/.*)?'
semanage fcontext -a -t cupsd_rw_etc_t '/opt/brother/Printers/HLL2375DW/inf(/.*)?'
semanage fcontext -a -t bin_t '/opt/brother/Printers/HLL2375DW/lpd(/.*)?'
semanage fcontext -a -t bin_t '/opt/brother/Printers/HLL2375DW/cupswrapper(/.*)?'
if [ "$(which restorecon 2> /dev/null)" != '' ];then
restorecon -R /opt/brother/Printers/HLL2375DW
restorecon -R /etc/opt/brother/Printers/HLL2375DW
fi
fi
We can break it down:
#!/bin/sh
if [ "$(echo $(uname -m) | grep -i 'arm')" != '' ]; then
ln -s /opt/brother/Printers/HLL2375DW/lpd/armv7l/rawtobr3 /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
ln -s /opt/brother/Printers/HLL2375DW/lpd/armv7l/brprintconflsr3 /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
elif [ "$(echo $(uname -m) | grep -i -e 'x86_64' -e 'amd64')" != '' ]; then
ln -s /opt/brother/Printers/HLL2375DW/lpd/x86_64/rawtobr3 /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
ln -s /opt/brother/Printers/HLL2375DW/lpd/x86_64/brprintconflsr3 /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
else
ln -s /opt/brother/Printers/HLL2375DW/lpd/i686/rawtobr3 /opt/brother/Printers/HLL2375DW/lpd/rawtobr3
ln -s /opt/brother/Printers/HLL2375DW/lpd/i686/brprintconflsr3 /opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3
fi
This links the executables rawtobr3
and brprintconflsr3
depending on the architecture. These are propitiatory binary files. We’ll come back to them later.
The next section just restarts the services after setting up Printercap
(not really relevant here).
ln -s /opt/brother/Printers/HLL2375DW/inf/brHLL2375DWrc /etc/opt/brother/Printers/HLL2375DW/inf/brHLL2375DWrc
This links the rc file from opt
to etc
. Also not really interessting, although the file will come in handy later on.
if [ ! -e /usr/bin/brprintconflsr3_HLL2375DW ];then
echo "#! /bin/sh" > /usr/bin/brprintconflsr3_HLL2375DW
echo "/opt/brother/Printers/HLL2375DW/lpd/brprintconflsr3 -P HLL2375DW" '$''*' >>/usr/bin/brprintconflsr3_HLL2375DW
chmod 755 /usr/bin/brprintconflsr3_HLL2375DW
fi
This creates a script which calls brprintconflsr3
with -P HLL2375DW $*
. So it seems brprintconflsr3
needs to know which printer it uses.
The next section just checks for perl
to be available. Since the scripts are perl
scripts, we obviously need it.
f [ -e /usr/lib/cups/filter ] && [ ! -e /usr/lib/cups/filter/lpdwrapper ];then
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper /usr/lib/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/lib32/cups/filter ] && [ ! -e /usr/lib32/cups/filter/lpdwrapper ];then
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper /usr/lib32/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/lib64/cups/filter ] && [ ! -e /usr/lib64/cups/filter/lpdwrapper ];then
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper /usr/lib64/cups/filter/brother_lpdwrapper_HLL2375DW
fi
if [ -e /usr/libexec/cups/filter ] && [ ! -e /usr/libexec/cups/filter/lpdwrapper ];then
ln -s /opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper /usr/libexec/cups/filter/brother_lpdwrapper_HLL2375DW
fi
Ah, now we finally come across our brother_lpdwrapper_HLL2375DW
file, which our PPD needs. So the lpdwrapper
(perl) is our file. This part justs links it to the correct directory.
The next part just copies the PPD file to the correct directory.
The next part sets up the printer with lpadmin
and the correct URI
. Also not relevant to the driver itself. The last part finally takes care of SELinux, which we don’t need to get involved in, either.
So, to sum it up:
- Link binary files
rawtobr3
andbrprintconflsr3
to the system directories - Link
rc
file to the system directories - Create startup script
brprintconflsr3_HLL2375DW
which just callsbrprintconflsr3
with-P HLL2375DW
- Link
lpdwrapper
tobrother_lpdwrapper_HLL2375DW
So, lets have a look at the lpdwrapper
file:
lpdwrapper
#! /usr/bin/perl
[...]
# log functions
$LOGFILE="/tmp/br_cupswrapper_ml2.log";
$LOGLEVEL=7;
$DEBUG=0;
$LOG_LATESTONLY=1;
$DEVICEURILOCK=1;
$LPD_DEBUG=0;
if ( $DEBUG > 0 ){
$LPD_DEBUG=2;
}
[...]
$LPDCONFIGEXE="brprintconflsr3";
my $INPUT_PS = "/tmp/br_cupswrapper_ml2_input.ps";
my $OUTPUT_PRN = "/tmp/br_cupswrapper_ml2_output.prn";
# main
logprint( 0 , "START\n");
$ENV{OWNER} = $ARGV[1];
$ENV{TITLE} = $ARGV[2];
$ENV{NODENAME} = `uname -n`;
my $basedir = Cwd::realpath ($0);
if ( $basedir eq '' ){
$basedir = `readlink $0`;
if ( $basedir eq '' ){
$basedir = `realpath $0`;
}
}
chomp($basedir);
$basedir =~ s/\/cupswrapper\/.*$//g;
my $cmdoptions=$ARGV[4];
my $PPD = $ENV{PPD};
my $CUPSINPUT='';
if ( @ARGV >= 6 ){
$CUPSINPUT=$ARGV[7];
}
my $PRINTER=$basedir;
$PRINTER =~ s/^\/opt\/.*\/Printers\///g;
$PRINTER =~ s/\/cupswrapper.*$//g;
$PRINTER =~ s/\///g;
for (my $i = 0 ; $i < @ARGV ; $i ++){
logprint( 0 , "ARG$i = $ARGV[$i]\n");
}
logprint( 0 , "PRINTER = $PRINTER \n");
logprint( 0 , "PPD = $PPD\n");
logprint( 0 , "BASEPATH = $basedir\n");
logprint( 0 , "export PPD=$PPD\n");
logprint( 0 , "$0 \"$ARGV[1]\" \"$ARGV[2]\" \"$ARGV[3]\" \"$ARGV[4]\" \"$ARGV[5]\" \"$ARGV[6]\"\n");
my $LPDFILTER =$basedir."/lpd/lpdfilter";
logprint( 0 , "\n");
[...]
my $LATESTINFO="/tmp/".$PRINTER."_latest_print_info";
unlink $LATESTINFO;
`touch $LATESTINFO`;
my $TEMPRC = "/tmp/br".$PRINTER."rc_".$$;
`cp $basedir/inf/br${PRINTER}rc $TEMPRC`;
$ENV{BRPRINTERRCFILE} = $TEMPRC;
logprint( 0 , "TEMPRC = $TEMPRC\n");
$LOCKFILE="/tmp/$PRINTER"."_lf_".$ENV{DEVICE_URI};
if ( $DEVICEURILOCK == 1){
open (FILE , "+> $LOCKFILE");
flock(FILE , 2);
}
$ENV{LPD_DEBUG} = $LPD_DEBUG;
$ENV{PS}=1;
$ENV{BRPAPERWIDTH} = $width;
$ENV{BRPAPERHEIGHT} = $height;
[...]
&exec_lpdconfig ( $basedir ,$PRINTER , \%lpr_options );
logprint( 2, "\n");
if ( $DEBUG == 0 ){
$command = "cat $CUPSINPUT | $LPDFILTER";
logprint( 2 , "$command\n");
system("$command");
}
[...]
elsif ($DEBUG > 1 ){
$command = "cat $CUPSINPUT > $INPUT_PS && cat $INPUT_PS |".
"$LPDFILTER > $OUTPUT_PRN";
logprint( 2, "export BRPAPERWIDTH=$ENV{BRPAPERWIDTH}\n");
logprint( 2, "export BRPAPERHEIGHT=$ENV{BRPAPERHEIGHT}\n");
logprint( 2, "export PPD=$ENV{PPD}\n");
logprint( 2, "export BRPRINTERRCFILE=$LATESTINFO\n");
logprint( 2, "export LPD_DEBUG=$ENV{LPD_DEBUG}\n");
logprint( 2, "export PS=$ENV{PS}\n");
logprint( 2, "cat $INPUT_PS | $LPDFILTER > $OUTPUT_PRN \n");
system("$command 2> /tmp/br_cupswrapper_ml2_lpderr");
print "\0";
}
`mv "$TEMPRC" "$LATESTINFO"`;
`echo "\n\nCUSTOM PAGE SIZE ${width}x${height}" >> $LATESTINFO`;
#unlink $TEMPRC;
[...]
exit 0;
#-----------------------------------------------------------
[...]
#exec lpd config
sub exec_lpdconfig {
(my $basedir , my $PRINTER , my $lpr_options_ref) = @_;
my $lpddir = $basedir;
my %lpr_options = %$lpr_options_ref;
$lpddir = $basedir."/lpd/";
my $lpdconf = $lpddir.'/'.$LPDCONFIGEXE;
while(($op , $val) = each(%lpr_options)){
my $lpdconf_command = "$lpdconf -P $PRINTER $op $val";
logprint( 0 , "$lpdconf_command\n");
`$lpdconf_command`;
}
}
I cut some stuff out. Its a long file. It should still give you a hint what it does. First, we see it works a lot with temporarly files. Either to log, or, more importantly, to setup the printer. Also some path are hardcoded. One of the more interessting lines is the following:
my $TEMPRC = "/tmp/br".$PRINTER."rc_".$$;
`cp $basedir/inf/br${PRINTER}rc $TEMPRC`;
$ENV{BRPRINTERRCFILE} = $TEMPRC;
logprint( 0 , "TEMPRC = $TEMPRC\n");
This creates a /tmp/br.HLL2375DW.rc_1234
variable, where the numbers are random (I think). Then the rc
file from above gets copied to this destination. This gets exported as an environment variable BRPRINTERRCFILE
. Later we can see the line
$command = "cat $CUPSINPUT | $LPDFILTER";
logprint( 2 , "$command\n");
system("$command");
The $CUPSINPUT
is one of the arguments the lpdwrapper
file is called with from cups. See https://www.cups.org/doc/api-filter.html for more infos.
So, what is the $LODFILTER
variable? Its my $LPDFILTER =$basedir."/lpd/lpdfilter";
So, wait.. CUPS calls the PPD, which calls this script, which calls..the next script?
Unfortunatly, it gets even worse. The lpdfilter
script calls the proprirotory pre-compiled binaries to set more options of the printer. But, look for yourself:
lpdfilter
#! /usr/bin/perl
#
# LPD/LPRng filter ver 2.01
[...]
my $basedir = Cwd::realpath ($0);
if ( $basedir eq '' ){
$basedir = `readlink $0`;
if ( $basedir eq '' ){
$basedir = `realpath $0`;
}
}
chomp($basedir);
$basedir =~ s/\/lpd\/.*$//g;
my $PRINTER=$basedir;
$PRINTER =~ s/^\/opt\/.*\/Printers\///g;
$PRINTER =~ s/\/lpd\/.*$//g;
$PRINTER =~ s/\///g;
my $INPUT_TEMP='';
my $FILE_TYPE="PostScript";
my $LOGFILE = "/tmp/br_lpdfilter_ml2.log";
$LOG_FIRSTTIME = 1;
$LOGLEVEL = 7;
$DEBUG = $ENV{LPD_DEBUG};
[...]
my $BR_PRT_PATH = $basedir;
my $RCFILE=$ENV{BRPRINTERRCFILE};
if ( $RCFILE eq '' ){
$RCFILE=sprintf ("$BR_PRT_PATH/inf/br%src",$PRINTER);
}
$FUNCFILE=sprintf ("$BR_PRT_PATH/inf/br%sfunc",$PRINTER);
$FLAG = `grep 'flags1=' $FUNCFILE | sed s/'flags1='//g`;
chomp($FLAG);
if ( $FLAG eq '' ){
$FLAG="0000000000000002";
}
$offset = `grep 'offset=' $FUNCFILE | sed s/'offset='//g`;
chomp($offset);
my $BRCONV="$BR_PRT_PATH/lpd/rawtobr3";
my $BRCONV_OP="-rc $RCFILE -flags $FLAG -offset $offset";
[...]
if ( $ENV{PS} ne '1' ){
$INPUT_TEMP=`mktemp /tmp/br_input.XXXXXX`;
chomp($INPUT_TEMP);
`cat > $INPUT_TEMP`;
$FILE_TYPE=`file $INPUT_TEMP`;
$FILE_TYPE=~ s/^.*:[ ]*//;
$FILE_TYPE=~ s/[ ].*//;
if ( $DEBUG ne '0' ){
copy "$INPUT_TEMP" , "/tmp/br_lpdfilter_ml2_input.ps";
}
}
else{
$INPUT_TEMP='';
$FILE_TYPE="PostScript";
if ( $DEBUG ne '0' ){
$INPUT_TEMP=`mktemp /tmp/br_input.XXXXXX`;
chomp($INPUT_TEMP);
`cat > $INPUT_TEMP`;
copy "$INPUT_TEMP" , "/tmp/br_lpdfilter_ml2_input.ps";
}
}
logprint (1, "PRINTER=$PRINTER");
logprint (1, "\$ENV{PS} = $ENV{PS}" );
logprint (1, "\$ENV{BRPRINTERRCFILE} = $ENV{BRPRINTERRCFILE}");
my $paper = "A4";
my $resolution = "600";
open (FPRCFILE , $RCFILE);
my $rcline ;
while ($rcline = <FPRCFILE>){
if ( $rcline =~ /Resolution/){
$resolution = $rcline;
$resolution =~ s/Resolution=//;
chomp($resolution);
}
elsif ( $rcline =~ /PaperType/){
$papertype = $rcline;
$papertype =~ s/PaperType=//;
chomp($papertype);
}
}
close(FPRCFILE);
$width = $ENV{BRPAPERWIDTH};
$height = $ENV{BRPAPERHEIGHT};
logprint(1, "\$ENV{BRPAPERWIDTH} = $ENV{BRPAPERWIDTH}");
logprint(1, "\$ENV{BRPAPERHEIGHT} = $ENV{BRPAPERHEIGHT}");
my $size_br = '';
if ( $width eq '' || $height eq '' ||
$width == 0 || $height == 0 ||
$width == -1 || $height == -1 ){
my $paperref = $PAPERTBL{$papertype};
$width = $paperref->{width};
$height = $paperref->{height};
logprint(1, " TYPE=$papertype w=$width h=$height size_br=$size_br \n");
}
$size_br = " -ps ${width}x${height}";
$BRCONV_OP .= $size_br;
[...]
my $GHOST_SCRIPT=`which gs`;
chomp($GHOST_SCRIPT);
my $OUTPUT_TYPE="bit";
my $GHOST_OPT="-q -dNOPROMPT -dNOPAUSE -dSAFER -sDEVICE=$OUTPUT_TYPE -sstdout=%stderr -sOutputFile=- - -c quit";
my $gscommand = "";
if ( $HWMARGINS eq "yes" ){
$gscommand =
"(echo '<</.HWMargins[12. 12. 12. 12.]>>setpagedevice';".
"cat $INPUT_TEMP)" .
" | $GHOST_SCRIPT -r$resolution -g$size_gs $GHOST_OPT ";
}
else{
$gscommand =
"cat $INPUT_TEMP | $GHOST_SCRIPT -r$resolution -g$size_gs $GHOST_OPT";
}
my $brcommand="$BRCONV $BRCONV_OP";
if ( $DEBUG eq '1' ){
system("$gscommand | $brcommand");
logprint( 0, "$gscommand | $brcommand") ;
}
elsif ( $DEBUG eq '2' ){
`$gscommand > /tmp/br_lpdfilter_ml2_gsout.dat`;
`cat /tmp/br_lpdfilter_ml2_gsout.dat | $brcommand >/tmp/br_lpdfilter_ml2_out.prn`;
system("cat /tmp/br_lpdfilter_ml2_out.prn");
`cp $RCFILE /tmp/br_lpdfilter_ml2.rc`;
$brcommand="$BRCONV -rc /tmp/br_lpdfilter_ml2.rc -flags $FLAG -offset $offset $size_br";
logprint(1, "$gscommand ".
"> /tmp/br_lpdfilter_ml2_gsout.dat");
logprint(1, "cat /tmp/br_lpdfilter_ml2_gsout.dat | $brcommand".
">/tmp/br_lpdfilter_ml2_out.prn\n");
logprint(1, "cat /tmp/br_lpdfilter_ml2_out.prn ");
}
else{
system("$gscommand | $brcommand");
}
if ( $INPUT_TEMP ne '' ){
unlink $INPUT_TEMP;
}
exit 0;
So, to make it short: It opens the file from the environment variable BRPRINTERRCFILE
, gets the values from it and calls gs
with -q -dNOPROMPT -dNOPAUSE -dSAFER -sDEVICE=$OUTPUT_TYPE -sstdout=%stderr -sOutputFile=- - -c quit
and rawtobr3
with -rc $RCFILE -flags $FLAG -offset $offset
.
So far, so good. We now have a little bit of an overview of how this driver works. We now need to get this into NixOS
. Fortunatly, someone already packaged a different, but very similiar brother printer driver: https://github.com/NixOS/nixpkgs/pull/165514
{ lib
, stdenv
, fetchurl
, dpkg
, autoPatchelfHook
, makeWrapper
, perl
, gnused
, ghostscript
, file
, coreutils
, gnugrep
, which
}:
let
arches = [ "x86_64" "i686" "armv7l" ];
runtimeDeps = [
ghostscript
file
gnused
gnugrep
coreutils
which
];
in
stdenv.mkDerivation rec {
pname = "cups-brother-mfcl2750dw";
version = "4.0.0-1";
nativeBuildInputs = [ dpkg makeWrapper autoPatchelfHook ];
buildInputs = [ perl ];
dontUnpack = true;
src = fetchurl {
url = "https://download.brother.com/welcome/dlf103566/mfcl2750dwpdrv-${version}.i386.deb";
hash = "sha256-3uDwzLQTF8r1tsGZ7ChGhk4ryQmVsZYdUaj9eFaC0jc=";
};
installPhase = ''
runHook preInstall
mkdir -p $out
dpkg-deb -x $src $out
# delete unnecessary files for the current architecture
'' + lib.concatMapStrings (arch: ''
echo Deleting files for ${arch}
rm -r "$out/opt/brother/Printers/MFCL2750DW/lpd/${arch}"
'') (builtins.filter (arch: arch != stdenv.hostPlatform.linuxArch) arches) + ''
# bundled scripts don't understand the arch subdirectories for some reason
ln -s \
"$out/opt/brother/Printers/MFCL2750DW/lpd/${stdenv.hostPlatform.linuxArch}/"* \
"$out/opt/brother/Printers/MFCL2750DW/lpd/"
# Fix global references and replace auto discovery mechanism with hardcoded values
substituteInPlace $out/opt/brother/Printers/MFCL2750DW/lpd/lpdfilter \
--replace /opt "$out/opt" \
--replace "my \$BR_PRT_PATH =" "my \$BR_PRT_PATH = \"$out/opt/brother/Printers/MFCL2750DW\"; #" \
--replace "PRINTER =~" "PRINTER = \"MFCL2750DW\"; #"
# Make sure all executables have the necessary runtime dependencies available
find "$out" -executable -and -type f | while read file; do
wrapProgram "$file" --prefix PATH : "${lib.makeBinPath runtimeDeps}"
done
# Symlink filter and ppd into a location where CUPS will discover it
mkdir -p $out/lib/cups/filter
mkdir -p $out/share/cups/model
ln -s \
$out/opt/brother/Printers/MFCL2750DW/lpd/lpdfilter \
$out/lib/cups/filter/brother_lpdwrapper_MFCL2750DW
ln -s \
$out/opt/brother/Printers/MFCL2750DW/cupswrapper/brother-MFCL2750DW-cups-en.ppd \
$out/share/cups/model/
runHook postInstall
'';
meta = with lib; {
homepage = "http://www.brother.com/";
description = "Brother MFC-L2750DW printer driver";
license = licenses.unfree;
platforms = builtins.map (arch: "${arch}-linux") arches;
maintainers = [ maintainers.lovesegfault ];
};
}
I’m not going to much into details here, as lovesegfault
(the author) commented everything pretty good.
I initially just adapted the file to use the correct source and changed the printer model. This was all that needed to be done! It worked! Yay!
Well, it did print.
It did not print two-sided, in different resolutions or different paper sizes. Whats going on?
Well, looking at the above scripts, we notice that the L2750DW
printer only uses the lpdfilter
file. But we don’t just have that, but also the lpdwrapper
, right?
The workflow seems to be:
CUPS –> PPD –> lpdwrapper –> lpdfilter –> ghostscript, own binaries
So, although it works, no settings are conveied from CUPS to the printer. Lets debug it:
First, I installed the .deb
file on a native Ubuntu install. There, everything worked. So it must be something in the way I packaged the driver. Digging a bit deeper, I can see the content of the RCFILE
on Ubuntu (which is under /tmp):
[HLL2375DW]
Language=LANG_USA
Resolution=600
PaperSource=Tray1
OutputBin=Auto
Duplex=OFF
DuplexType=Long
PaperType=A4
Media=PlainPaper
Copies=1
Sleep=PrinterDefault
TonerSaveMode=OFF
CUSTOM PAGE SIZE {Undefined}x{Undefined}
and on NixOS:
[HLL2375DW]
Language=LANG_USA
Resolution=600
PaperSource=Tray1
Duplex=OFF
DuplexType=Long
PaperType=A4
Media=PlainPaper
Copies=1
Sleep=PrinterDefault
TonerSaveMode=OFF
The NixOS file is exactly the same as the original one from the inf
directory, wheras the ubuntu one has some custom options in it. So apparently, this is the reason it didn’t convey the infos..
As we could see above, there is a DEBUG
flag in the perl scripts. How handy! Settings this to 2
will produce a few more files in the /tmp
directory that we can debug. (Please note: On NixOS those files are under /tmp/systemd-hash-cups
and are private to the process. You need to be root to access those).
When doing so, we can see: (br_cupswrapper_ml2_log)
START
ARG0 = 6
ARG1 = florian
ARG2 = Wikipedia
ARG3 = 1
ARG4 = PageSize=A4 EPRendering=None XRXColor=BW INK=MONO SelectColor=Grayscale HPColorMode=GrayscalePrint ProcessColorModel=Mono BRMonoColor=Mono CNGrayscale PrintoutMode=Normal.Gray BRPrintQuality=Black Collate Duplex=None ColorMode=Mono CNIJGrayScale=1 ARCMode=CMBW ColorModel=Gray OKControl=Gray XROutputColor=PrintAsGrayscale BLW=TrueM job-uuid=urn:uuid:70973965-f3d4-391e-52e7-ee87b516fb8a job-originating-host-name=localhost date-time-at-creation= date-time-at-processing= time-at-creation=1671024720 time-at-processing=1671024720
PRINTER = HLL2375DW
PPD = /etc/cups/ppd/HLL2375DW.ppd
BASEPATH = /opt/brother/Printers/HLL2375DW
export PPD=/etc/cups/ppd/HLL2375DW.ppd
/usr/lib/cups/filter/brother_lpdwrapper_HLL2375DW "florian" "Wikipedia" "1" "PageSize=A4 EPRendering=None XRXColor=BW INK=MONO SelectColor=Grayscale HPColorMode=GrayscalePrint ProcessColorModel=Mono BRMonoColor=Mono CNGrayscale PrintoutMode=Normal.Gray BRPrintQuality=Black Collate Duplex=None ColorMode=Mono CNIJGrayScale=1 ARCMode=CMBW ColorModel=Gray OKControl=Gray XROutputColor=PrintAsGrayscale BLW=TrueM job-uuid=urn:uuid:70973965-f3d4-391e-52e7-ee87b516fb8a job-originating-host-name=localhost date-time-at-creation= date-time-at-processing= time-at-creation=1671024720 time-at-processing=1671024720" "" ""
TEMPRC = /tmp/brHLL2375DWrc_3310
SET PPD OPTIONS
-ts <= OFF : (OFF)
-res <= 600 : (600dpi)
-pt <= A4 : (A4)
-sp <= PRINTER : (PrinterDefault)
-md <= PLAIN : (PLAIN)
-ps <= T1 : (TRAY1)
-dx <= ON : (DuplexNoTumble)
-dxt <= LONG : (DuplexNoTumble)
SET VENDOR COMMAND OPTIONS
SET PPD CMD OPTIONS
-dx <= OFF : (None)
-pt <= A4 : (A4)
SET VENDOR NUMERIC COMMAND OPTIONS
SET MEDIA (STANDARD) COMMAND OPTIONS
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -sp PRINTER
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -dxt LONG
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -cp 1
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ps T1
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -res 600
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ts OFF
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -pt A4
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -md PLAIN
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -dx OFF
export BRPAPERWIDTH=-1
export BRPAPERHEIGHT=-1
export PPD=/etc/cups/ppd/HLL2375DW.ppd
export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
export LPD_DEBUG=2
export PS=1
cat /tmp/br_cupswrapper_ml2_input.ps | /opt/brother/Printers/HLL2375DW/lpd/lpdfilter > /tmp/br_cupswrapper_ml2_output.prn
and br_cupswrapper_ml2_lpderr (on ubuntu, this file is empty):
Invalid output bin -1
Well. Thats not helpful.
When calling the last line cat /tmp/br_cupswrapper_ml2_input.ps | /opt/brother/Printers/HLL2375DW/lpd/lpdfilter > /tmp/br_cupswrapper_ml2_output.prn
(or /nix/store/hash-cups-brother-hll2375dw-4.0.0-1/opt/brother/Printers/HLL2375DW/lpd/lpdfilter
on NixOS) we get an error about cannot open file
. Remeber the environment settings? Yes, right, so lets try:
export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
cat /tmp/br_cupswrapper_ml2_input.ps | /opt/brother/Printers/HLL2375DW/lpd/lpdfilter > /tmp/br_cupswrapper_ml2_output.prn
Invalid output bin -1
Now we can reproduce the error Invalid output bin -1
. Isn’t that great?
We already took a look at the RC file. You’ll note the line OutputBin=Auto
in the ubuntu installation, but not in the NixOS one. Why?
Well, we come back to that. First, lets try whether we can get rid of the error when manually adding the line to the file (This file is identical to the RCFILE, because the script will copy the RCFILE to HLL2375DW_latest_print_info after the print job is done):
echo "OutputBin=Auto" >> /tmp/HLL2375DW_latest_print_info
export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
cat /tmp/br_cupswrapper_ml2_input.ps | /opt/brother/Printers/HLL2375DW/lpd/lpdfilter > /tmp/br_cupswrapper_ml2_output.prn
# no output
Yay, it worked!
Ok, so why the heck doesn’t this line exist in NixOS (And why doesn’t it exist in the original RC file)?
I have no answer for the second qustion. The first one is interessting:
Did you notice the line /opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO
and the others in the log above? We don’t really know, what brprintconflsr3
does. But it seems rather obvious it has something to do with our settings. Well, lets try to execute it manually:
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO
# cannot open file !!
Mmh.
Ah, right, environment variables:
export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO
# Error: /tmp/HLL2375DW_latest_print_info :cannot open file !!
What?
Well, now the file is right, but why can the binary not open the file and change the settings?
Lets look at it:
-r-------- 1 florian florian 183 Dez 14 15:03 HLL2375DW_latest_print_info
Why is my file 400
? Lets change it to 666
(just to make sure) and try again:
export BRPRINTERRCFILE=/tmp/HLL2375DW_latest_print_info
chmod 666 /tmp/HLL2375DW_latest_print_info
/opt/brother/Printers/HLL2375DW/lpd//brprintconflsr3 -P HLL2375DW -ob AUTO
# no output
Cool.
This is the culprit!
Btw: It is 400
because the original RCFILE
is read-only, too. That kinda makes sense. It is used as reference and the script copies the reference (including the file permissions) and it therefore stays read-only.
The fix is rather easy and amounts to this diff:
--- a/opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper 2022-12-07 18:32:29.950543083 +0100
+++ b/opt/brother/Printers/HLL2375DW/cupswrapper/lpdwrapper 2022-12-07 18:46:03.046989151 +0100
@@ -379,6 +379,7 @@
`cp $basedir/inf/br${PRINTER}rc $TEMPRC`;
+`chmod 666 $TEMPRC`;
$ENV{BRPRINTERRCFILE} = $TEMPRC;
logprint( 0 , "TEMPRC = $TEMPRC\n");
Now (after removing the DEBUG flag) the printer prints two-sided, in different resolutions and different paper sizes!
On ubuntu, the file original brHLL2375DWrc
file gets linked to /etc/opt/brother/Printers/HLL2375DWE/inf
and is read-write by root. Interesstingly the file is read-writable on NixOS, too. But for some reason gets the 400
permissions assigned in the /tmp
directory. I haven’t figured out why. If someone knows, please let me know and I’ll update my post accordingly.
The updated PR can be found at https://github.com/NixOS/nixpkgs/pull/204306