Installing Panasonic printer drivers on NixOS
At my workplace, we have a Panasonic KX-MB2138CN printer. One of my colleagues developed a web interface (including a backend) for it. It allows printing PDF files (one-sided), and it worked well for most of the time.
However, it's not prefect: it only supports printing PDF files single-sided, there are no configuration options, and it doesn't integrate well with rest of the system (things like Ctrl+P won't recognize it).
Plus, it hasn't been maintained for a while, so I decided to just set up the printer in CUPS on my machine and use that instead. Unfortunately it turned out to be a bit more difficult than I expected.
The printer it not supported out of the box, so I need to find and install the drivers manually.
Searching first led me to this page, which has a option named KX-MB2138CN
, but the link has rotten and is no longer valid.
After some more search I came across this page, which contains Linux drivers. The manual has stated support for Simplified Chinese, so it should work for the printer I have.
I downloaded the archive mccgdi-2.0.10-x86_64.tar.gz
, only to find that the installer is a lovely bash script that expects a FHS-compatible environment (which I don't have on NixOS) and /etc/init.d
from SysVinit (which I also don't).
It's certainly a bad sign and hints that the journey was going to be bumpy.
Taking a closer look at the installer, all it does is to copy some files and restart the CUPS service, so it should be possible to install manually.
We all know to avoid installing softwares manually, so let's create a Nix package!
{
stdenv,
fetchzip,
...
}:
stdenv.mkDerivation rec {
pname = "mccgdi";
version = "2.0.10";
src = fetchzip {
url = "https://www.psn-web.net/cs/support/fax/common/file/Linux_PrnDriver/Driver_Install_files/${pname}-${version}-x86_64.tar.gz";
hash = "sha256-cDXkQwzom4RmLQ9m9EegoRNRdGUUaUk3C4Qfn11V7qw=";
};
installPhase = ''
mkdir -p $out
'';
meta = {
description = "Panasonic multi-function station printer drivers";
homepage = "https://docs.connect.panasonic.com/pcc/support/fax/";
downloadPage = "https://docs.connect.panasonic.com/pcc/support/fax/common/table/linuxdriver.html";
platforms = [ "x86_64-linux" ];
};
}
The code should be pretty self-explanatory, the only thing that may need explanation is the hash
attribute.
To fill it, first set the hash
attribute to ""
, then build the package, which will fail due to hash mismatch and tell you the right hash.1
Let's actually install the drivers now. First, we need to install the PPD files.
installPhase = ''
mkdir -p $out/share/cups/model/panasonic
cp ppd/* $out/share/cups/model/panasonic/
'';
Add the printer in CUPS, we now see a error complaining /lib/cups/filter/L_H0JDGCZAZ
is not found.
Let's install that.
installPhase = ''
mkdir -p $out/lib/cups/filter
cp filter/L_H0JDGCZAZ $out/lib/cups/filter/
mkdir -p $out/share/cups/model/panasonic
cp ppd/* $out/share/cups/model/panasonic/
'';
Now there's another error saying
NixOS cannot run dynamically linked executables intended for generic
linux environments out of the box. For more information, see:
https://nix.dev/permalink/stub-ld
For this, we need to use autoPatchelfHook
.
nativeBuildInputs = [
autoPatchelfHook
];
buildInputs = [
libgcc.lib
];
Try again, this time we got
Cannot load libcups or libcups version too old then 1.1.19
This is where things get tricky, and we need to dig deep into the filter to know what had happened. To do this, I'm using Ghidra.
With some wandering, I found FUN_0045f494
, which contains a long list of paths to search.
Sadly, none of them are present on NixOS.
I actually stuck here for a while, thinking about how I could replace the paths. Luckly, with some search I found this, which is exactly what I need.
The filter loads libcups
through dlopen
, so we can hook that and fix paths on-the-fly.
We also need to wrap the filter to add LD_PRELOAD
to its environment.
After this there is also a similar issue about libgs
, which can be fixed with the same trick.
nativeBuildInputs = [
autoPatchelfHook
makeWrapper
];
buildPhase = ''
substitute ${./hook.c} hook.c \
--replace-fail "@cups@" ${cups.lib} \
--replace-fail "@ghostscript@" ${ghostscript}
cc -shared -fPIC hook.c -o libhook.so
'';
installPhase = ''
mkdir -p $out/lib
cp libhook.so $out/lib/
mkdir -p $out/lib/cups/filter
cp filter/L_H0JDGCZAZ $out/lib/cups/filter/
mkdir -p $out/share/cups/model/panasonic
cp ppd/* $out/share/cups/model/panasonic/
'';
postFixup = ''
wrapProgram $out/lib/cups/filter/L_H0JDGCZAZ \
--set LD_PRELOAD $out/lib/libhook.so
'';
#include <dlfcn.h>
#include <stddef.h>
#include <string.h>
void* (*dlopen_)(const char*, int);
void* dlopen(const char* file, int mode) {
if (dlopen_ == NULL) dlopen_ = dlsym(RTLD_NEXT, "dlopen");
if (strcmp(file, "/usr/lib/libcups.so") == 0)
file = "@cups@/lib/libcups.so";
else if (strcmp(file, "/usr/lib/libgs.so") == 0)
file = "@ghostscript@/lib/libgs.so";
return dlopen_(file, mode);
}
Finally we have two issues about data files and libpanjbig
.
ERROR: Cannot open data file(No such file or directory): /usr/local/share/panasonic/printer/data/L_LGJD.SPC
Cannot load libpanjbig or libpanjbig version too old
With Ghidra we can find out what to do.
The second one is simple, we can use the same dlopen
trick.
The first one needs a fopen
hook.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
#include <dlfcn.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void* (*dlopen_)(const char*, int);
FILE* (*fopen_)(const char*, const char*);
void* dlopen(const char* file, int mode) {
if (dlopen_ == NULL) dlopen_ = dlsym(RTLD_NEXT, "dlopen");
if (strcmp(file, "/usr/lib/libcups.so") == 0)
file = "@cups@/lib/libcups.so";
else if (strcmp(file, "/usr/lib/libgs.so") == 0)
file = "@ghostscript@/lib/libgs.so";
else if (strcmp(file, "/usr/lib/L_H0JDJCZAZ.so") == 0)
file = "@out@/lib/L_H0JDJCZAZ.so";
else if (strcmp(file, "/usr/lib/L_H0JDJCZAZ_2.so") == 0)
file = "@out@/lib/L_H0JDJCZAZ_2.so";
return dlopen_(file, mode);
}
#define DATADIR "/usr/local/share/panasonic/printer/data"
#define DATADIR_REAL "@datadir@"
FILE* fopen(const char* pathname, const char* mode) {
if (fopen_ == NULL) fopen_ = dlsym(RTLD_NEXT, "fopen");
if (strncmp(pathname, DATADIR, strlen(DATADIR)) == 0) {
char* buf =
malloc(strlen(DATADIR_REAL) + strlen(pathname) - strlen(DATADIR) + 1);
if (buf == NULL) return NULL;
strcpy(buf, DATADIR_REAL);
strcat(buf, pathname + strlen(DATADIR));
pathname = buf;
};
return fopen_(pathname, mode);
}
Now the printer works!
I did some cleanup to the code, and you can find the package in my dotfiles.