🆕 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.
- 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). - 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 (OKI) ship a custom self-extracting .exe instead of a zip,
with no filename table anywhere in the archive. fetchzip can't unpack
that. See vendors/oki/mc8x3-pcl6/default.nix and
tools/oki-sfx-extract.py for the 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.
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.