🆕 Adding a new vendor/series
This walks through adding a new driver package, using the layout
vendors/<vendor>/<series>/. Follow along with the existing
vendors/hp/upd-pcl6/ as a reference.
1. Identify the driver package
Find the vendor's redistributable driver archive (a .zip containing one or
more .inf files) and note:
- The download URL. Prefer the vendor's "driver only" download over a
"full driver & software package": the latter is sometimes just an
online installer that fetches the driver at run time and contains no
.infat all (Canon's imagePROGRAF TM package is one), which can't be pinned or packaged. - The
.inffile that is the actual entry point (a driver archive often contains several.inffiles for different OS versions/architectures — pick the one matching Windows 10/11 x64). Not every.infis a printer driver: checkClass=in its[Version]section. Canon's imagePROGRAF TM archive also ships aClass=USBnull/WinUSB driver for the USB interfaces, which a network printer never needs. - The exact driver name string(s) the
.infregisters — look at its[<Manufacturer>.NTamd64...]section(s). Some vendors (HP) ship one universal name that works for every model; others (UTAX/Kyocera) bundle a universal name and hundreds of per-model names in the same package, where the model string you pass actually selects a different driver, not just metadata — see Supported Printers for both patterns and how the UTAX list was extracted from the INF itself. Check which kind you have before writingInstall.ps1.
2. Scaffold the folder
mkdir -p vendors/<vendor>/<series>/scripts
3. Pin the driver hash
nix develop
nix-prefetch-url --unpack <driver-url>
nix hash convert --hash-algo sha256 --to sri <hash-from-previous-command>
4. Write default.nix
Keep it minimal: fetch, unpack, call mkIntunePackage. See
vendors/hp/upd-pcl6/default.nix for the exact shape — src uses
fetchzip with the SRI hash from step 3, then
lib.mkIntunePackage { name = ...; driver = ...; scripts = ./scripts; }.
If the archive ships architectures/utilities you don't need (see
vendors/utax/kx-upd/default.nix, which prunes a ~200 MB download down to
the ~61 MB x64 driver tree), add a runCommand step that copies just the
subfolder you need before passing it through as driver.
Some vendors ship a .exe instead of a .zip — check which kind before
assuming you need custom tooling:
- It's often still a real zip underneath (WinZip/other SFX stub
prepended to ordinary zip data —
7z l <file>or plainunzip -l <file>will tell you). If so, just usefetchzipwithextension = "zip"to override the filename-based format detection; seevendors/canon/gplus-pcl6/default.nix. No custom extraction needed. - OKI's is a genuinely proprietary self-extractor with no filename table
anywhere in the archive, where the above doesn't apply. See
vendors/oki/mc8x3-pcl6/default.nixandtools/oki-sfx-extract.pyfor that pattern:fetchurlthe raw.exe(hash-pinned like anything else), then reconstruct real filenames by content hash against a smallfiles.jsonmanifest recorded once from a real extraction — no need to reverse-engineer the archive's internal layout, only to know what bytes each file you need actually contains. Reach for this only after confirming7z/unzipreally can't read the file at all.
Also check whether the driver's own files are further packed into a
.cab referenced from the INF's [SourceDisksNames] (common on Canon,
and plausible elsewhere) — if so, leave the .cab as-is next to the
.inf/.cat; pnputil /add-driver expands it natively during staging,
same as it would from a driver CD. Don't try to unpack it yourself.
Don't prune by folder name alone, either: check where the x64 install
actually takes its files from. Epson's universal INF lists files under
WINX86/ in its [SourceDisksFiles.amd64] section (32-bit companion
DLLs that an x64 install still copies), so dropping the "x86" tree would
break staging. Resolve every [SourceDisksFiles] and
[SourceDisksFiles.amd64] entry through [SourceDisksNames] to a real
path (or a compressed twin, or a referenced CAB) before deciding what to
drop. See vendors/epson/upd/default.nix.
The same goes for MS-compressed driver files (*.dl_, *.ca_, ... -
the last character of the extension replaced by _), as Konica Minolta
ships them: the INF names the uncompressed files and pnputil finds and
expands the compressed ones. See vendors/konica-minolta/upd-pcl6/.
If the vendor's download link is a query-string proxy URL rather than a
plain file path (Konica Minolta's is), percent-encode any [/] in it as
%5B/%5D - fetchurl hands the URL to curl, which reads literal
brackets as a glob range and fails with bad range - and set
extension = "zip", since there's no .zip in the URL to detect the
format from.
Never commit lib.fakeHash — if you don't have the real hash yet, that's a
sign step 3 isn't done.
5. Write the scripts
scripts/Install.ps1 needs, at minimum:
- Parameters:
PrinterName(mandatory),IPAddress(mandatory),Model(optional; either purely informational, or — if this vendor bundles per-model driver names — the value that selects which one to install, falling back to the universal name when unset). - Dot-source
lib/common-scripts/TrustDriverPublisher.ps1(merged into every package's build tree automatically bymkIntunePackage.nix) and callImport-DriverPublisherTrust -InfPath $infPathright before staging. Without it, pnputil under SYSTEM fails with0xE0000242("publisher not trusted") for any signed third-party driver, because the interactive trust prompt Windows would otherwise show has no one to click it. - Idempotent driver staging (
pnputil /add-driver ... /installviaGet-PnputilPathfromlib/common-scripts/ResolvePnputil.ps1- never call plainpnputil.exe, since Intune can run the script through a 32-bit PowerShell host where WOW64 redirection hides the realSystem32\pnputil.exeand the call fails as an unrecognized command, treating exit codes0/259/3010as success) and registration viaAdd-PrinterDriver -Name $DriverName- without-InfPath. Passing-InfPathmakesAdd-PrinterDriverre-parse the INF itself, and a universal driver INF that repeats the same driver name once per matched hardware ID (HP's does; UTAX's/OKI's model-specific names mostly don't, but don't rely on that) trips it up with "one of the parameters specified is invalid." Since pnputil already staged the driver into the driver store in the previous step, plain-Nameis sufficient and avoids re-parsing entirely. - Idempotent port (
Add-PrinterPort/Set-PrinterPort) and printer (Add-Printer/Set-Printer) creation — checkGet-*first, reconcile rather than fail if the resource already exists. - Logging to
C:\ProgramData\IntunePrinterPackages\logs. - Exit code
0on success, non-zero on failure.
scripts/Uninstall.ps1 mirrors it: remove the named printer, and remove its
port only if no other printer on the box still uses it. Never remove the
shared driver — other printers of the same series depend on it.
6. Register the package in flake.nix
Add one entry to vendorPackages:
"<vendor>-<series>" = pkgs.callPackage ./vendors/<vendor>/<series> { inherit mkIntunePackage; };
7. Build and sanity-check
nix build .#<vendor>-<series>
unzip -l result/<vendor>-<series>.intunewin # expect IntuneWinPackage/Contents/... and .../Metadata/Detection.xml
nix build .#all # confirm the aggregate still builds
8. Validate against a real Intune tenant
Upload the built .intunewin to a test Win32 app in Intune, install it on a
test device with a real or simulated printer at some IP, and confirm the
printer appears and the registry detection key
(HKLM\SYSTEM\CurrentControlSet\Control\Print\Printers\<PrinterName>) is
created. See
architecture.md#format-validation-and-fallback
if the upload or install fails in a way that points at the package format
itself rather than the driver/script logic.