public inbox for linux-bluetooth@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH BlueZ v9 00/15] Add meson build system and HTML docs
@ 2026-04-24  9:11 Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 01/15] build: Add meson wrap for libell Bastien Nocera
                   ` (14 more replies)
  0 siblings, 15 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

Changes since v8:
- add btsnoop protocol to the docs and generate man page
- fix duplicated labels when linking to SCO and ISO docs
- add test-util to the test suite
- remove hcidump
- fix btpclient build
- remove glib dependency from btvirt
- add bluetoothctl completion crash regression test
- add config warning regression test
- link btmgmt against glib

Bastien Nocera (14):
  build: Add meson wrap for libell
  build: Add meson build system
  build: Separate systemd and libsystemd dependencies
  tools: Install gatttool if deprecated tools are enabled
  tools: Install avinfo tool by default
  emulator: Install the emulator if built
  build: Add option to allow disabling bluetoothd
  unit: Run test-bap tests concurrently
  unit: Make gobex-transfer tests run concurrently
  build: Only build profiles if the daemon is built
  build: Only build gdbus library if there is a user
  unit: Add integration tests
  doc: Add HTML documentation
  github: Add docs deployment

Paul Otto (1):
  doc: Add introduction to GATT

 .github/workflows/docs.yml                    |  54 +
 attrib/meson.build                            |  17 +
 btio/meson.build                              |   1 +
 client/btpclient/meson.build                  |  19 +
 client/meson.build                            |  33 +
 completion/meson.build                        |   3 +
 doc/_static/bluez.css                         | 452 +++++++++
 doc/_static/bluez.png                         | Bin 0 -> 18354 bytes
 doc/_static/bullet.svg                        |   1 +
 doc/_static/card.png                          | Bin 0 -> 35387 bytes
 doc/_static/figtree.woff2                     | Bin 0 -> 14364 bytes
 doc/_static/toc_bullet.svg                    |  39 +
 doc/bluetoothctl-cmd.rst                      |  34 +
 doc/conf.py                                   |  39 +
 doc/copy-subdir.py                            |  14 +
 doc/daemons.rst                               |  12 +
 doc/dbus-gatt.rst                             |  20 +
 doc/dbus-obex.rst                             |  33 +
 doc/dbus.rst                                  |  76 ++
 doc/deprecated-tools.rst                      |  22 +
 doc/dev-maint.rst                             |  17 +
 doc/favicon.ico                               | Bin 0 -> 29152 bytes
 doc/gatt-intro.rst                            | 141 +++
 doc/index.rst                                 |  47 +
 doc/meson.build                               | 293 ++++++
 doc/protocols.rst                             |  22 +
 doc/qualifications.rst                        |  76 ++
 doc/tools.rst                                 |  24 +
 emulator/meson.build                          |  36 +
 gdbus/meson.build                             |  19 +
 gobex/meson.build                             |   8 +
 lib/meson.build                               |  53 +
 mesh/meson.build                              |  60 ++
 meson.build                                   | 323 ++++++
 meson_options.txt                             |  55 ++
 monitor/meson.build                           |  37 +
 obexd/meson.build                             | 121 +++
 peripheral/meson.build                        |  13 +
 plugins/meson.build                           |  29 +
 profiles/meson.build                          | 167 ++++
 src/meson.build                               |  90 ++
 src/shared/meson.build                        |  85 ++
 subprojects/ell.wrap                          |  11 +
 .../0001-build-Add-meson-build-system.patch   | 922 ++++++++++++++++++
 test/meson.build                              |  37 +
 tools/mesh/meson.build                        |  16 +
 tools/meson.build                             | 262 +++++
 tools/mpris-proxy.service.in                  |   2 +-
 unit/integration-test-skipped.sh              |   3 +
 unit/integration-test.py                      | 271 +++++
 unit/meson.build                              | 194 ++++
 unit/output_checker.py                        | 214 ++++
 unit/unittest_inspector.py                    |  46 +
 unit/unittest_list.sh                         |  15 +
 54 files changed, 4577 insertions(+), 1 deletion(-)
 create mode 100644 .github/workflows/docs.yml
 create mode 100644 attrib/meson.build
 create mode 100644 btio/meson.build
 create mode 100644 client/btpclient/meson.build
 create mode 100644 client/meson.build
 create mode 100644 completion/meson.build
 create mode 100644 doc/_static/bluez.css
 create mode 100644 doc/_static/bluez.png
 create mode 100644 doc/_static/bullet.svg
 create mode 100644 doc/_static/card.png
 create mode 100644 doc/_static/figtree.woff2
 create mode 100644 doc/_static/toc_bullet.svg
 create mode 100644 doc/bluetoothctl-cmd.rst
 create mode 100644 doc/conf.py
 create mode 100644 doc/copy-subdir.py
 create mode 100644 doc/daemons.rst
 create mode 100644 doc/dbus-gatt.rst
 create mode 100644 doc/dbus-obex.rst
 create mode 100644 doc/dbus.rst
 create mode 100644 doc/deprecated-tools.rst
 create mode 100644 doc/dev-maint.rst
 create mode 100644 doc/favicon.ico
 create mode 100644 doc/gatt-intro.rst
 create mode 100644 doc/index.rst
 create mode 100644 doc/meson.build
 create mode 100644 doc/protocols.rst
 create mode 100644 doc/qualifications.rst
 create mode 100644 doc/tools.rst
 create mode 100644 emulator/meson.build
 create mode 100644 gdbus/meson.build
 create mode 100644 gobex/meson.build
 create mode 100644 lib/meson.build
 create mode 100644 mesh/meson.build
 create mode 100644 meson.build
 create mode 100644 meson_options.txt
 create mode 100644 monitor/meson.build
 create mode 100644 obexd/meson.build
 create mode 100644 peripheral/meson.build
 create mode 100644 plugins/meson.build
 create mode 100644 profiles/meson.build
 create mode 100644 src/meson.build
 create mode 100644 src/shared/meson.build
 create mode 100644 subprojects/ell.wrap
 create mode 100644 subprojects/packagefiles/0001-build-Add-meson-build-system.patch
 create mode 100644 test/meson.build
 create mode 100644 tools/mesh/meson.build
 create mode 100644 tools/meson.build
 create mode 100755 unit/integration-test-skipped.sh
 create mode 100755 unit/integration-test.py
 create mode 100644 unit/meson.build
 create mode 100644 unit/output_checker.py
 create mode 100755 unit/unittest_inspector.py
 create mode 100644 unit/unittest_list.sh

-- 
2.53.0


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 01/15] build: Add meson wrap for libell
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 02/15] build: Add meson build system Bastien Nocera
                   ` (13 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

Rather than relying on libell being able to build with meson from
upstream, apply the meson build patches on top of the latest upstream
release.

The build should still fallback to the system libell if available, and
will fail to build if a system libell is not available, and no network
access is available, as in many build systems.
---
 subprojects/ell.wrap                          |  11 +
 .../0001-build-Add-meson-build-system.patch   | 922 ++++++++++++++++++
 2 files changed, 933 insertions(+)
 create mode 100644 subprojects/ell.wrap
 create mode 100644 subprojects/packagefiles/0001-build-Add-meson-build-system.patch

diff --git a/subprojects/ell.wrap b/subprojects/ell.wrap
new file mode 100644
index 000000000000..f2ff070085f2
--- /dev/null
+++ b/subprojects/ell.wrap
@@ -0,0 +1,11 @@
+[wrap-file]
+directory = ell-0.82
+source_url = https://www.kernel.org/pub/linux/libs/ell/ell-0.82.tar.xz
+source_filename = ell-0.82.tar.xz
+source_hash = 133a9273151090b4cc37676063cc00ff350a093032ba20a2aaf8b3c5fd6f8b6d
+
+# Maintained in https://github.com/hadess/ell/tree/wip/hadess/add-meson
+diff_files = 0001-build-Add-meson-build-system.patch
+
+[provide]
+dependency_names = ell
diff --git a/subprojects/packagefiles/0001-build-Add-meson-build-system.patch b/subprojects/packagefiles/0001-build-Add-meson-build-system.patch
new file mode 100644
index 000000000000..c06896515795
--- /dev/null
+++ b/subprojects/packagefiles/0001-build-Add-meson-build-system.patch
@@ -0,0 +1,922 @@
+From 84f74ee4f0e199a6e80719f9eb365c45be405d96 Mon Sep 17 00:00:00 2001
+From: Bastien Nocera <hadess@hadess.net>
+Date: Thu, 17 Jul 2025 11:31:22 +0200
+Subject: [PATCH] build: Add meson build system
+
+--enable-pie is replaced by the meson base option "b_pie":
+https://mesonbuild.com/Builtin-options.html#base-options
+
+--enable-maintainer-mode is replaced by the "debug" build-types:
+https://mesonbuild.com/Builtin-options.html#core-options
+
+--enable-debug and --disable-optimization are replaced by the
+debug build type:
+https://mesonbuild.com/Builtin-options.html#details-for-buildtype
+
+Each of the sanitisers have their own b_sanitize option:
+- asan: b_sanitize=address
+- lsan: b_sanitize=leak
+- ubsan: b_sanitize=address,undefined
+https://mesonbuild.com/Builtin-options.html#base-options
+
+--enable-coverage is replaced by the b_coverage option:
+https://mesonbuild.com/Builtin-options.html#base-options
+---
+ ell/meson.build              | 187 +++++++++++++
+ examples/meson.build         |  32 +++
+ meson.build                  |  69 +++++
+ meson_options.txt            |   5 +
+ tools/meson.build            |  18 ++
+ unit/gen-cert-expired-pem.sh |  14 +
+ unit/meson.build             | 501 +++++++++++++++++++++++++++++++++++
+ unit/xxd.sh                  |   3 +
+ 8 files changed, 829 insertions(+)
+ create mode 100644 ell/meson.build
+ create mode 100644 examples/meson.build
+ create mode 100644 meson.build
+ create mode 100644 meson_options.txt
+ create mode 100644 tools/meson.build
+ create mode 100755 unit/gen-cert-expired-pem.sh
+ create mode 100644 unit/meson.build
+ create mode 100755 unit/xxd.sh
+
+diff --git a/ell/meson.build b/ell/meson.build
+new file mode 100644
+index 000000000000..ba61a39a8cc3
+--- /dev/null
++++ b/ell/meson.build
+@@ -0,0 +1,187 @@
++lib_headers = [
++  'ell.h',
++  'util.h',
++  'test.h',
++  'strv.h',
++  'utf8.h',
++  'queue.h',
++  'hashmap.h',
++  'string.h',
++  'settings.h',
++  'main.h',
++  'idle.h',
++  'signal.h',
++  'timeout.h',
++  'io.h',
++  'ringbuf.h',
++  'log.h',
++  'checksum.h',
++  'netlink.h',
++  'genl.h',
++  'rtnl.h',
++  'dbus.h',
++  'dbus-service.h',
++  'dbus-client.h',
++  'hwdb.h',
++  'cipher.h',
++  'random.h',
++  'uintset.h',
++  'base64.h',
++  'pem.h',
++  'tls.h',
++  'uuid.h',
++  'key.h',
++  'file.h',
++  'dir.h',
++  'net.h',
++  'dhcp.h',
++  'dhcp6.h',
++  'cert.h',
++  'ecc.h',
++  'ecdh.h',
++  'time.h',
++  'gpio.h',
++  'path.h',
++  'icmp6.h',
++  'acd.h',
++  'tester.h',
++  'cleanup.h',
++  'netconfig.h',
++  'sysctl.h',
++  'minheap.h',
++  'notifylist.h'
++]
++
++lib_sources = [
++  'private.h',
++  'useful.h',
++  'missing.h',
++  'util.c',
++  'test-private.h',
++  'test.c',
++  'test-dbus.c',
++  'strv.c',
++  'utf8.c',
++  'queue.c',
++  'hashmap.c',
++  'string.c',
++  'settings.c',
++  'main-private.h',
++  'main.c',
++  'idle.c',
++  'signal.c',
++  'timeout.c',
++  'io.c',
++  'ringbuf.c',
++  'log.c',
++  'checksum.c',
++  'netlink-private.h',
++  'netlink.c',
++  'genl.c',
++  'rtnl-private.h',
++  'rtnl.c',
++  'dbus-private.h',
++  'dbus.c',
++  'dbus-message.c',
++  'dbus-util.c',
++  'dbus-service.c',
++  'dbus-client.c',
++  'dbus-name-cache.c',
++  'dbus-filter.c',
++  'gvariant-private.h',
++  'gvariant-util.c',
++  'siphash-private.h',
++  'siphash.c',
++  'hwdb.c',
++  'cipher.c',
++  'random.c',
++  'uintset.c',
++  'base64.c',
++  'asn1-private.h',
++  'pem-private.h',
++  'pem.c',
++  'tls-private.h',
++  'tls.c',
++  'tls-record.c',
++  'tls-extensions.c',
++  'tls-suites.c',
++  'uuid.c',
++  'key.c',
++  'file.c',
++  'dir.c',
++  'net-private.h',
++  'net.c',
++  'dhcp-private.h',
++  'dhcp.c',
++  'dhcp-transport.c',
++  'dhcp-lease.c',
++  'dhcp6-private.h',
++  'dhcp6.c',
++  'dhcp6-transport.c',
++  'dhcp6-lease.c',
++  'dhcp-util.c',
++  'dhcp-server.c',
++  'cert-private.h',
++  'cert.c',
++  'cert-crypto.c',
++  'ecc-private.h',
++  'ecc.h',
++  'ecc-external.c',
++  'ecc.c',
++  'ecdh.c',
++  'time.c',
++  'time-private.h',
++  'gpio.c',
++  'path.c',
++  'icmp6.c',
++  'icmp6-private.h',
++  'acd.c',
++  'tester.c',
++  'netconfig.c',
++  'sysctl.c',
++  'minheap.c',
++  'notifylist.c'
++]
++
++linux_headers = [
++  '../linux/gpio.h'
++]
++
++libell = library('ell',
++  sources: lib_headers + lib_sources + linux_headers,
++  include_directories: include_directories('..'),
++  implicit_include_directories: false,
++  gnu_symbol_visibility: 'hidden',
++  version: '0.2.0',
++  install: get_option('default_library') != 'static'
++)
++
++libell_dep = declare_dependency(include_directories: include_directories('..'),
++  link_with: libell)
++
++if get_option('default_library') != 'static'
++  install_headers(lib_headers,
++    subdir: 'ell'
++  )
++
++  pkgconfig = import('pkgconfig')
++  pkgconfig.generate(
++    name: 'ell',
++    description: 'Embedded Linux library',
++    version: meson.project_version(),
++    libraries: libell,
++    # FIXME requires meson 1.9.0
++    # license: meson.project_license()
++  )
++endif
++
++libell_private = static_library('ell_private',
++  sources: lib_headers + lib_sources + linux_headers,
++  include_directories: include_directories('..'),
++  implicit_include_directories: false,
++  install: false
++)
++
++libell_private_dep = declare_dependency(
++  link_with: libell_private
++)
+diff --git a/examples/meson.build b/examples/meson.build
+new file mode 100644
+index 000000000000..831abb58be46
+--- /dev/null
++++ b/examples/meson.build
+@@ -0,0 +1,32 @@
++examples = [
++  'dbus-service',
++  'https-client-test',
++  'https-server-test',
++  'dbus-client',
++  'dhcp-client',
++  'dhcp6-client',
++  'dhcp-server',
++  'acd-client',
++  'netconfig-test',
++]
++
++if get_option('examples')
++  foreach example: examples
++    exe = executable(example,
++      '@0@.c'.format(example),
++      include_directories: include_directories('..'),
++      dependencies: libell_private_dep,
++      install: false
++    )
++  endforeach
++endif
++
++if get_option('glib') and get_option('examples')
++  exe = executable('glib-eventloop',
++    'glib-eventloop.c',
++    include_directories: include_directories('..'),
++    dependencies: [ libell_private_dep, glib_dep ],
++    install: false
++  )
++endif
++
+diff --git a/meson.build b/meson.build
+new file mode 100644
+index 000000000000..8427d4fe6ec9
+--- /dev/null
++++ b/meson.build
+@@ -0,0 +1,69 @@
++# SPDX-License-Identifier: LGPL-2.1-or-later
++project(
++  'ell', 'c',
++  version: '0.82',
++  license: 'LGPL-2.1-or-later',
++  default_options: [
++    'buildtype=debugoptimized',
++    'default_library=shared'
++  ],
++  meson_version: '>= 1.3.0'
++)
++
++cc = meson.get_compiler('c')
++find_program('awk', required: true)
++config_h = configuration_data()
++
++checked_funcs = [
++  'explicit_bzero',
++  'rawmemchr',
++]
++
++foreach func: checked_funcs
++  config_h.set('HAVE_' + func.to_upper(), cc.has_function(func))
++endforeach
++
++required_funcs = [
++  'getrandom',
++  'signalfd',
++  'timerfd_create',
++  'epoll_create'
++]
++
++foreach func: required_funcs
++  cc.has_function(func, required: true)
++endforeach
++
++required_headers = [
++  'linux/types.h',
++  'linux/if_alg.h'
++]
++
++foreach header: required_headers
++  cc.has_header(header, required: true)
++endforeach
++
++glib_dep = dependency('glib-2.0', version: '>= 2.32', required: get_option('glib'))
++openssl = find_program('openssl', required: get_option('cert-tests'))
++sh = find_program('sh', required: get_option('cert-tests'))
++xxd = find_program('xxd', required: get_option('cert-tests'))
++
++if openssl.found()
++  r = run_command(openssl, 'list', '-providers', check: false)
++  if r.returncode() == 0
++    openssl_legacy = [ '-provider', 'legacy', '-provider', 'default' ]
++  else
++    openssl_legacy = ''
++  endif
++endif
++
++add_project_arguments('-DHAVE_CONFIG_H', language: 'c')
++configure_file(
++  output: 'config.h',
++  configuration: config_h
++)
++
++subdir('ell')
++subdir('tools')
++subdir('unit')
++subdir('examples')
+diff --git a/meson_options.txt b/meson_options.txt
+new file mode 100644
+index 000000000000..5effa15916b4
+--- /dev/null
++++ b/meson_options.txt
+@@ -0,0 +1,5 @@
++option('glib', type: 'boolean', value: true, description: 'Enable ell/glib main loop example')
++option('tests', type: 'boolean', value: true, description: 'Enable unit tests compilation')
++option('cert-tests', type: 'boolean', value: true, description: 'Enable OpenSSL cert tests')
++option('tools', type: 'boolean', value: true, description: 'Enable extra tools compilation')
++option('examples', type: 'boolean', value: true, description: 'Enable code examples compilation')
+diff --git a/tools/meson.build b/tools/meson.build
+new file mode 100644
+index 000000000000..01505f266631
+--- /dev/null
++++ b/tools/meson.build
+@@ -0,0 +1,18 @@
++tools = [
++  'certchain-verify',
++  'genl-discover',
++  'genl-watch',
++  'genl-request',
++  'gpio'
++]
++
++if get_option('tools')
++  foreach tool: tools
++      exe = executable(tool,
++        '@0@.c'.format(tool),
++        include_directories: include_directories('..'),
++        dependencies: libell_private_dep,
++        install: false
++      )
++  endforeach
++endif
+diff --git a/unit/gen-cert-expired-pem.sh b/unit/gen-cert-expired-pem.sh
+new file mode 100755
+index 000000000000..f3e0b9d28424
+--- /dev/null
++++ b/unit/gen-cert-expired-pem.sh
+@@ -0,0 +1,14 @@
++#!/bin/sh -e
++
++echo -n > cert-ca-index.txt
++OUTDIR=`mktemp -d`
++openssl ca -batch \
++	-config "$4" -name example \
++	-cert "$2" \
++	-keyfile "$3" \
++	-outdir $OUTDIR \
++	-rand_serial -extensions cert_ext \
++	-extfile "$5" -md sha256 \
++	-startdate 000101120000Z -enddate 010101120000Z \
++	-preserveDN -notext -in "$1" -out "$6"
++rm -rf $OUTDIR cert-ca-index.txt*
+diff --git a/unit/meson.build b/unit/meson.build
+new file mode 100644
+index 000000000000..be9f2655f010
+--- /dev/null
++++ b/unit/meson.build
+@@ -0,0 +1,501 @@
++tests = [
++  'test-unit',
++  'test-queue',
++  'test-hashmap',
++  'test-endian',
++  'test-string',
++  'test-utf8',
++  'test-main',
++  'test-io',
++  'test-ringbuf',
++  'test-checksum',
++  'test-settings',
++  'test-netlink',
++  'test-genl-msg',
++  'test-rtnl',
++  'test-siphash',
++  'test-cipher',
++  'test-random',
++  'test-util',
++  'test-uintset',
++  'test-base64',
++  'test-uuid',
++  'test-pbkdf2',
++  'test-dhcp',
++  'test-dhcp6',
++  'test-dir-watch',
++  'test-ecc',
++  'test-ecdh',
++  'test-time',
++  'test-path',
++  'test-net',
++  'test-sysctl',
++  'test-minheap',
++  'test-notifylist',
++  'test-hwdb',
++  'test-dbus',
++  'test-dbus-util',
++  'test-dbus-message',
++  'test-dbus-message-fds',
++  'test-dbus-properties',
++  'test-dbus-service',
++  'test-dbus-watch',
++  'test-gvariant-util',
++  'test-gvariant-message'
++]
++
++gen_headers = []
++test_pem_cert_deps = []
++
++if get_option('cert-tests')
++  tests += [
++    'test-pem',
++    'test-tls',
++    'test-key'
++  ]
++
++  cert_server_key_pem_tgt = custom_target('cert-server-key.pem',
++    output: 'cert-server-key.pem',
++    command: [ openssl, 'genrsa', '-out', '@OUTPUT@' ]
++  )
++
++  ec_cert_server_key_pem_tgt = custom_target('ec-cert-server-key.pem',
++    output: 'ec-cert-server-key.pem',
++    command: [ openssl, 'ecparam', '-out', '@OUTPUT@', '-name', 'secp384r1', '-genkey' ]
++  )
++
++  cert_server_key_pkcs8_pem_tgt = custom_target('cert-server-key-pkcs8.pem',
++    input: cert_server_key_pem_tgt,
++    output: 'cert-server-key-pkcs8.pem',
++    command: [ openssl, 'pkcs8', '-topk8', '-nocrypt', '-in', '@INPUT@', '-out', '@OUTPUT@' ]
++  )
++
++  cert_server_csr_tgt = custom_target('cert-server.csr',
++    input: cert_server_key_pem_tgt,
++    output: 'cert-server.csr',
++    command: [ openssl, 'req', '-new', '-extensions', 'cert_ext',
++      '-config', files('gencerts.cnf'),
++      '-subj', '/O=Foo Example Organization/CN=Foo Example Organization/emailAddress=foo@mail.example',
++      '-key', '@INPUT@', '-out', '@OUTPUT@' ]
++  )
++
++  ec_cert_server_csr_tgt = custom_target('ec-cert-server.csr',
++    input: ec_cert_server_key_pem_tgt,
++    output: 'ec-cert-server.csr',
++    command: [ openssl, 'req', '-new', '-extensions', 'cert_ext',
++      '-config', files('gencerts.cnf'),
++      '-subj', '/O=Foo Example Organization/CN=Foo Example Organization/emailAddress=foo@mail.example',
++      '-key', '@INPUT@', '-out', '@OUTPUT@' ]
++  )
++
++  cert_ca_key_pem_tgt = custom_target('cert-ca-key.pem',
++    output: 'cert-ca-key.pem',
++    command: [ openssl, 'genrsa', '-out', '@OUTPUT@', '2048' ]
++  )
++
++  cert_ca_pem_tgt = custom_target('cert-ca.pem',
++    input: cert_ca_key_pem_tgt,
++    output: 'cert-ca.pem',
++    command: [ openssl, 'req', '-x509', '-new', '-nodes', '-extensions', 'ca_ext',
++      '-config', files('gencerts.cnf'), '-subj', '/O=International Union of Example Organizations/CN=Certificate issuer guy/emailAddress=ca@mail.example',
++      '-key', '@INPUT@', '-sha256', '-days', '10000', '-out', '@OUTPUT@' ],
++  )
++
++  cert_server_pem_tgt = custom_target('cert-server.pem',
++    input: [ cert_server_csr_tgt, cert_ca_pem_tgt, cert_ca_key_pem_tgt ],
++    output: 'cert-server.pem',
++    command: [ openssl, 'x509', '-req', '-extensions', 'server_ext',
++      '-extfile', files('gencerts.cnf'),
++      '-in', '@INPUT0@', '-CA', '@INPUT1@',
++      '-CAkey', '@INPUT2@',
++      '-CAserial', 'cert-ca.srl',
++      '-CAcreateserial', '-sha256', '-days', '10000', '-out', '@OUTPUT@' ]
++  )
++
++  ec_cert_ca_key_pem_tgt = custom_target('ec-cert-ca-key.pem',
++    output: 'ec-cert-ca-key.pem',
++    command: [ openssl, 'ecparam', '-out', '@OUTPUT@', '-name', 'secp384r1', '-genkey' ]
++  )
++
++  ec_cert_ca_pem_tgt = custom_target('ec-cert-ca.pem',
++  input: ec_cert_ca_key_pem_tgt,
++  output: 'ec-cert-ca.pem',
++  command: [ openssl, 'req', '-x509', '-new', '-nodes', '-extensions', 'ca_ext',
++    '-config', files('gencerts.cnf'), '-subj', '/O=International Union of Example Organizations/CN=Certificate issuer guy/emailAddress=ca@mail.example',
++    '-key', '@INPUT@', '-sha256', '-days', '10000', '-out', '@OUTPUT@' ]
++  )
++
++  ec_cert_server_pem_tgt = custom_target('ec-cert-server.pem',
++    input: [ ec_cert_server_csr_tgt, ec_cert_ca_pem_tgt, ec_cert_ca_key_pem_tgt ],
++    output: 'ec-cert-server.pem',
++    command: [ openssl, 'x509', '-req', '-extensions', 'server_ext',
++      '-extfile', files('gencerts.cnf'),
++      '-in', '@INPUT0@', '-CA', '@INPUT1@',
++      '-CAkey', '@INPUT2@',
++      '-CAserial', 'cert-ca.srl',
++      '-CAcreateserial', '-sha256', '-days', '10000', '-out', '@OUTPUT@' ]
++  )
++
++  cert_client_key_pkcs1_pem_tgt = custom_target('cert-client-key-pkcs1.pem',
++    output: 'cert-client-key-pkcs1.pem',
++    command: [ openssl, 'genrsa', '-out', '@OUTPUT@' ]
++  )
++
++  cert_client_key_pkcs8_pem_tgt = custom_target('cert-client-key-pkcs8.pem',
++    input: cert_client_key_pkcs1_pem_tgt,
++    output: 'cert-client-key-pkcs8.pem',
++    command: [ openssl, 'pkcs8', '-topk8', '-nocrypt', '-in', '@INPUT@', '-out', '@OUTPUT@' ]
++  )
++
++  cert_client_key_pkcs8_md5_des_pem_tgt = custom_target('cert-client-key-pkcs8-md5-des.pem',
++    input: cert_client_key_pkcs8_pem_tgt,
++    output: 'cert-client-key-pkcs8-md5-des.pem',
++    command: [ openssl, 'pkcs8', '-in', '@INPUT@', '-out', '@OUTPUT@',
++      '-topk8', '-v1', 'PBE-MD5-DES', '-passout', 'pass:abc', openssl_legacy ]
++  )
++
++  cert_client_key_pkcs8_v2_des_pem_tgt = custom_target('cert-client-key-pkcs8-v2-des.pem',
++    input: cert_client_key_pkcs8_pem_tgt,
++    output: 'cert-client-key-pkcs8-v2-des.pem',
++    command: [ openssl, 'pkcs8', '-in', '@INPUT@', '-out', '@OUTPUT@',
++      '-topk8', '-v2', 'des-cbc', '-v2prf', 'hmacWithSHA1', '-passout', 'pass:abc',
++      openssl_legacy ]
++  )
++
++  cert_client_key_pkcs8_v2_des_ede3_pem_tgt = custom_target('cert-client-key-pkcs8-v2-des-ede3.pem',
++    input: cert_client_key_pkcs8_pem_tgt,
++    output: 'cert-client-key-pkcs8-v2-des-ede3.pem',
++    command: [ openssl, 'pkcs8', '-in', '@INPUT@', '-out', '@OUTPUT@',
++        '-topk8', '-v2', 'des-ede3-cbc', '-v2prf', 'hmacWithSHA224', '-passout', 'pass:abc' ]
++  )
++
++  cert_client_key_pkcs8_v2_aes128_pem_tgt = custom_target('cert-client-key-pkcs8-v2-aes128.pem',
++    input: cert_client_key_pkcs8_pem_tgt,
++    output: 'cert-client-key-pkcs8-v2-aes128.pem',
++    command: [ openssl, 'pkcs8', '-in', '@INPUT@', '-out', '@OUTPUT@',
++      '-topk8', '-v2', 'aes128', '-v2prf', 'hmacWithSHA256', '-passout', 'pass:abc' ]
++  )
++
++  cert_client_key_pkcs8_sha1_des_pem_tgt = custom_target('cert-client-key-pkcs8-sha1-des.pem',
++    input: cert_client_key_pkcs8_pem_tgt,
++    output: 'cert-client-key-pkcs8-sha1-des.pem',
++    command: [ openssl, 'pkcs8', '-in', '@INPUT@', '-out', '@OUTPUT@',
++      '-topk8', '-v1', 'PBE-SHA1-DES', '-passout', 'pass:abc', openssl_legacy ]
++  )
++
++  cert_client_key_pkcs8_v2_aes256_pem_tgt = custom_target('cert-client-key-pkcs8-v2-aes256.pem',
++    input: cert_client_key_pkcs8_pem_tgt,
++    output: 'cert-client-key-pkcs8-v2-aes256.pem',
++    command: [ openssl, 'pkcs8', '-in', '@INPUT@', '-out', '@OUTPUT@',
++      '-topk8', '-v2', 'aes256', '-v2prf', 'hmacWithSHA512', '-passout', 'pass:abc' ]
++  )
++
++  cert_client_csr_tgt = custom_target('cert-client.csr',
++    input: cert_client_key_pkcs1_pem_tgt,
++    output: 'cert-client.csr',
++    command: [ openssl, 'req', '-new', '-extensions', 'cert_ext',
++      '-config', files('gencerts.cnf'),
++      '-subj', '/O=Bar Example Organization/CN=Bar Example Organization/emailAddress=bar@mail.example',
++      '-key', '@INPUT@', '-out', '@OUTPUT@'
++    ]
++  )
++
++  cert_client_pem_tgt = custom_target('cert-client.pem',
++    input: [ cert_client_csr_tgt, cert_ca_pem_tgt, cert_ca_key_pem_tgt ],
++    output: 'cert-client.pem',
++    command: [ openssl, 'x509', '-req', '-extensions', 'cert_ext',
++      '-extfile', files('gencerts.cnf'),
++      '-in', '@INPUT0@', '-CA', '@INPUT1@',
++      '-CAkey', '@INPUT2@',
++      '-CAserial', 'cert-ca.srl',
++      '-CAcreateserial', '-sha256', '-days', '10000', '-out', '@OUTPUT@' ]
++  )
++
++  cert_client_crt_tgt = custom_target('cert-client.crt',
++    input: cert_client_pem_tgt,
++    output: 'cert-client.crt',
++    command: [ openssl, 'x509', '-in', '@INPUT@', '-out', '@OUTPUT@', '-outform', 'der' ]
++  )
++
++  cert_intca_key_pem_tgt = custom_target('cert-intca-key.pem',
++    output: 'cert-intca-key.pem',
++    command: [ openssl, 'genrsa', '-out', '@OUTPUT@' ]
++  )
++
++  cert_intca_csr_tgt = custom_target('cert-intca.csr',
++    input: cert_intca_key_pem_tgt,
++    output: 'cert-intca.csr',
++    command: [ openssl, 'req', '-new', '-extensions', 'int_ext',
++      '-config', files('gencerts.cnf'),
++      '-subj', '/O=International Union of Example Organizations/CN=Certificate issuer guy/emailAddress=ca@mail.example',
++      '-key', '@INPUT@', '-out', '@OUTPUT@' ]
++  )
++
++  cert_intca_pem_tgt = custom_target('cert-intca.pem',
++    input: [ cert_intca_csr_tgt, cert_ca_pem_tgt, cert_ca_key_pem_tgt ],
++    output: 'cert-intca.pem',
++    command: [ openssl, 'x509', '-req', '-extensions', 'int_ext',
++      '-extfile', files('gencerts.cnf'),
++      '-in', '@INPUT0@', '-CA', '@INPUT1@',
++      '-CAkey', '@INPUT2@',
++      '-CAserial', 'cert-ca.srl',
++      '-CAcreateserial', '-sha256', '-days', '10000', '-out', '@OUTPUT@' ]
++  )
++
++  cat = find_program('cat')
++
++  cert_chain_pem_tgt = custom_target('cert-chain.pem',
++    input: [ cert_intca_pem_tgt, cert_ca_pem_tgt ],
++    output: 'cert-chain.pem',
++    capture: true,
++    command: [ cat, '@INPUT@' ]
++  )
++
++  cert_entity_int_key_pem_tgt = custom_target('cert-entity-int-key.pem',
++    output: 'cert-entity-int-key.pem',
++    command: [ openssl, 'genrsa', '-out', '@OUTPUT@' ]
++  )
++
++  cert_entity_int_csr_tgt = custom_target('cert-entity-int.csr',
++    input: cert_entity_int_key_pem_tgt,
++    output: 'cert-entity-int.csr',
++    command: [ openssl, 'req', '-new', '-extensions', 'cert_ext',
++      '-config', files('gencerts.cnf'),
++      '-subj', '/O=Baz Example Organization/CN=Baz Example Organization/emailAddress=baz@mail.example',
++      '-key', '@INPUT@', '-out', '@OUTPUT@' ]
++  )
++
++  cert_entity_int_pem_tgt = custom_target('cert-entity-int.pem',
++    input: [ cert_entity_int_csr_tgt, cert_intca_pem_tgt, cert_intca_key_pem_tgt ],
++    output: 'cert-entity-int.pem',
++    command: [ openssl, 'x509', '-req', '-extensions', 'cert_ext',
++      '-extfile', files('gencerts.cnf'),
++      '-in', '@INPUT0@', '-CA', '@INPUT1@',
++      '-CAkey', '@INPUT2@',
++      '-CAserial', 'cert-intca.srl',
++      '-CAcreateserial', '-sha256', '-days', '10000', '-out', '@OUTPUT@' ]
++  )
++
++  cert_ca2_pem_tgt = custom_target('cert-ca2.pem',
++    input: cert_ca_key_pem_tgt,
++    output: 'cert-ca2.pem',
++    command: [ openssl, 'req', '-x509', '-new', '-nodes', '-extensions', 'ca_no_akid_ext',
++      '-config', files('gencerts.cnf'),
++      '-subj', '/O=International Union of Example Organizations/CN=Certificate issuer guy/emailAddress=ca-no-akid@mail.example',
++      '-key', '@INPUT@', '-sha256', '-days', '10000', '-out', '@OUTPUT@' ]
++  )
++
++  cert_client_key_pkcs1_des_pem_tgt = custom_target('cert-client-key-pkcs1-des.pem',
++    input: cert_client_key_pkcs1_pem_tgt,
++    output: 'cert-client-key-pkcs1-des.pem',
++    command: [ openssl, 'rsa', '-in', '@INPUT@', '-out', '@OUTPUT@', '-des', '-passout', 'pass:abc', openssl_legacy ],
++  )
++
++  cert_no_keyid_csr_tgt = custom_target('cert-no-keyid.csr',
++    input: cert_client_key_pkcs1_pem_tgt,
++    output: 'cert-no-keyid.csr',
++    command: [ openssl, 'req', '-new',
++      '-config', files('gencerts.cnf'),
++      '-subj', '/O=Baz Example Organization/CN=Baz Example Organization/emailAddress=baz@mail.example',
++      '-key', '@INPUT@', '-out', '@OUTPUT@' ]
++  )
++
++  cert_no_keyid_pem_tgt = custom_target('cert-no-keyid.pem',
++    input: [ cert_no_keyid_csr_tgt, cert_ca2_pem_tgt, cert_ca_key_pem_tgt ],
++    output: 'cert-no-keyid.pem',
++    command: [ openssl, 'x509', '-req', '-extensions', 'no_keyid_ext',
++      '-extfile', files('gencerts.cnf'),
++      '-in', '@INPUT0@', '-CA', '@INPUT1@',
++      '-CAkey', '@INPUT2@',
++      '-CAserial', 'cert-ca2.srl',
++      '-CAcreateserial', '-sha256', '-days', '10000', '-out', '@OUTPUT@' ]
++  )
++
++  cert_expired_csr_tgt = custom_target('cert-expired.csr',
++    input: cert_client_key_pkcs1_pem_tgt,
++    output: 'cert-expired.csr',
++    command: [ openssl, 'req', '-new', '-extensions', 'cert_ext',
++      '-config', files('gencerts.cnf'),
++      '-subj', '/O=Bar Example Organization/CN=Bar Example Organization/emailAddress=bar@mail.example',
++      '-key', '@INPUT@', '-out', '@OUTPUT@' ]
++  )
++
++  echo = find_program('echo')
++
++  cert_ca_cnf_tgt = custom_target('cert-ca.cnf',
++    output: 'cert-ca.cnf',
++    capture: true,
++    command: [ echo, '-e',
++      '[example]\ndatabase=cert-ca-index.txt\nserial=cert-ca.srl\npolicy=dummy\n[dummy]' ]
++  )
++
++  cert_entity_pkcs12_rc2_sha1_p12_tgt = custom_target('cert-entity-pkcs12-rc2-sha1.p12',
++    input: [ cert_entity_int_key_pem_tgt, cert_entity_int_pem_tgt, cert_chain_pem_tgt ],
++    output: 'cert-entity-pkcs12-rc2-sha1.p12',
++    command: [ openssl, 'pkcs12', '-inkey', '@INPUT0@', '-in', '@INPUT1@', '-certfile', '@INPUT2@', '-out', '@OUTPUT@', '-export',
++      '-passout', 'pass:abc', '-certpbe', 'PBE-SHA1-RC2-40', '-keypbe', 'PBE-SHA1-RC2-128', '-macalg', 'sha1', openssl_legacy ]
++  )
++
++  cert_entity_combined_pem_tgt = custom_target('cert-entity-combined.pem',
++    input: cert_entity_pkcs12_rc2_sha1_p12_tgt,
++    output: 'cert-entity-combined.pem',
++    command: [ openssl, 'pkcs12', '-in', '@INPUT@', '-out', '@OUTPUT@', '-passin', 'pass:abc', '-passout', 'pass:abc', openssl_legacy ]
++  )
++
++  gen_cert_expired_pem_sh = find_program('gen-cert-expired-pem.sh')
++  cert_expired_pem_tgt = custom_target('cert-expired.pem',
++    input: [ cert_expired_csr_tgt, cert_ca_pem_tgt, cert_ca_key_pem_tgt, cert_ca_cnf_tgt, files('gencerts.cnf') ],
++    output: 'cert-expired.pem',
++    command: [ gen_cert_expired_pem_sh, '@INPUT0@', '@INPUT1@', '@INPUT2@', '@INPUT3@', '@INPUT4@', '@OUTPUT@' ]
++  )
++
++  cert_entity_pkcs12_nomac_p12_tgt = custom_target('cert-entity-pkcs12-nomac.p12',
++    input: [ cert_entity_int_key_pem_tgt, cert_entity_int_pem_tgt ],
++    output: 'cert-entity-pkcs12-nomac.p12',
++    command: [ openssl, 'pkcs12', '-inkey', '@INPUT0@', '-in', '@INPUT1@', '-out', '@OUTPUT@', '-export', '-passout', 'pass:abc', '-nomac' ]
++  )
++
++  cert_entity_pkcs12_des_sha256_p12_tgt = custom_target('cert-entity-pkcs12-des-sha256.p12',
++    input: [ cert_entity_int_key_pem_tgt, cert_entity_int_pem_tgt, cert_chain_pem_tgt ],
++    output: 'cert-entity-pkcs12-des-sha256.p12',
++    command: [ openssl, 'pkcs12', '-inkey', '@INPUT0@', '-in', '@INPUT1@', '-certfile', '@INPUT2@', '-out', '@OUTPUT@', '-export',
++      '-passout', 'pass:abc', '-certpbe', 'PBE-SHA1-3DES', '-keypbe', 'PBE-SHA1-2DES', '-macalg', 'sha256' ]
++  )
++
++  cert_entity_pkcs12_rc4_sha384_p12_tgt = custom_target('cert-entity-pkcs12-rc4-sha384.p12',
++    input: [ cert_entity_int_key_pem_tgt, cert_entity_int_pem_tgt, cert_chain_pem_tgt ],
++    output: 'cert-entity-pkcs12-rc4-sha384.p12',
++    command: [ openssl, 'pkcs12', '-inkey', '@INPUT0@', '-in', '@INPUT1@', '-certfile', '@INPUT2@', '-out', '@OUTPUT@', '-export',
++      '-passout', 'pass:abc', '-certpbe', 'PBE-SHA1-RC4-128', '-keypbe', 'PBE-SHA1-RC2-40', '-macalg', 'sha384', openssl_legacy ]
++  )
++
++  cert_entity_pkcs12_pkcs5_sha512_p12_tgt = custom_target('cert-entity-pkcs12-pkcs5-sha512.p12',
++    input: [ cert_entity_int_key_pem_tgt, cert_entity_int_pem_tgt, cert_chain_pem_tgt ],
++    output: 'cert-entity-pkcs12-pkcs5-sha512.p12',
++    command: [ openssl, 'pkcs12', '-inkey', '@INPUT0@', '-in', '@INPUT1@', '-certfile', '@INPUT2@', '-out', '@OUTPUT@', '-export',
++      '-passout', 'pass:abc', '-certpbe', 'des-cbc', '-keypbe', 'des-cbc', '-macalg', 'sha512', openssl_legacy ]
++  )
++
++  key_ciphertext_dat_tgt = custom_target('key-ciphertext.dat',
++    input: [ 'plaintext.txt', cert_client_pem_tgt ],
++    output: 'key-ciphertext.dat',
++    command: [ openssl, 'rsautl', '-encrypt', '-pkcs', '-in', '@INPUT0@', '-certin',
++      '-inkey', '@INPUT1@', '-out', '@OUTPUT@' ]
++  )
++
++  key_signature_dat_tgt = custom_target('key-signature.dat',
++    input: 'plaintext.txt',
++    output: 'key-signature.dat',
++    command: [ openssl, 'rsautl', '-sign', '-pkcs', '-in', '@INPUT@',
++      '-inkey', cert_client_key_pkcs1_pem_tgt, '-out', '@OUTPUT@' ]
++  )
++
++  pem_rsa_files = [
++    [ 'cert-client-key-pkcs1-des3.pem', '-des3' ],
++    [ 'cert-client-key-pkcs1-aes128.pem', '-aes128' ],
++    [ 'cert-client-key-pkcs1-aes192.pem', '-aes192' ],
++    [ 'cert-client-key-pkcs1-aes256.pem', '-aes256' ],
++  ]
++
++  foreach file: pem_rsa_files
++    test_pem_cert_deps += custom_target(file[0],
++      input: cert_client_key_pkcs1_pem_tgt,
++      output: file[0],
++      command: [ openssl, 'rsa', '-in', '@INPUT@', '-out', '@OUTPUT@', file[1], '-passout', 'pass:abc' ]
++    )
++  endforeach
++
++  built_headers = [
++    [ 'plaintext.txt', 'key-plaintext.h' ],
++    [ key_ciphertext_dat_tgt, 'key-ciphertext.h' ],
++    [ key_signature_dat_tgt, 'key-signature.h' ]
++  ]
++
++  xxd_sh = find_program('xxd.sh')
++
++  foreach header: built_headers
++    gen_headers += custom_target(header[1],
++      input: header[0],
++      output: header[1],
++      command: [ xxd_sh,'@INPUT@', '@OUTPUT@' ]
++    )
++  endforeach
++
++  cert_verifs = [
++    [ cert_server_pem_tgt, cert_ca_pem_tgt ],
++    [ ec_cert_server_pem_tgt, ec_cert_ca_pem_tgt ],
++    [ cert_client_pem_tgt, cert_ca_pem_tgt ],
++    [ cert_intca_pem_tgt, cert_ca_pem_tgt ],
++    [ cert_entity_int_pem_tgt, cert_chain_pem_tgt ],
++    [ cert_no_keyid_pem_tgt, cert_ca2_pem_tgt ]
++  ]
++  foreach cert_verif: cert_verifs
++    test(cert_verif[0].full_path() + '-verify',
++      openssl,
++      args: [ 'verify', '-CAfile', cert_verif[1].full_path(), cert_verif[0].full_path() ]
++    )
++  endforeach
++
++  test_pem_cert_deps += [
++    # test_pem
++    cert_client_key_pkcs8_md5_des_pem_tgt,
++    cert_client_key_pkcs8_sha1_des_pem_tgt,
++    cert_client_key_pkcs8_v2_des_pem_tgt,
++    cert_client_key_pkcs8_v2_des_ede3_pem_tgt,
++    cert_client_key_pkcs8_v2_aes128_pem_tgt,
++    cert_client_key_pkcs8_v2_aes256_pem_tgt,
++    cert_client_key_pkcs1_des_pem_tgt,
++    cert_client_key_pkcs1_pem_tgt,
++    cert_client_key_pkcs1_des_pem_tgt,
++    cert_client_key_pkcs8_pem_tgt,
++    cert_client_key_pkcs8_sha1_des_pem_tgt,
++    cert_client_pem_tgt,
++    cert_client_crt_tgt,
++    cert_entity_combined_pem_tgt,
++    cert_entity_pkcs12_nomac_p12_tgt,
++    cert_entity_pkcs12_rc2_sha1_p12_tgt,
++    cert_entity_pkcs12_des_sha256_p12_tgt,
++    cert_entity_pkcs12_rc4_sha384_p12_tgt,
++    cert_entity_pkcs12_pkcs5_sha512_p12_tgt,
++    # test_key
++    cert_ca_pem_tgt,
++    cert_server_pem_tgt,
++    cert_intca_pem_tgt,
++    cert_entity_int_pem_tgt,
++    cert_client_pem_tgt,
++    cert_client_key_pkcs8_pem_tgt,
++    # test_tls
++    cert_chain_pem_tgt,
++    cert_expired_pem_tgt,
++    cert_ca2_pem_tgt,
++    cert_no_keyid_pem_tgt,
++    ec_cert_ca_pem_tgt,
++    ec_cert_server_pem_tgt,
++    cert_server_key_pkcs8_pem_tgt
++  ]
++endif
++
++# Mocking l_getrandom() to get a stable random seed
++libell_private_ecdh_dep = declare_dependency(
++  link_with: libell_private,
++  link_args: '-Wl,-wrap,l_getrandom'
++)
++
++if get_option('tests') or get_option('cert-tests')
++  foreach test_name: tests
++    dep = test_name == 'test-ecdh' ? libell_private_ecdh_dep : libell_private_dep
++    exe = executable(test_name,
++      [ '@0@.c'.format(test_name), gen_headers, test_pem_cert_deps ],
++      include_directories: include_directories('..'),
++      c_args: [
++        '-DUNITDIR="@0@/unit/"'.format(meson.project_source_root()),
++        '-DCERTDIR="@0@/unit/"'.format(meson.project_build_root())
++      ],
++      dependencies: dep,
++      install: false
++    )
++
++    test(test_name, exe,
++      protocol: 'tap')
++  endforeach
++endif
+diff --git a/unit/xxd.sh b/unit/xxd.sh
+new file mode 100755
+index 000000000000..3fd26d2bce90
+--- /dev/null
++++ b/unit/xxd.sh
+@@ -0,0 +1,3 @@
++#!/bin/sh
++
++xxd -i < "$1" > "$2"
+-- 
+2.52.0
+
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 02/15] build: Add meson build system
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 01/15] build: Add meson wrap for libell Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 03/15] build: Separate systemd and libsystemd dependencies Bastien Nocera
                   ` (12 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

The options default were selected to try and match the existing
autotools default as best as possible. Further commits will likely
change those defaults, but at least it should be clear what defaults
were changed from.

However, a number of options were removed:

--enable-pie is replaced by the meson base option "b_pie":
https://mesonbuild.com/Builtin-options.html#base-options

--enable-maintainer-mode is replaced by the "debug" build-types:
https://mesonbuild.com/Builtin-options.html#core-options

--enable-debug and --disable-optimization are replaced by the
debug build type:
https://mesonbuild.com/Builtin-options.html#details-for-buildtype

Each of the sanitisers have their own b_sanitize option:
- asan: b_sanitize=address
- lsan: b_sanitize=leak
- ubsan: b_sanitize=address,undefined
https://mesonbuild.com/Builtin-options.html#base-options

--enable-coverage is replaced by the b_coverage option:
https://mesonbuild.com/Builtin-options.html#base-options

--enable-valgrind can be replicated by running the test suite with:
meson test --setup valgrind
---
 attrib/meson.build           |  17 ++
 btio/meson.build             |   1 +
 client/btpclient/meson.build |  19 +++
 client/meson.build           |  33 ++++
 completion/meson.build       |   3 +
 doc/meson.build              | 172 ++++++++++++++++++++
 emulator/meson.build         |  34 ++++
 gdbus/meson.build            |  19 +++
 gobex/meson.build            |   8 +
 lib/meson.build              |  53 +++++++
 mesh/meson.build             |  60 +++++++
 meson.build                  | 299 +++++++++++++++++++++++++++++++++++
 meson_options.txt            |  52 ++++++
 monitor/meson.build          |  37 +++++
 obexd/meson.build            | 121 ++++++++++++++
 peripheral/meson.build       |  13 ++
 plugins/meson.build          |  29 ++++
 profiles/meson.build         | 167 +++++++++++++++++++
 src/meson.build              |  88 +++++++++++
 src/shared/meson.build       |  85 ++++++++++
 test/meson.build             |  37 +++++
 tools/mesh/meson.build       |  16 ++
 tools/meson.build            | 261 ++++++++++++++++++++++++++++++
 tools/mpris-proxy.service.in |   2 +-
 unit/meson.build             | 126 +++++++++++++++
 25 files changed, 1751 insertions(+), 1 deletion(-)
 create mode 100644 attrib/meson.build
 create mode 100644 btio/meson.build
 create mode 100644 client/btpclient/meson.build
 create mode 100644 client/meson.build
 create mode 100644 completion/meson.build
 create mode 100644 doc/meson.build
 create mode 100644 emulator/meson.build
 create mode 100644 gdbus/meson.build
 create mode 100644 gobex/meson.build
 create mode 100644 lib/meson.build
 create mode 100644 mesh/meson.build
 create mode 100644 meson.build
 create mode 100644 meson_options.txt
 create mode 100644 monitor/meson.build
 create mode 100644 obexd/meson.build
 create mode 100644 peripheral/meson.build
 create mode 100644 plugins/meson.build
 create mode 100644 profiles/meson.build
 create mode 100644 src/meson.build
 create mode 100644 src/shared/meson.build
 create mode 100644 test/meson.build
 create mode 100644 tools/mesh/meson.build
 create mode 100644 tools/meson.build
 create mode 100644 unit/meson.build

diff --git a/attrib/meson.build b/attrib/meson.build
new file mode 100644
index 000000000000..0b7bc97933db
--- /dev/null
+++ b/attrib/meson.build
@@ -0,0 +1,17 @@
+attrib_sources = files(
+  'att.c',
+  'gatt.c',
+  'gattrib.c'
+)
+
+gatttool_sources = files(
+  'gatttool.c',
+  'att.c',
+  'gatt.c',
+  'gattrib.c',
+  'interactive.c',
+  'utils.c',
+  '../client/display.c',
+  '../btio/btio.c',
+  '../src/log.c'
+)
diff --git a/btio/meson.build b/btio/meson.build
new file mode 100644
index 000000000000..46ae5691f1d8
--- /dev/null
+++ b/btio/meson.build
@@ -0,0 +1 @@
+btio_sources = files('btio.c')
diff --git a/client/btpclient/meson.build b/client/btpclient/meson.build
new file mode 100644
index 000000000000..6c5ae11b8dd2
--- /dev/null
+++ b/client/btpclient/meson.build
@@ -0,0 +1,19 @@
+executable('btpclient',
+  sources: [
+    'btpclient.c',
+    'core.c',
+    'gap.c',
+    'gatt.c',
+    '../../src/shared/btp.c' ],
+  include_directories: '..',
+  dependencies: [ libbluetooth_internal_dep, libshared_ell_dep ],
+)
+executable('btpclientctl',
+  sources: [ 'btpclientctl.c', '../../client/display.c' ],
+  dependencies: [
+    libshared_mainloop_dep,
+    libshared_glib_dep,
+    libbluetooth_internal_dep,
+    readline_dep
+  ]
+)
diff --git a/client/meson.build b/client/meson.build
new file mode 100644
index 000000000000..a7642d13fd8b
--- /dev/null
+++ b/client/meson.build
@@ -0,0 +1,33 @@
+if get_option('client').enabled()
+  executable('bluetoothctl',
+    sources: [
+      'main.c',
+      'print.c',
+      'display.c',
+      'agent.c',
+      'advertising.c',
+      'adv_monitor.c',
+      'gatt.c',
+      'admin.c',
+      'player.c',
+      'mgmt.c',
+      'assistant.c',
+      'hci.c',
+      'telephony.c'
+      ],
+    dependencies: [
+      libbluetooth_internal_dep,
+      libgdbus_internal_dep,
+      libshared_glib_dep,
+      glib_dep,
+      udevlib_dep,
+      dbus_dep,
+      readline_dep
+      ],
+    install: true,
+  )
+endif
+
+if get_option('btpclient').allowed() and ell_dep.found()
+  subdir('btpclient')
+endif
diff --git a/completion/meson.build b/completion/meson.build
new file mode 100644
index 000000000000..24b71518c67d
--- /dev/null
+++ b/completion/meson.build
@@ -0,0 +1,3 @@
+install_data('zsh/_bluetoothctl',
+  install_dir: zshcompletiondir
+)
diff --git a/doc/meson.build b/doc/meson.build
new file mode 100644
index 000000000000..3d5ccd7d3034
--- /dev/null
+++ b/doc/meson.build
@@ -0,0 +1,172 @@
+man_pages = [
+  'hci.7',
+  'iso.7',
+  'mgmt.7',
+  'l2cap.7',
+  'rfcomm.7',
+  'sco.7',
+  'btsnoop.7',
+  'org.bluez.Adapter.5',
+  'org.bluez.Bearer.LE.5',
+  'org.bluez.Bearer.BREDR.5',
+  'org.bluez.Device.5',
+  'org.bluez.DeviceSet.5',
+  'org.bluez.AgentManager.5',
+  'org.bluez.Agent.5',
+  'org.bluez.ProfileManager.5',
+  'org.bluez.Profile.5',
+  'org.bluez.NetworkServer.5',
+  'org.bluez.Network.5',
+  'org.bluez.Input.5',
+  'org.bluez.BatteryProviderManager.5',
+  'org.bluez.BatteryProvider.5',
+  'org.bluez.Battery.5',
+  'org.bluez.AdminPolicySet.5',
+  'org.bluez.AdminPolicyStatus.5',
+  'org.bluez.Media.5',
+  'org.bluez.MediaControl.5',
+  'org.bluez.MediaPlayer.5',
+  'org.bluez.MediaFolder.5',
+  'org.bluez.MediaItem.5',
+  'org.bluez.MediaEndpoint.5',
+  'org.bluez.MediaTransport.5',
+  'org.bluez.MediaAssistant.5',
+  'org.bluez.GattManager.5',
+  'org.bluez.GattProfile.5',
+  'org.bluez.GattService.5',
+  'org.bluez.GattCharacteristic.5',
+  'org.bluez.GattDescriptor.5',
+  'org.bluez.LEAdvertisingManager.5',
+  'org.bluez.LEAdvertisement.5',
+  'org.bluez.AdvertisementMonitorManager.5',
+  'org.bluez.AdvertisementMonitor.5',
+  'org.bluez.Telephony.5',
+  'org.bluez.Call.5',
+]
+
+obex_man_pages = [
+  'org.bluez.obex.Client.5',
+  'org.bluez.obex.Session.5',
+  'org.bluez.obex.Transfer.5',
+  'org.bluez.obex.ObjectPush.5',
+  'org.bluez.obex.FileTransfer.5',
+  'org.bluez.obex.Synchronization.5',
+  'org.bluez.obex.PhonebookAccess.5',
+  'org.bluez.obex.MessageAccess.5',
+  'org.bluez.obex.Message.5',
+  'org.bluez.obex.AgentManager.5',
+  'org.bluez.obex.Agent.5',
+  'org.bluez.obex.Image.5',
+]
+
+if get_option('obex').enabled()
+  man_pages += obex_man_pages
+endif
+
+monitor_man_pages = [ 'btmon.1' ]
+
+if get_option('monitor').allowed()
+  man_pages += monitor_man_pages
+endif
+
+tools_man_pages = [
+  'rctest.1',
+  'l2ping.1',
+  'btattach.1',
+  'isotest.1',
+  'bdaddr.1',
+]
+
+noinst_tools_man_pages = [ 'btmgmt.1' ]
+
+if get_option('tools').allowed()
+  man_pages += tools_man_pages
+  man_pages += noinst_tools_man_pages
+endif
+
+deprecated_tools_man_pages = [
+  'hciattach.1',
+  'hciconfig.1',
+  'hcitool.1',
+  'rfcomm.1',
+  'sdptool.1',
+  'ciptool.1',
+]
+
+if get_option('deprecated').allowed()
+  man_pages += deprecated_tools_man_pages
+endif
+
+hid2hci_man_pages = [ 'hid2hci.1' ]
+
+if get_option('hid2hci').enabled()
+  man_pages += 'hid2hci.1'
+endif
+
+client_man_pages = [
+  'bluetoothctl.1',
+  'bluetoothctl-mgmt.1',
+  'bluetoothctl-monitor.1',
+  'bluetoothctl-admin.1',
+  'bluetoothctl-advertise.1',
+  'bluetoothctl-endpoint.1',
+  'bluetoothctl-gatt.1',
+  'bluetoothctl-player.1',
+  'bluetoothctl-scan.1',
+  'bluetoothctl-transport.1',
+  'bluetoothctl-assistant.1',
+  'bluetoothctl-hci.1',
+  'bluetoothctl-telephony.1'
+]
+
+if get_option('client').enabled()
+  man_pages += client_man_pages
+endif
+
+if rst2man.found()
+  foreach man_page: man_pages
+    man_page_name = man_page.substring(0, -2)
+    man_section = man_page.substring(-1)
+    if man_section.to_int() == 7
+      man_page_name = man_page_name + '-protocol'
+    endif
+    install = not noinst_tools_man_pages.contains(man_page)
+    custom_target(man_page,
+      input: man_page_name + '.rst',
+      output: man_page,
+      command: rst2man_command,
+      install: install,
+      install_dir: get_option('mandir') / 'man@0@'.format(man_section)
+    )
+  endforeach
+endif
+
+bluetoothd_rst = configure_file(
+  input: 'bluetoothd.rst.in',
+  output: 'bluetoothd.rst',
+  configuration: man_conf
+)
+if rst2man.found()
+  custom_target(
+    input: bluetoothd_rst,
+    output: 'bluetoothd.8',
+    command: rst2man_command,
+    install: true,
+    install_dir: get_option('mandir') / 'man8'
+  )
+endif
+
+bluetooth_meshd_rst = configure_file(
+  input: 'bluetooth-meshd.rst.in',
+  output: 'bluetooth-meshd.rst',
+  configuration: man_conf
+)
+if get_option('mesh').enabled() and rst2man.found()
+  custom_target(
+    input: bluetooth_meshd_rst,
+    output: 'bluetooth-meshd.8',
+    command: rst2man_command,
+    install: true,
+    install_dir: get_option('mandir') / 'man8'
+  )
+endif
diff --git a/emulator/meson.build b/emulator/meson.build
new file mode 100644
index 000000000000..40544567e4e4
--- /dev/null
+++ b/emulator/meson.build
@@ -0,0 +1,34 @@
+emulator_sources = files(
+  'hciemu.c',
+  'vhci.c',
+  'btdev.c',
+  'bthost.c',
+  'smp.c',
+)
+
+if get_option('tests').allowed()
+  executable('btvirt',
+    sources: [ 'main.c',
+      'serial.c',
+      'server.c',
+      'vhci.c',
+      'btdev.c',
+      'bthost.c',
+      'smp.c',
+      'phy.c',
+      'le.c' ],
+    dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep ]
+  )
+
+  executable('b1ee',
+    sources: 'b1ee.c',
+    include_directories: '../lib',
+    dependencies: libshared_mainloop_dep
+  )
+
+  executable('hfp',
+    sources: 'hfp.c',
+    dependencies: libshared_mainloop_dep
+  )
+endif
+
diff --git a/gdbus/meson.build b/gdbus/meson.build
new file mode 100644
index 000000000000..dc7f2887a60f
--- /dev/null
+++ b/gdbus/meson.build
@@ -0,0 +1,19 @@
+gdbus_sources = [
+  'gdbus.h',
+  'mainloop.c',
+  'watch.c',
+  'object.c',
+  'client.c',
+  'polkit.c'
+]
+
+libgdbus_internal = static_library('gdbus_internal',
+  sources: gdbus_sources,
+  dependencies: [ glib_dep, dbus_dep ],
+  install: false
+)
+
+libgdbus_internal_dep = declare_dependency(
+  dependencies: dbus_dep,
+  link_with: libgdbus_internal
+)
diff --git a/gobex/meson.build b/gobex/meson.build
new file mode 100644
index 000000000000..0ff6238a7b8c
--- /dev/null
+++ b/gobex/meson.build
@@ -0,0 +1,8 @@
+gobex_sources = files(
+  'gobex.c',
+  'gobex-defs.c',
+  'gobex-packet.c',
+  'gobex-header.c',
+  'gobex-transfer.c',
+  'gobex-apparam.c'
+)
diff --git a/lib/meson.build b/lib/meson.build
new file mode 100644
index 000000000000..e8d11f342bd8
--- /dev/null
+++ b/lib/meson.build
@@ -0,0 +1,53 @@
+lib_sources = [
+  'bluetooth/bluetooth.c',
+  'bluetooth/hci.c',
+  'bluetooth/sdp.c',
+]
+
+lib_extra_sources = [
+  'bluetooth/uuid.c'
+]
+
+lib_headers = [
+  'bluetooth/bluetooth.h',
+  'bluetooth/hci.h',
+  'bluetooth/hci_lib.h',
+  'bluetooth/sco.h',
+  'bluetooth/l2cap.h',
+  'bluetooth/sdp.h',
+  'bluetooth/sdp_lib.h',
+  'bluetooth/rfcomm.h',
+  'bluetooth/bnep.h',
+  'bluetooth/cmtp.h',
+  'bluetooth/hidp.h'
+  ]
+
+if get_option('library').allowed()
+  lib_bluetooth = shared_library('bluetooth',
+    sources: lib_headers + lib_sources,
+    version: '3.19.15',
+    install: true
+  )
+
+  install_headers(lib_headers,
+    subdir: 'bluetooth'
+  )
+
+  pkgconfig = import('pkgconfig')
+  pkgconfig.generate(
+    name: 'bluez',
+    description: 'Bluetooth protocol stack for Linux',
+    version: meson.project_version(),
+    libraries: lib_bluetooth,
+  )
+endif
+
+libbluetooth_internal = static_library('bluetooth_internal',
+  sources: lib_headers + lib_sources + lib_extra_sources,
+  install: false
+)
+
+libbluetooth_internal_dep = declare_dependency(
+  link_with: libbluetooth_internal,
+  include_directories: '.'
+)
diff --git a/mesh/meson.build b/mesh/meson.build
new file mode 100644
index 000000000000..cb4c9012300d
--- /dev/null
+++ b/mesh/meson.build
@@ -0,0 +1,60 @@
+if get_option('datafiles').allowed()
+  install_data('bluetooth-mesh.conf',
+    install_dir: dbusdir)
+  install_data('mesh-main.conf',
+    install_dir: configdir)
+endif
+
+if systemd_dep.found()
+  configure_file(
+    input: 'bluetooth-mesh.service.in',
+    output: 'bluetooth-mesh.service',
+    install: true,
+    install_dir: systemdsystemunitdir,
+    configuration: service_conf
+  )
+  install_data('org.bluez.mesh.service',
+    install_dir: dbussystembusdir
+  )
+endif
+
+executable('bluetooth-meshd',
+  sources: [
+    builtin_h,
+    'main.c',
+    'mesh.c',
+    'net-keys.c',
+    'mesh-io.c',
+    'mesh-mgmt.c',
+    'mesh-io-unit.c',
+    'mesh-io-mgmt.c',
+    'mesh-io-generic.c',
+    'net.c',
+    'crypto.c',
+    'friend.c',
+    'appkey.c',
+    'node.c',
+    'model.c',
+    'cfgmod-server.c',
+    'remprv-server.c',
+    'mesh-config-json.c',
+    'util.c',
+    'dbus.c',
+    'agent.c',
+    'prov-acceptor.c',
+    'prov-initiator.c',
+    'manager.c',
+    'pb-adv.c',
+    'keyring.c',
+    'rpl.c',
+    'prvbeac-server.c'
+  ],
+  include_directories: '../lib',
+  dependencies: [ libshared_ell_dep, jsonc_dep ],
+  c_args: [
+    '-DCONFIGDIR="@0@"'.format(configdir),
+    '-DMESH_STORAGEDIR="@0@"'.format(meshstoragedir)
+  ],
+  install: true,
+  install_dir: pkglibexecdir
+)
diff --git a/meson.build b/meson.build
new file mode 100644
index 000000000000..d3c43d79141f
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,299 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+project(
+  'bluez', 'c',
+  version: '5.86',
+  license: 'LGPL-2.1-or-later AND GPL-2.0-or-later',
+  meson_version: '>= 1.3.0'
+)
+
+datadir = get_option('datadir')
+storagedir = '/' / get_option('localstatedir') / 'lib/bluetooth'
+configdir = get_option('sysconfdir') / 'bluetooth'
+pkglibdir = get_option('libdir') / 'bluetooth'
+pkglibexecdir = get_option('libexecdir') / 'bluetooth'
+meshstoragedir = storagedir / 'mesh'
+
+fs = import('fs')
+service_conf = configuration_data()
+if not fs.is_absolute(pkglibexecdir)
+  pkglibexecdir = get_option('prefix') / pkglibexecdir
+endif
+service_conf.set('PKGLIBEXECDIR', pkglibexecdir)
+
+add_project_arguments('-DHAVE_CONFIG_H', language: 'c')
+config_h = configuration_data()
+
+cc = meson.get_compiler('c')
+
+required_funcs = [
+  'explicit_bzero',
+  'getrandom',
+  'rawmemchr',
+  'signalfd',
+]
+
+foreach func: required_funcs
+  cc.has_function(func, required: true)
+endforeach
+
+required_lib_funcs = [
+  [ 'clock_gettime', 'rt' ],
+  [ 'pthread_create', 'pthread' ],
+  [ 'dlopen', 'dl' ]
+]
+
+foreach func: required_lib_funcs
+  dep = cc.find_library(func[1], required: true)
+  cc.has_function(func[0], dependencies: dep, required: true)
+endforeach
+
+required_headers = [
+  'stdarg.h',
+  'stdio.h',
+  'string.h',
+  'linux/types.h',
+  'linux/if_alg.h',
+  'linux/uinput.h',
+  'linux/uhid.h',
+  'sys/random.h'
+]
+
+foreach header: required_headers
+  cc.has_header(header, required: true)
+endforeach
+
+# basename may be only available in libgen.h with the POSIX behavior,
+# not desired here
+config_h.set10('HAVE_DECL_BASENAME',
+  cc.has_function('basename', prefix: '#define _GNU_SOURCE 1\n#include <string.h>'))
+
+# To combine with -Wl,--gc-sections
+add_project_arguments([ '-ffunction-sections', '-fdata-sections' ] , language: 'c')
+
+# Maintainer mode
+if get_option('buildtype').contains('debug')
+  test_cflags = [
+    '-Wno-unused-parameter',
+    '-Wno-missing-field-initializers',
+    '-Wdeclaration-after-statement',
+    '-Wmissing-declarations',
+    '-Wredundant-decls',
+    '-Wcast-align',
+    '-Wswitch-enum',
+    '-Wformat',
+    '-Wformat-security',
+    '-Wstringop-overflow',
+  ]
+
+  if get_option('buildtype') == 'debugoptimized'
+    test_cflags += '-D_FORTIFY_SOURCE=3'
+  endif
+
+  common_flags = [
+    '-DG_DISABLE_DEPRECATED',
+    '-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_36',
+    '-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_36'
+  ]
+
+  common_flags += cc.get_supported_arguments(test_cflags)
+
+  add_project_arguments(common_flags, language: 'c')
+
+  config_h.set('HAVE_VALGRIND_MEMCHECK_H',
+    cc.has_header('valgrind/memcheck.h', required: get_option('valgrind')))
+endif
+
+test_cflags = [
+  '-ffunction-sections',
+  '-fdata-sections'
+]
+
+add_project_arguments(cc.get_supported_arguments(test_cflags), language: 'c')
+add_project_arguments('-I' + meson.project_build_root(), language: 'c')
+add_project_arguments('-I' + meson.project_source_root(), language: 'c')
+add_project_link_arguments('-Wl,--gc-sections', language: 'c')
+
+dummy_dep = dependency('dummy', required: false)
+glib_dep = dependency('glib-2.0', version: '>= 2.36')
+dbus_dep = dependency('dbus-1', version:'>= 1.10')
+
+cc.check_header('elfutils/libdwfl.h', required: get_option('backtrace'))
+backtrace_dep = cc.find_library('dw', required: get_option('backtrace'))
+config_h.set('HAVE_BACKTRACE_SUPPORT', backtrace_dep.found())
+
+config_h.set10('HAVE_DECL_SOF_TIMESTAMPING_TX_COMPLETION',
+  cc.has_header_symbol('linux/net_tstamp.h', 'SOF_TIMESTAMPING_TX_COMPLETION'))
+config_h.set10('HAVE_DECL_SCM_TSTAMP_COMPLETION',
+  cc.has_header_symbol('linux/errqueue.h', 'SCM_TSTAMP_COMPLETION', prefix: '#include <time.h>'))
+
+udevlib_dep = dependency('libudev', required: get_option('udev'))
+udevlib_sixaxis_dep = dependency('libudev', required: get_option('sixaxis'))
+udevlib_hid2hci_dep = dependency('libudev', required: get_option('hid2hci'))
+
+cups_dep = dependency('cups', required: get_option('cups'))
+jsonc_dep = dependency('json-c', version: '>= 0.13', required: get_option('mesh'))
+alsa_dep = dependency('alsa', required: get_option('midi'))
+ical_dep = dependency('libical', required: get_option('obex'))
+readline_dep = dependency('readline',
+  required: get_option('client').enabled() or
+    get_option('mesh').enabled())
+systemd_dep = dependency('systemd', required: get_option('systemd'))
+libsystemd_dep = dependency('libsystemd', required: get_option('systemd'))
+
+ell_args = {
+  'version': '>= 0.39',
+  'fallback' : ['ell', 'libell_dep'],
+  'default_options': [
+    'default_library=static',
+    'tests=false',
+    'cert-tests=false'
+  ]
+}
+ell_dep = dependency('ell',
+  kwargs: ell_args,
+  required: get_option('btpclient')
+)
+ell_mesh_dep = dependency('ell',
+  kwargs: ell_args,
+  required: get_option('mesh')
+)
+
+rst2man = find_program('rst2man', 'rst2man.py', required: get_option('manpages'))
+if rst2man.found()
+  rst2man_command = [
+    rst2man,
+    '--strict',
+    '--no-raw',
+    '--no-generator',
+    '--no-datestamp',
+    '@INPUT@',
+    '@OUTPUT@'
+  ]
+endif
+
+man_conf = configuration_data()
+man_conf.set('CONFIGDIR', get_option('prefix') / configdir)
+man_conf.set('MESH_STORAGEDIR', get_option('prefix') / meshstoragedir)
+
+if get_option('phonebook') == 'ebook'
+  ebook_dep = dependency('libebook-1.2', '>= 3.3', required: true)
+  edataserver_dep = dependency('libedataserver-1.2', '>= 3.3', required: true)
+else
+  ebook_dep = dummy_dep
+  edataserver_dep = dummy_dep
+endif
+
+plugindir = get_option('libdir') / 'bluetooth/plugins'
+
+dbusconfdir = get_option('dbusconfdir')
+if dbusconfdir == ''
+  dbusconfdir = dbus_dep.get_variable('datadir')
+endif
+dbusdir = dbusconfdir / 'dbus-1/system.d'
+
+dbussystembusdir = get_option('dbussystembusdir')
+if dbussystembusdir == ''
+  dbussystembusdir = dbus_dep.get_variable('system_bus_services_dir')
+endif
+
+dbussessionbusdir = get_option('dbussessionbusdir')
+if dbussessionbusdir == ''
+  dbussessionbusdir = dbus_dep.get_variable('session_bus_services_dir')
+endif
+
+service_in = configuration_data()
+service_in.set_quoted('PKGLIBEXECDIR', get_option('prefix') / pkglibexecdir)
+
+if systemd_dep.found()
+  systemdsystemunitdir = get_option('systemdsystemunitdir')
+  if systemdsystemunitdir == ''
+    systemdsystemunitdir = systemd_dep.get_variable('systemdsystemunitdir')
+  endif
+  systemduserunitdir = get_option('systemduserunitdir')
+  if systemduserunitdir == ''
+    systemduserunitdir = systemd_dep.get_variable('systemduserunitdir')
+  endif
+endif
+
+zshcompletiondir = get_option('zsh-completion-dir')
+if zshcompletiondir == ''
+  zshcompletiondir = datadir / 'zsh/site-functions'
+endif
+
+zsh_completions = fs.is_dir(zshcompletiondir)
+
+udev_dep = dependency('udev', version:'>= 196', required: udevlib_hid2hci_dep.found())
+if udev_dep.found()
+  udevdir = get_option('udevdir')
+  if udevdir == ''
+    udevdir = udev_dep.get_variable('udevdir')
+  endif
+endif
+
+if cups_dep.found()
+  cups_serverbin = get_option('cups_serverbin')
+  if cups_serverbin == ''
+    cups_serverbin = cups_dep.get_variable('cups_serverbin')
+  endif
+endif
+
+external_plugins_link_args = ''
+if get_option('external-plugins').enabled()
+  external_plugins_link_args = [
+    '-Wl,--export-dynamic',
+    '-Wl,--version-script=' + meson.project_source_root() / 'src/bluetooth.ver'
+  ]
+endif
+
+config_h.set('HAVE_A2DP', get_option('a2dp').allowed())
+config_h.set('HAVE_AVRCP', get_option('avrcp').allowed())
+config_h.set('HAVE_ASHA', get_option('asha').allowed())
+config_h.set('HAVE_VCP', get_option('vcp').allowed())
+config_h.set10('EXTERNAL_PLUGINS', get_option('external-plugins').enabled())
+config_h.set_quoted('VERSION', meson.project_version())
+
+configure_file(
+  output: 'config.h',
+  configuration: config_h
+)
+
+subdir('lib')
+subdir('gdbus')
+subdir('attrib')
+subdir('btio')
+subdir('plugins')
+subdir('profiles')
+subdir('src')
+subdir('client')
+if get_option('monitor').allowed()
+  subdir('monitor')
+endif
+subdir('emulator')
+subdir('gobex')
+subdir('tools')
+subdir('peripheral')
+if get_option('obex').enabled()
+  subdir('obexd')
+endif
+if zsh_completions
+  subdir('completion')
+endif
+if get_option('mesh').enabled()
+  subdir('mesh')
+endif
+if rst2man.found()
+  subdir('doc')
+endif
+if get_option('tests').allowed()
+  subdir('test')
+  subdir('unit')
+endif
+
+# Fix permissions on install
+install = find_program('install')
+sh = find_program('sh')
+meson.add_install_script(sh, '-c', 'install -dm755 ${DESTDIR}/' + configdir)
+meson.add_install_script(sh, '-c', 'install -dm700 ${DESTDIR}/' + storagedir)
+if get_option('mesh').enabled()
+  meson.add_install_script(sh, '-c', 'install -dm700 ${DESTDIR}/' + meshstoragedir)
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 000000000000..4a0c726c2098
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,52 @@
+option('valgrind', type: 'feature', value: 'disabled', description: 'Enable valgrind memory checks')
+option('backtrace', type: 'feature', value: 'disabled', description: 'Enable backtrace support')
+option('library', type: 'feature', value: 'enabled', description: 'Install Bluetooth library')
+option('examples', type: 'feature', value: 'auto', description: 'Enable test/example scripts')
+option('tests', type: 'feature', value: 'auto', description: 'Enable testing tools')
+option('tools', type: 'feature', value: 'enabled', description: 'Enable Bluetooth tools')
+option('monitor', type: 'feature', value: 'enabled', description: 'Enable Bluetooth monitor')
+option('client', type: 'feature', value: 'enabled', description: 'Enable command line client')
+option('systemd', type: 'feature', value: 'enabled', description: 'Enable systemd integration')
+option('udev', type: 'feature', value: 'enabled', description: 'Enable udev device support in tools')
+option('datafiles', type: 'feature', value: 'enabled', description: 'Install configuration and data files')
+option('manpages', type: 'feature', value: 'auto', description: 'Enable building of manual pages')
+option('experimental', type: 'feature', value: 'disabled', description: 'Enable experimental tools')
+option('deprecated', type: 'feature', value: 'disabled', description: 'Enable deprecated tools')
+option('external-plugins', type: 'feature', value: 'enabled', description: 'Enable support for external plugins')
+
+option('dbusconfdir', type: 'string', value: '', description: 'D-Bus configuration directory')
+option('dbussystembusdir', type: 'string', value: '', description: 'D-Bus system bus services dir')
+option('dbussessionbusdir', type:'string', value: '', description: 'D-Bus session bus services dir')
+option('systemdsystemunitdir', type: 'string', value: '', description: 'path to systemd system unit directory')
+option('systemduserunitdir', type: 'string', value: '', description: 'systemd user unit dir')
+option('zsh-completion-dir', type:'string', value: '', description: 'Path to install zsh completions')
+option('udevdir', type: 'string', value: '', description: 'udev directory')
+option('cups_serverbin', type: 'string', value: '', description: 'CUPS directory')
+
+option('nfc', type: 'feature', value: 'disabled', description: 'Enable NFC pairing')
+option('a2dp', type: 'feature', value: 'auto', description: 'Enable A2DP profile')
+option('avrcp', type: 'feature', value: 'auto', description: 'Enable AVRCP profile')
+option('network', type: 'feature', value: 'auto', description: 'Enable network profiles')
+option('hid', type: 'feature', value: 'auto', description: 'Enable HID profile')
+option('hog', type: 'feature', value: 'auto', description: 'Enable HoG profile')
+option('bap', type: 'feature', value: 'auto', description: 'Enable BAP profile')
+option('bass', type: 'feature', value: 'auto', description: 'Enable BASS service')
+option('mcp', type: 'feature', value: 'auto', description: 'Enable MCP profile')
+option('ccp', type: 'feature', value: 'auto', description: 'Enable CCP profile')
+option('vcp', type: 'feature', value: 'auto', description: 'Enable VCP profile')
+option('micp', type: 'feature', value: 'auto', description: 'Enable MICP profile')
+option('csip', type: 'feature', value: 'auto', description: 'Enable CSIP profile')
+option('asha', type: 'feature', value: 'auto', description: 'Enable ASHA support')
+option('cups', type: 'feature', value: 'auto', description: 'Enable CUPS printer support')
+option('mesh', type: 'feature', value: 'auto', description: 'Enable Mesh profile support')
+option('midi', type: 'feature', value: 'auto', description: 'Enable MIDI support')
+option('obex', type: 'feature', value: 'auto', description: 'Enable OBEX profile support')
+option('tmap', type: 'feature', value: 'auto', description: 'Enable TMAP profile support')
+option('gmap', type: 'feature', value: 'auto', description: 'Enable GMAP profile support')
+option('hfp', type: 'feature', value: 'auto', description: 'Enable HFP profile support')
+option('btpclient', type: 'feature', value: 'auto', description: 'Enable BTP client')
+option('sixaxis', type: 'feature', value: 'auto', description: 'Enable sixaxis plugin')
+option('hid2hci', type: 'feature', value: 'auto', description: 'Enable hid2hci tool')
+option('logger', type: 'feature', value: 'disabled', description: 'Enable HCI logger service')
+option('admin', type: 'feature', value: 'auto', description: 'Enable admin policy plugin')
+option('phonebook', type: 'combo', choices: [ 'dummy', 'ebook' ], value: 'dummy', description: 'Obexd phonebook plugin')
diff --git a/monitor/meson.build b/monitor/meson.build
new file mode 100644
index 000000000000..44197f2d3ba2
--- /dev/null
+++ b/monitor/meson.build
@@ -0,0 +1,37 @@
+executable('btmon',
+  sources: [
+    'main.c',
+    'display.c',
+    'hcidump.c',
+    'ellisys.c',
+    'control.c',
+    'packet.c',
+    'vendor.c',
+    'lmp.c',
+    'crc.c',
+    'll.c',
+    'l2cap.c',
+    'sdp.c',
+    'avctp.c',
+    'avdtp.c',
+    'a2dp.c',
+    'rfcomm.c',
+    'bnep.c',
+    'hwdb.c',
+    'keys.c',
+    'analyze.c',
+    'intel.c',
+    'broadcom.c',
+    'msft.c',
+    'jlink.c',
+    'att.c',
+    '../src/log.c',
+    '../src/textfile.c',
+    '../src/settings.c'
+  ],
+  c_args: [
+    '-DSTORAGEDIR="@0"'.format(storagedir)
+  ],
+  dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep, udevlib_dep, glib_dep ],
+  install: true
+)
diff --git a/obexd/meson.build b/obexd/meson.build
new file mode 100644
index 000000000000..baddafcd8873
--- /dev/null
+++ b/obexd/meson.build
@@ -0,0 +1,121 @@
+if get_option('datafiles').allowed()
+  install_data('src/obex.conf',
+    install_dir: dbusconfdir / 'dbus-1/system.d'
+  )
+endif
+
+if systemd_dep.found()
+  configure_file(
+    input: 'src/obex.service.in',
+    output: 'obex.service',
+    install: true,
+    install_dir: systemduserunitdir,
+    configuration: service_conf
+  )
+  configure_file(
+    input: 'src/org.bluez.obex.service.in',
+    output: 'org.bluez.obex.service',
+    install: true,
+    install_dir: dbussessionbusdir,
+    configuration: service_conf
+  )
+  install_symlink('dbus-org.bluez.obex.service',
+    pointing_to: dbussessionbusdir / 'org.bluez.obex.service',
+    install_dir: systemduserunitdir
+  )
+endif
+
+obexd_builtin_modules = [
+  'filesystem',
+  'bluetooth',
+  'opp',
+  'ftp',
+  'irmc',
+  'pbap',
+  'mas',
+  'mns'
+]
+
+obexd_builtin_sources = [
+  'plugins/filesystem.c',
+  'plugins/bluetooth.c',
+  'plugins/opp.c',
+  'plugins/ftp.c',
+  'plugins/irmc.c',
+  'plugins/pbap.c',
+  'plugins/vcard.c',
+  'plugins/phonebook-@0@.c'.format(get_option('phonebook')),
+  'plugins/mas.c',
+  'plugins/messages-dummy.c',
+  'client/mns.c',
+]
+
+if get_option('experimental').allowed()
+  obexd_builtin_modules += 'pcsuite'
+  obexd_builtin_sources += 'plugins/pcsuite.c'
+endif
+
+obex_plugindir = get_option('libdir') / 'obex/plugins'
+
+obexd_genbuiltin = find_program('src/genbuiltin')
+obexd_builtin_h = custom_target('obexd-builtin.h',
+  output: 'builtin.h',
+  capture: true,
+  command: [ obexd_genbuiltin, obexd_builtin_modules ]
+)
+
+executable('obexd',
+  sources: [
+    btio_sources,
+    gobex_sources,
+    obexd_builtin_sources,
+    obexd_builtin_h,
+    'src/main.c',
+    'src/plugin.c',
+    'src/log.c',
+    'src/logind.c',
+    'src/manager.c',
+    'src/obex.c',
+    'src/mimetype.c',
+    'src/service.c',
+    'src/transport.c',
+    'src/server.c',
+    'client/manager.c',
+    'client/session.c',
+    'client/bluetooth.c',
+    'client/sync.c',
+    'client/pbap.c',
+    'client/ftp.c',
+    'client/opp.c',
+    'client/map.c',
+    'client/bip.c',
+    'client/bip-common.c',
+    'client/map-event.c',
+    'client/transfer.c',
+    'client/transport.c',
+    'client/driver.c'
+  ],
+  dependencies: [
+    libbluetooth_internal_dep,
+    libgdbus_internal_dep,
+    libshared_glib_dep,
+    ical_dep,
+    dbus_dep,
+    ebook_dep,
+    edataserver_dep,
+    glib_dep,
+    libsystemd_dep.found() ? libsystemd_dep : dummy_dep
+  ],
+  c_args: [
+    '-DOBEX_PLUGIN_BUILTIN',
+    '-DPLUGINDIR="@0@"'.format(obex_plugindir),
+    '-D_FILE_OFFSET_BITS=64',
+    '-DCONFIGDIR="@0@"'.format(configdir),
+    libsystemd_dep.found() ? '-DSYSTEMD' : ''
+  ],
+  link_args: external_plugins_link_args,
+  install: true,
+  install_dir: pkglibexecdir,
+)
+
+
diff --git a/peripheral/meson.build b/peripheral/meson.build
new file mode 100644
index 000000000000..6a9fdb4cbc66
--- /dev/null
+++ b/peripheral/meson.build
@@ -0,0 +1,13 @@
+if get_option('tests').allowed()
+  executable('btsensor',
+    sources: [
+      'main.c',
+      'efivars.c',
+      'attach.c',
+      'log.c',
+      'gap.c',
+      'gatt.c',
+    ],
+    dependencies: [ libshared_mainloop_dep, libbluetooth_internal_dep ]
+  )
+endif
diff --git a/plugins/meson.build b/plugins/meson.build
new file mode 100644
index 000000000000..d02fb8530af0
--- /dev/null
+++ b/plugins/meson.build
@@ -0,0 +1,29 @@
+builtin_modules = [
+  'hostname',
+  'autopair',
+  'policy',
+]
+
+builtin_sources = files(
+  'hostname.c',
+  'autopair.c',
+  'policy.c'
+)
+
+builtin_deps = []
+
+if get_option('admin').allowed()
+  builtin_modules += 'admin'
+  builtin_sources += files('admin.c')
+endif
+
+if get_option('nfc').allowed()
+  builtin_modules += 'neard'
+  builtin_sources += files('neard.c')
+endif
+
+if udevlib_sixaxis_dep.found()
+  builtin_modules += 'sixaxis'
+  builtin_sources += files('sixaxis.c')
+  builtin_deps += udevlib_sixaxis_dep
+endif
diff --git a/profiles/meson.build b/profiles/meson.build
new file mode 100644
index 000000000000..7e33af9932d8
--- /dev/null
+++ b/profiles/meson.build
@@ -0,0 +1,167 @@
+builtin_modules += [
+  'gap',
+  'scanparam',
+  'deviceinfo',
+  'battery',
+  'rap',
+]
+
+builtin_sources += files(
+  'audio/media.c',
+  'audio/transport.c',
+  'audio/player.c',
+  'audio/avrcp-player.c',
+  'gap/gas.c',
+  'scanparam/scan.c',
+  'deviceinfo/deviceinfo.c',
+  'battery/battery.c',
+  'ranging/rap.c',
+)
+
+if get_option('a2dp').allowed()
+  builtin_modules += 'a2dp'
+  builtin_sources += files(
+    'audio/source.c',
+    'audio/sink.c',
+    'audio/a2dp.c',
+    'audio/avdtp.c'
+  )
+endif
+
+if get_option('avrcp').allowed()
+  builtin_modules += 'avrcp'
+  builtin_sources += files(
+    'audio/control.c',
+    'audio/avctp.c',
+    'audio/avctp.c',
+    'audio/avrcp.c'
+  )
+endif
+
+if get_option('network').allowed()
+  builtin_modules += 'network'
+  builtin_sources += files(
+    'network/manager.c',
+    'network/bnep.c',
+    'network/server.c',
+    'network/connection.c'
+  )
+  install_data('network/network.conf',
+    install_dir: configdir
+  )
+endif
+
+if get_option('hid').allowed()
+  builtin_modules += 'input'
+  builtin_sources += files(
+    'input/manager.c',
+    'input/server.c',
+    'input/device.c'
+  )
+
+  install_data('input/input.conf',
+    install_dir: configdir
+  )
+endif
+
+if get_option('hog').allowed()
+  builtin_modules += 'hog'
+  builtin_sources += files(
+    'input/hog.c',
+    'input/hog-lib.c',
+    'deviceinfo/dis.c',
+    'battery/bas.c',
+    'scanparam/scpp.c',
+    'input/suspend-none.c'
+  )
+endif
+
+if alsa_dep.found()
+  builtin_modules += 'midi'
+  builtin_sources += files(
+    'midi/midi.c',
+    'midi/libmidi.c'
+  )
+  builtin_deps += alsa_dep
+endif
+
+if get_option('bap').allowed()
+  builtin_modules += 'bap'
+  builtin_sources += files('audio/bap.c')
+endif
+
+if get_option('bass').allowed()
+  builtin_modules += 'bass'
+  builtin_sources += files('audio/bass.c')
+endif
+
+if get_option('mcp').allowed()
+  builtin_modules += 'mcp'
+  builtin_sources += files('audio/mcp.c')
+endif
+
+if get_option('vcp').allowed()
+  builtin_modules += 'vcp'
+  builtin_sources += files('audio/vcp.c')
+endif
+
+if get_option('micp').allowed()
+  builtin_modules += 'micp'
+  builtin_sources += files('audio/micp.c')
+endif
+
+if get_option('ccp').allowed()
+  builtin_modules += 'ccp'
+  builtin_sources += files('audio/ccp.c')
+endif
+
+if get_option('csip').allowed()
+  builtin_modules += 'csip'
+  builtin_sources += files('audio/csip.c')
+endif
+
+if get_option('asha').allowed()
+  builtin_modules += 'asha'
+  builtin_sources += files('audio/asha.c')
+endif
+
+if get_option('tmap').allowed()
+  builtin_modules += 'tmap'
+  builtin_sources += files('audio/tmap.c')
+endif
+
+if get_option('gmap').allowed()
+  builtin_modules += 'gmap'
+  builtin_sources += files('audio/gmap.c')
+endif
+
+if get_option('hfp').allowed()
+  builtin_modules += 'hfp'
+  builtin_sources += files('audio/telephony.c', 'audio/hfp-hf.c')
+endif
+
+if get_option('tools').allowed()
+  executable('iapd',
+    sources: 'iap/main.c',
+    dependencies: [ libgdbus_internal_dep, glib_dep, dbus_dep ]
+  )
+endif
+
+if cups_dep.found()
+  executable('bluetooth',
+    sources: [
+      'cups/main.c',
+      'cups/sdp.c',
+      'cups/spp.c',
+      'cups/hcrp.c'
+    ],
+    dependencies: [
+      libbluetooth_internal_dep,
+      libgdbus_internal_dep,
+      glib_dep,
+      dbus_dep
+    ],
+    install: true,
+    install_dir: cups_serverbin / 'backend'
+  )
+endif
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 000000000000..65b7131c86b4
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,88 @@
+subdir('shared')
+
+if get_option('datafiles').allowed()
+  install_data('bluetooth.conf',
+    install_dir: dbusconfdir / 'dbus-1/system.d'
+  )
+  install_data('main.conf',
+    install_dir: configdir
+  )
+endif
+
+if systemd_dep.found()
+  configure_file(
+    input: 'bluetooth.service.in',
+    output: 'bluetooth.service',
+    install: true,
+    install_dir: systemdsystemunitdir,
+    configuration: service_conf
+  )
+
+  install_data('org.bluez.service',
+    install_dir: dbussystembusdir
+  )
+endif
+
+genbuiltin = find_program('genbuiltin')
+builtin_h = custom_target('src-builtin.h',
+  output: 'builtin.h',
+  capture: true,
+  command: [ genbuiltin, builtin_modules ]
+)
+
+executable('bluetoothd',
+  sources: [ builtin_sources,
+    attrib_sources,
+    btio_sources,
+    builtin_h,
+    'main.c',
+    'log.c',
+    'backtrace.c',
+    'rfkill.c',
+    'sdpd-server.c',
+    'sdpd-request.c',
+    'sdpd-service.c',
+    'sdpd-database.c',
+    'gatt-database.c',
+    'sdp-xml.c',
+    'sdp-client.c',
+    'textfile.c',
+    'uuid-helper.c',
+    'plugin.c',
+    'storage.c',
+    'advertising.c',
+    'agent.c',
+    'bearer.c',
+    'error.c',
+    'adapter.c',
+    'profile.c',
+    'service.c',
+    'gatt-client.c',
+    'device.c',
+    'dbus-common.c',
+    'eir.c',
+    'adv_monitor.c',
+    'battery.c',
+    'settings.c',
+    'set.c' ],
+  dependencies: [
+    libbluetooth_internal_dep,
+    libgdbus_internal_dep,
+    libshared_glib_dep,
+    backtrace_dep,
+    glib_dep,
+    dbus_dep,
+    udevlib_dep,
+    builtin_deps
+  ],
+  override_options: [ 'b_lundef=false' ],
+  c_args: [
+    '-DBLUETOOTH_PLUGIN_BUILTIN',
+    '-DPLUGINDIR="@0@"'.format(plugindir),
+    '-DSTORAGEDIR="@0@"'.format(storagedir),
+    '-DCONFIGDIR="@0@"'.format(configdir)
+  ],
+  link_args: [ '-Wl,--gc-sections', external_plugins_link_args ],
+  install: true,
+  install_dir: pkglibexecdir
+)
diff --git a/src/shared/meson.build b/src/shared/meson.build
new file mode 100644
index 000000000000..ae9a65e1b5c6
--- /dev/null
+++ b/src/shared/meson.build
@@ -0,0 +1,85 @@
+shared_sources = [
+  'queue.c',
+  'util.c',
+  'mgmt.c',
+  'crypto.c',
+  'ecc.c',
+  'ringbuf.c',
+  'hci.c',
+  'hci-crypto.c',
+  'hfp.c',
+  'uhid.c',
+  'pcap.c',
+  'btsnoop.c',
+  'ad.c',
+  'att.c',
+  'gatt-helpers.c',
+  'gatt-client.c',
+  'gatt-server.c',
+  'gatt-db.c',
+  'gap.c',
+  'log.c',
+  'bap.c',
+  'bap-debug.c',
+  'mcp.c',
+  'vcp.c',
+  'micp.c',
+  'csip.c',
+  'bass.c',
+  'ccp.c',
+  'asha.c',
+  'battery.c',
+  'uinput.c',
+  'tmap.c',
+  'gmap.c',
+  'rap.c',
+]
+
+if readline_dep.found()
+  shared_sources += [ 'shell.c' ]
+endif
+
+libshared_glib = static_library('libshared-glib',
+  sources: [ shared_sources,
+    'io-glib.c',
+    'timeout-glib.c',
+    'mainloop-glib.c',
+    'mainloop-notify.c',
+    'tester.c' ],
+  include_directories: '../../lib',
+  dependencies: glib_dep
+)
+
+libshared_glib_dep = declare_dependency(
+  link_with: libshared_glib,
+  dependencies: glib_dep
+)
+
+libshared_mainloop = static_library('libshared-mainloop',
+  sources: [ shared_sources,
+    'io-mainloop.c',
+    'timeout-mainloop.c',
+    'mainloop.c',
+    'mainloop-notify.c' ],
+  include_directories: '../../lib',
+  dependencies: glib_dep
+)
+
+libshared_mainloop_dep = declare_dependency(
+  link_with: libshared_mainloop
+)
+
+if ell_dep.found()
+  libshared_ell = static_library('libshared-ell',
+    sources: [ shared_sources,
+      'io-ell.c',
+      'timeout-ell.c',
+      'mainloop-ell.c' ],
+    include_directories: [ '../..', '../../lib' ],
+    dependencies: [ glib_dep, ell_dep ]
+  )
+  libshared_ell_dep = declare_dependency(
+    link_with: libshared_ell,
+    dependencies: ell_dep
+  )
+endif
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 000000000000..6f93cf8a36e0
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,37 @@
+test_scripts = [
+  'bluezutils.py',
+  'dbusdef.py',
+  'monitor-bluetooth',
+  'list-devices',
+  'test-discovery',
+  'test-manager',
+  'test-adapter',
+  'test-device',
+  'simple-agent',
+  'simple-endpoint',
+  'test-network',
+  'test-profile',
+  'service-record.dtd',
+  'service-did.xml',
+  'service-spp.xml',
+  'service-opp.xml',
+  'service-ftp.xml',
+  'simple-player',
+  'test-nap',
+  'test-hfp',
+  'opp-client',
+  'ftp-client',
+  'pbap-client',
+  'map-client',
+  'example-advertisement',
+  'example-gatt-server',
+  'example-gatt-client',
+  'test-gatt-profile',
+  'test-mesh',
+  'agent.py'
+]
+
+install_data(
+  sources: test_scripts,
+  install_dir: pkglibdir / 'tests'
+)
diff --git a/tools/mesh/meson.build b/tools/mesh/meson.build
new file mode 100644
index 000000000000..ae1fbca6f6b6
--- /dev/null
+++ b/tools/mesh/meson.build
@@ -0,0 +1,16 @@
+if get_option('tools').allowed() and readline_dep.found()
+  executable('mesh-cfgclient',
+    sources: [ '../mesh-cfgclient.c',
+      'cfgcli.c',
+      'keys.c',
+      'util.c',
+      'remote.c',
+      'agent.c',
+      'mesh-db.c',
+      '../../mesh/util.c',
+      '../../mesh/crypto.c',
+    ],
+    dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep, ell_dep, dbus_dep, jsonc_dep, readline_dep ],
+    install: true
+  )
+endif
diff --git a/tools/meson.build b/tools/meson.build
new file mode 100644
index 000000000000..8fc0dc1d2fb5
--- /dev/null
+++ b/tools/meson.build
@@ -0,0 +1,261 @@
+if get_option('logger').allowed()
+  executable('btmon-logger',
+    [ 'btmon-logger.c' ],
+    dependencies: libshared_mainloop_dep,
+    include_directories: '../lib/',
+    install_dir: pkglibexecdir
+  )
+
+  if systemd_dep.found()
+    configure_file(
+      input: 'bluetooth-logger.service.in',
+      output: 'bluetooth-logger.service',
+      configuration: service_in,
+      install: true,
+      install_dir: systemdsystemunitdir
+    )
+  endif
+endif
+
+executable('3dsp',
+  sources: '3dsp.c',
+  dependencies: libshared_mainloop_dep
+)
+
+tester_tools = [
+  'mgmt',
+  'mesh',
+  'l2cap',
+  'rfcomm',
+  'bnep',
+  'smp',
+  'gap',
+  'sco',
+  'userchan',
+  'iso',
+  'ioctl',
+  'hci',
+  '6lowpan',
+]
+
+if get_option('tests').allowed()
+  foreach tool: tester_tools
+    executable(tool + '-tester',
+      sources: [ tool + '-tester.c', emulator_sources ],
+      dependencies: [ libbluetooth_internal_dep, libshared_glib_dep, libgdbus_internal_dep, glib_dep ]
+    )
+  endforeach
+endif
+
+if udevlib_hid2hci_dep.found()
+  executable('hid2hci',
+    sources: 'hid2hci.c',
+    dependencies: udevlib_hid2hci_dep,
+    install: true,
+    install_dir: udevdir
+  )
+  install_data('hid2hci.rules',
+    install_dir: udevdir / 'rules.d',
+    rename: '97-hid2hci.rules'
+  )
+endif
+
+tools_names = [
+  'avinfo',
+  'avtest',
+  'scotest',
+  'hwdb',
+  'hcieventmask',
+  'btinfo',
+  'btconfig',
+  'btsnoop',
+  'btproxy',
+  'cltest',
+  'oobtest',
+  'advtest',
+  'seq2bseq',
+  'nokfw',
+  'rtlfw',
+  'create-image',
+  'eddystone',
+  'ibeacon',
+]
+
+inst_tools_names = [
+  'rctest',
+  'l2test',
+  'l2ping',
+  'bluemoon',
+  'hex2hcd',
+  'mpris-proxy',
+  'btattach',
+  'isotest'
+]
+
+if get_option('tools').allowed()
+  foreach tool: tools_names
+    executable(tool,
+      sources: tool + '.c',
+      dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep ]
+    )
+  endforeach
+
+  foreach tool: inst_tools_names
+    executable(tool,
+      sources: tool + '.c',
+      dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep, libgdbus_internal_dep, dbus_dep, glib_dep ],
+      install: true
+    )
+  endforeach
+
+  if udevlib_dep.found()
+    executable('bdaddr',
+      sources: [ 'bdaddr.c', '../src/oui.c' ],
+      dependencies: [ libbluetooth_internal_dep, udevlib_dep ]
+    )
+  endif
+  executable('btiotest',
+    sources: [ 'btiotest.c', btio_sources ],
+    dependencies: [ libbluetooth_internal_dep, glib_dep ]
+  )
+  executable('bneptest',
+    sources: [ 'bneptest.c', btio_sources, '../src/log.c', '../profiles/network/bnep.c' ],
+    dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep, glib_dep ]
+  )
+  executable('btgatt-client',
+    sources: [ 'btgatt-client.c', '../src/uuid-helper.c' ],
+    dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep ]
+  )
+  executable('btgatt-server',
+    sources: [ 'btgatt-server.c', '../src/uuid-helper.c' ],
+    dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep ]
+  )
+  if udevlib_dep.found()
+    executable('gatt-service',
+      sources: 'gatt-service.c',
+      dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep, glib_dep, libgdbus_internal_dep, udevlib_dep ]
+    )
+  endif
+  executable('test-runner',
+    sources: 'test-runner.c',
+    dependencies: [ libbluetooth_internal_dep ]
+  )
+
+  if readline_dep.found()
+    executable('btmgmt',
+      sources: [ 'btmgmt.c', '../src/uuid-helper.c', '../client/display.c', '../client/mgmt.c' ],
+      dependencies: [ libbluetooth_internal_dep, libshared_glib_dep, readline_dep ]
+    )
+    executable('obex-client-tool',
+      sources: [ 'obex-client-tool.c', gobex_sources, btio_sources ],
+      dependencies: [ libbluetooth_internal_dep, libshared_glib_dep, readline_dep, glib_dep ]
+    )
+    executable('obex-server-tool',
+      sources: [ 'obex-server-tool.c', gobex_sources, btio_sources ],
+      dependencies: [ libbluetooth_internal_dep, libshared_glib_dep, glib_dep ]
+    )
+    executable('bluetooth-player',
+      sources: [ 'bluetooth-player.c', '../client/print.c', '../client/player.c' ],
+      dependencies: [ libbluetooth_internal_dep, libshared_glib_dep, libgdbus_internal_dep, readline_dep ]
+    )
+    executable('obexctl',
+      sources: [ 'obexctl.c' ],
+      dependencies: [ libbluetooth_internal_dep, libshared_glib_dep, libgdbus_internal_dep, readline_dep ]
+    )
+    if get_option('mesh').enabled()
+      executable('mesh-cfgtest',
+        sources: 'mesh-cfgtest.c',
+        dependencies: [ libbluetooth_internal_dep, dbus_dep, ell_dep ],
+        include_directories: '..',
+        install: true
+      )
+      if get_option('deprecated').allowed()
+        executable('meshctl',
+          sources: [
+            'meshctl.c',
+            'mesh/agent.c',
+            'mesh-gatt/node.c',
+            'mesh-gatt/gatt.c',
+            'mesh-gatt/crypto.c',
+            'mesh-gatt/net.c',
+            'mesh-gatt/prov.c',
+            'mesh-gatt/util.c',
+            'mesh-gatt/prov-db.c',
+            'mesh-gatt/config-client.c',
+            'mesh-gatt/config-server.c',
+            'mesh-gatt/onoff-model.c'
+            ],
+          dependencies: [
+            libgdbus_internal_dep,
+            libshared_glib_dep,
+            libbluetooth_internal_dep,
+            glib_dep,
+            dbus_dep,
+            jsonc_dep,
+            readline_dep
+            ],
+          install: true
+        )
+      endif
+    endif
+  endif
+
+  if get_option('deprecated').allowed()
+    executable('gatttool',
+      sources: gatttool_sources,
+      dependencies: [ libbluetooth_internal_dep, libshared_glib_dep, readline_dep ],
+    )
+    executable('hciattach',
+      sources: [
+        'hciattach.c',
+        'hciattach_st.c',
+        'hciattach_ti.c',
+        'hciattach_tialt.c',
+        'hciattach_ath3k.c',
+        'hciattach_qualcomm.c',
+        'hciattach_intel.c',
+        'hciattach_bcm43xx.c'
+      ],
+      dependencies: libbluetooth_internal_dep,
+      install: true
+    )
+    if udevlib_dep.found()
+      executable('hcitool',
+        sources: [ 'hcitool.c', '../src/oui.c' ],
+        dependencies: [ libbluetooth_internal_dep, udevlib_dep ],
+        install: true
+      )
+    endif
+    executable('sdptool',
+      sources: [ 'sdptool.c', '../src/sdp-xml.c' ],
+      dependencies: [ libbluetooth_internal_dep, glib_dep ],
+      install: true
+    )
+    deprecated_tools = [
+      'ciptool',
+      'hciconfig',
+      'rfcomm'
+    ]
+    foreach tool: deprecated_tools
+      executable(tool,
+        sources: tool + '.c',
+        dependencies: libbluetooth_internal_dep,
+        install: true
+      )
+    endforeach
+  endif
+
+  if systemd_dep.found()
+    configure_file(
+      input: 'mpris-proxy.service.in',
+      output: 'mpris-proxy.service',
+      configuration: service_in,
+      install: true,
+      install_dir: systemduserunitdir
+    )
+  endif
+endif
+
+if get_option('mesh').enabled()
+  subdir('mesh')
+endif
diff --git a/tools/mpris-proxy.service.in b/tools/mpris-proxy.service.in
index 6ae56c6720b9..a55f0254014a 100644
--- a/tools/mpris-proxy.service.in
+++ b/tools/mpris-proxy.service.in
@@ -8,7 +8,7 @@ ConditionUser=!@system
 
 [Service]
 Type=simple
-ExecStart=@PKGBINDIR@/mpris-proxy
+ExecStart=@PKGLIBEXECDIR@/mpris-proxy
 
 [Install]
 WantedBy=default.target
diff --git a/unit/meson.build b/unit/meson.build
new file mode 100644
index 000000000000..c8198574147d
--- /dev/null
+++ b/unit/meson.build
@@ -0,0 +1,126 @@
+
+tests1 = [
+  [ 'test-tester.c' ],
+  [ 'test-uuid.c' ],
+  [ 'test-eir.c', '../src/eir.c', '../src/uuid-helper.c' ],
+  [ 'test-sdp.c', '../src/sdpd-database.c', '../src/log.c', '../src/sdpd-service.c', '../src/sdpd-request.c' ],
+  [ 'test-avrcp.c', '../src/log.c', 'avctp.c', 'avrcp-lib.c' ],
+  [ 'test-lib.c' ],
+  [ 'test-gatt.c' ],
+  [ 'test-bap.c' ],
+  [ 'test-micp.c' ],
+  [ 'test-bass.c' ],
+  [ 'test-vcp.c' ],
+  [ 'test-battery.c' ],
+  [ 'test-tmap.c' ],
+  [ 'test-gmap.c' ],
+  [ 'test-rap.c' ],
+  [ 'test-mcp.c' ],
+  [ 'test-hog.c',
+    btio_sources,
+    '../profiles/input/hog-lib.c',
+    '../profiles/scanparam/scpp.c',
+    '../profiles/battery/bas.c',
+    '../profiles/deviceinfo/dis.c',
+    '../src/log.c',
+    '../attrib/att.c',
+    '../attrib/gatt.c',
+    '../attrib/gattrib.c'
+  ],
+]
+
+foreach t: tests1
+  test_name = t[0].substring(0, -2)
+  exe = executable(test_name,
+    sources: t,
+    dependencies: [ libshared_glib_dep, libbluetooth_internal_dep ],
+  )
+  test(test_name, exe)
+endforeach
+
+tests2 = [
+  [ 'test-textfile.c', '../src/textfile.c' ],
+  [ 'test-crc.c', '../monitor/crc.c' ],
+  [ 'test-crypto.c' ],
+  [ 'test-ecc.c' ],
+  [ 'test-ringbuf.c' ],
+  [ 'test-queue.c' ],
+  [ 'test-mgmt.c' ],
+  [ 'test-uhid.c' ],
+  [ 'test-hfp.c' ],
+  [ 'test-avdtp.c', '../src/log.c', 'avdtp.c' ],
+  [ 'test-avctp.c', '../src/log.c', 'avctp.c' ],
+  [ 'test-util.c' ],
+]
+
+if ical_dep.found()
+  tests2 += [
+    [ 'test-gobex.c', 'util.c', gobex_sources ],
+    [ 'test-gobex-packet.c', 'util.c', gobex_sources ],
+    [ 'test-gobex-header.c', 'util.c', gobex_sources ],
+    [ 'test-gobex-transfer.c', 'util.c', gobex_sources ],
+    [ 'test-gobex-apparam.c', 'util.c', gobex_sources ],
+  ]
+endif
+
+foreach t: tests2
+  test_name = t[0].substring(0, -2)
+  exe = executable(test_name,
+    sources: t,
+    c_args: [ '-DSTORAGEDIR="@0"'.format(storagedir) ],
+    include_directories: '../lib',
+    dependencies: libshared_glib_dep
+  )
+  test(test_name, exe)
+endforeach
+
+exe = executable('test-gdbus-client',
+  sources: 'test-gdbus-client.c',
+  dependencies: [ libgdbus_internal_dep, libshared_glib_dep ]
+)
+dbus_run_session = find_program('dbus-run-session')
+if dbus_run_session.found()
+  test('test-gdbus-client', dbus_run_session, args: [ '--', exe ])
+else
+  test('test-gdbus-client', exe)
+endif
+
+exe = executable('test-gattrib',
+  sources: [ 'test-gattrib.c', '../attrib/gattrib.c', btio_sources, '../src/log.c' ],
+  dependencies: [ libshared_glib_dep, libbluetooth_internal_dep ]
+)
+test('test-gattrib', exe)
+
+exe = executable('test-profile',
+  sources: [ 'test-profile.c', 'btd.c', '../src/profile.c' ],
+  dependencies: [ dbus_dep, libshared_glib_dep, libbluetooth_internal_dep ]
+)
+test('test-profile', exe)
+
+if alsa_dep.found()
+  exe = executable('test-midi',
+    sources: [ 'test-midi.c', '../profiles/midi/libmidi.c' ],
+    c_args: [ '-DMIDI_TEST' ],
+    dependencies: [ libshared_glib_dep, alsa_dep ]
+  )
+  test('test-midi', exe)
+endif
+
+if get_option('mesh').enabled()
+  exe = executable('test-mesh-crypto',
+    sources: [ 'test-mesh-crypto.c' ],
+    dependencies: [ ell_dep ]
+  )
+  test('test-mesh-crypto', exe)
+endif
+
+add_test_setup(
+  'valgrind',
+  exe_wrapper: [
+    'valgrind', '--quiet', '--leak-check=full', '--trace-children=yes',
+    '--suppressions=@0@'.format(meson.current_source_dir() / 'valgrind.supp'),
+    '--num-callers=30', '--show-reachable=no', '--error-exitcode=1',
+  ],
+  # default timeout in meson is 30s
+  timeout_multiplier: 4,
+)
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 03/15] build: Separate systemd and libsystemd dependencies
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 01/15] build: Add meson wrap for libell Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 02/15] build: Add meson build system Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 04/15] tools: Install gatttool if deprecated tools are enabled Bastien Nocera
                   ` (11 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

One is to install systemd service files, the other to enable deeper
logind integration into obexd.
---
 meson.build       | 3 ++-
 meson_options.txt | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/meson.build b/meson.build
index d3c43d79141f..0b5cc0b7ce69 100644
--- a/meson.build
+++ b/meson.build
@@ -138,7 +138,8 @@ readline_dep = dependency('readline',
   required: get_option('client').enabled() or
     get_option('mesh').enabled())
 systemd_dep = dependency('systemd', required: get_option('systemd'))
-libsystemd_dep = dependency('libsystemd', required: get_option('systemd'))
+libsystemd_dep = dependency('libsystemd',
+  required: get_option('obex').enabled() and get_option('logind').enabled())
 
 ell_args = {
   'version': '>= 0.39',
diff --git a/meson_options.txt b/meson_options.txt
index 4a0c726c2098..54cfdbb0d7c1 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -6,7 +6,8 @@ option('tests', type: 'feature', value: 'auto', description: 'Enable testing too
 option('tools', type: 'feature', value: 'enabled', description: 'Enable Bluetooth tools')
 option('monitor', type: 'feature', value: 'enabled', description: 'Enable Bluetooth monitor')
 option('client', type: 'feature', value: 'enabled', description: 'Enable command line client')
-option('systemd', type: 'feature', value: 'enabled', description: 'Enable systemd integration')
+option('systemd', type: 'feature', value: 'enabled', description: 'Install systemd service files')
+option('logind', type: 'feature', value: 'enabled', description: 'Enable logind integration in obexd')
 option('udev', type: 'feature', value: 'enabled', description: 'Enable udev device support in tools')
 option('datafiles', type: 'feature', value: 'enabled', description: 'Install configuration and data files')
 option('manpages', type: 'feature', value: 'auto', description: 'Enable building of manual pages')
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 04/15] tools: Install gatttool if deprecated tools are enabled
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (2 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 03/15] build: Separate systemd and libsystemd dependencies Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 05/15] tools: Install avinfo tool by default Bastien Nocera
                   ` (10 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

Fixes:
https://bugzilla.redhat.com/show_bug.cgi?id=1141909
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=720486
---
 tools/meson.build | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tools/meson.build b/tools/meson.build
index 8fc0dc1d2fb5..5f092e7e1b5c 100644
--- a/tools/meson.build
+++ b/tools/meson.build
@@ -204,6 +204,7 @@ if get_option('tools').allowed()
     executable('gatttool',
       sources: gatttool_sources,
       dependencies: [ libbluetooth_internal_dep, libshared_glib_dep, readline_dep ],
+      install: true
     )
     executable('hciattach',
       sources: [
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 05/15] tools: Install avinfo tool by default
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (3 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 04/15] tools: Install gatttool if deprecated tools are enabled Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 06/15] emulator: Install the emulator if built Bastien Nocera
                   ` (9 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

It's used for checking which audio codecs are supported by a Bluetooth
audio device, which is more useful now that PulseAudio/PipeWire support
alternative codecs like LDAC or aptX).

Fixes:
https://bugzilla.redhat.com/show_bug.cgi?id=1699680
---
 tools/meson.build | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/meson.build b/tools/meson.build
index 5f092e7e1b5c..1c2595a31f77 100644
--- a/tools/meson.build
+++ b/tools/meson.build
@@ -61,7 +61,6 @@ if udevlib_hid2hci_dep.found()
 endif
 
 tools_names = [
-  'avinfo',
   'avtest',
   'scotest',
   'hwdb',
@@ -82,6 +81,7 @@ tools_names = [
 ]
 
 inst_tools_names = [
+  'avinfo',
   'rctest',
   'l2test',
   'l2ping',
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 06/15] emulator: Install the emulator if built
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (4 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 05/15] tools: Install avinfo tool by default Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 07/15] build: Add option to allow disabling bluetoothd Bastien Nocera
                   ` (8 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

It's useful for testing.
---
 emulator/meson.build | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/emulator/meson.build b/emulator/meson.build
index 40544567e4e4..9da91b16671b 100644
--- a/emulator/meson.build
+++ b/emulator/meson.build
@@ -18,6 +18,8 @@ if get_option('tests').allowed()
       'phy.c',
       'le.c' ],
     dependencies: [ libbluetooth_internal_dep, libshared_mainloop_dep ]
+    install: true,
+    install_dir: pkglibexecdir
   )
 
   executable('b1ee',
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 07/15] build: Add option to allow disabling bluetoothd
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (5 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 06/15] emulator: Install the emulator if built Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 08/15] unit: Run test-bap tests concurrently Bastien Nocera
                   ` (7 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

This makes it possible to build, for example, just the library, or the
command-line client, without also building and installing bluetoothd.
---
 doc/meson.build   |   3 +-
 meson.build       |   8 ++-
 meson_options.txt |   1 +
 src/meson.build   | 164 +++++++++++++++++++++++-----------------------
 4 files changed, 92 insertions(+), 84 deletions(-)

diff --git a/doc/meson.build b/doc/meson.build
index 3d5ccd7d3034..498b6a479067 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -146,7 +146,8 @@ bluetoothd_rst = configure_file(
   output: 'bluetoothd.rst',
   configuration: man_conf
 )
-if rst2man.found()
+
+if get_option('daemon').enabled() and rst2man.found()
   custom_target(
     input: bluetoothd_rst,
     output: 'bluetoothd.8',
diff --git a/meson.build b/meson.build
index 0b5cc0b7ce69..f716eae1f063 100644
--- a/meson.build
+++ b/meson.build
@@ -293,8 +293,12 @@ endif
 # Fix permissions on install
 install = find_program('install')
 sh = find_program('sh')
-meson.add_install_script(sh, '-c', 'install -dm755 ${DESTDIR}/' + configdir)
-meson.add_install_script(sh, '-c', 'install -dm700 ${DESTDIR}/' + storagedir)
+if get_option('daemon').enabled()
+  meson.add_install_script(sh, '-c', 'install -dm755 ${DESTDIR}/' + configdir)
+endif
+if get_option('daemon').enabled() or get_option('mesh').enabled()
+  meson.add_install_script(sh, '-c', 'install -dm700 ${DESTDIR}/' + storagedir)
+endif
 if get_option('mesh').enabled()
   meson.add_install_script(sh, '-c', 'install -dm700 ${DESTDIR}/' + meshstoragedir)
 endif
diff --git a/meson_options.txt b/meson_options.txt
index 54cfdbb0d7c1..79e75e33ddaf 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -6,6 +6,7 @@ option('tests', type: 'feature', value: 'auto', description: 'Enable testing too
 option('tools', type: 'feature', value: 'enabled', description: 'Enable Bluetooth tools')
 option('monitor', type: 'feature', value: 'enabled', description: 'Enable Bluetooth monitor')
 option('client', type: 'feature', value: 'enabled', description: 'Enable command line client')
+option('daemon', type: 'feature', value: 'enabled', description: 'Enable bluetoothd daemon')
 option('systemd', type: 'feature', value: 'enabled', description: 'Install systemd service files')
 option('logind', type: 'feature', value: 'enabled', description: 'Enable logind integration in obexd')
 option('udev', type: 'feature', value: 'enabled', description: 'Enable udev device support in tools')
diff --git a/src/meson.build b/src/meson.build
index 65b7131c86b4..4962b782a7b6 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,88 +1,90 @@
 subdir('shared')
 
-if get_option('datafiles').allowed()
-  install_data('bluetooth.conf',
-    install_dir: dbusconfdir / 'dbus-1/system.d'
-  )
-  install_data('main.conf',
-    install_dir: configdir
-  )
-endif
+if get_option('daemon').allowed()
+  if get_option('datafiles').allowed()
+    install_data('bluetooth.conf',
+      install_dir: dbusconfdir / 'dbus-1/system.d'
+    )
+    install_data('main.conf',
+      install_dir: configdir
+    )
+  endif
 
-if systemd_dep.found()
-  configure_file(
-    input: 'bluetooth.service.in',
-    output: 'bluetooth.service',
-    install: true,
-    install_dir: systemdsystemunitdir,
-    configuration: service_conf
+  if systemd_dep.found()
+    configure_file(
+      input: 'bluetooth.service.in',
+      output: 'bluetooth.service',
+      install: true,
+      install_dir: systemdsystemunitdir,
+      configuration: service_conf
+    )
+
+    install_data('org.bluez.service',
+      install_dir: dbussystembusdir
+    )
+  endif
+
+  genbuiltin = find_program('genbuiltin')
+  builtin_h = custom_target('src-builtin.h',
+    output: 'builtin.h',
+    capture: true,
+    command: [ genbuiltin, builtin_modules ]
   )
 
-  install_data('org.bluez.service',
-    install_dir: dbussystembusdir
+  executable('bluetoothd',
+    sources: [ builtin_sources,
+      attrib_sources,
+      btio_sources,
+      builtin_h,
+      'main.c',
+      'log.c',
+      'backtrace.c',
+      'rfkill.c',
+      'sdpd-server.c',
+      'sdpd-request.c',
+      'sdpd-service.c',
+      'sdpd-database.c',
+      'gatt-database.c',
+      'sdp-xml.c',
+      'sdp-client.c',
+      'textfile.c',
+      'uuid-helper.c',
+      'plugin.c',
+      'storage.c',
+      'advertising.c',
+      'agent.c',
+      'bearer.c',
+      'error.c',
+      'adapter.c',
+      'profile.c',
+      'service.c',
+      'gatt-client.c',
+      'device.c',
+      'dbus-common.c',
+      'eir.c',
+      'adv_monitor.c',
+      'battery.c',
+      'settings.c',
+      'set.c' ],
+    dependencies: [
+      libbluetooth_internal_dep,
+      libgdbus_internal_dep,
+      libshared_glib_dep,
+      backtrace_dep,
+      glib_dep,
+      dbus_dep,
+      udevlib_dep,
+      builtin_deps
+    ],
+    override_options: [ 'b_lundef=false' ],
+    c_args: [
+      '-DBLUETOOTH_PLUGIN_BUILTIN',
+      '-DPLUGINDIR="@0@"'.format(plugindir),
+      '-DSTORAGEDIR="@0@"'.format(storagedir),
+      '-DCONFIGDIR="@0@"'.format(configdir)
+    ],
+    link_args: [ '-Wl,--gc-sections', external_plugins_link_args ],
+    install: true,
+    install_dir: pkglibexecdir
   )
 endif
-
-genbuiltin = find_program('genbuiltin')
-builtin_h = custom_target('src-builtin.h',
-  output: 'builtin.h',
-  capture: true,
-  command: [ genbuiltin, builtin_modules ]
-)
-
-executable('bluetoothd',
-  sources: [ builtin_sources,
-    attrib_sources,
-    btio_sources,
-    builtin_h,
-    'main.c',
-    'log.c',
-    'backtrace.c',
-    'rfkill.c',
-    'sdpd-server.c',
-    'sdpd-request.c',
-    'sdpd-service.c',
-    'sdpd-database.c',
-    'gatt-database.c',
-    'sdp-xml.c',
-    'sdp-client.c',
-    'textfile.c',
-    'uuid-helper.c',
-    'plugin.c',
-    'storage.c',
-    'advertising.c',
-    'agent.c',
-    'bearer.c',
-    'error.c',
-    'adapter.c',
-    'profile.c',
-    'service.c',
-    'gatt-client.c',
-    'device.c',
-    'dbus-common.c',
-    'eir.c',
-    'adv_monitor.c',
-    'battery.c',
-    'settings.c',
-    'set.c' ],
-  dependencies: [
-    libbluetooth_internal_dep,
-    libgdbus_internal_dep,
-    libshared_glib_dep,
-    backtrace_dep,
-    glib_dep,
-    dbus_dep,
-    udevlib_dep,
-    builtin_deps
-  ],
-  override_options: [ 'b_lundef=false' ],
-  c_args: [
-    '-DBLUETOOTH_PLUGIN_BUILTIN',
-    '-DPLUGINDIR="@0@"'.format(plugindir),
-    '-DSTORAGEDIR="@0@"'.format(storagedir),
-    '-DCONFIGDIR="@0@"'.format(configdir)
-  ],
-  link_args: [ '-Wl,--gc-sections', external_plugins_link_args ],
-  install: true,
-  install_dir: pkglibexecdir
-)
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 08/15] unit: Run test-bap tests concurrently
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (6 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 07/15] build: Add option to allow disabling bluetoothd Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 09/15] unit: Make gobex-transfer tests run concurrently Bastien Nocera
                   ` (6 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

---
 meson.build           |  2 +-
 unit/meson.build      | 25 ++++++++++++++++++++++---
 unit/unittest_list.sh | 15 +++++++++++++++
 3 files changed, 38 insertions(+), 4 deletions(-)
 create mode 100644 unit/unittest_list.sh

diff --git a/meson.build b/meson.build
index f716eae1f063..586b69459d8a 100644
--- a/meson.build
+++ b/meson.build
@@ -285,6 +285,7 @@ endif
 if rst2man.found()
   subdir('doc')
 endif
+sh = find_program('sh', required: true)
 if get_option('tests').allowed()
   subdir('test')
   subdir('unit')
@@ -292,7 +293,6 @@ endif
 
 # Fix permissions on install
 install = find_program('install')
-sh = find_program('sh')
 if get_option('daemon').enabled()
   meson.add_install_script(sh, '-c', 'install -dm755 ${DESTDIR}/' + configdir)
 endif
diff --git a/unit/meson.build b/unit/meson.build
index c8198574147d..1bd3b7a85992 100644
--- a/unit/meson.build
+++ b/unit/meson.build
@@ -7,7 +7,6 @@ tests1 = [
   [ 'test-avrcp.c', '../src/log.c', 'avctp.c', 'avrcp-lib.c' ],
   [ 'test-lib.c' ],
   [ 'test-gatt.c' ],
-  [ 'test-bap.c' ],
   [ 'test-micp.c' ],
   [ 'test-bass.c' ],
   [ 'test-vcp.c' ],
@@ -39,6 +38,26 @@ foreach t: tests1
 endforeach
 
 tests2 = [
+  [ 'test-bap.c' ],
+]
+
+unittest_list = find_program('unittest_list.sh')
+
+foreach t: tests2
+  test_name = t[0].substring(0, -2)
+  exe = executable(test_name,
+    sources: t,
+    dependencies: [ libshared_glib_dep, libbluetooth_internal_dep ],
+  )
+
+  r = run_command(sh, unittest_list, t, check: true)
+  unit_tests = r.stdout().strip().split(' ')
+  foreach ut: unit_tests
+    test(ut, exe, args: [ '-p', ut ])
+  endforeach
+endforeach
+
+tests3 = [
   [ 'test-textfile.c', '../src/textfile.c' ],
   [ 'test-crc.c', '../monitor/crc.c' ],
   [ 'test-crypto.c' ],
@@ -54,7 +73,7 @@ tests2 = [
 ]
 
 if ical_dep.found()
-  tests2 += [
+  tests3 += [
     [ 'test-gobex.c', 'util.c', gobex_sources ],
     [ 'test-gobex-packet.c', 'util.c', gobex_sources ],
     [ 'test-gobex-header.c', 'util.c', gobex_sources ],
@@ -63,7 +82,7 @@ if ical_dep.found()
   ]
 endif
 
-foreach t: tests2
+foreach t: tests3
   test_name = t[0].substring(0, -2)
   exe = executable(test_name,
     sources: t,
diff --git a/unit/unittest_list.sh b/unit/unittest_list.sh
new file mode 100644
index 000000000000..c478edc9d3e4
--- /dev/null
+++ b/unit/unittest_list.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+if [ $# -lt 1 ] ; then
+	echo "Usage: $0 FILE..."
+	exit 1
+fi
+
+TESTS=$(grep '^[[:space:]]*define_test[_a-z]*("' $* | cut -d \" -f 2 | cut -d " " -f 1)
+if [ -z "$TESTS" ] ; then
+	TESTS=$(grep '^[[:space:]]*tester_add("' $* | cut -d \" -f 2 | cut -d " " -f 1)
+fi
+if [ -z "$TESTS" ] ; then
+	TESTS=$(grep '^[[:space:]]*g_test_add_*func("' $* | cut -d \" -f 2 | cut -d " " -f 1)
+fi
+echo $TESTS
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 09/15] unit: Make gobex-transfer tests run concurrently
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (7 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 08/15] unit: Run test-bap tests concurrently Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 10/15] build: Only build profiles if the daemon is built Bastien Nocera
                   ` (5 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

---
 unit/meson.build | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/unit/meson.build b/unit/meson.build
index 1bd3b7a85992..92e1c6f66caa 100644
--- a/unit/meson.build
+++ b/unit/meson.build
@@ -71,15 +71,18 @@ tests3 = [
   [ 'test-avctp.c', '../src/log.c', 'avctp.c' ],
   [ 'test-util.c' ],
 ]
+tests4 = []
 
 if ical_dep.found()
   tests3 += [
     [ 'test-gobex.c', 'util.c', gobex_sources ],
     [ 'test-gobex-packet.c', 'util.c', gobex_sources ],
     [ 'test-gobex-header.c', 'util.c', gobex_sources ],
-    [ 'test-gobex-transfer.c', 'util.c', gobex_sources ],
     [ 'test-gobex-apparam.c', 'util.c', gobex_sources ],
   ]
+  tests4 = [
+    [ 'test-gobex-transfer.c', 'util.c', gobex_sources ],
+  ]
 endif
 
 foreach t: tests3
@@ -93,6 +96,22 @@ foreach t: tests3
   test(test_name, exe)
 endforeach
 
+foreach t: tests4
+  test_name = t[0].substring(0, -2)
+  exe = executable(test_name,
+    sources: t,
+    c_args: [ '-DSTORAGEDIR="@0"'.format(storagedir) ],
+    include_directories: '../lib',
+    dependencies: libshared_glib_dep
+  )
+
+  r = run_command(sh, unittest_list, t, check: true)
+  unit_tests = r.stdout().strip().split(' ')
+  foreach ut: unit_tests
+    test(ut, exe, args: [ '-p', ut ])
+  endforeach
+endforeach
+
 exe = executable('test-gdbus-client',
   sources: 'test-gdbus-client.c',
   dependencies: [ libgdbus_internal_dep, libshared_glib_dep ]
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 10/15] build: Only build profiles if the daemon is built
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (8 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 09/15] unit: Make gobex-transfer tests run concurrently Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 11/15] build: Only build gdbus library if there is a user Bastien Nocera
                   ` (4 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

---
 meson.build | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/meson.build b/meson.build
index 586b69459d8a..b00e378d04ca 100644
--- a/meson.build
+++ b/meson.build
@@ -263,7 +263,9 @@ subdir('gdbus')
 subdir('attrib')
 subdir('btio')
 subdir('plugins')
-subdir('profiles')
+if get_option('daemon').enabled()
+  subdir('profiles')
+endif
 subdir('src')
 subdir('client')
 if get_option('monitor').allowed()
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 11/15] build: Only build gdbus library if there is a user
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (9 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 10/15] build: Only build profiles if the daemon is built Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 12/15] unit: Add integration tests Bastien Nocera
                   ` (3 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

---
 meson.build | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/meson.build b/meson.build
index b00e378d04ca..e034cdf80fb9 100644
--- a/meson.build
+++ b/meson.build
@@ -259,7 +259,9 @@ configure_file(
 )
 
 subdir('lib')
-subdir('gdbus')
+if not get_option('tools').disabled() or get_option('client').enabled() or get_option('obex').enabled() or not get_option('tests').disabled() or not get_option('daemon').disabled()
+  subdir('gdbus')
+endif
 subdir('attrib')
 subdir('btio')
 subdir('plugins')
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 12/15] unit: Add integration tests
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (10 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 11/15] build: Only build gdbus library if there is a user Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 13/15] doc: Add HTML documentation Bastien Nocera
                   ` (2 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

Add integration tests that will start btvirt if no Bluetooth adapters
are available, start bluetoothd if not already started, and run some
of our tools in conditions where they would have caused problems.

If a Bluetooth adapter is plugged in and bluetoothd started, the tests
will use those to run the tests against, making it easy to run tests on
a developer's machine.

If there are no available Bluetooth adapters and bluetoothd isn't
running, the tests will need to run as root, as is common on CI setups.
---
 meson.build                      |  11 ++
 unit/integration-test-skipped.sh |   3 +
 unit/integration-test.py         | 271 +++++++++++++++++++++++++++++++
 unit/meson.build                 |  30 ++++
 unit/output_checker.py           | 214 ++++++++++++++++++++++++
 unit/unittest_inspector.py       |  46 ++++++
 6 files changed, 575 insertions(+)
 create mode 100755 unit/integration-test-skipped.sh
 create mode 100755 unit/integration-test.py
 create mode 100644 unit/output_checker.py
 create mode 100755 unit/unittest_inspector.py

diff --git a/meson.build b/meson.build
index e034cdf80fb9..91042e437438 100644
--- a/meson.build
+++ b/meson.build
@@ -258,6 +258,14 @@ configure_file(
   configuration: config_h
 )
 
+python = import('python')
+python3 = python.find_installation('python3')
+has_pythonmodules = false
+script = 'import dbusmock\nimport pexpect'
+if run_command(python3, '-c', script, check: false).returncode() == 0
+  has_pythonmodules = true
+endif
+
 subdir('lib')
 if not get_option('tools').disabled() or get_option('client').enabled() or get_option('obex').enabled() or not get_option('tests').disabled() or not get_option('daemon').disabled()
   subdir('gdbus')
@@ -306,3 +314,6 @@ endif
 if get_option('mesh').enabled()
   meson.add_install_script(sh, '-c', 'install -dm700 ${DESTDIR}/' + meshstoragedir)
 endif
+
+summary({'Integration tests': has_pythonmodules},
+  section: 'General')
diff --git a/unit/integration-test-skipped.sh b/unit/integration-test-skipped.sh
new file mode 100755
index 000000000000..ac1449442020
--- /dev/null
+++ b/unit/integration-test-skipped.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exit 77
diff --git a/unit/integration-test.py b/unit/integration-test.py
new file mode 100755
index 000000000000..7d5f8d51ea17
--- /dev/null
+++ b/unit/integration-test.py
@@ -0,0 +1,271 @@
+#!/usr/bin/python3
+
+# Copyright: (C) 2025 Bastien Nocera <hadess@hadess.net>
+# Copyright: (C) 2023 cagney, for the AsciiDecoder
+#
+# 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.
+
+import os
+import sys
+import subprocess
+import unittest
+import time
+import re
+from output_checker import OutputChecker
+
+try:
+    import gi
+    from gi.repository import GLib
+except ImportError as e:
+    sys.stderr.write('Skipping tests, PyGobject not available for Python 3, or missing GI typelibs: %s\n' % str(e))
+    sys.exit(77)
+
+builddir = os.getenv('top_builddir', '.')
+
+try:
+    import dbusmock
+    import dbus
+except ImportError:
+    sys.stderr.write('Skipping tests, python-dbusmock not available (https://pypi.python.org/pypi/python-dbusmock).\n')
+    sys.exit(77)
+
+try:
+    import pexpect
+except ImportError:
+    sys.stderr.write('Skipping tests, pexpect not available (https://pypi.org/project/pexpect/).\n')
+    sys.exit(77)
+
+def count_adapters():
+    try:
+        for entry in os.walk('/sys/class/bluetooth'):
+            return len(entry[1])
+        return 0
+    except OSError:
+        return 0
+
+def is_root():
+    return os.geteuid() == 0
+
+# https://stackoverflow.com/a/76543345
+# CC-BY-SA 4.0 relicensed under GPLv2 or later
+# see https://web.archive.org/web/20200805075926/http://ascii-table.com/ansi-escape-sequences.php
+class AsciiDecoder(object):
+    def __init__(self):
+        self.buf = b''
+    def encode(self, b, final=False):
+        return b
+    def decode(self, b, final=False):
+        # escape sequences can be split so
+        # work on lines
+        self.buf = self.buf + b
+        i = self.buf.find(b'\n')
+        if i >= 0:
+            c = self.buf[0:i+1]
+            self.buf = self.buf[i+1:]
+            d = re.sub(rb'\x1b\[[0-9;=?]*[HfABCDsuJKmhlr]', b'*', c)
+            e = re.sub(rb'\x1b', b'<ESC>', d)
+            if e != e:
+                print(">", e, "<")
+            return e
+        return b''
+
+class Tests(dbusmock.DBusTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.dbus_con = cls.get_dbus(True)
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+
+    def setUp(self):
+        super().setUp()
+        self.daemon_log = OutputChecker()
+
+    def stop_virt(self):
+        if self.virt:
+            try:
+                self.virt.terminate()
+            except OSError:
+                pass
+            self.assertEqual(self.virt.wait(timeout=3000), 0)
+
+        self.virt = None
+
+    def start_virt(self):
+        if not is_root():
+            self.skipTest('btvirt cannot start as we are not root')
+
+        num_adapters = count_adapters()
+
+        self.virt = subprocess.Popen(
+            [ self.btvirt_path(), '-l1' ],
+        )
+        self.addCleanup(self.stop_virt)
+
+        self.assert_eventually(lambda: count_adapters() > num_adapters)
+
+    def start_virt_if_needed(self):
+        if count_adapters() == 0:
+            self.start_virt()
+
+    def stop_daemon(self):
+        if self.daemon:
+            try:
+                self.daemon.terminate()
+            except OSError:
+                pass
+            self.assertEqual(self.daemon.wait(timeout=3000), 0)
+
+        self.daemon = None
+
+    def daemon_check(self):
+        if not self.dbus_con.name_has_owner('org.bluez'):
+            return False
+
+        try:
+            p = dbus.Interface(self.dbus_con.get_object('org.bluez', '/'),
+                               dbus_interface=dbus.INTROSPECTABLE_IFACE)
+            p.Introspect()
+            return True
+        except dbus.exceptions.DBusException as e:
+            last_exc = e
+            if '.UnknownInterface' in str(e):
+                return False
+
+    def start_daemon(self):
+        if not is_root():
+            self.skipTest('bluetoothd cannot start as we are not root')
+
+        self.daemon = subprocess.Popen(
+            [ self.bluetoothd_path(), '--nodetach', '-f', self.bluetooth_config_path() ],
+            stdout=subprocess.DEVNULL, stderr=self.daemon_log.fd
+        )
+        self.daemon_log.writer_attached()
+        self.addCleanup(self.stop_daemon)
+
+        self.assert_eventually(lambda: self.daemon_check())
+
+    def start_daemon_if_needed(self):
+        # Only start daemon if one is not already available
+        if not self.daemon_check():
+            self.start_daemon()
+
+    def binary_path(self, binary):
+        builddir = os.getenv('top_builddir', '.')
+        return os.path.join(builddir, binary)
+
+    def bluetoothctl_path(self):
+        return self.binary_path('client/bluetoothctl')
+
+    def btmgmt_path(self):
+        return self.binary_path('tools/btmgmt')
+
+    def btvirt_path(self):
+        return self.binary_path('emulator/btvirt')
+
+    def bluetoothd_path(self):
+        return self.binary_path('src/bluetoothd')
+
+    def bluetooth_config_path(self):
+        srcdir = os.getenv('top_srcdir', '.')
+        return os.path.join(srcdir, 'src/main.conf')
+
+    def assert_eventually(self, condition, message=None, timeout=5000, keep_checking=0):
+        """Assert that condition function eventually returns True.
+
+        Timeout is in milliseconds, defaulting to 5000 (5 seconds). message is
+        printed on failure.
+        """
+        if not keep_checking:
+            if condition():
+                return
+
+        done = False
+
+        def on_timeout_reached():
+            nonlocal done
+            done = True
+
+        source = GLib.timeout_add(timeout, on_timeout_reached)
+        while not done:
+            if condition():
+                GLib.source_remove(source)
+                if keep_checking > 0:
+                    self.assert_condition_persists(
+                        condition, message, timeout=keep_checking
+                    )
+                return
+            GLib.MainContext.default().iteration(False)
+
+        self.fail(message() if message else f"timed out waiting for {condition}")
+
+    def test_bluetoothctl_no_output(self):
+        self.start_virt_if_needed()
+        self.start_daemon_if_needed()
+
+        num_adapters = count_adapters()
+
+        cmd = subprocess.run([ self.bluetoothctl_path(), 'list' ], check=False, capture_output=True)
+        output = cmd.stdout.decode('UTF-8')
+        self.assertEqual(len(output.strip().split('\n')), num_adapters)
+        self.assertRegex(output, '.*\\[default\\]')
+
+    def test_btmgmt_stdin_hang(self):
+        self.start_virt_if_needed()
+
+        num_adapters = count_adapters()
+
+        cmd = subprocess.run([ self.btmgmt_path(), 'info' ], stdin=subprocess.DEVNULL, check=False, capture_output=True)
+        output = cmd.stdout.decode('UTF-8')
+        self.assertRegex(output, '.*Index list with ' + str(num_adapters) + ' item')
+
+    def test_btmgmt_help_hang(self):
+        devnull = open(os.devnull, 'w')
+        cmd_log = OutputChecker(out=devnull)
+        cmd = subprocess.Popen([ self.btmgmt_path(), '--help' ], stdout=cmd_log.fd)
+        cmd_log.writer_attached()
+
+        # Check that btmgmt --help doesn't hang
+        self.assertEqual(cmd.wait(timeout=1), 0)
+        devnull.close()
+
+        # Check that it contains the help output
+        cmd_log.check_line('Send HCI Command and wait for Event', timeout=1)
+
+    def test_bluetoothd_failed_to_set_default_config(self):
+        if self.daemon_check():
+            # We need to start bluetoothd ourselves, so skip the test
+            self.skipTest('Test requires that we start bluetoothd ourselves')
+
+        self.start_virt_if_needed()
+        self.start_daemon()
+
+        self.daemon_log.check_line('Battery Provider Manager created', timeout=1)
+        self.daemon_log.check_no_line('Failed to set default system config for',
+                                      failmsg='Error output when using stock configuration')
+
+    def test_bluetoothctl_completion_crash(self):
+        child = pexpect.spawn(self.bluetoothctl_path())
+        child._decoder = AsciiDecoder()
+        child.expect(r'\[bluetoothctl\]> ')
+        child.sendline(' \t')
+        child.expect(r'\[bluetoothctl\]> ')
+
+        time.sleep(0.5)
+
+        self.assertTrue(child.isalive())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/unit/meson.build b/unit/meson.build
index 92e1c6f66caa..1fea15766897 100644
--- a/unit/meson.build
+++ b/unit/meson.build
@@ -162,3 +162,33 @@ add_test_setup(
   # default timeout in meson is 30s
   timeout_multiplier: 4,
 )
+
+if not has_pythonmodules
+  test('integration-test-skipped',
+    find_program('integration-test-skipped.sh'),
+  )
+else
+  integration_test = find_program('integration-test.py')
+
+  envs = environment()
+  envs.set ('top_builddir', meson.global_build_root())
+  envs.set ('top_srcdir', meson.global_source_root())
+
+  test_deps = []
+
+  python3 = find_program('python3')
+  unittest_inspector = find_program('unittest_inspector.py')
+  r = run_command(unittest_inspector, files('integration-test.py'), check: true)
+  unit_tests = r.stdout().strip().split('\n')
+
+  foreach ut: unit_tests
+    ut_args = files('integration-test.py')
+    ut_args += ut
+    test(ut,
+      python3,
+      args: ut_args,
+      env: envs,
+      depends: test_deps
+    )
+  endforeach
+endif
diff --git a/unit/output_checker.py b/unit/output_checker.py
new file mode 100644
index 000000000000..037e248b9311
--- /dev/null
+++ b/unit/output_checker.py
@@ -0,0 +1,214 @@
+#! /usr/bin/env python3
+# Copyright © 2020, RedHat Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+# Authors:
+#       Benjamin Berg <bberg@redhat.com>
+
+import os
+import sys
+import fcntl
+import io
+import re
+import time
+import threading
+import select
+import errno
+
+
+class OutputChecker(object):
+    def __init__(self, out=sys.stdout):
+        self._output = out
+        self._pipe_fd_r, self._pipe_fd_w = os.pipe()
+        self._partial_buf = b""
+        self._lines_sem = threading.Semaphore()
+        self._lines = []
+        self._reader_io = io.StringIO()
+
+        # Just to be sure, shouldn't be a problem even if we didn't set it
+        fcntl.fcntl(
+            self._pipe_fd_r,
+            fcntl.F_SETFL,
+            fcntl.fcntl(self._pipe_fd_r, fcntl.F_GETFL) | os.O_CLOEXEC | os.O_NONBLOCK,
+        )
+        fcntl.fcntl(
+            self._pipe_fd_w,
+            fcntl.F_SETFL,
+            fcntl.fcntl(self._pipe_fd_w, fcntl.F_GETFL) | os.O_CLOEXEC,
+        )
+
+        # Start copier thread
+        self._thread = threading.Thread(target=self._copy, daemon=True)
+        self._thread.start()
+
+    def _copy(self):
+        p = select.poll()
+        p.register(self._pipe_fd_r)
+        while True:
+            try:
+                # Be lazy and wake up occasionally in case _pipe_fd_r became invalid
+                # The reason to do this is because os.read() will *not* return if the
+                # FD is forcefully closed.
+                p.poll(0.1)
+
+                r = os.read(self._pipe_fd_r, 1024)
+                if not r:
+                    os.close(self._pipe_fd_r)
+                    self._pipe_fd_r = -1
+                    self._lines_sem.release()
+                    return
+            except OSError as e:
+                if e.errno == errno.EWOULDBLOCK:
+                    continue
+
+                # We get a bad file descriptor error when the outside closes the FD
+                if self._pipe_fd_r >= 0:
+                    os.close(self._pipe_fd_r)
+                self._pipe_fd_r = -1
+                self._lines_sem.release()
+                return
+
+            l = r.split(b"\n")
+            l[0] = self._partial_buf + l[0]
+            self._lines.extend(l[:-1])
+            self._partial_buf = l[-1]
+
+            self._lines_sem.release()
+
+            os.write(self._output.fileno(), r)
+
+    def check_line_re(self, needle_re, timeout=0, failmsg=None):
+        deadline = time.time() + timeout
+
+        if isinstance(needle_re, str):
+            needle_re = needle_re.encode("ascii")
+
+        r = re.compile(needle_re)
+        ret = []
+
+        while True:
+            try:
+                l = self._lines.pop(0)
+            except IndexError:
+                # EOF, throw error
+                if self._pipe_fd_r == -1:
+                    if failmsg:
+                        raise AssertionError(
+                            "No further messages: " % failmsg
+                        ) from None
+                    else:
+                        raise AssertionError(
+                            "No client waiting for needle %s" % (str(needle_re))
+                        ) from None
+
+                # Check if should wake up
+                if not self._lines_sem.acquire(timeout=deadline - time.time()):
+                    if failmsg:
+                        raise AssertionError(failmsg) from None
+                    else:
+                        raise AssertionError(
+                            "Timed out waiting for needle %s (timeout: %0.2f)"
+                            % (str(needle_re), timeout)
+                        ) from None
+                continue
+
+            ret.append(l)
+            if r.search(l):
+                return ret
+
+    def check_line(self, needle, timeout=0, failmsg=None):
+        if isinstance(needle, str):
+            needle = needle.encode("ascii")
+
+        needle_re = re.escape(needle)
+
+        return self.check_line_re(needle_re, timeout=timeout, failmsg=failmsg)
+
+    def check_no_line_re(self, needle_re, wait=0, failmsg=None):
+        deadline = time.time() + wait
+
+        if isinstance(needle_re, str):
+            needle_re = needle_re.encode("ascii")
+
+        r = re.compile(needle_re)
+        ret = []
+
+        while True:
+            try:
+                l = self._lines.pop(0)
+            except IndexError:
+                # EOF, so everything good
+                if self._pipe_fd_r == -1:
+                    break
+
+                # Check if should wake up
+                if not self._lines_sem.acquire(timeout=deadline - time.time()):
+                    # Timed out, so everything is good
+                    break
+                continue
+
+            ret.append(l)
+            if r.search(l):
+                if failmsg:
+                    raise AssertionError(failmsg)
+                else:
+                    raise AssertionError(
+                        "Found needle %s but shouldn't have been there (timeout: %0.2f)"
+                        % (str(needle_re), wait)
+                    )
+
+        return ret
+
+    def check_no_line(self, needle, wait=0, failmsg=None):
+        if isinstance(needle, str):
+            needle = needle.encode("ascii")
+
+        needle_re = re.escape(needle)
+
+        return self.check_no_line_re(needle_re, wait=wait, failmsg=failmsg)
+
+    def clear(self):
+        ret = self._lines
+        self._lines = []
+        return ret
+
+    def assert_closed(self, timeout=1):
+        self._thread.join(timeout)
+
+        if self._thread.is_alive() != False:
+            raise AssertionError("OutputCheck: Write side has not been closed yet!")
+
+    def force_close(self):
+        fd = self._pipe_fd_r
+        self._pipe_fd_r = -1
+        if fd >= 0:
+            os.close(fd)
+
+        self._thread.join()
+
+    @property
+    def fd(self):
+        return self._pipe_fd_w
+
+    def writer_attached(self):
+        os.close(self._pipe_fd_w)
+        self._pipe_fd_w = -1
+
+    def __del__(self):
+        if self._pipe_fd_r >= 0:
+            os.close(self._pipe_fd_r)
+            self._pipe_fd_r = -1
+        if self._pipe_fd_w >= 0:
+            os.close(self._pipe_fd_w)
+            self._pipe_fd_w = -1
diff --git a/unit/unittest_inspector.py b/unit/unittest_inspector.py
new file mode 100755
index 000000000000..fe8304686da7
--- /dev/null
+++ b/unit/unittest_inspector.py
@@ -0,0 +1,46 @@
+#! /usr/bin/env python3
+# Copyright © 2020, Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+# Authors:
+#       Marco Trevisan <marco.trevisan@canonical.com>
+
+import argparse
+import importlib.util
+import inspect
+import os
+import unittest
+
+def list_tests(module):
+    tests = []
+    for name, obj in inspect.getmembers(module):
+        if inspect.isclass(obj) and issubclass(obj, unittest.TestCase) and not name.startswith('OopTests'):
+            cases = unittest.defaultTestLoader.getTestCaseNames(obj)
+            tests += [ (obj, '{}.{}'.format(name, t)) for t in cases ]
+    return tests
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument('unittest_source', type=argparse.FileType('r'))
+
+    args = parser.parse_args()
+    source_path = args.unittest_source.name
+    spec = importlib.util.spec_from_file_location(
+        os.path.basename(source_path), source_path)
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+
+    for machine, human in list_tests(module):
+        print(human)
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 13/15] doc: Add HTML documentation
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (11 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 12/15] unit: Add integration tests Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 14/15] github: Add docs deployment Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 15/15] doc: Add introduction to GATT Bastien Nocera
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

Based off the XDG desktop portal documentation at:
https://github.com/flatpak/xdg-desktop-portal/commits/main/doc
---
 doc/_static/bluez.css      | 452 +++++++++++++++++++++++++++++++++++++
 doc/_static/bluez.png      | Bin 0 -> 18354 bytes
 doc/_static/bullet.svg     |   1 +
 doc/_static/card.png       | Bin 0 -> 35387 bytes
 doc/_static/figtree.woff2  | Bin 0 -> 14364 bytes
 doc/_static/toc_bullet.svg |  39 ++++
 doc/bluetoothctl-cmd.rst   |  34 +++
 doc/conf.py                |  39 ++++
 doc/copy-subdir.py         |  14 ++
 doc/daemons.rst            |  12 +
 doc/dbus-gatt.rst          |  18 ++
 doc/dbus-obex.rst          |  33 +++
 doc/dbus.rst               |  76 +++++++
 doc/deprecated-tools.rst   |  22 ++
 doc/dev-maint.rst          |  17 ++
 doc/favicon.ico            | Bin 0 -> 29152 bytes
 doc/index.rst              |  47 ++++
 doc/meson.build            | 119 ++++++++++
 doc/protocols.rst          |  22 ++
 doc/qualifications.rst     |  76 +++++++
 doc/tools.rst              |  24 ++
 meson.build                |   6 +-
 meson_options.txt          |   1 +
 23 files changed, 1051 insertions(+), 1 deletion(-)
 create mode 100644 doc/_static/bluez.css
 create mode 100644 doc/_static/bluez.png
 create mode 100644 doc/_static/bullet.svg
 create mode 100644 doc/_static/card.png
 create mode 100644 doc/_static/figtree.woff2
 create mode 100644 doc/_static/toc_bullet.svg
 create mode 100644 doc/bluetoothctl-cmd.rst
 create mode 100644 doc/conf.py
 create mode 100644 doc/copy-subdir.py
 create mode 100644 doc/daemons.rst
 create mode 100644 doc/dbus-gatt.rst
 create mode 100644 doc/dbus-obex.rst
 create mode 100644 doc/dbus.rst
 create mode 100644 doc/deprecated-tools.rst
 create mode 100644 doc/dev-maint.rst
 create mode 100644 doc/favicon.ico
 create mode 100644 doc/index.rst
 create mode 100644 doc/protocols.rst
 create mode 100644 doc/qualifications.rst
 create mode 100644 doc/tools.rst

diff --git a/doc/_static/bluez.css b/doc/_static/bluez.css
new file mode 100644
index 000000000000..e0e281d79cdc
--- /dev/null
+++ b/doc/_static/bluez.css
@@ -0,0 +1,452 @@
+/* XDG CSS theme for Sphinx + Furo
+
+https://www.sphinx-doc.org
+https://pypi.org/project/furo/
+*/
+
+/* FONTS */
+
+@font-face {
+  font-family: "Figtree var";
+  font-weight: 100 900;
+  font-display: swap;
+  font-style: normal;
+  font-named-instance: "Regular";
+  src: url("figtree.woff2") format("woff2");
+}
+
+:root {
+  --rounded-corner: 12px;
+  --blue1: rgb(153,193,241);
+  --blue2: rgb(98,160,234);
+  --blue3: rgb(53,132,228);
+  --blue4: rgb(28,113,216);
+  --blue5: rgb(26,95,180);
+  --green1: rgb(143,240,164);
+  --green2: rgb(87,227,137);
+  --green3: rgb(51,209,122);
+  --green4: rgb(46,194,126);
+  --green5: rgb(38,162,105);
+  --yellow1: rgb(249,240,107);
+  --yellow2: rgb(248,228,92);
+  --yellow3: rgb(246,211,45);
+  --yellow4: rgb(245,194,17);
+  --yellow5: rgb(229,165,10);
+  --orange1: rgb(255,190,111);
+  --orange2: rgb(255,163,72);
+  --orange3: rgb(255,120,0);
+  --orange4: rgb(230,97,0);
+  --orange5: rgb(198,70,0);
+  --red1: rgb(246,97,81);
+  --red2: rgb(237,51,59);
+  --red3: rgb(224,27,36);
+  --red4: rgb(192,28,40);
+  --red5: rgb(165,29,45);
+  --purple1: rgb(220,138,221);
+  --purple2: rgb(192,97,203);
+  --purple3: rgb(145,65,172);
+  --purple4: rgb(129,61,156);
+  --purple5: rgb(97,53,131);
+  --brown1: rgb(205,171,143);
+  --brown2: rgb(181,131,90);
+  --brown3: rgb(152,106,68);
+  --brown4: rgb(134,94,60);
+  --brown5: rgb(99,69,44);
+  --light1: rgb(255,255,255);
+  --light2: rgb(246,245,244);
+  --light3: rgb(222,221,218);
+  --light4: rgb(192,191,188);
+  --light5: rgb(154,153,150);
+  --dark1: rgb(119,118,123);
+  --dark2: rgb(94,92,100);
+  --dark3: rgb(61,56,70);
+  --dark4: rgb(36,31,49);
+  --dark5: rgb(0,0,0);
+  --text: #f6f5f4;
+  --tocbg: var(--light2);
+  --tocfg: var(--dark3);
+  --borders: var(--light3);
+  --link: var(--blue5);
+  --link-underline: var(--blue1);
+}
+
+::selection {
+	background-color:  rgba(153,193,241,0.5);
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --tocbg: var(--dark5);
+    --tocfg: var(--light5);
+    --borders: var(--dark3);
+    --link: var(--blue1);
+    --link-underline: var(--blue5);
+  }
+	::selection {
+		background-color:  rgba(26,95,180,0.4);
+	}
+}
+
+* {
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+html, body {
+  margin: 0;
+  padding: 0;
+  font-size: 16px;
+  font-family: "Figtree var", sans-serif;
+  font-weight: 400;
+  line-height: 1.6;
+}
+  @media (min-width: 700px) {
+    html, body {
+      font-size: 18px;
+    }
+  }
+
+a {
+  color: var(--link);
+  text-decoration-color: var(--link-underline);
+}
+
+p strong {
+  font-weight: 600;
+  opacity: 0.9;
+}
+
+h1 {
+   font-weight: 500;
+   font-size: 2rem;
+   margin-top: '0';
+   margin-bottom: 1.5rem;
+   font-family: "Figtree var", sans-serif;
+}
+h2 {
+   font-weight: 800;
+   font-size: 1.7rem;
+   margin-top: 2.5rem;
+   margin-bottom: 1.2rem;
+   font-family: "Figtree var", sans-serif;
+}
+h3 {
+   font-weight: 800;
+   font-size: 1.3rem;
+   margin-top: 2.2rem;
+   margin-bottom: 1rem;
+   font-family: "Figtree var", sans-serif;
+}
+h4 {
+   font-weight: 700;
+   font-size: 1.1rem;
+   margin-top: 1rem;
+   margin-bottom: 1rem;
+   font-family: "Figtree var", sans-serif;
+}
+h5 {
+   font-weight: 700;
+   font-size: 1rem;
+   margin-top: 1rem;
+   margin-bottom: 1rem;
+   font-family: "Figtree var", sans-serif;
+}
+  h2,h3,h4,h5 { opacity: 0.7; }
+
+ul.simple {
+  margin: 1rem 1rem 3rem;
+}
+
+  ul.simple li {
+    list-style-image: url(bullet.svg);
+    margin-inline-end: 1ch;
+    margin-bottom: 1rem;
+  }
+	ul.simple li:hover::marker {	}
+
+.related-pages {
+  font-size: 10pt;
+  font-weight: 800;
+  display: flex;
+  flex-direction: row-reverse;
+  align-items: stretch;
+  justify-content: space-between;
+}
+
+  .related-pages a .title {
+    color: var(--dark1);
+  }
+  .related-pages a svg {
+    width: 32px; height: 32px;
+  }
+
+  .prev-page {
+    border-radius: 8px;
+    padding: 8px 24px 8px 8px;
+
+  }
+  .next-page {
+    border-radius: 8px;
+    padding: 8px 8px 8px 24px;
+  }
+  .related-pages a.prev-page,
+  .related-pages a.next-page {
+    float: none;
+    max-width: inherit; /* cancel furo's 50% */
+  }
+  .related-pages a.prev-page:hover, .related-pages a.next-page:hover {
+    background-color: var(--tocbg);
+  }
+
+  @media (min-width: 700px) {
+    .related-pages {
+      font-size: 14pt;
+    }
+    .prev-page {
+      padding: 16px 48px 16px 16px;
+      border-radius: 24px;
+
+    }
+    .next-page {
+      padding: 16px 16px 16px 48px;
+      border-radius: 24px;
+    }
+  }
+  .related-pages a.prev-page:only-child,
+  .related-pages a.next-page:only-child,
+  .page-info {
+      /* align-self: flex-end; i wish */
+      width: 100%; /* big buttons instead */
+  }
+
+.toctree-wrapper ul {
+	margin: 0; padding: 0;
+}
+
+  .toctree-wrapper li {
+		list-style: none;
+    margin: 0 0 1rem 0;
+  }
+  .toctree-wrapper li.toctree-l1 {
+    font-size: 14pt;
+    font-weight: 600;
+
+  }
+    .toctree-wrapper li.toctree-l1 > a { color: var(--tocfg); }
+
+  .toctree-wrapper li.toctree-l2 {
+		list-style-image: url(toc_bullet.svg);
+		margin: 0;
+    font-size: 12pt;
+    font-weight: 400;
+    padding: 0.2rem;
+  }
+    .toctree-wrapper > ul ul {
+      margin-top: 1rem;
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
+      gap: .2rem;
+    }
+    .toctree-wrapper > ul ul a {
+      background-color: transparent;
+    }
+/* tiled TOC with images */
+
+ul.tiled-toc {
+  display: grid;
+  gap: 1rem;
+  grid-auto-flow: row;
+  margin: 1rem 0; padding: 0;
+  text-align: center;
+  margin-top: 2rem;
+  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+}
+  ul.tiled-toc li {
+    display: block;
+    list-style: none;
+  }
+  ul.tiled-toc li img {
+    display: block;
+    width: 100%;
+    image-rendering: crisp-edges;
+    image-rendering: pixelated;
+  }
+  ul.tiled-toc li img::after {
+    content: "";
+  }
+
+  ul.tiled-toc a {
+    color: black;
+    text-decoration: none;
+    font-weight: 600;
+  }
+
+  @media (prefers-color-scheme: dark) {
+    ul.tiled-toc a { color: white; }
+  }
+
+
+table.docutils {
+  font-size: 90%;
+  box-shadow: none;
+  border-collapse: collapse;
+  border-spacing: 0;
+  border-radius: 0;
+  text-align: left;
+  border-width: 0;
+  box-sizing: border-box;
+}
+  /* reset crazy */
+  table.docutils th, table.docutils tr, table.docutils td {
+    background-color: transparent;
+    text-align: left;
+  }
+
+  table.docutils td, table.docutils th {
+    border-width: 0;
+    padding: .25rem;
+  }
+  table.docutils tr {
+    border-width: 0;
+    border-bottom: 1px solid var(--borders);
+  }
+
+  table.docutils thead tr {
+    font-weight: 800;
+    border-width: 0;
+    box-sizing: inherit;
+    border-bottom: 2px solid var(--borders);
+  }
+
+  .hig-palette-table.align-default {
+    display: table;
+    width: 100%;
+  }
+
+  img.hig-palette-swatch {
+    display: block;
+    width: 42px;
+    aspect-ratio: 1 / 1;
+    border-radius: 50%;
+  }
+
+  .hig-palette-table colgroup { display: none; }
+  .hig-palette-table .stub,
+  .hig-palette-table .head.stub {
+    width: 50px;
+  }
+
+video {
+  width: 100%;
+  height: auto;
+}
+
+footer {
+  margin-bottom: 3rem;
+}
+
+.sidebar-drawer {
+
+}
+
+.sidebar-brand-text {
+  font-family: Cantarell, -apple-system, BlinkMacSystemFont, "Helvetica", sans-serif;
+  font-size: 14pt;
+  font-weight: 800;
+}
+
+.sidebar-search-container,
+.sidebar-search {
+	border-radius: 99999px;
+	border: none;
+}
+.sidebar-search-container {
+	margin: 0 var(--sidebar-item-spacing-horizontal);
+	transition: background-color 0.2s linear;
+	background-color: rgba(0,0,0,0.06);
+}
+.sidebar-search-container:hover {
+  background-color: rgba(0,0,0,0.09);
+}
+.sidebar-search-container:focus-within {
+  background-color: rgba(0,0,0,0.12);
+}
+
+@media (prefers-color-scheme: dark) {
+  .sidebar-search-container {
+    background-color: rgba(255,255,255,0.06);
+  }
+  .sidebar-search-container:hover {
+    background-color: rgba(255,255,255,0.09);
+  }
+  .sidebar-search-container:focus-within {
+    background-color: rgba(255,255,255,0.12);
+  }
+}
+
+.sidebar-tree .toctree-l1,
+.sidebar-tree .toctree-l2 {
+	margin: 1px 12px;
+	border-radius: 6px;
+  color: var(--link);
+}
+
+.sidebar-tree .toctree-l1 > .reference {
+  color: var(--link);
+}
+
+.sidebar-tree .toctree-l2 {
+	margin: 1px 0;
+	font-size: 90%;
+	text-wrap: balance;
+	overflow-wrap: break-word;
+	hyphens: manual;
+}
+
+.sidebar-tree li > ul {
+	margin-left: 0;
+}
+.sidebar-tree li > ul .reference {
+	padding-left: calc(1.5*var(--sidebar-item-spacing-horizontal));
+}
+
+.sidebar-tree label[for^="toctree-checkbox"] {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+
+.sidebar-tree li.toctree-l3 a {
+  margin: 1px 0;
+  padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)
+           var(--sidebar-item-spacing-vertical) calc(2*var(--sidebar-item-spacing-horizontal)) ; /* extra indentation */
+}
+
+.sidebar-tree .reference {
+	background: rgba(0,0,0,0);
+	transition: background 0.2s linear;
+	border-radius: 6px;
+	padding: var(--sidebar-item-spacing-vertical) calc(0.75*var(--sidebar-item-spacing-horizontal));
+}
+.sidebar-tree .reference:hover,
+.sidebar-tree .current > .reference:hover {
+	/* background: var(--color-background-hover); */
+	background: rgba(0,0,0,0.03);
+}
+
+.sidebar-tree .reference.current {
+	background: rgba(0,0,0,0.06);
+}
+.sidebar-tree .reference.current:hover {
+	background: rgba(0,0,0,0.09);
+}
+
+.sidebar-tree .current-page > .reference {
+	font-weight: 600;
+}
+
+.theme-toggle {
+  display: none; /* just go with auto */
+}
diff --git a/doc/_static/bluez.png b/doc/_static/bluez.png
new file mode 100644
index 0000000000000000000000000000000000000000..105aac0e1d857e0ced2b931a54153ff1eb8a285e
GIT binary patch
literal 18354
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
QL_|a!?Om@jpaH4>2jhNBdjJ3c

literal 0
HcmV?d00001

diff --git a/doc/_static/bullet.svg b/doc/_static/bullet.svg
new file mode 100644
index 000000000000..e75341131991
--- /dev/null
+++ b/doc/_static/bullet.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M6.662 4.586a1.143 1.143 0 00-.838.334l-.531.531c-.458.458-.442 1.21.037 1.69l2.873 2.87-2.873 2.874c-.479.479-.495 1.231-.037 1.69l.531.53c.458.458 1.213.442 1.692-.037l4.068-4.07a1.22 1.22 0 00.348-.986 1.22 1.22 0 00-.348-.985l-4.068-4.07a1.249 1.249 0 00-.854-.371z" fill="#9a9996"/></svg>
\ No newline at end of file
diff --git a/doc/_static/card.png b/doc/_static/card.png
new file mode 100644
index 0000000000000000000000000000000000000000..60383d7e792ae320f226fc5540dd10a232e92243
GIT binary patch
literal 35387
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

literal 0
HcmV?d00001

diff --git a/doc/_static/figtree.woff2 b/doc/_static/figtree.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..857341f7c678b1c5d85729ff9ea8d486fe2b7a04
GIT binary patch
literal 14364
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
XnJx~kuFKepMr;1_w*ej>008_K_<*wS

literal 0
HcmV?d00001

diff --git a/doc/_static/toc_bullet.svg b/doc/_static/toc_bullet.svg
new file mode 100644
index 000000000000..d4a77331c98f
--- /dev/null
+++ b/doc/_static/toc_bullet.svg
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="toc_bullet.svg"
+   inkscape:version="1.1-beta1 (77e7b44db3, 2021-03-28)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     id="namedview6"
+     pagecolor="#505050"
+     bordercolor="#ffffff"
+     borderopacity="1"
+     objecttolerance="10.0"
+     gridtolerance="10.0"
+     guidetolerance="10.0"
+     inkscape:pageshadow="0"
+     inkscape:pageopacity="0"
+     inkscape:pagecheckerboard="1"
+     showgrid="true"
+     inkscape:current-layer="svg4">
+    <inkscape:grid
+       type="xygrid"
+       id="grid49" />
+  </sodipodi:namedview>
+  <path
+     id="path2"
+     sodipodi:type="inkscape:offset"
+     inkscape:radius="-0.81220347"
+     inkscape:original="M 6.6621094 4.5859375 A 1.143 1.143 0 0 0 5.8242188 4.9199219 L 5.2929688 5.4511719 C 4.8349687 5.9091719 4.8510781 6.660625 5.3300781 7.140625 L 8.203125 10.011719 L 5.3300781 12.884766 C 4.8510781 13.363766 4.8349688 14.115219 5.2929688 14.574219 L 5.8242188 15.105469 C 6.2822188 15.563469 7.036625 15.547359 7.515625 15.068359 L 11.583984 10.998047 A 1.22 1.22 0 0 0 11.931641 10.011719 A 1.22 1.22 0 0 0 11.583984 9.0273438 L 7.515625 4.9570312 A 1.249 1.249 0 0 0 6.6621094 4.5859375 z "
+     d="m 6.6386719,5.3984375 c -0.088798,-0.00181 -0.1773948,0.032925 -0.2402344,0.095703 l -0.53125,0.53125 C 5.7300188,6.1625593 5.7182537,6.3799747 5.9042969,6.5664062 L 8.7773438,9.4375 a 0.81228469,0.81228469 0 0 1 0,1.148438 L 5.9042969,13.458984 C 5.719186,13.644095 5.7289844,13.861495 5.8671875,14 l 0.53125,0.53125 c 0.135934,0.135934 0.3590378,0.146822 0.5429687,-0.03711 l 4.0664058,-4.06836 0.002,-0.002 c 0.08366,-0.0854 0.125702,-0.209206 0.113281,-0.328125 a 0.81228469,0.81228469 0 0 1 0,-0.1679686 c 0.01222,-0.1187076 -0.0298,-0.2409206 -0.113281,-0.3261719 l -0.002,-0.00195 L 6.9414062,5.53125 A 0.81228469,0.81228469 0 0 1 6.9375,5.5273438 C 6.8598316,5.4486568 6.7511214,5.4020564 6.640625,5.3984375 Z"
+     style="fill:#deddda" />
+</svg>
diff --git a/doc/bluetoothctl-cmd.rst b/doc/bluetoothctl-cmd.rst
new file mode 100644
index 000000000000..3fa9b4c984e7
--- /dev/null
+++ b/doc/bluetoothctl-cmd.rst
@@ -0,0 +1,34 @@
+bluetoothctl
+============
+
+.. toctree::
+   :hidden:
+   :maxdepth: 2
+
+   bluetoothctl
+   bluetoothctl-mgmt
+   bluetoothctl-monitor
+   bluetoothctl-admin
+   bluetoothctl-advertise
+   bluetoothctl-endpoint
+   bluetoothctl-gatt
+   bluetoothctl-player
+   bluetoothctl-scan
+   bluetoothctl-transport
+   bluetoothctl-assistant
+   bluetoothctl-hci
+   bluetoothctl-telephony
+
+* :doc:`bluetoothctl <bluetoothctl>`
+* :doc:`bluetoothctl-mgmt <bluetoothctl-mgmt>`
+* :doc:`bluetoothctl-monitor <bluetoothctl-monitor>`
+* :doc:`bluetoothctl-admin <bluetoothctl-admin>`
+* :doc:`bluetoothctl-advertise <bluetoothctl-advertise>`
+* :doc:`bluetoothctl-endpoint <bluetoothctl-endpoint>`
+* :doc:`bluetoothctl-gatt <bluetoothctl-gatt>`
+* :doc:`bluetoothctl-player <bluetoothctl-player>`
+* :doc:`bluetoothctl-scan <bluetoothctl-scan>`
+* :doc:`bluetoothctl-transport <bluetoothctl-transport>`
+* :doc:`bluetoothctl-assistant <bluetoothctl-assistant>`
+* :doc:`bluetoothctl-hci <bluetoothctl-hci>`
+* :doc:`bluetoothctl-telephony <bluetoothctl-telephony>`
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 000000000000..07f87510be6f
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,39 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = "BlueZ"
+copyright = "2026, BlueZ authors"
+author = "BlueZ authors"
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+html_favicon = "favicon.ico"
+
+extensions = [
+    "sphinxext.opengraph",
+    "sphinx_copybutton",
+]
+
+templates_path = ["_templates"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = "furo"
+# add custom files that are stored in _static
+html_css_files = ["bluez.css"]
+html_static_path = ["_static"]
+
+# -- Options for OpenGraph ---------------------------------------------------
+
+ogp_site_url = "https://bluez.github.io/docs/"
+ogp_image = "_static/card.png"
+
+html_permalinks_icon = "🔗"
diff --git a/doc/copy-subdir.py b/doc/copy-subdir.py
new file mode 100644
index 000000000000..8cb64f54bf9b
--- /dev/null
+++ b/doc/copy-subdir.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import shutil
+
+subdir = os.environ["MESON_SUBDIR"]
+
+input_path = os.path.join(os.environ["MESON_SOURCE_ROOT"], subdir, sys.argv[1])
+output_path = os.path.join(os.environ["MESON_BUILD_ROOT"], subdir, sys.argv[2])
+
+os.makedirs(os.path.dirname(output_path), exist_ok=True)
+
+shutil.copyfile(input_path, output_path)
diff --git a/doc/daemons.rst b/doc/daemons.rst
new file mode 100644
index 000000000000..7c5f61f74c79
--- /dev/null
+++ b/doc/daemons.rst
@@ -0,0 +1,12 @@
+Daemons
+=======
+
+.. toctree::
+   :hidden:
+   :maxdepth: 2
+
+   bluetoothd
+   bluetooth-meshd
+
+* :doc:`bluetoothd <bluetoothd>`
+* :doc:`bluetooth-meshd <bluetooth-meshd>`
diff --git a/doc/dbus-gatt.rst b/doc/dbus-gatt.rst
new file mode 100644
index 000000000000..fb026e9f9ede
--- /dev/null
+++ b/doc/dbus-gatt.rst
@@ -0,0 +1,18 @@
+D-Bus GATT APIs
+===============
+
+.. toctree::
+   :hidden:
+   :maxdepth: 2
+
+   org.bluez.GattManager
+   org.bluez.GattProfile
+   org.bluez.GattService
+   org.bluez.GattCharacteristic
+   org.bluez.GattDescriptor
+
+* :doc:`org.bluez.GattManager <org.bluez.GattManager>`
+* :doc:`org.bluez.GattProfile <org.bluez.GattProfile>`
+* :doc:`org.bluez.GattService <org.bluez.GattService>`
+* :doc:`org.bluez.GattCharacteristic <org.bluez.GattCharacteristic>`
+* :doc:`org.bluez.GattDescriptor <org.bluez.GattDescriptor>`
diff --git a/doc/dbus-obex.rst b/doc/dbus-obex.rst
new file mode 100644
index 000000000000..b3d0e5c90ada
--- /dev/null
+++ b/doc/dbus-obex.rst
@@ -0,0 +1,33 @@
+OBEX D-Bus APIs
+===============
+
+.. toctree::
+   :hidden:
+   :maxdepth: 2
+
+   org.bluez.obex.Agent
+   org.bluez.obex.Image
+   org.bluez.obex.Client
+   org.bluez.obex.Session
+   org.bluez.obex.Transfer
+   org.bluez.obex.ObjectPush
+   org.bluez.obex.FileTransfer
+   org.bluez.obex.Synchronization
+   org.bluez.obex.PhonebookAccess
+   org.bluez.obex.MessageAccess
+   org.bluez.obex.Message
+   org.bluez.obex.AgentManager
+
+* :doc:`org.bluez.obex.Client </org.bluez.obex.Client>`
+* :doc:`org.bluez.obex.Session <org.bluez.obex.Session>`
+* :doc:`org.bluez.obex.Transfer <org.bluez.obex.Transfer>`
+* :doc:`org.bluez.obex.ObjectPush <org.bluez.obex.ObjectPush>`
+* :doc:`org.bluez.obex.FileTransfer <org.bluez.obex.FileTransfer>`
+* :doc:`org.bluez.obex.Synchronization <org.bluez.obex.Synchronization>`
+* :doc:`org.bluez.obex.PhonebookAccess <org.bluez.obex.PhonebookAccess>`
+* :doc:`org.bluez.obex.MessageAccess <org.bluez.obex.MessageAccess>`
+* :doc:`org.bluez.obex.Message <org.bluez.obex.Message>`
+* :doc:`org.bluez.obex.AgentManager <org.bluez.obex.AgentManager>`
+* :doc:`org.bluez.obex.Agent <org.bluez.obex.Agent>`
+* :doc:`org.bluez.obex.Image <org.bluez.obex.Image>`
+
diff --git a/doc/dbus.rst b/doc/dbus.rst
new file mode 100644
index 000000000000..3bed4711f56d
--- /dev/null
+++ b/doc/dbus.rst
@@ -0,0 +1,76 @@
+D-Bus APIs
+==========
+
+.. toctree::
+   :hidden:
+   :maxdepth: 2
+
+   org.bluez.Adapter
+   org.bluez.AdminPolicySet
+   org.bluez.AdminPolicyStatus
+   org.bluez.Agent
+   org.bluez.AgentManager
+   org.bluez.Battery
+   org.bluez.BatteryProvider
+   org.bluez.BatteryProviderManager
+   org.bluez.Bearer.BREDR
+   org.bluez.Bearer.LE
+   org.bluez.LEAdvertisingManager
+   org.bluez.LEAdvertisement
+   org.bluez.AdvertisementMonitorManager
+   org.bluez.AdvertisementMonitor
+   org.bluez.Device
+   org.bluez.DeviceSet
+   org.bluez.Input
+   org.bluez.Media
+   org.bluez.MediaAssistant
+   org.bluez.MediaControl
+   org.bluez.MediaEndpoint
+   org.bluez.MediaItem
+   org.bluez.MediaTransport
+   org.bluez.MediaFolder
+   org.bluez.MediaPlayer
+   org.bluez.Network
+   org.bluez.NetworkServer
+   org.bluez.Profile
+   org.bluez.ProfileManager
+   org.bluez.Telephony
+   org.bluez.Call
+   org.bluez.ThermometerManager
+   org.bluez.Thermometer
+   org.bluez.ThermometerWatcher
+
+* :doc:`org.bluez.Adapter <org.bluez.Adapter>`
+* :doc:`org.bluez.AdminPolicySet <org.bluez.AdminPolicySet>`
+* :doc:`org.bluez.AdminPolicyStatus <org.bluez.AdminPolicyStatus>`
+* :doc:`org.bluez.Agent <org.bluez.Agent>`
+* :doc:`org.bluez.AgentManager <org.bluez.AgentManager>`
+* :doc:`org.bluez.Battery <org.bluez.Battery>`
+* :doc:`org.bluez.BatteryProvider <org.bluez.BatteryProvider>`
+* :doc:`org.bluez.BatteryProviderManager <org.bluez.BatteryProviderManager>`
+* :doc:`org.bluez.Bearer.BREDR <org.bluez.Bearer.BREDR>`
+* :doc:`org.bluez.Bearer.LE <org.bluez.Bearer.LE>`
+* :doc:`org.bluez.LEAdvertisingManager <org.bluez.LEAdvertisingManager>`
+* :doc:`org.bluez.LEAdvertisement <org.bluez.LEAdvertisement>`
+* :doc:`org.bluez.AdvertisementMonitorManager <org.bluez.AdvertisementMonitorManager>`
+* :doc:`org.bluez.AdvertisementMonitor <org.bluez.AdvertisementMonitor>`
+* :doc:`org.bluez.Device <org.bluez.Device>`
+* :doc:`org.bluez.DeviceSet <org.bluez.DeviceSet>`
+* :doc:`org.bluez.Input <org.bluez.Input>`
+* :doc:`org.bluez.Media <org.bluez.Media>`
+* :doc:`org.bluez.MediaAssistant <org.bluez.MediaAssistant>`
+* :doc:`org.bluez.MediaControl <org.bluez.MediaControl>`
+* :doc:`org.bluez.MediaEndpoint <org.bluez.MediaEndpoint>`
+* :doc:`org.bluez.MediaItem <org.bluez.MediaItem>`
+* :doc:`org.bluez.MediaTransport <org.bluez.MediaTransport>`
+* :doc:`org.bluez.MediaFolder <org.bluez.MediaFolder>`
+* :doc:`org.bluez.MediaPlayer <org.bluez.MediaPlayer>`
+* :doc:`org.bluez.Network <org.bluez.Network>`
+* :doc:`org.bluez.NetworkServer <org.bluez.NetworkServer>`
+* :doc:`org.bluez.Profile <org.bluez.Profile>`
+* :doc:`org.bluez.ProfileManager <org.bluez.ProfileManager>`
+* :doc:`org.bluez.Telephony <org.bluez.Telephony>`
+* :doc:`org.bluez.Call <org.bluez.Call>`
+* :doc:`org.bluez.ThermometerManager <org.bluez.ThermometerManager>`
+* :doc:`org.bluez.Thermometer <org.bluez.Thermometer>`
+* :doc:`org.bluez.ThermometerWatcher <org.bluez.ThermometerWatcher>`
diff --git a/doc/deprecated-tools.rst b/doc/deprecated-tools.rst
new file mode 100644
index 000000000000..0c86f5265751
--- /dev/null
+++ b/doc/deprecated-tools.rst
@@ -0,0 +1,22 @@
+Deprecated Tools
+================
+
+.. toctree::
+   :hidden:
+   :maxdepth: 2
+
+   hciattach
+   hciconfig
+   hcitool
+   hcidump
+   rfcomm
+   sdptool
+   ciptool
+
+* :doc:`hciattach <hciattach>`
+* :doc:`hciconfig <hciconfig>`
+* :doc:`hcitool <hcitool>`
+* :doc:`hcidump <hcidump>`
+* :doc:`rfcomm <rfcomm>`
+* :doc:`sdptool <sdptool>`
+* :doc:`ciptool <ciptool>`
diff --git a/doc/dev-maint.rst b/doc/dev-maint.rst
new file mode 100644
index 000000000000..d92e892ba352
--- /dev/null
+++ b/doc/dev-maint.rst
@@ -0,0 +1,17 @@
+Development and Maintenance
+===========================
+
+
+.. toctree::
+   :hidden:
+
+   test-runner
+   security-bugs
+   coding-style
+   maintainer-guidelines
+
+* :doc:`Test kernel changes to the Bluetooth subsystem <test-runner>`
+* :doc:`Security bugs handling <security-bugs>`
+* :doc:`Coding style <coding-style>`
+* :doc:`Maintainers guidelines <maintainer-guidelines>`
+
diff --git a/doc/favicon.ico b/doc/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..1e136f240e08be84dc0e214daf84715a98bcb307
GIT binary patch
literal 29152
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
Pmn=m&HQ550*YExp%bT+2

literal 0
HcmV?d00001

diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 000000000000..f7b35ee0925a
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,47 @@
+
+.. image:: _static/bluez.png
+
+BlueZ
+=====
+
+BlueZ overview lives here.
+
+Here's a link to the website `BlueZ <http://www.bluez.org>`_.
+
+* :doc:`Development and Maintenance <dev-maint>`
+* :doc:`Low-level Protocol and Management APIs <protocols>`
+
+Man pages
+---------
+
+* :doc:`Daemons <daemons>`
+* :doc:`bluetoothctl <bluetoothctl-cmd>`
+* :doc:`Tools <tools>`
+* :doc:`Deprecated tools <deprecated-tools>`
+
+D-Bus APIs
+----------
+
+* :doc:`D-Bus APIs <dbus>`
+* :doc:`D-Bus GATT APIs <dbus>`
+* :doc:`OBEX D-Bus APIs <dbus-obex>`
+
+Qualifications
+--------------
+
+* :doc:`Qualifications <qualifications>`
+
+.. toctree::
+   :maxdepth: 2
+   :hidden:
+
+   dev-maint
+   protocols
+   daemons
+   bluetoothctl-cmd
+   tools
+   deprecated-tools
+   dbus
+   dbus-gatt
+   dbus-obex
+   qualifications
diff --git a/doc/meson.build b/doc/meson.build
index 498b6a479067..722f1407f51b 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -42,7 +42,11 @@ man_pages = [
   'org.bluez.AdvertisementMonitor.5',
   'org.bluez.Telephony.5',
   'org.bluez.Call.5',
+  'org.bluez.ThermometerManager.5',
+  'org.bluez.Thermometer.5',
+  'org.bluez.ThermometerWatcher.5',
 ]
+doc_man_pages = man_pages
 
 obex_man_pages = [
   'org.bluez.obex.Client.5',
@@ -58,12 +62,14 @@ obex_man_pages = [
   'org.bluez.obex.Agent.5',
   'org.bluez.obex.Image.5',
 ]
+doc_man_pages += obex_man_pages
 
 if get_option('obex').enabled()
   man_pages += obex_man_pages
 endif
 
 monitor_man_pages = [ 'btmon.1' ]
+doc_man_pages += monitor_man_pages
 
 if get_option('monitor').allowed()
   man_pages += monitor_man_pages
@@ -76,8 +82,10 @@ tools_man_pages = [
   'isotest.1',
   'bdaddr.1',
 ]
+doc_man_pages += tools_man_pages
 
 noinst_tools_man_pages = [ 'btmgmt.1' ]
+doc_man_pages += noinst_tools_man_pages
 
 if get_option('tools').allowed()
   man_pages += tools_man_pages
@@ -92,12 +100,14 @@ deprecated_tools_man_pages = [
   'sdptool.1',
   'ciptool.1',
 ]
+doc_man_pages += deprecated_tools_man_pages
 
 if get_option('deprecated').allowed()
   man_pages += deprecated_tools_man_pages
 endif
 
 hid2hci_man_pages = [ 'hid2hci.1' ]
+doc_man_pages += hid2hci_man_pages
 
 if get_option('hid2hci').enabled()
   man_pages += 'hid2hci.1'
@@ -118,6 +128,7 @@ client_man_pages = [
   'bluetoothctl-hci.1',
   'bluetoothctl-telephony.1'
 ]
+doc_man_pages += client_man_pages
 
 if get_option('client').enabled()
   man_pages += client_man_pages
@@ -141,6 +152,43 @@ if rst2man.found()
   endforeach
 endif
 
+qualifications_docs = [
+  'qualification/a2dp-pics.rst',
+  'qualification/a2dp-pts.rst',
+  'qualification/avctp-pics.rst',
+  'qualification/avctp-pts.rst',
+  'qualification/avdtp-pics.rst',
+  'qualification/avdtp-pts.rst',
+  'qualification/avrcp-pics.rst',
+  'qualification/avrcp-pts.rst',
+  'qualification/gap-pics.rst',
+  'qualification/gap-pts.rst',
+  'qualification/gatt-pics.rst',
+  'qualification/gatt-pts.rst',
+  'qualification/gavdp-pics.rst',
+  'qualification/gavdp-pts.rst',
+  'qualification/hid-pics.rst',
+  'qualification/hid-pts.rst',
+  'qualification/hogp-pics.rst',
+  'qualification/hogp-pts.rst',
+  'qualification/hsp-pics.rst',
+  'qualification/hsp-pts.rst',
+  'qualification/iopt-pics.rst',
+  'qualification/iopt-pts.rst',
+  'qualification/l2cap-pics.rst',
+  'qualification/l2cap-pts.rst',
+  'qualification/rfcomm-pics.rst',
+  'qualification/rfcomm-pts.rst',
+  'qualification/scpp-pics.rst',
+  'qualification/scpp-pts.rst',
+  'qualification/sdp-pics.rst',
+  'qualification/sdp-pts.rst',
+  'qualification/sm-pics.rst',
+  'qualification/sm-pts.rst',
+  'qualification/spp-pics.rst',
+  'qualification/spp-pts.rst',
+]
+
 bluetoothd_rst = configure_file(
   input: 'bluetoothd.rst.in',
   output: 'bluetoothd.rst',
@@ -171,3 +219,74 @@ if get_option('mesh').enabled() and rst2man.found()
     install_dir: get_option('mandir') / 'man8'
   )
 endif
+
+if get_option('documentation').enabled()
+  docs_sources = [
+    'conf.py',
+    'favicon.ico',
+
+    'index.rst',
+    'dev-maint.rst',
+    'test-runner.rst',
+    'assigned-numbers.rst',
+    'security-bugs.rst',
+    'coding-style.rst',
+    'maintainer-guidelines.rst',
+    'protocols.rst',
+    'daemons.rst',
+    'bluetoothctl-cmd.rst',
+    'tools.rst',
+    'deprecated-tools.rst',
+    'dbus.rst',
+    'dbus-gatt.rst',
+    'dbus-obex.rst',
+    'qualifications.rst',
+  ]
+  docs_sources += qualifications_docs
+
+  foreach man_page: doc_man_pages
+    man_page_name = man_page.substring(0, -2)
+    man_section = man_page.substring(-1)
+    if man_section.to_int() == 7
+      man_page_name = man_page_name + '-protocol'
+    endif
+    docs_sources += man_page_name + '.rst'
+  endforeach
+
+  copied_docs_sources = []
+  foreach d: docs_sources
+    copied_docs_sources += configure_file(input: d, output: fs.name(d), copy: true)
+  endforeach
+  copied_docs_sources += [ bluetooth_meshd_rst, bluetoothd_rst ]
+
+  docs_inputs = copied_docs_sources
+
+  # Copy static files to the appropriate output subdir
+  copy = find_program('copy-subdir.py')
+
+  static_files = [
+    'bullet.svg',
+    'card.png',
+    'figtree.woff2',
+    'toc_bullet.svg',
+    'bluez.css',
+    'bluez.png',
+  ]
+
+  foreach f: static_files
+    run_command(copy, '_static' / f, '_static' / f, check: true)
+  endforeach
+
+  sphinx_command = [ sphinx_build ]
+
+  if get_option('werror')
+    sphinx_command += [ '-W', '--keep-going' ]
+  endif
+
+  custom_target('docs',
+    input: docs_inputs,
+    output: 'html',
+    command: sphinx_command + [ meson.current_build_dir(), '@OUTPUT@' ],
+    build_by_default: true,
+  )
+endif
diff --git a/doc/protocols.rst b/doc/protocols.rst
new file mode 100644
index 000000000000..1b61a72dd605
--- /dev/null
+++ b/doc/protocols.rst
@@ -0,0 +1,22 @@
+Protocol and Management References
+==================================
+
+.. toctree::
+   :hidden:
+
+   hci-protocol
+   mgmt-protocol
+   l2cap-protocol
+   rfcomm-protocol
+   assigned-numbers
+   sco-protocol
+   iso-protocol
+   btsnoop-protocol
+
+* :doc:`HCI protocol <hci-protocol>`
+* :doc:`MGMT protocol <mgmt-protocol>`
+* :doc:`L2CAP protocol <l2cap-protocol>`
+* :doc:`RFCOMM protocol <rfcomm-protocol>` and :doc:`channel numbers <assigned-numbers>`
+* :doc:`SCO transport <sco-protocol>`
+* :doc:`ISO transport <iso-protocol>`
+* :doc:`BTSnoop/Monitor protocol <btsnoop-protocol>`
diff --git a/doc/qualifications.rst b/doc/qualifications.rst
new file mode 100644
index 000000000000..d7eb6b803f01
--- /dev/null
+++ b/doc/qualifications.rst
@@ -0,0 +1,76 @@
+Qualifications
+--------------
+
+.. toctree::
+   :hidden:
+   :maxdepth: 2
+
+   a2dp-pics
+   a2dp-pts
+   avctp-pics
+   avctp-pts
+   avdtp-pics
+   avdtp-pts
+   avrcp-pics
+   avrcp-pts
+   gap-pics
+   gap-pts
+   gatt-pics
+   gatt-pts
+   gavdp-pics
+   gavdp-pts
+   hid-pics
+   hid-pts
+   hogp-pics
+   hogp-pts
+   hsp-pics
+   hsp-pts
+   iopt-pics
+   iopt-pts
+   l2cap-pics
+   l2cap-pts
+   rfcomm-pics
+   rfcomm-pts
+   scpp-pics
+   scpp-pts
+   sdp-pics
+   sdp-pts
+   sm-pics
+   sm-pts
+   spp-pics
+   spp-pts
+
+* :doc:`a2dp-pics <a2dp-pics>`
+* :doc:`a2dp-pts <a2dp-pts>`
+* :doc:`avctp-pics <avctp-pics>`
+* :doc:`avctp-pts <avctp-pts>`
+* :doc:`avdtp-pics <avdtp-pics>`
+* :doc:`avdtp-pts <avdtp-pts>`
+* :doc:`avrcp-pics <avrcp-pics>`
+* :doc:`avrcp-pts <avrcp-pts>`
+* :doc:`gap-pics <gap-pics>`
+* :doc:`gap-pts <gap-pts>`
+* :doc:`gatt-pics <gatt-pics>`
+* :doc:`gatt-pts <gatt-pts>`
+* :doc:`gavdp-pics <gavdp-pics>`
+* :doc:`gavdp-pts <gavdp-pts>`
+* :doc:`hid-pics <hid-pics>`
+* :doc:`hid-pts <hid-pts>`
+* :doc:`hogp-pics <hogp-pics>`
+* :doc:`hogp-pts <hogp-pts>`
+* :doc:`hsp-pics <hsp-pics>`
+* :doc:`hsp-pts <hsp-pts>`
+* :doc:`iopt-pics <iopt-pics>`
+* :doc:`iopt-pts <iopt-pts>`
+* :doc:`l2cap-pics <l2cap-pics>`
+* :doc:`l2cap-pts <l2cap-pts>`
+* :doc:`rfcomm-pics <rfcomm-pics>`
+* :doc:`rfcomm-pts <rfcomm-pts>`
+* :doc:`scpp-pics <scpp-pics>`
+* :doc:`scpp-pts <scpp-pts>`
+* :doc:`sdp-pics <sdp-pics>`
+* :doc:`sdp-pts <sdp-pts>`
+* :doc:`sm-pics <sm-pics>`
+* :doc:`sm-pts <sm-pts>`
+* :doc:`spp-pics <spp-pics>`
+* :doc:`spp-pts <spp-pts>`
diff --git a/doc/tools.rst b/doc/tools.rst
new file mode 100644
index 000000000000..6da8ca58f6fc
--- /dev/null
+++ b/doc/tools.rst
@@ -0,0 +1,24 @@
+Tools
+=====
+
+.. toctree::
+   :hidden:
+   :maxdepth: 2
+
+   btmon
+   rctest
+   l2ping
+   btattach
+   isotest
+   btmgmt
+   hid2hci
+   bdaddr
+
+* :doc:`btmon <btmon>`
+* :doc:`rctest <rctest>`
+* :doc:`l2ping <l2ping>`
+* :doc:`btattach <btattach>`
+* :doc:`isotest <isotest>`
+* :doc:`btmgmt <btmgmt>`
+* :doc:`hid2hci <hid2hci>`
+* :doc:`bdaddr <bdaddr>`
diff --git a/meson.build b/meson.build
index 91042e437438..c7971430d340 100644
--- a/meson.build
+++ b/meson.build
@@ -159,6 +159,10 @@ ell_mesh_dep = dependency('ell',
   required: get_option('mesh')
 )
 
+pymod = import('python')
+sphinx_build = find_program('sphinx-build', native: true, required: get_option('documentation'))
+sphinx_modules = pymod.find_installation(modules: ['sphinxext.opengraph', 'sphinx_copybutton', 'furo'], required: get_option('documentation'))
+
 rst2man = find_program('rst2man', 'rst2man.py', required: get_option('manpages'))
 if rst2man.found()
   rst2man_command = [
@@ -294,7 +298,7 @@ endif
 if get_option('mesh').enabled()
   subdir('mesh')
 endif
-if rst2man.found()
+if get_option('manpages').enabled() or get_option('documentation').enabled()
   subdir('doc')
 endif
 sh = find_program('sh', required: true)
diff --git a/meson_options.txt b/meson_options.txt
index 79e75e33ddaf..d097a7c91fcf 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -12,6 +12,7 @@ option('logind', type: 'feature', value: 'enabled', description: 'Enable logind
 option('udev', type: 'feature', value: 'enabled', description: 'Enable udev device support in tools')
 option('datafiles', type: 'feature', value: 'enabled', description: 'Install configuration and data files')
 option('manpages', type: 'feature', value: 'auto', description: 'Enable building of manual pages')
+option('documentation', type: 'feature', value: 'auto', description: 'Enable building of online documentation')
 option('experimental', type: 'feature', value: 'disabled', description: 'Enable experimental tools')
 option('deprecated', type: 'feature', value: 'disabled', description: 'Enable deprecated tools')
 option('external-plugins', type: 'feature', value: 'enabled', description: 'Enable support for external plugins')
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 14/15] github: Add docs deployment
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (12 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 13/15] doc: Add HTML documentation Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  2026-04-24  9:11 ` [PATCH BlueZ v9 15/15] doc: Add introduction to GATT Bastien Nocera
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth

---
 .github/workflows/docs.yml | 54 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 54 insertions(+)
 create mode 100644 .github/workflows/docs.yml

diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 000000000000..07169d0bc5c9
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,54 @@
+name: Deploy Docs
+
+on:
+  push:
+    branches:
+      - master
+  workflow_dispatch:
+
+permissions:
+  contents: read
+  pages: write
+  id-token: write
+
+concurrency:
+  group: pages
+  cancel-in-progress: true
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+
+      - name: Install deps
+        run: |
+          sudo apt-get update
+          sudo apt install -y libglib2.0-dev libdbus-1-dev meson python3-docutils furo python3-sphinx python3-sphinx-copybutton python3-sphinxext-opengraph
+
+      - name: Build site
+        run: |
+          meson setup --prefix /tmp/bluez-install --libdir lib -Dudev=disabled -Dauto_features=disabled -Dtools=disabled -Dclient=disabled -Ddaemon=disabled -Dmonitor=disabled -Ddeprecated=disabled -Dexperimental=disabled -Ddocumentation=enabled -Dlocalstatedir=/var _build
+          meson compile -C _build
+
+      - name: Upload Pages artifact
+        uses: actions/upload-pages-artifact@v3
+        with:
+          path: _build/doc/html/
+
+  deploy:
+    runs-on: ubuntu-latest
+    needs: build
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+    steps:
+      - name: Deploy to GitHub Pages
+        id: deployment
+        uses: actions/deploy-pages@v4
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH BlueZ v9 15/15] doc: Add introduction to GATT
  2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
                   ` (13 preceding siblings ...)
  2026-04-24  9:11 ` [PATCH BlueZ v9 14/15] github: Add docs deployment Bastien Nocera
@ 2026-04-24  9:11 ` Bastien Nocera
  14 siblings, 0 replies; 16+ messages in thread
From: Bastien Nocera @ 2026-04-24  9:11 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Paul Otto

From: Paul Otto <potto@ieee.org>

As originally written for the bluetoothctl gatt submenu at:
https://github.com/potto216/bluez/blob/master/client/bluetoothctl-gatt.rst

Closes: https://github.com/bluez/bluez/issues/747
---
 doc/dbus-gatt.rst  |   2 +
 doc/gatt-intro.rst | 141 +++++++++++++++++++++++++++++++++++++++++++++
 doc/meson.build    |   1 +
 3 files changed, 144 insertions(+)
 create mode 100644 doc/gatt-intro.rst

diff --git a/doc/dbus-gatt.rst b/doc/dbus-gatt.rst
index fb026e9f9ede..264fb12bae11 100644
--- a/doc/dbus-gatt.rst
+++ b/doc/dbus-gatt.rst
@@ -5,12 +5,14 @@ D-Bus GATT APIs
    :hidden:
    :maxdepth: 2
 
+   gatt-intro
    org.bluez.GattManager
    org.bluez.GattProfile
    org.bluez.GattService
    org.bluez.GattCharacteristic
    org.bluez.GattDescriptor
 
+* :doc:`Intro to GATT (Generic Attribute Profile) <gatt-intro>`
 * :doc:`org.bluez.GattManager <org.bluez.GattManager>`
 * :doc:`org.bluez.GattProfile <org.bluez.GattProfile>`
 * :doc:`org.bluez.GattService <org.bluez.GattService>`
diff --git a/doc/gatt-intro.rst b/doc/gatt-intro.rst
new file mode 100644
index 000000000000..271f9bac08ce
--- /dev/null
+++ b/doc/gatt-intro.rst
@@ -0,0 +1,141 @@
+=========================================
+Intro to GATT (Generic Attribute Profile)
+=========================================
+
+The Generic Attribute Profile (GATT) defines how two connected Bluetooth
+LE devices can share data using concepts called Services and Characteristics.
+Data is orgnaized into a hierarchical structure of profiles, services, and
+characteristics in the GATT database. The device containing the GATT database
+is known as the server device and the device accessing the GATT database is
+the client device. Only the database's services and characteristics are
+documented here.
+
+BlueZ provides D-Bus interfaces for accessing GATT services and profiles which
+are documented in the :doc:`D-Bus GATT APIs <dbus>` document.
+
+One-off usage and prototyping can also be done using the bluetoothctl
+command-line utility.
+
+UUIDs
+-----
+
+Services and characteristics are differentiated by Universally Unique
+Identifiers (UUIDs). For GATT there are several formats of UUIDs: 128-bit,
+32-bit, and 16-bit.
+
+128-bit UUIDs: Displayed in five groups of 32 hexadecimal digits separated
+by hyphens (e.g., 123e4567-e89b-12d3-a456-426614174000). 128-bit UUIDs allow
+for custom services and characteristics that do not conflict with those of
+other vendors or standard profiles defined by the Bluetooth SIG.
+
+32-bit UUIDs: A subset of the 128-bit UUID, where only 32 bits are used to
+represent the UUID. The UUID values fall within a certain range defined by
+the Bluetooth specification. The 32-bit value is inserted into a base UUID
+(UUUUUUUU-0000-1000-8000-00805F9B34FB) to form a full 128-bit UUID. The
+UUUUUUUU in the base UUID is replaced by the 32-bit value. This format is
+used within the Bluetooth protocol for standardized services and profiles.
+
+16-bit UUIDs: Similar to the 32-bit UUID, but more common. This is also
+used for standardized services, characteristics, and descriptors defined by
+the Bluetooth SIG. The 16-bit value is inserted into a base UUID
+(0000UUUU-0000-1000-8000-00805F9B34FB) to form a full 128-bit UUID.
+
+Services and Characteristics
+----------------------------
+
+Services: Collections of related data that together perform a specific
+function or feature of the device. Each service is identified by a
+UUID and contains Characteristics that define the actual data and behaviors
+of the service.
+
+Characteristics: Pieces of data under a service, representing a specific
+piece of information or a function of the device. Each characteristic can
+have its own unique value, associated properties and access permissions that
+define how it can be interacted with. These properties include read, write,
+notify, indicate, and more. A Characteristic may include one or more
+Descriptors that provide additional metadata or control specific aspects
+of the Characteristic's behavior.
+
+Properties
+----------
+
+Read Property: Allows a connected device to read the value of a characteristic.
+Information meant to be consumed but not altered by the client device, such
+as the device's battery level or sensor readings. Characteristics marked with
+the read property are essentially broadcasting their data to be accessed by
+other devices, ensuring that the information can be consumed without
+being modified.
+
+Write Property: Enables a connected device to change the value of a
+characteristic. An example of when a client device needs to alter data on the
+server device is a Bluetooth controlled light bulb, where the characteristic
+controlling the color or brightness of the light would have the write property,
+allowing external devices to modify these settings.
+
+Read/Write Property: Characteristics can also be configured to support both
+read and write properties simultaneously. This dual capability is useful for
+characteristics that hold information which not only needs to be displayed to
+external devices but also can be updated by them. For example, a Bluetooth
+thermostat may have a characteristic with read/write properties for the target
+temperature, enabling devices to both monitor the current setting and adjust
+it as needed.
+
+Notifications and Indications
+-----------------------------
+
+In addition to read and write properties,
+Bluetooth characteristics can also be configured to notify or indicate to
+connected devices when their value has changed. This is particularly useful
+for dynamic data that changes over time, such as sensor measurements or
+device status updates.
+
+Notifications: When a characteristic's notify property is enabled, it can
+send an update to connected devices whenever its value changes. This allows
+for real-time data streaming without the client having to continuously read
+the characteristic's value. Notifications are unacknowledged, meaning the
+sender does not receive confirmation that the message has been
+received successfully.
+
+Indications: Similar to notifications, indications inform connected devices
+about changes in a characteristic's value. The key difference is that
+indications are acknowledged; the receiver sends back a confirmation to the
+sender upon successfully receiving the update. This ensures reliable delivery
+of important data changes but at the cost of higher power consumption and
+potentially lower throughput due to the additional confirmation step.
+
+Organization of a GATT database The Bluetooth GATT database is a structured
+collection of values that are used to share information between two Bluetooth
+Low Energy (LE) devices. The GATT database is organized as a hierarchical
+structure composed of Services, Characteristics, and Descriptors. The two LE
+devices share the database information through read, writes, and subscription
+to notifications or indications of data values. The two LE devices perform
+this sharing by referencing 16-bit indexes known as "handles" which are
+numerical identifiers for each element in the GATT database. This avoids
+the need to use the UUIDs to reference the elements in the database.
+
+Service: A GATT database service is a collection of related functionalities
+that a device offers. It groups together one or more Characteristics that are
+logically related to perform a specific function or feature of the server
+device. Each service is identified by a UUID and contains Characteristics
+that define the actual data and behaviors of the service.
+
+Characteristic: Each characteristic can have its own unique value, associated
+properties, and access permissions that define how it can be interacted with.
+These properties include read, write, notify, indicate, and more. A
+Characteristic may include one or more Descriptors that provide additional
+metadata or control specific aspects of the Characteristic's behavior.
+
+Descriptors: A descriptor is an optional attribute that describes or controls
+certain aspects of a Characteristic's behavior. They provide additional context
+or metadata about a Characteristic. Some common types of Descriptors include the
+Characteristic User Description (provides human-readable description), the
+Characteristic Presentation Format (defines the format of the Characteristic
+value), and the Client Characteristic Configuration (used to enable or
+disable notifications or indications for a Characteristic).
+
+Handles: Every element in the GATT database (Service, Characteristic,
+Descriptor) is assigned a unique handle. Handles are 16-bit numerical
+identifiers that are used to reference these elements in GATT operations.
+The handles are used to avoid the need to use the UUIDs to reference the
+elements in the database.
+
diff --git a/doc/meson.build b/doc/meson.build
index 722f1407f51b..658980bda3f9 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -238,6 +238,7 @@ if get_option('documentation').enabled()
     'tools.rst',
     'deprecated-tools.rst',
     'dbus.rst',
+    'gatt-intro.rst',
     'dbus-gatt.rst',
     'dbus-obex.rst',
     'qualifications.rst',
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 16+ messages in thread

end of thread, other threads:[~2026-04-24  9:13 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-24  9:11 [PATCH BlueZ v9 00/15] Add meson build system and HTML docs Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 01/15] build: Add meson wrap for libell Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 02/15] build: Add meson build system Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 03/15] build: Separate systemd and libsystemd dependencies Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 04/15] tools: Install gatttool if deprecated tools are enabled Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 05/15] tools: Install avinfo tool by default Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 06/15] emulator: Install the emulator if built Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 07/15] build: Add option to allow disabling bluetoothd Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 08/15] unit: Run test-bap tests concurrently Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 09/15] unit: Make gobex-transfer tests run concurrently Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 10/15] build: Only build profiles if the daemon is built Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 11/15] build: Only build gdbus library if there is a user Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 12/15] unit: Add integration tests Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 13/15] doc: Add HTML documentation Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 14/15] github: Add docs deployment Bastien Nocera
2026-04-24  9:11 ` [PATCH BlueZ v9 15/15] doc: Add introduction to GATT Bastien Nocera

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox