programmer: Add bitbanging programmer driver for Linux libgpiod

With this driver, any single board computer, old smartphone, etc. with
a few spare GPIOs can be used for flashrom.

Tested by reading of a 2048 kB flash chip on a Qualcomm MSM8916 SoC
@800 MHz, ran the following command:

time flashrom -p linux_gpiod:gpiochip=0,cs=18,sck=19,mosi=13,miso=56 -r test.bin

This command uses /dev/gpiochip0 with the specified GPIO numbers for the
SPI lines. All arguments are mandatory.

Output:
[...]
Found GigaDevice flash chip "GD25LQ16" (2048 kB, SPI) on linux_gpiod.
[...]
real    1m 33.96s

Change-Id: Icad3eb7764f28feaea51bda3a7893da724c86d06
Signed-off-by: Steve Markgraf <steve@steve-m.de>
Signed-off-by: Nico Huber <nico.h@gmx.de>
Reviewed-on: https://review.coreboot.org/c/flashrom-stable/+/73290
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
diff --git a/Documentation/building_meson.md b/Documentation/building_meson.md
index 21d400e..74dc65b 100644
--- a/Documentation/building_meson.md
+++ b/Documentation/building_meson.md
@@ -11,6 +11,7 @@
   * libusb1 >=1.0.9 ***
   * libftdi1 ***
   * libjaylink ***
+  * libgpiod (Linux only) ***
 
 \*   Compile time dependency
 \*** Runtime / Programmer specific
@@ -46,14 +47,14 @@
 ```
 apt-get install -y gcc meson ninja-build pkg-config \
 	linux-headers-generic libpci-dev libusb-1.0-0-dev libftdi1-dev \
-	libjaylink-dev
+	libjaylink-dev libgpiod-dev
 ```
 
 ### ArchLinux / Manjaro
   * __libjaylink__ is not available through the package manager
 ```
 pacman -S --noconfirm gcc meson ninja pkg-config \
-	pciutils libusb libftdi
+	pciutils libusb libftdi libgpiod
 ```
 
 ### NixOS / Nixpkgs
@@ -62,20 +63,20 @@
 ```
 or
 ```
-nix-shell -p meson ninja pkg-config pciutils libusb1 libftdi1 libjaylink
+nix-shell -p meson ninja pkg-config pciutils libusb1 libftdi1 libjaylink libgpiod
 ```
 
 ### OpenSUSE
 ```
 zypper install -y gcc meson ninja pkg-config \
 	pciutils-devel libusb-1_0-devel libftdi1-devel \
-	libjaylink-devel
+	libjaylink-devel libgpiod-devel
 ```
 
 ### Alpine
 ```
 apk add build-base meson ninja pkgconf pciutils-dev libusb-dev \
-	libftdi1-dev libjaylink-dev linux-headers
+	libftdi1-dev libjaylink-dev linux-headers libgpiod-dev
 ```
 
 ### Freebsd / DragonFly BSD
diff --git a/Makefile b/Makefile
index bcf795b..5db5e03 100644
--- a/Makefile
+++ b/Makefile
@@ -104,6 +104,7 @@
 DEPENDS_ON_BITBANG_SPI := \
 	CONFIG_DEVELOPERBOX_SPI \
 	CONFIG_INTERNAL_X86 \
+	CONFIG_LINUX_GPIOD \
 	CONFIG_NICINTEL_SPI \
 	CONFIG_OGP_SPI \
 	CONFIG_PONY_SPI \
@@ -177,6 +178,9 @@
 DEPENDS_ON_LINUX_I2C := \
 	CONFIG_MSTARDDC_SPI \
 
+DEPENDS_ON_LIBGPIOD := \
+	CONFIG_LINUX_GPIOD \
+
 ifeq ($(CONFIG_ENABLE_LIBUSB1_PROGRAMMERS), no)
 $(call disable_all,$(DEPENDS_ON_LIBUSB1))
 endif
@@ -231,6 +235,10 @@
 CONFIG_LIBPCI_CFLAGS       := $(call dependency_cflags, libpci)
 CONFIG_LIBPCI_LDFLAGS      := $(call dependency_ldflags, libpci)
 
+CONFIG_LIBGPIOD_VERSION      := $(call dependency_version, libgpiod)
+CONFIG_LIBGPIOD_CFLAGS       := $(call dependency_cflags, libgpiod)
+CONFIG_LIBGPIOD_LDFLAGS      := $(call dependency_ldflags, libgpiod)
+
 # Determine the destination OS, architecture and endian
 # IMPORTANT: The following lines must be placed before TARGET_OS, ARCH or ENDIAN
 # is ever used (of course), but should come after any lines setting CC because
@@ -245,6 +253,7 @@
 HAS_LIBJAYLINK      := $(call find_dependency, libjaylink)
 HAS_LIBUSB1         := $(call find_dependency, libusb-1.0)
 HAS_LIBPCI          := $(call find_dependency, libpci)
+HAS_LIBGPIOD        := $(call find_dependency, libgpiod)
 
 HAS_PCI_OLD_GET_DEV := $(call c_compile_test, Makefile.d/pci_old_get_dev_test.c, $(CONFIG_LIBPCI_CFLAGS))
 HAS_FT232H          := $(call c_compile_test, Makefile.d/ft232h_test.c, $(CONFIG_LIBFTDI1_CFLAGS))
@@ -351,6 +360,10 @@
 $(call mark_unsupported,$(DEPENDS_ON_LIBUSB1))
 endif
 
+ifeq ($(HAS_LIBGPIOD), no)
+$(call mark_unsupported,$(DEPENDS_ON_LIBGPIOD))
+endif
+
 ifeq ($(HAS_SERIAL), no)
 $(call mark_unsupported, $(DEPENDS_ON_SERIAL))
 endif
@@ -497,9 +510,10 @@
 # Always enable Marvell SATA controllers for now.
 CONFIG_SATAMV ?= yes
 
-# Enable Linux spidev and MTD interfaces by default. We disable them on non-Linux targets.
+# Enable Linux spidev, MTD and gpiod interfaces by default. We disable them on non-Linux targets.
 CONFIG_LINUX_MTD ?= yes
 CONFIG_LINUX_SPI ?= yes
+CONFIG_LINUX_GPIOD ?= yes
 
 # Always enable ITE IT8212F PATA controllers for now.
 CONFIG_IT8212 ?= yes
@@ -731,6 +745,11 @@
 PROGRAMMER_OBJS += linux_spi.o
 endif
 
+ifeq ($(CONFIG_LINUX_GPIOD), yes)
+FEATURE_FLAGS += -D'CONFIG_LINUX_GPIOD=1'
+PROGRAMMER_OBJS += linux_gpio_spi.o
+endif
+
 ifeq ($(CONFIG_MSTARDDC_SPI), yes)
 FEATURE_FLAGS += -D'CONFIG_MSTARDDC_SPI=1'
 PROGRAMMER_OBJS += mstarddc_spi.o
@@ -839,6 +858,12 @@
 endif
 endif
 
+USE_LIBGPIOD := $(if $(call filter_deps,$(DEPENDS_ON_LIBGPIOD)),yes,no)
+ifeq ($(USE_LIBGPIOD), yes)
+override CFLAGS  += $(CONFIG_LIBGPIOD_CFLAGS)
+override LDFLAGS += $(CONFIG_LIBGPIOD_LDFLAGS)
+endif
+
 USE_LIB_NI845X := $(if $(call filter_deps,$(DEPENDS_ON_LIB_NI845X)),yes,no)
 ifeq ($(USE_LIB_NI845X), yes)
 override CFLAGS += $(CONFIG_LIB_NI845X_CFLAGS)
@@ -917,6 +942,11 @@
 		echo "  CFLAGS: $(CONFIG_LIBFTDI1_CFLAGS)";	\
 		echo "  LDFLAGS: $(CONFIG_LIBFTDI1_LDFLAGS)";	\
 	fi
+	@echo Dependency libgpiod found: $(HAS_LIBGPIOD) $(CONFIG_LIBGPIOD_VERSION)
+	@if [ $(HAS_LIBGPIOD) = yes ]; then			\
+		echo "  CFLAGS: $(CONFIG_LIBGPIOD_CFLAGS)";	\
+		echo "  LDFLAGS: $(CONFIG_LIBGPIOD_LDFLAGS)";	\
+	fi
 	@echo "Checking for header \"mtd/mtd-user.h\": $(HAS_LINUX_MTD)"
 	@echo "Checking for header \"linux/spi/spidev.h\": $(HAS_LINUX_SPI)"
 	@echo "Checking for header \"linux/i2c-dev.h\": $(HAS_LINUX_I2C)"
diff --git a/README b/README
index 71e8735..ac30d6a 100644
--- a/README
+++ b/README
@@ -57,6 +57,7 @@
  * pciutils / libpci
  * pciutils-devel / pciutils-dev / libpci-dev
  * zlib-devel / zlib1g-dev (needed if libpci was compiled with libz support)
+ * libgpiod-dev (if you want support for Linux GPIO devices)
 
 On FreeBSD, you need the following ports:
 
diff --git a/flashrom.8.tmpl b/flashrom.8.tmpl
index 87a9587..fd5fc2a 100644
--- a/flashrom.8.tmpl
+++ b/flashrom.8.tmpl
@@ -331,6 +331,8 @@
 .sp
 .BR "* ogp_spi" " (for SPI flash ROMs on Open Graphics Project graphics card)"
 .sp
+.BR "* linux_gpio_spi" " (for SPI flash ROMs attached to a GPIO chip device accessible via /dev/gpiochipX on Linux)"
+.sp
 .BR "* linux_mtd" " (for SPI flash ROMs accessible via /dev/mtdX on Linux)"
 .sp
 .BR "* linux_spi" " (for SPI flash ROMs accessible via /dev/spidevX.Y on Linux)"
@@ -1132,6 +1134,34 @@
 .B nic3com et al.\&
 section above.
 .SS
+.BR "linux_gpio_spi " programmer
+.IP
+Either the GPIO device node or the chip number as well as the GPIO numbers
+of the SPI lines must be specified like in the following examples:
+.sp
+.B "  flashrom \-p linux_gpio_spi:dev=/dev/gpiochip0,cs=8,sck=11,mosi=10,miso=9"
+.sp
+or
+.sp
+.B "  flashrom \-p linux_gpio_spi:gpiochip=0,cs=8,sck=11,mosi=10,miso=9"
+.sp
+Here,
+.B gpiochip=0
+selects the GPIO chip 0, accessible through Linux device node /dev/gpiochip0, and the
+.B cs, sck, mosi, miso
+arguments select the GPIO numbers used as SPI lines connected to the flash ROM chip. In this example
+the GPIO numbers of the hardware SPI lines of of a Raspberry Pi single board computer are specified.
+All programmer arguments are mandatory.
+Note that this is a bitbanged driver, and if your device has a hardware SPI controller, use the
+.B linux_spi
+programmer driver instead for better performance.
+.sp
+Refer to the output of the
+.B gpioinfo
+utility to make sure the GPIO numbers are correct and unused.
+.sp
+Please note that the linux_gpio_spi driver only works on Linux, and depends on libgpiod.
+.SS
 .BR "linux_mtd " programmer
 .IP
 You may specify the MTD device to use with the
@@ -1584,6 +1614,8 @@
 .br
 Stephan Guilloux
 .br
+Steve Markgraf
+.br
 Steven James
 .br
 Urja Rannikko
diff --git a/include/programmer.h b/include/programmer.h
index b0eac19..6798851 100644
--- a/include/programmer.h
+++ b/include/programmer.h
@@ -76,6 +76,7 @@
 extern const struct programmer_entry programmer_internal;
 extern const struct programmer_entry programmer_it8212;
 extern const struct programmer_entry programmer_jlink_spi;
+extern const struct programmer_entry programmer_linux_gpio_spi;
 extern const struct programmer_entry programmer_linux_mtd;
 extern const struct programmer_entry programmer_linux_spi;
 extern const struct programmer_entry programmer_mstarddc_spi;
diff --git a/linux_gpio_spi.c b/linux_gpio_spi.c
new file mode 100644
index 0000000..0103d19
--- /dev/null
+++ b/linux_gpio_spi.c
@@ -0,0 +1,183 @@
+/*
+ * This file is part of the flashrom project.
+ *
+ * Copyright (C) 2023 Steve Markgraf <steve@steve-m.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <errno.h>
+#include <gpiod.h>
+#include "programmer.h"
+#include "spi.h"
+#include "flash.h"
+
+#define CONSUMER "flashrom"
+
+struct linux_gpio_spi {
+	struct gpiod_chip *chip;
+	struct gpiod_line_bulk bulk;
+	struct gpiod_line *cs_line, *sck_line, *mosi_line, *miso_line;
+};
+
+static void linux_gpio_spi_bitbang_set_cs(int val, void *spi_data)
+{
+	struct linux_gpio_spi *data = spi_data;
+	if (gpiod_line_set_value(data->cs_line, val) < 0)
+		msg_perr("Setting cs line failed\n");
+}
+
+static void linux_gpio_spi_bitbang_set_sck(int val, void *spi_data)
+{
+	struct linux_gpio_spi *data = spi_data;
+	if (gpiod_line_set_value(data->sck_line, val) < 0)
+		msg_perr("Setting sck line failed\n");
+}
+
+static void linux_gpio_spi_bitbang_set_mosi(int val, void *spi_data)
+{
+	struct linux_gpio_spi *data = spi_data;
+	if (gpiod_line_set_value(data->mosi_line, val) < 0)
+		msg_perr("Setting sck line failed\n");
+}
+
+static int linux_gpio_spi_bitbang_get_miso(void *spi_data)
+{
+	struct linux_gpio_spi *data = spi_data;
+	int r = gpiod_line_get_value(data->miso_line);
+	if (r < 0)
+		msg_perr("Getting miso line failed\n");
+	return r;
+}
+
+static const struct bitbang_spi_master bitbang_spi_master_gpiod = {
+	.set_cs		= linux_gpio_spi_bitbang_set_cs,
+	.set_sck	= linux_gpio_spi_bitbang_set_sck,
+	.set_mosi	= linux_gpio_spi_bitbang_set_mosi,
+	.get_miso	= linux_gpio_spi_bitbang_get_miso,
+};
+
+static int linux_gpio_spi_shutdown(void *spi_data)
+{
+	struct linux_gpio_spi *data = spi_data;
+
+	if (gpiod_line_bulk_num_lines(&data->bulk) > 0)
+		gpiod_line_release_bulk(&data->bulk);
+
+	if (data->chip)
+		gpiod_chip_close(data->chip);
+
+	free(data);
+
+	return 0;
+}
+
+static int linux_gpio_spi_init(void)
+{
+	struct linux_gpio_spi *data = NULL;
+	struct gpiod_chip *chip = NULL;
+	const char *param_str[] = { "cs", "sck", "mosi", "miso", "gpiochip" };
+	const bool param_required[] = { true, true, true, true, false };
+	unsigned int param_int[ARRAY_SIZE(param_str)];
+	unsigned int i;
+	int r;
+
+	data = calloc(1, sizeof(*data));
+	if (!data) {
+		msg_perr("Unable to allocate space for SPI master data\n");
+		return 1;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(param_str); i++) {
+		char *param = extract_programmer_param(param_str[i]);
+		char *endptr;
+		r = 1;
+
+		if (param) {
+			errno = 0;
+			param_int[i] = strtoul(param, &endptr, 10);
+			r = (*endptr != '\0') || (errno != 0);
+			free(param);
+		} else {
+			param_int[i] = UINT_MAX;
+		}
+
+		if ((param_required[i] || param) && r) {
+			msg_perr("Missing or invalid required programmer "
+				 "parameter %s=<n>\n", param_str[i]);
+			goto err_exit;
+		}
+	}
+
+	char *const dev = extract_programmer_param("dev");
+	if (!dev && param_int[4] == UINT_MAX) {
+		msg_perr("Either a 'dev' or 'gpiochip' parameter must be specified.\n");
+		goto err_exit;
+	}
+	if (dev && param_int[4] != UINT_MAX) {
+		msg_perr("Only one of 'dev' or 'gpiochip' parameters can be specified.\n");
+		free(dev);
+		goto err_exit;
+	}
+
+	if (dev) {
+		chip = gpiod_chip_open(dev);
+		free(dev);
+	} else {
+		chip = gpiod_chip_open_by_number(param_int[4]);
+	}
+	if (!chip) {
+		msg_perr("Failed to open gpiochip: %s\n", strerror(errno));
+		goto err_exit;
+	}
+
+	data->chip = chip;
+
+	if (gpiod_chip_get_lines(chip, param_int, 4, &data->bulk)) {
+		msg_perr("Error getting GPIO lines\n");
+		goto err_exit;
+	}
+
+	data->cs_line = gpiod_line_bulk_get_line(&data->bulk, 0);
+	data->sck_line = gpiod_line_bulk_get_line(&data->bulk, 1);
+	data->mosi_line = gpiod_line_bulk_get_line(&data->bulk, 2);
+	data->miso_line = gpiod_line_bulk_get_line(&data->bulk, 3);
+
+	r = gpiod_line_request_output(data->cs_line, CONSUMER, 1);
+	r |= gpiod_line_request_output(data->sck_line, CONSUMER, 1);
+	r |= gpiod_line_request_output(data->mosi_line, CONSUMER, 1);
+	r |= gpiod_line_request_input(data->miso_line, CONSUMER);
+
+	if (r < 0) {
+		msg_perr("Requesting GPIO lines failed\n");
+		goto err_exit;
+	}
+
+	if (register_shutdown(linux_gpio_spi_shutdown, data))
+		goto err_exit;
+
+	/* shutdown function does the cleanup */
+	return register_spi_bitbang_master(&bitbang_spi_master_gpiod, data);
+
+err_exit:
+	linux_gpio_spi_shutdown(data);
+	return 1;
+}
+
+const struct programmer_entry programmer_linux_gpio_spi = {
+	.name		= "linux_gpio_spi",
+	.type		= OTHER,
+	.devs.note	= "Device file /dev/gpiochip<n>\n",
+	.init		= linux_gpio_spi_init,
+};
diff --git a/meson.build b/meson.build
index cd98e5b..3090808 100644
--- a/meson.build
+++ b/meson.build
@@ -109,6 +109,7 @@
 group_i2c    = get_option('programmer').contains('group_i2c')
 group_serial = get_option('programmer').contains('group_serial')
 group_jlink  = get_option('programmer').contains('group_jlink')
+group_gpiod  = get_option('programmer').contains('group_gpiod')
 group_internal = get_option('programmer').contains('group_internal')
 group_external = get_option('programmer').contains('group_external')
 
@@ -116,6 +117,7 @@
 libusb1    = dependency('libusb-1.0', required : group_usb)
 libftdi1   = dependency('libftdi1', required : group_ftdi)
 libjaylink = dependency('libjaylink', required : group_jlink)
+libgpiod   = dependency('libgpiod', required : group_gpiod)
 
 subdir('platform')
 
@@ -284,6 +286,13 @@
     'flags'   : [ '-DCONFIG_JLINK_SPI=1' ],
     'default' : false,
   },
+  'linux_gpio_spi' : {
+    'systems' : [ 'linux' ],
+    'deps'    : [ libgpiod ],
+    'groups'  : [ group_gpiod, group_external ],
+    'srcs'    : files('linux_gpio_spi.c'),
+    'flags'   : [ '-DCONFIG_LINUX_GPIOD=1' ],
+  },
   'linux_mtd' : {
     'systems' : [ 'linux' ],
     'deps'    : [ linux_headers ],
diff --git a/meson_options.txt b/meson_options.txt
index 2d7f5a6..616a9f4 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -10,8 +10,8 @@
         'group_ftdi', 'group_i2c', 'group_jlink', 'group_pci', 'group_serial', 'group_usb',
         'atahpt', 'atapromise', 'atavia', 'buspirate_spi', 'ch341a_spi', 'ch347_spi', 'dediprog', 'developerbox_spi',
         'digilent_spi', 'dirtyjtag_spi', 'drkaiser', 'dummy', 'ft2232_spi', 'gfxnvidia', 'internal', 'it8212',
-        'jlink_spi', 'linux_mtd', 'linux_spi', 'mediatek_i2c_spi', 'mstarddc_spi', 'nic3com', 'nicintel',
-        'nicintel_eeprom', 'nicintel_spi', 'nicnatsemi', 'nicrealtek', 'ogp_spi', 'parade_lspcon',
+        'jlink_spi', 'linux_gpio_spi', 'linux_mtd', 'linux_spi', 'mediatek_i2c_spi', 'mstarddc_spi', 'nic3com',
+        'nicintel', 'nicintel_eeprom', 'nicintel_spi', 'nicnatsemi', 'nicrealtek', 'ogp_spi', 'parade_lspcon',
         'pickit2_spi', 'pony_spi', 'raiden_debug_spi', 'rayer_spi', 'realtek_mst_i2c_spi', 'satamv',
         'satasii', 'serprog',  'stlinkv3_spi', 'usbblaster_spi',
 ], description: 'Active programmers')
diff --git a/programmer_table.c b/programmer_table.c
index 7df088c..5e716eb 100644
--- a/programmer_table.c
+++ b/programmer_table.c
@@ -124,6 +124,10 @@
     &programmer_satamv,
 #endif
 
+#if CONFIG_LINUX_GPIOD == 1
+    &programmer_linux_gpio_spi,
+#endif
+
 #if CONFIG_LINUX_MTD == 1
     &programmer_linux_mtd,
 #endif
diff --git a/util/manibuilder/Dockerfile.alpine b/util/manibuilder/Dockerfile.alpine
index 58d89c8..7f738e1 100644
--- a/util/manibuilder/Dockerfile.alpine
+++ b/util/manibuilder/Dockerfile.alpine
@@ -8,6 +8,8 @@
 	apk add ca-certificates build-base linux-headers git ccache \
 		pciutils-dev libusb-compat-dev libusb-dev
 
+RUN apk add libgpiod-dev || true
+
 # fix weird permissions in armhf-v3.11
 RUN [ -d /usr/share/git-core/templates ] && \
 	chmod -R a+r /usr/share/git-core/templates
diff --git a/util/manibuilder/Dockerfile.debian-debootstrap b/util/manibuilder/Dockerfile.debian-debootstrap
index e363fd2..3f5f0cb 100644
--- a/util/manibuilder/Dockerfile.debian-debootstrap
+++ b/util/manibuilder/Dockerfile.debian-debootstrap
@@ -6,7 +6,7 @@
 	apt-get -qq upgrade && \
 	apt-get -qqy install gcc make git doxygen ccache pkg-config \
 		libpci-dev libusb-dev libftdi-dev libusb-1.0-0-dev && \
-	{ apt-get -qqy install libjaylink-dev || true; } && \
+	{ apt-get -qqy install libjaylink-dev libgpiod-dev || true; } && \
 	apt-get clean
 
 ENV GIT_SSL_NO_VERIFY=1
diff --git a/util/manibuilder/Dockerfile.fedora b/util/manibuilder/Dockerfile.fedora
index 8a22fa8..179c45f 100644
--- a/util/manibuilder/Dockerfile.fedora
+++ b/util/manibuilder/Dockerfile.fedora
@@ -5,6 +5,7 @@
 	dnf install -q -y ca-certificates git gcc ccache make systemd-devel \
 		pciutils-devel libusb-devel libusbx-devel libftdi-devel \
 		libjaylink-devel && \
+	{ dnf install -q -y libgpiod-devel || true; } && \
 	dnf clean -q -y all
 
 ENV GIT_SSL_NO_VERIFY=1
diff --git a/util/manibuilder/Dockerfile.ubuntu-debootstrap b/util/manibuilder/Dockerfile.ubuntu-debootstrap
index f1088c5..c30a672 100644
--- a/util/manibuilder/Dockerfile.ubuntu-debootstrap
+++ b/util/manibuilder/Dockerfile.ubuntu-debootstrap
@@ -19,7 +19,7 @@
 	apt-get -qq upgrade && \
 	apt-get -qqy install gcc make git doxygen ccache pkg-config \
 		libpci-dev libusb-dev libftdi-dev libusb-1.0-0-dev && \
-	{ apt-get -qqy install libjaylink-dev || true; } && \
+	{ apt-get -qqy install libjaylink-dev libgpiod-dev || true; } && \
 	apt-get clean
 
 ENV GIT_SSL_NO_VERIFY=1
diff --git a/util/shell.nix b/util/shell.nix
index c870ebc..3c983fe 100644
--- a/util/shell.nix
+++ b/util/shell.nix
@@ -9,6 +9,7 @@
 		libftdi1
 		libjaylink
 		libusb1
+		libgpiod
 		meson
 		ninja
 		pciutils