9 AddingAPrinter
Forgejo Actions edited this page 2026-11-04 18:00:06 +00:00

🆕 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 .inf at all (Canon's imagePROGRAF TM package is one), which can't be pinned or packaged.
  • The .inf file that is the actual entry point (a driver archive often contains several .inf files for different OS versions/architectures — pick the one matching Windows 10/11 x64). Not every .inf is a printer driver: check Class= in its [Version] section. Canon's imagePROGRAF TM archive also ships a Class=USB null/WinUSB driver for the USB interfaces, which a network printer never needs.
  • The exact driver name string(s) the .inf registers — 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 writing Install.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 plain unzip -l <file> will tell you). If so, just use fetchzip with extension = "zip" to override the filename-based format detection; see vendors/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.nix and tools/oki-sfx-extract.py for that pattern: fetchurl the raw .exe (hash-pinned like anything else), then reconstruct real filenames by content hash against a small files.json manifest 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 confirming 7z/unzip really 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 by mkIntunePackage.nix) and call Import-DriverPublisherTrust -InfPath $infPath right before staging. Without it, pnputil under SYSTEM fails with 0xE0000242 ("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 ... /install via Get-PnputilPath from lib/common-scripts/ResolvePnputil.ps1 - never call plain pnputil.exe, since Intune can run the script through a 32-bit PowerShell host where WOW64 redirection hides the real System32\pnputil.exe and the call fails as an unrecognized command, treating exit codes 0/259/3010 as success) and registration via Add-PrinterDriver -Name $DriverName - without -InfPath. Passing -InfPath makes Add-PrinterDriver re-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 -Name is sufficient and avoids re-parsing entirely.
  • Idempotent port (Add-PrinterPort/Set-PrinterPort) and printer (Add-Printer/Set-Printer) creation — check Get-* first, reconcile rather than fail if the resource already exists.
  • Logging to C:\ProgramData\IntunePrinterPackages\logs.
  • Exit code 0 on 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.