5 AddingAPrinter
Forgejo Actions edited this page 2026-08-11 14:35:46 +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.
  • 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).
  • 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 (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 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.